Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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"
}
12 changes: 6 additions & 6 deletions common/config/subspaces/build-tests-subspace/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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"
}
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
12 changes: 10 additions & 2 deletions libraries/rush-sdk/src/generate-stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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});`
);
}
}
Expand Down
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,32 @@
// 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',
// 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);
});
`
]);
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);
});
});
9 changes: 7 additions & 2 deletions libraries/rush-sdk/src/test/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down Expand Up @@ -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'), '<RUSH_GLOBAL_FOLDER>')
).toMatchSnapshot('stdout');
expect(result.status).toBe(0);
});
});
13 changes: 12 additions & 1 deletion libraries/rush-sdk/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;
Expand Down
25 changes: 22 additions & 3 deletions webpack/webpack-deep-imports-plugin/src/DeepImportsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModuleGraph['getExportsInfo']>;

/**
* @public
Expand Down Expand Up @@ -147,6 +155,7 @@ export class DeepImportsPlugin extends DllPlugin {
libPathWithoutExtension: string;
moduleId: string | number | null;
secondaryChunkId: string | undefined;
exportsInfo: IExportsInfo;
}

const pathsToIgnoreWithoutExtension: Set<string> = this._pathsToIgnoreWithoutExtensions;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down
Loading