@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.
@@ -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 ≥ 2 DOM elements (no components, no spread attrs)
22
- * are compiled to `_tpl(html, bindFn)` calls instead of nested `h()` calls.
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: TypeScript parser for positions + magic-string replacements.
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 (which mutates a real DOM via
58
- * `document.createElement` etc.) and falls back to plain `h()` calls so
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
- export { type CompilerWarning, type ComponentInfo, type ErrorDiagnosis, type IslandInfo, type MigrationChange, type MigrationResult, type ProjectContext, type ReactDiagnostic, type ReactDiagnosticCode, type RouteInfo, type TransformResult, detectReactPatterns, diagnoseError, generateContext, hasReactPatterns, migrateReactCode, transformJSX };
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.13.1",
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'