Skip to content

Commit

Permalink
Support importHelpers with module:preserve (#59852)
Browse files Browse the repository at this point in the history
  • Loading branch information
rbuckton committed Sep 5, 2024
1 parent d514dab commit f39fe7d
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 58 deletions.
96 changes: 49 additions & 47 deletions src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
Expression,
ExpressionStatement,
externalHelpersModuleNameText,
filter,
first,
firstOrUndefined,
ForInitializer,
Expand Down Expand Up @@ -131,7 +132,6 @@ import {
MultiplicativeOperator,
MultiplicativeOperatorOrHigher,
Mutable,
NamedImportBindings,
Node,
NodeArray,
NodeFactory,
Expand Down Expand Up @@ -173,6 +173,7 @@ import {
Token,
TransformFlags,
TypeNode,
UnscopedEmitHelper,
WrappedExpression,
} from "../_namespaces/ts.js";

Expand Down Expand Up @@ -688,26 +689,29 @@ export function hasRecordedExternalHelpers(sourceFile: SourceFile) {
/** @internal */
export function createExternalHelpersImportDeclarationIfNeeded(nodeFactory: NodeFactory, helperFactory: EmitHelperFactory, sourceFile: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStar?: boolean, hasImportDefault?: boolean) {
if (compilerOptions.importHelpers && isEffectiveExternalModule(sourceFile, compilerOptions)) {
let namedBindings: NamedImportBindings | undefined;
const moduleKind = getEmitModuleKind(compilerOptions);
if ((moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) || getImpliedNodeFormatForEmitWorker(sourceFile, compilerOptions) === ModuleKind.ESNext) {
// use named imports
const helpers = getEmitHelpers(sourceFile);
const impliedModuleKind = getImpliedNodeFormatForEmitWorker(sourceFile, compilerOptions);
const helpers = getImportedHelpers(sourceFile);
if (
(moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext) ||
impliedModuleKind === ModuleKind.ESNext ||
impliedModuleKind === undefined && moduleKind === ModuleKind.Preserve
) {
// When we emit as an ES module, generate an `import` declaration that uses named imports for helpers.
// If we cannot determine the implied module kind under `module: preserve` we assume ESM.
if (helpers) {
const helperNames: string[] = [];
for (const helper of helpers) {
if (!helper.scoped) {
const importName = helper.importName;
if (importName) {
pushIfUnique(helperNames, importName);
}
const importName = helper.importName;
if (importName) {
pushIfUnique(helperNames, importName);
}
}
if (some(helperNames)) {
helperNames.sort(compareStringsCaseSensitive);
// Alias the imports if the names are used somewhere in the file.
// NOTE: We don't need to care about global import collisions as this is a module.
namedBindings = nodeFactory.createNamedImports(
const namedBindings = nodeFactory.createNamedImports(
map(helperNames, name =>
isFileLevelUniqueName(sourceFile, name)
? nodeFactory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, nodeFactory.createIdentifier(name))
Expand All @@ -716,55 +720,53 @@ export function createExternalHelpersImportDeclarationIfNeeded(nodeFactory: Node
const parseNode = getOriginalNode(sourceFile, isSourceFile);
const emitNode = getOrCreateEmitNode(parseNode);
emitNode.externalHelpers = true;

const externalHelpersImportDeclaration = nodeFactory.createImportDeclaration(
/*modifiers*/ undefined,
nodeFactory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, namedBindings),
nodeFactory.createStringLiteral(externalHelpersModuleNameText),
/*attributes*/ undefined,
);
addInternalEmitFlags(externalHelpersImportDeclaration, InternalEmitFlags.NeverApplyImportHelper);
return externalHelpersImportDeclaration;
}
}
}
else {
// use a namespace import
const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(nodeFactory, sourceFile, compilerOptions, hasExportStarsToExportValues, hasImportStar || hasImportDefault);
// When we emit to a non-ES module, generate a synthetic `import tslib = require("tslib")` to be further transformed.
const externalHelpersModuleName = getOrCreateExternalHelpersModuleNameIfNeeded(nodeFactory, sourceFile, compilerOptions, helpers, hasExportStarsToExportValues, hasImportStar || hasImportDefault);
if (externalHelpersModuleName) {
namedBindings = nodeFactory.createNamespaceImport(externalHelpersModuleName);
const externalHelpersImportDeclaration = nodeFactory.createImportEqualsDeclaration(
/*modifiers*/ undefined,
/*isTypeOnly*/ false,
externalHelpersModuleName,
nodeFactory.createExternalModuleReference(nodeFactory.createStringLiteral(externalHelpersModuleNameText)),
);
addInternalEmitFlags(externalHelpersImportDeclaration, InternalEmitFlags.NeverApplyImportHelper);
return externalHelpersImportDeclaration;
}
}
if (namedBindings) {
const externalHelpersImportDeclaration = nodeFactory.createImportDeclaration(
/*modifiers*/ undefined,
nodeFactory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, namedBindings),
nodeFactory.createStringLiteral(externalHelpersModuleNameText),
/*attributes*/ undefined,
);
addInternalEmitFlags(externalHelpersImportDeclaration, InternalEmitFlags.NeverApplyImportHelper);
return externalHelpersImportDeclaration;
}
}
}

function getOrCreateExternalHelpersModuleNameIfNeeded(factory: NodeFactory, node: SourceFile, compilerOptions: CompilerOptions, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) {
if (compilerOptions.importHelpers && isEffectiveExternalModule(node, compilerOptions)) {
const externalHelpersModuleName = getExternalHelpersModuleName(node);
if (externalHelpersModuleName) {
return externalHelpersModuleName;
}
function getImportedHelpers(sourceFile: SourceFile) {
return filter(getEmitHelpers(sourceFile), helper => !helper.scoped);
}

function getOrCreateExternalHelpersModuleNameIfNeeded(factory: NodeFactory, node: SourceFile, compilerOptions: CompilerOptions, helpers: UnscopedEmitHelper[] | undefined, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) {
const externalHelpersModuleName = getExternalHelpersModuleName(node);
if (externalHelpersModuleName) {
return externalHelpersModuleName;
}

let create = (hasExportStarsToExportValues || (getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault))
const create = some(helpers)
|| (hasExportStarsToExportValues || (getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault))
&& getEmitModuleFormatOfFileWorker(node, compilerOptions) < ModuleKind.System;
if (!create) {
const helpers = getEmitHelpers(node);
if (helpers) {
for (const helper of helpers) {
if (!helper.scoped) {
create = true;
break;
}
}
}
}

if (create) {
const parseNode = getOriginalNode(node, isSourceFile);
const emitNode = getOrCreateEmitNode(parseNode);
return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = factory.createUniqueName(externalHelpersModuleNameText));
}
if (create) {
const parseNode = getOriginalNode(node, isSourceFile);
const emitNode = getOrCreateEmitNode(parseNode);
return emitNode.externalHelpersModuleName || (emitNode.externalHelpersModuleName = factory.createUniqueName(externalHelpersModuleNameText));
}
}

Expand Down
33 changes: 25 additions & 8 deletions src/compiler/transformers/module/esnextAnd2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
getEmitFlags,
getEmitModuleKind,
getEmitScriptTarget,
getExternalHelpersModuleName,
getExternalModuleNameLiteral,
getIsolatedModules,
getNodeId,
hasSyntacticModifier,
Identifier,
idText,
Expand All @@ -36,6 +38,7 @@ import {
ModuleKind,
Node,
NodeFlags,
NodeId,
ScriptTarget,
setOriginalNode,
setTextRange,
Expand All @@ -46,6 +49,7 @@ import {
SyntaxKind,
TransformationContext,
VariableStatement,
visitArray,
visitEachChild,
visitNodes,
VisitResult,
Expand All @@ -68,6 +72,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
context.enableEmitNotification(SyntaxKind.SourceFile);
context.enableSubstitution(SyntaxKind.Identifier);

const noSubstitution = new Set<NodeId>();
let helperNameSubstitutions: Map<string, Identifier> | undefined;
let currentSourceFile: SourceFile | undefined;
let importRequireStatements: [ImportDeclaration, VariableStatement] | undefined;
Expand Down Expand Up @@ -106,8 +111,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
if (externalHelpersImportDeclaration) {
const statements: Statement[] = [];
const statementOffset = factory.copyPrologue(node.statements, statements);
append(statements, externalHelpersImportDeclaration);

addRange(statements, visitArray([externalHelpersImportDeclaration], visitor, isStatement));
addRange(statements, visitNodes(node.statements, visitor, isStatement, statementOffset));
return factory.updateSourceFile(
node,
Expand Down Expand Up @@ -318,7 +322,9 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
if ((isExternalModule(node) || getIsolatedModules(compilerOptions)) && compilerOptions.importHelpers) {
helperNameSubstitutions = new Map<string, Identifier>();
}
currentSourceFile = node;
previousOnEmitNode(hint, node, emitCallback);
currentSourceFile = undefined;
helperNameSubstitutions = undefined;
}
else {
Expand All @@ -338,19 +344,30 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S
*/
function onSubstituteNode(hint: EmitHint, node: Node) {
node = previousOnSubstituteNode(hint, node);
if (helperNameSubstitutions && isIdentifier(node) && getEmitFlags(node) & EmitFlags.HelperName) {
if (node.id && noSubstitution.has(node.id)) {
return node;
}
if (isIdentifier(node) && getEmitFlags(node) & EmitFlags.HelperName) {
return substituteHelperName(node);
}

return node;
}

function substituteHelperName(node: Identifier): Expression {
const name = idText(node);
let substitution = helperNameSubstitutions!.get(name);
if (!substitution) {
helperNameSubstitutions!.set(name, substitution = factory.createUniqueName(name, GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel));
const externalHelpersModuleName = currentSourceFile && getExternalHelpersModuleName(currentSourceFile);
if (externalHelpersModuleName) {
noSubstitution.add(getNodeId(node));
return factory.createPropertyAccessExpression(externalHelpersModuleName, node);
}
if (helperNameSubstitutions) {
const name = idText(node);
let substitution = helperNameSubstitutions.get(name);
if (!substitution) {
helperNameSubstitutions.set(name, substitution = factory.createUniqueName(name, GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel));
}
return substitution;
}
return substitution;
return node;
}
}
2 changes: 1 addition & 1 deletion src/compiler/transformers/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function getOriginalNodeId(node: Node) {
/** @internal */
export interface ExternalModuleInfo {
externalImports: (ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration)[]; // imports of other external modules
externalHelpersImportDeclaration: ImportDeclaration | undefined; // import of external helpers
externalHelpersImportDeclaration: ImportDeclaration | ImportEqualsDeclaration | undefined; // import of external helpers
exportSpecifiers: IdentifierNameMap<ExportSpecifier[]>; // file-local export specifiers by name (no reexports)
exportedBindings: Identifier[][]; // exported names of local declarations
exportedNames: ModuleExportName[] | undefined; // all exported names in the module, both local and reexported, excluding the names of locally exported function declarations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export = foo;


//// [main.cjs]
import * as tslib_1 from "tslib";
const tslib_1 = require("tslib");
function foo(args) {
const { bar } = args, extraArgs = __rest(args, ["bar"]);
const { bar } = args, extraArgs = tslib_1.__rest(args, ["bar"]);
return extraArgs;
}
module.exports = foo;
97 changes: 97 additions & 0 deletions tests/baselines/reference/modulePreserveImportHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//// [tests/cases/compiler/modulePreserveImportHelpers.ts] ////

//// [a.mts]
declare var dec: any

@dec()
export class A {}

//// [b.cts]
declare var dec: any

@dec()
class B {}
export {};

//// [c.ts]
declare var dec: any

@dec()
export class C {}

//// [package.json]
{
"type": "module"
}

//// [package.json]
{
"name": "tslib",
"main": "tslib.js",
"types": "tslib.d.ts"
}

//// [tslib.d.ts]
export declare function __esDecorate(...args: any[]): any;
export declare function __runInitializers(...args: any[]): any;


//// [a.mjs]
import { __esDecorate, __runInitializers } from "tslib";
let A = (() => {
let _classDecorators = [dec()];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
var A = class {
static { _classThis = this; }
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
A = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
}
};
return A = _classThis;
})();
export { A };
//// [b.cjs]
const tslib_1 = require("tslib");
let B = (() => {
let _classDecorators = [dec()];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
var B = class {
static { _classThis = this; }
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
tslib_1.__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
B = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
tslib_1.__runInitializers(_classThis, _classExtraInitializers);
}
};
return B = _classThis;
})();
//// [c.js]
import { __esDecorate, __runInitializers } from "tslib";
let C = (() => {
let _classDecorators = [dec()];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
var C = class {
static { _classThis = this; }
static {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
C = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
}
};
return C = _classThis;
})();
export { C };
Loading

0 comments on commit f39fe7d

Please sign in to comment.