@pyreon/compiler 0.24.4 → 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,156 +0,0 @@
1
- /**
2
- * Native binding loader — resolves the @pyreon/compiler napi-rs binary
3
- * via two paths in priority order:
4
- *
5
- * 1. **In-tree binary** at `<package>/native/pyreon-compiler.node`.
6
- * Populated by `scripts/build-native.ts` during local development
7
- * (Phase 2). Faster path because it skips npm-package resolution.
8
- *
9
- * 2. **Per-platform npm package** (Phase 5b — not active until per-
10
- * platform packages are published). Resolves `@pyreon/compiler-
11
- * <platform>-<arch>[-<libc>]` via the standard Node module
12
- * resolution algorithm. End users on machines without a local
13
- * `cargo` install will hit this path: `bun install` resolves
14
- * `optionalDependencies` to the matching per-platform package and
15
- * this loader picks it up.
16
- *
17
- * 3. **JS fallback** (caller's responsibility) — if both paths fail,
18
- * `loadNativeBinding()` returns `null` and the caller uses the
19
- * pure-JS implementation. Slower but correctness-equivalent.
20
- *
21
- * Platform detection follows the napi-rs convention. Linux variants
22
- * include a `libc` suffix (`gnu` for glibc, `musl` for musl) per
23
- * https://napi.rs/docs/cli/build#deployment.
24
- *
25
- * The two-path resolution lets dev-mode (where `cargo build` produced
26
- * an in-tree binary) and production-mode (where the user has only the
27
- * published per-platform package) coexist with no flag flipping.
28
- */
29
-
30
- import { createRequire } from 'node:module'
31
- import { fileURLToPath } from 'node:url'
32
- import { dirname, join } from 'node:path'
33
-
34
- export interface NativeBinding {
35
- transformJsx: (
36
- code: string,
37
- filename: string,
38
- ssr: boolean,
39
- knownSignals: string[] | null,
40
- reactivityLens: boolean,
41
- ) => unknown
42
- }
43
-
44
- // Local Node-process surface. `@pyreon/runtime-dom` ships an ambient
45
- // `declare var process: { env: { NODE_ENV?: string } }` to enforce the
46
- // bundler-agnostic dev-gate pattern, which narrows `process` for ANY
47
- // file pulled in by runtime-dom's typecheck — including this one when
48
- // imported via the `bun` condition. Casting through a local interface
49
- // restores access to the platform/arch/report fields we genuinely need.
50
- interface NodeProcess {
51
- platform: string
52
- arch: string
53
- report?: {
54
- getReport(): unknown
55
- }
56
- }
57
- const nodeProcess = process as unknown as NodeProcess
58
-
59
- /**
60
- * Resolve the per-platform package name following the napi-rs naming
61
- * convention: `@pyreon/compiler-<platform>-<arch>[-<libc>]`.
62
- *
63
- * Examples:
64
- * darwin + arm64 → @pyreon/compiler-darwin-arm64
65
- * darwin + x64 → @pyreon/compiler-darwin-x64
66
- * linux + x64 + gnu → @pyreon/compiler-linux-x64-gnu
67
- * linux + arm64 + gnu → @pyreon/compiler-linux-arm64-gnu
68
- * win32 + x64 + msvc → @pyreon/compiler-win32-x64-msvc
69
- *
70
- * Returns `null` for unsupported (platform, arch) combinations — caller
71
- * skips per-platform resolution entirely and falls through to JS.
72
- */
73
- export function getPlatformPackageName(
74
- platform: string = nodeProcess.platform,
75
- arch: string = nodeProcess.arch,
76
- libc: string | null = detectLibc(platform),
77
- ): string | null {
78
- // Build the suffix for libc-bearing platforms (Linux glibc/musl,
79
- // Windows MSVC). Single source of truth — no per-platform branching.
80
- const suffix = libc ? `-${libc}` : ''
81
- // Allowlist of (platform, arch) combos that the cross-platform CI
82
- // workflow actually builds. Keep in sync with
83
- // `.github/workflows/release-native.yml` matrix.
84
- const supported: Record<string, string[]> = {
85
- darwin: ['arm64', 'x64'],
86
- linux: ['x64', 'arm64'],
87
- win32: ['x64'],
88
- }
89
- if (!supported[platform]?.includes(arch)) return null
90
- return `@pyreon/compiler-${platform}-${arch}${suffix}`
91
- }
92
-
93
- /**
94
- * Detect the libc family for the current Linux runtime. Returns:
95
- * - `'gnu'` on glibc-based distros (Debian, Ubuntu, RHEL, …)
96
- * - `'musl'` on musl-based distros (Alpine, …)
97
- * - `null` on macOS / Windows (no libc differentiation)
98
- * - `'msvc'` on Windows (we only ship MSVC binaries)
99
- *
100
- * `process.report.getReport().header.glibcVersionRuntime` is the
101
- * Node-canonical detection: present on glibc, absent on musl. Falls
102
- * back to `gnu` on read failure since glibc is the more common case.
103
- */
104
- function detectLibc(platform: string): string | null {
105
- if (platform === 'win32') return 'msvc'
106
- if (platform !== 'linux') return null
107
- try {
108
- const report = nodeProcess.report?.getReport()
109
- if (typeof report === 'object' && report !== null) {
110
- const header = (report as { header?: { glibcVersionRuntime?: string } }).header
111
- return header?.glibcVersionRuntime ? 'gnu' : 'musl'
112
- }
113
- } catch {
114
- // Best-effort detection — fall through to glibc default.
115
- }
116
- return 'gnu'
117
- }
118
-
119
- /**
120
- * Load the native binding by trying paths in order:
121
- * 1. In-tree binary (`<package>/native/pyreon-compiler.node`)
122
- * 2. Per-platform npm package (`@pyreon/compiler-<triple>`)
123
- *
124
- * Returns `null` if both paths fail — caller falls back to the
125
- * pure-JS implementation. NEVER throws — every error path swallows
126
- * silently because a missing native binary is a perf optimization
127
- * miss, not a correctness failure.
128
- */
129
- export function loadNativeBinding(metaUrl: string): NativeBinding | null {
130
- const nativeRequire = createRequire(metaUrl)
131
-
132
- // Path 1: in-tree binary (dev mode + Phase 2 local-build path).
133
- try {
134
- const __filename = fileURLToPath(metaUrl)
135
- const __dirname = dirname(__filename)
136
- const nativePath = join(__dirname, '..', 'native', 'pyreon-compiler.node')
137
- return nativeRequire(nativePath) as NativeBinding
138
- } catch {
139
- // In-tree binary not present — fall through to per-platform package.
140
- }
141
-
142
- // Path 2: per-platform npm package (production install path).
143
- // Will start working once Phase 5b publishes the per-platform
144
- // packages and `optionalDependencies` resolves them at install time.
145
- const pkgName = getPlatformPackageName()
146
- if (pkgName !== null) {
147
- try {
148
- return nativeRequire(pkgName) as NativeBinding
149
- } catch {
150
- // Per-platform package not installed (typical pre-Phase-5b
151
- // state, or a platform we don't yet ship binaries for).
152
- }
153
- }
154
-
155
- return null
156
- }
package/src/lpih.ts DELETED
@@ -1,270 +0,0 @@
1
- /**
2
- * Live Program Inlay Hints (LPIH) — merge runtime fire data onto static
3
- * Reactivity-Lens findings.
4
- *
5
- * This is a PURE function. The runtime side (`@pyreon/reactivity`)
6
- * captures source locations at signal/computed/effect creation and emits
7
- * fire counts via `getFireSummaries()`. The editor/LSP side calls
8
- * `analyzeReactivity()` to get static findings. This module bridges them:
9
- * given findings + fires, produces enriched findings whose `detail` field
10
- * carries the live fire count.
11
- *
12
- * No I/O, no devtools dependency. The LSP transport is the consumer's
13
- * responsibility (read fires from a cache file, an IPC bridge, or
14
- * the editor's devtools panel — whatever the editor extension wants).
15
- *
16
- * ## The category
17
- *
18
- * Editors today show STATIC errors, types, and lint warnings at the
19
- * cursor. They do NOT show LIVE program data — "this signal fires 240×
20
- * per second", "this effect re-runs 3× per render", "this computed has
21
- * 12 downstream subscribers". That data lives in a separate devtools
22
- * panel that the developer has to context-switch to.
23
- *
24
- * LPIH closes that gap: live runtime data appears AT THE SOURCE LINE
25
- * via LSP inlay hints, like a type annotation or a TypeScript error.
26
- * No category like this exists for any reactive framework today.
27
- *
28
- * @example
29
- * import { analyzeReactivity, mergeFireDataIntoFindings } from '@pyreon/compiler'
30
- * import { getFireSummaries } from '@pyreon/reactivity'
31
- *
32
- * const code = `const count = signal(0)\nreturn <div>{count()}</div>`
33
- * const { findings } = analyzeReactivity(code, 'app.tsx')
34
- * const fires = getFireSummaries().map(s => ({
35
- * file: s.loc.file, line: s.loc.line, count: s.count, kind: s.kind,
36
- * }))
37
- * const enriched = mergeFireDataIntoFindings(findings, fires, 'app.tsx')
38
- * // enriched[0].detail might now be "live — signal fired 240×"
39
- */
40
-
41
- import type { ReactivityFinding, ReactivityFindingKind } from './reactivity-lens'
42
-
43
- /**
44
- * Runtime fire data carried into the merge function. Shape mirrors
45
- * `@pyreon/reactivity`'s `FireSummary` but is duplicated here to keep
46
- * `@pyreon/compiler` free of a runtime-package import. The consumer
47
- * adapts the shape at the call site.
48
- */
49
- export interface LPIHFireDatum {
50
- /** Source file path captured from `new Error().stack`. */
51
- file: string
52
- /** 1-based line number (V8 stack format). */
53
- line: number
54
- /** Total fires recorded at this location. */
55
- count: number
56
- /** `performance.now()` of most recent fire, or null. */
57
- lastFire?: number | null | undefined
58
- /** Node kind that fired (signal / derived / effect). */
59
- kind?: 'signal' | 'derived' | 'effect' | undefined
60
- /**
61
- * Exponentially-decayed fire rate, fires/sec (1s time constant). 0
62
- * when the node has been idle longer than several time constants.
63
- * Used by the default formatter to add a "12/s" suffix when active.
64
- * See `@pyreon/reactivity`'s `FireSummary.rate1s` for the math.
65
- */
66
- rate1s?: number | undefined
67
- }
68
-
69
- /** Options for `mergeFireDataIntoFindings`. */
70
- export interface LPIHMergeOptions {
71
- /**
72
- * Optional file-path normalizer. Used for both the analyzed source
73
- * file and each fire's `file` field. Useful when fires come from
74
- * runtime stacks (absolute paths) but the source file is identified
75
- * relative (e.g. workspace-rooted). Defaults to identity.
76
- */
77
- normalizeFile?: (path: string) => string
78
- /**
79
- * Optional formatter for the enriched detail. Receives the original
80
- * detail + the matched fire datum. Defaults to:
81
- * `${detail} — ${kind ? kind + ' ' : ''}fired ${count}×`
82
- */
83
- formatDetail?: (detail: string, fire: LPIHFireDatum) => string
84
- }
85
-
86
- /**
87
- * Threshold below which the rate suffix is omitted. A long-dormant node
88
- * decays toward 0; showing "0/s" or "0.001/s" is noise. The 0.5 cutoff
89
- * means "less than once every 2 seconds at steady state" — at that
90
- * rate, the cumulative count is the more useful signal.
91
- *
92
- * @internal — exported for tests + tunability.
93
- */
94
- export const _LPIH_RATE_VISIBLE_THRESHOLD = 0.5
95
-
96
- function _formatRate(rate1s: number): string {
97
- if (rate1s < _LPIH_RATE_VISIBLE_THRESHOLD) return ''
98
- // < 10/s: 1 decimal place. ≥ 10/s: rounded integer.
99
- return rate1s < 10
100
- ? ` (${rate1s.toFixed(1)}/s)`
101
- : ` (${Math.round(rate1s)}/s)`
102
- }
103
-
104
- const DEFAULT_FORMAT = (detail: string, fire: LPIHFireDatum): string => {
105
- const kindLabel = fire.kind ? `${fire.kind} ` : ''
106
- const rate = typeof fire.rate1s === 'number' ? _formatRate(fire.rate1s) : ''
107
- return `${detail} — ${kindLabel}fired ${fire.count}×${rate}`
108
- }
109
-
110
- /**
111
- * Merge runtime fire data onto static reactivity findings. Pure function,
112
- * deterministic, input not mutated.
113
- *
114
- * Matching rules:
115
- * - Only fires whose normalized `file` matches the analyzed source file
116
- * are considered (cross-file fires are silently skipped).
117
- * - Line-level matching only (column is ignored). V8 stack columns
118
- * differ from compiler-emitted span columns by 1+ chars in practice,
119
- * and the user-visible affordance is "this signal at this line is
120
- * firing" — line precision is sufficient.
121
- * - Multiple fires at the same `line` are summed; latest `lastFire`
122
- * and corresponding `kind` win.
123
- * - Findings of kind `footgun`, `hoisted-static`, or `static-text` are
124
- * passed through unchanged — they're not runtime-active reactive
125
- * reads, so a fire count at their location is unrelated to them.
126
- */
127
- export function mergeFireDataIntoFindings(
128
- findings: ReactivityFinding[],
129
- fires: readonly LPIHFireDatum[],
130
- sourceFile: string,
131
- options: LPIHMergeOptions = {},
132
- ): ReactivityFinding[] {
133
- if (fires.length === 0) return findings
134
- const norm = options.normalizeFile ?? ((p) => p)
135
- const format = options.formatDetail ?? DEFAULT_FORMAT
136
- const targetFile = norm(sourceFile)
137
-
138
- // Build line-keyed index. Sum counts at the same line; latest wins for
139
- // lastFire + kind.
140
- const byLine = new Map<number, LPIHFireDatum>()
141
- for (const f of fires) {
142
- if (norm(f.file) !== targetFile) continue
143
- const existing = byLine.get(f.line)
144
- if (existing) {
145
- existing.count += f.count
146
- if (typeof f.rate1s === 'number') {
147
- existing.rate1s = (existing.rate1s ?? 0) + f.rate1s
148
- }
149
- const incomingLast = f.lastFire ?? -Infinity
150
- const existingLast = existing.lastFire ?? -Infinity
151
- if (incomingLast > existingLast) {
152
- existing.lastFire = f.lastFire
153
- existing.kind = f.kind ?? existing.kind
154
- }
155
- } else {
156
- byLine.set(f.line, { ...f })
157
- }
158
- }
159
-
160
- if (byLine.size === 0) return findings
161
-
162
- return findings.map((finding) => {
163
- // Footguns + static spans are NOT enriched — fire data at those lines
164
- // belongs to a SEPARATE reactive expression on the same line, and
165
- // attributing it to the footgun would be misleading.
166
- if (
167
- finding.kind === 'footgun' ||
168
- finding.kind === 'hoisted-static' ||
169
- finding.kind === 'static-text'
170
- ) {
171
- return finding
172
- }
173
- const fire = byLine.get(finding.line)
174
- if (!fire) return finding
175
- return {
176
- ...finding,
177
- detail: format(finding.detail, fire),
178
- }
179
- })
180
- }
181
-
182
- /**
183
- * Synthesize "creation-site" inlay-hint findings directly from fire data.
184
- *
185
- * `analyzeReactivity()` produces findings at REACTIVE READ sites (JSX
186
- * expressions). But the runtime captures fires at CREATION sites
187
- * (`signal(0)`, `computed(...)`, `effect(...)`). These are usually
188
- * different source lines — so the merge function above only helps when
189
- * they happen to coincide.
190
- *
191
- * The simpler, more useful editor surface is: show fire counts AT THE
192
- * CREATION LINE. The user writes `const count = signal(0)` and sees
193
- * `(signal fired 129×)` as ghost text on that line, the same way
194
- * TypeScript shows the inferred type.
195
- *
196
- * This function turns each fire datum into a synthetic finding the LSP
197
- * can serve as an inlay hint. No static analysis required — pure runtime
198
- * data → editor hint.
199
- *
200
- * Returns findings sorted by (line, column). Files that don't match
201
- * `sourceFile` (after normalization) are skipped.
202
- *
203
- * @example
204
- * import { firesToCreationSiteFindings } from '@pyreon/compiler'
205
- * import { getFireSummaries } from '@pyreon/reactivity'
206
- *
207
- * const fires = getFireSummaries().map(s => ({
208
- * file: s.loc.file, line: s.loc.line, count: s.count, kind: s.kind,
209
- * }))
210
- * const findings = firesToCreationSiteFindings(fires, 'app.tsx')
211
- * // [{ kind: 'live-fire', line: 5, detail: 'signal fired 129×', ... }]
212
- */
213
- export function firesToCreationSiteFindings(
214
- fires: readonly LPIHFireDatum[],
215
- sourceFile: string,
216
- options: LPIHMergeOptions = {},
217
- ): ReactivityFinding[] {
218
- if (fires.length === 0) return []
219
- const norm = options.normalizeFile ?? ((p) => p)
220
- const targetFile = norm(sourceFile)
221
-
222
- // Per-line aggregation (multiple nodes on the same line — rare but
223
- // possible: `const [a, b] = [signal(0), signal(0)]`).
224
- const byLine = new Map<number, LPIHFireDatum>()
225
- for (const f of fires) {
226
- if (norm(f.file) !== targetFile) continue
227
- const existing = byLine.get(f.line)
228
- if (existing) {
229
- existing.count += f.count
230
- // Sum rates at the same line (e.g. destructured signal pair).
231
- if (typeof f.rate1s === 'number') {
232
- existing.rate1s = (existing.rate1s ?? 0) + f.rate1s
233
- }
234
- const incomingLast = f.lastFire ?? -Infinity
235
- const existingLast = existing.lastFire ?? -Infinity
236
- if (incomingLast > existingLast) {
237
- existing.lastFire = f.lastFire
238
- existing.kind = f.kind ?? existing.kind
239
- }
240
- } else {
241
- byLine.set(f.line, { ...f })
242
- }
243
- }
244
-
245
- const format = options.formatDetail ?? ((_: string, fire: LPIHFireDatum) => {
246
- const kindLabel = fire.kind ?? 'node'
247
- const rate = typeof fire.rate1s === 'number' ? _formatRate(fire.rate1s) : ''
248
- return `${kindLabel} fired ${fire.count}×${rate}`
249
- })
250
-
251
- // 'live-fire' is a new finding kind — synthetic, not produced by
252
- // `analyzeReactivity()`. The LSP renders it as an inlay hint the same
253
- // way as the structural kinds (reactive/static-text/etc).
254
- const LIVE_KIND = 'live-fire' as ReactivityFindingKind
255
-
256
- const out: ReactivityFinding[] = []
257
- for (const [line, fire] of byLine) {
258
- out.push({
259
- kind: LIVE_KIND,
260
- line,
261
- column: 0,
262
- endLine: line,
263
- // 9999 = "end of line" sentinel; the LSP can clamp to actual line length.
264
- endColumn: 9999,
265
- detail: format('', fire),
266
- })
267
- }
268
- out.sort((a, b) => a.line - b.line || a.column - b.column)
269
- return out
270
- }