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