Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5574c08
feat(rush-sdk): add named export support for CommonJS compatibility
LPegasus Jan 6, 2026
3d52a11
Update common/changes/@microsoft/rush/chore-optimize-named-exports_20…
LPegasus Jan 7, 2026
8c8cc14
[rush-sdk] Revert test snapshot
LPegasus Jan 8, 2026
7c42835
feat(webpack-deep-imports-plugin): add named exports code generation …
LPegasus Jan 16, 2026
0e9d213
refactor(rush-sdk): reuse rush-lib/lib assets exports placehold code
LPegasus Jan 16, 2026
e160ebe
fix(rush-sdk): fix unit test in CI with node <= 20.18
LPegasus Jan 17, 2026
b286070
Update webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts
LPegasus Feb 4, 2026
7105e45
Update libraries/rush-sdk/webpack.config.js
LPegasus Feb 4, 2026
cce271a
Update libraries/rush-sdk/src/generate-stubs.ts
LPegasus Feb 4, 2026
f9eb19d
refactor(webpack-deep-imports-plugin): Use compilation.webpack.Webpac…
LPegasus Feb 4, 2026
475c61a
feat: Generate sidecar .exports.json files instead of injecting expor…
iclanton Feb 5, 2026
255fd67
fix(rush-sdk): Fix ESM named exports for deep imports
iclanton Feb 5, 2026
96c84e9
refactor(rush-sdk): Use footer instead of banner for ESM exports hints
iclanton Feb 5, 2026
8d3f74e
chore(rush-lib): Exclude .exports.json sidecar files from npm package
iclanton Feb 5, 2026
5438963
refactor(rush-sdk): Use async filesystem operations in generate-stubs
iclanton Feb 5, 2026
ed0b358
refactor(tests): Convert synchronous named exports tests to async
iclanton Feb 5, 2026
6b573a5
Merge remote-tracking branch 'microsoft/main' into chore/optimize-nam…
iclanton Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/file-doc-tickets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
fi
- name: File ticket
if: ${{ env.FILE_TICKET == '1' }}
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
with:
repository: microsoft/rushstack-websites
token: '${{ secrets.RUSHSTACK_WEBSITES_PR_TOKEN }}'
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions libraries/rush-lib/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
4 changes: 2 additions & 2 deletions libraries/rush-sdk/config/jest.config.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"extends": "local-node-rig/profiles/default/config/jest.config.json",

"roots": ["<rootDir>/lib-shim"],
"roots": ["<rootDir>/lib-commonjs"],

"testMatch": ["<rootDir>/lib-shim/**/*.test.js"],
"testMatch": ["<rootDir>/lib-commonjs/**/*.test.js"],

"collectCoverageFrom": [
"lib-shim/**/*.js",
Expand Down
142 changes: 104 additions & 38 deletions libraries/rush-sdk/src/generate-stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,133 @@

import * as path from 'node:path';

import { FileSystem, Import, 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<IFileTask> {
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);

FileSystem.writeFile(
targetPath,
// Example:
// module.exports = require("../../../lib-shim/index")._rushSdk_loadInternalModule("logic/policy/GitEmailPolicy");
`module.exports = require(${shimPathLiteral})._rushSdk_loadInternalModule(${srcImportPathLiteral});`
);
} 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<void> {
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<void> {
export async function runAsync(options: IRunScriptOptions): Promise<void> {
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<IFileTask> = 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.');
}
2 changes: 1 addition & 1 deletion libraries/rush-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`
);
Expand Down
103 changes: 103 additions & 0 deletions libraries/rush-sdk/src/test/__snapshots__/script.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 \\"<RUSH_GLOBAL_FOLDER>\\"
Failed to load @microsoft/rush-lib from rush global folder: File does not exist: <RUSH_GLOBAL_FOLDER>
ENOENT: no such file or directory, lstat '<RUSH_GLOBAL_FOLDER>'
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'
]"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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)', 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
`
import('@rushstack/rush-sdk').then(({ RushConfiguration }) => {
console.log(typeof RushConfiguration.loadFromConfigurationFile);
});
`
]);
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)', async () => {
const childProcess = Executable.spawn(process.argv0, [
'-e',
`
import('@rushstack/rush-sdk/lib/utilities/NullTerminalProvider').then(({ NullTerminalProvider }) => {
console.log(NullTerminalProvider.name);
});
`
]);
const { stdout, exitCode, signal } = await Executable.waitForExitAsync(childProcess, {
encoding: 'utf8'
});

expect(stdout.trim()).toEqual('NullTerminalProvider');
expect(exitCode).toBe(0);
expect(signal).toBeNull();
});
});
Loading