UNPKG

11.1 kBJavaScriptView Raw
1
2
3import {ContextualKeyword} from "../parser/tokenizer/keywords";
4import {TokenType as tt} from "../parser/tokenizer/types";
5
6import elideImportEquals from "../util/elideImportEquals";
7import getDeclarationInfo, {
8
9 EMPTY_DECLARATION_INFO,
10} from "../util/getDeclarationInfo";
11import {getNonTypeIdentifiers} from "../util/getNonTypeIdentifiers";
12import shouldElideDefaultExport from "../util/shouldElideDefaultExport";
13
14import Transformer from "./Transformer";
15
16/**
17 * Class for editing import statements when we are keeping the code as ESM. We still need to remove
18 * type-only imports in TypeScript and Flow.
19 */
20export default class ESMImportTransformer extends Transformer {
21
22
23
24 constructor(
25 tokens,
26 nameManager,
27 reactHotLoaderTransformer,
28 isTypeScriptTransformEnabled,
29 options,
30 ) {
31 super();this.tokens = tokens;this.nameManager = nameManager;this.reactHotLoaderTransformer = reactHotLoaderTransformer;this.isTypeScriptTransformEnabled = isTypeScriptTransformEnabled;;
32 this.nonTypeIdentifiers = isTypeScriptTransformEnabled
33 ? getNonTypeIdentifiers(tokens, options)
34 : new Set();
35 this.declarationInfo = isTypeScriptTransformEnabled
36 ? getDeclarationInfo(tokens)
37 : EMPTY_DECLARATION_INFO;
38 }
39
40 process() {
41 // TypeScript `import foo = require('foo');` should always just be translated to plain require.
42 if (this.tokens.matches3(tt._import, tt.name, tt.eq)) {
43 return this.processImportEquals();
44 }
45 if (this.tokens.matches2(tt._export, tt.eq)) {
46 this.tokens.replaceToken("module.exports");
47 return true;
48 }
49 if (this.tokens.matches1(tt._import)) {
50 return this.processImport();
51 }
52 if (this.tokens.matches2(tt._export, tt._default)) {
53 return this.processExportDefault();
54 }
55 if (this.tokens.matches2(tt._export, tt.braceL)) {
56 return this.processNamedExports();
57 }
58 if (
59 this.tokens.matches3(tt._export, tt.name, tt.braceL) &&
60 this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._type)
61 ) {
62 // TS `export type {` case: just remove the export entirely.
63 this.tokens.removeInitialToken();
64 while (!this.tokens.matches1(tt.braceR)) {
65 this.tokens.removeToken();
66 }
67 this.tokens.removeToken();
68
69 // Remove type re-export `... } from './T'`
70 if (
71 this.tokens.matchesContextual(ContextualKeyword._from) &&
72 this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.string)
73 ) {
74 this.tokens.removeToken();
75 this.tokens.removeToken();
76 }
77 return true;
78 }
79 return false;
80 }
81
82 processImportEquals() {
83 const importName = this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 1);
84 if (this.isTypeName(importName)) {
85 // If this name is only used as a type, elide the whole import.
86 elideImportEquals(this.tokens);
87 } else {
88 // Otherwise, switch `import` to `const`.
89 this.tokens.replaceToken("const");
90 }
91 return true;
92 }
93
94 processImport() {
95 if (this.tokens.matches2(tt._import, tt.parenL)) {
96 // Dynamic imports don't need to be transformed.
97 return false;
98 }
99
100 const snapshot = this.tokens.snapshot();
101 const allImportsRemoved = this.removeImportTypeBindings();
102 if (allImportsRemoved) {
103 this.tokens.restoreToSnapshot(snapshot);
104 while (!this.tokens.matches1(tt.string)) {
105 this.tokens.removeToken();
106 }
107 this.tokens.removeToken();
108 if (this.tokens.matches1(tt.semi)) {
109 this.tokens.removeToken();
110 }
111 }
112 return true;
113 }
114
115 /**
116 * Remove type bindings from this import, leaving the rest of the import intact.
117 *
118 * Return true if this import was ONLY types, and thus is eligible for removal. This will bail out
119 * of the replacement operation, so we can return early here.
120 */
121 removeImportTypeBindings() {
122 this.tokens.copyExpectedToken(tt._import);
123 if (
124 this.tokens.matchesContextual(ContextualKeyword._type) &&
125 !this.tokens.matches1AtIndex(this.tokens.currentIndex() + 1, tt.comma) &&
126 !this.tokens.matchesContextualAtIndex(this.tokens.currentIndex() + 1, ContextualKeyword._from)
127 ) {
128 // This is an "import type" statement, so exit early.
129 return true;
130 }
131
132 if (this.tokens.matches1(tt.string)) {
133 // This is a bare import, so we should proceed with the import.
134 this.tokens.copyToken();
135 return false;
136 }
137
138 let foundNonTypeImport = false;
139
140 if (this.tokens.matches1(tt.name)) {
141 if (this.isTypeName(this.tokens.identifierName())) {
142 this.tokens.removeToken();
143 if (this.tokens.matches1(tt.comma)) {
144 this.tokens.removeToken();
145 }
146 } else {
147 foundNonTypeImport = true;
148 this.tokens.copyToken();
149 if (this.tokens.matches1(tt.comma)) {
150 this.tokens.copyToken();
151 }
152 }
153 }
154
155 if (this.tokens.matches1(tt.star)) {
156 if (this.isTypeName(this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 2))) {
157 this.tokens.removeToken();
158 this.tokens.removeToken();
159 this.tokens.removeToken();
160 } else {
161 foundNonTypeImport = true;
162 this.tokens.copyExpectedToken(tt.star);
163 this.tokens.copyExpectedToken(tt.name);
164 this.tokens.copyExpectedToken(tt.name);
165 }
166 } else if (this.tokens.matches1(tt.braceL)) {
167 this.tokens.copyToken();
168 while (!this.tokens.matches1(tt.braceR)) {
169 if (
170 this.tokens.matches3(tt.name, tt.name, tt.comma) ||
171 this.tokens.matches3(tt.name, tt.name, tt.braceR)
172 ) {
173 // type foo
174 this.tokens.removeToken();
175 this.tokens.removeToken();
176 if (this.tokens.matches1(tt.comma)) {
177 this.tokens.removeToken();
178 }
179 } else if (
180 this.tokens.matches5(tt.name, tt.name, tt.name, tt.name, tt.comma) ||
181 this.tokens.matches5(tt.name, tt.name, tt.name, tt.name, tt.braceR)
182 ) {
183 // type foo as bar
184 this.tokens.removeToken();
185 this.tokens.removeToken();
186 this.tokens.removeToken();
187 this.tokens.removeToken();
188 if (this.tokens.matches1(tt.comma)) {
189 this.tokens.removeToken();
190 }
191 } else if (
192 this.tokens.matches2(tt.name, tt.comma) ||
193 this.tokens.matches2(tt.name, tt.braceR)
194 ) {
195 // foo
196 if (this.isTypeName(this.tokens.identifierName())) {
197 this.tokens.removeToken();
198 if (this.tokens.matches1(tt.comma)) {
199 this.tokens.removeToken();
200 }
201 } else {
202 foundNonTypeImport = true;
203 this.tokens.copyToken();
204 if (this.tokens.matches1(tt.comma)) {
205 this.tokens.copyToken();
206 }
207 }
208 } else if (
209 this.tokens.matches4(tt.name, tt.name, tt.name, tt.comma) ||
210 this.tokens.matches4(tt.name, tt.name, tt.name, tt.braceR)
211 ) {
212 // foo as bar
213 if (this.isTypeName(this.tokens.identifierNameAtIndex(this.tokens.currentIndex() + 2))) {
214 this.tokens.removeToken();
215 this.tokens.removeToken();
216 this.tokens.removeToken();
217 if (this.tokens.matches1(tt.comma)) {
218 this.tokens.removeToken();
219 }
220 } else {
221 foundNonTypeImport = true;
222 this.tokens.copyToken();
223 this.tokens.copyToken();
224 this.tokens.copyToken();
225 if (this.tokens.matches1(tt.comma)) {
226 this.tokens.copyToken();
227 }
228 }
229 } else {
230 throw new Error("Unexpected import form.");
231 }
232 }
233 this.tokens.copyExpectedToken(tt.braceR);
234 }
235
236 return !foundNonTypeImport;
237 }
238
239 isTypeName(name) {
240 return this.isTypeScriptTransformEnabled && !this.nonTypeIdentifiers.has(name);
241 }
242
243 processExportDefault() {
244 if (
245 shouldElideDefaultExport(this.isTypeScriptTransformEnabled, this.tokens, this.declarationInfo)
246 ) {
247 // If the exported value is just an identifier and should be elided by TypeScript
248 // rules, then remove it entirely. It will always have the form `export default e`,
249 // where `e` is an identifier.
250 this.tokens.removeInitialToken();
251 this.tokens.removeToken();
252 this.tokens.removeToken();
253 return true;
254 }
255
256 const alreadyHasName =
257 this.tokens.matches4(tt._export, tt._default, tt._function, tt.name) ||
258 // export default async function
259 this.tokens.matches5(tt._export, tt._default, tt.name, tt._function, tt.name) ||
260 this.tokens.matches4(tt._export, tt._default, tt._class, tt.name) ||
261 this.tokens.matches5(tt._export, tt._default, tt._abstract, tt._class, tt.name);
262
263 if (!alreadyHasName && this.reactHotLoaderTransformer) {
264 // This is a plain "export default E" statement and we need to assign E to a variable.
265 // Change "export default E" to "let _default; export default _default = E"
266 const defaultVarName = this.nameManager.claimFreeName("_default");
267 this.tokens.replaceToken(`let ${defaultVarName}; export`);
268 this.tokens.copyToken();
269 this.tokens.appendCode(` ${defaultVarName} =`);
270 this.reactHotLoaderTransformer.setExtractedDefaultExportName(defaultVarName);
271 return true;
272 }
273 return false;
274 }
275
276 /**
277 * In TypeScript, we need to remove named exports that were never declared or only declared as a
278 * type.
279 */
280 processNamedExports() {
281 if (!this.isTypeScriptTransformEnabled) {
282 return false;
283 }
284 this.tokens.copyExpectedToken(tt._export);
285 this.tokens.copyExpectedToken(tt.braceL);
286
287 while (!this.tokens.matches1(tt.braceR)) {
288 if (!this.tokens.matches1(tt.name)) {
289 throw new Error("Expected identifier at the start of named export.");
290 }
291 if (this.shouldElideExportedName(this.tokens.identifierName())) {
292 while (
293 !this.tokens.matches1(tt.comma) &&
294 !this.tokens.matches1(tt.braceR) &&
295 !this.tokens.isAtEnd()
296 ) {
297 this.tokens.removeToken();
298 }
299 if (this.tokens.matches1(tt.comma)) {
300 this.tokens.removeToken();
301 }
302 } else {
303 while (
304 !this.tokens.matches1(tt.comma) &&
305 !this.tokens.matches1(tt.braceR) &&
306 !this.tokens.isAtEnd()
307 ) {
308 this.tokens.copyToken();
309 }
310 if (this.tokens.matches1(tt.comma)) {
311 this.tokens.copyToken();
312 }
313 }
314 }
315 this.tokens.copyExpectedToken(tt.braceR);
316 return true;
317 }
318
319 /**
320 * ESM elides all imports with the rule that we only elide if we see that it's
321 * a type and never see it as a value. This is in contract to CJS, which
322 * elides imports that are completely unknown.
323 */
324 shouldElideExportedName(name) {
325 return (
326 this.isTypeScriptTransformEnabled &&
327 this.declarationInfo.typeDeclarations.has(name) &&
328 !this.declarationInfo.valueDeclarations.has(name)
329 );
330 }
331}