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/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 322ade61597..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.7 + '@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.7 + '@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.7 + '@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.7 + '@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.7 + '@rushstack/heft': 1.1.10 '@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..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": "32f13ef1f15898a4f614bf9897cc1d74d8fdf2dd", + "pnpmShrinkwrapHash": "c6d1471b39de5ed4a3333f737afa4411b63435df", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", "packageJsonInjectedDependenciesHash": "cb59d652ae8cf04249e1fa557d15d2958128a5e8" } 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/src/generate-stubs.ts b/libraries/rush-sdk/src/generate-stubs.ts index c25cce990cc..7000b6fe714 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 { Encoding, FileSystem, Import, Path } from '@rushstack/node-core-library'; function generateLibFilesRecursively(options: { parentSourcePath: string; @@ -14,6 +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, folderItem.name); if (folderItem.isDirectory()) { // create destination folder @@ -36,11 +37,18 @@ 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, { encoding: Encoding.Utf8 }); + let namedExportsPlaceholder: string = rushLibCommonjsCode.match(/exports\..* = void 0;/)?.[0] || ''; + if (namedExportsPlaceholder) { + namedExportsPlaceholder += '\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});` ); } } 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 new file mode 100644 index 00000000000..b497003e629 --- /dev/null +++ b/libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap @@ -0,0 +1,103 @@ +// 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 install-run (for standalone tools): stdout 1`] = ` +"Try to load @microsoft/rush-lib from rush global folder +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`] = `""`; + +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..fc45e9db213 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,13 @@ ${loadAndPrintRushSdkModule} } } ); + + 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()).toMatchSnapshot('stdout'); + expect( + result.stdout.replace(new RegExp(userRushSdkFolder.replace(/\\/g, '\\\\'), '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; 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)