@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.
- package/package.json +11 -13
- package/src/defer-inline.ts +0 -686
- package/src/event-names.ts +0 -65
- package/src/index.ts +0 -61
- package/src/island-audit.ts +0 -675
- package/src/jsx.ts +0 -2792
- package/src/load-native.ts +0 -156
- package/src/lpih.ts +0 -270
- package/src/manifest.ts +0 -280
- package/src/project-scanner.ts +0 -214
- package/src/pyreon-intercept.ts +0 -1029
- package/src/react-intercept.ts +0 -1217
- package/src/reactivity-lens.ts +0 -190
- package/src/ssg-audit.ts +0 -513
- package/src/test-audit.ts +0 -435
- package/src/tests/backend-parity-r7-r9.test.ts +0 -91
- package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
- package/src/tests/collapse-bail-census.test.ts +0 -330
- package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
- package/src/tests/component-child-no-wrap.test.ts +0 -204
- package/src/tests/defer-inline.test.ts +0 -387
- package/src/tests/depth-stress.test.ts +0 -16
- package/src/tests/detector-tag-consistency.test.ts +0 -101
- package/src/tests/dynamic-collapse-detector.test.ts +0 -164
- package/src/tests/dynamic-collapse-emit.test.ts +0 -192
- package/src/tests/dynamic-collapse-scan.test.ts +0 -111
- package/src/tests/element-valued-const-child.test.ts +0 -61
- package/src/tests/falsy-child-characterization.test.ts +0 -48
- package/src/tests/island-audit.test.ts +0 -524
- package/src/tests/jsx.test.ts +0 -2908
- package/src/tests/load-native.test.ts +0 -53
- package/src/tests/lpih.test.ts +0 -404
- package/src/tests/malformed-input-resilience.test.ts +0 -50
- package/src/tests/manifest-snapshot.test.ts +0 -55
- package/src/tests/native-equivalence.test.ts +0 -924
- package/src/tests/partial-collapse-detector.test.ts +0 -121
- package/src/tests/partial-collapse-emit.test.ts +0 -104
- package/src/tests/partial-collapse-robustness.test.ts +0 -53
- package/src/tests/project-scanner.test.ts +0 -269
- package/src/tests/prop-derived-shadow.test.ts +0 -96
- package/src/tests/pure-call-reactive-args.test.ts +0 -50
- package/src/tests/pyreon-intercept.test.ts +0 -816
- package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
- package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
- package/src/tests/r15-elemconst-propderived.test.ts +0 -47
- package/src/tests/r19-defer-inline-robust.test.ts +0 -54
- package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
- package/src/tests/react-intercept.test.ts +0 -1104
- package/src/tests/reactivity-lens.test.ts +0 -170
- package/src/tests/rocketstyle-collapse.test.ts +0 -208
- package/src/tests/runtime/control-flow.test.ts +0 -159
- package/src/tests/runtime/dom-properties.test.ts +0 -138
- package/src/tests/runtime/events.test.ts +0 -301
- package/src/tests/runtime/harness.ts +0 -94
- package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
- package/src/tests/runtime/reactive-props.test.ts +0 -81
- package/src/tests/runtime/signals.test.ts +0 -129
- package/src/tests/runtime/whitespace.test.ts +0 -106
- package/src/tests/signal-autocall-shadow.test.ts +0 -86
- package/src/tests/sourcemap-fidelity.test.ts +0 -77
- package/src/tests/ssg-audit.test.ts +0 -402
- package/src/tests/static-text-baking.test.ts +0 -64
- package/src/tests/test-audit.test.ts +0 -549
- 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
|
-
})
|