@pyreon/compiler 0.24.5 → 0.24.6

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 (64) hide show
  1. package/package.json +11 -13
  2. package/src/defer-inline.ts +0 -686
  3. package/src/event-names.ts +0 -65
  4. package/src/index.ts +0 -61
  5. package/src/island-audit.ts +0 -675
  6. package/src/jsx.ts +0 -2792
  7. package/src/load-native.ts +0 -156
  8. package/src/lpih.ts +0 -270
  9. package/src/manifest.ts +0 -280
  10. package/src/project-scanner.ts +0 -214
  11. package/src/pyreon-intercept.ts +0 -1029
  12. package/src/react-intercept.ts +0 -1217
  13. package/src/reactivity-lens.ts +0 -190
  14. package/src/ssg-audit.ts +0 -513
  15. package/src/test-audit.ts +0 -435
  16. package/src/tests/backend-parity-r7-r9.test.ts +0 -91
  17. package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
  18. package/src/tests/collapse-bail-census.test.ts +0 -330
  19. package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
  20. package/src/tests/component-child-no-wrap.test.ts +0 -204
  21. package/src/tests/defer-inline.test.ts +0 -387
  22. package/src/tests/depth-stress.test.ts +0 -16
  23. package/src/tests/detector-tag-consistency.test.ts +0 -101
  24. package/src/tests/dynamic-collapse-detector.test.ts +0 -164
  25. package/src/tests/dynamic-collapse-emit.test.ts +0 -192
  26. package/src/tests/dynamic-collapse-scan.test.ts +0 -111
  27. package/src/tests/element-valued-const-child.test.ts +0 -61
  28. package/src/tests/falsy-child-characterization.test.ts +0 -48
  29. package/src/tests/island-audit.test.ts +0 -524
  30. package/src/tests/jsx.test.ts +0 -2908
  31. package/src/tests/load-native.test.ts +0 -53
  32. package/src/tests/lpih.test.ts +0 -404
  33. package/src/tests/malformed-input-resilience.test.ts +0 -50
  34. package/src/tests/manifest-snapshot.test.ts +0 -55
  35. package/src/tests/native-equivalence.test.ts +0 -924
  36. package/src/tests/partial-collapse-detector.test.ts +0 -121
  37. package/src/tests/partial-collapse-emit.test.ts +0 -104
  38. package/src/tests/partial-collapse-robustness.test.ts +0 -53
  39. package/src/tests/project-scanner.test.ts +0 -269
  40. package/src/tests/prop-derived-shadow.test.ts +0 -96
  41. package/src/tests/pure-call-reactive-args.test.ts +0 -50
  42. package/src/tests/pyreon-intercept.test.ts +0 -816
  43. package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
  44. package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
  45. package/src/tests/r15-elemconst-propderived.test.ts +0 -47
  46. package/src/tests/r19-defer-inline-robust.test.ts +0 -54
  47. package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
  48. package/src/tests/react-intercept.test.ts +0 -1104
  49. package/src/tests/reactivity-lens.test.ts +0 -170
  50. package/src/tests/rocketstyle-collapse.test.ts +0 -208
  51. package/src/tests/runtime/control-flow.test.ts +0 -159
  52. package/src/tests/runtime/dom-properties.test.ts +0 -138
  53. package/src/tests/runtime/events.test.ts +0 -301
  54. package/src/tests/runtime/harness.ts +0 -94
  55. package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
  56. package/src/tests/runtime/reactive-props.test.ts +0 -81
  57. package/src/tests/runtime/signals.test.ts +0 -129
  58. package/src/tests/runtime/whitespace.test.ts +0 -106
  59. package/src/tests/signal-autocall-shadow.test.ts +0 -86
  60. package/src/tests/sourcemap-fidelity.test.ts +0 -77
  61. package/src/tests/ssg-audit.test.ts +0 -402
  62. package/src/tests/static-text-baking.test.ts +0 -64
  63. package/src/tests/test-audit.test.ts +0 -549
  64. package/src/tests/transform-state-isolation.test.ts +0 -49
@@ -1,387 +0,0 @@
1
- import { transformDeferInline } from '../defer-inline'
2
-
3
- describe('transformDeferInline — basic rewrites', () => {
4
- test('rewrites <Defer when={x}><Modal /></Defer> with named import', () => {
5
- const input = `
6
- import { Defer } from '@pyreon/core'
7
- import { Modal } from './Modal'
8
-
9
- export function App() {
10
- const open = () => true
11
- return <Defer when={open}><Modal /></Defer>
12
- }
13
- `
14
- const result = transformDeferInline(input, 'app.tsx')
15
- expect(result.changed).toBe(true)
16
- expect(result.code).not.toContain("import { Modal } from './Modal'")
17
- expect(result.code).toContain(`chunk={() => import('./Modal').then((__m) => ({ default: __m.Modal }))}`)
18
- expect(result.code).toContain('{(__C) => <__C />}')
19
- })
20
-
21
- test('rewrites with default import', () => {
22
- const input = `
23
- import { Defer } from '@pyreon/core'
24
- import Modal from './Modal'
25
-
26
- export function App() {
27
- return <Defer when={() => true}><Modal /></Defer>
28
- }
29
- `
30
- const result = transformDeferInline(input, 'app.tsx')
31
- expect(result.changed).toBe(true)
32
- expect(result.code).not.toContain('import Modal from')
33
- expect(result.code).toContain(`chunk={() => import('./Modal')}`)
34
- expect(result.code).not.toContain(`.then((__m) =>`)
35
- })
36
-
37
- test('preserves other props on Defer (fallback, when, on)', () => {
38
- const input = `
39
- import { Defer } from '@pyreon/core'
40
- import { Modal } from './Modal'
41
- export function App() {
42
- return <Defer when={() => true} fallback={<span>loading</span>}><Modal /></Defer>
43
- }
44
- `
45
- const result = transformDeferInline(input, 'app.tsx')
46
- expect(result.changed).toBe(true)
47
- expect(result.code).toContain('when={() => true}')
48
- expect(result.code).toContain('fallback={<span>loading</span>}')
49
- })
50
-
51
- test('works for on="visible" trigger', () => {
52
- const input = `
53
- import { Defer } from '@pyreon/core'
54
- import { Comments } from './Comments'
55
- export function Post() {
56
- return <Defer on="visible"><Comments /></Defer>
57
- }
58
- `
59
- const result = transformDeferInline(input, 'post.tsx')
60
- expect(result.changed).toBe(true)
61
- expect(result.code).toContain('on="visible"')
62
- expect(result.code).toContain(`chunk={() => import('./Comments').then((__m) => ({ default: __m.Comments }))}`)
63
- })
64
- })
65
-
66
- describe('transformDeferInline — bail-out cases', () => {
67
- test('leaves unchanged when chunk prop is already provided', () => {
68
- const input = `
69
- import { Defer } from '@pyreon/core'
70
- import { Modal } from './Modal'
71
- export function App() {
72
- return (
73
- <Defer chunk={() => import('./Modal')} when={() => true}>
74
- {Modal => <Modal />}
75
- </Defer>
76
- )
77
- }
78
- `
79
- const result = transformDeferInline(input, 'app.tsx')
80
- expect(result.changed).toBe(false)
81
- expect(result.code).toBe(input)
82
- expect(result.warnings).toEqual([])
83
- })
84
-
85
- test('warns when inline child is also used outside the Defer', () => {
86
- const input = `
87
- import { Defer } from '@pyreon/core'
88
- import { Modal } from './Modal'
89
- const eagerCopy = <Modal />
90
- export function App() {
91
- return <Defer when={() => true}><Modal /></Defer>
92
- }
93
- `
94
- const result = transformDeferInline(input, 'app.tsx')
95
- expect(result.changed).toBe(false)
96
- expect(result.warnings).toHaveLength(1)
97
- expect(result.warnings[0]!.code).toBe('defer-inline/import-used-elsewhere')
98
- })
99
-
100
- test('warns when inline child is not imported', () => {
101
- const input = `
102
- import { Defer } from '@pyreon/core'
103
- export function App() {
104
- return <Defer when={() => true}><LocalThing /></Defer>
105
- }
106
- function LocalThing() { return null }
107
- `
108
- const result = transformDeferInline(input, 'app.tsx')
109
- expect(result.changed).toBe(false)
110
- expect(result.warnings).toHaveLength(1)
111
- expect(result.warnings[0]!.code).toBe('defer-inline/import-not-found')
112
- })
113
-
114
- test('skips Defer with multiple children (still requires render-prop form)', () => {
115
- const input = `
116
- import { Defer } from '@pyreon/core'
117
- import { Modal } from './Modal'
118
- import { Spinner } from './Spinner'
119
- export function App() {
120
- return <Defer when={() => true}><Modal /><Spinner /></Defer>
121
- }
122
- `
123
- 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 silent — that was a footgun.
126
- expect(result.changed).toBe(false)
127
- expect(result.warnings[0]?.code).toBe('defer-inline/multiple-children')
128
- })
129
-
130
- test('fast-path: no Defer in source returns unchanged', () => {
131
- const input = `
132
- import { signal } from '@pyreon/reactivity'
133
- export const count = signal(0)
134
- `
135
- const result = transformDeferInline(input, 'count.ts')
136
- expect(result.changed).toBe(false)
137
- expect(result.code).toBe(input)
138
- })
139
-
140
- test('does not blow up on syntactically-invalid source — returns unchanged', () => {
141
- const input = `import {{{ Defer broken syntax`
142
- const result = transformDeferInline(input, 'broken.tsx')
143
- expect(result.changed).toBe(false)
144
- // Returns the input unchanged; downstream parser will surface the real error.
145
- expect(result.code).toBe(input)
146
- })
147
-
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 }', () => {
216
- const input = `
217
- import { Defer } from '@pyreon/core'
218
- import { Modal as M } from './Modal'
219
- export function App() {
220
- return <Defer when={() => true}><M /></Defer>
221
- }
222
- `
223
- const result = transformDeferInline(input, 'app.tsx')
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 }))}`)
250
- })
251
- })
252
-
253
- describe('transformDeferInline — multiple Defers in one file', () => {
254
- test('rewrites two independent Defers with distinct imports', () => {
255
- const input = `
256
- import { Defer } from '@pyreon/core'
257
- import { Modal } from './Modal'
258
- import { Comments } from './Comments'
259
- export function App() {
260
- return (
261
- <div>
262
- <Defer when={() => true}><Modal /></Defer>
263
- <Defer on="visible"><Comments /></Defer>
264
- </div>
265
- )
266
- }
267
- `
268
- const result = transformDeferInline(input, 'app.tsx')
269
- expect(result.changed).toBe(true)
270
- expect(result.code).not.toContain("import { Modal } from './Modal'")
271
- expect(result.code).not.toContain("import { Comments } from './Comments'")
272
- expect(result.code).toContain(`chunk={() => import('./Modal').then((__m) => ({ default: __m.Modal }))}`)
273
- expect(result.code).toContain(`chunk={() => import('./Comments').then((__m) => ({ default: __m.Comments }))}`)
274
- })
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
- })
@@ -1,16 +0,0 @@
1
- import { transformJSX } from '../jsx'
2
-
3
- const t = (code: string) => transformJSX(code, 'input.tsx').code
4
-
5
- describe('T0.2 — chain depth stress test', () => {
6
- for (const depth of [4, 5, 10, 20, 50]) {
7
- test(`depth ${depth} chain compiles without crashing`, () => {
8
- const lines = ['const v0 = props.x']
9
- for (let i = 1; i <= depth; i++) lines.push(`const v${i} = v${i - 1} + 1`)
10
- const code = `function Comp(props) { ${lines.join('; ')}; return <div>{v${depth}}</div> }`
11
- const result = t(code)
12
- expect(result).toContain('props.x')
13
- expect(result).toContain('_bind')
14
- })
15
- }
16
- })
@@ -1,101 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { dirname, resolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- // Drift guard between Pyreon's static detectors (compiler + lint) and the
6
- // `[detector: <code>]` annotations on `.claude/rules/anti-patterns.md`.
7
- // Without this test, a new bullet can land without a detector tag, or
8
- // a detector code can be renamed without updating the doc. Either
9
- // direction is a silent inconsistency — consumers read the doc and
10
- // expect the detector to back it up.
11
- //
12
- // The test does one thing: every `[detector: CODE]` tag in the doc
13
- // must reference a known detector (compiler PyreonDiagnosticCode OR
14
- // @pyreon/lint rule ID without the `pyreon/` prefix), and every
15
- // compiler PyreonDiagnosticCode must appear at least once in the doc
16
- // so the tag-documentation loop is closed.
17
- //
18
- // Lint rules are NOT required to appear in anti-patterns.md (some are
19
- // stylistic, not anti-pattern shaped). When they DO appear with a
20
- // `[detector:]` tag, the tag must match the rule ID's local part.
21
-
22
- const HERE = dirname(fileURLToPath(import.meta.url))
23
- const REPO_ROOT = resolve(HERE, '../../../../../')
24
- const ANTI_PATTERNS_PATH = resolve(REPO_ROOT, '.claude/rules/anti-patterns.md')
25
-
26
- // Kept in sync with the `PyreonDiagnosticCode` union in
27
- // `pyreon-intercept.ts`. When adding a new code, ALSO add a bullet
28
- // (with the `[detector: <code>]` tag) to `anti-patterns.md`.
29
- const COMPILER_CODES = [
30
- 'for-missing-by',
31
- 'for-with-key',
32
- 'props-destructured',
33
- 'props-destructured-body',
34
- 'process-dev-gate',
35
- 'empty-theme',
36
- 'raw-add-event-listener',
37
- 'raw-remove-event-listener',
38
- 'date-math-random-id',
39
- 'on-click-undefined',
40
- 'signal-write-as-call',
41
- 'static-return-null-conditional',
42
- 'as-unknown-as-vnodechild',
43
- 'island-never-with-registry-entry',
44
- 'query-options-as-function',
45
- ] as const
46
- type CompilerCode = (typeof COMPILER_CODES)[number]
47
-
48
- // `@pyreon/lint` rule IDs that may appear as `[detector:]` tags. Listed
49
- // WITHOUT the `pyreon/` prefix (the tag convention strips it for
50
- // readability). Add the rule ID here when documenting a new lint rule
51
- // in anti-patterns.md.
52
- const LINT_RULE_DETECTORS = [
53
- 'storage-signal-v-forwarding',
54
- ] as const
55
-
56
- function readAntiPatterns(): string {
57
- return readFileSync(ANTI_PATTERNS_PATH, 'utf8')
58
- }
59
-
60
- function extractDetectorTags(doc: string): string[] {
61
- const re = /\[detector:\s*([a-z-/ ]+?)\]/gi
62
- const found: string[] = []
63
- for (const m of doc.matchAll(re)) {
64
- // Some bullets document multiple codes on one pattern, e.g.
65
- // `[detector: raw-add-event-listener / raw-remove-event-listener]`.
66
- const raw = m[1]!
67
- for (const code of raw.split('/')) {
68
- const trimmed = code.trim()
69
- if (trimmed) found.push(trimmed)
70
- }
71
- }
72
- return found
73
- }
74
-
75
- describe('anti-patterns.md detector tags vs static detectors', () => {
76
- const doc = readAntiPatterns()
77
- const tags = extractDetectorTags(doc)
78
-
79
- it('every [detector: CODE] tag references a known detector (compiler or lint)', () => {
80
- const validCodes = new Set<string>([...COMPILER_CODES, ...LINT_RULE_DETECTORS])
81
- const unknown = tags.filter((t) => !validCodes.has(t) && t !== 'N/A')
82
- expect(unknown).toEqual([])
83
- })
84
-
85
- it('every PyreonDiagnosticCode appears at least once as a [detector:] tag', () => {
86
- const tagSet = new Set(tags)
87
- const missing: CompilerCode[] = []
88
- for (const code of COMPILER_CODES) {
89
- if (!tagSet.has(code)) missing.push(code)
90
- }
91
- // If this fails, add a bullet for the new detector code to
92
- // `.claude/rules/anti-patterns.md` with the `[detector: <code>]`
93
- // suffix. The doc is the human-readable catalog; the detector is
94
- // the static enforcement arm — they have to name each other.
95
- expect(missing).toEqual([])
96
- })
97
-
98
- it('reports at least as many tags as compiler detector codes (multi-code bullets allowed)', () => {
99
- expect(tags.length).toBeGreaterThanOrEqual(COMPILER_CODES.length)
100
- })
101
- })
@@ -1,164 +0,0 @@
1
- /**
2
- * PR 2 of the dynamic-prop partial-collapse build (`.claude/plans/open-work-2026-q3.md`
3
- * → #1 dynamic-prop bucket = 15.3% of all real-corpus sites; the
4
- * next-bigger bite after the just-shipped `on*`-handler partial-collapse
5
- * via `detectPartialCollapsibleShape` + `_rsCollapseH` + emit).
6
- *
7
- * Mirrors `detectPartialCollapsibleShape`'s "extend the bail catalogue
8
- * with ONE relaxation" pattern (see that detector's docstring + tests).
9
- * The single relaxation: a `JSXExpressionContainer` whose expression is
10
- * a `ConditionalExpression` with BOTH branches being `StringLiteral` is
11
- * acceptable as a "ternary-of-two-literals" dynamic prop. Captured as a
12
- * `DynamicCollapsibleProp` with cond source span + the two literal
13
- * values, so PR 3's resolver can pre-render BOTH values via the
14
- * existing SSR pipeline and PR 3's emit can dispatch via the cond.
15
- *
16
- * Contract under test:
17
- *
18
- * - literal-prop + ONE ternary-of-two-literals + optional `on*` handlers
19
- * + static-text children → { props, dynamicProp, handlers, childrenText }
20
- * - ZERO ternaries (literal-only) → null (defers to full / on*-only paths)
21
- * - 2+ ternaries → null (multi-axis combinatorics is separable scope)
22
- * - ternary with ANY non-literal branch (template literal, identifier,
23
- * non-string literal, computed expr) → null
24
- * - spread / boolean attr / element child / expression child → null
25
- * - cond span (`condStart`/`condEnd`) slices the EXACT source of the
26
- * ternary's test expression (load-bearing for PR 3's emit, which
27
- * re-emits `code.slice(condStart, condEnd)` into `_rsCollapseDyn`)
28
- *
29
- * Bisect-verify (documented in the PR body): replace the body of
30
- * `detectDynamicCollapsibleShape` with `return null` → the POSITIVE
31
- * specs fail with `expected null to be …`; the NEGATIVE specs still
32
- * pass. Restore → all pass. That asymmetry proves the positive
33
- * assertions are load-bearing on the ternary-relaxation logic.
34
- */
35
- import { describe, expect, it } from 'vitest'
36
- import { parseSync } from 'oxc-parser'
37
- import { detectDynamicCollapsibleShape } from '../jsx'
38
-
39
- function firstJsxElement(code: string): any {
40
- const { program } = parseSync('input.tsx', code, { sourceType: 'module', lang: 'tsx' })
41
- let found: any = null
42
- const visit = (node: any): void => {
43
- if (found || !node || typeof node !== 'object') return
44
- if (node.type === 'JSXElement') {
45
- found = node
46
- return
47
- }
48
- for (const k in node) {
49
- const v = node[k]
50
- if (Array.isArray(v)) for (const c of v) visit(c)
51
- else if (v && typeof v === 'object' && typeof v.type === 'string') visit(v)
52
- }
53
- }
54
- visit(program)
55
- return found
56
- }
57
-
58
- const detect = (code: string) => detectDynamicCollapsibleShape(firstJsxElement(code), 'Button')
59
-
60
- describe('detectDynamicCollapsibleShape — PR 2 (ternary-of-two-literals dynamic-prop subset)', () => {
61
- // ── POSITIVE: the dynamic-collapsible subset ────────────────────────────
62
- it('claims a literal-prop site with ONE ternary-of-two-literals', () => {
63
- const code = 'const x = <Button state={cond ? "primary" : "secondary"} size="medium">Save</Button>'
64
- const r = detect(code)
65
- expect(r).not.toBeNull()
66
- expect(r!.props).toEqual({ size: 'medium' })
67
- expect(r!.childrenText).toBe('Save')
68
- expect(r!.handlers).toEqual([])
69
- expect(r!.dynamicProp.name).toBe('state')
70
- expect(r!.dynamicProp.valueTruthy).toBe('primary')
71
- expect(r!.dynamicProp.valueFalsy).toBe('secondary')
72
- expect(code.slice(r!.dynamicProp.condStart, r!.dynamicProp.condEnd)).toBe('cond')
73
- })
74
-
75
- it('captures the EXACT cond span for a complex condition', () => {
76
- // The condStart/condEnd MUST slice the original source of the test
77
- // expression so PR 3's emit can re-thread it into the dispatcher
78
- // verbatim (paren-wrapped to keep it a single expr like the
79
- // on*-handler emit does for arrow bodies).
80
- const code = 'const x = <Btn state={user.role === "admin" ? "primary" : "danger"}>Go</Btn>'
81
- const r = detect(code)
82
- expect(r).not.toBeNull()
83
- expect(code.slice(r!.dynamicProp.condStart, r!.dynamicProp.condEnd)).toBe(
84
- 'user.role === "admin"',
85
- )
86
- })
87
-
88
- it('composes with on*-handler relaxation — one ternary + one handler', () => {
89
- // Real-corpus shape: a Button with state={cond ? ... : ...} almost
90
- // always also has an onClick. PR 3's emit will route to a combined
91
- // helper when handlers are non-empty; this PR (detector) just
92
- // carries both for the dispatcher's sake.
93
- const code =
94
- 'const x = <Button state={cond ? "primary" : "secondary"} onClick={go}>Save</Button>'
95
- const r = detect(code)
96
- expect(r).not.toBeNull()
97
- expect(r!.dynamicProp.name).toBe('state')
98
- expect(r!.handlers.map((h) => h.name)).toEqual(['onClick'])
99
- })
100
-
101
- it('handles ternary on a non-state dim prop (size, variant, …)', () => {
102
- const code = 'const x = <Button size={isLarge ? "large" : "medium"}>S</Button>'
103
- const r = detect(code)
104
- expect(r).not.toBeNull()
105
- expect(r!.dynamicProp.name).toBe('size')
106
- expect(r!.dynamicProp.valueTruthy).toBe('large')
107
- expect(r!.dynamicProp.valueFalsy).toBe('medium')
108
- })
109
-
110
- it('trims static-text children (parity with the rest of the family)', () => {
111
- const code =
112
- 'const x = <Button state={c ? "a" : "b"}>\n Save\n</Button>'
113
- const r = detect(code)
114
- expect(r).not.toBeNull()
115
- expect(r!.childrenText).toBe('Save')
116
- })
117
-
118
- // ── NEGATIVE: every uncertain shape bails (null) ────────────────────────
119
- it('returns null for ZERO ternaries (defers to full / on*-only paths)', () => {
120
- // Load-bearing separation: a fully-literal site is the FULL-collapse
121
- // shape; on*-handler-only is the partial-collapse shape. The
122
- // dynamic-prop detector must NOT claim either, so the three
123
- // detectors never both/all-three fire on one site.
124
- expect(detect('const x = <Button state="primary">Save</Button>')).toBeNull()
125
- expect(detect('const x = <Button state="primary" onClick={h}>Save</Button>')).toBeNull()
126
- })
127
-
128
- it('returns null for 2+ ternaries (multi-axis combinatorics is separable scope)', () => {
129
- const code =
130
- 'const x = <Button state={a ? "x" : "y"} size={b ? "small" : "large"}>S</Button>'
131
- expect(detect(code)).toBeNull()
132
- })
133
-
134
- it('returns null for a ternary with a NON-string-literal branch', () => {
135
- // TemplateLiteral, Identifier, numeric/boolean literal — none of
136
- // these are statically resolvable to a known dimension value.
137
- expect(detect('const x = <Button state={c ? `pri` : "sec"}>S</Button>')).toBeNull()
138
- expect(detect('const x = <Button state={c ? maybePrimary : "sec"}>S</Button>')).toBeNull()
139
- expect(detect('const x = <Button state={c ? "pri" : 1}>S</Button>')).toBeNull()
140
- })
141
-
142
- it('returns null for a non-ternary dynamic prop alongside literals', () => {
143
- // Signal-call / function-call / arbitrary expression — not statically
144
- // enumerable, no resolution possible.
145
- expect(detect('const x = <Button state={getMy()} size="medium">S</Button>')).toBeNull()
146
- expect(detect('const x = <Button state={sig()} size="medium">S</Button>')).toBeNull()
147
- })
148
-
149
- it('returns null for a spread attribute', () => {
150
- expect(detect('const x = <Button {...rest} state={c ? "a" : "b"}>X</Button>')).toBeNull()
151
- })
152
-
153
- it('returns null for a boolean attribute', () => {
154
- expect(detect('const x = <Button disabled state={c ? "a" : "b"}>X</Button>')).toBeNull()
155
- })
156
-
157
- it('returns null for an element child', () => {
158
- expect(detect('const x = <Button state={c ? "a" : "b"}><span /></Button>')).toBeNull()
159
- })
160
-
161
- it('returns null for an expression child', () => {
162
- expect(detect('const x = <Button state={c ? "a" : "b"}>{label}</Button>')).toBeNull()
163
- })
164
- })