@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,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
|
-
})
|