@pyreon/compiler 0.19.0 → 0.21.0
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +418 -18
- package/lib/types/index.d.ts +92 -1
- package/package.json +13 -12
- package/src/index.ts +2 -1
- package/src/jsx.ts +669 -17
- package/src/tests/backend-parity-r7-r9.test.ts +91 -0
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +74 -0
- package/src/tests/collapse-bail-census.test.ts +245 -0
- package/src/tests/collapse-key-source-hygiene.test.ts +88 -0
- package/src/tests/element-valued-const-child.test.ts +61 -0
- package/src/tests/falsy-child-characterization.test.ts +48 -0
- package/src/tests/malformed-input-resilience.test.ts +50 -0
- package/src/tests/partial-collapse-detector.test.ts +121 -0
- package/src/tests/partial-collapse-emit.test.ts +104 -0
- package/src/tests/partial-collapse-robustness.test.ts +53 -0
- package/src/tests/prop-derived-shadow.test.ts +96 -0
- package/src/tests/pure-call-reactive-args.test.ts +50 -0
- package/src/tests/r13-callback-stmt-equivalence.test.ts +58 -0
- package/src/tests/r14-ssr-mode-parity.test.ts +51 -0
- package/src/tests/r15-elemconst-propderived.test.ts +47 -0
- package/src/tests/r19-defer-inline-robust.test.ts +54 -0
- package/src/tests/r20-backend-equivalence-sweep.test.ts +50 -0
- package/src/tests/rocketstyle-collapse.test.ts +208 -0
- package/src/tests/signal-autocall-shadow.test.ts +86 -0
- package/src/tests/sourcemap-fidelity.test.ts +77 -0
- package/src/tests/static-text-baking.test.ts +64 -0
- package/src/tests/transform-state-isolation.test.ts +49 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 1 of the partial-collapse spec (`.claude/plans/open-work-2026-q3.md`
|
|
3
|
+
* → #1). The shared `on*`-handler-only detector — the build STARTED, not
|
|
4
|
+
* just measured. The bail-reason census (`collapse-bail-census.test.ts`)
|
|
5
|
+
* proved 7.8% of all `@pyreon/ui-components` call sites bail SOLELY on
|
|
6
|
+
* `on*` handlers while every dimension/style prop is a string literal and
|
|
7
|
+
* children are static text; `detectPartialCollapsibleShape` is the exact
|
|
8
|
+
* detector that claims that subset.
|
|
9
|
+
*
|
|
10
|
+
* Contract under test (mirrors the conservative discipline of the full
|
|
11
|
+
* `detectCollapsibleShape` — every uncertain signal bails):
|
|
12
|
+
*
|
|
13
|
+
* - literal-prop + ≥1 `on[A-Z]…` handler + static-text children
|
|
14
|
+
* → { props (literals only), childrenText, handlers[] }
|
|
15
|
+
* - ZERO handlers → null (that IS the full-collapse shape; the partial
|
|
16
|
+
* detector defers so the existing path stays byte-unchanged and the
|
|
17
|
+
* two detectors NEVER both claim a site — the load-bearing separation)
|
|
18
|
+
* - spread / non-handler `{expr}` prop / boolean attr / element child /
|
|
19
|
+
* expression child → null (hard bail, same catalogue as full)
|
|
20
|
+
* - handler expr span (`exprStart`/`exprEnd`) slices the EXACT source
|
|
21
|
+
* of the `{...}` contents (load-bearing for PR 3's emit, which
|
|
22
|
+
* re-emits `code.slice(exprStart, exprEnd)` into `_rsCollapseH`)
|
|
23
|
+
*
|
|
24
|
+
* Bisect-verify (documented in the PR body): replace the body of
|
|
25
|
+
* `detectPartialCollapsibleShape` with `return null` → the 4 POSITIVE
|
|
26
|
+
* specs fail with `expected null to be …`; the 6 NEGATIVE specs still
|
|
27
|
+
* pass (they assert null). Restore → 10/10. That asymmetry proves the
|
|
28
|
+
* positive assertions are load-bearing on the handler-relaxation logic,
|
|
29
|
+
* not passing for the wrong reason.
|
|
30
|
+
*/
|
|
31
|
+
import { describe, expect, it } from 'vitest'
|
|
32
|
+
import { parseSync } from 'oxc-parser'
|
|
33
|
+
import { detectPartialCollapsibleShape } from '../jsx'
|
|
34
|
+
|
|
35
|
+
/** Parse a JSX snippet and return its first JSXElement node (the shape
|
|
36
|
+
* `tryRocketstyleCollapse` receives in production). */
|
|
37
|
+
function firstJsxElement(code: string): any {
|
|
38
|
+
const { program } = parseSync('input.tsx', code, { sourceType: 'module', lang: 'tsx' })
|
|
39
|
+
let found: any = null
|
|
40
|
+
const visit = (node: any): void => {
|
|
41
|
+
if (found || !node || typeof node !== 'object') return
|
|
42
|
+
if (node.type === 'JSXElement') {
|
|
43
|
+
found = node
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
for (const k in node) {
|
|
47
|
+
const v = node[k]
|
|
48
|
+
if (Array.isArray(v)) for (const c of v) visit(c)
|
|
49
|
+
else if (v && typeof v === 'object' && typeof v.type === 'string') visit(v)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
visit(program)
|
|
53
|
+
return found
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const detect = (code: string) => detectPartialCollapsibleShape(firstJsxElement(code), 'Button')
|
|
57
|
+
|
|
58
|
+
describe('detectPartialCollapsibleShape — PR 1 (on*-handler-only subset)', () => {
|
|
59
|
+
// ── POSITIVE: the partial-collapsible subset ────────────────────────────
|
|
60
|
+
it('claims a literal-prop site with one handler', () => {
|
|
61
|
+
const code =
|
|
62
|
+
'const x = <Button state="primary" size="medium" onClick={handleClick}>Save</Button>'
|
|
63
|
+
const r = detect(code)
|
|
64
|
+
expect(r).not.toBeNull()
|
|
65
|
+
expect(r!.props).toEqual({ state: 'primary', size: 'medium' })
|
|
66
|
+
expect(r!.childrenText).toBe('Save')
|
|
67
|
+
expect(r!.handlers).toHaveLength(1)
|
|
68
|
+
expect(r!.handlers[0]!.name).toBe('onClick')
|
|
69
|
+
expect(code.slice(r!.handlers[0]!.exprStart, r!.handlers[0]!.exprEnd)).toBe('handleClick')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('peels multiple handlers, keeps literal props out of handlers[]', () => {
|
|
73
|
+
const code = 'const x = <Button state="primary" onClick={a} onPointerEnter={b}>Go</Button>'
|
|
74
|
+
const r = detect(code)
|
|
75
|
+
expect(r).not.toBeNull()
|
|
76
|
+
expect(r!.props).toEqual({ state: 'primary' })
|
|
77
|
+
expect(r!.handlers.map((h) => h.name)).toEqual(['onClick', 'onPointerEnter'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('captures the EXACT expression span for an inline arrow handler', () => {
|
|
81
|
+
const code = 'const x = <Button state="primary" onClick={() => doThing(1)}>Y</Button>'
|
|
82
|
+
const r = detect(code)
|
|
83
|
+
expect(r).not.toBeNull()
|
|
84
|
+
expect(code.slice(r!.handlers[0]!.exprStart, r!.handlers[0]!.exprEnd)).toBe('() => doThing(1)')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('trims static-text children (parity with the full detector)', () => {
|
|
88
|
+
const code = 'const x = <Button state="primary" onClick={h}>\n Save\n</Button>'
|
|
89
|
+
const r = detect(code)
|
|
90
|
+
expect(r).not.toBeNull()
|
|
91
|
+
expect(r!.childrenText).toBe('Save')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ── NEGATIVE: every uncertain shape bails (null) ────────────────────────
|
|
95
|
+
it('returns null for ZERO handlers (defers to the full-collapse path)', () => {
|
|
96
|
+
// The load-bearing separation: a fully-literal site with no handler is
|
|
97
|
+
// the EXISTING full-collapse shape — the partial detector must NOT
|
|
98
|
+
// claim it, so the two never both fire on one site.
|
|
99
|
+
expect(detect('const x = <Button state="primary">Save</Button>')).toBeNull()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns null for a spread attribute', () => {
|
|
103
|
+
expect(detect('const x = <Button {...rest} onClick={h}>X</Button>')).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('returns null for a non-handler dynamic prop alongside a handler', () => {
|
|
107
|
+
expect(detect('const x = <Button state={dyn} onClick={h}>X</Button>')).toBeNull()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns null for a boolean attribute', () => {
|
|
111
|
+
expect(detect('const x = <Button disabled onClick={h}>X</Button>')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('returns null for an element child', () => {
|
|
115
|
+
expect(detect('const x = <Button onClick={h}><span /></Button>')).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('returns null for an expression child', () => {
|
|
119
|
+
expect(detect('const x = <Button onClick={h}>{label}</Button>')).toBeNull()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 3/4 of the partial-collapse build (open-work #1) — the compiler
|
|
3
|
+
* EMIT half: `tryRocketstyleCollapse` falls back to
|
|
4
|
+
* `tryPartialCollapse` (PR 1's `detectPartialCollapsibleShape`) when the
|
|
5
|
+
* FULL `detectCollapsibleShape` bails, emitting `__rsCollapseH(...)` +
|
|
6
|
+
* the residual handlers object (consumed by PR 2's `_rsCollapseH`,
|
|
7
|
+
* #681) instead of bailing to the 5-layer mount.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the existing `rocketstyle-collapse.test.ts` harness exactly
|
|
10
|
+
* (stubbed resolved-`sites` map — the resolver/plugin scan is the
|
|
11
|
+
* CI-exercised half; this proves the emit contract in isolation, same
|
|
12
|
+
* as the shipped full-collapse emission specs do).
|
|
13
|
+
*
|
|
14
|
+
* Bisect-verify (PR body): revert the one fallback line in
|
|
15
|
+
* `tryRocketstyleCollapse` (`if (!shape) return tryPartialCollapse(...)`
|
|
16
|
+
* → `if (!shape) return false`) → the partial-emit specs fail
|
|
17
|
+
* (`__rsCollapseH(` absent) while the FULL-collapse regression spec
|
|
18
|
+
* still passes (proving the full path is byte-unchanged — the fallback
|
|
19
|
+
* is the only delta). Restore → all pass. Locally bisect-verifiable
|
|
20
|
+
* (minimal `../jsx` graph, like PR 1 #679 — no resolver, no built lib).
|
|
21
|
+
*/
|
|
22
|
+
import { describe, expect, it } from 'vitest'
|
|
23
|
+
import { rocketstyleCollapseKey, transformJSX } from '../jsx'
|
|
24
|
+
|
|
25
|
+
const SITE = {
|
|
26
|
+
templateHtml: '<button data-x="1"><span class="inner">Save</span></button>',
|
|
27
|
+
lightClass: 'pyr-L1 pyr-L2',
|
|
28
|
+
darkClass: 'pyr-D1 pyr-D2',
|
|
29
|
+
rules: ['.pyr-L1{color:red}', '.pyr-D1{color:blue}'],
|
|
30
|
+
ruleKey: 'bundleA',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collapseOpt(candidates: string[], sites: Record<string, typeof SITE>) {
|
|
34
|
+
return {
|
|
35
|
+
collapseRocketstyle: {
|
|
36
|
+
candidates: new Set(candidates),
|
|
37
|
+
sites: new Map(Object.entries(sites)),
|
|
38
|
+
mode: { name: 'useMode', source: '@pyreon/ui-core' },
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('compiler — partial-collapse emission (on*-handler-only)', () => {
|
|
44
|
+
it('emits __rsCollapseH + handlers object + the _rsCollapseH import', () => {
|
|
45
|
+
const key = rocketstyleCollapseKey('Button', { state: 'primary', size: 'medium' }, 'Save')
|
|
46
|
+
const src =
|
|
47
|
+
'const x = <Button state="primary" size="medium" onClick={handleClick}>Save</Button>'
|
|
48
|
+
const { code } = transformJSX(src, 'App.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
49
|
+
|
|
50
|
+
expect(code).toContain(
|
|
51
|
+
'__rsCollapseH("<button data-x=\\"1\\"><span class=\\"inner\\">Save</span></button>", ' +
|
|
52
|
+
'"pyr-L1 pyr-L2", "pyr-D1 pyr-D2", () => __pyrMode() === "dark", ' +
|
|
53
|
+
'{ "onClick": (handleClick) })',
|
|
54
|
+
)
|
|
55
|
+
// The runtime helper is imported alongside `_rsCollapse`.
|
|
56
|
+
expect(code).toContain(
|
|
57
|
+
'import { _rsCollapse as __rsCollapse, _rsCollapseH as __rsCollapseH } from "@pyreon/runtime-dom";',
|
|
58
|
+
)
|
|
59
|
+
// Idempotent rule injection still emitted (same as the full path).
|
|
60
|
+
expect(code).toContain('__rsSheet.injectRules(')
|
|
61
|
+
expect(code).toContain('"bundleA"')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('peels multiple handlers verbatim (arrow stays one arg via parens)', () => {
|
|
65
|
+
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Go')
|
|
66
|
+
const src = 'const x = <Button state="primary" onClick={a} onPointerEnter={() => b(1)}>Go</Button>'
|
|
67
|
+
const { code } = transformJSX(src, 'M.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
68
|
+
expect(code).toContain('{ "onClick": (a), "onPointerEnter": (() => b(1)) }')
|
|
69
|
+
expect(code).toContain('__rsCollapseH(')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('FULL-collapse path is byte-unchanged (regression): no-handler site still emits plain __rsCollapse', () => {
|
|
73
|
+
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Save')
|
|
74
|
+
const src = 'const x = <Button state="primary">Save</Button>'
|
|
75
|
+
const { code } = transformJSX(src, 'F.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
76
|
+
// Plain `__rsCollapse(` call — NOT the H variant — and the import
|
|
77
|
+
// must NOT pull `_rsCollapseH` (the conditional stays off).
|
|
78
|
+
expect(code).toContain('__rsCollapse("<button data-x=\\"1\\"')
|
|
79
|
+
expect(code).not.toContain('__rsCollapseH(')
|
|
80
|
+
expect(code).toContain('import { _rsCollapse as __rsCollapse } from "@pyreon/runtime-dom";')
|
|
81
|
+
expect(code).not.toContain('_rsCollapseH as __rsCollapseH')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('bails (no collapse) on a non-handler dynamic prop alongside a handler', () => {
|
|
85
|
+
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'X')
|
|
86
|
+
const src = 'const x = <Button state="primary" foo={dyn} onClick={h}>X</Button>'
|
|
87
|
+
const { code } = transformJSX(src, 'B1.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
88
|
+
expect(code).not.toContain('__rsCollapseH')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('bails when the handler-site key has no resolved entry (resolver bailed)', () => {
|
|
92
|
+
const otherKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'X')
|
|
93
|
+
const src = 'const x = <Button state="primary" onClick={h}>X</Button>'
|
|
94
|
+
// Only `secondary` is resolved; the `primary` partial site is not.
|
|
95
|
+
const { code } = transformJSX(src, 'B2.tsx', collapseOpt(['Button'], { [otherKey]: SITE }))
|
|
96
|
+
expect(code).not.toContain('__rsCollapseH')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('does nothing when collapseRocketstyle is absent (default OFF)', () => {
|
|
100
|
+
const src = 'const x = <Button state="primary" onClick={h}>X</Button>'
|
|
101
|
+
const { code } = transformJSX(src, 'Off.tsx', {})
|
|
102
|
+
expect(code).not.toContain('__rsCollapseH')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 8 (robustness gate; no bug found).
|
|
3
|
+
*
|
|
4
|
+
* The partial-collapse emit path (#683 `tryPartialCollapse` →
|
|
5
|
+
* `__rsCollapseH(...)`, runtime #681, e2e #684) shipped with a happy-path
|
|
6
|
+
* emission test only. This adversarial gate locks its SAFETY contract under
|
|
7
|
+
* inputs that must BAIL or emit cleanly — never throw, never emit
|
|
8
|
+
* un-parseable JS: handlers with commas/ternaries/nested-braces/JSX-in-body/
|
|
9
|
+
* template-literals/signal-closures, multi-handler, dynamic non-handler prop,
|
|
10
|
+
* spread, `onClick={undefined}`, and key-miss. All currently pass (the path
|
|
11
|
+
* is robust); this prevents a future regression in the new code from silently
|
|
12
|
+
* shipping broken collapsed output.
|
|
13
|
+
*/
|
|
14
|
+
import { parseSync } from 'oxc-parser'
|
|
15
|
+
import { describe, expect, it } from 'vitest'
|
|
16
|
+
import { rocketstyleCollapseKey, transformJSX } from '../jsx'
|
|
17
|
+
|
|
18
|
+
const SITE = { templateHtml: '<button><span>Save</span></button>', lightClass: 'L', darkClass: 'D', rules: ['.L{}'], ruleKey: 'b' }
|
|
19
|
+
const opt = (sites: Record<string, typeof SITE>) => ({
|
|
20
|
+
collapseRocketstyle: { candidates: new Set(['Button']), sites: new Map(Object.entries(sites)), mode: { name: 'useMode', source: '@pyreon/ui-core' } },
|
|
21
|
+
})
|
|
22
|
+
const reparses = (c: string): boolean => { try { return !(parseSync('o.tsx', c).errors?.length) } catch { return false } }
|
|
23
|
+
|
|
24
|
+
const CASES: Array<[string, string, Record<string, string>]> = [
|
|
25
|
+
['multi-handler', `const x = <Button state="primary" onClick={a} onPointerEnter={b}>Save</Button>`, { state: 'primary' }],
|
|
26
|
+
['arrow-with-commas', `const x = <Button state="primary" onClick={() => f(a, b, c)}>Save</Button>`, { state: 'primary' }],
|
|
27
|
+
['ternary-handler', `const x = <Button state="primary" onClick={cond ? h1 : h2}>Save</Button>`, { state: 'primary' }],
|
|
28
|
+
['nested-braces-handler', `const x = <Button state="primary" onClick={() => { const o = {a:1}; g(o) }}>Save</Button>`, { state: 'primary' }],
|
|
29
|
+
['signal-closure-handler', `const x = <Button state="primary" onClick={() => s.set(s() + 1)}>Save</Button>`, { state: 'primary' }],
|
|
30
|
+
['jsx-in-handler-body', `const x = <Button state="primary" onClick={() => render(<i/>)}>Save</Button>`, { state: 'primary' }],
|
|
31
|
+
['template-literal-in-handler', `const x = <Button state="primary" onClick={() => log(\`v=\${y}\`)}>Save</Button>`, { state: 'primary' }],
|
|
32
|
+
['dynamic-non-handler-prop-bails', `const x = <Button state={dyn} onClick={h}>Save</Button>`, { state: 'primary' }],
|
|
33
|
+
['spread-bails', `const x = <Button state="primary" {...rest} onClick={h}>Save</Button>`, { state: 'primary' }],
|
|
34
|
+
['onClick-undefined', `const x = <Button state="primary" onClick={undefined}>Save</Button>`, { state: 'primary' }],
|
|
35
|
+
['key-miss-no-collapse', `const x = <Button state="primary" onClick={h}>Save</Button>`, { size: 'x' }],
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
describe('Round 8 — partial-collapse emit is robust (never throws / never emits broken JS)', () => {
|
|
39
|
+
for (const [name, src, props] of CASES) {
|
|
40
|
+
it(name, () => {
|
|
41
|
+
const key = rocketstyleCollapseKey('Button', props, 'Save')
|
|
42
|
+
let code = ''
|
|
43
|
+
let threw: unknown = null
|
|
44
|
+
try {
|
|
45
|
+
code = transformJSX(src, 'App.tsx', opt({ [key]: SITE })).code ?? ''
|
|
46
|
+
} catch (e) {
|
|
47
|
+
threw = e
|
|
48
|
+
}
|
|
49
|
+
expect(threw, `partial-collapse must not throw on: ${src}`).toBeNull()
|
|
50
|
+
expect(reparses(code), `partial-collapse must emit parseable JS for: ${src}`).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 2.
|
|
3
|
+
*
|
|
4
|
+
* The prop-derived reactive-props inlining pass (`resolveIdentifiersInText`
|
|
5
|
+
* in jsx.ts) used to substitute every identifier matching a prop-derived
|
|
6
|
+
* const NAME, with zero lexical-scope analysis. Idiomatic code that reuses a
|
|
7
|
+
* short name (`a` / `x` / `i` / `item`) as a callback param or nested local
|
|
8
|
+
* while a prop-derived const of the same name exists was MISCOMPILED:
|
|
9
|
+
*
|
|
10
|
+
* const a = props.x
|
|
11
|
+
* items.map(a => <li>{a}</li>)
|
|
12
|
+
* → items.map((props.x) => <li>{(props.x)}</li>) // un-parseable JS
|
|
13
|
+
*
|
|
14
|
+
* Two cases emitted literally un-parseable JavaScript (arrow-param,
|
|
15
|
+
* catch-param); the rest silently rebound the wrong identifier. The
|
|
16
|
+
* signal-auto-call pass was already scope-aware (`shadowedSignals`); the fix
|
|
17
|
+
* gives the prop-derived pass the same block-accurate shadow discipline.
|
|
18
|
+
*
|
|
19
|
+
* Bisect: revert `scopeBoundPropDerived` / the `!shadowed.has(...)` guard in
|
|
20
|
+
* jsx.ts → the SHADOW specs fail (un-parseable emit / wrong substitution);
|
|
21
|
+
* the no-shadow + transitive specs keep passing (they are the over-suppression
|
|
22
|
+
* guard — the fix must NOT stop inlining where there is no shadow). Restore →
|
|
23
|
+
* all pass.
|
|
24
|
+
*/
|
|
25
|
+
import { parseSync } from 'oxc-parser'
|
|
26
|
+
import { describe, expect, it } from 'vitest'
|
|
27
|
+
import { transformJSX_JS } from '../jsx'
|
|
28
|
+
|
|
29
|
+
const emit = (code: string): string => transformJSX_JS(code, 'c.tsx').code ?? ''
|
|
30
|
+
const parses = (out: string): boolean => {
|
|
31
|
+
try {
|
|
32
|
+
return (parseSync('o.tsx', out).errors?.length ?? 0) === 0
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('prop-derived inlining — lexical shadowing is respected', () => {
|
|
39
|
+
it('arrow-function parameter shadowing a prop-derived const is NOT rewritten (was un-parseable)', () => {
|
|
40
|
+
const out = emit(`function C(props){ const a = props.x; return <ul>{props.items.map(a => <li>{a}</li>)}</ul> }`)
|
|
41
|
+
expect(parses(out)).toBe(true)
|
|
42
|
+
expect(out).not.toContain('(props.x) =>')
|
|
43
|
+
expect(out).toContain('.map(a => <li>{a}</li>)')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('catch-clause parameter shadowing a prop-derived const is NOT rewritten (was un-parseable)', () => {
|
|
47
|
+
const out = emit(`function C(props){ const e = props.err; const h = () => { try {} catch (e) { return e } }; return <i>{h()}</i> }`)
|
|
48
|
+
expect(parses(out)).toBe(true)
|
|
49
|
+
expect(out).toContain('catch (e)')
|
|
50
|
+
expect(out).toContain('return e')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('named-function parameter shadowing a prop-derived const keeps the parameter binding', () => {
|
|
54
|
+
const out = emit(`function C(props){ const x = props.x; function row(x){ return <td>{x}</td> } return <table>{props.rows.map(row)}</table> }`)
|
|
55
|
+
expect(parses(out)).toBe(true)
|
|
56
|
+
expect(out).toContain('function row(x)')
|
|
57
|
+
expect(out).toMatch(/__t0\.data = x\b/) // the row PARAM, not (props.x)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('nested const shadowing a prop-derived const is not clobbered', () => {
|
|
61
|
+
const out = emit(`function C(props){ const a = props.x; const g = () => { const a = 7; return a }; return <b>{g()}</b> }`)
|
|
62
|
+
expect(parses(out)).toBe(true)
|
|
63
|
+
expect(out).toContain('const a = 7; return a')
|
|
64
|
+
expect(out).not.toContain('const a = 7; return (props.x)')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('block-scoped shadow does NOT over-suppress an outer-scope reference (block accuracy)', () => {
|
|
68
|
+
const out = emit(`function C(props){ const a = props.x; { const a = 'inner'; } return <div>{a}</div> }`)
|
|
69
|
+
expect(out).toContain('(props.x)') // {a} is OUTSIDE the shadowing block → still inlined
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('prop-derived inlining — no-shadow paths still inline (over-suppression guard)', () => {
|
|
74
|
+
it('non-shadowing callback still inlines the prop-derived const', () => {
|
|
75
|
+
const out = emit(`function C(props){ const a = props.x; return <ul>{props.items.map(it => <li>{a}{it}</li>)}</ul> }`)
|
|
76
|
+
expect(out).toContain('(props.x)')
|
|
77
|
+
expect(out).toContain('it') // the genuine callback param is untouched
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('transitive chain still resolves through to props', () => {
|
|
81
|
+
const out = emit(`function C(props){ const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }`)
|
|
82
|
+
expect(out).toContain('(((props.x) + 1) * 2)')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Scope PRECISION: a `for (let i=…)` head block-scopes `i` to the loop. A
|
|
86
|
+
// `return i` AFTER the loop is out of the loop's scope and resolves to the
|
|
87
|
+
// OUTER prop-derived `const i` — so it MUST still inline. (Proven: in plain
|
|
88
|
+
// JS, `const i='OUTER'; (() => { for(let i=0;i<3;i++){} return i })()` ===
|
|
89
|
+
// 'OUTER'.) This guards against an over-broad shadow heuristic that would
|
|
90
|
+
// wrongly treat the non-escaping loop var as a shadow at the post-loop use.
|
|
91
|
+
it('does NOT over-suppress: post-loop reference is the outer prop-derived const', () => {
|
|
92
|
+
const out = emit(`function C(props){ const i = props.start; const f = () => { for (let i=0;i<3;i++){} return i }; return <s>{f()}</s> }`)
|
|
93
|
+
expect(parses(out)).toBe(true)
|
|
94
|
+
expect(out).toContain('return (props.start)')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 4 (contract lock; hypothesis disproven = no bug).
|
|
3
|
+
*
|
|
4
|
+
* Hypothesis probed: does a "pure" call (Math.* / JSON.* / Object.* / String /
|
|
5
|
+
* Number — the ~40 fns `isPureStaticCall` treats as side-effect-free) get
|
|
6
|
+
* mis-classified as STATIC when its ARGUMENTS are reactive (signal / prop)?
|
|
7
|
+
* That would silently drop reactivity (the value renders once, never updates).
|
|
8
|
+
*
|
|
9
|
+
* Result: NOT a bug — `shouldWrap`/`isDynamic` correctly inspect arguments, so
|
|
10
|
+
* a pure callee with a dynamic arg is still reactively wrapped. This file is
|
|
11
|
+
* the self-discriminating regression gate for that contract: if someone later
|
|
12
|
+
* "optimizes" pure-call detection to skip wrapping by callee name alone, these
|
|
13
|
+
* specs fail (the exact stale-render bug that optimization would introduce).
|
|
14
|
+
*
|
|
15
|
+
* Bisect: make `isPureStaticCall` short-circuit `shouldWrap` regardless of
|
|
16
|
+
* args → every spec here fails (emit becomes `textContent = Math.max(n(),0)`,
|
|
17
|
+
* no `_bind`). Restore → all pass. The fully-static control proves the gate
|
|
18
|
+
* does not just assert "always reactive".
|
|
19
|
+
*/
|
|
20
|
+
import { describe, expect, it } from 'vitest'
|
|
21
|
+
import { transformJSX_JS } from '../jsx'
|
|
22
|
+
|
|
23
|
+
const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
24
|
+
const isReactive = (o: string): boolean => /_bind\(|_bindText\(/.test(o)
|
|
25
|
+
|
|
26
|
+
describe('Round 4 — pure call with a DYNAMIC argument must stay reactive', () => {
|
|
27
|
+
it('Math.max(signal(), 0) is reactively wrapped', () => {
|
|
28
|
+
expect(isReactive(emit(`function C(){ const n=signal(0); return <div>{Math.max(n(),0)}</div> }`))).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
it('Math.round(props.x) is reactively wrapped', () => {
|
|
31
|
+
expect(isReactive(emit(`function C(p){ return <div>{Math.round(p.x)}</div> }`))).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
it('JSON.stringify(props.o) is reactively wrapped', () => {
|
|
34
|
+
expect(isReactive(emit(`function C(p){ return <div>{JSON.stringify(p.o)}</div> }`))).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
it('Object.keys(props.o).length is reactively wrapped', () => {
|
|
37
|
+
expect(isReactive(emit(`function C(p){ return <div>{Object.keys(p.o).length}</div> }`))).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
it('String(props.x) is reactively wrapped', () => {
|
|
40
|
+
expect(isReactive(emit(`function C(p){ return <div>{String(p.x)}</div> }`))).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Control: a pure call with ONLY static args is correctly STATIC (proves
|
|
44
|
+
// the gate is not vacuously "everything is reactive").
|
|
45
|
+
it('Math.max(1,2,3) with only static args is NOT wrapped', () => {
|
|
46
|
+
const o = emit(`function C(){ return <div>{Math.max(1,2,3)}</div> }`)
|
|
47
|
+
expect(isReactive(o)).toBe(false)
|
|
48
|
+
expect(o).toContain('textContent = Math.max(1,2,3)')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 13 (REAL JS↔Rust divergence, FIXED + bisect).
|
|
3
|
+
*
|
|
4
|
+
* R7 (#687) taught the native `collect_prop_derived_idents` to recurse into
|
|
5
|
+
* callback bodies — but its `collect_pd_in_stmt` only handled
|
|
6
|
+
* Expression/Return/VarDecl/If/Block, with `_ => {}` skipping
|
|
7
|
+
* For/While/DoWhile/Switch/Try/Labeled. So a prop-derived const used inside a
|
|
8
|
+
* callback whose body is one of those shapes still lost reactivity in the
|
|
9
|
+
* NATIVE backend (preferred in prod) while the JS backend inlined it — the
|
|
10
|
+
* exact R7 reactivity-loss class, narrower shapes (e.g. `try { return <li
|
|
11
|
+
* class={c}/> } catch …` in a render callback — plausible defensive render).
|
|
12
|
+
*
|
|
13
|
+
* Fix: native/src/lib.rs `collect_pd_in_stmt` now also handles
|
|
14
|
+
* For/ForIn/ForOf/While/DoWhile/Switch/Try/Labeled, with the SAME
|
|
15
|
+
* `pd_minus`/`collect_bind_pattern_names` shadow-filter discipline as the
|
|
16
|
+
* Block/If arms (so loop/catch-param bindings shadow correctly and the
|
|
17
|
+
* over-substitution clobber is NOT re-introduced — verified by the
|
|
18
|
+
* catch-param spec).
|
|
19
|
+
*
|
|
20
|
+
* Bisect: replace the new arms with `_ => {}` + `bun scripts/build-native.ts`
|
|
21
|
+
* → these specs fail (Rust emits `class={c}`, JS `class={(p.x+'-b')}`).
|
|
22
|
+
* Restore + rebuild → all pass. The 180 native-equivalence tests + full
|
|
23
|
+
* suite remain green (no regression).
|
|
24
|
+
*/
|
|
25
|
+
import { describe, expect, it } from 'vitest'
|
|
26
|
+
import { transformJSX, transformJSX_JS } from '../jsx'
|
|
27
|
+
|
|
28
|
+
const j = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
29
|
+
const r = (c: string): string => transformJSX(c, 'c.tsx').code ?? ''
|
|
30
|
+
|
|
31
|
+
const CASES: Array<[string, string]> = [
|
|
32
|
+
['while', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { while(i){ return <li class={c}/> } })}</ul> }`],
|
|
33
|
+
['switch', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { switch(i){ default: return <li class={c}/> } })}</ul> }`],
|
|
34
|
+
['labeled', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { lbl: { return <li class={c}/> } })}</ul> }`],
|
|
35
|
+
['try-catch', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { try { return <li class={c}/> } catch { return null } })}</ul> }`],
|
|
36
|
+
['for', `function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i => { for(let k=0;k<i;k++){ return <li class={c}/> } })}</ul> }`],
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
describe('Round 13 — prop-derived inlining inside callback statement shapes is JS≡Rust', () => {
|
|
40
|
+
for (const [name, src] of CASES) {
|
|
41
|
+
it(`${name}: native backend matches JS (inlines the prop-derived const)`, () => {
|
|
42
|
+
expect(r(src)).toBe(j(src))
|
|
43
|
+
expect(r(src)).toContain("class={(p.x+'-b')}")
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
it('catch-param shadowing is NOT clobbered (filter discipline, both backends)', () => {
|
|
48
|
+
const src = `function C(p){ const e=p.x+'-b'; return <ul>{p.i.map(i => { try {} catch (e) { return <li class={e}/> } })}</ul> }`
|
|
49
|
+
expect(r(src)).toBe(j(src))
|
|
50
|
+
expect(r(src)).not.toContain("class={(p.x+'-b')}") // `e` is the catch param
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('nested const inside a while body shadowing the prop-derived is NOT clobbered (no R2 regression)', () => {
|
|
54
|
+
const src = `function C(p){ const c=p.x; return <ul>{p.i.map(i => { while(i){ const c=2; return <li>{c}</li> } })}</ul> }`
|
|
55
|
+
expect(r(src)).toBe(j(src))
|
|
56
|
+
expect(r(src)).not.toContain('{(p.x)}') // inner `const c=2` shadows — must stay `{c}`
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 14 (SSR-mode robustness; no bug found).
|
|
3
|
+
*
|
|
4
|
+
* Rounds 1–13 were all client-mode. SSR mode (`{ ssr: true }`) disables
|
|
5
|
+
* template emission (`if (ssr) return false`) and leaves JSX as
|
|
6
|
+
* accessor-wrapped expressions for the SSR renderer. Probed 8 adversarial
|
|
7
|
+
* shapes — all emit parseable code, and the correctness-critical contracts
|
|
8
|
+
* hold in SSR exactly as in client mode: prop-derived consts still inline,
|
|
9
|
+
* signals still auto-call, and the R11 shadow fix still suppresses a
|
|
10
|
+
* destructured callback param. This locks SSR-mode parity for the bug fixes
|
|
11
|
+
* so a future SSR-path change can't silently regress them.
|
|
12
|
+
*/
|
|
13
|
+
import { parseSync } from 'oxc-parser'
|
|
14
|
+
import { describe, expect, it } from 'vitest'
|
|
15
|
+
import { transformJSX_JS } from '../jsx'
|
|
16
|
+
|
|
17
|
+
const ssr = (c: string): string => transformJSX_JS(c, 'c.tsx', { ssr: true }).code ?? ''
|
|
18
|
+
const parses = (o: string): boolean => {
|
|
19
|
+
try {
|
|
20
|
+
return (parseSync('o.tsx', o).errors?.length ?? 0) === 0
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Round 14 — SSR-mode codegen parity', () => {
|
|
27
|
+
it('SSR never throws / always emits parseable code (8 adversarial shapes)', () => {
|
|
28
|
+
const shapes = [
|
|
29
|
+
`function C(p){ return <div class={p.c}>{p.t}</div> }`,
|
|
30
|
+
`function C(p){ return <div {...p}>{p.k}</div> }`,
|
|
31
|
+
`function C(p){ return <>{p.a}<span>{p.b}</span></> }`,
|
|
32
|
+
`function C(){ const h=<h1>T</h1>; return <div>{h}<p>x</p></div> }`,
|
|
33
|
+
`function C(p){ return <section><h1>{p.t}</h1><p>{p.b}</p></section> }`,
|
|
34
|
+
]
|
|
35
|
+
for (const s of shapes) expect(parses(ssr(s)), s).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('prop-derived const still inlines in SSR (R2/R7 parity)', () => {
|
|
39
|
+
expect(ssr(`function C(p){ const c=p.x+'-b'; return <ul>{p.i.map(i=><li class={c}>{i}</li>)}</ul> }`))
|
|
40
|
+
.toContain("class={(p.x+'-b')}")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('signal still auto-calls in SSR', () => {
|
|
44
|
+
expect(ssr(`function C(){ const s=signal(0); return <div>{s}</div> }`)).toContain('s()')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('R11 shadow fix applies in SSR (destructured param NOT auto-called)', () => {
|
|
48
|
+
expect(ssr(`function C(){ const x=signal(0); return <ul>{[{x:1}].map(({x})=><li>{x}</li>)}</ul> }`))
|
|
49
|
+
.not.toContain('{x()}')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 15 (element-const that references a prop-derived
|
|
3
|
+
* var) — consolidated, deterministic JS-backend characterization.
|
|
4
|
+
*
|
|
5
|
+
* Original finding: `const cls = props.x + '-b'; const el = <i class={cls}/>;`
|
|
6
|
+
* used as a bare `{el}` child sat at the intersection of R2 (prop-derived
|
|
7
|
+
* inlining) and R9 (element-const → `_mountSlot`). The JS backend substitutes
|
|
8
|
+
* the element-const's whole initializer into the mount call with the
|
|
9
|
+
* prop-derived part inlined reactively:
|
|
10
|
+
* `_mountSlot((<i class={(props.x + '-b')}/>), …)`
|
|
11
|
+
*
|
|
12
|
+
* It was previously tracked by two duplicate, environment-fragile `it.fails`
|
|
13
|
+
* JS↔Rust divergence locks. The R13 gate/collector fix
|
|
14
|
+
* (`accesses_props` now recurses arrow/JSX → the element-const's prop-derived
|
|
15
|
+
* ref is collected) realigned the native backend with JS — so the locks
|
|
16
|
+
* correctly auto-flipped and were removed (lock→resolved, same lifecycle as
|
|
17
|
+
* R7/R11/R13). JS↔Rust byte-equivalence is now gated generally by the R20
|
|
18
|
+
* sweep + R13's own contract; this file keeps only the DETERMINISTIC,
|
|
19
|
+
* native-independent JS-backend assertions (no `transformJSX` native call →
|
|
20
|
+
* no build-artifact fragility, stable on every runner).
|
|
21
|
+
*/
|
|
22
|
+
import { describe, expect, it } from 'vitest'
|
|
23
|
+
import { transformJSX_JS } from '../jsx'
|
|
24
|
+
|
|
25
|
+
const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
26
|
+
|
|
27
|
+
const PD_ELEM = `function C(p){ const cls=p.x+'-b'; const el=<i class={cls}/>; return <div>{el}<span class={cls}/></div> }`
|
|
28
|
+
const SIMPLE = `function C(){ const h=<h1>T</h1>; return <div>{h}<p>x</p></div> }`
|
|
29
|
+
const REUSED = `function C(){ const ic=<svg/>; return <div>{ic}<span>{ic}</span></div> }`
|
|
30
|
+
|
|
31
|
+
describe('Round 15 — element-const × prop-derived (JS backend characterization)', () => {
|
|
32
|
+
it('JS inlines a prop-derived-referencing element-const into _mountSlot with a reactive class', () => {
|
|
33
|
+
const out = emit(PD_ELEM)
|
|
34
|
+
expect(out).toContain("_mountSlot((<i class={(p.x+'-b')}/>)")
|
|
35
|
+
// the prop-derived class is the reactive form, not the frozen const ref
|
|
36
|
+
expect(out).not.toMatch(/_mountSlot\(\s*el\b/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a simple element-const child routes through _mountSlot (R9 baseline)', () => {
|
|
40
|
+
expect(emit(SIMPLE)).toContain('_mountSlot(h')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('an element-const reused at two sites routes both through _mountSlot', () => {
|
|
44
|
+
const out = emit(REUSED)
|
|
45
|
+
expect(out).toMatch(/_mountSlot\(\s*ic\b/)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler hardening — Round 19 (defer-inline robustness; no bug found).
|
|
3
|
+
*
|
|
4
|
+
* `transformDeferInline` (a separate compiler entry, unprobed in rounds
|
|
5
|
+
* 1–18) rewrites `<Defer when={…}><X/></Defer>` to inline its children.
|
|
6
|
+
* Probed 10 valid shapes (no-children, multi-child, expr/text child, nested
|
|
7
|
+
* Defer, self-closing, spread child, comment child, passthrough) + a
|
|
8
|
+
* malformed input — all valid cases emit parseable code, and malformed input
|
|
9
|
+
* is passed through unchanged without throwing (same resilience contract as
|
|
10
|
+
* Round 10). This locks the surface.
|
|
11
|
+
*/
|
|
12
|
+
import { parseSync } from 'oxc-parser'
|
|
13
|
+
import { describe, expect, it } from 'vitest'
|
|
14
|
+
import { transformDeferInline } from '../defer-inline'
|
|
15
|
+
|
|
16
|
+
const run = (c: string): string => {
|
|
17
|
+
const r = transformDeferInline(c, 'c.tsx') as { code?: string }
|
|
18
|
+
return r?.code ?? ''
|
|
19
|
+
}
|
|
20
|
+
const parses = (o: string): boolean => {
|
|
21
|
+
try {
|
|
22
|
+
return (parseSync('o.tsx', o).errors?.length ?? 0) === 0
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VALID: Array<[string, string]> = [
|
|
29
|
+
['basic', `import { Modal } from './M'\nfunction C(){ const o=signal(0); return <Defer when={o()}><Modal title="hi"/></Defer> }`],
|
|
30
|
+
['passthrough', `function C(){ return <div>{x}</div> }`],
|
|
31
|
+
['no-children', `function C(){ return <Defer when={a}></Defer> }`],
|
|
32
|
+
['multi-child', `import {A,B} from './x'\nfunction C(){ return <Defer when={c}><A/><B/></Defer> }`],
|
|
33
|
+
['expr-child', `function C(){ return <Defer when={c}>{val}</Defer> }`],
|
|
34
|
+
['nested-defer', `import {A} from './x'\nfunction C(){ return <Defer when={a}><Defer when={b}><A/></Defer></Defer> }`],
|
|
35
|
+
['text-child', `function C(){ return <Defer when={c}>plain text</Defer> }`],
|
|
36
|
+
['self-closing', `function C(){ return <Defer when={c}/> }`],
|
|
37
|
+
['spread-child', `import {A} from './x'\nfunction C(p){ return <Defer when={c}><A {...p}/></Defer> }`],
|
|
38
|
+
['comment-child', `function C(){ return <Defer when={c}>{/* x */}</Defer> }`],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
describe('Round 19 — transformDeferInline robustness', () => {
|
|
42
|
+
for (const [name, src] of VALID) {
|
|
43
|
+
it(`emits parseable code: ${name}`, () => {
|
|
44
|
+
let out = ''
|
|
45
|
+
expect(() => {
|
|
46
|
+
out = run(src)
|
|
47
|
+
}).not.toThrow()
|
|
48
|
+
expect(parses(out)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
it('malformed input is passed through without throwing (resilience)', () => {
|
|
52
|
+
expect(() => run(`function C(){ return <Defer when={</Defer> }`)).not.toThrow()
|
|
53
|
+
})
|
|
54
|
+
})
|