@knighted/jsx 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/cjs/transpile.cjs +186 -10
- package/dist/cjs/transpile.d.cts +2 -0
- package/dist/transpile.d.ts +2 -0
- package/dist/transpile.js +186 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,21 @@ transpileJsxSource(input, {
|
|
|
94
94
|
})
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
By default, TypeScript syntax is preserved in the output. If your source needs to run directly
|
|
98
|
+
as JavaScript (for example, code entered in an editor), enable type stripping:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
transpileJsxSource(input, {
|
|
102
|
+
typescript: 'strip',
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Supported `typescript` modes:
|
|
107
|
+
|
|
108
|
+
- `'preserve'` (default): keep TypeScript syntax in output.
|
|
109
|
+
- `'strip'`: remove type-only declarations and erase inline type syntax (`: T`, `as T`,
|
|
110
|
+
`satisfies T`, non-null assertions, and type assertions) while still transpiling JSX.
|
|
111
|
+
|
|
97
112
|
### React runtime (`reactJsx`)
|
|
98
113
|
|
|
99
114
|
Need to compose React elements instead of DOM nodes? Import the dedicated helper from the `@knighted/jsx/react` subpath (React 18+ and `react-dom` are still required to mount the tree):
|
package/dist/cjs/transpile.cjs
CHANGED
|
@@ -35,15 +35,24 @@ const isSourceRange = (value) => Array.isArray(value) &&
|
|
|
35
35
|
typeof value[0] === 'number' &&
|
|
36
36
|
typeof value[1] === 'number';
|
|
37
37
|
const hasSourceRange = (value) => isObjectRecord(value) && isSourceRange(value.range);
|
|
38
|
+
const tsWrapperExpressionNodeTypes = new Set([
|
|
39
|
+
'TSAsExpression',
|
|
40
|
+
'TSSatisfiesExpression',
|
|
41
|
+
'TSInstantiationExpression',
|
|
42
|
+
'TSNonNullExpression',
|
|
43
|
+
'TSTypeAssertion',
|
|
44
|
+
]);
|
|
38
45
|
const compareByRangeStartDesc = (first, second) => second.range[0] - first.range[0];
|
|
39
46
|
class SourceJsxReactBuilder {
|
|
40
47
|
source;
|
|
41
48
|
createElementRef;
|
|
42
49
|
fragmentRef;
|
|
43
|
-
|
|
50
|
+
stripTypes;
|
|
51
|
+
constructor(source, createElementRef, fragmentRef, stripTypes) {
|
|
44
52
|
this.source = source;
|
|
45
53
|
this.createElementRef = createElementRef;
|
|
46
54
|
this.fragmentRef = fragmentRef;
|
|
55
|
+
this.stripTypes = stripTypes;
|
|
47
56
|
}
|
|
48
57
|
compile(node) {
|
|
49
58
|
return this.compileNode(node);
|
|
@@ -179,6 +188,17 @@ class SourceJsxReactBuilder {
|
|
|
179
188
|
if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
|
|
180
189
|
return this.compileNode(node);
|
|
181
190
|
}
|
|
191
|
+
if (this.stripTypes && isObjectRecord(node)) {
|
|
192
|
+
if ('expression' in node && node.type === 'ParenthesizedExpression') {
|
|
193
|
+
return `(${this.compileExpression(node.expression)})`;
|
|
194
|
+
}
|
|
195
|
+
if ('expression' in node &&
|
|
196
|
+
typeof node.type === 'string' &&
|
|
197
|
+
tsWrapperExpressionNodeTypes.has(node.type)) {
|
|
198
|
+
return this.compileExpression(node.expression);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/* c8 ignore next 3 -- defensive guard for malformed external AST nodes */
|
|
182
202
|
if (!hasSourceRange(node)) {
|
|
183
203
|
throw new Error('[jsx] Unable to read source range for expression node.');
|
|
184
204
|
}
|
|
@@ -230,26 +250,182 @@ const collectRootJsxNodes = (root) => {
|
|
|
230
250
|
walk(root, false);
|
|
231
251
|
return nodes;
|
|
232
252
|
};
|
|
253
|
+
const MAX_TYPESCRIPT_STRIP_PASSES = 5;
|
|
254
|
+
const hasStringProperty = (value, key) => isObjectRecord(value) && typeof value[key] === 'string';
|
|
255
|
+
const hasSourceAndExpressionRanges = (value) => isObjectRecord(value) &&
|
|
256
|
+
typeof value.type === 'string' &&
|
|
257
|
+
hasSourceRange(value) &&
|
|
258
|
+
'expression' in value &&
|
|
259
|
+
hasSourceRange(value.expression);
|
|
260
|
+
const isTypeOnlyImportExport = (value) => hasStringProperty(value, 'importKind')
|
|
261
|
+
? value.importKind === 'type'
|
|
262
|
+
: hasStringProperty(value, 'exportKind') && value.exportKind === 'type';
|
|
263
|
+
const isTypeOnlyNode = (value) => {
|
|
264
|
+
if (!isObjectRecord(value) || typeof value.type !== 'string') {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return [
|
|
268
|
+
'TSTypeAnnotation',
|
|
269
|
+
'TSTypeParameterDeclaration',
|
|
270
|
+
'TSTypeAliasDeclaration',
|
|
271
|
+
'TSInterfaceDeclaration',
|
|
272
|
+
'TSDeclareFunction',
|
|
273
|
+
'TSImportEqualsDeclaration',
|
|
274
|
+
'TSNamespaceExportDeclaration',
|
|
275
|
+
'TSModuleDeclaration',
|
|
276
|
+
].includes(value.type);
|
|
277
|
+
};
|
|
278
|
+
const createStripEditForTsWrapper = (value, source) => {
|
|
279
|
+
if (!hasSourceAndExpressionRanges(value)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (value.type !== 'TSAsExpression' &&
|
|
283
|
+
value.type !== 'TSSatisfiesExpression' &&
|
|
284
|
+
value.type !== 'TSInstantiationExpression' &&
|
|
285
|
+
value.type !== 'TSNonNullExpression' &&
|
|
286
|
+
value.type !== 'TSTypeAssertion') {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const [exprStart, exprEnd] = value.expression.range;
|
|
290
|
+
return {
|
|
291
|
+
range: value.range,
|
|
292
|
+
replacement: source.slice(exprStart, exprEnd),
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
const collectTypeScriptStripEdits = (source, root) => {
|
|
296
|
+
const edits = [];
|
|
297
|
+
const walk = (value) => {
|
|
298
|
+
if (!isObjectRecord(value)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
value.forEach(walk);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (hasSourceRange(value)) {
|
|
306
|
+
if (isTypeOnlyNode(value) || isTypeOnlyImportExport(value)) {
|
|
307
|
+
edits.push({ range: value.range });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const wrapperEdit = createStripEditForTsWrapper(value, source);
|
|
312
|
+
if (wrapperEdit) {
|
|
313
|
+
edits.push(wrapperEdit);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const entry of Object.values(value)) {
|
|
319
|
+
walk(entry);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
walk(root);
|
|
323
|
+
return edits;
|
|
324
|
+
};
|
|
325
|
+
const rangeOverlaps = (first, second) => first[0] < second[1] && second[0] < first[1];
|
|
326
|
+
const compareStripEditPriority = (first, second) => {
|
|
327
|
+
const firstLength = first.range[1] - first.range[0];
|
|
328
|
+
const secondLength = second.range[1] - second.range[0];
|
|
329
|
+
if (firstLength !== secondLength) {
|
|
330
|
+
return secondLength - firstLength;
|
|
331
|
+
}
|
|
332
|
+
return compareByRangeStartDesc(first, second);
|
|
333
|
+
};
|
|
334
|
+
const applyStripEdits = (magic, edits) => {
|
|
335
|
+
if (!edits.length) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const appliedRanges = [];
|
|
339
|
+
let changed = false;
|
|
340
|
+
edits
|
|
341
|
+
.slice()
|
|
342
|
+
.sort(compareStripEditPriority)
|
|
343
|
+
.forEach(edit => {
|
|
344
|
+
/* c8 ignore next -- overlap handling is defensive after de-duplicated collection */
|
|
345
|
+
if (appliedRanges.some(range => rangeOverlaps(range, edit.range))) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const [start, end] = edit.range;
|
|
349
|
+
if (edit.replacement === undefined) {
|
|
350
|
+
magic.remove(start, end);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
magic.overwrite(start, end, edit.replacement);
|
|
354
|
+
}
|
|
355
|
+
appliedRanges.push(edit.range);
|
|
356
|
+
changed = true;
|
|
357
|
+
});
|
|
358
|
+
return changed;
|
|
359
|
+
};
|
|
360
|
+
const stripTypeScriptSyntax = (source, sourceType) => {
|
|
361
|
+
let currentCode = source;
|
|
362
|
+
let changed = false;
|
|
363
|
+
let reachedStripPassLimit = true;
|
|
364
|
+
for (let pass = 0; pass < MAX_TYPESCRIPT_STRIP_PASSES; pass += 1) {
|
|
365
|
+
const parsed = (0, oxc_parser_1.parseSync)('transpile-jsx-source.tsx', currentCode, createModuleParserOptions(sourceType));
|
|
366
|
+
const error = parsed.errors[0];
|
|
367
|
+
if (error) {
|
|
368
|
+
throw new Error(formatParserError(error));
|
|
369
|
+
}
|
|
370
|
+
const edits = collectTypeScriptStripEdits(currentCode, parsed.program);
|
|
371
|
+
if (!edits.length) {
|
|
372
|
+
reachedStripPassLimit = false;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const magic = new magic_string_1.default(currentCode);
|
|
376
|
+
const passChanged = applyStripEdits(magic, edits);
|
|
377
|
+
if (!passChanged) {
|
|
378
|
+
reachedStripPassLimit = false;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
currentCode = magic.toString();
|
|
382
|
+
changed = true;
|
|
383
|
+
}
|
|
384
|
+
if (reachedStripPassLimit) {
|
|
385
|
+
const parsed = (0, oxc_parser_1.parseSync)('transpile-jsx-source.tsx', currentCode, createModuleParserOptions(sourceType));
|
|
386
|
+
const error = parsed.errors[0];
|
|
387
|
+
if (error) {
|
|
388
|
+
throw new Error(formatParserError(error));
|
|
389
|
+
}
|
|
390
|
+
const remainingEdits = collectTypeScriptStripEdits(currentCode, parsed.program);
|
|
391
|
+
if (remainingEdits.length) {
|
|
392
|
+
throw new Error(`[jsx] TypeScript strip did not converge after ${MAX_TYPESCRIPT_STRIP_PASSES} passes (${remainingEdits.length} removable TypeScript nodes remain).`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
code: currentCode,
|
|
397
|
+
changed,
|
|
398
|
+
};
|
|
399
|
+
};
|
|
233
400
|
function transpileJsxSource(source, options = {}) {
|
|
234
401
|
const sourceType = options.sourceType ?? 'module';
|
|
235
402
|
const createElementRef = options.createElement ?? 'React.createElement';
|
|
236
403
|
const fragmentRef = options.fragment ?? 'React.Fragment';
|
|
404
|
+
const typescriptMode = options.typescript ?? 'preserve';
|
|
237
405
|
const parsed = (0, oxc_parser_1.parseSync)('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
|
|
238
406
|
const firstError = parsed.errors[0];
|
|
239
407
|
if (firstError) {
|
|
240
408
|
throw new Error(formatParserError(firstError));
|
|
241
409
|
}
|
|
242
410
|
const jsxRoots = collectRootJsxNodes(parsed.program);
|
|
243
|
-
|
|
244
|
-
|
|
411
|
+
const jsxMagic = new magic_string_1.default(source);
|
|
412
|
+
if (jsxRoots.length) {
|
|
413
|
+
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef, typescriptMode === 'strip');
|
|
414
|
+
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
|
|
415
|
+
jsxMagic.overwrite(node.range[0], node.range[1], builder.compile(node));
|
|
416
|
+
});
|
|
245
417
|
}
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
418
|
+
const jsxCode = jsxRoots.length ? jsxMagic.toString() : source;
|
|
419
|
+
const jsxChanged = jsxRoots.length > 0;
|
|
420
|
+
if (typescriptMode !== 'strip') {
|
|
421
|
+
return {
|
|
422
|
+
code: jsxCode,
|
|
423
|
+
changed: jsxChanged,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const stripResult = stripTypeScriptSyntax(jsxCode, sourceType);
|
|
251
427
|
return {
|
|
252
|
-
code:
|
|
253
|
-
changed:
|
|
428
|
+
code: stripResult.code,
|
|
429
|
+
changed: jsxChanged || stripResult.changed,
|
|
254
430
|
};
|
|
255
431
|
}
|
package/dist/cjs/transpile.d.cts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
type TranspileSourceType = 'module' | 'script';
|
|
2
|
+
type TranspileTypeScriptMode = 'preserve' | 'strip';
|
|
2
3
|
export type TranspileJsxSourceOptions = {
|
|
3
4
|
sourceType?: TranspileSourceType;
|
|
4
5
|
createElement?: string;
|
|
5
6
|
fragment?: string;
|
|
7
|
+
typescript?: TranspileTypeScriptMode;
|
|
6
8
|
};
|
|
7
9
|
export type TranspileJsxSourceResult = {
|
|
8
10
|
code: string;
|
package/dist/transpile.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
type TranspileSourceType = 'module' | 'script';
|
|
2
|
+
type TranspileTypeScriptMode = 'preserve' | 'strip';
|
|
2
3
|
export type TranspileJsxSourceOptions = {
|
|
3
4
|
sourceType?: TranspileSourceType;
|
|
4
5
|
createElement?: string;
|
|
5
6
|
fragment?: string;
|
|
7
|
+
typescript?: TranspileTypeScriptMode;
|
|
6
8
|
};
|
|
7
9
|
export type TranspileJsxSourceResult = {
|
|
8
10
|
code: string;
|
package/dist/transpile.js
CHANGED
|
@@ -29,15 +29,24 @@ const isSourceRange = (value) => Array.isArray(value) &&
|
|
|
29
29
|
typeof value[0] === 'number' &&
|
|
30
30
|
typeof value[1] === 'number';
|
|
31
31
|
const hasSourceRange = (value) => isObjectRecord(value) && isSourceRange(value.range);
|
|
32
|
+
const tsWrapperExpressionNodeTypes = new Set([
|
|
33
|
+
'TSAsExpression',
|
|
34
|
+
'TSSatisfiesExpression',
|
|
35
|
+
'TSInstantiationExpression',
|
|
36
|
+
'TSNonNullExpression',
|
|
37
|
+
'TSTypeAssertion',
|
|
38
|
+
]);
|
|
32
39
|
const compareByRangeStartDesc = (first, second) => second.range[0] - first.range[0];
|
|
33
40
|
class SourceJsxReactBuilder {
|
|
34
41
|
source;
|
|
35
42
|
createElementRef;
|
|
36
43
|
fragmentRef;
|
|
37
|
-
|
|
44
|
+
stripTypes;
|
|
45
|
+
constructor(source, createElementRef, fragmentRef, stripTypes) {
|
|
38
46
|
this.source = source;
|
|
39
47
|
this.createElementRef = createElementRef;
|
|
40
48
|
this.fragmentRef = fragmentRef;
|
|
49
|
+
this.stripTypes = stripTypes;
|
|
41
50
|
}
|
|
42
51
|
compile(node) {
|
|
43
52
|
return this.compileNode(node);
|
|
@@ -173,6 +182,17 @@ class SourceJsxReactBuilder {
|
|
|
173
182
|
if (node.type === 'JSXElement' || node.type === 'JSXFragment') {
|
|
174
183
|
return this.compileNode(node);
|
|
175
184
|
}
|
|
185
|
+
if (this.stripTypes && isObjectRecord(node)) {
|
|
186
|
+
if ('expression' in node && node.type === 'ParenthesizedExpression') {
|
|
187
|
+
return `(${this.compileExpression(node.expression)})`;
|
|
188
|
+
}
|
|
189
|
+
if ('expression' in node &&
|
|
190
|
+
typeof node.type === 'string' &&
|
|
191
|
+
tsWrapperExpressionNodeTypes.has(node.type)) {
|
|
192
|
+
return this.compileExpression(node.expression);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/* c8 ignore next 3 -- defensive guard for malformed external AST nodes */
|
|
176
196
|
if (!hasSourceRange(node)) {
|
|
177
197
|
throw new Error('[jsx] Unable to read source range for expression node.');
|
|
178
198
|
}
|
|
@@ -224,26 +244,182 @@ const collectRootJsxNodes = (root) => {
|
|
|
224
244
|
walk(root, false);
|
|
225
245
|
return nodes;
|
|
226
246
|
};
|
|
247
|
+
const MAX_TYPESCRIPT_STRIP_PASSES = 5;
|
|
248
|
+
const hasStringProperty = (value, key) => isObjectRecord(value) && typeof value[key] === 'string';
|
|
249
|
+
const hasSourceAndExpressionRanges = (value) => isObjectRecord(value) &&
|
|
250
|
+
typeof value.type === 'string' &&
|
|
251
|
+
hasSourceRange(value) &&
|
|
252
|
+
'expression' in value &&
|
|
253
|
+
hasSourceRange(value.expression);
|
|
254
|
+
const isTypeOnlyImportExport = (value) => hasStringProperty(value, 'importKind')
|
|
255
|
+
? value.importKind === 'type'
|
|
256
|
+
: hasStringProperty(value, 'exportKind') && value.exportKind === 'type';
|
|
257
|
+
const isTypeOnlyNode = (value) => {
|
|
258
|
+
if (!isObjectRecord(value) || typeof value.type !== 'string') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return [
|
|
262
|
+
'TSTypeAnnotation',
|
|
263
|
+
'TSTypeParameterDeclaration',
|
|
264
|
+
'TSTypeAliasDeclaration',
|
|
265
|
+
'TSInterfaceDeclaration',
|
|
266
|
+
'TSDeclareFunction',
|
|
267
|
+
'TSImportEqualsDeclaration',
|
|
268
|
+
'TSNamespaceExportDeclaration',
|
|
269
|
+
'TSModuleDeclaration',
|
|
270
|
+
].includes(value.type);
|
|
271
|
+
};
|
|
272
|
+
const createStripEditForTsWrapper = (value, source) => {
|
|
273
|
+
if (!hasSourceAndExpressionRanges(value)) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (value.type !== 'TSAsExpression' &&
|
|
277
|
+
value.type !== 'TSSatisfiesExpression' &&
|
|
278
|
+
value.type !== 'TSInstantiationExpression' &&
|
|
279
|
+
value.type !== 'TSNonNullExpression' &&
|
|
280
|
+
value.type !== 'TSTypeAssertion') {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
const [exprStart, exprEnd] = value.expression.range;
|
|
284
|
+
return {
|
|
285
|
+
range: value.range,
|
|
286
|
+
replacement: source.slice(exprStart, exprEnd),
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
const collectTypeScriptStripEdits = (source, root) => {
|
|
290
|
+
const edits = [];
|
|
291
|
+
const walk = (value) => {
|
|
292
|
+
if (!isObjectRecord(value)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
value.forEach(walk);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (hasSourceRange(value)) {
|
|
300
|
+
if (isTypeOnlyNode(value) || isTypeOnlyImportExport(value)) {
|
|
301
|
+
edits.push({ range: value.range });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const wrapperEdit = createStripEditForTsWrapper(value, source);
|
|
306
|
+
if (wrapperEdit) {
|
|
307
|
+
edits.push(wrapperEdit);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const entry of Object.values(value)) {
|
|
313
|
+
walk(entry);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
walk(root);
|
|
317
|
+
return edits;
|
|
318
|
+
};
|
|
319
|
+
const rangeOverlaps = (first, second) => first[0] < second[1] && second[0] < first[1];
|
|
320
|
+
const compareStripEditPriority = (first, second) => {
|
|
321
|
+
const firstLength = first.range[1] - first.range[0];
|
|
322
|
+
const secondLength = second.range[1] - second.range[0];
|
|
323
|
+
if (firstLength !== secondLength) {
|
|
324
|
+
return secondLength - firstLength;
|
|
325
|
+
}
|
|
326
|
+
return compareByRangeStartDesc(first, second);
|
|
327
|
+
};
|
|
328
|
+
const applyStripEdits = (magic, edits) => {
|
|
329
|
+
if (!edits.length) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
const appliedRanges = [];
|
|
333
|
+
let changed = false;
|
|
334
|
+
edits
|
|
335
|
+
.slice()
|
|
336
|
+
.sort(compareStripEditPriority)
|
|
337
|
+
.forEach(edit => {
|
|
338
|
+
/* c8 ignore next -- overlap handling is defensive after de-duplicated collection */
|
|
339
|
+
if (appliedRanges.some(range => rangeOverlaps(range, edit.range))) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const [start, end] = edit.range;
|
|
343
|
+
if (edit.replacement === undefined) {
|
|
344
|
+
magic.remove(start, end);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
magic.overwrite(start, end, edit.replacement);
|
|
348
|
+
}
|
|
349
|
+
appliedRanges.push(edit.range);
|
|
350
|
+
changed = true;
|
|
351
|
+
});
|
|
352
|
+
return changed;
|
|
353
|
+
};
|
|
354
|
+
const stripTypeScriptSyntax = (source, sourceType) => {
|
|
355
|
+
let currentCode = source;
|
|
356
|
+
let changed = false;
|
|
357
|
+
let reachedStripPassLimit = true;
|
|
358
|
+
for (let pass = 0; pass < MAX_TYPESCRIPT_STRIP_PASSES; pass += 1) {
|
|
359
|
+
const parsed = parseSync('transpile-jsx-source.tsx', currentCode, createModuleParserOptions(sourceType));
|
|
360
|
+
const error = parsed.errors[0];
|
|
361
|
+
if (error) {
|
|
362
|
+
throw new Error(formatParserError(error));
|
|
363
|
+
}
|
|
364
|
+
const edits = collectTypeScriptStripEdits(currentCode, parsed.program);
|
|
365
|
+
if (!edits.length) {
|
|
366
|
+
reachedStripPassLimit = false;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
const magic = new MagicString(currentCode);
|
|
370
|
+
const passChanged = applyStripEdits(magic, edits);
|
|
371
|
+
if (!passChanged) {
|
|
372
|
+
reachedStripPassLimit = false;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
currentCode = magic.toString();
|
|
376
|
+
changed = true;
|
|
377
|
+
}
|
|
378
|
+
if (reachedStripPassLimit) {
|
|
379
|
+
const parsed = parseSync('transpile-jsx-source.tsx', currentCode, createModuleParserOptions(sourceType));
|
|
380
|
+
const error = parsed.errors[0];
|
|
381
|
+
if (error) {
|
|
382
|
+
throw new Error(formatParserError(error));
|
|
383
|
+
}
|
|
384
|
+
const remainingEdits = collectTypeScriptStripEdits(currentCode, parsed.program);
|
|
385
|
+
if (remainingEdits.length) {
|
|
386
|
+
throw new Error(`[jsx] TypeScript strip did not converge after ${MAX_TYPESCRIPT_STRIP_PASSES} passes (${remainingEdits.length} removable TypeScript nodes remain).`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
code: currentCode,
|
|
391
|
+
changed,
|
|
392
|
+
};
|
|
393
|
+
};
|
|
227
394
|
export function transpileJsxSource(source, options = {}) {
|
|
228
395
|
const sourceType = options.sourceType ?? 'module';
|
|
229
396
|
const createElementRef = options.createElement ?? 'React.createElement';
|
|
230
397
|
const fragmentRef = options.fragment ?? 'React.Fragment';
|
|
398
|
+
const typescriptMode = options.typescript ?? 'preserve';
|
|
231
399
|
const parsed = parseSync('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
|
|
232
400
|
const firstError = parsed.errors[0];
|
|
233
401
|
if (firstError) {
|
|
234
402
|
throw new Error(formatParserError(firstError));
|
|
235
403
|
}
|
|
236
404
|
const jsxRoots = collectRootJsxNodes(parsed.program);
|
|
237
|
-
|
|
238
|
-
|
|
405
|
+
const jsxMagic = new MagicString(source);
|
|
406
|
+
if (jsxRoots.length) {
|
|
407
|
+
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef, typescriptMode === 'strip');
|
|
408
|
+
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
|
|
409
|
+
jsxMagic.overwrite(node.range[0], node.range[1], builder.compile(node));
|
|
410
|
+
});
|
|
239
411
|
}
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
412
|
+
const jsxCode = jsxRoots.length ? jsxMagic.toString() : source;
|
|
413
|
+
const jsxChanged = jsxRoots.length > 0;
|
|
414
|
+
if (typescriptMode !== 'strip') {
|
|
415
|
+
return {
|
|
416
|
+
code: jsxCode,
|
|
417
|
+
changed: jsxChanged,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const stripResult = stripTypeScriptSyntax(jsxCode, sourceType);
|
|
245
421
|
return {
|
|
246
|
-
code:
|
|
247
|
-
changed:
|
|
422
|
+
code: stripResult.code,
|
|
423
|
+
changed: jsxChanged || stripResult.changed,
|
|
248
424
|
};
|
|
249
425
|
}
|