• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    addRange, append, Bundle, chainBundle, createEmptyExports, createExternalHelpersImportDeclarationIfNeeded, Debug, EmitFlags,
3    EmitHint, ESMap, ExportAssignment, ExportDeclaration, Expression, GeneratedIdentifierFlags, getEmitFlags,
4    getEmitModuleKind, getEmitScriptTarget, getExternalModuleNameLiteral, hasSyntacticModifier, Identifier, idText,
5    ImportDeclaration, ImportEqualsDeclaration, insertStatementsAfterCustomPrologue,
6    isExportNamespaceAsDefaultDeclaration, isExternalModule, isExternalModuleImportEqualsDeclaration,
7    isExternalModuleIndicator, isIdentifier, isNamespaceExport, isOnlyAnnotationsAreExportedOrImported, isSourceFile, isStatement, Map, ModifierFlags,
8    ModuleKind, Node, NodeFlags, ScriptTarget, setOriginalNode, setTextRange, singleOrMany, some, SourceFile, Statement,
9    SyntaxKind, TransformationContext, VariableStatement, visitEachChild, visitNodes, VisitResult,
10} from "../../_namespaces/ts";
11
12/** @internal */
13export function transformECMAScriptModule(context: TransformationContext): (x: SourceFile | Bundle) => SourceFile | Bundle {
14    const {
15        factory,
16        getEmitHelperFactory: emitHelpers,
17    } = context;
18    const host = context.getEmitHost();
19    const resolver = context.getEmitResolver();
20    const compilerOptions = context.getCompilerOptions();
21    const languageVersion = getEmitScriptTarget(compilerOptions);
22    const previousOnEmitNode = context.onEmitNode;
23    const previousOnSubstituteNode = context.onSubstituteNode;
24    context.onEmitNode = onEmitNode;
25    context.onSubstituteNode = onSubstituteNode;
26    context.enableEmitNotification(SyntaxKind.SourceFile);
27    context.enableSubstitution(SyntaxKind.Identifier);
28
29    let helperNameSubstitutions: ESMap<string, Identifier> | undefined;
30    let currentSourceFile: SourceFile | undefined;
31    let importRequireStatements: [ImportDeclaration, VariableStatement] | undefined;
32    return chainBundle(context, transformSourceFile);
33
34    function transformSourceFile(node: SourceFile) {
35        if (node.isDeclarationFile) {
36            return node;
37        }
38
39        if (isExternalModule(node) || compilerOptions.isolatedModules) {
40            currentSourceFile = node;
41            importRequireStatements = undefined;
42            let result = updateExternalModule(node);
43            currentSourceFile = undefined;
44            if (importRequireStatements) {
45                result = factory.updateSourceFile(
46                    result,
47                    setTextRange(factory.createNodeArray(insertStatementsAfterCustomPrologue(result.statements.slice(), importRequireStatements)), result.statements),
48                );
49            }
50            if (!isExternalModule(node) || some(result.statements, isExternalModuleIndicator)) {
51                return result;
52            }
53            if (isOnlyAnnotationsAreExportedOrImported(result, resolver)) {
54                return result;
55            }
56            return factory.updateSourceFile(
57                result,
58                setTextRange(factory.createNodeArray([...result.statements, createEmptyExports(factory)]), result.statements),
59            );
60        }
61
62        return node;
63    }
64
65    function updateExternalModule(node: SourceFile) {
66        const externalHelpersImportDeclaration = createExternalHelpersImportDeclarationIfNeeded(factory, emitHelpers(), node, compilerOptions);
67        if (externalHelpersImportDeclaration) {
68            const statements: Statement[] = [];
69            const statementOffset = factory.copyPrologue(node.statements, statements);
70            append(statements, externalHelpersImportDeclaration);
71
72            addRange(statements, visitNodes(node.statements, visitor, isStatement, statementOffset));
73            return factory.updateSourceFile(
74                node,
75                setTextRange(factory.createNodeArray(statements), node.statements));
76        }
77        else {
78            return visitEachChild(node, visitor, context);
79        }
80    }
81
82    function visitor(node: Node): VisitResult<Node> {
83        switch (node.kind) {
84            case SyntaxKind.ImportEqualsDeclaration:
85                // Though an error in es2020 modules, in node-flavor es2020 modules, we can helpfully transform this to a synthetic `require` call
86                // To give easy access to a synchronous `require` in node-flavor esm. We do the transform even in scenarios where we error, but `import.meta.url`
87                // is available, just because the output is reasonable for a node-like runtime.
88                return getEmitModuleKind(compilerOptions) >= ModuleKind.Node16 ? visitImportEqualsDeclaration(node as ImportEqualsDeclaration) : undefined;
89            case SyntaxKind.ExportAssignment:
90                return visitExportAssignment(node as ExportAssignment);
91            case SyntaxKind.ExportDeclaration:
92                const exportDecl = (node as ExportDeclaration);
93                return visitExportDeclaration(exportDecl);
94        }
95
96        return node;
97    }
98
99    /**
100     * Creates a `require()` call to import an external module.
101     *
102     * @param importNode The declaration to import.
103     */
104     function createRequireCall(importNode: ImportDeclaration | ImportEqualsDeclaration | ExportDeclaration) {
105        const moduleName = getExternalModuleNameLiteral(factory, importNode, Debug.checkDefined(currentSourceFile), host, resolver, compilerOptions);
106        const args: Expression[] = [];
107        if (moduleName) {
108            args.push(moduleName);
109        }
110
111        if (!importRequireStatements) {
112            const createRequireName = factory.createUniqueName("_createRequire", GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel);
113            const importStatement = factory.createImportDeclaration(
114                /*modifiers*/ undefined,
115                factory.createImportClause(
116                    /*isTypeOnly*/ false,
117                    /*name*/ undefined,
118                    factory.createNamedImports([
119                        factory.createImportSpecifier(/*isTypeOnly*/ false, factory.createIdentifier("createRequire"), createRequireName)
120                    ])
121                ),
122                factory.createStringLiteral("module")
123            );
124            const requireHelperName = factory.createUniqueName("__require", GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel);
125            const requireStatement = factory.createVariableStatement(
126                /*modifiers*/ undefined,
127                factory.createVariableDeclarationList(
128                    [
129                        factory.createVariableDeclaration(
130                            requireHelperName,
131                            /*exclamationToken*/ undefined,
132                            /*type*/ undefined,
133                            factory.createCallExpression(factory.cloneNode(createRequireName), /*typeArguments*/ undefined, [
134                                factory.createPropertyAccessExpression(factory.createMetaProperty(SyntaxKind.ImportKeyword, factory.createIdentifier("meta")), factory.createIdentifier("url"))
135                            ])
136                        )
137                    ],
138                    /*flags*/ languageVersion >= ScriptTarget.ES2015 ? NodeFlags.Const : NodeFlags.None
139                )
140            );
141            importRequireStatements = [importStatement, requireStatement];
142
143        }
144
145        const name = importRequireStatements[1].declarationList.declarations[0].name;
146        Debug.assertNode(name, isIdentifier);
147        return factory.createCallExpression(factory.cloneNode(name), /*typeArguments*/ undefined, args);
148    }
149
150    /**
151     * Visits an ImportEqualsDeclaration node.
152     *
153     * @param node The node to visit.
154     */
155    function visitImportEqualsDeclaration(node: ImportEqualsDeclaration): VisitResult<Statement> {
156        Debug.assert(isExternalModuleImportEqualsDeclaration(node), "import= for internal module references should be handled in an earlier transformer.");
157
158        let statements: Statement[] | undefined;
159        statements = append(statements,
160            setOriginalNode(
161                setTextRange(
162                    factory.createVariableStatement(
163                        /*modifiers*/ undefined,
164                        factory.createVariableDeclarationList(
165                            [
166                                factory.createVariableDeclaration(
167                                    factory.cloneNode(node.name),
168                                    /*exclamationToken*/ undefined,
169                                    /*type*/ undefined,
170                                    createRequireCall(node)
171                                )
172                            ],
173                            /*flags*/ languageVersion >= ScriptTarget.ES2015 ? NodeFlags.Const : NodeFlags.None
174                        )
175                    ),
176                    node),
177                node
178            )
179        );
180
181        statements = appendExportsOfImportEqualsDeclaration(statements, node);
182
183        return singleOrMany(statements);
184    }
185
186    function appendExportsOfImportEqualsDeclaration(statements: Statement[] | undefined, node: ImportEqualsDeclaration) {
187        if (hasSyntacticModifier(node, ModifierFlags.Export)) {
188            statements = append(statements, factory.createExportDeclaration(
189                /*modifiers*/ undefined,
190                node.isTypeOnly,
191                factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, idText(node.name))])
192            ));
193        }
194        return statements;
195    }
196
197    function visitExportAssignment(node: ExportAssignment): VisitResult<ExportAssignment> {
198        // Elide `export=` as it is not legal with --module ES6
199        return node.isExportEquals ? undefined : node;
200    }
201
202    function visitExportDeclaration(node: ExportDeclaration) {
203        // `export * as ns` only needs to be transformed in ES2015
204        if (compilerOptions.module !== undefined && compilerOptions.module > ModuleKind.ES2015) {
205            return node;
206        }
207
208        // Either ill-formed or don't need to be tranformed.
209        if (!node.exportClause || !isNamespaceExport(node.exportClause) || !node.moduleSpecifier) {
210            return node;
211        }
212
213        const oldIdentifier = node.exportClause.name;
214        const synthName = factory.getGeneratedNameForNode(oldIdentifier);
215        const importDecl = factory.createImportDeclaration(
216            /*modifiers*/ undefined,
217            factory.createImportClause(
218                /*isTypeOnly*/ false,
219                /*name*/ undefined,
220                factory.createNamespaceImport(
221                    synthName
222                )
223            ),
224            node.moduleSpecifier,
225            node.assertClause
226        );
227        setOriginalNode(importDecl, node.exportClause);
228
229        const exportDecl = isExportNamespaceAsDefaultDeclaration(node) ? factory.createExportDefault(synthName) : factory.createExportDeclaration(
230            /*modifiers*/ undefined,
231            /*isTypeOnly*/ false,
232            factory.createNamedExports([factory.createExportSpecifier(/*isTypeOnly*/ false, synthName, oldIdentifier)]),
233        );
234        setOriginalNode(exportDecl, node);
235
236        return [importDecl, exportDecl];
237    }
238
239    //
240    // Emit Notification
241    //
242
243    /**
244     * Hook for node emit.
245     *
246     * @param hint A hint as to the intended usage of the node.
247     * @param node The node to emit.
248     * @param emit A callback used to emit the node in the printer.
249     */
250    function onEmitNode(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void {
251        if (isSourceFile(node)) {
252            if ((isExternalModule(node) || compilerOptions.isolatedModules) && compilerOptions.importHelpers) {
253                helperNameSubstitutions = new Map<string, Identifier>();
254            }
255            previousOnEmitNode(hint, node, emitCallback);
256            helperNameSubstitutions = undefined;
257        }
258        else {
259            previousOnEmitNode(hint, node, emitCallback);
260        }
261    }
262
263    //
264    // Substitutions
265    //
266
267    /**
268     * Hooks node substitutions.
269     *
270     * @param hint A hint as to the intended usage of the node.
271     * @param node The node to substitute.
272     */
273    function onSubstituteNode(hint: EmitHint, node: Node) {
274        node = previousOnSubstituteNode(hint, node);
275        if (helperNameSubstitutions && isIdentifier(node) && getEmitFlags(node) & EmitFlags.HelperName) {
276            return substituteHelperName(node);
277        }
278
279        return node;
280    }
281
282    function substituteHelperName(node: Identifier): Expression {
283        const name = idText(node);
284        let substitution = helperNameSubstitutions!.get(name);
285        if (!substitution) {
286            helperNameSubstitutions!.set(name, substitution = factory.createUniqueName(name, GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel));
287        }
288        return substitution;
289    }
290}
291