Skip to content

Commit 246f74c

Browse files
fix: overlapping edits when transpiling with stripped types. (#86)
1 parent 720732c commit 246f74c

4 files changed

Lines changed: 122 additions & 23 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/jsx",
3-
"version": "1.9.1",
3+
"version": "1.9.2",
44
"description": "Runtime JSX tagged template that renders DOM or React trees anywhere with or without a build step.",
55
"keywords": [
66
"jsx runtime",

src/transpile.ts

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ type StripEdit = {
342342
replacement?: string
343343
}
344344

345+
const MAX_TYPESCRIPT_STRIP_PASSES = 5
346+
345347
const hasStringProperty = <K extends string>(
346348
value: unknown,
347349
key: K,
@@ -486,6 +488,67 @@ const applyStripEdits = (magic: MagicString, edits: StripEdit[]) => {
486488
return changed
487489
}
488490

491+
const stripTypeScriptSyntax = (
492+
source: string,
493+
sourceType: TranspileSourceType,
494+
): TranspileJsxSourceResult => {
495+
let currentCode = source
496+
let changed = false
497+
let reachedStripPassLimit = true
498+
499+
for (let pass = 0; pass < MAX_TYPESCRIPT_STRIP_PASSES; pass += 1) {
500+
const parsed = parseSync(
501+
'transpile-jsx-source.tsx',
502+
currentCode,
503+
createModuleParserOptions(sourceType),
504+
)
505+
const error = parsed.errors[0]
506+
if (error) {
507+
throw new Error(formatParserError(error))
508+
}
509+
510+
const edits = collectTypeScriptStripEdits(currentCode, parsed.program)
511+
if (!edits.length) {
512+
reachedStripPassLimit = false
513+
break
514+
}
515+
516+
const magic = new MagicString(currentCode)
517+
const passChanged = applyStripEdits(magic, edits)
518+
if (!passChanged) {
519+
reachedStripPassLimit = false
520+
break
521+
}
522+
523+
currentCode = magic.toString()
524+
changed = true
525+
}
526+
527+
if (reachedStripPassLimit) {
528+
const parsed = parseSync(
529+
'transpile-jsx-source.tsx',
530+
currentCode,
531+
createModuleParserOptions(sourceType),
532+
)
533+
const error = parsed.errors[0]
534+
if (error) {
535+
throw new Error(formatParserError(error))
536+
}
537+
538+
const remainingEdits = collectTypeScriptStripEdits(currentCode, parsed.program)
539+
if (remainingEdits.length) {
540+
throw new Error(
541+
`[jsx] TypeScript strip did not converge after ${MAX_TYPESCRIPT_STRIP_PASSES} passes (${remainingEdits.length} removable TypeScript nodes remain).`,
542+
)
543+
}
544+
}
545+
546+
return {
547+
code: currentCode,
548+
changed,
549+
}
550+
}
551+
489552
export function transpileJsxSource(
490553
source: string,
491554
options: TranspileJsxSourceOptions = {},
@@ -506,32 +569,36 @@ export function transpileJsxSource(
506569
throw new Error(formatParserError(firstError))
507570
}
508571

509-
const magic = new MagicString(source)
510-
const stripChanged =
511-
typescriptMode === 'strip'
512-
? applyStripEdits(magic, collectTypeScriptStripEdits(source, parsed.program))
513-
: false
514-
515572
const jsxRoots = collectRootJsxNodes(parsed.program)
516-
if (!jsxRoots.length) {
573+
const jsxMagic = new MagicString(source)
574+
575+
if (jsxRoots.length) {
576+
const builder = new SourceJsxReactBuilder(
577+
source,
578+
createElementRef,
579+
fragmentRef,
580+
typescriptMode === 'strip',
581+
)
582+
583+
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
584+
jsxMagic.overwrite(node.range[0], node.range[1], builder.compile(node))
585+
})
586+
}
587+
588+
const jsxCode = jsxRoots.length ? jsxMagic.toString() : source
589+
const jsxChanged = jsxRoots.length > 0
590+
591+
if (typescriptMode !== 'strip') {
517592
return {
518-
code: stripChanged ? magic.toString() : source,
519-
changed: stripChanged,
593+
code: jsxCode,
594+
changed: jsxChanged,
520595
}
521596
}
522597

523-
const builder = new SourceJsxReactBuilder(
524-
source,
525-
createElementRef,
526-
fragmentRef,
527-
typescriptMode === 'strip',
528-
)
529-
jsxRoots.sort(compareByRangeStartDesc).forEach(node => {
530-
magic.overwrite(node.range[0], node.range[1], builder.compile(node))
531-
})
598+
const stripResult = stripTypeScriptSyntax(jsxCode, sourceType)
532599

533600
return {
534-
code: magic.toString(),
535-
changed: true,
601+
code: stripResult.code,
602+
changed: jsxChanged || stripResult.changed,
536603
}
537604
}

test/transpile.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,36 @@ const beta = (right as B)
248248
expect(result.code).not.toContain(' as A')
249249
expect(result.code).not.toContain(' as B')
250250
})
251+
252+
it('strips chained TS casts around JSX expressions', () => {
253+
const input = `
254+
const node = (<Checkbox checked={true} /> as unknown as HTMLElement)
255+
`
256+
257+
const result = transpileJsxSource(input, {
258+
sourceType: 'script',
259+
typescript: 'strip',
260+
})
261+
262+
expect(result.changed).toBe(true)
263+
expect(result.code).toContain(
264+
'const node = (React.createElement(Checkbox, { "checked": true }))',
265+
)
266+
expect(result.code).not.toContain(' as unknown')
267+
expect(result.code).not.toContain(' as HTMLElement')
268+
expect(() => new Function(result.code)).not.toThrow()
269+
})
270+
271+
it('throws a clear error when strip mode does not converge', () => {
272+
const input = `
273+
const node = ((((((value as A) as B) as C) as D) as E) as F)
274+
`
275+
276+
expect(() =>
277+
transpileJsxSource(input, {
278+
sourceType: 'script',
279+
typescript: 'strip',
280+
}),
281+
).toThrow(/TypeScript strip did not converge after 5 passes/)
282+
})
251283
})

0 commit comments

Comments
 (0)