@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,170 +0,0 @@
|
|
|
1
|
-
import { transformJSX_JS } from '../jsx'
|
|
2
|
-
import { analyzeReactivity, formatReactivityLens } from '../reactivity-lens'
|
|
3
|
-
import type { ReactivityFinding } from '../reactivity-lens'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Reactivity Lens — unit + drift gate.
|
|
7
|
-
*
|
|
8
|
-
* The load-bearing correctness contract: a lens span is a FAITHFUL RECORD of
|
|
9
|
-
* a codegen decision, never an approximation. Two invariants are gated here:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Additive** — collecting the lens does NOT change emitted `code`
|
|
12
|
-
* (kill-criterion a). Bisect: if a future edit makes lens collection
|
|
13
|
-
* mutate codegen, `additive` fails.
|
|
14
|
-
* 2. **Drift** — every positive `reactive*` span's OUTPUT carries the
|
|
15
|
-
* matching codegen token (`_bind`/`_bindText`/`_rp`), and every
|
|
16
|
-
* `static-text` span's text is NOT reactively bound (kill-criterion b).
|
|
17
|
-
* Bisect: reverting the `lens(...)` call at any instrumented site drops
|
|
18
|
-
* the corresponding span and the matching `expect(kinds).toContain(...)`
|
|
19
|
-
* fails — documented per-fixture below.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
function kindsAt(code: string): string[] {
|
|
23
|
-
return analyzeReactivity(code).findings.map((f) => f.kind)
|
|
24
|
-
}
|
|
25
|
-
function find(code: string, kind: string): ReactivityFinding[] {
|
|
26
|
-
return analyzeReactivity(code).findings.filter((f) => f.kind === kind)
|
|
27
|
-
}
|
|
28
|
-
function sliceFinding(code: string, f: ReactivityFinding): string {
|
|
29
|
-
// Re-derive the byte slice from line/col for a human-readable assertion.
|
|
30
|
-
const lines = code.split('\n')
|
|
31
|
-
const line = lines[f.line - 1] ?? ''
|
|
32
|
-
return f.endLine === f.line
|
|
33
|
-
? line.slice(f.column, f.endColumn)
|
|
34
|
-
: line.slice(f.column)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('reactivity-lens — additive contract (kill-criterion a)', () => {
|
|
38
|
-
const FIXTURES = [
|
|
39
|
-
`function C(){ return <div>{count()}</div> }`,
|
|
40
|
-
`function C(p){ return <span>{p.label}</span> }`,
|
|
41
|
-
`function C(){ return <Box title={n()} /> }`,
|
|
42
|
-
`function C(){ return <div class="static">hi</div> }`,
|
|
43
|
-
`function C(){ return <a class={() => cls()}>x</a> }`,
|
|
44
|
-
`const x = 1; function C(){ const {a}=props; return <i>{a}</i> }`,
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
it('lens collection NEVER changes emitted code (byte-identical)', () => {
|
|
48
|
-
for (const src of FIXTURES) {
|
|
49
|
-
const off = transformJSX_JS(src, 'f.tsx').code
|
|
50
|
-
const on = transformJSX_JS(src, 'f.tsx', { reactivityLens: true }).code
|
|
51
|
-
expect(on).toBe(off)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('lens field is absent unless opted in', () => {
|
|
56
|
-
const r = transformJSX_JS(FIXTURES[0]!, 'f.tsx')
|
|
57
|
-
expect(r.reactivityLens).toBeUndefined()
|
|
58
|
-
const r2 = transformJSX_JS(FIXTURES[0]!, 'f.tsx', { reactivityLens: true })
|
|
59
|
-
expect(Array.isArray(r2.reactivityLens)).toBe(true)
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe('reactivity-lens — drift gate (positive claim = codegen record)', () => {
|
|
64
|
-
it('reactive text: {count()} → reactive span + _bind in output', () => {
|
|
65
|
-
const src = `function C(){ return <div>{count()}</div> }`
|
|
66
|
-
const reactive = find(src, 'reactive')
|
|
67
|
-
expect(reactive.length).toBe(1)
|
|
68
|
-
expect(sliceFinding(src, reactive[0]!)).toBe('count()')
|
|
69
|
-
// Drift proof: the codegen actually emitted a reactive binding.
|
|
70
|
-
const out = transformJSX_JS(src, 'f.tsx').code
|
|
71
|
-
expect(out).toMatch(/_bind(Text|Direct)?\(/)
|
|
72
|
-
expect(kindsAt(src)).not.toContain('static-text')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('static text: {p.x}-free plain identifier → static-text, NOT reactive', () => {
|
|
76
|
-
const src = `function C(){ const label = "hi"; return <div>{label}</div> }`
|
|
77
|
-
const k = kindsAt(src)
|
|
78
|
-
expect(k).toContain('static-text')
|
|
79
|
-
expect(k).not.toContain('reactive')
|
|
80
|
-
const st = find(src, 'static-text')[0]!
|
|
81
|
-
expect(sliceFinding(src, st)).toBe('label')
|
|
82
|
-
// Drift proof: codegen baked it (no reactive binding helper for this).
|
|
83
|
-
const out = transformJSX_JS(src, 'f.tsx').code
|
|
84
|
-
expect(out).not.toMatch(/_bind\(\(\) => \{ \w+\.data = label \}/)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('reactive prop: <Box title={n()} /> → reactive-prop + _rp in output', () => {
|
|
88
|
-
const src = `function C(){ return <Box title={n()} /> }`
|
|
89
|
-
const rp = find(src, 'reactive-prop')
|
|
90
|
-
expect(rp.length).toBe(1)
|
|
91
|
-
expect(sliceFinding(src, rp[0]!)).toBe('n()')
|
|
92
|
-
expect(transformJSX_JS(src, 'f.tsx').code).toContain('_rp(() =>')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('hoisted static: static JSX in a non-template position → hoisted-static + module preamble', () => {
|
|
96
|
-
// A top-level returned static element becomes a `_tpl()` clone (template
|
|
97
|
-
// path) — that's not a hoist, and the lens correctly stays silent (no
|
|
98
|
-
// span = "not asserted"). maybeHoist only fires for static JSX in an
|
|
99
|
-
// expression slot of a non-DOM (component) parent; that's the faithful
|
|
100
|
-
// trigger and what the lens records.
|
|
101
|
-
const src = `function C(){ return <Comp>{<b class="x">hi</b>}</Comp> }`
|
|
102
|
-
const hs = find(src, 'hoisted-static')
|
|
103
|
-
expect(hs.length).toBeGreaterThanOrEqual(1)
|
|
104
|
-
expect(transformJSX_JS(src, 'f.tsx').code).toMatch(/const _\$h\d+ =/)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('reactive attr: class={() => cls()} → reactive-attr', () => {
|
|
108
|
-
const src = `function C(){ return <a class={() => cls()}>x</a> }`
|
|
109
|
-
const ra = find(src, 'reactive-attr')
|
|
110
|
-
expect(ra.length).toBe(1)
|
|
111
|
-
expect(ra[0]!.detail).toContain('class')
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
describe('reactivity-lens — footgun merge (existing detectPyreonPatterns)', () => {
|
|
116
|
-
it('param-destructured props surface a footgun finding with the detector code', () => {
|
|
117
|
-
// detectPyreonPatterns catches the PARAMETER-destructure shape
|
|
118
|
-
// `({ name })`. The body-scope `const {x}=props` shape is the static
|
|
119
|
-
// layer's known cliff (doc-only anti-pattern, no reliable AST detector)
|
|
120
|
-
// — the lens's structural `static-text`/`reactive` signals are what
|
|
121
|
-
// compensate for that downstream. This asserts the merge surfaces
|
|
122
|
-
// whatever the existing detector finds, faithfully.
|
|
123
|
-
const src = `function C({ name }){ return <div>{name}</div> }`
|
|
124
|
-
const fg = find(src, 'footgun')
|
|
125
|
-
expect(fg.length).toBeGreaterThanOrEqual(1)
|
|
126
|
-
expect(fg.some((f) => f.code === 'props-destructured')).toBe(true)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('findings are sorted by (line, column)', () => {
|
|
130
|
-
const src = [
|
|
131
|
-
`function C(props){`,
|
|
132
|
-
` const { a } = props`,
|
|
133
|
-
` return <div>{count()}</div>`,
|
|
134
|
-
`}`,
|
|
135
|
-
].join('\n')
|
|
136
|
-
const { findings } = analyzeReactivity(src)
|
|
137
|
-
for (let i = 1; i < findings.length; i++) {
|
|
138
|
-
const prev = findings[i - 1]!
|
|
139
|
-
const cur = findings[i]!
|
|
140
|
-
expect(
|
|
141
|
-
prev.line < cur.line ||
|
|
142
|
-
(prev.line === cur.line && prev.column <= cur.column),
|
|
143
|
-
).toBe(true)
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('reactivity-lens — zero false "live" on idiomatic code (kill-criterion b)', () => {
|
|
149
|
-
it('purely static component yields no reactive* findings', () => {
|
|
150
|
-
const src = `function Card(){ return <div class="card"><h2>Title</h2><p>Body</p></div> }`
|
|
151
|
-
const k = kindsAt(src)
|
|
152
|
-
expect(k).not.toContain('reactive')
|
|
153
|
-
expect(k).not.toContain('reactive-prop')
|
|
154
|
-
expect(k).not.toContain('reactive-attr')
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('parse failure → empty, never throws', () => {
|
|
158
|
-
const r = analyzeReactivity(`function C( { return <div`)
|
|
159
|
-
expect(Array.isArray(r.findings)).toBe(true)
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('reactivity-lens — formatter', () => {
|
|
164
|
-
it('renders annotated source with kind badges', () => {
|
|
165
|
-
const src = `function C(){ return <div>{count()}</div> }`
|
|
166
|
-
const out = formatReactivityLens(src, analyzeReactivity(src))
|
|
167
|
-
expect(out).toContain('live')
|
|
168
|
-
expect(out).toContain('1 |')
|
|
169
|
-
})
|
|
170
|
-
})
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { rocketstyleCollapseKey, transformJSX } from '../jsx'
|
|
3
|
-
|
|
4
|
-
// Layer 4: the compiler DETECTS a literal-prop rocketstyle call site
|
|
5
|
-
// (bail catalogue — RFC decision 3) and EMITS the collapsed
|
|
6
|
-
// `_rsCollapse(...)` + once-per-module idempotent `injectRules`, when
|
|
7
|
-
// the Vite plugin supplies an SSR-resolved `sites` entry. It never runs
|
|
8
|
-
// the rocketstyle chain itself. These tests stub the resolved `sites`
|
|
9
|
-
// map directly (no Vite) — the real SSR resolution is proven in
|
|
10
|
-
// @pyreon/vite-plugin's resolver test; the end-to-end byte-parity is
|
|
11
|
-
// proven by the ui-showcase e2e gate (Phase 4).
|
|
12
|
-
|
|
13
|
-
const SITE = {
|
|
14
|
-
templateHtml: '<button data-x="1"><span class="inner">Save</span></button>',
|
|
15
|
-
lightClass: 'pyr-L1 pyr-L2',
|
|
16
|
-
darkClass: 'pyr-D1 pyr-D2',
|
|
17
|
-
rules: ['.pyr-L1{color:red}', '.pyr-D1{color:blue}'],
|
|
18
|
-
ruleKey: 'bundleA',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function collapseOpt(candidates: string[], sites: Record<string, typeof SITE>) {
|
|
22
|
-
return {
|
|
23
|
-
collapseRocketstyle: {
|
|
24
|
-
candidates: new Set(candidates),
|
|
25
|
-
sites: new Map(Object.entries(sites)),
|
|
26
|
-
mode: { name: 'useMode', source: '@pyreon/ui-core' },
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('rocketstyleCollapseKey — stable + order-independent', () => {
|
|
32
|
-
it('same component+props+text ⇒ same key regardless of attr order', () => {
|
|
33
|
-
const a = rocketstyleCollapseKey('Button', { state: 'primary', size: 'md' }, 'Save')
|
|
34
|
-
const b = rocketstyleCollapseKey('Button', { size: 'md', state: 'primary' }, 'Save')
|
|
35
|
-
expect(a).toBe(b)
|
|
36
|
-
expect(a).not.toBe(rocketstyleCollapseKey('Button', { state: 'secondary' }, 'Save'))
|
|
37
|
-
expect(a).not.toBe(rocketstyleCollapseKey('Button', { state: 'primary', size: 'md' }, 'Go'))
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('compiler — collapsible call site emission', () => {
|
|
42
|
-
it('emits _rsCollapse + dual-emit mode thunk + once-per-module injectRules', () => {
|
|
43
|
-
const key = rocketstyleCollapseKey('Button', { state: 'primary', size: 'medium' }, 'Save')
|
|
44
|
-
const src = `
|
|
45
|
-
import { Button } from '@pyreon/ui-components'
|
|
46
|
-
export function App() {
|
|
47
|
-
return <Button state="primary" size="medium">Save</Button>
|
|
48
|
-
}`
|
|
49
|
-
const { code } = transformJSX(src, 'App.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
50
|
-
// collapsed call replaces the JSX
|
|
51
|
-
expect(code).toContain(
|
|
52
|
-
'__rsCollapse("<button data-x=\\"1\\"><span class=\\"inner\\">Save</span></button>", "pyr-L1 pyr-L2", "pyr-D1 pyr-D2", () => __pyrMode() === "dark")',
|
|
53
|
-
)
|
|
54
|
-
// dual-emit mode accessor imported from the configured source
|
|
55
|
-
expect(code).toContain('import { useMode as __pyrMode } from "@pyreon/ui-core";')
|
|
56
|
-
// runtime helper + styler sheet imports
|
|
57
|
-
expect(code).toContain('import { _rsCollapse as __rsCollapse } from "@pyreon/runtime-dom";')
|
|
58
|
-
expect(code).toContain('import { sheet as __rsSheet } from "@pyreon/styler";')
|
|
59
|
-
// once-per-module idempotent rule injection, keyed by ruleKey
|
|
60
|
-
expect(code).toContain('__rsSheet.injectRules(')
|
|
61
|
-
expect(code).toContain(JSON.stringify(SITE.rules))
|
|
62
|
-
expect(code).toContain('"bundleA")')
|
|
63
|
-
// the original <Button …> JSX is gone
|
|
64
|
-
expect(code).not.toContain('<Button')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('two identical sites in one module emit ONE injectRules (deduped by ruleKey)', () => {
|
|
68
|
-
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'X')
|
|
69
|
-
const src = `
|
|
70
|
-
import { Button } from '@pyreon/ui-components'
|
|
71
|
-
export const A = () => <Button state="primary">X</Button>
|
|
72
|
-
export const B = () => <Button state="primary">X</Button>`
|
|
73
|
-
const { code } = transformJSX(src, 'M.tsx', collapseOpt(['Button'], { [key]: SITE }))
|
|
74
|
-
const injCount = code.split('__rsSheet.injectRules(').length - 1
|
|
75
|
-
expect(injCount).toBe(1)
|
|
76
|
-
const callCount = code.split('__rsCollapse(').length - 1
|
|
77
|
-
// 2 call sites (the `_rsCollapse as __rsCollapse` import alias has
|
|
78
|
-
// no trailing `(`, so it doesn't count)
|
|
79
|
-
expect(callCount).toBe(2)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
describe('compiler — bail catalogue (RFC decision 3): NO collapse', () => {
|
|
84
|
-
const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Save')
|
|
85
|
-
const sites = { [key]: SITE }
|
|
86
|
-
|
|
87
|
-
function noCollapse(src: string, opt = collapseOpt(['Button'], sites)) {
|
|
88
|
-
const { code } = transformJSX(src, 'B.tsx', opt)
|
|
89
|
-
expect(code).not.toContain('__rsCollapse')
|
|
90
|
-
return code
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
it('bails on a non-literal (signal/expr) dimension prop', () => {
|
|
94
|
-
noCollapse(`
|
|
95
|
-
import { Button } from '@pyreon/ui-components'
|
|
96
|
-
export const A = (p) => <Button state={p.s}>Save</Button>`)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('bails on a JSX spread attribute', () => {
|
|
100
|
-
noCollapse(`
|
|
101
|
-
import { Button } from '@pyreon/ui-components'
|
|
102
|
-
export const A = (p) => <Button state="primary" {...p}>Save</Button>`)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('bails on an element child (non-static-text children)', () => {
|
|
106
|
-
noCollapse(`
|
|
107
|
-
import { Button } from '@pyreon/ui-components'
|
|
108
|
-
export const A = () => <Button state="primary"><i>Save</i></Button>`)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('bails on an expression child', () => {
|
|
112
|
-
noCollapse(`
|
|
113
|
-
import { Button } from '@pyreon/ui-components'
|
|
114
|
-
export const A = (p) => <Button state="primary">{p.label}</Button>`)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('bails when the component is not a registered candidate', () => {
|
|
118
|
-
noCollapse(
|
|
119
|
-
`
|
|
120
|
-
import { Card } from '@pyreon/ui-components'
|
|
121
|
-
export const A = () => <Card state="primary">Save</Card>`,
|
|
122
|
-
collapseOpt(['Button'], sites),
|
|
123
|
-
)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('bails when there is no resolved site for the key (resolver bailed / not data)', () => {
|
|
127
|
-
noCollapse(
|
|
128
|
-
`
|
|
129
|
-
import { Button } from '@pyreon/ui-components'
|
|
130
|
-
export const A = () => <Button state="zzz">Save</Button>`,
|
|
131
|
-
collapseOpt(['Button'], sites),
|
|
132
|
-
)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('does nothing when collapseRocketstyle option is absent (default OFF)', () => {
|
|
136
|
-
const { code } = transformJSX(
|
|
137
|
-
`
|
|
138
|
-
import { Button } from '@pyreon/ui-components'
|
|
139
|
-
export const A = () => <Button state="primary">Save</Button>`,
|
|
140
|
-
'Off.tsx',
|
|
141
|
-
{},
|
|
142
|
-
)
|
|
143
|
-
expect(code).not.toContain('__rsCollapse')
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('bisect: collapse forces the JS path', () => {
|
|
148
|
-
it('emits the collapse even though a native binary may be present', () => {
|
|
149
|
-
// transformJSX must short-circuit to transformJSX_JS when
|
|
150
|
-
// collapseRocketstyle is set (the Rust binary doesn't implement it).
|
|
151
|
-
// If the force-JS guard were removed and a native binary were
|
|
152
|
-
// loaded, this would emit no __rsCollapse — proving the guard is
|
|
153
|
-
// load-bearing. With the guard, JS path always runs.
|
|
154
|
-
const key = rocketstyleCollapseKey('Button', {}, 'Hi')
|
|
155
|
-
const { code } = transformJSX(
|
|
156
|
-
`
|
|
157
|
-
import { Button } from '@pyreon/ui-components'
|
|
158
|
-
export const A = () => <Button>Hi</Button>`,
|
|
159
|
-
'J.tsx',
|
|
160
|
-
collapseOpt(['Button'], { [key]: SITE }),
|
|
161
|
-
)
|
|
162
|
-
expect(code).toContain('__rsCollapse(')
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
describe('scanCollapsibleSites — plugin scanner == compiler detection', () => {
|
|
167
|
-
it('finds the collapsible site with the SAME key the compiler looks up', async () => {
|
|
168
|
-
const { scanCollapsibleSites } = await import('../jsx')
|
|
169
|
-
const src = `
|
|
170
|
-
import { Button as Btn } from '@pyreon/ui-components'
|
|
171
|
-
import { useState } from 'somewhere'
|
|
172
|
-
export const A = () => <Btn state="primary" size="medium">Save</Btn>
|
|
173
|
-
export const B = (p) => <Btn state={p.s}>x</Btn>
|
|
174
|
-
export const C = () => <div state="primary">not a candidate</div>`
|
|
175
|
-
const sites = scanCollapsibleSites(src, 'A.tsx', new Set(['@pyreon/ui-components']))
|
|
176
|
-
// only the literal-prop, static-text <Btn> collapses; the {expr}
|
|
177
|
-
// one and the <div> are bailed (catalogue) / non-candidate.
|
|
178
|
-
expect(sites).toHaveLength(1)
|
|
179
|
-
const s = sites[0]!
|
|
180
|
-
expect(s.componentName).toBe('Btn') // LOCAL alias — key uses this
|
|
181
|
-
expect(s.importedName).toBe('Button') // resolver imports this
|
|
182
|
-
expect(s.source).toBe('@pyreon/ui-components')
|
|
183
|
-
expect(s.props).toEqual({ state: 'primary', size: 'medium' })
|
|
184
|
-
expect(s.childrenText).toBe('Save')
|
|
185
|
-
// The key the plugin computes here MUST equal the key the compiler
|
|
186
|
-
// recomputes from the JSX node — proven by feeding a sites map
|
|
187
|
-
// keyed by s.key and asserting the compiler collapses.
|
|
188
|
-
const { code } = transformJSX(src, 'A.tsx', {
|
|
189
|
-
collapseRocketstyle: {
|
|
190
|
-
candidates: new Set(['Btn']),
|
|
191
|
-
sites: new Map([[s.key, SITE]]),
|
|
192
|
-
mode: { name: 'useMode', source: '@pyreon/ui-core' },
|
|
193
|
-
},
|
|
194
|
-
})
|
|
195
|
-
// exactly the literal-prop site collapsed; the {expr} <Btn> bailed
|
|
196
|
-
// and remains as JSX (1 collapse call, 1 surviving <Btn).
|
|
197
|
-
expect(code.split('__rsCollapse(').length - 1).toBe(1)
|
|
198
|
-
expect(code).toContain('<Btn state={')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('skips a component imported from a non-collapsible source', async () => {
|
|
202
|
-
const { scanCollapsibleSites } = await import('../jsx')
|
|
203
|
-
const src = `
|
|
204
|
-
import { Button } from './local-button'
|
|
205
|
-
export const A = () => <Button state="primary">Save</Button>`
|
|
206
|
-
expect(scanCollapsibleSites(src, 'A.tsx', new Set(['@pyreon/ui-components']))).toHaveLength(0)
|
|
207
|
-
})
|
|
208
|
-
})
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
/// <reference lib="dom" />
|
|
3
|
-
import { For, h, Show } from '@pyreon/core'
|
|
4
|
-
import { signal } from '@pyreon/reactivity'
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
6
|
-
import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compiler-runtime tests — control-flow primitives.
|
|
10
|
-
*
|
|
11
|
-
* These tests verify `<For>` and `<Show>` integrate correctly with the
|
|
12
|
-
* Pyreon mount path. They use direct `h()` calls instead of JSX because
|
|
13
|
-
* the harness's `compileAndMount` runs only the template-optimization
|
|
14
|
-
* pass of `@pyreon/compiler` — the bundler-level JSX → `h()` transform
|
|
15
|
-
* (normally done by Vite's esbuild) does NOT run in the harness, so JSX
|
|
16
|
-
* containing components like `<For>` would be left raw and unparseable.
|
|
17
|
-
*
|
|
18
|
-
* `<Match>`, `<Suspense>`, `<ErrorBoundary>` are deferred to Phase C1
|
|
19
|
-
* because they need real Chromium for the async / boundary shapes.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
describe('compiler-runtime — control flow (h() form)', () => {
|
|
23
|
-
it('<For> renders each item and reacts to signal updates', async () => {
|
|
24
|
-
const items = signal([
|
|
25
|
-
{ id: 1, name: 'a' },
|
|
26
|
-
{ id: 2, name: 'b' },
|
|
27
|
-
])
|
|
28
|
-
const { container, unmount } = mountInBrowser(
|
|
29
|
-
h(
|
|
30
|
-
'div',
|
|
31
|
-
{ id: 'root' },
|
|
32
|
-
h(For, {
|
|
33
|
-
each: items,
|
|
34
|
-
by: (i: { id: number; name: string }) => i.id,
|
|
35
|
-
children: (i: { name: string }) => h('span', null, i.name),
|
|
36
|
-
}),
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
const root = container.querySelector('#root')!
|
|
40
|
-
expect(root.querySelectorAll('span').length).toBe(2)
|
|
41
|
-
expect(root.textContent).toBe('ab')
|
|
42
|
-
items.set([
|
|
43
|
-
{ id: 1, name: 'a' },
|
|
44
|
-
{ id: 2, name: 'b' },
|
|
45
|
-
{ id: 3, name: 'c' },
|
|
46
|
-
])
|
|
47
|
-
await flush()
|
|
48
|
-
expect(root.querySelectorAll('span').length).toBe(3)
|
|
49
|
-
expect(root.textContent).toBe('abc')
|
|
50
|
-
unmount()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('<For> handles removal correctly', async () => {
|
|
54
|
-
const items = signal([
|
|
55
|
-
{ id: 1, name: 'a' },
|
|
56
|
-
{ id: 2, name: 'b' },
|
|
57
|
-
{ id: 3, name: 'c' },
|
|
58
|
-
])
|
|
59
|
-
const { container, unmount } = mountInBrowser(
|
|
60
|
-
h(
|
|
61
|
-
'div',
|
|
62
|
-
{ id: 'root' },
|
|
63
|
-
h(For, {
|
|
64
|
-
each: items,
|
|
65
|
-
by: (i: { id: number; name: string }) => i.id,
|
|
66
|
-
children: (i: { name: string }) => h('span', null, i.name),
|
|
67
|
-
}),
|
|
68
|
-
),
|
|
69
|
-
)
|
|
70
|
-
const root = container.querySelector('#root')!
|
|
71
|
-
expect(root.querySelectorAll('span').length).toBe(3)
|
|
72
|
-
items.set([{ id: 2, name: 'b' }])
|
|
73
|
-
await flush()
|
|
74
|
-
expect(root.querySelectorAll('span').length).toBe(1)
|
|
75
|
-
expect(root.textContent).toBe('b')
|
|
76
|
-
unmount()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('<Show> conditionally renders based on signal', async () => {
|
|
80
|
-
const visible = signal(true)
|
|
81
|
-
const { container, unmount } = mountInBrowser(
|
|
82
|
-
h(
|
|
83
|
-
'div',
|
|
84
|
-
{ id: 'root' },
|
|
85
|
-
h(Show, { when: () => visible(), children: h('span', { id: 'x' }, 'visible') }),
|
|
86
|
-
),
|
|
87
|
-
)
|
|
88
|
-
const root = container.querySelector('#root')!
|
|
89
|
-
expect(root.querySelector('#x')).not.toBeNull()
|
|
90
|
-
visible.set(false)
|
|
91
|
-
await flush()
|
|
92
|
-
expect(root.querySelector('#x')).toBeNull()
|
|
93
|
-
visible.set(true)
|
|
94
|
-
await flush()
|
|
95
|
-
expect(root.querySelector('#x')).not.toBeNull()
|
|
96
|
-
unmount()
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('<Show> with fallback renders fallback when condition is false', async () => {
|
|
100
|
-
const flag = signal(false)
|
|
101
|
-
const { container, unmount } = mountInBrowser(
|
|
102
|
-
h(
|
|
103
|
-
'div',
|
|
104
|
-
{ id: 'root' },
|
|
105
|
-
h(Show, {
|
|
106
|
-
when: () => flag(),
|
|
107
|
-
fallback: h('span', { id: 'fb' }, 'fallback'),
|
|
108
|
-
children: h('span', { id: 'x' }, 'visible'),
|
|
109
|
-
}),
|
|
110
|
-
),
|
|
111
|
-
)
|
|
112
|
-
const root = container.querySelector('#root')!
|
|
113
|
-
expect(root.querySelector('#fb')).not.toBeNull()
|
|
114
|
-
expect(root.querySelector('#x')).toBeNull()
|
|
115
|
-
flag.set(true)
|
|
116
|
-
await flush()
|
|
117
|
-
expect(root.querySelector('#fb')).toBeNull()
|
|
118
|
-
expect(root.querySelector('#x')).not.toBeNull()
|
|
119
|
-
unmount()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('<Show> with value prop (not accessor) accepts boolean', () => {
|
|
123
|
-
// Per #352's `<Show>` defensive normalization fix — `when` accepts
|
|
124
|
-
// both `() => boolean` accessor AND raw boolean (for static cases +
|
|
125
|
-
// signal auto-call edge case).
|
|
126
|
-
const { container, unmount } = mountInBrowser(
|
|
127
|
-
h(
|
|
128
|
-
'div',
|
|
129
|
-
{ id: 'root' },
|
|
130
|
-
h(Show, { when: true, children: h('span', { id: 'x' }, 'on') }),
|
|
131
|
-
),
|
|
132
|
-
)
|
|
133
|
-
expect(container.querySelector('#x')).not.toBeNull()
|
|
134
|
-
unmount()
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('nested control flow: <Show> inside <For>', async () => {
|
|
138
|
-
const items = signal([
|
|
139
|
-
{ id: 1, name: 'a', visible: true },
|
|
140
|
-
{ id: 2, name: 'b', visible: false },
|
|
141
|
-
{ id: 3, name: 'c', visible: true },
|
|
142
|
-
])
|
|
143
|
-
const { container, unmount } = mountInBrowser(
|
|
144
|
-
h(
|
|
145
|
-
'div',
|
|
146
|
-
{ id: 'root' },
|
|
147
|
-
h(For, {
|
|
148
|
-
each: items,
|
|
149
|
-
by: (i: { id: number }) => i.id,
|
|
150
|
-
children: (i: { name: string; visible: boolean }) =>
|
|
151
|
-
h(Show, { when: () => i.visible, children: h('span', null, i.name) }),
|
|
152
|
-
}),
|
|
153
|
-
),
|
|
154
|
-
)
|
|
155
|
-
const root = container.querySelector('#root')!
|
|
156
|
-
expect(root.textContent).toBe('ac')
|
|
157
|
-
unmount()
|
|
158
|
-
})
|
|
159
|
-
})
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
/// <reference lib="dom" />
|
|
3
|
-
import { signal } from '@pyreon/reactivity'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import { flush } from '@pyreon/test-utils/browser'
|
|
6
|
-
import { compileAndMount } from './harness'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compiler-runtime tests — DOM-property assignment.
|
|
10
|
-
*
|
|
11
|
-
* The #352 DOM-property bug used `setAttribute("value", v)` instead of
|
|
12
|
-
* `el.value = v` for IDL properties whose live value diverges from the
|
|
13
|
-
* content attribute. The fix added a `DOM_PROPS` set covering: value,
|
|
14
|
-
* checked, selected, disabled, multiple, readOnly, indeterminate. This
|
|
15
|
-
* file pins down each property + asserts the compiled output uses
|
|
16
|
-
* property assignment so the live state reflects updates correctly.
|
|
17
|
-
*
|
|
18
|
-
* Note: happy-dom's `.value` getter follows the attribute even in
|
|
19
|
-
* static cases, so for `value` specifically the assertion verifies
|
|
20
|
-
* the post-update read works (which would also work via setAttribute
|
|
21
|
-
* in happy-dom — the real differentiator is in real Chromium after a
|
|
22
|
-
* user types). For `checked` / `disabled` / etc., happy-dom DOES
|
|
23
|
-
* differentiate property vs attribute, so those assertions are robust.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
describe('compiler-runtime — DOM properties', () => {
|
|
27
|
-
it('value property reflects signal updates via .value', async () => {
|
|
28
|
-
const text = signal('initial')
|
|
29
|
-
const { container, unmount } = compileAndMount(
|
|
30
|
-
`<div><input id="i" value={() => text()} /></div>`,
|
|
31
|
-
{ text },
|
|
32
|
-
)
|
|
33
|
-
const input = container.querySelector<HTMLInputElement>('#i')!
|
|
34
|
-
expect(input.value).toBe('initial')
|
|
35
|
-
text.set('updated')
|
|
36
|
-
await flush()
|
|
37
|
-
expect(input.value).toBe('updated')
|
|
38
|
-
text.set('')
|
|
39
|
-
await flush()
|
|
40
|
-
expect(input.value).toBe('')
|
|
41
|
-
unmount()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('checked property reflects via .checked (not boolean attribute)', async () => {
|
|
45
|
-
const isOn = signal(true)
|
|
46
|
-
const { container, unmount } = compileAndMount(
|
|
47
|
-
`<div><input id="c" type="checkbox" checked={() => isOn()} /></div>`,
|
|
48
|
-
{ isOn },
|
|
49
|
-
)
|
|
50
|
-
const cb = container.querySelector<HTMLInputElement>('#c')!
|
|
51
|
-
expect(cb.checked).toBe(true)
|
|
52
|
-
isOn.set(false)
|
|
53
|
-
await flush()
|
|
54
|
-
expect(cb.checked).toBe(false)
|
|
55
|
-
isOn.set(true)
|
|
56
|
-
await flush()
|
|
57
|
-
expect(cb.checked).toBe(true)
|
|
58
|
-
unmount()
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('disabled property reflects via .disabled', async () => {
|
|
62
|
-
const off = signal(false)
|
|
63
|
-
const { container, unmount } = compileAndMount(
|
|
64
|
-
`<div><button id="b" disabled={() => off()}>x</button></div>`,
|
|
65
|
-
{ off },
|
|
66
|
-
)
|
|
67
|
-
const btn = container.querySelector<HTMLButtonElement>('#b')!
|
|
68
|
-
expect(btn.disabled).toBe(false)
|
|
69
|
-
off.set(true)
|
|
70
|
-
await flush()
|
|
71
|
-
expect(btn.disabled).toBe(true)
|
|
72
|
-
off.set(false)
|
|
73
|
-
await flush()
|
|
74
|
-
expect(btn.disabled).toBe(false)
|
|
75
|
-
unmount()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('selected on <option> reflects via .selected', async () => {
|
|
79
|
-
// Need a sibling option so the browser's "at least one option must
|
|
80
|
-
// be selected" auto-selection doesn't pick our option after we
|
|
81
|
-
// unselect it.
|
|
82
|
-
const sel = signal(false)
|
|
83
|
-
const { container, unmount } = compileAndMount(
|
|
84
|
-
`<div><select><option>first</option><option id="o" selected={() => sel()}>a</option></select></div>`,
|
|
85
|
-
{ sel },
|
|
86
|
-
)
|
|
87
|
-
const opt = container.querySelector<HTMLOptionElement>('#o')!
|
|
88
|
-
expect(opt.selected).toBe(false)
|
|
89
|
-
sel.set(true)
|
|
90
|
-
await flush()
|
|
91
|
-
expect(opt.selected).toBe(true)
|
|
92
|
-
unmount()
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('multiple on <select> reflects via .multiple', async () => {
|
|
96
|
-
const multi = signal(true)
|
|
97
|
-
const { container, unmount } = compileAndMount(
|
|
98
|
-
`<div><select id="s" multiple={() => multi()}><option>a</option></select></div>`,
|
|
99
|
-
{ multi },
|
|
100
|
-
)
|
|
101
|
-
const sel = container.querySelector<HTMLSelectElement>('#s')!
|
|
102
|
-
expect(sel.multiple).toBe(true)
|
|
103
|
-
multi.set(false)
|
|
104
|
-
await flush()
|
|
105
|
-
expect(sel.multiple).toBe(false)
|
|
106
|
-
unmount()
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('readOnly on <input> reflects via .readOnly', async () => {
|
|
110
|
-
const ro = signal(false)
|
|
111
|
-
const { container, unmount } = compileAndMount(
|
|
112
|
-
`<div><input id="i" readOnly={() => ro()} /></div>`,
|
|
113
|
-
{ ro },
|
|
114
|
-
)
|
|
115
|
-
const input = container.querySelector<HTMLInputElement>('#i')!
|
|
116
|
-
expect(input.readOnly).toBe(false)
|
|
117
|
-
ro.set(true)
|
|
118
|
-
await flush()
|
|
119
|
-
expect(input.readOnly).toBe(true)
|
|
120
|
-
unmount()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('non-DOM-prop attributes still go through setAttribute', async () => {
|
|
124
|
-
// `placeholder` is a real HTML attribute, not a DOM IDL property
|
|
125
|
-
// that diverges. Should still flow through setAttribute (not break).
|
|
126
|
-
const placeholder = signal('type here')
|
|
127
|
-
const { container, unmount } = compileAndMount(
|
|
128
|
-
`<div><input id="i" placeholder={() => placeholder()} /></div>`,
|
|
129
|
-
{ placeholder },
|
|
130
|
-
)
|
|
131
|
-
const input = container.querySelector<HTMLInputElement>('#i')!
|
|
132
|
-
expect(input.getAttribute('placeholder')).toBe('type here')
|
|
133
|
-
placeholder.set('changed')
|
|
134
|
-
await flush()
|
|
135
|
-
expect(input.getAttribute('placeholder')).toBe('changed')
|
|
136
|
-
unmount()
|
|
137
|
-
})
|
|
138
|
-
})
|