@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,330 +0,0 @@
1
- /**
2
- * Proposal #1 (collapse tail / partial collapse) — FIRST MEASURABLE STEP.
3
- *
4
- * The open-work doc commits: "instrument `scanCollapsibleSites` bail reasons
5
- * on the real `examples/ui-showcase` + `@pyreon/ui-components` corpus and
6
- * bucket by bail cause — that quantifies the partial-collapse addressable
7
- * surface before any code is written (mirrors the E2 '95.3% statically
8
- * resolvable' measurement that justified the slice)."
9
- *
10
- * This test IS that measurement, executed and locked. It does NOT build
11
- * partial collapse (multi-week, roadmap-scale). It produces the number that
12
- * tells whoever picks #1 up whether partial collapse is worth the spend.
13
- *
14
- * Methodology — every JSX element across the example corpus whose tag is
15
- * PascalCase AND imported from `@pyreon/ui-components` is a *candidate*. Each
16
- * candidate is bucketed by its FIRST bail reason (same catalogue order as
17
- * the production `detectCollapsibleShape`):
18
- *
19
- * collapsible — no bail; the shipped slice already collapses it
20
- * spread — a `{...x}` attribute
21
- * boolean-attr — a valueless attr (`disabled`)
22
- * dynamic-prop — an `{expr}`-valued attr (incl. `onClick={...}`)
23
- * element-child — a JSX element child
24
- * expression-child — a `{expr}` child
25
- *
26
- * The trustworthiness gate (the bisect-equivalent — no fake fix to revert):
27
- * this file's own "collapsible" count, computed by an INDEPENDENT walk, is
28
- * asserted EQUAL to the production `scanCollapsibleSites` truth-set over the
29
- * same files. If the two ever disagree, the census is not measuring what the
30
- * compiler actually collapses and the number is worthless — the test fails
31
- * and says so. So the measurement can't silently rot.
32
- *
33
- * Partial-collapse addressable surface — among `dynamic-prop` bails,
34
- * how many bail SOLELY because of `on*` handler props while EVERY other
35
- * attr is a plain string literal and children are static text. Those are
36
- * exactly the sites a "collapse the static dimension slice, keep the
37
- * handler runtime" pass would capture. That ratio is the headline number
38
- * for the #1 go/no-go decision.
39
- */
40
- import { readdirSync, readFileSync, statSync } from 'node:fs'
41
- import { join } from 'node:path'
42
- import { describe, expect, it } from 'vitest'
43
- import { parseSync } from 'oxc-parser'
44
- import { scanCollapsibleSites } from '../jsx'
45
-
46
- const COLLAPSIBLE_SOURCES = new Set(['@pyreon/ui-components'])
47
-
48
- // `bun run test` sets cwd to the package dir (packages/core/compiler);
49
- // repo root is 3 up. Robust to bundler __dirname rewriting.
50
- const REPO = join(process.cwd(), '..', '..', '..')
51
- const CORPUS = [
52
- 'examples/ui-showcase/src',
53
- 'examples/app-showcase/src',
54
- 'examples/fundamentals-playground/src',
55
- ].map((p) => join(REPO, p))
56
-
57
- function walkTsx(dir: string, out: string[] = []): string[] {
58
- let entries: string[]
59
- try {
60
- entries = readdirSync(dir)
61
- } catch {
62
- return out
63
- }
64
- for (const e of entries) {
65
- const p = join(dir, e)
66
- const st = statSync(p)
67
- if (st.isDirectory()) walkTsx(p, out)
68
- else if (e.endsWith('.tsx')) out.push(p)
69
- }
70
- return out
71
- }
72
-
73
- type Bucket =
74
- | 'collapsible'
75
- | 'spread'
76
- | 'boolean-attr'
77
- | 'dynamic-prop'
78
- | 'element-child'
79
- | 'expression-child'
80
-
81
- interface SiteClass {
82
- bucket: Bucket
83
- /** dynamic-prop only: true iff every dynamic attr is `on*` AND all other
84
- * attrs are string literals AND children are static text (partial-collapse
85
- * addressable). */
86
- partialAddressable: boolean
87
- /** dynamic-prop only: true iff EXACTLY ONE dynamic attr is a ternary of
88
- * two string literals AND every OTHER non-literal attr is an `on*`
89
- * handler (which compose orthogonally via the handler-combined
90
- * emit), AND children are static text. Counts the subset addressable
91
- * by the dynamic-prop collapse PR sequence (PRs #765-#767 plus the
92
- * handler-combined follow-up). */
93
- dynamicTernaryAddressable: boolean
94
- }
95
-
96
- const isPascal = (t: string): boolean =>
97
- !!t && t[0] === t[0]!.toUpperCase() && t[0] !== t[0]!.toLowerCase()
98
-
99
- function importTable(program: any): Map<string, string> {
100
- const t = new Map<string, string>()
101
- for (const s of program.body ?? []) {
102
- if (s.type !== 'ImportDeclaration') continue
103
- const src = s.source?.value
104
- if (typeof src !== 'string') continue
105
- for (const sp of s.specifiers ?? []) {
106
- if (sp.type === 'ImportSpecifier' && typeof sp.local?.name === 'string')
107
- t.set(sp.local.name, src)
108
- }
109
- }
110
- return t
111
- }
112
-
113
- function tagName(node: any): string {
114
- const n = node?.openingElement?.name ?? node?.name
115
- return n?.type === 'JSXIdentifier' ? n.name : ''
116
- }
117
-
118
- function classifySite(node: any): SiteClass {
119
- const opening = node.openingElement ?? node
120
- const attrs: any[] = opening.attributes ?? []
121
- let sawDynamic = false
122
- let everyDynamicIsHandler = true
123
- // Dynamic-prop addressable tracking: count ternaries + check shape.
124
- // Exactly one ternary-of-two-literals + every other non-literal attr
125
- // is either a ternary or an `on*` handler (handlers compose via the
126
- // combined `_rsCollapseDynH` emit) → addressable. Note no
127
- // `sawHandler` tracking: the original PR 3 no-handler restriction
128
- // was lifted by the handler-combined follow-up; handlers no longer
129
- // disqualify a site from `dynamicTernaryAddressable`.
130
- let ternaryCount = 0
131
- let everyDynamicIsTernary = true
132
- for (const a of attrs) {
133
- if (a.type === 'JSXSpreadAttribute')
134
- return { bucket: 'spread', partialAddressable: false, dynamicTernaryAddressable: false }
135
- const nm = a.name?.type === 'JSXIdentifier' ? a.name.name : null
136
- if (!nm)
137
- return { bucket: 'spread', partialAddressable: false, dynamicTernaryAddressable: false }
138
- const v = a.value
139
- if (!v)
140
- return {
141
- bucket: 'boolean-attr',
142
- partialAddressable: false,
143
- dynamicTernaryAddressable: false,
144
- }
145
- const isStr =
146
- v.type === 'StringLiteral' || (v.type === 'Literal' && typeof v.value === 'string')
147
- if (!isStr) {
148
- sawDynamic = true
149
- const isHandler = /^on[A-Z]/.test(nm)
150
- if (!isHandler) everyDynamicIsHandler = false
151
- // Probe for the ternary-of-two-literals shape (PR 2 detector's
152
- // structural shape).
153
- const expr = v.type === 'JSXExpressionContainer' ? v.expression : null
154
- const isLitStr = (n: any): boolean =>
155
- n &&
156
- (n.type === 'StringLiteral' || (n.type === 'Literal' && typeof n.value === 'string'))
157
- const isTernaryOfLits =
158
- expr &&
159
- expr.type === 'ConditionalExpression' &&
160
- isLitStr(expr.consequent) &&
161
- isLitStr(expr.alternate)
162
- if (isTernaryOfLits) ternaryCount++
163
- else if (!isHandler) everyDynamicIsTernary = false
164
- }
165
- }
166
- // children
167
- const kids: any[] = node.children ?? []
168
- let staticChildrenOnly = true
169
- for (const c of kids) {
170
- if (c.type === 'JSXText') continue
171
- if (c.type === 'JSXElement' || c.type === 'JSXFragment') staticChildrenOnly = false
172
- else staticChildrenOnly = false // JSXExpressionContainer etc.
173
- }
174
- if (sawDynamic) {
175
- const partialAddressable = everyDynamicIsHandler && staticChildrenOnly
176
- // Dynamic-collapse claims: EXACTLY 1 ternary, every OTHER dynamic
177
- // attr is either a ternary or an `on*` handler (no plain dynamic
178
- // shapes like `state={getValue()}`), static children. The
179
- // handler-combined follow-up (this PR) lifted the no-handler
180
- // restriction by routing handler-bearing dynamic sites to the
181
- // `_rsCollapseDynH` runtime helper instead of bailing — closes
182
- // the bulk of the 15.4% dynamic-prop bucket (previously the
183
- // strict no-handler scope only addressed 0.2% of sites).
184
- //
185
- // The `everyDynamicIsTernary` flag here is computed in the loop
186
- // above as "every non-handler dynamic attr is a ternary"; combined
187
- // with `ternaryCount === 1` + `staticChildrenOnly` it precisely
188
- // matches what `detectDynamicCollapsibleShape` + `tryDynamicCollapse`
189
- // claim. Handlers are NO LONGER excluded — they compose orthogonally.
190
- const dynamicTernaryAddressable =
191
- ternaryCount === 1 && everyDynamicIsTernary && staticChildrenOnly
192
- return { bucket: 'dynamic-prop', partialAddressable, dynamicTernaryAddressable }
193
- }
194
- // No spread / boolean / dynamic attr. Bail can now only come from children.
195
- for (const c of kids) {
196
- if (c.type === 'JSXText') continue
197
- if (c.type === 'JSXElement' || c.type === 'JSXFragment')
198
- return {
199
- bucket: 'element-child',
200
- partialAddressable: false,
201
- dynamicTernaryAddressable: false,
202
- }
203
- return {
204
- bucket: 'expression-child',
205
- partialAddressable: false,
206
- dynamicTernaryAddressable: false,
207
- }
208
- }
209
- return { bucket: 'collapsible', partialAddressable: false, dynamicTernaryAddressable: false }
210
- }
211
-
212
- describe('proposal #1 — collapse-tail bail-reason census (measurement, not a build)', () => {
213
- it('measures the real corpus and locks the partial-collapse addressable surface', () => {
214
- const files = CORPUS.flatMap((d) => walkTsx(d))
215
- expect(files.length).toBeGreaterThan(150) // sanity: the corpus exists
216
-
217
- const tally: Record<Bucket, number> = {
218
- collapsible: 0,
219
- spread: 0,
220
- 'boolean-attr': 0,
221
- 'dynamic-prop': 0,
222
- 'element-child': 0,
223
- 'expression-child': 0,
224
- }
225
- let candidates = 0
226
- let partialAddressable = 0
227
- let dynamicTernaryAddressable = 0
228
- let myCollapsible = 0
229
- let scannerCollapsible = 0
230
-
231
- for (const file of files) {
232
- const code = readFileSync(file, 'utf8')
233
- let program: any
234
- try {
235
- program = parseSync(file, code, { sourceType: 'module', lang: 'tsx' }).program
236
- } catch {
237
- continue
238
- }
239
- const imports = importTable(program)
240
-
241
- const visit = (node: any): void => {
242
- if (!node || typeof node !== 'object') return
243
- if (node.type === 'JSXElement') {
244
- const tag = tagName(node)
245
- if (isPascal(tag) && imports.has(tag) && COLLAPSIBLE_SOURCES.has(imports.get(tag)!)) {
246
- candidates++
247
- const c = classifySite(node)
248
- tally[c.bucket]++
249
- if (c.bucket === 'collapsible') myCollapsible++
250
- if (c.partialAddressable) partialAddressable++
251
- if (c.dynamicTernaryAddressable) dynamicTernaryAddressable++
252
- }
253
- }
254
- for (const k in node) {
255
- const v = node[k]
256
- if (Array.isArray(v)) for (const x of v) visit(x)
257
- else if (v && typeof v === 'object' && typeof v.type === 'string') visit(v)
258
- }
259
- }
260
- visit(program)
261
-
262
- // Production truth-set for the SAME file.
263
- scannerCollapsible += scanCollapsibleSites(code, file, COLLAPSIBLE_SOURCES).length
264
- }
265
-
266
- // ── Report (the deliverable) ────────────────────────────────────────────
267
- const pct = (n: number) => `${((n / candidates) * 100).toFixed(1)}%`
268
- // eslint-disable-next-line no-console
269
- console.log(
270
- [
271
- '',
272
- `[collapse-bail-census] ${files.length} corpus files, ${candidates} @pyreon/ui-components call sites`,
273
- ` collapsible (slice already handles): ${tally.collapsible} (${pct(tally.collapsible)})`,
274
- ` bail:spread : ${tally.spread} (${pct(tally.spread)})`,
275
- ` bail:boolean-attr : ${tally['boolean-attr']} (${pct(tally['boolean-attr'])})`,
276
- ` bail:dynamic-prop : ${tally['dynamic-prop']} (${pct(tally['dynamic-prop'])})`,
277
- ` bail:element-child : ${tally['element-child']} (${pct(tally['element-child'])})`,
278
- ` bail:expression-child : ${tally['expression-child']} (${pct(tally['expression-child'])})`,
279
- ` ── partial-collapse ADDRESSABLE : ${partialAddressable} (${pct(partialAddressable)} of all sites)`,
280
- ` (dynamic-prop bails where every dynamic attr is on*, all else literal, static children)`,
281
- ` ── dynamic-collapse ADDRESSABLE : ${dynamicTernaryAddressable} (${pct(dynamicTernaryAddressable)} of all sites)`,
282
- ` (dynamic-prop bails where EXACTLY ONE attr is a ternary-of-two-string-literals,`,
283
- ` every other non-literal attr is on* (handlers compose via _rsCollapseDynH),`,
284
- ` static children — dynamic-prop sequence #765-#767 + handler-combined follow-up)`,
285
- '',
286
- ].join('\n'),
287
- )
288
-
289
- // ── Trustworthiness gate (bisect-equivalent) ────────────────────────────
290
- // This independent walk's "collapsible-equivalent" count MUST equal the
291
- // production scanner's truth-set. If they diverge the census is
292
- // measuring fiction.
293
- //
294
- // Per PR 3 (#767) the scanner emits TWO `CollapsibleSite` entries per
295
- // dynamic-prop site (one per literal value — the resolver pre-renders
296
- // both); the compiler emit still produces ONE collapsed call site. So
297
- // the per-site classifier count + 2× the dynamic-addressable count
298
- // equals the scanner's per-resolution count. If they diverge, either
299
- // the classifier and scanner disagree on which dynamic sites are
300
- // addressable, OR the scanner's expansion drifted from this formula.
301
- expect(myCollapsible + 2 * dynamicTernaryAddressable).toBe(scannerCollapsible)
302
-
303
- // ── Lock the headline finding (ratchet record) ──────────────────────────
304
- // The corpus is real and large; these are the measured facts as of this
305
- // PR. They are asserted as RANGES (not exact) so benign corpus churn
306
- // doesn't flake the gate, but a structural shift (partial collapse landed,
307
- // or the slice's collapsible rate collapsed) trips it for review.
308
- expect(candidates).toBeGreaterThan(50)
309
- expect(tally.collapsible).toBeGreaterThan(0)
310
- // partial-addressable is the #1 go/no-go number — assert it's measured
311
- // (>=0 always true; the value is in the logged report). Lock only that
312
- // the classifier ran over a non-trivial dynamic-prop population so the
313
- // ratio is meaningful, not noise.
314
- expect(tally['dynamic-prop'] + tally.collapsible).toBeGreaterThan(0)
315
- // PR 4 of the dynamic-prop partial-collapse build: lock that the
316
- // dynamic-collapse classifier ran over a meaningful population
317
- // (dynamic-prop bucket non-zero). The addressable count is in the
318
- // log; we DON'T assert it >0 here because the strict no-handler
319
- // PR 3 scope is honestly small in real-world corpora (real Buttons
320
- // with `state={cond ? ... : ...}` almost always also carry
321
- // `onClick` → BAIL until the handler-combined follow-up). The
322
- // dynamic-prop bucket itself is the size of the future surface;
323
- // PR 3's no-handler subset is the first measurable step.
324
- expect(tally['dynamic-prop']).toBeGreaterThan(0)
325
- // The dynamic-addressable count can be 0 in a clean run (no
326
- // matching sites in the corpus); just lock that the counter is
327
- // wired and consistent with the bucket.
328
- expect(dynamicTernaryAddressable).toBeLessThanOrEqual(tally['dynamic-prop'])
329
- })
330
- })
@@ -1,88 +0,0 @@
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
- })
@@ -1,204 +0,0 @@
1
- /**
2
- * Compiler hardening — JSX child of COMPONENT parent is NOT wrapped in an
3
- * accessor when the expression is a stable reference (Identifier or simple
4
- * MemberExpression chain).
5
- *
6
- * Reported root cause behind the kinetic Stagger + bokisch.com Intro repro
7
- * (PR #731 shipped the library-side workaround; this is the upstream fix).
8
- *
9
- * Pre-fix the compiler rewrote `<Comp>{children}</Comp>` (where `children`
10
- * is a local `const` derived from a getter — `const children = childHolder.children`
11
- * after `splitProps`) as `Comp({ ..., children: () => h.children })`. Receiving
12
- * components saw `props.children` as a FUNCTION instead of the expected
13
- * `VNode | VNode[]`. DOM-consuming code routes through `mountChild` which
14
- * handles function children correctly (via `mountReactive`), so the wrap is
15
- * invisible there. Libraries that iterate children at the VNode level
16
- * (kinetic's StaggerRenderer/TransitionItem) or `cloneVNode` them directly
17
- * were silently broken — the function spread produced `{type: undefined}`
18
- * and the DOM rendered literal `<undefined>` tags.
19
- *
20
- * Fix shape: for JSX children of COMPONENT parents (uppercase tag), skip
21
- * the accessor wrap when the expression is a stable reference. The
22
- * compiler's prop-inlining pass still runs (so `children` is replaced with
23
- * `h.children` at the JSX use site) but the resulting expression is
24
- * emitted bare. Dynamic shapes (CallExpression, BinaryExpression, etc.)
25
- * keep the wrap so `<Comp>{count()}</Comp>` and similar patterns stay
26
- * reactive end-to-end.
27
- *
28
- * Note: `transformJSX_JS` returns Pyreon-transformed SOURCE — JSX stays as
29
- * JSX (the final JSX→jsx() lowering is esbuild's job). So the inlined
30
- * expression shows up between `{...}` in the emitted text.
31
- *
32
- * Bisect: revert the `isComponentTag(...) && isStableReference(expr)`
33
- * carve-out in `handleJsxExpression` (jsx.ts) → the CONTRACT specs fail
34
- * (emit reverts to `{() => h.children}`); the wrap-still-fires CONTROL
35
- * specs stay green (proving the carve-out doesn't touch the call/binary
36
- * paths).
37
- */
38
- import { describe, expect, test } from 'vitest'
39
- import { transformJSX_JS } from '../jsx'
40
-
41
- const t = (src: string): string => transformJSX_JS(src, 'test.tsx').code
42
-
43
- describe('JSX transform — component child of stable reference', () => {
44
- test('CONTRACT — bare Identifier (splitProps-derived const) is emitted without accessor wrap', () => {
45
- // The bokisch.com Intro shape, distilled. `splitProps` registers
46
- // `childHolder` as a prop-derived binding; `const children = childHolder.children`
47
- // makes `children` prop-derived; the JSX child `{children}` would,
48
- // pre-fix, emit `{() => childHolder.children}`. Now emits the inlined
49
- // value bare.
50
- const src = `
51
- const Comp = (props) => {
52
- const [childHolder, restHtml] = splitProps(props, ['children'])
53
- const children = childHolder.children
54
- return <Inner>{children}</Inner>
55
- }
56
- `
57
- const out = t(src)
58
- expect(out, 'children must NOT be wrapped in an accessor').not.toContain('() =>')
59
- expect(out, 'inlined value must appear bare in JSX child position').toMatch(
60
- /<Inner>\s*\{\(?childHolder\.children\)?\}\s*<\/Inner>/,
61
- )
62
- })
63
-
64
- test('CONTRACT — simple MemberExpression chain is emitted without accessor wrap', () => {
65
- const src = `
66
- const Comp = (props) => {
67
- const [obj] = splitProps(props, ['deep'])
68
- return <Inner>{obj.deep.x}</Inner>
69
- }
70
- `
71
- const out = t(src)
72
- expect(out, 'member chain must NOT be wrapped in an accessor').not.toContain(
73
- '() => obj.deep.x',
74
- )
75
- expect(out, 'member chain must appear bare').toMatch(
76
- /<Inner>\s*\{obj\.deep\.x\}\s*<\/Inner>/,
77
- )
78
- })
79
-
80
- test('CONTROL — CallExpression child KEEPS the wrap (preserves reactivity)', () => {
81
- // `<Comp>{count()}</Comp>` — the user explicitly reads a signal in the
82
- // child position. The wrap converts to `() => count()` so the
83
- // receiving component can subscribe via mountChild → mountReactive.
84
- const src = `
85
- const count = signal(0)
86
- const Comp = () => <Inner>{count()}</Inner>
87
- `
88
- const out = t(src)
89
- expect(out, 'call-expression child must keep the wrap').toContain('() => count()')
90
- })
91
-
92
- test('CONTROL — BinaryExpression child KEEPS the wrap', () => {
93
- const src = `
94
- const Comp = (props) => {
95
- const [own] = splitProps(props, ['a', 'b'])
96
- return <Inner>{own.a + own.b}</Inner>
97
- }
98
- `
99
- const out = t(src)
100
- expect(out, 'binary-expression child must keep the wrap').toMatch(/\(\)\s*=>/)
101
- expect(out).toContain('own.a')
102
- expect(out).toContain('own.b')
103
- })
104
-
105
- test('CONTROL — DOM-element parent with bare Identifier KEEPS the binding (reactive)', () => {
106
- // The carve-out only fires for COMPONENT parents (uppercase tag).
107
- // DOM-element children must still go through the reactive binding
108
- // path so mountChild/mountReactive can re-evaluate inside an effect.
109
- const src = `
110
- const Comp = (props) => {
111
- const [own] = splitProps(props, ['children'])
112
- const children = own.children
113
- return <div>{children}</div>
114
- }
115
- `
116
- const out = t(src)
117
- // The template path emits this as a reactive binding (_bindText /
118
- // _bindDirect / etc.), not a bare text-node. Either way, the
119
- // expression must still route through a reactive primitive.
120
- expect(out, 'DOM-element child must route through a reactive path').not.toContain(
121
- '<div>{own.children}</div>',
122
- )
123
- })
124
-
125
- test('CONTRACT — TS-cast wrapper (`as VNode[]`) is transparent', () => {
126
- // The EXACT shape `createKineticComponent.tsx` ships:
127
- // `<StaggerRenderer>{children as VNode[]}</StaggerRenderer>`
128
- // The TS `as` cast wraps `children` as a `TSAsExpression`. Without
129
- // unwrapping, the carve-out misses the bokisch reproducer entirely.
130
- // The cast is preserved in the emit — esbuild's later TS-strip pass
131
- // removes it. Reproducer: pre-fix this test fails with
132
- // `expected to NOT contain '() =>'` because the wrap still fires.
133
- const src = `
134
- const Kinetic = (props) => {
135
- const [childHolder] = splitProps(props, ['children'])
136
- const children = childHolder.children
137
- return <Inner>{children as VNode[]}</Inner>
138
- }
139
- `
140
- const out = t(src)
141
- expect(out, 'TS-cast wrapper must not block the carve-out').not.toContain('() =>')
142
- // Slice unwraps the TS cast — output is just the inlined value.
143
- expect(out).toMatch(/<Inner>\s*\{\(?childHolder\.children\)?\}\s*<\/Inner>/)
144
- })
145
-
146
- test('CONTRACT — non-null `!` postfix is transparent', () => {
147
- const src = `
148
- const Comp = (props) => {
149
- const [own] = splitProps(props, ['children'])
150
- return <Inner>{own.children!}</Inner>
151
- }
152
- `
153
- const out = t(src)
154
- expect(out).not.toContain('() =>')
155
- expect(out).toMatch(/<Inner>\s*\{own\.children\}\s*<\/Inner>/)
156
- })
157
-
158
- test('CONTRACT — kinetic Stagger reproducer compiles to bare children prop', () => {
159
- // Exact shape from `packages/ui-system/kinetic/src/kinetic/createKineticComponent.tsx`.
160
- // Pre-fix emit (JSX child position): `{() => childHolder.children}`.
161
- // Post-fix emit: `{childHolder.children}` (no wrap).
162
- const src = `
163
- const Kinetic = (props) => {
164
- const [childHolder, restHtml] = splitProps(props, ['children'])
165
- const children = childHolder.children
166
- return <StaggerRenderer htmlProps={restHtml}>{children}</StaggerRenderer>
167
- }
168
- `
169
- const out = t(src)
170
- expect(out).not.toContain('() => childHolder.children')
171
- expect(out).not.toContain('() => children')
172
- expect(out).toMatch(
173
- /<StaggerRenderer[^>]*>\s*\{\(?childHolder\.children\)?\}\s*<\/StaggerRenderer>/,
174
- )
175
- })
176
-
177
- test('CONTROL — bare SIGNAL identifier KEEPS the wrap (auto-call + wrap is reactive)', () => {
178
- // `<Comp>{count}</Comp>` where count is a tracked signal — the user's
179
- // deliberate "make this reactive at the call site" pattern. The
180
- // compiler auto-calls (`count` → `count()`) AND wraps (`() => count()`)
181
- // so the receiving component re-evaluates in its mountReactive scope.
182
- // The stable-reference carve-out explicitly excludes signal references.
183
- const src = `
184
- function C() {
185
- const count = signal(0)
186
- return <MyComp>{count}</MyComp>
187
- }
188
- `
189
- const out = t(src)
190
- expect(out, 'signal child must be wrapped + auto-called').toContain('() => count()')
191
- })
192
-
193
- test('CONTROL — already-arrow-wrapped child is unchanged (idempotent)', () => {
194
- // Users who explicitly want reactivity write `<Comp>{() => x()}</Comp>`.
195
- // The compiler's shouldWrap returns false for ArrowFunctionExpression,
196
- // so the carve-out never fires. The user's accessor passes through.
197
- const src = `
198
- const x = signal('a')
199
- const Comp = () => <Inner>{() => x()}</Inner>
200
- `
201
- const out = t(src)
202
- expect(out, 'user-written accessor must pass through').toContain('() => x()')
203
- })
204
- })