@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,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR 3 of the dynamic-prop partial-collapse build — the compiler EMIT
|
|
3
|
-
* half: `tryRocketstyleCollapse` falls through to `tryDynamicCollapse`
|
|
4
|
-
* when BOTH the full and the `on*`-handler-partial paths bail. Emits
|
|
5
|
-
* `__rsCollapseDyn(html, [stride-2 classes], () => cond ? 0 : 1, () =>
|
|
6
|
-
* __pyrMode() === "dark")` consumed by PR 1's runtime helper `_rsCollapseDyn`
|
|
7
|
-
* (#765).
|
|
8
|
-
*
|
|
9
|
-
* Mirrors the existing `partial-collapse-emit.test.ts` harness exactly
|
|
10
|
-
* (stubbed resolved-`sites` map — the resolver/plugin scan is PR 4's
|
|
11
|
-
* gate; this proves the emit contract in isolation).
|
|
12
|
-
*
|
|
13
|
-
* Bisect-verify (PR body): revert the fallback chain
|
|
14
|
-
* (`return tryPartialCollapse(...) || tryDynamicCollapse(...)` →
|
|
15
|
-
* `return tryPartialCollapse(...)`) → the dynamic-emit specs fail
|
|
16
|
-
* (`__rsCollapseDyn(` absent) while the FULL + PARTIAL specs still
|
|
17
|
-
* pass (proving the dynamic fallthrough is the only delta).
|
|
18
|
-
* Restore → all pass.
|
|
19
|
-
*/
|
|
20
|
-
import { describe, expect, it } from 'vitest'
|
|
21
|
-
import { rocketstyleCollapseKey, transformJSX } from '../jsx'
|
|
22
|
-
|
|
23
|
-
// Per-value resolved sites — the dynamic emit looks up BOTH literals
|
|
24
|
-
// via separate keys. `templateHtml` MUST be byte-identical across
|
|
25
|
-
// values for the dispatcher to share one `_tpl` (cross-value template
|
|
26
|
-
// parity bail).
|
|
27
|
-
const TPL = '<button>Save</button>'
|
|
28
|
-
|
|
29
|
-
const PRIMARY = {
|
|
30
|
-
templateHtml: TPL,
|
|
31
|
-
lightClass: 'pyr-pri-L',
|
|
32
|
-
darkClass: 'pyr-pri-D',
|
|
33
|
-
rules: ['.pyr-pri-L{color:red}', '.pyr-pri-D{color:darkred}'],
|
|
34
|
-
ruleKey: 'bundle-pri',
|
|
35
|
-
}
|
|
36
|
-
const SECONDARY = {
|
|
37
|
-
templateHtml: TPL,
|
|
38
|
-
lightClass: 'pyr-sec-L',
|
|
39
|
-
darkClass: 'pyr-sec-D',
|
|
40
|
-
rules: ['.pyr-sec-L{color:blue}', '.pyr-sec-D{color:darkblue}'],
|
|
41
|
-
ruleKey: 'bundle-sec',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function collapseOpt(
|
|
45
|
-
candidates: string[],
|
|
46
|
-
sites: Record<string, { templateHtml: string; lightClass: string; darkClass: string; rules: string[]; ruleKey: string }>,
|
|
47
|
-
) {
|
|
48
|
-
return {
|
|
49
|
-
collapseRocketstyle: {
|
|
50
|
-
candidates: new Set(candidates),
|
|
51
|
-
sites: new Map(Object.entries(sites)),
|
|
52
|
-
mode: { name: 'useMode', source: '@pyreon/ui-core' },
|
|
53
|
-
},
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('compiler — dynamic-prop collapse emission (PR 3, ternary-of-two-literals)', () => {
|
|
58
|
-
it('emits __rsCollapseDyn with stride-2 value-major classes + cond dispatcher', () => {
|
|
59
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary', size: 'medium' }, 'Save')
|
|
60
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary', size: 'medium' }, 'Save')
|
|
61
|
-
const src =
|
|
62
|
-
'const x = <Button state={isPrimary ? "primary" : "secondary"} size="medium">Save</Button>'
|
|
63
|
-
const { code } = transformJSX(
|
|
64
|
-
src,
|
|
65
|
-
'A.tsx',
|
|
66
|
-
collapseOpt(['Button'], { [truthyKey]: PRIMARY, [falsyKey]: SECONDARY }),
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// Stride-2 value-major class layout: `[v0_light, v0_dark, v1_light, v1_dark]`.
|
|
70
|
-
// v0 = consequent (cond → 0), v1 = alternate (cond → 1) — matches
|
|
71
|
-
// `_rsCollapseDyn` doc + bisect-verified in PR 1 (#765).
|
|
72
|
-
expect(code).toContain(
|
|
73
|
-
'__rsCollapseDyn("<button>Save</button>", ' +
|
|
74
|
-
'["pyr-pri-L","pyr-pri-D","pyr-sec-L","pyr-sec-D"], ' +
|
|
75
|
-
'() => (isPrimary) ? 0 : 1, ' +
|
|
76
|
-
'() => __pyrMode() === "dark")',
|
|
77
|
-
)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('imports the _rsCollapseDyn helper (and NOT the full / H helpers when not used)', () => {
|
|
81
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Go')
|
|
82
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'Go')
|
|
83
|
-
const src = 'const x = <Button state={c ? "primary" : "secondary"}>Go</Button>'
|
|
84
|
-
const { code } = transformJSX(
|
|
85
|
-
src,
|
|
86
|
-
'B.tsx',
|
|
87
|
-
collapseOpt(['Button'], { [truthyKey]: PRIMARY, [falsyKey]: SECONDARY }),
|
|
88
|
-
)
|
|
89
|
-
// Conditional import — dynamic-only modules pull `_rsCollapseDyn`
|
|
90
|
-
// ONLY (tree-shake-friendly per-feature granularity).
|
|
91
|
-
expect(code).toContain('import { _rsCollapseDyn as __rsCollapseDyn } from "@pyreon/runtime-dom";')
|
|
92
|
-
expect(code).not.toContain('_rsCollapse as __rsCollapse')
|
|
93
|
-
expect(code).not.toContain('_rsCollapseH as __rsCollapseH')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('unions BOTH values\' rule bundles via injectRules (de-duped by ruleKey)', () => {
|
|
97
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'X')
|
|
98
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'X')
|
|
99
|
-
const src = 'const x = <Button state={c ? "primary" : "secondary"}>X</Button>'
|
|
100
|
-
const { code } = transformJSX(
|
|
101
|
-
src,
|
|
102
|
-
'C.tsx',
|
|
103
|
-
collapseOpt(['Button'], { [truthyKey]: PRIMARY, [falsyKey]: SECONDARY }),
|
|
104
|
-
)
|
|
105
|
-
// Each value's rule bundle injected separately (different ruleKeys
|
|
106
|
-
// ⇒ no dedupe). Same idempotent injectRules contract as the full
|
|
107
|
-
// path — styler dedupes by key at runtime.
|
|
108
|
-
expect(code).toContain('"bundle-pri"')
|
|
109
|
-
expect(code).toContain('"bundle-sec"')
|
|
110
|
-
expect(code).toContain('__rsSheet.injectRules(')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('preserves complex cond source verbatim (paren-wrapped for safe re-emission)', () => {
|
|
114
|
-
const truthyKey = rocketstyleCollapseKey('Btn', { state: 'primary' }, 'Hi')
|
|
115
|
-
const falsyKey = rocketstyleCollapseKey('Btn', { state: 'danger' }, 'Hi')
|
|
116
|
-
const src = 'const x = <Btn state={user.role === "admin" ? "primary" : "danger"}>Hi</Btn>'
|
|
117
|
-
const { code } = transformJSX(
|
|
118
|
-
src,
|
|
119
|
-
'D.tsx',
|
|
120
|
-
collapseOpt(['Btn'], {
|
|
121
|
-
[truthyKey]: { ...PRIMARY },
|
|
122
|
-
[falsyKey]: { ...SECONDARY, lightClass: 'pyr-dng-L', darkClass: 'pyr-dng-D' },
|
|
123
|
-
}),
|
|
124
|
-
)
|
|
125
|
-
// The paren-wrap (`(user.role === "admin")`) keeps the cond a single
|
|
126
|
-
// expression — same shape as the on*-handler emit re-emits arrow bodies.
|
|
127
|
-
expect(code).toContain('() => (user.role === "admin") ? 0 : 1')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
// ── Conservative-bail discipline ───────────────────────────────────────
|
|
131
|
-
it('BAILS when EITHER expanded site is missing from the resolved map', () => {
|
|
132
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'S')
|
|
133
|
-
// Only the truthy half resolved — falsy is absent (resolver returned
|
|
134
|
-
// null for that variant). Half-resolved ⇒ keep the normal mount.
|
|
135
|
-
const src = 'const x = <Button state={c ? "primary" : "secondary"}>S</Button>'
|
|
136
|
-
const { code } = transformJSX(
|
|
137
|
-
src,
|
|
138
|
-
'E.tsx',
|
|
139
|
-
collapseOpt(['Button'], { [truthyKey]: PRIMARY }),
|
|
140
|
-
)
|
|
141
|
-
expect(code).not.toContain('__rsCollapseDyn(')
|
|
142
|
-
// Normal mount preserved — `<Button …>` JSX still appears (or its
|
|
143
|
-
// standard `h()` form post-transform).
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('BAILS when the structural template diverges across values', () => {
|
|
147
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'D')
|
|
148
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'D')
|
|
149
|
-
const src = 'const x = <Button state={c ? "primary" : "secondary"}>D</Button>'
|
|
150
|
-
const { code } = transformJSX(
|
|
151
|
-
src,
|
|
152
|
-
'F.tsx',
|
|
153
|
-
collapseOpt(['Button'], {
|
|
154
|
-
[truthyKey]: { ...PRIMARY, templateHtml: '<button>D</button>' },
|
|
155
|
-
[falsyKey]: { ...SECONDARY, templateHtml: '<button data-extra>D</button>' }, // divergent
|
|
156
|
-
}),
|
|
157
|
-
)
|
|
158
|
-
expect(code).not.toContain('__rsCollapseDyn(')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('BAILS when the dynamic site ALSO has on*-handlers (PR 3 scope: no-handler only)', () => {
|
|
162
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'H')
|
|
163
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'H')
|
|
164
|
-
const src =
|
|
165
|
-
'const x = <Button state={c ? "primary" : "secondary"} onClick={go}>H</Button>'
|
|
166
|
-
const { code } = transformJSX(
|
|
167
|
-
src,
|
|
168
|
-
'G.tsx',
|
|
169
|
-
collapseOpt(['Button'], { [truthyKey]: PRIMARY, [falsyKey]: SECONDARY }),
|
|
170
|
-
)
|
|
171
|
-
expect(code).not.toContain('__rsCollapseDyn(')
|
|
172
|
-
expect(code).not.toContain('__rsCollapseH(')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
// ── Regression: FULL + on*-PARTIAL paths byte-unchanged ────────────────
|
|
176
|
-
it('FULL-collapse path byte-unchanged: no-handler literal site emits plain __rsCollapse', () => {
|
|
177
|
-
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Save')
|
|
178
|
-
const src = 'const x = <Button state="primary">Save</Button>'
|
|
179
|
-
const { code } = transformJSX(src, 'H.tsx', collapseOpt(['Button'], { [key]: PRIMARY }))
|
|
180
|
-
expect(code).toContain('__rsCollapse(')
|
|
181
|
-
expect(code).not.toContain('__rsCollapseH(')
|
|
182
|
-
expect(code).not.toContain('__rsCollapseDyn(')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('PARTIAL on*-handler path byte-unchanged: literal site + handler emits __rsCollapseH', () => {
|
|
186
|
-
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Save')
|
|
187
|
-
const src = 'const x = <Button state="primary" onClick={go}>Save</Button>'
|
|
188
|
-
const { code } = transformJSX(src, 'I.tsx', collapseOpt(['Button'], { [key]: PRIMARY }))
|
|
189
|
-
expect(code).toContain('__rsCollapseH(')
|
|
190
|
-
expect(code).not.toContain('__rsCollapseDyn(')
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR 3 of the dynamic-prop partial-collapse build — `scanCollapsibleSites`
|
|
3
|
-
* extension. The plugin-side scan (`@pyreon/vite-plugin`) calls this to
|
|
4
|
-
* learn WHICH (component, props, text) tuples need resolution. For
|
|
5
|
-
* dynamic-prop sites it must expand into TWO `CollapsibleSite` entries
|
|
6
|
-
* (one per literal value) so the resolver pre-renders both via the
|
|
7
|
-
* existing SSR pipeline AND the compiler emit (PR 3 `tryDynamicCollapse`)
|
|
8
|
-
* looks up both via identical key construction.
|
|
9
|
-
*
|
|
10
|
-
* Key invariant: the keys this scan emits must EQUAL the keys
|
|
11
|
-
* `tryDynamicCollapse` computes from the same JSX — same load-bearing
|
|
12
|
-
* separation as the existing full / on*-handler scan↔emit invariants
|
|
13
|
-
* (`scanCollapsibleSites` ↔ `detectCollapsibleShape`).
|
|
14
|
-
*/
|
|
15
|
-
import { describe, expect, it } from 'vitest'
|
|
16
|
-
import { rocketstyleCollapseKey, scanCollapsibleSites } from '../jsx'
|
|
17
|
-
|
|
18
|
-
const COLLAPSIBLE = new Set(['@pyreon/ui-components'])
|
|
19
|
-
|
|
20
|
-
describe('scanCollapsibleSites — dynamic-prop expansion (PR 3)', () => {
|
|
21
|
-
it('expands a single ternary site into TWO CollapsibleSite entries (one per literal)', () => {
|
|
22
|
-
const src = `
|
|
23
|
-
import { Button } from '@pyreon/ui-components'
|
|
24
|
-
const x = <Button state={cond ? "primary" : "secondary"} size="medium">Save</Button>
|
|
25
|
-
`
|
|
26
|
-
const sites = scanCollapsibleSites(src, 'A.tsx', COLLAPSIBLE)
|
|
27
|
-
expect(sites).toHaveLength(2)
|
|
28
|
-
const byState = new Map(sites.map((s) => [s.props.state, s]))
|
|
29
|
-
expect(byState.get('primary')!.props).toEqual({ state: 'primary', size: 'medium' })
|
|
30
|
-
expect(byState.get('secondary')!.props).toEqual({ state: 'secondary', size: 'medium' })
|
|
31
|
-
expect(byState.get('primary')!.childrenText).toBe('Save')
|
|
32
|
-
expect(byState.get('secondary')!.childrenText).toBe('Save')
|
|
33
|
-
// Both entries share the same component / source / importedName —
|
|
34
|
-
// only `props.state` differs.
|
|
35
|
-
expect(byState.get('primary')!.componentName).toBe('Button')
|
|
36
|
-
expect(byState.get('secondary')!.componentName).toBe('Button')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('emits keys IDENTICAL to what tryDynamicCollapse will look up', () => {
|
|
40
|
-
// The cross-detector invariant: the scan and the emit MUST agree
|
|
41
|
-
// on the key for each expanded value. Drift would cause the emit
|
|
42
|
-
// to miss its own pre-resolved sites and bail to normal mount.
|
|
43
|
-
const src = `
|
|
44
|
-
import { Button } from '@pyreon/ui-components'
|
|
45
|
-
const x = <Button state={c ? "primary" : "secondary"}>Go</Button>
|
|
46
|
-
`
|
|
47
|
-
const sites = scanCollapsibleSites(src, 'B.tsx', COLLAPSIBLE)
|
|
48
|
-
expect(sites).toHaveLength(2)
|
|
49
|
-
const truthyKey = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Go')
|
|
50
|
-
const falsyKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'Go')
|
|
51
|
-
const keys = sites.map((s) => s.key).sort()
|
|
52
|
-
expect(keys).toEqual([truthyKey, falsyKey].sort())
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('EXPANDS handler-combined dynamic sites too (handler-combined emit unlocks the 15.4% bucket)', () => {
|
|
56
|
-
// The follow-up emit (`__rsCollapseDynH` from `tryDynamicCollapse`)
|
|
57
|
-
// handles ternary-plus-handler sites via the combined runtime
|
|
58
|
-
// helper. The scan emits both literal-value expansions identically
|
|
59
|
-
// — handlers don't affect the resolver's input (componentName,
|
|
60
|
-
// props, childrenText). Handlers are re-attached by the runtime
|
|
61
|
-
// helper, not by the resolver.
|
|
62
|
-
//
|
|
63
|
-
// Previously the scan SKIPPED handler-combined sites (matching
|
|
64
|
-
// the PR 3 emit's no-handler scope); the follow-up lifts that
|
|
65
|
-
// restriction so the resolver pre-renders both values for
|
|
66
|
-
// handler-bearing sites too.
|
|
67
|
-
const src = `
|
|
68
|
-
import { Button } from '@pyreon/ui-components'
|
|
69
|
-
const x = <Button state={c ? "primary" : "secondary"} onClick={go}>H</Button>
|
|
70
|
-
`
|
|
71
|
-
const sites = scanCollapsibleSites(src, 'C.tsx', COLLAPSIBLE)
|
|
72
|
-
expect(sites).toHaveLength(2)
|
|
73
|
-
const byState = new Map(sites.map((s) => [s.props.state, s]))
|
|
74
|
-
expect(byState.get('primary')!.childrenText).toBe('H')
|
|
75
|
-
expect(byState.get('secondary')!.childrenText).toBe('H')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('does not double-emit when the FULL detector already claims the site (literal-only)', () => {
|
|
79
|
-
// A fully-literal site is the FULL-collapse shape — claimed by
|
|
80
|
-
// detectCollapsibleShape; the dynamic-fallthrough branch in the
|
|
81
|
-
// scan only runs when the full detector returned null.
|
|
82
|
-
const src = `
|
|
83
|
-
import { Button } from '@pyreon/ui-components'
|
|
84
|
-
const x = <Button state="primary" size="medium">Save</Button>
|
|
85
|
-
`
|
|
86
|
-
const sites = scanCollapsibleSites(src, 'D.tsx', COLLAPSIBLE)
|
|
87
|
-
expect(sites).toHaveLength(1)
|
|
88
|
-
expect(sites[0]!.props).toEqual({ state: 'primary', size: 'medium' })
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
it('emits 4 entries for a module with 2 ternary sites (no dedupe across distinct sites)', () => {
|
|
92
|
-
const src = `
|
|
93
|
-
import { Button } from '@pyreon/ui-components'
|
|
94
|
-
const a = <Button state={c1 ? "primary" : "secondary"}>A</Button>
|
|
95
|
-
const b = <Button state={c2 ? "danger" : "success"}>B</Button>
|
|
96
|
-
`
|
|
97
|
-
const sites = scanCollapsibleSites(src, 'E.tsx', COLLAPSIBLE)
|
|
98
|
-
expect(sites).toHaveLength(4)
|
|
99
|
-
const states = sites.map((s) => `${s.props.state}/${s.childrenText}`).sort()
|
|
100
|
-
expect(states).toEqual(['danger/B', 'primary/A', 'secondary/A', 'success/B'])
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('skips multi-ternary site entirely (separable scope, not this PR)', () => {
|
|
104
|
-
const src = `
|
|
105
|
-
import { Button } from '@pyreon/ui-components'
|
|
106
|
-
const x = <Button state={a ? "x" : "y"} size={b ? "small" : "large"}>S</Button>
|
|
107
|
-
`
|
|
108
|
-
const sites = scanCollapsibleSites(src, 'F.tsx', COLLAPSIBLE)
|
|
109
|
-
expect(sites).toHaveLength(0)
|
|
110
|
-
})
|
|
111
|
-
})
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compiler hardening — Round 9 (REAL bug, FIXED + bisect-verified).
|
|
3
|
-
*
|
|
4
|
-
* const header = <h1>T</h1>
|
|
5
|
-
* return <div>{header}<p>x</p></div>
|
|
6
|
-
*
|
|
7
|
-
* Pre-fix the compiler lowered the const to `_tpl(...)` (so it KNEW `header`
|
|
8
|
-
* was a `NativeItem` element) yet still emitted
|
|
9
|
-
* `document.createTextNode(header)` for the `{header}` child — `createTextNode`
|
|
10
|
-
* string-coerces the NativeItem → "[object Object]" instead of the `<h1>`.
|
|
11
|
-
* Only `props.children` / `own.children` reached the correct `_mountSlot`.
|
|
12
|
-
*
|
|
13
|
-
* Fix (jsx.ts): an `elementVars` set tracks `const`/`let` bindings whose
|
|
14
|
-
* initializer is a JSX element/fragment (optionally parenthesized); a bare
|
|
15
|
-
* `{el}` child of such a binding routes through `_mountSlot` — the same
|
|
16
|
-
* general child-insert `props.children` uses. Tight by construction: only a
|
|
17
|
-
* DIRECT JSX initializer reclassifies, so string/number/prop-derived/inline-
|
|
18
|
-
* hoisted children keep their existing (correct) paths. Routing is safe even
|
|
19
|
-
* under later same-name shadowing — `_mountSlot` renders strings/numbers
|
|
20
|
-
* correctly too; the only cost of imprecision is skipping the text fast path.
|
|
21
|
-
*
|
|
22
|
-
* NOT contradicted by `jsx.test.ts:777` `createTextNode(label)` — that pins
|
|
23
|
-
* the FREE undeclared identifier default (genuinely ambiguous); this fix only
|
|
24
|
-
* fires when the binding's initializer is provably JSX.
|
|
25
|
-
*
|
|
26
|
-
* Bisect: revert the `isElementValuedIdent` clause in `processOneChild`
|
|
27
|
-
* (jsx.ts) → the CONTRACT specs fail (emit reverts to `createTextNode(header)`)
|
|
28
|
-
* while every CONTROL spec stays green (proving the fix doesn't touch the
|
|
29
|
-
* text/reactive fast paths). Restore → all pass.
|
|
30
|
-
*/
|
|
31
|
-
import { describe, expect, it } from 'vitest'
|
|
32
|
-
import { transformJSX_JS } from '../jsx'
|
|
33
|
-
|
|
34
|
-
const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
35
|
-
const ELEMENT_CONST = `function C(){ const header = <h1>T</h1>; return <div>{header}<p>x</p></div> }`
|
|
36
|
-
|
|
37
|
-
describe('Round 9 — element-valued const used as a bare JSX child', () => {
|
|
38
|
-
it('CONTROL: string/number const child still uses the correct text fast path', () => {
|
|
39
|
-
expect(emit(`function C(){ const t = 'T'; return <div>{t}<p>x</p></div> }`)).toContain('createTextNode(t)')
|
|
40
|
-
expect(emit(`function C(){ const n = 5; return <div>{n}</div> }`)).toContain('textContent = n')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('CONTROL: an INLINE element child is correctly hoisted (not text-coerced)', () => {
|
|
44
|
-
const out = emit(`function C(){ return <div>{<h1>T</h1>}<p>x</p></div> }`)
|
|
45
|
-
expect(out).toMatch(/const _\$h\d+ =/)
|
|
46
|
-
expect(out).not.toMatch(/createTextNode\(_\$h\d+\)/)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('CONTRACT: element-valued const child is mounted via _mountSlot, not text-coerced', () => {
|
|
50
|
-
const out = emit(ELEMENT_CONST)
|
|
51
|
-
expect(out).toContain('const header = _tpl("<h1>T</h1>"')
|
|
52
|
-
expect(out).not.toContain('createTextNode(header)')
|
|
53
|
-
expect(out).toMatch(/_mountSlot\(\s*header\b/)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('CONTRACT: single bare element-const child, parenthesized init, and let all mount', () => {
|
|
57
|
-
expect(emit(`function C(){ const el = <span>hi</span>; return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
|
|
58
|
-
expect(emit(`function C(){ const el = (<b>x</b>); return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
|
|
59
|
-
expect(emit(`function C(){ let el = <a/>; return <div>{el}</div> }`)).toMatch(/_mountSlot\(\s*el\b/)
|
|
60
|
-
})
|
|
61
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compiler hardening — Round 3 (characterization, NOT a bug fix).
|
|
3
|
-
*
|
|
4
|
-
* Investigated: how the JSX transform emits falsy / boolean / null literal
|
|
5
|
-
* children vs the JSX rendering contract (`true`/`false`/`null`/`undefined`
|
|
6
|
-
* render nothing; `0` renders "0"; `''` renders empty).
|
|
7
|
-
*
|
|
8
|
-
* Finding: the patterns real code actually writes are CORRECT — a conditional
|
|
9
|
-
* (`{c ? x : null}`) or short-circuit (`{c && <X/>}`) child is wrapped in a
|
|
10
|
-
* `() =>` accessor and the null/boolean is filtered by runtime `mountChild`,
|
|
11
|
-
* so nothing renders (Pyreon's documented `VNodeChildAtom` `&&` contract
|
|
12
|
-
* holds). Only a CONTRIVED bare literal child (`<div>{false}</div>` — never
|
|
13
|
-
* written in practice) takes the static path and emits `textContent = false`
|
|
14
|
-
* → the DOM stringifies to "false". This is a spec divergence on input no one
|
|
15
|
-
* writes; fixing it would touch the hot child-emission path for zero
|
|
16
|
-
* real-world benefit, so the behavior is pinned here instead (any future
|
|
17
|
-
* change to it must be deliberate, and this test will flag it).
|
|
18
|
-
*/
|
|
19
|
-
import { describe, expect, it } from 'vitest'
|
|
20
|
-
import { transformJSX_JS } from '../jsx'
|
|
21
|
-
|
|
22
|
-
const emit = (c: string): string => transformJSX_JS(c, 'c.tsx').code ?? ''
|
|
23
|
-
|
|
24
|
-
describe('Round 3 — conditional/short-circuit children are accessor-wrapped (the contract that matters)', () => {
|
|
25
|
-
it('ternary with a null branch is wrapped in an accessor (runtime filters null)', () => {
|
|
26
|
-
const out = emit(`function C(p){ return <div>{p.cond ? <a/> : null}</div> }`)
|
|
27
|
-
expect(out).toContain('() => p.cond ? <a/> : null')
|
|
28
|
-
expect(out).not.toContain('createTextNode(null)')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('&& short-circuit is wrapped in an accessor (the documented && pattern)', () => {
|
|
32
|
-
const out = emit(`function C(p){ return <div>{p.show && <b/>}</div> }`)
|
|
33
|
-
expect(out).toContain('() => p.show && <b/>')
|
|
34
|
-
expect(out).not.toContain('createTextNode(false)')
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('Round 3 — bare literal falsy children: pinned current behavior (contrived input)', () => {
|
|
39
|
-
it('numeric 0 child renders "0" (JSX-correct)', () => {
|
|
40
|
-
expect(emit(`function C(){ return <div>{0}</div> }`)).toContain('__root.textContent = 0')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// Pinned divergence: a bare `{false}` literal stringifies via textContent.
|
|
44
|
-
// Documented, not fixed — see file header for the rationale.
|
|
45
|
-
it('bare {false} literal takes the static textContent path (known, contrived)', () => {
|
|
46
|
-
expect(emit(`function C(){ return <div>{false}</div> }`)).toContain('__root.textContent = false')
|
|
47
|
-
})
|
|
48
|
-
})
|