From 5574c08e96d08519c8673506f01db0081e2a377a Mon Sep 17 00:00:00 2001 From: LPegasus Date: Tue, 6 Jan 2026 20:33:21 +0800 Subject: [PATCH 01/16] feat(rush-sdk): add named export support for CommonJS compatibility This commit enhances @rushstack/rush-sdk to support named imports when the package is consumed via ESM project. --- ...timize-named-exports_2026-01-06-12-36.json | 10 +++ .../rush/browser-approved-packages.json | 4 + .../build-tests-subspace/pnpm-lock.yaml | 12 +-- .../build-tests-subspace/repo-state.json | 4 +- .../config/subspaces/default/pnpm-lock.yaml | 42 +++++---- .../config/subspaces/default/repo-state.json | 2 +- libraries/rush-sdk/config/jest.config.json | 4 +- libraries/rush-sdk/package.json | 1 + libraries/rush-sdk/src/generate-stubs.ts | 22 ++++- .../test/__snapshots__/script.test.ts.snap | 89 +++++++++++++++++++ .../build-assets-with-named-exports.test.ts | 30 +++++++ libraries/rush-sdk/src/test/script.test.ts | 4 +- libraries/rush-sdk/webpack.config.js | 13 ++- 13 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json create mode 100644 libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap create mode 100644 libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts diff --git a/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json b/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json new file mode 100644 index 00000000000..a79521ea98c --- /dev/null +++ b/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Chore: add named exports to support for `@rushstack/rush-sdk` used via ESM named import.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 23c585145b8..f4d208b771e 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -58,6 +58,10 @@ "name": "axios", "allowedCategories": [ "libraries" ] }, + { + "name": "cjs-module-lexer", + "allowedCategories": [ "libraries" ] + }, { "name": "dependency-path", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 322ade61597..da32274ad19 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -839,7 +839,7 @@ packages: '@rushstack/heft-api-extractor-plugin@file:../../../heft-plugins/heft-api-extractor-plugin': resolution: {directory: ../../../heft-plugins/heft-api-extractor-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.7 + '@rushstack/heft': 1.1.8 '@rushstack/heft-config-file@file:../../../libraries/heft-config-file': resolution: {directory: ../../../libraries/heft-config-file, type: directory} @@ -848,7 +848,7 @@ packages: '@rushstack/heft-jest-plugin@file:../../../heft-plugins/heft-jest-plugin': resolution: {directory: ../../../heft-plugins/heft-jest-plugin, type: directory} peerDependencies: - '@rushstack/heft': ^1.1.7 + '@rushstack/heft': ^1.1.8 jest-environment-jsdom: ^29.5.0 jest-environment-node: ^29.5.0 peerDependenciesMeta: @@ -860,17 +860,17 @@ packages: '@rushstack/heft-lint-plugin@file:../../../heft-plugins/heft-lint-plugin': resolution: {directory: ../../../heft-plugins/heft-lint-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.7 + '@rushstack/heft': 1.1.8 '@rushstack/heft-node-rig@file:../../../rigs/heft-node-rig': resolution: {directory: ../../../rigs/heft-node-rig, type: directory} peerDependencies: - '@rushstack/heft': ^1.1.7 + '@rushstack/heft': ^1.1.8 '@rushstack/heft-typescript-plugin@file:../../../heft-plugins/heft-typescript-plugin': resolution: {directory: ../../../heft-plugins/heft-typescript-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.7 + '@rushstack/heft': 1.1.8 '@rushstack/heft@file:../../../apps/heft': resolution: {directory: ../../../apps/heft, type: directory} @@ -7488,7 +7488,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.5.4 + semver: 7.7.3 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 6359c7b9568..7983c138b27 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "32f13ef1f15898a4f614bf9897cc1d74d8fdf2dd", + "pnpmShrinkwrapHash": "402417ff6a3ef549c064c379cb9793ec4b4d64af", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "cb59d652ae8cf04249e1fa557d15d2958128a5e8" + "packageJsonInjectedDependenciesHash": "248fe4df023dec4d802dbb3f8d3f90842fd458ed" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 04b834fe051..ccb19fe043a 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -734,7 +734,7 @@ importers: version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/cli': specifier: ~6.4.18 - version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) + version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) '@storybook/components': specifier: ~6.4.18 version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -770,7 +770,7 @@ importers: version: 5.2.7(webpack@4.47.0) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19) + version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) react: specifier: ~17.0.2 version: 17.0.2 @@ -956,7 +956,7 @@ importers: version: 5.2.7(webpack@5.103.0) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19) + version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) react: specifier: ~19.2.3 version: 19.2.3 @@ -4235,6 +4235,9 @@ importers: '@types/webpack-env': specifier: 1.18.8 version: 1.18.8 + cjs-module-lexer: + specifier: 2.1.0 + version: 2.1.0 eslint: specifier: ~9.37.0 version: 9.37.0 @@ -11292,6 +11295,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.1.0: + resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + class-utils@0.3.6: resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} engines: {node: '>=0.10.0'} @@ -22070,7 +22076,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -22084,7 +22090,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.3) + jest-config: 29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -24586,7 +24592,7 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': + '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': dependencies: '@babel/core': 7.20.12 '@babel/preset-env': 7.28.5(@babel/core@7.20.12) @@ -24606,7 +24612,7 @@ snapshots: fs-extra: 9.1.0 get-port: 5.1.1 globby: 11.1.0 - jest: 29.3.1(@types/node@20.17.19) + jest: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) jscodeshift: 0.13.1(@babel/preset-env@7.28.5(@babel/core@7.20.12)) json5: 2.2.3 leven: 3.1.0 @@ -27840,6 +27846,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.1.0: {} + class-utils@0.3.6: dependencies: arr-union: 3.1.0 @@ -28177,13 +28185,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-jest@29.7.0(@types/node@20.17.19): + create-jest@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.19) + jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -31556,16 +31564,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.19): + jest-cli@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) '@jest/test-result': 29.7.0(@types/node@20.17.19) '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.19) + create-jest: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.19) + jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -31635,7 +31643,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.19): + jest-config@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@20.17.19) @@ -31665,7 +31673,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.9.3): + jest-config@29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@22.9.3) @@ -32053,12 +32061,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.3.1(@types/node@20.17.19): + jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0): dependencies: '@jest/core': 29.5.0(babel-plugin-macros@3.1.0) '@jest/types': 29.5.0 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.19) + jest-cli: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index b36695cb88f..d49085203d5 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "02e03149d5bf0b6a4e8dd2df668ed5350b0506c5", + "pnpmShrinkwrapHash": "f91ada7a0ec37139e2f0cff3fb869d1f514ee628", "preferredVersionsHash": "a9b67c38568259823f9cfb8270b31bf6d8470b27" } diff --git a/libraries/rush-sdk/config/jest.config.json b/libraries/rush-sdk/config/jest.config.json index 62da56b72ce..81cf6b77794 100644 --- a/libraries/rush-sdk/config/jest.config.json +++ b/libraries/rush-sdk/config/jest.config.json @@ -1,9 +1,9 @@ { "extends": "local-node-rig/profiles/default/config/jest.config.json", - "roots": ["/lib-shim"], + "roots": ["/lib-commonjs"], - "testMatch": ["/lib-shim/**/*.test.js"], + "testMatch": ["/lib-commonjs/**/*.test.js"], "collectCoverageFrom": [ "lib-shim/**/*.js", diff --git a/libraries/rush-sdk/package.json b/libraries/rush-sdk/package.json index 262f3037ef0..5c924b535d2 100644 --- a/libraries/rush-sdk/package.json +++ b/libraries/rush-sdk/package.json @@ -56,6 +56,7 @@ "@rushstack/webpack-preserve-dynamic-require-plugin": "workspace:*", "@types/semver": "7.5.0", "@types/webpack-env": "1.18.8", + "cjs-module-lexer": "2.1.0", "eslint": "~9.37.0", "local-node-rig": "workspace:*", "webpack": "~5.103.0" diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index c25cce990cc..3bcfb3fe66a 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -3,7 +3,9 @@ import * as path from 'node:path'; -import { FileSystem, Import, Path } from '@rushstack/node-core-library'; +import { initSync, parse } from 'cjs-module-lexer'; + +import { Encoding, FileSystem, Import, Path } from '@rushstack/node-core-library'; function generateLibFilesRecursively(options: { parentSourcePath: string; @@ -14,6 +16,10 @@ function generateLibFilesRecursively(options: { for (const folderItem of FileSystem.readFolderItems(options.parentSourcePath)) { const sourcePath: string = path.join(options.parentSourcePath, folderItem.name); const targetPath: string = path.join(options.parentTargetPath, folderItem.name); + const commonjsPath: string = path.join( + options.parentSourcePath.replace('/rush-lib/lib', '/rush-lib/lib-commonjs'), + folderItem.name + ); if (folderItem.isDirectory()) { // create destination folder @@ -36,11 +42,17 @@ function generateLibFilesRecursively(options: { const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); const srcImportPathLiteral: string = JSON.stringify(srcImportPath); + const sourceCode: string = FileSystem.readFile(commonjsPath, { encoding: Encoding.Utf8 }); + const exportedNames: string[] = extractNamedExports(sourceCode); + const namedExportsPlaceholder: string = exportedNames.length + ? `${exportedNames.map((name) => `exports.${name}`).join(' = ')} = undefined;\n\n` + : ''; + FileSystem.writeFile( targetPath, // Example: // module.exports = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); - `module.exports = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});` + `${namedExportsPlaceholder}module.exports = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});` ); } } @@ -58,6 +70,7 @@ export async function runAsync(): Promise { const stubsTargetPath: string = path.resolve(__dirname, '../lib'); // eslint-disable-next-line no-console console.log('generate-stubs: Generating stub files under: ' + stubsTargetPath); + initSync(); generateLibFilesRecursively({ parentSourcePath: path.join(rushLibFolder, 'lib'), parentTargetPath: stubsTargetPath, @@ -67,3 +80,8 @@ export async function runAsync(): Promise { // eslint-disable-next-line no-console console.log('generate-stubs: Completed successfully.'); } + +export function extractNamedExports(source: string): string[] { + const { exports, reexports } = parse(source); + return [...exports, ...reexports].filter((d) => d !== '__esModule'); +} diff --git a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap new file mode 100644 index 00000000000..ff59f939add --- /dev/null +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@rushstack/rush-sdk Should load via env when Rush has loaded (for child processes): stderr 1`] = `""`; + +exports[`@rushstack/rush-sdk Should load via env when Rush has loaded (for child processes): stdout 1`] = ` +"Try to load @microsoft/rush-lib from process.env._RUSH_LIB_PATH from caller package +Loaded @microsoft/rush-lib from process.env._RUSH_LIB_PATH +[ + 'ApprovedPackagesConfiguration', + 'ApprovedPackagesItem', + 'ApprovedPackagesPolicy', + 'BuildCacheConfiguration', + 'BumpType', + 'ChangeManager', + 'CobuildConfiguration', + 'CommonVersionsConfiguration', + 'CredentialCache', + 'CustomTipId', + 'CustomTipSeverity', + 'CustomTipType', + 'CustomTipsConfiguration', + 'DependencyType', + 'EnvironmentConfiguration', + 'EnvironmentVariableNames', + 'Event', + 'EventHooks', + 'ExperimentsConfiguration', + 'FileSystemBuildCacheProvider', + 'IndividualVersionPolicy', + 'LockStepVersionPolicy', + 'LookupByPath', + 'NpmOptionsConfiguration', + 'Operation', + 'OperationStatus', + 'PackageJsonDependency', + 'PackageJsonDependencyMeta', + 'PackageJsonEditor', + 'PackageManager', + 'PackageManagerOptionsConfigurationBase', + 'PhasedCommandHooks', + 'PnpmOptionsConfiguration', + 'ProjectChangeAnalyzer', + 'RepoStateFile', + 'Rush', + 'RushCommandLine', + 'RushConfiguration', + 'RushConfigurationProject', + 'RushConstants', + 'RushLifecycleHooks', + 'RushProjectConfiguration', + 'RushSession', + 'RushUserConfiguration', + 'Subspace', + 'SubspacesConfiguration', + 'VersionPolicy', + 'VersionPolicyConfiguration', + 'VersionPolicyDefinitionName', + 'YarnOptionsConfiguration', + '_FlagFile', + '_OperationBuildCache', + '_OperationMetadataManager', + '_OperationStateFile', + '_RushGlobalFolder', + '_RushInternals', + '_rushSdk_loadInternalModule' +]" +`; + +exports[`@rushstack/rush-sdk Should load via global (for plugins): stderr 1`] = `""`; + +exports[`@rushstack/rush-sdk Should load via global (for plugins): stdout 1`] = ` +"[ + '_rushSdk_loadInternalModule', + 'foo' +]" +`; + +exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stderr 1`] = `""`; + +exports[`@rushstack/rush-sdk Should load via process.env._RUSH_LIB_PATH (for child processes): stderr 1`] = `""`; + +exports[`@rushstack/rush-sdk Should load via process.env._RUSH_LIB_PATH (for child processes): stdout 1`] = ` +"Try to load @microsoft/rush-lib from process.env._RUSH_LIB_PATH from caller package +Loaded @microsoft/rush-lib from process.env._RUSH_LIB_PATH +[ + '_rushSdk_loadInternalModule', + 'foo' +]" +`; diff --git a/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts new file mode 100644 index 00000000000..aee44a88f62 --- /dev/null +++ b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Executable } from '@rushstack/node-core-library'; + +describe('@rushstack/rush-sdk named exports check', () => { + it('Should import named exports correctly (lib-shim)', () => { + const result = Executable.spawnSync('node', [ + '-e', + ` +const { RushConfiguration } = await import('@rushstack/rush-sdk'); +console.log(typeof RushConfiguration.loadFromConfigurationFile); +` + ]); + expect(result.stdout.trim()).toEqual('function'); + expect(result.status).toBe(0); + }); + + it('Should import named exports correctly (lib)', () => { + const result = Executable.spawnSync('node', [ + '-e', + ` +const { RushConfiguration } = await import('@rushstack/rush-sdk/lib/api/RushConfiguration'); +console.log(typeof RushConfiguration.loadFromConfigurationFile); +` + ]); + expect(result.stdout.trim()).toEqual('function'); + expect(result.status).toBe(0); + }); +}); diff --git a/libraries/rush-sdk/src/test/script.test.ts b/libraries/rush-sdk/src/test/script.test.ts index d7c8b8d0de1..4d643198996 100644 --- a/libraries/rush-sdk/src/test/script.test.ts +++ b/libraries/rush-sdk/src/test/script.test.ts @@ -102,7 +102,9 @@ ${loadAndPrintRushSdkModule} } ); expect(result.stderr.trim()).toMatchSnapshot('stderr'); - expect(result.stdout.trim()).toMatchSnapshot('stdout'); + expect(result.stdout.trim()).toContain( + 'Trying to load @microsoft/rush-lib installed by install-run-rush' + ); expect(result.status).toBe(0); }); }); diff --git a/libraries/rush-sdk/webpack.config.js b/libraries/rush-sdk/webpack.config.js index b86d582f7a8..8d84313b1cf 100644 --- a/libraries/rush-sdk/webpack.config.js +++ b/libraries/rush-sdk/webpack.config.js @@ -3,12 +3,20 @@ const { PackageJsonLookup } = require('@rushstack/node-core-library'); const { PreserveDynamicRequireWebpackPlugin } = require('@rushstack/webpack-preserve-dynamic-require-plugin'); +const { BannerPlugin } = require('webpack'); module.exports = () => { const packageJson = PackageJsonLookup.loadOwnPackageJson(__dirname); const externalDependencyNames = new Set([...Object.keys(packageJson.dependencies || {})]); + // Get all export specifiers by require rush-lib + const rushLib = require('@microsoft/rush-lib'); + const exportSpecifiers = Object.keys(rushLib); + const bannerCodeForLibShim = exportSpecifiers.length + ? exportSpecifiers.map((name) => `exports.${name}`).join(' = ') + ' = undefined;\n\n' + : ''; + // Explicitly exclude @microsoft/rush-lib externalDependencyNames.delete('@microsoft/rush-lib'); @@ -41,7 +49,10 @@ module.exports = () => { innerGraph: true }, target: 'node', - plugins: [new PreserveDynamicRequireWebpackPlugin()], + plugins: [ + new BannerPlugin({ raw: true, banner: bannerCodeForLibShim }), + new PreserveDynamicRequireWebpackPlugin() + ], externals: [ ({ request }, callback) => { let packageName; From 3d52a11e83081e181049626a12f10d0c2563bdf5 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Wed, 7 Jan 2026 11:23:27 +0800 Subject: [PATCH 02/16] Update common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json Co-authored-by: Ian Clanton-Thuon --- .../rush/chore-optimize-named-exports_2026-01-06-12-36.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json b/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json index a79521ea98c..2f84e0e56f8 100644 --- a/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json +++ b/common/changes/@microsoft/rush/chore-optimize-named-exports_2026-01-06-12-36.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Chore: add named exports to support for `@rushstack/rush-sdk` used via ESM named import.", + "comment": "Add named exports to support named imports to `@rushstack/rush-sdk`.", "type": "none" } ], From 8c8cc143d3f4f055fe6ee1f80d4050aa49adbfc1 Mon Sep 17 00:00:00 2001 From: LPegasus Date: Thu, 8 Jan 2026 21:58:59 +0800 Subject: [PATCH 03/16] [rush-sdk] Revert test snapshot --- .../src/test/__snapshots__/script.test.ts.snap | 13 +++++++++++++ libraries/rush-sdk/src/test/script.test.ts | 10 ++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap index ff59f939add..365c39e8dad 100644 --- a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -77,6 +77,19 @@ exports[`@rushstack/rush-sdk Should load via global (for plugins): stdout 1`] = exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stderr 1`] = `""`; +exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stdout 1`] = ` +"Try to load @microsoft/rush-lib from rush global folder +The expected global rush installed folder is \\"/node-v22.20.0/rush-5.57.0\\" +Failed to load @microsoft/rush-lib from rush global folder: File does not exist: /node-v22.20.0/rush-5.57.0 +ENOENT: no such file or directory, lstat '/node-v22.20.0/rush-5.57.0' +Trying to load @microsoft/rush-lib installed by install-run-rush +Loaded @microsoft/rush-lib installed by install-run-rush +[ + '_rushSdk_loadInternalModule', + 'foo' +]" +`; + exports[`@rushstack/rush-sdk Should load via process.env._RUSH_LIB_PATH (for child processes): stderr 1`] = `""`; exports[`@rushstack/rush-sdk Should load via process.env._RUSH_LIB_PATH (for child processes): stdout 1`] = ` diff --git a/libraries/rush-sdk/src/test/script.test.ts b/libraries/rush-sdk/src/test/script.test.ts index 4d643198996..f06e4628d44 100644 --- a/libraries/rush-sdk/src/test/script.test.ts +++ b/libraries/rush-sdk/src/test/script.test.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import * as path from 'node:path'; -import { Executable } from '@rushstack/node-core-library'; +import { Executable, User } from '@rushstack/node-core-library'; const rushSdkPath: string = path.join(__dirname, '../../lib-shim/index.js'); const sandboxRepoPath: string = `${__dirname}/sandbox`; @@ -101,10 +101,12 @@ ${loadAndPrintRushSdkModule} } } ); + + const userRushSdkFolder = path.join(User.getHomeFolder(), '.rush'); expect(result.stderr.trim()).toMatchSnapshot('stderr'); - expect(result.stdout.trim()).toContain( - 'Trying to load @microsoft/rush-lib installed by install-run-rush' - ); + expect( + result.stdout.trim().replace(new RegExp(userRushSdkFolder, 'g'), '') + ).toMatchSnapshot('stdout'); expect(result.status).toBe(0); }); }); From 7c428354b0f2551c08345d9c2abdf2827fbc6588 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Sat, 17 Jan 2026 01:01:08 +0800 Subject: [PATCH 04/16] feat(webpack-deep-imports-plugin): add named exports code generation logic --- .../src/DeepImportsPlugin.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts index 281035faa6f..8e44c75b9f2 100644 --- a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts +++ b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts @@ -3,13 +3,21 @@ import path from 'node:path'; -import { DllPlugin, type Compiler, WebpackError, type Chunk, type NormalModule } from 'webpack'; +import { + DllPlugin, + type Compiler, + WebpackError, + type Chunk, + type NormalModule, + type ModuleGraph +} from 'webpack'; import { Async, FileSystem, LegacyAdapters, Path } from '@rushstack/node-core-library'; const PLUGIN_NAME: 'DeepImportsPlugin' = 'DeepImportsPlugin'; type DllPluginOptions = DllPlugin['options']; +type IExportsInfo = ReturnType; /** * @public @@ -147,6 +155,7 @@ export class DeepImportsPlugin extends DllPlugin { libPathWithoutExtension: string; moduleId: string | number | null; secondaryChunkId: string | undefined; + exportsInfo: IExportsInfo; } const pathsToIgnoreWithoutExtension: Set = this._pathsToIgnoreWithoutExtensions; @@ -170,7 +179,8 @@ export class DeepImportsPlugin extends DllPlugin { libModules.push({ libPathWithoutExtension: relativePathWithoutExtension, moduleId: compilation.chunkGraph.getModuleId(runtimeChunkModule), - secondaryChunkId + secondaryChunkId, + exportsInfo: compilation.moduleGraph.getExportsInfo(runtimeChunkModule) // Record exportsInfo to generate named exports placeholder code }); encounteredLibPaths.add(relativePathWithoutExtension); @@ -233,7 +243,7 @@ export class DeepImportsPlugin extends DllPlugin { await Async.forEachAsync( libModules, - async ({ libPathWithoutExtension, moduleId, secondaryChunkId }) => { + async ({ libPathWithoutExtension, moduleId, secondaryChunkId, exportsInfo }) => { const depth: number = countSlashes(libPathWithoutExtension); const requirePath: string = '../'.repeat(depth) + libOutFolderRelativeOutputPath; let moduleText: string; @@ -250,6 +260,15 @@ export class DeepImportsPlugin extends DllPlugin { ].join('\n'); } + const providedExports: null | true | string[] = exportsInfo.getProvidedExports(); + if (Array.isArray(providedExports) && providedExports.length > 0) { + moduleText = [ + `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;`, + '', + moduleText + ].join('\n'); + } + compilation.emitAsset( `${outputPathRelativeLibOutFolder}/${libPathWithoutExtension}${JS_EXTENSION}`, new compiler.webpack.sources.RawSource(moduleText) From 0e9d21324292d95249313db4a8663ee3d2858757 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Sat, 17 Jan 2026 02:05:28 +0800 Subject: [PATCH 05/16] refactor(rush-sdk): reuse rush-lib/lib assets exports placehold code --- .../rush/browser-approved-packages.json | 4 -- .../build-tests-subspace/pnpm-lock.yaml | 10 ++--- .../build-tests-subspace/repo-state.json | 4 +- .../config/subspaces/default/pnpm-lock.yaml | 42 ++++++++----------- .../config/subspaces/default/repo-state.json | 2 +- libraries/rush-sdk/package.json | 1 - libraries/rush-sdk/src/generate-stubs.ts | 24 ++++------- libraries/rush-sdk/src/index.ts | 2 +- .../test/__snapshots__/script.test.ts.snap | 9 ++-- libraries/rush-sdk/src/test/script.test.ts | 5 ++- 10 files changed, 41 insertions(+), 62 deletions(-) diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index f4d208b771e..23c585145b8 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -58,10 +58,6 @@ "name": "axios", "allowedCategories": [ "libraries" ] }, - { - "name": "cjs-module-lexer", - "allowedCategories": [ "libraries" ] - }, { "name": "dependency-path", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index da32274ad19..16625ce5afb 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -839,7 +839,7 @@ packages: '@rushstack/heft-api-extractor-plugin@file:../../../heft-plugins/heft-api-extractor-plugin': resolution: {directory: ../../../heft-plugins/heft-api-extractor-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.8 + '@rushstack/heft': 1.1.10 '@rushstack/heft-config-file@file:../../../libraries/heft-config-file': resolution: {directory: ../../../libraries/heft-config-file, type: directory} @@ -848,7 +848,7 @@ packages: '@rushstack/heft-jest-plugin@file:../../../heft-plugins/heft-jest-plugin': resolution: {directory: ../../../heft-plugins/heft-jest-plugin, type: directory} peerDependencies: - '@rushstack/heft': ^1.1.8 + '@rushstack/heft': ^1.1.10 jest-environment-jsdom: ^29.5.0 jest-environment-node: ^29.5.0 peerDependenciesMeta: @@ -860,17 +860,17 @@ packages: '@rushstack/heft-lint-plugin@file:../../../heft-plugins/heft-lint-plugin': resolution: {directory: ../../../heft-plugins/heft-lint-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.8 + '@rushstack/heft': 1.1.10 '@rushstack/heft-node-rig@file:../../../rigs/heft-node-rig': resolution: {directory: ../../../rigs/heft-node-rig, type: directory} peerDependencies: - '@rushstack/heft': ^1.1.8 + '@rushstack/heft': ^1.1.10 '@rushstack/heft-typescript-plugin@file:../../../heft-plugins/heft-typescript-plugin': resolution: {directory: ../../../heft-plugins/heft-typescript-plugin, type: directory} peerDependencies: - '@rushstack/heft': 1.1.8 + '@rushstack/heft': 1.1.10 '@rushstack/heft@file:../../../apps/heft': resolution: {directory: ../../../apps/heft, type: directory} diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 7983c138b27..e66bd37ae9a 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "402417ff6a3ef549c064c379cb9793ec4b4d64af", + "pnpmShrinkwrapHash": "c6d1471b39de5ed4a3333f737afa4411b63435df", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "248fe4df023dec4d802dbb3f8d3f90842fd458ed" + "packageJsonInjectedDependenciesHash": "cb59d652ae8cf04249e1fa557d15d2958128a5e8" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index ccb19fe043a..04b834fe051 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -734,7 +734,7 @@ importers: version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@storybook/cli': specifier: ~6.4.18 - version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) + version: 6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2) '@storybook/components': specifier: ~6.4.18 version: 6.4.22(@types/react@17.0.74)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) @@ -770,7 +770,7 @@ importers: version: 5.2.7(webpack@4.47.0) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + version: 29.3.1(@types/node@20.17.19) react: specifier: ~17.0.2 version: 17.0.2 @@ -956,7 +956,7 @@ importers: version: 5.2.7(webpack@5.103.0) jest: specifier: ~29.3.1 - version: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + version: 29.3.1(@types/node@20.17.19) react: specifier: ~19.2.3 version: 19.2.3 @@ -4235,9 +4235,6 @@ importers: '@types/webpack-env': specifier: 1.18.8 version: 1.18.8 - cjs-module-lexer: - specifier: 2.1.0 - version: 2.1.0 eslint: specifier: ~9.37.0 version: 9.37.0 @@ -11295,9 +11292,6 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - cjs-module-lexer@2.1.0: - resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} - class-utils@0.3.6: resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} engines: {node: '>=0.10.0'} @@ -22076,7 +22070,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -22090,7 +22084,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@22.9.3) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -24592,7 +24586,7 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': + '@storybook/cli@6.4.22(eslint@9.37.0)(jest@29.3.1(@types/node@20.17.19))(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(typescript@5.8.2)': dependencies: '@babel/core': 7.20.12 '@babel/preset-env': 7.28.5(@babel/core@7.20.12) @@ -24612,7 +24606,7 @@ snapshots: fs-extra: 9.1.0 get-port: 5.1.1 globby: 11.1.0 - jest: 29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest: 29.3.1(@types/node@20.17.19) jscodeshift: 0.13.1(@babel/preset-env@7.28.5(@babel/core@7.20.12)) json5: 2.2.3 leven: 3.1.0 @@ -27846,8 +27840,6 @@ snapshots: cjs-module-lexer@1.4.3: {} - cjs-module-lexer@2.1.0: {} - class-utils@0.3.6: dependencies: arr-union: 3.1.0 @@ -28185,13 +28177,13 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-jest@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + create-jest@29.7.0(@types/node@20.17.19): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.17.19) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -31564,16 +31556,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest-cli@29.7.0(@types/node@20.17.19): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0(@types/node@20.17.19) '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + create-jest: 29.7.0(@types/node@20.17.19) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-config: 29.7.0(@types/node@20.17.19) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -31643,7 +31635,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@20.17.19): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@20.17.19) @@ -31673,7 +31665,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.9.3)(babel-plugin-macros@3.1.0): + jest-config@29.7.0(@types/node@22.9.3): dependencies: '@babel/core': 7.20.12 '@jest/test-sequencer': 29.7.0(@types/node@22.9.3) @@ -32061,12 +32053,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.3.1(@types/node@20.17.19)(babel-plugin-macros@3.1.0): + jest@29.3.1(@types/node@20.17.19): dependencies: '@jest/core': 29.5.0(babel-plugin-macros@3.1.0) '@jest/types': 29.5.0 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.19)(babel-plugin-macros@3.1.0) + jest-cli: 29.7.0(@types/node@20.17.19) transitivePeerDependencies: - '@types/node' - babel-plugin-macros diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index d49085203d5..b36695cb88f 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "f91ada7a0ec37139e2f0cff3fb869d1f514ee628", + "pnpmShrinkwrapHash": "02e03149d5bf0b6a4e8dd2df668ed5350b0506c5", "preferredVersionsHash": "a9b67c38568259823f9cfb8270b31bf6d8470b27" } diff --git a/libraries/rush-sdk/package.json b/libraries/rush-sdk/package.json index 5c924b535d2..262f3037ef0 100644 --- a/libraries/rush-sdk/package.json +++ b/libraries/rush-sdk/package.json @@ -56,7 +56,6 @@ "@rushstack/webpack-preserve-dynamic-require-plugin": "workspace:*", "@types/semver": "7.5.0", "@types/webpack-env": "1.18.8", - "cjs-module-lexer": "2.1.0", "eslint": "~9.37.0", "local-node-rig": "workspace:*", "webpack": "~5.103.0" diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index 3bcfb3fe66a..7000b6fe714 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -3,8 +3,6 @@ import * as path from 'node:path'; -import { initSync, parse } from 'cjs-module-lexer'; - import { Encoding, FileSystem, Import, Path } from '@rushstack/node-core-library'; function generateLibFilesRecursively(options: { @@ -16,10 +14,7 @@ function generateLibFilesRecursively(options: { for (const folderItem of FileSystem.readFolderItems(options.parentSourcePath)) { const sourcePath: string = path.join(options.parentSourcePath, folderItem.name); const targetPath: string = path.join(options.parentTargetPath, folderItem.name); - const commonjsPath: string = path.join( - options.parentSourcePath.replace('/rush-lib/lib', '/rush-lib/lib-commonjs'), - folderItem.name - ); + const commonjsPath: string = path.join(options.parentSourcePath, folderItem.name); if (folderItem.isDirectory()) { // create destination folder @@ -42,11 +37,12 @@ function generateLibFilesRecursively(options: { const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); const srcImportPathLiteral: string = JSON.stringify(srcImportPath); - const sourceCode: string = FileSystem.readFile(commonjsPath, { encoding: Encoding.Utf8 }); - const exportedNames: string[] = extractNamedExports(sourceCode); - const namedExportsPlaceholder: string = exportedNames.length - ? `${exportedNames.map((name) => `exports.${name}`).join(' = ')} = undefined;\n\n` - : ''; + // Since the DeepImportsPlugin has already generated the named exports placeholder code, we reuse it here + const rushLibCommonjsCode: string = FileSystem.readFile(commonjsPath, { encoding: Encoding.Utf8 }); + let namedExportsPlaceholder: string = rushLibCommonjsCode.match(/exports\..* = void 0;/)?.[0] || ''; + if (namedExportsPlaceholder) { + namedExportsPlaceholder += '\n\n'; + } FileSystem.writeFile( targetPath, @@ -70,7 +66,6 @@ export async function runAsync(): Promise { const stubsTargetPath: string = path.resolve(__dirname, '../lib'); // eslint-disable-next-line no-console console.log('generate-stubs: Generating stub files under: ' + stubsTargetPath); - initSync(); generateLibFilesRecursively({ parentSourcePath: path.join(rushLibFolder, 'lib'), parentTargetPath: stubsTargetPath, @@ -80,8 +75,3 @@ export async function runAsync(): Promise { // eslint-disable-next-line no-console console.log('generate-stubs: Completed successfully.'); } - -export function extractNamedExports(source: string): string[] { - const { exports, reexports } = parse(source); - return [...exports, ...reexports].filter((d) => d !== '__esModule'); -} diff --git a/libraries/rush-sdk/src/index.ts b/libraries/rush-sdk/src/index.ts index d42a1398b91..beaa80bb959 100644 --- a/libraries/rush-sdk/src/index.ts +++ b/libraries/rush-sdk/src/index.ts @@ -138,7 +138,7 @@ if (sdkContext.rushLibModule === undefined) { terminal.writeVerboseLine(`Try to load ${RUSH_LIB_NAME} from rush global folder`); const rushGlobalFolder: RushGlobalFolder = new RushGlobalFolder(); // The path needs to keep align with the logic inside RushVersionSelector - const expectedGlobalRushInstalledFolder: string = `${rushGlobalFolder.nodeSpecificPath}/rush-${rushVersion}`; + const expectedGlobalRushInstalledFolder: string = `${rushGlobalFolder.nodeSpecificPath}${path.sep}rush-${rushVersion}`; terminal.writeVerboseLine( `The expected global rush installed folder is "${expectedGlobalRushInstalledFolder}"` ); diff --git a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap index 365c39e8dad..b497003e629 100644 --- a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -79,15 +79,16 @@ exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): exports[`@rushstack/rush-sdk Should load via install-run (for standalone tools): stdout 1`] = ` "Try to load @microsoft/rush-lib from rush global folder -The expected global rush installed folder is \\"/node-v22.20.0/rush-5.57.0\\" -Failed to load @microsoft/rush-lib from rush global folder: File does not exist: /node-v22.20.0/rush-5.57.0 -ENOENT: no such file or directory, lstat '/node-v22.20.0/rush-5.57.0' +The expected global rush installed folder is \\"\\" +Failed to load @microsoft/rush-lib from rush global folder: File does not exist: +ENOENT: no such file or directory, lstat '' Trying to load @microsoft/rush-lib installed by install-run-rush Loaded @microsoft/rush-lib installed by install-run-rush [ '_rushSdk_loadInternalModule', 'foo' -]" +] +" `; exports[`@rushstack/rush-sdk Should load via process.env._RUSH_LIB_PATH (for child processes): stderr 1`] = `""`; diff --git a/libraries/rush-sdk/src/test/script.test.ts b/libraries/rush-sdk/src/test/script.test.ts index f06e4628d44..fc45e9db213 100644 --- a/libraries/rush-sdk/src/test/script.test.ts +++ b/libraries/rush-sdk/src/test/script.test.ts @@ -102,10 +102,11 @@ ${loadAndPrintRushSdkModule} } ); - const userRushSdkFolder = path.join(User.getHomeFolder(), '.rush'); + const nodeVersion = process.version; + const userRushSdkFolder = path.join(User.getHomeFolder(), '.rush', `node-${nodeVersion}`, 'rush-5.57.0'); expect(result.stderr.trim()).toMatchSnapshot('stderr'); expect( - result.stdout.trim().replace(new RegExp(userRushSdkFolder, 'g'), '') + result.stdout.replace(new RegExp(userRushSdkFolder.replace(/\\/g, '\\\\'), 'g'), '') ).toMatchSnapshot('stdout'); expect(result.status).toBe(0); }); From e160ebe279cb81495b7b2636c8600cb6f6c446b6 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Sat, 17 Jan 2026 16:17:35 +0800 Subject: [PATCH 06/16] fix(rush-sdk): fix unit test in CI with node <= 20.18 --- .../src/test/build-assets-with-named-exports.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts index aee44a88f62..44640bcd912 100644 --- a/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts +++ b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts @@ -7,9 +7,11 @@ describe('@rushstack/rush-sdk named exports check', () => { it('Should import named exports correctly (lib-shim)', () => { const result = Executable.spawnSync('node', [ '-e', + // Do not use top level await here because it is not supported in Node.js < 20.20 ` -const { RushConfiguration } = await import('@rushstack/rush-sdk'); +import('@rushstack/rush-sdk').then(({ RushConfiguration }) => { console.log(typeof RushConfiguration.loadFromConfigurationFile); + }); ` ]); expect(result.stdout.trim()).toEqual('function'); @@ -20,8 +22,9 @@ console.log(typeof RushConfiguration.loadFromConfigurationFile); const result = Executable.spawnSync('node', [ '-e', ` -const { RushConfiguration } = await import('@rushstack/rush-sdk/lib/api/RushConfiguration'); +import('@rushstack/rush-sdk/lib/api/RushConfiguration').then(({ RushConfiguration }) => { console.log(typeof RushConfiguration.loadFromConfigurationFile); + }); ` ]); expect(result.stdout.trim()).toEqual('function'); From b28607023f325645f319460373e0f4d3ef31be0c Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Wed, 4 Feb 2026 17:56:46 +0800 Subject: [PATCH 07/16] Update webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts Co-authored-by: David Michon --- .../webpack-deep-imports-plugin/src/DeepImportsPlugin.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts index 8e44c75b9f2..d00996d140c 100644 --- a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts +++ b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts @@ -262,11 +262,7 @@ export class DeepImportsPlugin extends DllPlugin { const providedExports: null | true | string[] = exportsInfo.getProvidedExports(); if (Array.isArray(providedExports) && providedExports.length > 0) { - moduleText = [ - `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;`, - '', - moduleText - ].join('\n'); + moduleText = `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;\n\n` + moduleText; } compilation.emitAsset( From 7105e457482c2ec5d1f4ea0e123a4e879cbf8936 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Wed, 4 Feb 2026 17:57:10 +0800 Subject: [PATCH 08/16] Update libraries/rush-sdk/webpack.config.js Co-authored-by: David Michon --- libraries/rush-sdk/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-sdk/webpack.config.js b/libraries/rush-sdk/webpack.config.js index 8d84313b1cf..b4c604f739f 100644 --- a/libraries/rush-sdk/webpack.config.js +++ b/libraries/rush-sdk/webpack.config.js @@ -8,7 +8,7 @@ const { BannerPlugin } = require('webpack'); module.exports = () => { const packageJson = PackageJsonLookup.loadOwnPackageJson(__dirname); - const externalDependencyNames = new Set([...Object.keys(packageJson.dependencies || {})]); + const externalDependencyNames = new Set(Object.keys(packageJson.dependencies || {})); // Get all export specifiers by require rush-lib const rushLib = require('@microsoft/rush-lib'); From cce271ad13d386d2813adeeb8a8f7c8eab2979e3 Mon Sep 17 00:00:00 2001 From: Pegasusknight Date: Wed, 4 Feb 2026 17:57:29 +0800 Subject: [PATCH 09/16] Update libraries/rush-sdk/src/generate-stubs.ts Co-authored-by: David Michon --- libraries/rush-sdk/src/generate-stubs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index 7000b6fe714..8d43531912c 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -38,7 +38,7 @@ function generateLibFilesRecursively(options: { const srcImportPathLiteral: string = JSON.stringify(srcImportPath); // Since the DeepImportsPlugin has already generated the named exports placeholder code, we reuse it here - const rushLibCommonjsCode: string = FileSystem.readFile(commonjsPath, { encoding: Encoding.Utf8 }); + const rushLibCommonjsCode: string = FileSystem.readFile(commonjsPath); let namedExportsPlaceholder: string = rushLibCommonjsCode.match(/exports\..* = void 0;/)?.[0] || ''; if (namedExportsPlaceholder) { namedExportsPlaceholder += '\n\n'; From f9eb19d8c7e2e65527f42b2dc5ca48b4dc94e6ce Mon Sep 17 00:00:00 2001 From: LPegasus Date: Wed, 4 Feb 2026 19:14:09 +0800 Subject: [PATCH 10/16] refactor(webpack-deep-imports-plugin): Use compilation.webpack.WebpackError --- libraries/rush-sdk/src/generate-stubs.ts | 2 +- libraries/rush-sdk/src/test/script.test.ts | 8 +++++++- .../src/DeepImportsPlugin.ts | 15 ++++++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index 8d43531912c..aec7cde7b91 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; -import { Encoding, FileSystem, Import, Path } from '@rushstack/node-core-library'; +import { FileSystem, Import, Path } from '@rushstack/node-core-library'; function generateLibFilesRecursively(options: { parentSourcePath: string; diff --git a/libraries/rush-sdk/src/test/script.test.ts b/libraries/rush-sdk/src/test/script.test.ts index fc45e9db213..cdc34846f2a 100644 --- a/libraries/rush-sdk/src/test/script.test.ts +++ b/libraries/rush-sdk/src/test/script.test.ts @@ -7,6 +7,7 @@ import { Executable, User } from '@rushstack/node-core-library'; const rushSdkPath: string = path.join(__dirname, '../../lib-shim/index.js'); const sandboxRepoPath: string = `${__dirname}/sandbox`; const mockPackageFolder: string = `${sandboxRepoPath}/mock-package`; +const mockRushJsonPath: string = `${sandboxRepoPath}/rush.json`; const mockRushLibPath: string = `${__dirname}/fixture/mock-rush-lib.js`; const coreLibPath: string = require.resolve('@rushstack/node-core-library'); @@ -103,7 +104,12 @@ ${loadAndPrintRushSdkModule} ); const nodeVersion = process.version; - const userRushSdkFolder = path.join(User.getHomeFolder(), '.rush', `node-${nodeVersion}`, 'rush-5.57.0'); + const userRushSdkFolder = path.join( + User.getHomeFolder(), + '.rush', + `node-${nodeVersion}`, + 'rush-' + require(mockRushJsonPath).rushVersion + ); expect(result.stderr.trim()).toMatchSnapshot('stderr'); expect( result.stdout.replace(new RegExp(userRushSdkFolder.replace(/\\/g, '\\\\'), 'g'), '') diff --git a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts index d00996d140c..dcc757ce03b 100644 --- a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts +++ b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts @@ -3,14 +3,7 @@ import path from 'node:path'; -import { - DllPlugin, - type Compiler, - WebpackError, - type Chunk, - type NormalModule, - type ModuleGraph -} from 'webpack'; +import { DllPlugin, type Compiler, type Chunk, type NormalModule, type ModuleGraph } from 'webpack'; import { Async, FileSystem, LegacyAdapters, Path } from '@rushstack/node-core-library'; @@ -130,6 +123,8 @@ export class DeepImportsPlugin extends DllPlugin { public apply(compiler: Compiler): void { super.apply(compiler); + const { WebpackError } = compiler.webpack; + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { compilation.hooks.processAssets.tapPromise(PLUGIN_NAME, async () => { const runtimeChunks: Chunk[] = []; @@ -262,7 +257,9 @@ export class DeepImportsPlugin extends DllPlugin { const providedExports: null | true | string[] = exportsInfo.getProvidedExports(); if (Array.isArray(providedExports) && providedExports.length > 0) { - moduleText = `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;\n\n` + moduleText; + moduleText = + `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;\n\n` + + moduleText; } compilation.emitAsset( From 475c61a2592bad3969566e0fa2d9492738ce3857 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:12:27 -0800 Subject: [PATCH 11/16] feat: Generate sidecar .exports.json files instead of injecting exports prefix - DeepImportsPlugin now emits a separate .exports.json file containing { moduleExports: [...] } instead of injecting 'exports.X = void 0' prefix into JS files - Updated rush-sdk generate-stubs.ts to read exports from the sidecar JSON files - Updated rush-sdk webpack.config.js to read exports from the sidecar JSON files --- libraries/rush-sdk/src/generate-stubs.ts | 28 ++++++++++++++----- libraries/rush-sdk/webpack.config.js | 22 +++++++++------ .../src/DeepImportsPlugin.ts | 16 ++++++----- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index aec7cde7b91..33f9d08d886 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; -import { FileSystem, Import, Path } from '@rushstack/node-core-library'; +import { FileSystem, Import, JsonFile, Path } from '@rushstack/node-core-library'; function generateLibFilesRecursively(options: { parentSourcePath: string; @@ -14,7 +14,6 @@ function generateLibFilesRecursively(options: { for (const folderItem of FileSystem.readFolderItems(options.parentSourcePath)) { const sourcePath: string = path.join(options.parentSourcePath, folderItem.name); const targetPath: string = path.join(options.parentTargetPath, folderItem.name); - const commonjsPath: string = path.join(options.parentSourcePath, folderItem.name); if (folderItem.isDirectory()) { // create destination folder @@ -37,17 +36,32 @@ function generateLibFilesRecursively(options: { const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); const srcImportPathLiteral: string = JSON.stringify(srcImportPath); - // Since the DeepImportsPlugin has already generated the named exports placeholder code, we reuse it here - const rushLibCommonjsCode: string = FileSystem.readFile(commonjsPath); - let namedExportsPlaceholder: string = rushLibCommonjsCode.match(/exports\..* = void 0;/)?.[0] || ''; - if (namedExportsPlaceholder) { - namedExportsPlaceholder += '\n\n'; + let namedExportsPlaceholder: string = ''; + try { + // Read the sidecar .exports.json file generated by DeepImportsPlugin to get module exports + const exportsJsonPath: string = sourcePath.slice(0, -'.js'.length) + '.exports.json'; + const { moduleExports }: { moduleExports: string[] } = JsonFile.load(exportsJsonPath); + if (moduleExports.length > 0) { + namedExportsPlaceholder = + '// Hinting of exported names to allow importing from ES modules\n' + + moduleExports.map((exportName) => `exports.${exportName}`).join(' = ') + + ' = undefined;\n\n'; + } + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; + } } FileSystem.writeFile( targetPath, // Example: + // ``` + // // Hinting of exported names to allow importing from ES modules + // exports.GitEmailPolicy = undefined; + // // module.exports = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); + // ``` `${namedExportsPlaceholder}module.exports = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});` ); } diff --git a/libraries/rush-sdk/webpack.config.js b/libraries/rush-sdk/webpack.config.js index b4c604f739f..45478f860e2 100644 --- a/libraries/rush-sdk/webpack.config.js +++ b/libraries/rush-sdk/webpack.config.js @@ -1,21 +1,25 @@ /* eslint-env es6 */ 'use strict'; -const { PackageJsonLookup } = require('@rushstack/node-core-library'); +const { PackageJsonLookup, Import } = require('@rushstack/node-core-library'); const { PreserveDynamicRequireWebpackPlugin } = require('@rushstack/webpack-preserve-dynamic-require-plugin'); -const { BannerPlugin } = require('webpack'); -module.exports = () => { +module.exports = ({ webpack: { BannerPlugin } }) => { const packageJson = PackageJsonLookup.loadOwnPackageJson(__dirname); const externalDependencyNames = new Set(Object.keys(packageJson.dependencies || {})); - // Get all export specifiers by require rush-lib - const rushLib = require('@microsoft/rush-lib'); - const exportSpecifiers = Object.keys(rushLib); - const bannerCodeForLibShim = exportSpecifiers.length - ? exportSpecifiers.map((name) => `exports.${name}`).join(' = ') + ' = undefined;\n\n' - : ''; + // Get all export specifiers from the sidecar .exports.json file generated by DeepImportsPlugin + const rushLibFolder = Import.resolvePackage({ + baseFolderPath: __dirname, + packageName: '@microsoft/rush-lib', + useNodeJSResolver: true + }); + const { moduleExports: exportSpecifiers } = require(`${rushLibFolder}/lib/index.exports.json`); + let bannerCodeForLibShim = + '// Hinting of exported names to allow importing from ES modules\n' + + exportSpecifiers.map((name) => `exports.${name}`).join(' = ') + + ' = undefined;\n\n'; // Explicitly exclude @microsoft/rush-lib externalDependencyNames.delete('@microsoft/rush-lib'); diff --git a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts index dcc757ce03b..ff5b2327d8a 100644 --- a/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts +++ b/webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts @@ -255,18 +255,20 @@ export class DeepImportsPlugin extends DllPlugin { ].join('\n'); } - const providedExports: null | true | string[] = exportsInfo.getProvidedExports(); - if (Array.isArray(providedExports) && providedExports.length > 0) { - moduleText = - `${providedExports.map((exportName) => `exports.${exportName}`).join(' = ')} = void 0;\n\n` + - moduleText; - } - compilation.emitAsset( `${outputPathRelativeLibOutFolder}/${libPathWithoutExtension}${JS_EXTENSION}`, new compiler.webpack.sources.RawSource(moduleText) ); + const providedExports: null | true | string[] = exportsInfo.getProvidedExports(); + if (Array.isArray(providedExports) && providedExports.length > 0) { + const exportsJson: string = JSON.stringify({ moduleExports: providedExports }, undefined, 2); + compilation.emitAsset( + `${outputPathRelativeLibOutFolder}/${libPathWithoutExtension}.exports.json`, + new compiler.webpack.sources.RawSource(exportsJson) + ); + } + if (resolvedDtsFilesInputFolderName) { const dtsFilePath: string = path.join( resolvedDtsFilesInputFolderName, From 255fd67c6669c15f2fb07052a2e958ee0e1d35da Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:25:26 -0800 Subject: [PATCH 12/16] fix(rush-sdk): Fix ESM named exports for deep imports Use intermediate variable and explicit exports assignment to ensure Node.js CJS lexer properly detects named exports for ESM interop. Remove unnecessary namedExportsPlaceholder since the explicit exports.X = _m.X assignments are sufficient. --- libraries/rush-sdk/src/generate-stubs.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index 33f9d08d886..4887d1ef376 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -36,16 +36,16 @@ function generateLibFilesRecursively(options: { const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); const srcImportPathLiteral: string = JSON.stringify(srcImportPath); - let namedExportsPlaceholder: string = ''; + let namedExportsAssignment: string = ''; try { // Read the sidecar .exports.json file generated by DeepImportsPlugin to get module exports const exportsJsonPath: string = sourcePath.slice(0, -'.js'.length) + '.exports.json'; const { moduleExports }: { moduleExports: string[] } = JsonFile.load(exportsJsonPath); if (moduleExports.length > 0) { - namedExportsPlaceholder = - '// Hinting of exported names to allow importing from ES modules\n' + - moduleExports.map((exportName) => `exports.${exportName}`).join(' = ') + - ' = undefined;\n\n'; + // Assign named exports after module.exports to ensure they're properly exposed for ESM imports + namedExportsAssignment = + '\n' + + moduleExports.map((exportName) => `exports.${exportName} = _m.${exportName};`).join('\n'); } } catch (e) { if (!FileSystem.isNotExistError(e)) { @@ -57,12 +57,11 @@ function generateLibFilesRecursively(options: { targetPath, // Example: // ``` - // // Hinting of exported names to allow importing from ES modules - // exports.GitEmailPolicy = undefined; - // - // module.exports = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); + // const _m = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); + // module.exports = _m; + // exports.GitEmailPolicy = _m.GitEmailPolicy; // ``` - `${namedExportsPlaceholder}module.exports = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});` + `const _m = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});\nmodule.exports = _m;${namedExportsAssignment}` ); } } From 96c84e910bedc932680e4e0ee152ffd29067c64a Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:30:12 -0800 Subject: [PATCH 13/16] refactor(rush-sdk): Use footer instead of banner for ESM exports hints Replace the exports placeholder banner at the top of the webpack bundle with a footer that explicitly assigns exports.X = module.exports.X. Only apply to index.js bundle, not loader.js. --- libraries/rush-sdk/webpack.config.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/libraries/rush-sdk/webpack.config.js b/libraries/rush-sdk/webpack.config.js index 45478f860e2..df7ae5c3929 100644 --- a/libraries/rush-sdk/webpack.config.js +++ b/libraries/rush-sdk/webpack.config.js @@ -3,6 +3,7 @@ const { PackageJsonLookup, Import } = require('@rushstack/node-core-library'); const { PreserveDynamicRequireWebpackPlugin } = require('@rushstack/webpack-preserve-dynamic-require-plugin'); +const {} = require('webpack'); module.exports = ({ webpack: { BannerPlugin } }) => { const packageJson = PackageJsonLookup.loadOwnPackageJson(__dirname); @@ -16,10 +17,10 @@ module.exports = ({ webpack: { BannerPlugin } }) => { useNodeJSResolver: true }); const { moduleExports: exportSpecifiers } = require(`${rushLibFolder}/lib/index.exports.json`); - let bannerCodeForLibShim = - '// Hinting of exported names to allow importing from ES modules\n' + - exportSpecifiers.map((name) => `exports.${name}`).join(' = ') + - ' = undefined;\n\n'; + // Assign named exports after the bundle to ensure they're properly exposed for ESM imports + const footerCodeForLibShim = exportSpecifiers + .map((name) => `exports.${name} = module.exports.${name};`) + .join('\n'); // Explicitly exclude @microsoft/rush-lib externalDependencyNames.delete('@microsoft/rush-lib'); @@ -54,7 +55,12 @@ module.exports = ({ webpack: { BannerPlugin } }) => { }, target: 'node', plugins: [ - new BannerPlugin({ raw: true, banner: bannerCodeForLibShim }), + new BannerPlugin({ + raw: true, + footer: true, + include: /index\.js$/, + banner: footerCodeForLibShim + }), new PreserveDynamicRequireWebpackPlugin() ], externals: [ From 8d3f74eb6e8870481565f8f3f170dc204f4e2476 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:33:49 -0800 Subject: [PATCH 14/16] chore(rush-lib): Exclude .exports.json sidecar files from npm package --- libraries/rush-lib/.npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/rush-lib/.npmignore b/libraries/rush-lib/.npmignore index 2c1b4d582e5..9194692aaff 100644 --- a/libraries/rush-lib/.npmignore +++ b/libraries/rush-lib/.npmignore @@ -20,6 +20,7 @@ /lib/**/test/ /lib-*/**/test/ *.test.js +*.exports.json # NOTE: These don't need to be specified, because NPM includes them automatically. # From 54389637e5dc7ff057c1dc8f35fa57f67af42d20 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:48:49 -0800 Subject: [PATCH 15/16] refactor(rush-sdk): Use async filesystem operations in generate-stubs Refactor generate-stubs.ts to use async filesystem operations and process file tasks in parallel with controlled concurrency using Async.forEachAsync with an async generator. --- libraries/rush-sdk/src/generate-stubs.ts | 163 +++++++++++++++-------- 1 file changed, 104 insertions(+), 59 deletions(-) diff --git a/libraries/rush-sdk/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index 4887d1ef376..6baeaa2943f 100644 --- a/libraries/rush-sdk/src/generate-stubs.ts +++ b/libraries/rush-sdk/src/generate-stubs.ts @@ -3,88 +3,133 @@ import * as path from 'node:path'; -import { FileSystem, Import, JsonFile, Path } from '@rushstack/node-core-library'; +import type { IRunScriptOptions } from '@rushstack/heft'; +import { Async, FileSystem, type FolderItem, Import, JsonFile, Path } from '@rushstack/node-core-library'; -function generateLibFilesRecursively(options: { +interface IGenerateOptions { parentSourcePath: string; parentTargetPath: string; parentSrcImportPathWithSlash: string; libShimIndexPath: string; -}): void { - for (const folderItem of FileSystem.readFolderItems(options.parentSourcePath)) { - const sourcePath: string = path.join(options.parentSourcePath, folderItem.name); - const targetPath: string = path.join(options.parentTargetPath, folderItem.name); +} + +interface IFileTask { + type: 'dts' | 'js'; + sourcePath: string; + targetPath: string; + srcImportPath?: string; + shimPathLiteral?: string; +} + +async function* collectFileTasksAsync(options: IGenerateOptions): AsyncGenerator { + const { parentSourcePath, parentTargetPath, parentSrcImportPathWithSlash, libShimIndexPath } = options; + const folderItems: FolderItem[] = await FileSystem.readFolderItemsAsync(options.parentSourcePath); + + for (const folderItem of folderItems) { + const itemName: string = folderItem.name; + const sourcePath: string = `${parentSourcePath}/${itemName}`; + const targetPath: string = `${parentTargetPath}/${itemName}`; if (folderItem.isDirectory()) { - // create destination folder - FileSystem.ensureEmptyFolder(targetPath); - generateLibFilesRecursively({ + // Ensure destination folder exists + await FileSystem.ensureFolderAsync(targetPath); + // Recursively yield tasks from subdirectory + yield* collectFileTasksAsync({ parentSourcePath: sourcePath, parentTargetPath: targetPath, - parentSrcImportPathWithSlash: options.parentSrcImportPathWithSlash + folderItem.name + '/', - libShimIndexPath: options.libShimIndexPath + parentSrcImportPathWithSlash: parentSrcImportPathWithSlash + itemName + '/', + libShimIndexPath }); - } else { - if (folderItem.name.endsWith('.d.ts')) { - FileSystem.copyFile({ - sourcePath: sourcePath, - destinationPath: targetPath - }); - } else if (folderItem.name.endsWith('.js')) { - const srcImportPath: string = options.parentSrcImportPathWithSlash + path.parse(folderItem.name).name; - const shimPath: string = path.relative(options.parentTargetPath, options.libShimIndexPath); - const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); - const srcImportPathLiteral: string = JSON.stringify(srcImportPath); - - let namedExportsAssignment: string = ''; - try { - // Read the sidecar .exports.json file generated by DeepImportsPlugin to get module exports - const exportsJsonPath: string = sourcePath.slice(0, -'.js'.length) + '.exports.json'; - const { moduleExports }: { moduleExports: string[] } = JsonFile.load(exportsJsonPath); - if (moduleExports.length > 0) { - // Assign named exports after module.exports to ensure they're properly exposed for ESM imports - namedExportsAssignment = - '\n' + - moduleExports.map((exportName) => `exports.${exportName} = _m.${exportName};`).join('\n'); - } - } catch (e) { - if (!FileSystem.isNotExistError(e)) { - throw e; - } - } - - FileSystem.writeFile( - targetPath, - // Example: - // ``` - // const _m = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); - // module.exports = _m; - // exports.GitEmailPolicy = _m.GitEmailPolicy; - // ``` - `const _m = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});\nmodule.exports = _m;${namedExportsAssignment}` - ); + } else if (folderItem.name.endsWith('.d.ts')) { + yield { + type: 'dts', + sourcePath, + targetPath + }; + } else if (folderItem.name.endsWith('.js')) { + const srcImportPath: string = parentSrcImportPathWithSlash + path.parse(folderItem.name).name; + const shimPath: string = path.relative(parentTargetPath, libShimIndexPath); + const shimPathLiteral: string = JSON.stringify(Path.convertToSlashes(shimPath)); + + yield { + type: 'js', + sourcePath, + targetPath, + srcImportPath, + shimPathLiteral + }; + } + } +} + +async function processFileTaskAsync(task: IFileTask): Promise { + const { type, sourcePath, targetPath, srcImportPath, shimPathLiteral } = task; + if (type === 'dts') { + await FileSystem.copyFileAsync({ + sourcePath, + destinationPath: targetPath + }); + } else { + const srcImportPathLiteral: string = JSON.stringify(srcImportPath); + + let namedExportsAssignment: string = ''; + try { + // Read the sidecar .exports.json file generated by DeepImportsPlugin to get module exports + const exportsJsonPath: string = sourcePath.slice(0, -'.js'.length) + '.exports.json'; + const { moduleExports }: { moduleExports: string[] } = await JsonFile.loadAsync(exportsJsonPath); + if (moduleExports.length > 0) { + // Assign named exports after module.exports to ensure they're properly exposed for ESM imports + namedExportsAssignment = + '\n' + moduleExports.map((exportName) => `exports.${exportName} = _m.${exportName};`).join('\n'); + } + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; } } + + await FileSystem.writeFileAsync( + targetPath, + // Example: + // ``` + // const _m = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy"); + // module.exports = _m; + // exports.GitEmailPolicy = _m.GitEmailPolicy; + // ``` + `const _m = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});\nmodule.exports = _m;${namedExportsAssignment}\n` + ); } } // Entry point invoked by "runScript" action from config/heft.json -export async function runAsync(): Promise { +export async function runAsync(options: IRunScriptOptions): Promise { + const { + heftConfiguration: { buildFolderPath }, + heftTaskSession: { + logger: { terminal } + } + } = options; + const rushLibFolder: string = Import.resolvePackage({ baseFolderPath: __dirname, packageName: '@microsoft/rush-lib', useNodeJSResolver: true }); - const stubsTargetPath: string = path.resolve(__dirname, '../lib'); - // eslint-disable-next-line no-console - console.log('generate-stubs: Generating stub files under: ' + stubsTargetPath); - generateLibFilesRecursively({ - parentSourcePath: path.join(rushLibFolder, 'lib'), + const stubsTargetPath: string = `${buildFolderPath}/lib`; + terminal.writeLine('generate-stubs: Generating stub files under: ' + stubsTargetPath); + + // Ensure the target folder exists + await FileSystem.ensureFolderAsync(stubsTargetPath); + + // Collect and process file tasks in parallel with controlled concurrency + const tasks: AsyncGenerator = collectFileTasksAsync({ + parentSourcePath: `${rushLibFolder}/lib`, parentTargetPath: stubsTargetPath, parentSrcImportPathWithSlash: '', - libShimIndexPath: path.join(__dirname, '../lib-shim/index') + libShimIndexPath: `${buildFolderPath}/lib-shim/index.js` }); - // eslint-disable-next-line no-console - console.log('generate-stubs: Completed successfully.'); + await Async.forEachAsync(tasks, processFileTaskAsync, { concurrency: 50 }); + + terminal.writeLine('generate-stubs: Completed successfully.'); } From ed0b358d90520b8093043e0f357c0fcde75e1691 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 4 Feb 2026 20:52:11 -0800 Subject: [PATCH 16/16] refactor(tests): Convert synchronous named exports tests to async --- .../build-assets-with-named-exports.test.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts index 44640bcd912..4edcb80d19e 100644 --- a/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts +++ b/libraries/rush-sdk/src/test/build-assets-with-named-exports.test.ts @@ -4,8 +4,8 @@ import { Executable } from '@rushstack/node-core-library'; describe('@rushstack/rush-sdk named exports check', () => { - it('Should import named exports correctly (lib-shim)', () => { - const result = Executable.spawnSync('node', [ + it('Should import named exports correctly (lib-shim)', async () => { + const childProcess = Executable.spawn(process.argv0, [ '-e', // Do not use top level await here because it is not supported in Node.js < 20.20 ` @@ -14,20 +14,30 @@ console.log(typeof RushConfiguration.loadFromConfigurationFile); }); ` ]); - expect(result.stdout.trim()).toEqual('function'); - expect(result.status).toBe(0); + const { stdout, exitCode, signal } = await Executable.waitForExitAsync(childProcess, { + encoding: 'utf8' + }); + + expect(stdout.trim()).toEqual('function'); + expect(exitCode).toBe(0); + expect(signal).toBeNull(); }); - it('Should import named exports correctly (lib)', () => { - const result = Executable.spawnSync('node', [ + it('Should import named exports correctly (lib)', async () => { + const childProcess = Executable.spawn(process.argv0, [ '-e', ` -import('@rushstack/rush-sdk/lib/api/RushConfiguration').then(({ RushConfiguration }) => { -console.log(typeof RushConfiguration.loadFromConfigurationFile); +import('@rushstack/rush-sdk/lib/utilities/NullTerminalProvider').then(({ NullTerminalProvider }) => { +console.log(NullTerminalProvider.name); }); ` ]); - expect(result.stdout.trim()).toEqual('function'); - expect(result.status).toBe(0); + const { stdout, exitCode, signal } = await Executable.waitForExitAsync(childProcess, { + encoding: 'utf8' + }); + + expect(stdout.trim()).toEqual('NullTerminalProvider'); + expect(exitCode).toBe(0); + expect(signal).toBeNull(); }); });