@pyreon/compiler 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +11 -13
  2. package/src/defer-inline.ts +0 -686
  3. package/src/event-names.ts +0 -65
  4. package/src/index.ts +0 -61
  5. package/src/island-audit.ts +0 -675
  6. package/src/jsx.ts +0 -2792
  7. package/src/load-native.ts +0 -156
  8. package/src/lpih.ts +0 -270
  9. package/src/manifest.ts +0 -280
  10. package/src/project-scanner.ts +0 -214
  11. package/src/pyreon-intercept.ts +0 -1029
  12. package/src/react-intercept.ts +0 -1217
  13. package/src/reactivity-lens.ts +0 -190
  14. package/src/ssg-audit.ts +0 -513
  15. package/src/test-audit.ts +0 -435
  16. package/src/tests/backend-parity-r7-r9.test.ts +0 -91
  17. package/src/tests/backend-prop-derived-callback-divergence.test.ts +0 -74
  18. package/src/tests/collapse-bail-census.test.ts +0 -330
  19. package/src/tests/collapse-key-source-hygiene.test.ts +0 -88
  20. package/src/tests/component-child-no-wrap.test.ts +0 -204
  21. package/src/tests/defer-inline.test.ts +0 -387
  22. package/src/tests/depth-stress.test.ts +0 -16
  23. package/src/tests/detector-tag-consistency.test.ts +0 -101
  24. package/src/tests/dynamic-collapse-detector.test.ts +0 -164
  25. package/src/tests/dynamic-collapse-emit.test.ts +0 -192
  26. package/src/tests/dynamic-collapse-scan.test.ts +0 -111
  27. package/src/tests/element-valued-const-child.test.ts +0 -61
  28. package/src/tests/falsy-child-characterization.test.ts +0 -48
  29. package/src/tests/island-audit.test.ts +0 -524
  30. package/src/tests/jsx.test.ts +0 -2908
  31. package/src/tests/load-native.test.ts +0 -53
  32. package/src/tests/lpih.test.ts +0 -404
  33. package/src/tests/malformed-input-resilience.test.ts +0 -50
  34. package/src/tests/manifest-snapshot.test.ts +0 -55
  35. package/src/tests/native-equivalence.test.ts +0 -924
  36. package/src/tests/partial-collapse-detector.test.ts +0 -121
  37. package/src/tests/partial-collapse-emit.test.ts +0 -104
  38. package/src/tests/partial-collapse-robustness.test.ts +0 -53
  39. package/src/tests/project-scanner.test.ts +0 -269
  40. package/src/tests/prop-derived-shadow.test.ts +0 -96
  41. package/src/tests/pure-call-reactive-args.test.ts +0 -50
  42. package/src/tests/pyreon-intercept.test.ts +0 -816
  43. package/src/tests/r13-callback-stmt-equivalence.test.ts +0 -58
  44. package/src/tests/r14-ssr-mode-parity.test.ts +0 -51
  45. package/src/tests/r15-elemconst-propderived.test.ts +0 -47
  46. package/src/tests/r19-defer-inline-robust.test.ts +0 -54
  47. package/src/tests/r20-backend-equivalence-sweep.test.ts +0 -50
  48. package/src/tests/react-intercept.test.ts +0 -1104
  49. package/src/tests/reactivity-lens.test.ts +0 -170
  50. package/src/tests/rocketstyle-collapse.test.ts +0 -208
  51. package/src/tests/runtime/control-flow.test.ts +0 -159
  52. package/src/tests/runtime/dom-properties.test.ts +0 -138
  53. package/src/tests/runtime/events.test.ts +0 -301
  54. package/src/tests/runtime/harness.ts +0 -94
  55. package/src/tests/runtime/pr-352-shapes.test.ts +0 -121
  56. package/src/tests/runtime/reactive-props.test.ts +0 -81
  57. package/src/tests/runtime/signals.test.ts +0 -129
  58. package/src/tests/runtime/whitespace.test.ts +0 -106
  59. package/src/tests/signal-autocall-shadow.test.ts +0 -86
  60. package/src/tests/sourcemap-fidelity.test.ts +0 -77
  61. package/src/tests/ssg-audit.test.ts +0 -402
  62. package/src/tests/static-text-baking.test.ts +0 -64
  63. package/src/tests/test-audit.test.ts +0 -549
  64. package/src/tests/transform-state-isolation.test.ts +0 -49
@@ -1,121 +0,0 @@
1
- /**
2
- * PR 1 of the partial-collapse spec (`.claude/plans/open-work-2026-q3.md`
3
- * → #1). The shared `on*`-handler-only detector — the build STARTED, not
4
- * just measured. The bail-reason census (`collapse-bail-census.test.ts`)
5
- * proved 7.8% of all `@pyreon/ui-components` call sites bail SOLELY on
6
- * `on*` handlers while every dimension/style prop is a string literal and
7
- * children are static text; `detectPartialCollapsibleShape` is the exact
8
- * detector that claims that subset.
9
- *
10
- * Contract under test (mirrors the conservative discipline of the full
11
- * `detectCollapsibleShape` — every uncertain signal bails):
12
- *
13
- * - literal-prop + ≥1 `on[A-Z]…` handler + static-text children
14
- * → { props (literals only), childrenText, handlers[] }
15
- * - ZERO handlers → null (that IS the full-collapse shape; the partial
16
- * detector defers so the existing path stays byte-unchanged and the
17
- * two detectors NEVER both claim a site — the load-bearing separation)
18
- * - spread / non-handler `{expr}` prop / boolean attr / element child /
19
- * expression child → null (hard bail, same catalogue as full)
20
- * - handler expr span (`exprStart`/`exprEnd`) slices the EXACT source
21
- * of the `{...}` contents (load-bearing for PR 3's emit, which
22
- * re-emits `code.slice(exprStart, exprEnd)` into `_rsCollapseH`)
23
- *
24
- * Bisect-verify (documented in the PR body): replace the body of
25
- * `detectPartialCollapsibleShape` with `return null` → the 4 POSITIVE
26
- * specs fail with `expected null to be …`; the 6 NEGATIVE specs still
27
- * pass (they assert null). Restore → 10/10. That asymmetry proves the
28
- * positive assertions are load-bearing on the handler-relaxation logic,
29
- * not passing for the wrong reason.
30
- */
31
- import { describe, expect, it } from 'vitest'
32
- import { parseSync } from 'oxc-parser'
33
- import { detectPartialCollapsibleShape } from '../jsx'
34
-
35
- /** Parse a JSX snippet and return its first JSXElement node (the shape
36
- * `tryRocketstyleCollapse` receives in production). */
37
- function firstJsxElement(code: string): any {
38
- const { program } = parseSync('input.tsx', code, { sourceType: 'module', lang: 'tsx' })
39
- let found: any = null
40
- const visit = (node: any): void => {
41
- if (found || !node || typeof node !== 'object') return
42
- if (node.type === 'JSXElement') {
43
- found = node
44
- return
45
- }
46
- for (const k in node) {
47
- const v = node[k]
48
- if (Array.isArray(v)) for (const c of v) visit(c)
49
- else if (v && typeof v === 'object' && typeof v.type === 'string') visit(v)
50
- }
51
- }
52
- visit(program)
53
- return found
54
- }
55
-
56
- const detect = (code: string) => detectPartialCollapsibleShape(firstJsxElement(code), 'Button')
57
-
58
- describe('detectPartialCollapsibleShape — PR 1 (on*-handler-only subset)', () => {
59
- // ── POSITIVE: the partial-collapsible subset ────────────────────────────
60
- it('claims a literal-prop site with one handler', () => {
61
- const code =
62
- 'const x = <Button state="primary" size="medium" onClick={handleClick}>Save</Button>'
63
- const r = detect(code)
64
- expect(r).not.toBeNull()
65
- expect(r!.props).toEqual({ state: 'primary', size: 'medium' })
66
- expect(r!.childrenText).toBe('Save')
67
- expect(r!.handlers).toHaveLength(1)
68
- expect(r!.handlers[0]!.name).toBe('onClick')
69
- expect(code.slice(r!.handlers[0]!.exprStart, r!.handlers[0]!.exprEnd)).toBe('handleClick')
70
- })
71
-
72
- it('peels multiple handlers, keeps literal props out of handlers[]', () => {
73
- const code = 'const x = <Button state="primary" onClick={a} onPointerEnter={b}>Go</Button>'
74
- const r = detect(code)
75
- expect(r).not.toBeNull()
76
- expect(r!.props).toEqual({ state: 'primary' })
77
- expect(r!.handlers.map((h) => h.name)).toEqual(['onClick', 'onPointerEnter'])
78
- })
79
-
80
- it('captures the EXACT expression span for an inline arrow handler', () => {
81
- const code = 'const x = <Button state="primary" onClick={() => doThing(1)}>Y</Button>'
82
- const r = detect(code)
83
- expect(r).not.toBeNull()
84
- expect(code.slice(r!.handlers[0]!.exprStart, r!.handlers[0]!.exprEnd)).toBe('() => doThing(1)')
85
- })
86
-
87
- it('trims static-text children (parity with the full detector)', () => {
88
- const code = 'const x = <Button state="primary" onClick={h}>\n Save\n</Button>'
89
- const r = detect(code)
90
- expect(r).not.toBeNull()
91
- expect(r!.childrenText).toBe('Save')
92
- })
93
-
94
- // ── NEGATIVE: every uncertain shape bails (null) ────────────────────────
95
- it('returns null for ZERO handlers (defers to the full-collapse path)', () => {
96
- // The load-bearing separation: a fully-literal site with no handler is
97
- // the EXISTING full-collapse shape — the partial detector must NOT
98
- // claim it, so the two never both fire on one site.
99
- expect(detect('const x = <Button state="primary">Save</Button>')).toBeNull()
100
- })
101
-
102
- it('returns null for a spread attribute', () => {
103
- expect(detect('const x = <Button {...rest} onClick={h}>X</Button>')).toBeNull()
104
- })
105
-
106
- it('returns null for a non-handler dynamic prop alongside a handler', () => {
107
- expect(detect('const x = <Button state={dyn} onClick={h}>X</Button>')).toBeNull()
108
- })
109
-
110
- it('returns null for a boolean attribute', () => {
111
- expect(detect('const x = <Button disabled onClick={h}>X</Button>')).toBeNull()
112
- })
113
-
114
- it('returns null for an element child', () => {
115
- expect(detect('const x = <Button onClick={h}><span /></Button>')).toBeNull()
116
- })
117
-
118
- it('returns null for an expression child', () => {
119
- expect(detect('const x = <Button onClick={h}>{label}</Button>')).toBeNull()
120
- })
121
- })
@@ -1,104 +0,0 @@
1
- /**
2
- * PR 3/4 of the partial-collapse build (open-work #1) — the compiler
3
- * EMIT half: `tryRocketstyleCollapse` falls back to
4
- * `tryPartialCollapse` (PR 1's `detectPartialCollapsibleShape`) when the
5
- * FULL `detectCollapsibleShape` bails, emitting `__rsCollapseH(...)` +
6
- * the residual handlers object (consumed by PR 2's `_rsCollapseH`,
7
- * #681) instead of bailing to the 5-layer mount.
8
- *
9
- * Mirrors the existing `rocketstyle-collapse.test.ts` harness exactly
10
- * (stubbed resolved-`sites` map — the resolver/plugin scan is the
11
- * CI-exercised half; this proves the emit contract in isolation, same
12
- * as the shipped full-collapse emission specs do).
13
- *
14
- * Bisect-verify (PR body): revert the one fallback line in
15
- * `tryRocketstyleCollapse` (`if (!shape) return tryPartialCollapse(...)`
16
- * → `if (!shape) return false`) → the partial-emit specs fail
17
- * (`__rsCollapseH(` absent) while the FULL-collapse regression spec
18
- * still passes (proving the full path is byte-unchanged — the fallback
19
- * is the only delta). Restore → all pass. Locally bisect-verifiable
20
- * (minimal `../jsx` graph, like PR 1 #679 — no resolver, no built lib).
21
- */
22
- import { describe, expect, it } from 'vitest'
23
- import { rocketstyleCollapseKey, transformJSX } from '../jsx'
24
-
25
- const SITE = {
26
- templateHtml: '<button data-x="1"><span class="inner">Save</span></button>',
27
- lightClass: 'pyr-L1 pyr-L2',
28
- darkClass: 'pyr-D1 pyr-D2',
29
- rules: ['.pyr-L1{color:red}', '.pyr-D1{color:blue}'],
30
- ruleKey: 'bundleA',
31
- }
32
-
33
- function collapseOpt(candidates: string[], sites: Record<string, typeof SITE>) {
34
- return {
35
- collapseRocketstyle: {
36
- candidates: new Set(candidates),
37
- sites: new Map(Object.entries(sites)),
38
- mode: { name: 'useMode', source: '@pyreon/ui-core' },
39
- },
40
- }
41
- }
42
-
43
- describe('compiler — partial-collapse emission (on*-handler-only)', () => {
44
- it('emits __rsCollapseH + handlers object + the _rsCollapseH import', () => {
45
- const key = rocketstyleCollapseKey('Button', { state: 'primary', size: 'medium' }, 'Save')
46
- const src =
47
- 'const x = <Button state="primary" size="medium" onClick={handleClick}>Save</Button>'
48
- const { code } = transformJSX(src, 'App.tsx', collapseOpt(['Button'], { [key]: SITE }))
49
-
50
- expect(code).toContain(
51
- '__rsCollapseH("<button data-x=\\"1\\"><span class=\\"inner\\">Save</span></button>", ' +
52
- '"pyr-L1 pyr-L2", "pyr-D1 pyr-D2", () => __pyrMode() === "dark", ' +
53
- '{ "onClick": (handleClick) })',
54
- )
55
- // The runtime helper is imported alongside `_rsCollapse`.
56
- expect(code).toContain(
57
- 'import { _rsCollapse as __rsCollapse, _rsCollapseH as __rsCollapseH } from "@pyreon/runtime-dom";',
58
- )
59
- // Idempotent rule injection still emitted (same as the full path).
60
- expect(code).toContain('__rsSheet.injectRules(')
61
- expect(code).toContain('"bundleA"')
62
- })
63
-
64
- it('peels multiple handlers verbatim (arrow stays one arg via parens)', () => {
65
- const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Go')
66
- const src = 'const x = <Button state="primary" onClick={a} onPointerEnter={() => b(1)}>Go</Button>'
67
- const { code } = transformJSX(src, 'M.tsx', collapseOpt(['Button'], { [key]: SITE }))
68
- expect(code).toContain('{ "onClick": (a), "onPointerEnter": (() => b(1)) }')
69
- expect(code).toContain('__rsCollapseH(')
70
- })
71
-
72
- it('FULL-collapse path is byte-unchanged (regression): no-handler site still emits plain __rsCollapse', () => {
73
- const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'Save')
74
- const src = 'const x = <Button state="primary">Save</Button>'
75
- const { code } = transformJSX(src, 'F.tsx', collapseOpt(['Button'], { [key]: SITE }))
76
- // Plain `__rsCollapse(` call — NOT the H variant — and the import
77
- // must NOT pull `_rsCollapseH` (the conditional stays off).
78
- expect(code).toContain('__rsCollapse("<button data-x=\\"1\\"')
79
- expect(code).not.toContain('__rsCollapseH(')
80
- expect(code).toContain('import { _rsCollapse as __rsCollapse } from "@pyreon/runtime-dom";')
81
- expect(code).not.toContain('_rsCollapseH as __rsCollapseH')
82
- })
83
-
84
- it('bails (no collapse) on a non-handler dynamic prop alongside a handler', () => {
85
- const key = rocketstyleCollapseKey('Button', { state: 'primary' }, 'X')
86
- const src = 'const x = <Button state="primary" foo={dyn} onClick={h}>X</Button>'
87
- const { code } = transformJSX(src, 'B1.tsx', collapseOpt(['Button'], { [key]: SITE }))
88
- expect(code).not.toContain('__rsCollapseH')
89
- })
90
-
91
- it('bails when the handler-site key has no resolved entry (resolver bailed)', () => {
92
- const otherKey = rocketstyleCollapseKey('Button', { state: 'secondary' }, 'X')
93
- const src = 'const x = <Button state="primary" onClick={h}>X</Button>'
94
- // Only `secondary` is resolved; the `primary` partial site is not.
95
- const { code } = transformJSX(src, 'B2.tsx', collapseOpt(['Button'], { [otherKey]: SITE }))
96
- expect(code).not.toContain('__rsCollapseH')
97
- })
98
-
99
- it('does nothing when collapseRocketstyle is absent (default OFF)', () => {
100
- const src = 'const x = <Button state="primary" onClick={h}>X</Button>'
101
- const { code } = transformJSX(src, 'Off.tsx', {})
102
- expect(code).not.toContain('__rsCollapseH')
103
- })
104
- })
@@ -1,53 +0,0 @@
1
- /**
2
- * Compiler hardening — Round 8 (robustness gate; no bug found).
3
- *
4
- * The partial-collapse emit path (#683 `tryPartialCollapse` →
5
- * `__rsCollapseH(...)`, runtime #681, e2e #684) shipped with a happy-path
6
- * emission test only. This adversarial gate locks its SAFETY contract under
7
- * inputs that must BAIL or emit cleanly — never throw, never emit
8
- * un-parseable JS: handlers with commas/ternaries/nested-braces/JSX-in-body/
9
- * template-literals/signal-closures, multi-handler, dynamic non-handler prop,
10
- * spread, `onClick={undefined}`, and key-miss. All currently pass (the path
11
- * is robust); this prevents a future regression in the new code from silently
12
- * shipping broken collapsed output.
13
- */
14
- import { parseSync } from 'oxc-parser'
15
- import { describe, expect, it } from 'vitest'
16
- import { rocketstyleCollapseKey, transformJSX } from '../jsx'
17
-
18
- const SITE = { templateHtml: '<button><span>Save</span></button>', lightClass: 'L', darkClass: 'D', rules: ['.L{}'], ruleKey: 'b' }
19
- const opt = (sites: Record<string, typeof SITE>) => ({
20
- collapseRocketstyle: { candidates: new Set(['Button']), sites: new Map(Object.entries(sites)), mode: { name: 'useMode', source: '@pyreon/ui-core' } },
21
- })
22
- const reparses = (c: string): boolean => { try { return !(parseSync('o.tsx', c).errors?.length) } catch { return false } }
23
-
24
- const CASES: Array<[string, string, Record<string, string>]> = [
25
- ['multi-handler', `const x = <Button state="primary" onClick={a} onPointerEnter={b}>Save</Button>`, { state: 'primary' }],
26
- ['arrow-with-commas', `const x = <Button state="primary" onClick={() => f(a, b, c)}>Save</Button>`, { state: 'primary' }],
27
- ['ternary-handler', `const x = <Button state="primary" onClick={cond ? h1 : h2}>Save</Button>`, { state: 'primary' }],
28
- ['nested-braces-handler', `const x = <Button state="primary" onClick={() => { const o = {a:1}; g(o) }}>Save</Button>`, { state: 'primary' }],
29
- ['signal-closure-handler', `const x = <Button state="primary" onClick={() => s.set(s() + 1)}>Save</Button>`, { state: 'primary' }],
30
- ['jsx-in-handler-body', `const x = <Button state="primary" onClick={() => render(<i/>)}>Save</Button>`, { state: 'primary' }],
31
- ['template-literal-in-handler', `const x = <Button state="primary" onClick={() => log(\`v=\${y}\`)}>Save</Button>`, { state: 'primary' }],
32
- ['dynamic-non-handler-prop-bails', `const x = <Button state={dyn} onClick={h}>Save</Button>`, { state: 'primary' }],
33
- ['spread-bails', `const x = <Button state="primary" {...rest} onClick={h}>Save</Button>`, { state: 'primary' }],
34
- ['onClick-undefined', `const x = <Button state="primary" onClick={undefined}>Save</Button>`, { state: 'primary' }],
35
- ['key-miss-no-collapse', `const x = <Button state="primary" onClick={h}>Save</Button>`, { size: 'x' }],
36
- ]
37
-
38
- describe('Round 8 — partial-collapse emit is robust (never throws / never emits broken JS)', () => {
39
- for (const [name, src, props] of CASES) {
40
- it(name, () => {
41
- const key = rocketstyleCollapseKey('Button', props, 'Save')
42
- let code = ''
43
- let threw: unknown = null
44
- try {
45
- code = transformJSX(src, 'App.tsx', opt({ [key]: SITE })).code ?? ''
46
- } catch (e) {
47
- threw = e
48
- }
49
- expect(threw, `partial-collapse must not throw on: ${src}`).toBeNull()
50
- expect(reparses(code), `partial-collapse must emit parseable JS for: ${src}`).toBe(true)
51
- })
52
- }
53
- })
@@ -1,269 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import * as os from 'node:os'
3
- import * as path from 'node:path'
4
- import { generateContext } from '../project-scanner'
5
-
6
- /** Create a temporary directory with the given file structure. */
7
- function createTempProject(files: Record<string, string>): string {
8
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pyreon-scanner-'))
9
- for (const [filePath, content] of Object.entries(files)) {
10
- const fullPath = path.join(dir, filePath)
11
- fs.mkdirSync(path.dirname(fullPath), { recursive: true })
12
- fs.writeFileSync(fullPath, content)
13
- }
14
- return dir
15
- }
16
-
17
- function cleanupDir(dir: string): void {
18
- fs.rmSync(dir, { recursive: true, force: true })
19
- }
20
-
21
- describe('project-scanner — generateContext', () => {
22
- test('returns valid ProjectContext shape for empty project', () => {
23
- const dir = createTempProject({
24
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
25
- })
26
- try {
27
- const ctx = generateContext(dir)
28
- expect(ctx.framework).toBe('pyreon')
29
- expect(ctx.routes).toEqual([])
30
- expect(ctx.components).toEqual([])
31
- expect(ctx.islands).toEqual([])
32
- expect(ctx.generatedAt).toBeTruthy()
33
- } finally {
34
- cleanupDir(dir)
35
- }
36
- })
37
-
38
- test('reads version from @pyreon/* dependency', () => {
39
- const dir = createTempProject({
40
- 'package.json': JSON.stringify({
41
- name: 'test',
42
- dependencies: { '@pyreon/core': '^0.7.0' },
43
- }),
44
- })
45
- try {
46
- const ctx = generateContext(dir)
47
- expect(ctx.version).toBe('0.7.0')
48
- } finally {
49
- cleanupDir(dir)
50
- }
51
- })
52
-
53
- test('falls back to package version when no @pyreon deps', () => {
54
- const dir = createTempProject({
55
- 'package.json': JSON.stringify({ name: 'test', version: '2.0.0' }),
56
- })
57
- try {
58
- const ctx = generateContext(dir)
59
- expect(ctx.version).toBe('2.0.0')
60
- } finally {
61
- cleanupDir(dir)
62
- }
63
- })
64
-
65
- test("returns 'unknown' version when no package.json", () => {
66
- const dir = createTempProject({})
67
- try {
68
- const ctx = generateContext(dir)
69
- expect(ctx.version).toBe('unknown')
70
- } finally {
71
- cleanupDir(dir)
72
- }
73
- })
74
- })
75
-
76
- describe('project-scanner — extractRoutes', () => {
77
- test('extracts routes from createRouter call', () => {
78
- const dir = createTempProject({
79
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
80
- 'src/router.ts': `
81
- const router = createRouter([
82
- { path: "/", name: "home", component: Home },
83
- { path: "/users/:id", name: "user", loader: fetchUser },
84
- { path: "/admin", beforeEnter: authGuard },
85
- ])
86
- `,
87
- })
88
- try {
89
- const ctx = generateContext(dir)
90
- expect(ctx.routes).toHaveLength(3)
91
- expect(ctx.routes[0]?.path).toBe('/')
92
- expect(ctx.routes[0]?.name).toBe('home')
93
- expect(ctx.routes[1]?.path).toBe('/users/:id')
94
- expect(ctx.routes[1]?.params).toEqual(['id'])
95
- expect(ctx.routes[1]?.hasLoader).toBe(true)
96
- expect(ctx.routes[2]?.path).toBe('/admin')
97
- expect(ctx.routes[2]?.hasGuard).toBe(true)
98
- } finally {
99
- cleanupDir(dir)
100
- }
101
- })
102
-
103
- test('extracts routes from const routes = [] pattern', () => {
104
- const dir = createTempProject({
105
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
106
- 'src/routes.ts': `
107
- const routes: RouteRecord[] = [
108
- { path: "/about", name: "about" },
109
- ]
110
- `,
111
- })
112
- try {
113
- const ctx = generateContext(dir)
114
- expect(ctx.routes).toHaveLength(1)
115
- expect(ctx.routes[0]?.path).toBe('/about')
116
- expect(ctx.routes[0]?.name).toBe('about')
117
- } finally {
118
- cleanupDir(dir)
119
- }
120
- })
121
-
122
- test('extracts multiple params from route path', () => {
123
- const dir = createTempProject({
124
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
125
- 'src/router.ts': `
126
- const router = createRouter([
127
- { path: "/users/:userId/posts/:postId" },
128
- ])
129
- `,
130
- })
131
- try {
132
- const ctx = generateContext(dir)
133
- expect(ctx.routes[0]?.params).toEqual(['userId', 'postId'])
134
- } finally {
135
- cleanupDir(dir)
136
- }
137
- })
138
- })
139
-
140
- describe('project-scanner — extractComponents', () => {
141
- test('extracts component with signals', () => {
142
- const dir = createTempProject({
143
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
144
- 'src/Counter.tsx': `
145
- export const Counter = ({ initial }) => {
146
- const count = signal<number>(0)
147
- const doubled = signal(0)
148
- return <div>{count()}</div>
149
- }
150
- `,
151
- })
152
- try {
153
- const ctx = generateContext(dir)
154
- const counter = ctx.components.find((c) => c.name === 'Counter')
155
- expect(counter).toBeTruthy()
156
- expect(counter?.hasSignals).toBe(true)
157
- expect(counter?.signalNames).toEqual(['count', 'doubled'])
158
- } finally {
159
- cleanupDir(dir)
160
- }
161
- })
162
-
163
- test('extracts component without signals', () => {
164
- const dir = createTempProject({
165
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
166
- 'src/Header.tsx': `
167
- export function Header({ title }) {
168
- return <h1>{title}</h1>
169
- }
170
- `,
171
- })
172
- try {
173
- const ctx = generateContext(dir)
174
- const header = ctx.components.find((c) => c.name === 'Header')
175
- expect(header).toBeTruthy()
176
- expect(header?.hasSignals).toBe(false)
177
- expect(header?.signalNames).toEqual([])
178
- } finally {
179
- cleanupDir(dir)
180
- }
181
- })
182
- })
183
-
184
- describe('project-scanner — extractIslands', () => {
185
- test('extracts island definitions', () => {
186
- const dir = createTempProject({
187
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
188
- 'src/islands.ts': `
189
- const Counter = island(() => import("./Counter"), { hydrate: "visible", name: "Counter" })
190
- const Nav = island(() => import("./Nav"), { name: "Nav" })
191
- `,
192
- })
193
- try {
194
- const ctx = generateContext(dir)
195
- expect(ctx.islands).toHaveLength(2)
196
- const counter = ctx.islands.find((i) => i.name === 'Counter')
197
- expect(counter).toBeTruthy()
198
- // hydrate comes before name in the object literal, but the regex captures
199
- // name first, so hydrate is only captured when it follows name directly
200
- const nav = ctx.islands.find((i) => i.name === 'Nav')
201
- expect(nav?.hydrate).toBe('load') // default when hydrate not specified
202
- } finally {
203
- cleanupDir(dir)
204
- }
205
- })
206
- })
207
-
208
- describe('project-scanner — collectSourceFiles', () => {
209
- test('skips node_modules and dist directories', () => {
210
- const dir = createTempProject({
211
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
212
- 'src/app.tsx': 'export const App = () => <div />',
213
- 'node_modules/pkg/index.ts': 'export const x = 1',
214
- 'dist/app.js': 'export const App = () => {}',
215
- })
216
- try {
217
- const ctx = generateContext(dir)
218
- // Should only find the component in src/, not in node_modules or dist
219
- expect(ctx.components.length).toBeGreaterThanOrEqual(1)
220
- const files = ctx.components.map((c) => c.file)
221
- for (const f of files) {
222
- expect(f).not.toContain('node_modules')
223
- expect(f).not.toContain('dist')
224
- }
225
- } finally {
226
- cleanupDir(dir)
227
- }
228
- })
229
-
230
- test('handles non-existent directory gracefully', () => {
231
- const dir = path.join(os.tmpdir(), `pyreon-scanner-nonexistent-${Date.now()}`)
232
- // generateContext calls collectSourceFiles which catches readdir errors
233
- const ctx = generateContext(dir)
234
- expect(ctx.routes).toEqual([])
235
- expect(ctx.components).toEqual([])
236
- expect(ctx.islands).toEqual([])
237
- expect(ctx.version).toBe('unknown')
238
- })
239
-
240
- test('skips hidden directories (dot-prefixed)', () => {
241
- const dir = createTempProject({
242
- 'package.json': JSON.stringify({ name: 'test', version: '1.0.0' }),
243
- 'src/app.tsx': 'export const App = () => <div />',
244
- '.hidden/secret.tsx': 'export const Secret = () => <div />',
245
- })
246
- try {
247
- const ctx = generateContext(dir)
248
- const files = ctx.components.map((c) => c.file)
249
- for (const f of files) {
250
- expect(f).not.toContain('.hidden')
251
- }
252
- } finally {
253
- cleanupDir(dir)
254
- }
255
- })
256
-
257
- test('version falls back to "unknown" when package.json has no version', () => {
258
- const dir = createTempProject({
259
- 'package.json': JSON.stringify({ name: 'test' }),
260
- })
261
- try {
262
- const ctx = generateContext(dir)
263
- // No @pyreon deps and no version field → 'unknown' (line 210 branch)
264
- expect(ctx.version).toBe('unknown')
265
- } finally {
266
- cleanupDir(dir)
267
- }
268
- })
269
- })
@@ -1,96 +0,0 @@
1
- /**
2
- * Compiler hardening — Round 2.
3
- *
4
- * The prop-derived reactive-props inlining pass (`resolveIdentifiersInText`
5
- * in jsx.ts) used to substitute every identifier matching a prop-derived
6
- * const NAME, with zero lexical-scope analysis. Idiomatic code that reuses a
7
- * short name (`a` / `x` / `i` / `item`) as a callback param or nested local
8
- * while a prop-derived const of the same name exists was MISCOMPILED:
9
- *
10
- * const a = props.x
11
- * items.map(a => <li>{a}</li>)
12
- * → items.map((props.x) => <li>{(props.x)}</li>) // un-parseable JS
13
- *
14
- * Two cases emitted literally un-parseable JavaScript (arrow-param,
15
- * catch-param); the rest silently rebound the wrong identifier. The
16
- * signal-auto-call pass was already scope-aware (`shadowedSignals`); the fix
17
- * gives the prop-derived pass the same block-accurate shadow discipline.
18
- *
19
- * Bisect: revert `scopeBoundPropDerived` / the `!shadowed.has(...)` guard in
20
- * jsx.ts → the SHADOW specs fail (un-parseable emit / wrong substitution);
21
- * the no-shadow + transitive specs keep passing (they are the over-suppression
22
- * guard — the fix must NOT stop inlining where there is no shadow). Restore →
23
- * all pass.
24
- */
25
- import { parseSync } from 'oxc-parser'
26
- import { describe, expect, it } from 'vitest'
27
- import { transformJSX_JS } from '../jsx'
28
-
29
- const emit = (code: string): string => transformJSX_JS(code, 'c.tsx').code ?? ''
30
- const parses = (out: string): boolean => {
31
- try {
32
- return (parseSync('o.tsx', out).errors?.length ?? 0) === 0
33
- } catch {
34
- return false
35
- }
36
- }
37
-
38
- describe('prop-derived inlining — lexical shadowing is respected', () => {
39
- it('arrow-function parameter shadowing a prop-derived const is NOT rewritten (was un-parseable)', () => {
40
- const out = emit(`function C(props){ const a = props.x; return <ul>{props.items.map(a => <li>{a}</li>)}</ul> }`)
41
- expect(parses(out)).toBe(true)
42
- expect(out).not.toContain('(props.x) =>')
43
- expect(out).toContain('.map(a => <li>{a}</li>)')
44
- })
45
-
46
- it('catch-clause parameter shadowing a prop-derived const is NOT rewritten (was un-parseable)', () => {
47
- const out = emit(`function C(props){ const e = props.err; const h = () => { try {} catch (e) { return e } }; return <i>{h()}</i> }`)
48
- expect(parses(out)).toBe(true)
49
- expect(out).toContain('catch (e)')
50
- expect(out).toContain('return e')
51
- })
52
-
53
- it('named-function parameter shadowing a prop-derived const keeps the parameter binding', () => {
54
- const out = emit(`function C(props){ const x = props.x; function row(x){ return <td>{x}</td> } return <table>{props.rows.map(row)}</table> }`)
55
- expect(parses(out)).toBe(true)
56
- expect(out).toContain('function row(x)')
57
- expect(out).toMatch(/__t0\.data = x\b/) // the row PARAM, not (props.x)
58
- })
59
-
60
- it('nested const shadowing a prop-derived const is not clobbered', () => {
61
- const out = emit(`function C(props){ const a = props.x; const g = () => { const a = 7; return a }; return <b>{g()}</b> }`)
62
- expect(parses(out)).toBe(true)
63
- expect(out).toContain('const a = 7; return a')
64
- expect(out).not.toContain('const a = 7; return (props.x)')
65
- })
66
-
67
- it('block-scoped shadow does NOT over-suppress an outer-scope reference (block accuracy)', () => {
68
- const out = emit(`function C(props){ const a = props.x; { const a = 'inner'; } return <div>{a}</div> }`)
69
- expect(out).toContain('(props.x)') // {a} is OUTSIDE the shadowing block → still inlined
70
- })
71
- })
72
-
73
- describe('prop-derived inlining — no-shadow paths still inline (over-suppression guard)', () => {
74
- it('non-shadowing callback still inlines the prop-derived const', () => {
75
- const out = emit(`function C(props){ const a = props.x; return <ul>{props.items.map(it => <li>{a}{it}</li>)}</ul> }`)
76
- expect(out).toContain('(props.x)')
77
- expect(out).toContain('it') // the genuine callback param is untouched
78
- })
79
-
80
- it('transitive chain still resolves through to props', () => {
81
- const out = emit(`function C(props){ const a = props.x; const b = a + 1; const c = b * 2; return <div>{c}</div> }`)
82
- expect(out).toContain('(((props.x) + 1) * 2)')
83
- })
84
-
85
- // Scope PRECISION: a `for (let i=…)` head block-scopes `i` to the loop. A
86
- // `return i` AFTER the loop is out of the loop's scope and resolves to the
87
- // OUTER prop-derived `const i` — so it MUST still inline. (Proven: in plain
88
- // JS, `const i='OUTER'; (() => { for(let i=0;i<3;i++){} return i })()` ===
89
- // 'OUTER'.) This guards against an over-broad shadow heuristic that would
90
- // wrongly treat the non-escaping loop var as a shadow at the post-loop use.
91
- it('does NOT over-suppress: post-loop reference is the outer prop-derived const', () => {
92
- const out = emit(`function C(props){ const i = props.start; const f = () => { for (let i=0;i<3;i++){} return i }; return <s>{f()}</s> }`)
93
- expect(parses(out)).toBe(true)
94
- expect(out).toContain('return (props.start)')
95
- })
96
- })