@pyreon/compiler 0.13.1 → 0.15.0
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/README.md +14 -4
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1330 -409
- package/lib/types/index.d.ts +152 -14
- package/package.json +12 -1
- package/src/event-names.ts +65 -0
- package/src/index.ts +10 -1
- package/src/jsx.ts +974 -784
- package/src/pyreon-intercept.ts +728 -0
- package/src/test-audit.ts +435 -0
- package/src/tests/depth-stress.test.ts +16 -0
- package/src/tests/detector-tag-consistency.test.ts +86 -0
- package/src/tests/jsx.test.ts +1170 -4
- package/src/tests/native-equivalence.test.ts +731 -0
- package/src/tests/project-scanner.test.ts +30 -0
- package/src/tests/pyreon-intercept.test.ts +486 -0
- package/src/tests/react-intercept.test.ts +354 -0
- package/src/tests/runtime/control-flow.test.ts +159 -0
- package/src/tests/runtime/dom-properties.test.ts +138 -0
- package/src/tests/runtime/events.test.ts +301 -0
- package/src/tests/runtime/harness.ts +94 -0
- package/src/tests/runtime/pr-352-shapes.test.ts +121 -0
- package/src/tests/runtime/reactive-props.test.ts +81 -0
- package/src/tests/runtime/signals.test.ts +129 -0
- package/src/tests/runtime/whitespace.test.ts +106 -0
- package/src/tests/test-audit.test.ts +549 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -18,20 +18,15 @@
|
|
|
18
18
|
* values, and all children are text nodes or other static JSX nodes.
|
|
19
19
|
*
|
|
20
20
|
* Template emission:
|
|
21
|
-
* - JSX element trees with ≥
|
|
22
|
-
* are compiled to `_tpl(html, bindFn)` calls instead of nested
|
|
21
|
+
* - JSX element trees with ≥ 1 DOM elements (no components, no spread attrs on
|
|
22
|
+
* inner elements) are compiled to `_tpl(html, bindFn)` calls instead of nested
|
|
23
|
+
* `h()` calls.
|
|
23
24
|
* - The HTML string is parsed once via <template>.innerHTML, then cloneNode(true)
|
|
24
25
|
* for each instance (~5-10x faster than sequential createElement calls).
|
|
25
26
|
* - Static attributes are baked into the HTML string; dynamic attributes and
|
|
26
27
|
* text content use renderEffect in the bind function.
|
|
27
28
|
*
|
|
28
|
-
* Implementation:
|
|
29
|
-
* No extra runtime dependencies — `typescript` is already in devDependencies.
|
|
30
|
-
*
|
|
31
|
-
* Known limitation (v0): expressions inside *nested* JSX within a child
|
|
32
|
-
* expression container are not individually wrapped. They are still reactive
|
|
33
|
-
* because the outer wrapper re-evaluates the whole subtree, just at a coarser
|
|
34
|
-
* granularity. Fine-grained nested wrapping is planned for a future pass.
|
|
29
|
+
* Implementation: Rust native binary (napi-rs) when available, JS fallback via oxc-parser.
|
|
35
30
|
*/
|
|
36
31
|
interface CompilerWarning {
|
|
37
32
|
/** Warning message */
|
|
@@ -54,14 +49,27 @@ interface TransformResult {
|
|
|
54
49
|
interface TransformOptions {
|
|
55
50
|
/**
|
|
56
51
|
* Compile for server-side rendering. When true, the compiler skips the
|
|
57
|
-
* `_tpl()` template optimization
|
|
58
|
-
* `
|
|
59
|
-
* `@pyreon/runtime-server` can walk the VNode tree. Client builds keep
|
|
60
|
-
* the `_tpl()` fast path. Default: false.
|
|
52
|
+
* `_tpl()` template optimization and falls back to plain `h()` calls so
|
|
53
|
+
* `@pyreon/runtime-server` can walk the VNode tree. Default: false.
|
|
61
54
|
*/
|
|
62
55
|
ssr?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Known signal variable names from resolved imports.
|
|
58
|
+
* The Vite plugin maintains a cross-module signal export registry and
|
|
59
|
+
* passes imported signal names here so the compiler can auto-call them
|
|
60
|
+
* in JSX even though the `signal()` declaration is in another file.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // store.ts: export const count = signal(0)
|
|
64
|
+
* // component.tsx: import { count } from './store'
|
|
65
|
+
* transformJSX(code, 'component.tsx', { knownSignals: ['count'] })
|
|
66
|
+
* // {count} in JSX → {() => count()}
|
|
67
|
+
*/
|
|
68
|
+
knownSignals?: string[];
|
|
63
69
|
}
|
|
64
70
|
declare function transformJSX(code: string, filename?: string, options?: TransformOptions): TransformResult;
|
|
71
|
+
/** JS fallback implementation — used when the native binary isn't available. */
|
|
72
|
+
declare function transformJSX_JS(code: string, filename?: string, options?: TransformOptions): TransformResult;
|
|
65
73
|
//#endregion
|
|
66
74
|
//#region src/project-scanner.d.ts
|
|
67
75
|
/**
|
|
@@ -154,5 +162,135 @@ interface ErrorDiagnosis {
|
|
|
154
162
|
/** Diagnose an error message and return structured fix information */
|
|
155
163
|
declare function diagnoseError(error: string): ErrorDiagnosis | null;
|
|
156
164
|
//#endregion
|
|
157
|
-
|
|
165
|
+
//#region src/pyreon-intercept.d.ts
|
|
166
|
+
/**
|
|
167
|
+
* Pyreon Pattern Interceptor — detects Pyreon-specific anti-patterns in
|
|
168
|
+
* code that has ALREADY committed to the framework (imports are Pyreon,
|
|
169
|
+
* not React). Complements `react-intercept.ts` — the React detector
|
|
170
|
+
* catches "coming from React" mistakes; this one catches "using Pyreon
|
|
171
|
+
* wrong" mistakes.
|
|
172
|
+
*
|
|
173
|
+
* Catalog of detected patterns (grounded in `.claude/rules/anti-patterns.md`):
|
|
174
|
+
*
|
|
175
|
+
* - `for-missing-by` — `<For each={...}>` without a `by` prop
|
|
176
|
+
* - `for-with-key` — `<For key={...}>` (JSX reserves `key`; the keying
|
|
177
|
+
* prop is `by` in Pyreon)
|
|
178
|
+
* - `props-destructured` — `({ foo }: Props) => <JSX />` destructures at
|
|
179
|
+
* the component signature; reading is captured once
|
|
180
|
+
* and loses reactivity. Access `props.foo` instead
|
|
181
|
+
* or use `splitProps(props, [...])`.
|
|
182
|
+
* - `process-dev-gate` — `typeof process !== 'undefined' &&
|
|
183
|
+
* process.env.NODE_ENV !== 'production'` is dead
|
|
184
|
+
* code in real Vite browser bundles. Use
|
|
185
|
+
* `import.meta.env?.DEV` instead.
|
|
186
|
+
* - `empty-theme` — `.theme({})` chain is a no-op; remove it.
|
|
187
|
+
* - `raw-add-event-listener` — raw `addEventListener(...)` in a component
|
|
188
|
+
* or hook body. Use `useEventListener(...)` from
|
|
189
|
+
* `@pyreon/hooks` for auto-cleanup.
|
|
190
|
+
* - `raw-remove-event-listener` — same, for removeEventListener.
|
|
191
|
+
* - `date-math-random-id` — `Date.now() + Math.random()` / template-concat
|
|
192
|
+
* variants. Under rapid operations (paste, clone)
|
|
193
|
+
* collision probability is non-trivial. Use a
|
|
194
|
+
* monotonic counter.
|
|
195
|
+
* - `on-click-undefined` — `onClick={undefined}` explicitly; the runtime
|
|
196
|
+
* used to crash on this pattern. Omit the prop.
|
|
197
|
+
* - `signal-write-as-call` — `sig(value)` is a no-op read that ignores
|
|
198
|
+
* its argument; the runtime warns in dev. Static
|
|
199
|
+
* detector spots it pre-runtime when `sig` was
|
|
200
|
+
* declared as `const sig = signal(...)` /
|
|
201
|
+
* `computed(...)` and called with ≥1 argument.
|
|
202
|
+
* - `static-return-null-conditional` — `if (cond) return null` at the
|
|
203
|
+
* top of a component body runs ONCE; signal changes
|
|
204
|
+
* in `cond` never re-evaluate the early-return.
|
|
205
|
+
* Wrap in a returned reactive accessor.
|
|
206
|
+
* - `as-unknown-as-vnodechild` — defensive `as unknown as VNodeChild`
|
|
207
|
+
* cast on JSX returns is unnecessary (`JSX.Element`
|
|
208
|
+
* is already assignable to `VNodeChild`).
|
|
209
|
+
*
|
|
210
|
+
* Two-mode surface mirrors `react-intercept.ts`:
|
|
211
|
+
* - `detectPyreonPatterns(code)` — diagnostics only
|
|
212
|
+
* - `hasPyreonPatterns(code)` — fast regex pre-filter
|
|
213
|
+
*
|
|
214
|
+
* ## fixable: false (invariant)
|
|
215
|
+
*
|
|
216
|
+
* Every Pyreon diagnostic reports `fixable: false` — no exceptions.
|
|
217
|
+
* The `migrate_react` MCP tool only knows React mappings, so claiming
|
|
218
|
+
* a Pyreon code is auto-fixable would mislead a consumer who wires
|
|
219
|
+
* their UX off the flag and finds nothing applies the fix. Flip to
|
|
220
|
+
* `true` ONLY when a companion `migrate_pyreon` tool ships in a
|
|
221
|
+
* subsequent PR. The invariant is locked in
|
|
222
|
+
* `tests/pyreon-intercept.test.ts` under "fixable contract".
|
|
223
|
+
*
|
|
224
|
+
* Designed for three consumers:
|
|
225
|
+
* 1. Compiler pre-pass warnings during build
|
|
226
|
+
* 2. CLI `pyreon doctor`
|
|
227
|
+
* 3. MCP server `validate` tool
|
|
228
|
+
*/
|
|
229
|
+
type PyreonDiagnosticCode = 'for-missing-by' | 'for-with-key' | 'props-destructured' | 'process-dev-gate' | 'empty-theme' | 'raw-add-event-listener' | 'raw-remove-event-listener' | 'date-math-random-id' | 'on-click-undefined' | 'signal-write-as-call' | 'static-return-null-conditional' | 'as-unknown-as-vnodechild';
|
|
230
|
+
interface PyreonDiagnostic {
|
|
231
|
+
/** Machine-readable code for filtering + programmatic handling */
|
|
232
|
+
code: PyreonDiagnosticCode;
|
|
233
|
+
/** Human-readable message explaining the issue */
|
|
234
|
+
message: string;
|
|
235
|
+
/** 1-based line number */
|
|
236
|
+
line: number;
|
|
237
|
+
/** 0-based column */
|
|
238
|
+
column: number;
|
|
239
|
+
/** The code as written */
|
|
240
|
+
current: string;
|
|
241
|
+
/** The suggested Pyreon fix */
|
|
242
|
+
suggested: string;
|
|
243
|
+
/** Whether a mechanical auto-fix is safe */
|
|
244
|
+
fixable: boolean;
|
|
245
|
+
}
|
|
246
|
+
declare function detectPyreonPatterns(code: string, filename?: string): PyreonDiagnostic[];
|
|
247
|
+
/** Fast regex pre-filter — returns true if the code is worth a full AST walk. */
|
|
248
|
+
declare function hasPyreonPatterns(code: string): boolean;
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/test-audit.d.ts
|
|
251
|
+
type AuditRisk = 'high' | 'medium' | 'low';
|
|
252
|
+
interface TestAuditEntry {
|
|
253
|
+
/** Absolute path to the test file */
|
|
254
|
+
path: string;
|
|
255
|
+
/** Path relative to the repo root for readable reporting */
|
|
256
|
+
relPath: string;
|
|
257
|
+
/** Count of object-literal `{ type: ..., props: ..., children: ... }` patterns */
|
|
258
|
+
mockVNodeLiteralCount: number;
|
|
259
|
+
/** Count of `vnode` / `mockVNode` / `createVNode` helper DEFINITIONS */
|
|
260
|
+
mockHelperCount: number;
|
|
261
|
+
/**
|
|
262
|
+
* Count of CALLS to a known mock-helper name. Captures pervasiveness:
|
|
263
|
+
* a file with one helper definition and 50 call-sites has the same
|
|
264
|
+
* `mockHelperCount` (1) as one with zero calls, but very different
|
|
265
|
+
* exposure. This metric surfaces that.
|
|
266
|
+
*/
|
|
267
|
+
mockHelperCallCount: number;
|
|
268
|
+
/** Count of lines that look like real `h(...)` calls (`h(Tag, props)` / `h(Component, ...)` shape) */
|
|
269
|
+
realHCallCount: number;
|
|
270
|
+
/** True if the file imports `h` from `@pyreon/core` */
|
|
271
|
+
importsH: boolean;
|
|
272
|
+
/** Risk classification */
|
|
273
|
+
risk: AuditRisk;
|
|
274
|
+
}
|
|
275
|
+
interface TestAuditResult {
|
|
276
|
+
/** Repo root discovered by walking up for `packages/` */
|
|
277
|
+
root: string | null;
|
|
278
|
+
/** Every test file scanned, sorted by risk (high → low) then path */
|
|
279
|
+
entries: TestAuditEntry[];
|
|
280
|
+
/** Total files scanned */
|
|
281
|
+
totalScanned: number;
|
|
282
|
+
}
|
|
283
|
+
declare function auditTestEnvironment(startDir: string): TestAuditResult;
|
|
284
|
+
interface AuditFormatOptions {
|
|
285
|
+
/** Only include entries at or above this risk level. Default 'medium'. */
|
|
286
|
+
minRisk?: AuditRisk | undefined;
|
|
287
|
+
/** Maximum entries to show per risk group. Default 20. */
|
|
288
|
+
limit?: number | undefined;
|
|
289
|
+
}
|
|
290
|
+
declare function formatTestAudit(result: TestAuditResult, {
|
|
291
|
+
minRisk,
|
|
292
|
+
limit
|
|
293
|
+
}?: AuditFormatOptions): string;
|
|
294
|
+
//#endregion
|
|
295
|
+
export { type AuditFormatOptions, type AuditRisk, type CompilerWarning, type ComponentInfo, type ErrorDiagnosis, type IslandInfo, type MigrationChange, type MigrationResult, type ProjectContext, type PyreonDiagnostic, type PyreonDiagnosticCode, type ReactDiagnostic, type ReactDiagnosticCode, type RouteInfo, type TestAuditEntry, type TestAuditResult, type TransformResult, auditTestEnvironment, detectPyreonPatterns, detectReactPatterns, diagnoseError, formatTestAudit, generateContext, hasPyreonPatterns, hasReactPatterns, migrateReactCode, transformJSX, transformJSX_JS };
|
|
158
296
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Template and JSX compiler for Pyreon",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/compiler#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
+
"!lib/**/*.map",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
@@ -41,6 +42,16 @@
|
|
|
41
42
|
"lint": "oxlint .",
|
|
42
43
|
"prepublishOnly": "bun run build"
|
|
43
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"oxc-parser": "^0.129.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@pyreon/core": "^0.15.0",
|
|
50
|
+
"@pyreon/reactivity": "^0.15.0",
|
|
51
|
+
"@pyreon/runtime-dom": "^0.15.0",
|
|
52
|
+
"@pyreon/test-utils": "^0.13.2",
|
|
53
|
+
"happy-dom": "^20.8.3"
|
|
54
|
+
},
|
|
44
55
|
"peerDependencies": {
|
|
45
56
|
"typescript": ">=5.0.0"
|
|
46
57
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-style → DOM event-name remap.
|
|
3
|
+
*
|
|
4
|
+
* The compiler translates JSX event handler attributes (`onClick`,
|
|
5
|
+
* `onMouseEnter`, ...) to DOM event names by stripping the `on` prefix
|
|
6
|
+
* and lowercasing. That rule covers MOST React event-name conventions
|
|
7
|
+
* because the underlying DOM event name happens to be the lowercased
|
|
8
|
+
* multi-word form (e.g. `onKeyDown` → `keydown`, `onMouseEnter` →
|
|
9
|
+
* `mouseenter`, `onPointerLeave` → `pointerleave`,
|
|
10
|
+
* `onAnimationStart` → `animationstart`, `onContextMenu` → `contextmenu`).
|
|
11
|
+
*
|
|
12
|
+
* **The exceptions** — where lowercasing produces the WRONG DOM event
|
|
13
|
+
* name — are listed in `REACT_EVENT_REMAP` below. Each entry maps the
|
|
14
|
+
* lowercased React form to the actual DOM event name.
|
|
15
|
+
*
|
|
16
|
+
* Today there is exactly ONE remap: `doubleclick → dblclick`. React
|
|
17
|
+
* inherits this mismatch from the DOM spec — `dblclick` is the canonical
|
|
18
|
+
* event name (RFC at `https://dom.spec.whatwg.org/#interface-mouseevent`),
|
|
19
|
+
* while React's component-prop convention is the `onDoubleClick` shape.
|
|
20
|
+
*
|
|
21
|
+
* **Audit completeness.** The full React event-prop list from
|
|
22
|
+
* `https://react.dev/reference/react-dom/components/common` was checked
|
|
23
|
+
* against canonical DOM event names. Every multi-word event other than
|
|
24
|
+
* `onDoubleClick` lowercases correctly:
|
|
25
|
+
* - Pointer family: `onPointerDown` → `pointerdown`, `onGotPointerCapture` → `gotpointercapture`, …
|
|
26
|
+
* - Mouse family: `onMouseEnter` → `mouseenter`, `onMouseLeave` → `mouseleave`, …
|
|
27
|
+
* - Drag family: `onDragStart` → `dragstart`, `onDragEnd` → `dragend`, …
|
|
28
|
+
* - Touch family: `onTouchStart` → `touchstart`, `onTouchEnd` → `touchend`, …
|
|
29
|
+
* - Composition family: `onCompositionEnd` → `compositionend`, …
|
|
30
|
+
* - Animation/transition: `onAnimationStart` → `animationstart`, `onTransitionEnd` → `transitionend`, …
|
|
31
|
+
* - Media family: `onCanPlayThrough` → `canplaythrough`, `onLoadedData` → `loadeddata`, `onTimeUpdate` → `timeupdate`, `onVolumeChange` → `volumechange`, …
|
|
32
|
+
* - Form family: `onContextMenu` → `contextmenu`, `onBeforeInput` → `beforeinput`, …
|
|
33
|
+
*
|
|
34
|
+
* If a future React release adds a new event-prop with a non-trivial
|
|
35
|
+
* mismatch, append the entry here. Both compiler backends (JS and Rust)
|
|
36
|
+
* read the same shape — the Rust port lives in `native/src/lib.rs` next
|
|
37
|
+
* to `emit_event_listener`. Keep them in sync.
|
|
38
|
+
*
|
|
39
|
+
* **Testing.** `packages/core/compiler/src/tests/runtime/events.test.ts`
|
|
40
|
+
* exercises this table end-to-end via a real-Chromium harness:
|
|
41
|
+
* - `onDoubleClick fires (multi-word + delegated)` — locks in the remap.
|
|
42
|
+
* - `onContextMenu fires (multi-word, lowercases to contextmenu)` —
|
|
43
|
+
* locks in the no-remap default for an adjacent multi-word event.
|
|
44
|
+
* - `event-name-remap-table sanity` — asserts that every entry in
|
|
45
|
+
* `REACT_EVENT_REMAP` has a corresponding runtime test.
|
|
46
|
+
*/
|
|
47
|
+
export const REACT_EVENT_REMAP: Readonly<Record<string, string>> = Object.freeze({
|
|
48
|
+
doubleclick: 'dblclick',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Translate a React-style event prop name (`onDoubleClick`) to the
|
|
53
|
+
* canonical DOM event name (`dblclick`). Returns null for non-event
|
|
54
|
+
* attribute names (anything not starting with `on` or shorter than 3
|
|
55
|
+
* characters — single-letter `on*` props don't correspond to DOM events).
|
|
56
|
+
*
|
|
57
|
+
* The compiler uses the returned name in two emission shapes:
|
|
58
|
+
* - Delegated events (in `DELEGATED_EVENTS`): `el.__ev_${eventName} = handler`
|
|
59
|
+
* - Direct listeners: `el.addEventListener("${eventName}", handler)`
|
|
60
|
+
*/
|
|
61
|
+
export function reactEventToDom(attrName: string): string | null {
|
|
62
|
+
if (attrName.length <= 2 || !attrName.startsWith('on')) return null
|
|
63
|
+
const lower = attrName.slice(2).toLowerCase()
|
|
64
|
+
return REACT_EVENT_REMAP[lower] ?? lower
|
|
65
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @pyreon/compiler — JSX reactive transform for Pyreon
|
|
2
2
|
|
|
3
3
|
export type { CompilerWarning, TransformResult } from './jsx'
|
|
4
|
-
export { transformJSX } from './jsx'
|
|
4
|
+
export { transformJSX, transformJSX_JS } from './jsx'
|
|
5
5
|
export type { ComponentInfo, IslandInfo, ProjectContext, RouteInfo } from './project-scanner'
|
|
6
6
|
export { generateContext } from './project-scanner'
|
|
7
7
|
export type {
|
|
@@ -17,3 +17,12 @@ export {
|
|
|
17
17
|
hasReactPatterns,
|
|
18
18
|
migrateReactCode,
|
|
19
19
|
} from './react-intercept'
|
|
20
|
+
export type { PyreonDiagnostic, PyreonDiagnosticCode } from './pyreon-intercept'
|
|
21
|
+
export { detectPyreonPatterns, hasPyreonPatterns } from './pyreon-intercept'
|
|
22
|
+
export type {
|
|
23
|
+
AuditFormatOptions,
|
|
24
|
+
AuditRisk,
|
|
25
|
+
TestAuditEntry,
|
|
26
|
+
TestAuditResult,
|
|
27
|
+
} from './test-audit'
|
|
28
|
+
export { auditTestEnvironment, formatTestAudit } from './test-audit'
|