@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.
- 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
package/src/load-native.ts
DELETED
|
@@ -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
|
-
}
|