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..2f84e0e56f8 --- /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": "Add named exports to support named imports to `@rushstack/rush-sdk`.", + "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 7f949d4ccb1..b4c663ca928 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 index 77607c81853..365c39e8dad 100644 --- a/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -57,6 +57,7 @@ Loaded @microsoft/rush-lib from process.env._RUSH_LIB_PATH 'VersionPolicyDefinitionName', 'YarnOptionsConfiguration', '_FlagFile', + '_OperationBuildCache', '_OperationMetadataManager', '_OperationStateFile', '_RushGlobalFolder', @@ -77,7 +78,11 @@ 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`] = ` -"Trying to load @microsoft/rush-lib installed by install-run-rush +"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', 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..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,8 +101,12 @@ ${loadAndPrintRushSdkModule} } } ); + + const userRushSdkFolder = path.join(User.getHomeFolder(), '.rush'); expect(result.stderr.trim()).toMatchSnapshot('stderr'); - expect(result.stdout.trim()).toMatchSnapshot('stdout'); + expect( + result.stdout.trim().replace(new RegExp(userRushSdkFolder, 'g'), '') + ).toMatchSnapshot('stdout'); 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;