@pyreon/compiler 0.18.0 → 0.20.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.
Files changed (41) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +2081 -1262
  3. package/lib/types/index.d.ts +310 -125
  4. package/package.json +14 -12
  5. package/src/defer-inline.ts +397 -157
  6. package/src/index.ts +14 -2
  7. package/src/jsx.ts +784 -19
  8. package/src/load-native.ts +1 -0
  9. package/src/manifest.ts +280 -0
  10. package/src/pyreon-intercept.ts +164 -0
  11. package/src/react-intercept.ts +59 -0
  12. package/src/reactivity-lens.ts +190 -0
  13. package/src/tests/backend-parity-r7-r9.test.ts +91 -0
  14. package/src/tests/backend-prop-derived-callback-divergence.test.ts +74 -0
  15. package/src/tests/collapse-bail-census.test.ts +245 -0
  16. package/src/tests/collapse-key-source-hygiene.test.ts +88 -0
  17. package/src/tests/defer-inline.test.ts +209 -21
  18. package/src/tests/detector-tag-consistency.test.ts +2 -0
  19. package/src/tests/element-valued-const-child.test.ts +61 -0
  20. package/src/tests/falsy-child-characterization.test.ts +48 -0
  21. package/src/tests/malformed-input-resilience.test.ts +50 -0
  22. package/src/tests/manifest-snapshot.test.ts +55 -0
  23. package/src/tests/native-equivalence.test.ts +104 -3
  24. package/src/tests/partial-collapse-detector.test.ts +121 -0
  25. package/src/tests/partial-collapse-emit.test.ts +104 -0
  26. package/src/tests/partial-collapse-robustness.test.ts +53 -0
  27. package/src/tests/prop-derived-shadow.test.ts +96 -0
  28. package/src/tests/pure-call-reactive-args.test.ts +50 -0
  29. package/src/tests/pyreon-intercept.test.ts +189 -0
  30. package/src/tests/r13-callback-stmt-equivalence.test.ts +58 -0
  31. package/src/tests/r14-ssr-mode-parity.test.ts +51 -0
  32. package/src/tests/r15-elemconst-propderived.test.ts +47 -0
  33. package/src/tests/r19-defer-inline-robust.test.ts +54 -0
  34. package/src/tests/r20-backend-equivalence-sweep.test.ts +50 -0
  35. package/src/tests/react-intercept.test.ts +50 -2
  36. package/src/tests/reactivity-lens.test.ts +170 -0
  37. package/src/tests/rocketstyle-collapse.test.ts +208 -0
  38. package/src/tests/signal-autocall-shadow.test.ts +86 -0
  39. package/src/tests/sourcemap-fidelity.test.ts +77 -0
  40. package/src/tests/static-text-baking.test.ts +64 -0
  41. package/src/tests/transform-state-isolation.test.ts +49 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Compiler hardening — Round 1.
3
+ *
4
+ * Two locked invariants, one root cause.
5
+ *
6
+ * `rocketstyleCollapseKey` (jsx.ts) and its Vite-plugin twin used to embed
7
+ * RAW C0 control bytes (NUL 0x00 / SOH 0x01) directly inside source string
8
+ * literals as FNV-1a field separators. Three measured consequences:
9
+ *
10
+ * 1. BSD `file(1)` classifies the file as binary `data` (siblings with no
11
+ * raw C0 are correctly "UTF-8 text").
12
+ * 2. Plain `grep`/`rg` silently skip the file (binary-skip) — the
13
+ * compiler's primary source became un-greppable.
14
+ * 3. Silent-correctness fragility: a raw NUL/SOH in a `.ts` string literal
15
+ * is mutable by formatters / editors / copy-paste / git text filters.
16
+ * If the separator byte is altered, the cache key changes with ZERO
17
+ * compile error — the "cache key from raw input" anti-pattern family.
18
+ *
19
+ * Fix: escape sequences (`U+0001` SOH / `U+0000` NUL) — byte-identical at runtime
20
+ * (`String.fromCharCode(1)` is identical to the raw byte), so every emitted key is unchanged, but
21
+ * the source is plain UTF-8 text again.
22
+ *
23
+ * Test A pins the ground-truth keys (proves the fix is byte-identical AND
24
+ * locks the algorithm against any future change). Test B is the
25
+ * self-discriminating repo-wide regression gate: before the fix three files
26
+ * carry raw C0 → it fails; after → it passes. Bisect-verified.
27
+ */
28
+ import { execFileSync } from 'node:child_process'
29
+ import { existsSync, readFileSync } from 'node:fs'
30
+ import { dirname, join, resolve } from 'node:path'
31
+ import { describe, expect, it } from 'vitest'
32
+ import { rocketstyleCollapseKey } from '../jsx'
33
+
34
+ describe('rocketstyleCollapseKey — ground-truth key lock (escape fix is byte-identical)', () => {
35
+ // Captured from the ORIGINAL raw-byte implementation before the escape fix.
36
+ // The escape fix MUST reproduce these exactly (proves zero behavior change);
37
+ // any future algorithm change is also caught here.
38
+ it('emits the exact pre-fix keys', () => {
39
+ expect(rocketstyleCollapseKey('Button', { state: 'primary', size: 'lg' }, 'Click')).toBe('zfm01z')
40
+ expect(rocketstyleCollapseKey('Card', {}, '')).toBe('mzrimv')
41
+ expect(rocketstyleCollapseKey('Comp', { a: '1' }, '')).toBe('1l6zbih')
42
+ expect(rocketstyleCollapseKey('Comp', {}, 'a=1')).toBe('zteym7')
43
+ expect(rocketstyleCollapseKey('日本', { 'aria-label': 'café' }, 'arrow ok')).toBe('vnvy01')
44
+ })
45
+
46
+ it('is order-independent over props (sort) and shape-distinct (separators do their job)', () => {
47
+ expect(rocketstyleCollapseKey('Button', { state: 'primary', size: 'lg' }, 'Click')).toBe(
48
+ rocketstyleCollapseKey('Button', { size: 'lg', state: 'primary' }, 'Click'),
49
+ )
50
+ // Without NUL field separators, `{a:'1'},''` and `{},'a=1'` would collide.
51
+ expect(rocketstyleCollapseKey('Comp', { a: '1' }, '')).not.toBe(
52
+ rocketstyleCollapseKey('Comp', {}, 'a=1'),
53
+ )
54
+ })
55
+ })
56
+
57
+ function repoRoot(): string {
58
+ let d = resolve(__dirname)
59
+ while (!existsSync(join(d, '.git')) && dirname(d) !== d) d = dirname(d)
60
+ return d
61
+ }
62
+
63
+ describe('source hygiene — no raw C0/DEL control bytes in tracked source', () => {
64
+ it('every tracked .ts/.tsx/.js/.mjs/.rs file is plain text (no raw NUL/SOH/ESC/DEL)', () => {
65
+ const root = repoRoot()
66
+ const files = execFileSync(
67
+ 'git',
68
+ ['ls-files', '*.ts', '*.tsx', '*.js', '*.mjs', '*.rs'],
69
+ { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 },
70
+ )
71
+ .split('\n')
72
+ .filter(Boolean)
73
+
74
+ const offenders: string[] = []
75
+ for (const rel of files) {
76
+ const buf = readFileSync(join(root, rel))
77
+ for (let i = 0; i < buf.length; i++) {
78
+ const b = buf[i]!
79
+ // Allow only tab (9), LF (10), CR (13); flag all other C0 + DEL (127).
80
+ if ((b < 32 && b !== 9 && b !== 10 && b !== 13) || b === 127) {
81
+ offenders.push(`${rel} (byte 0x${b.toString(16).padStart(2, '0')} at offset ${i})`)
82
+ break
83
+ }
84
+ }
85
+ }
86
+ expect(offenders, `raw control bytes in source — escape them (\\u00NN):\n${offenders.join('\n')}`).toEqual([])
87
+ })
88
+ })
@@ -121,22 +121,10 @@ export function App() {
121
121
  }
122
122
  `
123
123
  const result = transformDeferInline(input, 'app.tsx')
124
- // No transform fires (multi-child shape doesn't match the inline-eligible
125
- // single-component-child pattern). No warning eitherv1 just leaves it
126
- // alone; downstream Defer's runtime behaviour handles the malformed shape.
127
- expect(result.changed).toBe(false)
128
- })
129
-
130
- test('skips Defer whose child has props (multi-prop closure capture)', () => {
131
- const input = `
132
- import { Defer } from '@pyreon/core'
133
- import { Modal } from './Modal'
134
- export function App() {
135
- return <Defer when={() => true}><Modal title="hi" /></Defer>
136
- }
137
- `
138
- const result = transformDeferInline(input, 'app.tsx')
124
+ // v2 now emits a `multiple-children` warning so the author knows to use
125
+ // the explicit `chunk` form. v1 was silentthat was a footgun.
139
126
  expect(result.changed).toBe(false)
127
+ expect(result.warnings[0]?.code).toBe('defer-inline/multiple-children')
140
128
  })
141
129
 
142
130
  test('fast-path: no Defer in source returns unchanged', () => {
@@ -157,7 +145,74 @@ export const count = signal(0)
157
145
  expect(result.code).toBe(input)
158
146
  })
159
147
 
160
- test('skips renamed imports { Modal as M } not handled in v1', () => {
148
+ // v1 bailed on `{ Modal as M }`. v2 handles renamed imports the
149
+ // local name in JSX is `M`, but the chunk extracts `__m.Modal` (the
150
+ // original exported name). See the positive test in the v2 section.
151
+ })
152
+
153
+ describe('transformDeferInline — v2 capabilities', () => {
154
+ test('preserves props on inline child', () => {
155
+ const input = `
156
+ import { Defer } from '@pyreon/core'
157
+ import { Modal } from './Modal'
158
+ export function App() {
159
+ return <Defer when={() => true}><Modal title="Confirm" size="md" /></Defer>
160
+ }
161
+ `
162
+ const result = transformDeferInline(input, 'app.tsx')
163
+ expect(result.changed).toBe(true)
164
+ expect(result.code).not.toContain("import { Modal }")
165
+ // Props pass through verbatim. Only the component name is replaced.
166
+ expect(result.code).toContain('{(__C) => <__C title="Confirm" size="md" />}')
167
+ })
168
+
169
+ test('preserves nested children on inline child (non-self-closing)', () => {
170
+ const input = `
171
+ import { Defer } from '@pyreon/core'
172
+ import { Modal } from './Modal'
173
+ export function App() {
174
+ return (
175
+ <Defer when={() => true}>
176
+ <Modal title="Hello">
177
+ <p>nested content</p>
178
+ </Modal>
179
+ </Defer>
180
+ )
181
+ }
182
+ `
183
+ const result = transformDeferInline(input, 'app.tsx')
184
+ expect(result.changed).toBe(true)
185
+ // Both opening AND closing tag names replaced with __C; nested JSX intact.
186
+ expect(result.code).toContain('<__C title="Hello">')
187
+ expect(result.code).toContain('</__C>')
188
+ expect(result.code).toContain('<p>nested content</p>')
189
+ })
190
+
191
+ test('captures closure variables via render-prop scope (signal in handler)', () => {
192
+ const input = `
193
+ import { Defer } from '@pyreon/core'
194
+ import { signal } from '@pyreon/reactivity'
195
+ import { Modal } from './Modal'
196
+
197
+ export function App() {
198
+ const open = signal(false)
199
+ const count = signal(0)
200
+ return (
201
+ <Defer when={open}>
202
+ <Modal count={count} onClose={() => open.set(false)} />
203
+ </Defer>
204
+ )
205
+ }
206
+ `
207
+ const result = transformDeferInline(input, 'app.tsx')
208
+ expect(result.changed).toBe(true)
209
+ // The render-prop arrow naturally captures `count` + `open.set` from
210
+ // the App function's scope — no closure-tracking pass needed, JS
211
+ // lexical scope just works.
212
+ expect(result.code).toContain('<__C count={count} onClose={() => open.set(false)} />')
213
+ })
214
+
215
+ test('handles renamed imports — { Modal as M }', () => {
161
216
  const input = `
162
217
  import { Defer } from '@pyreon/core'
163
218
  import { Modal as M } from './Modal'
@@ -166,11 +221,32 @@ export function App() {
166
221
  }
167
222
  `
168
223
  const result = transformDeferInline(input, 'app.tsx')
169
- expect(result.changed).toBe(false)
170
- // Renamed-import case is not yet supported falls through to the
171
- // import-not-found warning (no specifier whose `local.name === 'M'`
172
- // AND `imported.name === local.name` matches).
173
- expect(result.warnings[0]?.code).toBe('defer-inline/import-not-found')
224
+ expect(result.changed).toBe(true)
225
+ // Local name `M` is used at the JSX site, but the chunk extracts
226
+ // `__m.Modal` (the original exported name from './Modal').
227
+ expect(result.code).toContain(`chunk={() => import('./Modal').then((__m) => ({ default: __m.Modal }))}`)
228
+ expect(result.code).not.toContain("import { Modal as M }")
229
+ expect(result.code).toContain('{(__C) => <__C />}')
230
+ })
231
+
232
+ test('multi-specifier import: drops only the Defer-targeted binding', () => {
233
+ const input = `
234
+ import { Defer } from '@pyreon/core'
235
+ import { Modal, OtherThing } from './shared'
236
+ export function App() {
237
+ const use = OtherThing
238
+ return <Defer when={() => true}><Modal /></Defer>
239
+ }
240
+ `
241
+ const result = transformDeferInline(input, 'app.tsx')
242
+ expect(result.changed).toBe(true)
243
+ // OtherThing is referenced elsewhere — its import must survive.
244
+ expect(result.code).toContain("OtherThing")
245
+ expect(result.code).toMatch(/import \{\s*OtherThing\s*\} from '\.\/shared'/)
246
+ // Modal binding is gone from the import declaration.
247
+ expect(result.code).not.toMatch(/import \{[^}]*\bModal\b[^}]*\}/)
248
+ // ...but the dynamic chunk pulls Modal from './shared' (same source).
249
+ expect(result.code).toContain(`chunk={() => import('./shared').then((__m) => ({ default: __m.Modal }))}`)
174
250
  })
175
251
  })
176
252
 
@@ -197,3 +273,115 @@ export function App() {
197
273
  expect(result.code).toContain(`chunk={() => import('./Comments').then((__m) => ({ default: __m.Comments }))}`)
198
274
  })
199
275
  })
276
+
277
+ // Gap 4 from the v2 follow-up roadmap. Namespace imports were the last
278
+ // inline-Defer shape that fell back to the explicit form — closing this
279
+ // gap means EVERY common import shape is supported by the inline form.
280
+ describe('transformDeferInline — namespace imports (v3)', () => {
281
+ test('rewrites <M.Modal /> with namespace import — chunk extracts __m.Modal', () => {
282
+ const input = `
283
+ import { Defer } from '@pyreon/core'
284
+ import * as M from './Modal'
285
+ export function App() {
286
+ return <Defer when={() => true}><M.Modal /></Defer>
287
+ }
288
+ `
289
+ const result = transformDeferInline(input, 'app.tsx')
290
+ expect(result.changed).toBe(true)
291
+ expect(result.code).toContain(`chunk={() => import('./Modal').then((__m) => ({ default: __m.Modal }))}`)
292
+ // M.Modal in the JSX replaced with __C (the whole member expression
293
+ // is the "name" range that gets substituted).
294
+ expect(result.code).toContain('{(__C) => <__C />}')
295
+ expect(result.code).not.toContain('import * as M from')
296
+ })
297
+
298
+ test('rewrites with props on member-expression child', () => {
299
+ const input = `
300
+ import { Defer } from '@pyreon/core'
301
+ import * as M from './Modal'
302
+ export function App() {
303
+ return <Defer when={() => true}><M.Modal title="hi" /></Defer>
304
+ }
305
+ `
306
+ const result = transformDeferInline(input, 'app.tsx')
307
+ expect(result.changed).toBe(true)
308
+ expect(result.code).toContain('{(__C) => <__C title="hi" />}')
309
+ })
310
+
311
+ test('non-self-closing member-expression child preserves opening + closing replacement', () => {
312
+ const input = `
313
+ import { Defer } from '@pyreon/core'
314
+ import * as M from './Modal'
315
+ export function App() {
316
+ return <Defer when={() => true}><M.Modal title="hi"><span>body</span></M.Modal></Defer>
317
+ }
318
+ `
319
+ const result = transformDeferInline(input, 'app.tsx')
320
+ expect(result.changed).toBe(true)
321
+ expect(result.code).toContain('{(__C) => <__C title="hi"><span>body</span></__C>}')
322
+ })
323
+
324
+ test('bails when namespace is referenced elsewhere in the file', () => {
325
+ // `M` is used for multiple components. Removing the static import
326
+ // would break the other usage AND the dynamic import becomes a
327
+ // no-op (Rolldown bundles the module statically when ANY part is
328
+ // referenced).
329
+ const input = `
330
+ import { Defer } from '@pyreon/core'
331
+ import * as M from './Modal'
332
+ export function App() {
333
+ void M.Settings
334
+ return <Defer when={() => true}><M.Modal /></Defer>
335
+ }
336
+ `
337
+ const result = transformDeferInline(input, 'app.tsx')
338
+ expect(result.changed).toBe(false)
339
+ expect(result.warnings).toHaveLength(1)
340
+ expect(result.warnings[0]!.code).toBe('defer-inline/import-used-elsewhere')
341
+ })
342
+
343
+ test('bails on deeper member expression — <M.Sub.X /> not supported', () => {
344
+ const input = `
345
+ import { Defer } from '@pyreon/core'
346
+ import * as M from './Modal'
347
+ export function App() {
348
+ return <Defer when={() => true}><M.Sub.Modal /></Defer>
349
+ }
350
+ `
351
+ const result = transformDeferInline(input, 'app.tsx')
352
+ // analyzeChildElement returns null for non-depth-1 member
353
+ // expressions → no match in findDeferMatches → no warning. The
354
+ // Defer is left alone; runtime errors with "missing chunk".
355
+ expect(result.changed).toBe(false)
356
+ })
357
+
358
+ test('bails when member property is lowercase — <M.helper /> is not a component', () => {
359
+ const input = `
360
+ import { Defer } from '@pyreon/core'
361
+ import * as M from './lib'
362
+ export function App() {
363
+ return <Defer when={() => true}><M.helper /></Defer>
364
+ }
365
+ `
366
+ const result = transformDeferInline(input, 'app.tsx')
367
+ expect(result.changed).toBe(false)
368
+ })
369
+
370
+ test('bails when member expression but import is default (not namespace)', () => {
371
+ // `import M from './X'` (default) followed by `<M.Modal />` is a
372
+ // member access on the default-exported component itself, not a
373
+ // namespace lookup. Different semantics; out of scope. Compiler
374
+ // emits `unsupported-import-shape` so the author knows why.
375
+ const input = `
376
+ import { Defer } from '@pyreon/core'
377
+ import M from './Modal'
378
+ export function App() {
379
+ return <Defer when={() => true}><M.Modal /></Defer>
380
+ }
381
+ `
382
+ const result = transformDeferInline(input, 'app.tsx')
383
+ expect(result.changed).toBe(false)
384
+ expect(result.warnings).toHaveLength(1)
385
+ expect(result.warnings[0]!.code).toBe('defer-inline/unsupported-import-shape')
386
+ })
387
+ })
@@ -30,6 +30,7 @@ const COMPILER_CODES = [
30
30
  'for-missing-by',
31
31
  'for-with-key',
32
32
  'props-destructured',
33
+ 'props-destructured-body',
33
34
  'process-dev-gate',
34
35
  'empty-theme',
35
36
  'raw-add-event-listener',
@@ -40,6 +41,7 @@ const COMPILER_CODES = [
40
41
  'static-return-null-conditional',
41
42
  'as-unknown-as-vnodechild',
42
43
  'island-never-with-registry-entry',
44
+ 'query-options-as-function',
43
45
  ] as const
44
46
  type CompilerCode = (typeof COMPILER_CODES)[number]
45
47
 
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Compiler hardening — Round 9 (REAL bug, FIXED + bisect-verified).
3
+ *
4
+ * const header = <h1>T</h1>
5
+ * return <div>{header}<p>x</p></div>
6
+ *
7
+ * Pre-fix the compiler lowered the const to `_tpl(...)` (so it KNEW `header`
8
+ * was a `NativeItem` element) yet still emitted
9
+ * `document.createTextNode(header)` for the `{header}` child — `createTextNode`
10
+ * string-coerces the NativeItem → "[object Object]" instead of the `<h1>`.
11
+ * Only `props.children` / `own.children` reached the correct `_mountSlot`.
12
+ *
13
+ * Fix (jsx.ts): an `elementVars` set tracks `const`/`let` bindings whose
14
+ * initializer is a JSX element/fragment (optionally parenthesized); a bare
15
+ * `{el}` child of such a binding routes through `_mountSlot` — the same
16
+ * general child-insert `props.children` uses. Tight by construction: only a
17
+ * DIRECT JSX initializer reclassifies, so string/number/prop-derived/inline-
18
+ * hoisted children keep their existing (correct) paths. Routing is safe even
19
+ * under later same-name shadowing — `_mountSlot` renders strings/numbers
20
+ * correctly too; the only cost of imprecision is skipping the text fast path.
21
+ *
22
+ * NOT contradicted by `jsx.test.ts:777` `createTextNode(label)` — that pins
23
+ * the FREE undeclared identifier default (genuinely ambiguous); this fix only
24
+ * fires when the binding's initializer is provably JSX.
25
+ *
26
+ * Bisect: revert the `isElementValuedIdent` clause in `processOneChild`
27
+ * (jsx.ts) → the CONTRACT specs fail (emit reverts to `createTextNode(header)`)
28
+ * while every CONTROL spec stays green (proving the fix doesn't touch the
29
+ * text/reactive fast paths). Restore → all pass.
30
+ */
31
+ import { describe, expect, it } from 'vitest'
32
+ import { transformJSX_JS } from '../jsx'
33
+
34
+ const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
35
+ const ELEMENT_CONST = `function C(){ const header = <h1>T</h1>; return <div>{header}<p>x</p></div> }`
36
+
37
+ describe('Round 9 — element-valued const used as a bare JSX child', () => {
38
+ it('CONTROL: string/number const child still uses the correct text fast path', () => {
39
+ expect(emit(`function C(){ const t = 'T'; return <div>{t}<p>x</p></div> }`)).toContain('createTextNode(t)')
40
+ expect(emit(`function C(){ const n = 5; return <div>{n}</div> }`)).toContain('textContent = n')
41
+ })
42
+
43
+ it('CONTROL: an INLINE element child is correctly hoisted (not text-coerced)', () => {
44
+ const out = emit(`function C(){ return <div>{<h1>T</h1>}<p>x</p></div> }`)
45
+ expect(out).toMatch(/const _\$h\d+ =/)
46
+ expect(out).not.toMatch(/createTextNode\(_\$h\d+\)/)
47
+ })
48
+
49
+ it('CONTRACT: element-valued const child is mounted via _mountSlot, not text-coerced', () => {
50
+ const out = emit(ELEMENT_CONST)
51
+ expect(out).toContain('const header = _tpl("<h1>T</h1>"')
52
+ expect(out).not.toContain('createTextNode(header)')
53
+ expect(out).toMatch(/_mountSlot\(\s*header\b/)
54
+ })
55
+
56
+ it('CONTRACT: single bare element-const child, parenthesized init, and let all mount', () => {
57
+ expect(emit(`function C(){ const el = <span>hi</span>; return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
58
+ expect(emit(`function C(){ const el = (<b>x</b>); return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
59
+ expect(emit(`function C(){ let el = <a/>; return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
60
+ })
61
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Compiler hardening — Round 3 (characterization, NOT a bug fix).
3
+ *
4
+ * Investigated: how the JSX transform emits falsy / boolean / null literal
5
+ * children vs the JSX rendering contract (`true`/`false`/`null`/`undefined`
6
+ * render nothing; `0` renders "0"; `''` renders empty).
7
+ *
8
+ * Finding: the patterns real code actually writes are CORRECT — a conditional
9
+ * (`{c ? x : null}`) or short-circuit (`{c && <X/>}`) child is wrapped in a
10
+ * `() =>` accessor and the null/boolean is filtered by runtime `mountChild`,
11
+ * so nothing renders (Pyreon's documented `VNodeChildAtom` `&&` contract
12
+ * holds). Only a CONTRIVED bare literal child (`<div>{false}</div>` — never
13
+ * written in practice) takes the static path and emits `textContent = false`
14
+ * → the DOM stringifies to "false". This is a spec divergence on input no one
15
+ * writes; fixing it would touch the hot child-emission path for zero
16
+ * real-world benefit, so the behavior is pinned here instead (any future
17
+ * change to it must be deliberate, and this test will flag it).
18
+ */
19
+ import { describe, expect, it } from 'vitest'
20
+ import { transformJSX_JS } from '../jsx'
21
+
22
+ const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
23
+
24
+ describe('Round 3 — conditional/short-circuit children are accessor-wrapped (the contract that matters)', () => {
25
+ it('ternary with a null branch is wrapped in an accessor (runtime filters null)', () => {
26
+ const out = emit(`function C(p){ return <div>{p.cond ? <a/> : null}</div> }`)
27
+ expect(out).toContain('() => p.cond ? <a/> : null')
28
+ expect(out).not.toContain('createTextNode(null)')
29
+ })
30
+
31
+ it('&& short-circuit is wrapped in an accessor (the documented && pattern)', () => {
32
+ const out = emit(`function C(p){ return <div>{p.show && <b/>}</div> }`)
33
+ expect(out).toContain('() => p.show && <b/>')
34
+ expect(out).not.toContain('createTextNode(false)')
35
+ })
36
+ })
37
+
38
+ describe('Round 3 — bare literal falsy children: pinned current behavior (contrived input)', () => {
39
+ it('numeric 0 child renders "0" (JSX-correct)', () => {
40
+ expect(emit(`function C(){ return <div>{0}</div> }`)).toContain('__root.textContent = 0')
41
+ })
42
+
43
+ // Pinned divergence: a bare `{false}` literal stringifies via textContent.
44
+ // Documented, not fixed — see file header for the rationale.
45
+ it('bare {false} literal takes the static textContent path (known, contrived)', () => {
46
+ expect(emit(`function C(){ return <div>{false}</div> }`)).toContain('__root.textContent = false')
47
+ })
48
+ })
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Compiler hardening — Round 10 (resilience gate; no bug found).
3
+ *
4
+ * `transformJSX` runs per-file inside the Vite dev server; a throw on
5
+ * malformed input crashes the dev server (the documented contract is "a Rust
6
+ * panic / parse error must not crash Vite — fall back gracefully"). Probed 15
7
+ * adversarial inputs (unclosed/mismatched tags, stray brace, invalid attr,
8
+ * unterminated string, 500-deep nesting, BOM, raw control bytes, empty,
9
+ * comment-only, JSX in type position) through BOTH backends — all returned a
10
+ * `{ code: string }` result without throwing. This locks that resilience so a
11
+ * future change can't regress the compiler into throwing on bad input.
12
+ */
13
+ import { describe, expect, it } from 'vitest'
14
+ import { transformJSX, transformJSX_JS } from '../jsx'
15
+
16
+ const INPUTS: Array<[string, string]> = [
17
+ ['unclosed-tag', `function C(){ return <div>oops }`],
18
+ ['mismatched-tags', `function C(){ return <div></span> }`],
19
+ ['invalid-attr', `function C(){ return <div class=></div> }`],
20
+ ['stray-brace', `function C(){ return <div>{</div> }`],
21
+ ['empty', ``],
22
+ ['whitespace-only', ` \n `],
23
+ ['non-jsx-ts', `const x: number = 1; export function f(){ return x }`],
24
+ ['deeply-unbalanced', `function C(){ return <a><b><c></a> }`],
25
+ ['unterminated-string-attr', `function C(){ return <div title="abc>x</div> }`],
26
+ ['huge-nesting-500', `function C(){ return ${'<a>'.repeat(500)}x${'</a>'.repeat(500)} }`],
27
+ ['bom-prefixed', `function C(){ return <div>ok</div> }`],
28
+ ['comment-only', `// just a comment`],
29
+ ['fragment-unclosed', `function C(){ return <>x }`],
30
+ ['raw-control-garbage', String.fromCharCode(0, 1) + ' not code <div'],
31
+ ]
32
+
33
+ describe('Round 10 — transform never throws on malformed input (Vite-dev-server resilience)', () => {
34
+ for (const [name, src] of INPUTS) {
35
+ it(`JS backend tolerates: ${name}`, () => {
36
+ let res: { code?: unknown } | undefined
37
+ expect(() => {
38
+ res = transformJSX_JS(src, 'c.tsx')
39
+ }).not.toThrow()
40
+ expect(typeof res?.code).toBe('string')
41
+ })
42
+ it(`native backend tolerates: ${name}`, () => {
43
+ let res: { code?: unknown } | undefined
44
+ expect(() => {
45
+ res = transformJSX(src, 'c.tsx')
46
+ }).not.toThrow()
47
+ expect(typeof res?.code).toBe('string')
48
+ })
49
+ }
50
+ })
@@ -0,0 +1,55 @@
1
+ import {
2
+ renderApiReferenceEntries,
3
+ renderLlmsFullSection,
4
+ renderLlmsTxtLine,
5
+ } from '@pyreon/manifest'
6
+ import manifest from '../manifest'
7
+
8
+ describe('gen-docs — compiler snapshot', () => {
9
+ it('renders a llms.txt bullet starting with the package prefix', () => {
10
+ const line = renderLlmsTxtLine(manifest)
11
+ expect(line.startsWith('- @pyreon/compiler —')).toBe(true)
12
+ })
13
+
14
+ it('renders a llms-full.txt section with the right header', () => {
15
+ const section = renderLlmsFullSection(manifest)
16
+ expect(section.startsWith('## @pyreon/compiler —')).toBe(true)
17
+ expect(section).toContain('```typescript')
18
+ })
19
+
20
+ it('renders MCP api-reference entries for every api[] item', () => {
21
+ const record = renderApiReferenceEntries(manifest)
22
+ expect(Object.keys(record).sort()).toEqual([
23
+ 'compiler/analyzeReactivity',
24
+ 'compiler/auditIslands',
25
+ 'compiler/auditSsg',
26
+ 'compiler/auditTestEnvironment',
27
+ 'compiler/detectPyreonPatterns',
28
+ 'compiler/detectReactPatterns',
29
+ 'compiler/diagnoseError',
30
+ 'compiler/formatIslandAudit',
31
+ 'compiler/formatReactivityLens',
32
+ 'compiler/formatSsgAudit',
33
+ 'compiler/formatTestAudit',
34
+ 'compiler/generateContext',
35
+ 'compiler/hasPyreonPatterns',
36
+ 'compiler/hasReactPatterns',
37
+ 'compiler/migrateReactCode',
38
+ 'compiler/transformDeferInline',
39
+ 'compiler/transformJSX',
40
+ 'compiler/transformJSX_JS',
41
+ ])
42
+ })
43
+
44
+ it('flags the experimental Reactivity-Lens entries', () => {
45
+ const r = renderApiReferenceEntries(manifest)
46
+ expect(r['compiler/analyzeReactivity']?.notes).toContain('[EXPERIMENTAL]')
47
+ expect(r['compiler/formatReactivityLens']?.notes).toContain('[EXPERIMENTAL]')
48
+ })
49
+
50
+ it('carries the foot-gun catalog into MCP mistakes for flagship APIs', () => {
51
+ const r = renderApiReferenceEntries(manifest)
52
+ expect(r['compiler/transformJSX']?.mistakes).toBeTruthy()
53
+ expect(r['compiler/detectPyreonPatterns']?.mistakes).toContain('fixable')
54
+ })
55
+ })