@knighted/jsx 1.9.0 → 1.9.1
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 +137 -4
- package/dist/cjs/transpile.d.cts +2 -0
- package/dist/transpile.d.ts +2 -0
- package/dist/transpile.js +137 -4
- 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,21 +250,134 @@ const collectRootJsxNodes = (root) => {
|
|
|
230
250
|
walk(root, false);
|
|
231
251
|
return nodes;
|
|
232
252
|
};
|
|
253
|
+
const hasStringProperty = (value, key) => isObjectRecord(value) && typeof value[key] === 'string';
|
|
254
|
+
const hasSourceAndExpressionRanges = (value) => isObjectRecord(value) &&
|
|
255
|
+
typeof value.type === 'string' &&
|
|
256
|
+
hasSourceRange(value) &&
|
|
257
|
+
'expression' in value &&
|
|
258
|
+
hasSourceRange(value.expression);
|
|
259
|
+
const isTypeOnlyImportExport = (value) => hasStringProperty(value, 'importKind')
|
|
260
|
+
? value.importKind === 'type'
|
|
261
|
+
: hasStringProperty(value, 'exportKind') && value.exportKind === 'type';
|
|
262
|
+
const isTypeOnlyNode = (value) => {
|
|
263
|
+
if (!isObjectRecord(value) || typeof value.type !== 'string') {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return [
|
|
267
|
+
'TSTypeAnnotation',
|
|
268
|
+
'TSTypeParameterDeclaration',
|
|
269
|
+
'TSTypeAliasDeclaration',
|
|
270
|
+
'TSInterfaceDeclaration',
|
|
271
|
+
'TSDeclareFunction',
|
|
272
|
+
'TSImportEqualsDeclaration',
|
|
273
|
+
'TSNamespaceExportDeclaration',
|
|
274
|
+
'TSModuleDeclaration',
|
|
275
|
+
].includes(value.type);
|
|
276
|
+
};
|
|
277
|
+
const createStripEditForTsWrapper = (value, source) => {
|
|
278
|
+
if (!hasSourceAndExpressionRanges(value)) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
if (value.type !== 'TSAsExpression' &&
|
|
282
|
+
value.type !== 'TSSatisfiesExpression' &&
|
|
283
|
+
value.type !== 'TSInstantiationExpression' &&
|
|
284
|
+
value.type !== 'TSNonNullExpression' &&
|
|
285
|
+
value.type !== 'TSTypeAssertion') {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const [exprStart, exprEnd] = value.expression.range;
|
|
289
|
+
return {
|
|
290
|
+
range: value.range,
|
|
291
|
+
replacement: source.slice(exprStart, exprEnd),
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
const collectTypeScriptStripEdits = (source, root) => {
|
|
295
|
+
const edits = [];
|
|
296
|
+
const walk = (value) => {
|
|
297
|
+
if (!isObjectRecord(value)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(value)) {
|
|
301
|
+
value.forEach(walk);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (hasSourceRange(value)) {
|
|
305
|
+
if (isTypeOnlyNode(value) || isTypeOnlyImportExport(value)) {
|
|
306
|
+
edits.push({ range: value.range });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const wrapperEdit = createStripEditForTsWrapper(value, source);
|
|
311
|
+
if (wrapperEdit) {
|
|
312
|
+
edits.push(wrapperEdit);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const entry of Object.values(value)) {
|
|
318
|
+
walk(entry);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
walk(root);
|
|
322
|
+
return edits;
|
|
323
|
+
};
|
|
324
|
+
const rangeOverlaps = (first, second) => first[0] < second[1] && second[0] < first[1];
|
|
325
|
+
const compareStripEditPriority = (first, second) => {
|
|
326
|
+
const firstLength = first.range[1] - first.range[0];
|
|
327
|
+
const secondLength = second.range[1] - second.range[0];
|
|
328
|
+
if (firstLength !== secondLength) {
|
|
329
|
+
return secondLength - firstLength;
|
|
330
|
+
}
|
|
331
|
+
return compareByRangeStartDesc(first, second);
|
|
332
|
+
};
|
|
333
|
+
const applyStripEdits = (magic, edits) => {
|
|
334
|
+
if (!edits.length) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const appliedRanges = [];
|
|
338
|
+
let changed = false;
|
|
339
|
+
edits
|
|
340
|
+
.slice()
|
|
341
|
+
.sort(compareStripEditPriority)
|
|
342
|
+
.forEach(edit => {
|
|
343
|
+
/* c8 ignore next -- overlap handling is defensive after de-duplicated collection */
|
|
344
|
+
if (appliedRanges.some(range => rangeOverlaps(range, edit.range))) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const [start, end] = edit.range;
|
|
348
|
+
if (edit.replacement === undefined) {
|
|
349
|
+
magic.remove(start, end);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
magic.overwrite(start, end, edit.replacement);
|
|
353
|
+
}
|
|
354
|
+
appliedRanges.push(edit.range);
|
|
355
|
+
changed = true;
|
|
356
|
+
});
|
|
357
|
+
return changed;
|
|
358
|
+
};
|
|
233
359
|
function transpileJsxSource(source, options = {}) {
|
|
234
360
|
const sourceType = options.sourceType ?? 'module';
|
|
235
361
|
const createElementRef = options.createElement ?? 'React.createElement';
|
|
236
362
|
const fragmentRef = options.fragment ?? 'React.Fragment';
|
|
363
|
+
const typescriptMode = options.typescript ?? 'preserve';
|
|
237
364
|
const parsed = (0, oxc_parser_1.parseSync)('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
|
|
238
365
|
const firstError = parsed.errors[0];
|
|
239
366
|
if (firstError) {
|
|
240
367
|
throw new Error(formatParserError(firstError));
|
|
241
368
|
}
|
|
369
|
+
const magic = new magic_string_1.default(source);
|
|
370
|
+
const stripChanged = typescriptMode === 'strip'
|
|
371
|
+
? applyStripEdits(magic, collectTypeScriptStripEdits(source, parsed.program))
|
|
372
|
+
: false;
|
|
242
373
|
const jsxRoots = collectRootJsxNodes(parsed.program);
|
|
243
374
|
if (!jsxRoots.length) {
|
|
244
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
code: stripChanged ? magic.toString() : source,
|
|
377
|
+
changed: stripChanged,
|
|
378
|
+
};
|
|
245
379
|
}
|
|
246
|
-
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef);
|
|
247
|
-
const magic = new magic_string_1.default(source);
|
|
380
|
+
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef, typescriptMode === 'strip');
|
|
248
381
|
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
|
|
249
382
|
magic.overwrite(node.range[0], node.range[1], builder.compile(node));
|
|
250
383
|
});
|
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,21 +244,134 @@ const collectRootJsxNodes = (root) => {
|
|
|
224
244
|
walk(root, false);
|
|
225
245
|
return nodes;
|
|
226
246
|
};
|
|
247
|
+
const hasStringProperty = (value, key) => isObjectRecord(value) && typeof value[key] === 'string';
|
|
248
|
+
const hasSourceAndExpressionRanges = (value) => isObjectRecord(value) &&
|
|
249
|
+
typeof value.type === 'string' &&
|
|
250
|
+
hasSourceRange(value) &&
|
|
251
|
+
'expression' in value &&
|
|
252
|
+
hasSourceRange(value.expression);
|
|
253
|
+
const isTypeOnlyImportExport = (value) => hasStringProperty(value, 'importKind')
|
|
254
|
+
? value.importKind === 'type'
|
|
255
|
+
: hasStringProperty(value, 'exportKind') && value.exportKind === 'type';
|
|
256
|
+
const isTypeOnlyNode = (value) => {
|
|
257
|
+
if (!isObjectRecord(value) || typeof value.type !== 'string') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return [
|
|
261
|
+
'TSTypeAnnotation',
|
|
262
|
+
'TSTypeParameterDeclaration',
|
|
263
|
+
'TSTypeAliasDeclaration',
|
|
264
|
+
'TSInterfaceDeclaration',
|
|
265
|
+
'TSDeclareFunction',
|
|
266
|
+
'TSImportEqualsDeclaration',
|
|
267
|
+
'TSNamespaceExportDeclaration',
|
|
268
|
+
'TSModuleDeclaration',
|
|
269
|
+
].includes(value.type);
|
|
270
|
+
};
|
|
271
|
+
const createStripEditForTsWrapper = (value, source) => {
|
|
272
|
+
if (!hasSourceAndExpressionRanges(value)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (value.type !== 'TSAsExpression' &&
|
|
276
|
+
value.type !== 'TSSatisfiesExpression' &&
|
|
277
|
+
value.type !== 'TSInstantiationExpression' &&
|
|
278
|
+
value.type !== 'TSNonNullExpression' &&
|
|
279
|
+
value.type !== 'TSTypeAssertion') {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const [exprStart, exprEnd] = value.expression.range;
|
|
283
|
+
return {
|
|
284
|
+
range: value.range,
|
|
285
|
+
replacement: source.slice(exprStart, exprEnd),
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
const collectTypeScriptStripEdits = (source, root) => {
|
|
289
|
+
const edits = [];
|
|
290
|
+
const walk = (value) => {
|
|
291
|
+
if (!isObjectRecord(value)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
value.forEach(walk);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (hasSourceRange(value)) {
|
|
299
|
+
if (isTypeOnlyNode(value) || isTypeOnlyImportExport(value)) {
|
|
300
|
+
edits.push({ range: value.range });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const wrapperEdit = createStripEditForTsWrapper(value, source);
|
|
305
|
+
if (wrapperEdit) {
|
|
306
|
+
edits.push(wrapperEdit);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const entry of Object.values(value)) {
|
|
312
|
+
walk(entry);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
walk(root);
|
|
316
|
+
return edits;
|
|
317
|
+
};
|
|
318
|
+
const rangeOverlaps = (first, second) => first[0] < second[1] && second[0] < first[1];
|
|
319
|
+
const compareStripEditPriority = (first, second) => {
|
|
320
|
+
const firstLength = first.range[1] - first.range[0];
|
|
321
|
+
const secondLength = second.range[1] - second.range[0];
|
|
322
|
+
if (firstLength !== secondLength) {
|
|
323
|
+
return secondLength - firstLength;
|
|
324
|
+
}
|
|
325
|
+
return compareByRangeStartDesc(first, second);
|
|
326
|
+
};
|
|
327
|
+
const applyStripEdits = (magic, edits) => {
|
|
328
|
+
if (!edits.length) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const appliedRanges = [];
|
|
332
|
+
let changed = false;
|
|
333
|
+
edits
|
|
334
|
+
.slice()
|
|
335
|
+
.sort(compareStripEditPriority)
|
|
336
|
+
.forEach(edit => {
|
|
337
|
+
/* c8 ignore next -- overlap handling is defensive after de-duplicated collection */
|
|
338
|
+
if (appliedRanges.some(range => rangeOverlaps(range, edit.range))) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const [start, end] = edit.range;
|
|
342
|
+
if (edit.replacement === undefined) {
|
|
343
|
+
magic.remove(start, end);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
magic.overwrite(start, end, edit.replacement);
|
|
347
|
+
}
|
|
348
|
+
appliedRanges.push(edit.range);
|
|
349
|
+
changed = true;
|
|
350
|
+
});
|
|
351
|
+
return changed;
|
|
352
|
+
};
|
|
227
353
|
export function transpileJsxSource(source, options = {}) {
|
|
228
354
|
const sourceType = options.sourceType ?? 'module';
|
|
229
355
|
const createElementRef = options.createElement ?? 'React.createElement';
|
|
230
356
|
const fragmentRef = options.fragment ?? 'React.Fragment';
|
|
357
|
+
const typescriptMode = options.typescript ?? 'preserve';
|
|
231
358
|
const parsed = parseSync('transpile-jsx-source.tsx', source, createModuleParserOptions(sourceType));
|
|
232
359
|
const firstError = parsed.errors[0];
|
|
233
360
|
if (firstError) {
|
|
234
361
|
throw new Error(formatParserError(firstError));
|
|
235
362
|
}
|
|
363
|
+
const magic = new MagicString(source);
|
|
364
|
+
const stripChanged = typescriptMode === 'strip'
|
|
365
|
+
? applyStripEdits(magic, collectTypeScriptStripEdits(source, parsed.program))
|
|
366
|
+
: false;
|
|
236
367
|
const jsxRoots = collectRootJsxNodes(parsed.program);
|
|
237
368
|
if (!jsxRoots.length) {
|
|
238
|
-
return {
|
|
369
|
+
return {
|
|
370
|
+
code: stripChanged ? magic.toString() : source,
|
|
371
|
+
changed: stripChanged,
|
|
372
|
+
};
|
|
239
373
|
}
|
|
240
|
-
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef);
|
|
241
|
-
const magic = new MagicString(source);
|
|
374
|
+
const builder = new SourceJsxReactBuilder(source, createElementRef, fragmentRef, typescriptMode === 'strip');
|
|
242
375
|
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
|
|
243
376
|
magic.overwrite(node.range[0], node.range[1], builder.compile(node));
|
|
244
377
|
});
|