From f39fe7dbe631b8e5c4ebd31273c2a1a0f7627414 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 5 Sep 2024 17:48:39 -0400 Subject: [PATCH] Support importHelpers with module:preserve (#59852) --- src/compiler/factory/utilities.ts | 96 +++++++++--------- .../transformers/module/esnextAnd2015.ts | 33 +++++-- src/compiler/transformers/utilities.ts | 2 +- .../importHelpersVerbatimModuleSyntax.js | 4 +- .../reference/modulePreserveImportHelpers.js | 97 +++++++++++++++++++ .../modulePreserveImportHelpers.symbols | 43 ++++++++ .../modulePreserveImportHelpers.types | 53 ++++++++++ .../compiler/modulePreserveImportHelpers.ts | 39 ++++++++ 8 files changed, 309 insertions(+), 58 deletions(-) create mode 100644 tests/baselines/reference/modulePreserveImportHelpers.js create mode 100644 tests/baselines/reference/modulePreserveImportHelpers.symbols create mode 100644 tests/baselines/reference/modulePreserveImportHelpers.types create mode 100644 tests/cases/compiler/modulePreserveImportHelpers.ts diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index d6d0de9e515..b4bedf4aa2e 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -40,6 +40,7 @@ import { Expression, ExpressionStatement, externalHelpersModuleNameText, + filter, first, firstOrUndefined, ForInitializer, @@ -131,7 +132,6 @@ import { MultiplicativeOperator, MultiplicativeOperatorOrHigher, Mutable, - NamedImportBindings, Node, NodeArray, NodeFactory, @@ -173,6 +173,7 @@ import { Token, TransformFlags, TypeNode, + UnscopedEmitHelper, WrappedExpression, } from "../_namespaces/ts.js"; @@ -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)) @@ -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); +} - let create = (hasExportStarsToExportValues || (getESModuleInterop(compilerOptions) && hasImportStarOrImportDefault)) +function getOrCreateExternalHelpersModuleNameIfNeeded(factory: NodeFactory, node: SourceFile, compilerOptions: CompilerOptions, helpers: UnscopedEmitHelper[] | undefined, hasExportStarsToExportValues?: boolean, hasImportStarOrImportDefault?: boolean) { + const externalHelpersModuleName = getExternalHelpersModuleName(node); + if (externalHelpersModuleName) { + return externalHelpersModuleName; + } + + 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)); } } diff --git a/src/compiler/transformers/module/esnextAnd2015.ts b/src/compiler/transformers/module/esnextAnd2015.ts index 0c3bbc8129a..f36388bf898 100644 --- a/src/compiler/transformers/module/esnextAnd2015.ts +++ b/src/compiler/transformers/module/esnextAnd2015.ts @@ -16,8 +16,10 @@ import { getEmitFlags, getEmitModuleKind, getEmitScriptTarget, + getExternalHelpersModuleName, getExternalModuleNameLiteral, getIsolatedModules, + getNodeId, hasSyntacticModifier, Identifier, idText, @@ -36,6 +38,7 @@ import { ModuleKind, Node, NodeFlags, + NodeId, ScriptTarget, setOriginalNode, setTextRange, @@ -46,6 +49,7 @@ import { SyntaxKind, TransformationContext, VariableStatement, + visitArray, visitEachChild, visitNodes, VisitResult, @@ -68,6 +72,7 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S context.enableEmitNotification(SyntaxKind.SourceFile); context.enableSubstitution(SyntaxKind.Identifier); + const noSubstitution = new Set(); let helperNameSubstitutions: Map | undefined; let currentSourceFile: SourceFile | undefined; let importRequireStatements: [ImportDeclaration, VariableStatement] | undefined; @@ -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, @@ -318,7 +322,9 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S if ((isExternalModule(node) || getIsolatedModules(compilerOptions)) && compilerOptions.importHelpers) { helperNameSubstitutions = new Map(); } + currentSourceFile = node; previousOnEmitNode(hint, node, emitCallback); + currentSourceFile = undefined; helperNameSubstitutions = undefined; } else { @@ -338,7 +344,10 @@ 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); } @@ -346,11 +355,19 @@ export function transformECMAScriptModule(context: TransformationContext): (x: S } 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); } - return substitution; + 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 node; } } diff --git a/src/compiler/transformers/utilities.ts b/src/compiler/transformers/utilities.ts index fd1501e1d06..241728903db 100644 --- a/src/compiler/transformers/utilities.ts +++ b/src/compiler/transformers/utilities.ts @@ -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; // 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 diff --git a/tests/baselines/reference/importHelpersVerbatimModuleSyntax.js b/tests/baselines/reference/importHelpersVerbatimModuleSyntax.js index 48c128f184d..dc7ce7199b5 100644 --- a/tests/baselines/reference/importHelpersVerbatimModuleSyntax.js +++ b/tests/baselines/reference/importHelpersVerbatimModuleSyntax.js @@ -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; diff --git a/tests/baselines/reference/modulePreserveImportHelpers.js b/tests/baselines/reference/modulePreserveImportHelpers.js new file mode 100644 index 00000000000..37cdf756150 --- /dev/null +++ b/tests/baselines/reference/modulePreserveImportHelpers.js @@ -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 }; diff --git a/tests/baselines/reference/modulePreserveImportHelpers.symbols b/tests/baselines/reference/modulePreserveImportHelpers.symbols new file mode 100644 index 00000000000..e2421002862 --- /dev/null +++ b/tests/baselines/reference/modulePreserveImportHelpers.symbols @@ -0,0 +1,43 @@ +//// [tests/cases/compiler/modulePreserveImportHelpers.ts] //// + +=== /a.mts === +declare var dec: any +>dec : Symbol(dec, Decl(a.mts, 0, 11)) + +@dec() +>dec : Symbol(dec, Decl(a.mts, 0, 11)) + +export class A {} +>A : Symbol(A, Decl(a.mts, 0, 20)) + +=== /b.cts === +declare var dec: any +>dec : Symbol(dec, Decl(b.cts, 0, 11)) + +@dec() +>dec : Symbol(dec, Decl(b.cts, 0, 11)) + +class B {} +>B : Symbol(B, Decl(b.cts, 0, 20)) + +export {}; + +=== /c.ts === +declare var dec: any +>dec : Symbol(dec, Decl(c.ts, 0, 11)) + +@dec() +>dec : Symbol(dec, Decl(c.ts, 0, 11)) + +export class C {} +>C : Symbol(C, Decl(c.ts, 0, 20)) + +=== /node_modules/tslib/tslib.d.ts === +export declare function __esDecorate(...args: any[]): any; +>__esDecorate : Symbol(__esDecorate, Decl(tslib.d.ts, --, --)) +>args : Symbol(args, Decl(tslib.d.ts, --, --)) + +export declare function __runInitializers(...args: any[]): any; +>__runInitializers : Symbol(__runInitializers, Decl(tslib.d.ts, --, --)) +>args : Symbol(args, Decl(tslib.d.ts, --, --)) + diff --git a/tests/baselines/reference/modulePreserveImportHelpers.types b/tests/baselines/reference/modulePreserveImportHelpers.types new file mode 100644 index 00000000000..b5abcb95538 --- /dev/null +++ b/tests/baselines/reference/modulePreserveImportHelpers.types @@ -0,0 +1,53 @@ +//// [tests/cases/compiler/modulePreserveImportHelpers.ts] //// + +=== /a.mts === +declare var dec: any +>dec : any + +@dec() +>dec() : any +>dec : any + +export class A {} +>A : A +> : ^ + +=== /b.cts === +declare var dec: any +>dec : any + +@dec() +>dec() : any +>dec : any + +class B {} +>B : B +> : ^ + +export {}; + +=== /c.ts === +declare var dec: any +>dec : any + +@dec() +>dec() : any +>dec : any + +export class C {} +>C : C +> : ^ + +=== /node_modules/tslib/tslib.d.ts === +export declare function __esDecorate(...args: any[]): any; +>__esDecorate : (...args: any[]) => any +> : ^^^^ ^^ ^^^^^ +>args : any[] +> : ^^^^^ + +export declare function __runInitializers(...args: any[]): any; +>__runInitializers : (...args: any[]) => any +> : ^^^^ ^^ ^^^^^ +>args : any[] +> : ^^^^^ + diff --git a/tests/cases/compiler/modulePreserveImportHelpers.ts b/tests/cases/compiler/modulePreserveImportHelpers.ts new file mode 100644 index 00000000000..e3f7878fe2d --- /dev/null +++ b/tests/cases/compiler/modulePreserveImportHelpers.ts @@ -0,0 +1,39 @@ +// @module: preserve +// @target: es2022 +// @importHelpers: true + + +// @Filename: /a.mts +declare var dec: any + +@dec() +export class A {} + +// @Filename: /b.cts +declare var dec: any + +@dec() +class B {} +export {}; + +// @Filename: /c.ts +declare var dec: any + +@dec() +export class C {} + +// @filename: /package.json +{ + "type": "module" +} + +// @filename: /node_modules/tslib/package.json +{ + "name": "tslib", + "main": "tslib.js", + "types": "tslib.d.ts" +} + +// @filename: /node_modules/tslib/tslib.d.ts +export declare function __esDecorate(...args: any[]): any; +export declare function __runInitializers(...args: any[]): any;