@pyreon/compiler 0.14.0 → 0.16.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.
@@ -194,6 +194,26 @@ declare function diagnoseError(error: string): ErrorDiagnosis | null;
194
194
  * monotonic counter.
195
195
  * - `on-click-undefined` — `onClick={undefined}` explicitly; the runtime
196
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
+ * - `island-never-with-registry-entry` — an `island()` declared with
210
+ * `hydrate: 'never'` is also registered in the same
211
+ * file's `hydrateIslands({ ... })` call. The whole
212
+ * point of `'never'` is shipping zero client JS;
213
+ * registering pulls the component module into the
214
+ * client bundle graph (the runtime short-circuits
215
+ * and never calls the loader, but the bundler still
216
+ * includes the import). Drop the registry entry.
197
217
  *
198
218
  * Two-mode surface mirrors `react-intercept.ts`:
199
219
  * - `detectPyreonPatterns(code)` — diagnostics only
@@ -214,7 +234,7 @@ declare function diagnoseError(error: string): ErrorDiagnosis | null;
214
234
  * 2. CLI `pyreon doctor`
215
235
  * 3. MCP server `validate` tool
216
236
  */
217
- 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';
237
+ 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' | 'island-never-with-registry-entry';
218
238
  interface PyreonDiagnostic {
219
239
  /** Machine-readable code for filtering + programmatic handling */
220
240
  code: PyreonDiagnosticCode;
@@ -280,5 +300,92 @@ declare function formatTestAudit(result: TestAuditResult, {
280
300
  limit
281
301
  }?: AuditFormatOptions): string;
282
302
  //#endregion
283
- 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 };
303
+ //#region src/island-audit.d.ts
304
+ type IslandFindingCode = 'never-with-registry-entry' | 'duplicate-name' | 'registry-mismatch' | 'nested-island' | 'dead-island';
305
+ interface IslandLocation {
306
+ /** Absolute path */
307
+ path: string;
308
+ /** Path relative to the repo root for readable reporting */
309
+ relPath: string;
310
+ /** 1-based line number */
311
+ line: number;
312
+ /** 1-based column number */
313
+ column: number;
314
+ }
315
+ interface IslandFinding {
316
+ code: IslandFindingCode;
317
+ /** One-paragraph human-readable explanation, including the fix path. */
318
+ message: string;
319
+ /** Where the finding surfaces. */
320
+ location: IslandLocation;
321
+ /**
322
+ * Companion locations for cross-file findings (`duplicate-name` lists
323
+ * the OTHER occurrence; `nested-island` lists the inner island's
324
+ * declaration; `never-with-registry-entry` lists the matching island
325
+ * declaration).
326
+ */
327
+ related?: IslandLocation[] | undefined;
328
+ }
329
+ interface IslandAuditResult {
330
+ root: string | null;
331
+ findings: IslandFinding[];
332
+ summary: {
333
+ filesScanned: number;
334
+ islandsDeclared: number;
335
+ registryEntries: number;
336
+ findingsByCode: Record<IslandFindingCode, number>;
337
+ };
338
+ }
339
+ declare function auditIslands(rootDir: string): IslandAuditResult;
340
+ interface IslandAuditFormatOptions {
341
+ /** When true, emit JSON instead of markdown-ish text. */
342
+ json?: boolean | undefined;
343
+ }
344
+ declare function formatIslandAudit(result: IslandAuditResult, options?: IslandAuditFormatOptions): string;
345
+ //#endregion
346
+ //#region src/ssg-audit.d.ts
347
+ type SsgFindingCode = '404-outside-layout-dir' | 'dynamic-route-missing-get-static-paths' | 'non-literal-revalidate-export';
348
+ interface SsgLocation {
349
+ /** Absolute path */
350
+ path: string;
351
+ /** Path relative to the repo root for readable reporting */
352
+ relPath: string;
353
+ /** 1-based line number */
354
+ line: number;
355
+ /** 1-based column number */
356
+ column: number;
357
+ }
358
+ interface SsgFinding {
359
+ code: SsgFindingCode;
360
+ /** One-paragraph human-readable explanation, including the fix path. */
361
+ message: string;
362
+ /** Where the finding surfaces. */
363
+ location: SsgLocation;
364
+ /**
365
+ * Companion locations for cross-file findings. Not currently emitted
366
+ * by any detector but kept in the contract so future codes have the
367
+ * shape available without an API change.
368
+ */
369
+ related?: SsgLocation[] | undefined;
370
+ }
371
+ interface SsgAuditResult {
372
+ root: string | null;
373
+ findings: SsgFinding[];
374
+ summary: {
375
+ filesScanned: number;
376
+ routesScanned: number;
377
+ dynamicRoutes: number;
378
+ revalidateExports: number;
379
+ findingsByCode: Record<SsgFindingCode, number>;
380
+ };
381
+ }
382
+ declare function auditSsg(rootDir: string): SsgAuditResult;
383
+ interface SsgAuditFormatOptions {
384
+ /** Filter findings to a minimum severity. Currently all SSG findings
385
+ * are 'warning'-level; reserved for future severity tiers. */
386
+ minSeverity?: 'warning' | 'error' | undefined;
387
+ }
388
+ declare function formatSsgAudit(result: SsgAuditResult, _options?: SsgAuditFormatOptions): string;
389
+ //#endregion
390
+ export { type AuditFormatOptions, type AuditRisk, type CompilerWarning, type ComponentInfo, type ErrorDiagnosis, type IslandAuditFormatOptions, type IslandAuditResult, type IslandFinding, type IslandFindingCode, type IslandInfo, type IslandLocation, type MigrationChange, type MigrationResult, type ProjectContext, type PyreonDiagnostic, type PyreonDiagnosticCode, type ReactDiagnostic, type ReactDiagnosticCode, type RouteInfo, type SsgAuditFormatOptions, type SsgAuditResult, type SsgFinding, type SsgFindingCode, type SsgLocation, type TestAuditEntry, type TestAuditResult, type TransformResult, auditIslands, auditSsg, auditTestEnvironment, detectPyreonPatterns, detectReactPatterns, diagnoseError, formatIslandAudit, formatSsgAudit, formatTestAudit, generateContext, hasPyreonPatterns, hasReactPatterns, migrateReactCode, transformJSX, transformJSX_JS };
284
391
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/compiler",
3
- "version": "0.14.0",
3
+ "version": "0.16.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"
@@ -35,6 +36,7 @@
35
36
  },
36
37
  "scripts": {
37
38
  "build": "vl_rolldown_build",
39
+ "build:native": "bun scripts/build-native.ts",
38
40
  "dev": "vl_rolldown_build-watch",
39
41
  "test": "vitest run",
40
42
  "typecheck": "tsc --noEmit",
@@ -42,7 +44,23 @@
42
44
  "prepublishOnly": "bun run build"
43
45
  },
44
46
  "dependencies": {
45
- "oxc-parser": "^0.123.0"
47
+ "oxc-parser": "^0.129.0"
48
+ },
49
+ "optionalDependencies": {
50
+ "@pyreon/compiler-darwin-arm64": "workspace:^",
51
+ "@pyreon/compiler-darwin-x64": "workspace:^",
52
+ "@pyreon/compiler-linux-arm64-gnu": "workspace:^",
53
+ "@pyreon/compiler-linux-arm64-musl": "workspace:^",
54
+ "@pyreon/compiler-linux-x64-gnu": "workspace:^",
55
+ "@pyreon/compiler-linux-x64-musl": "workspace:^",
56
+ "@pyreon/compiler-win32-x64-msvc": "workspace:^"
57
+ },
58
+ "devDependencies": {
59
+ "@pyreon/core": "^0.16.0",
60
+ "@pyreon/reactivity": "^0.16.0",
61
+ "@pyreon/runtime-dom": "^0.16.0",
62
+ "@pyreon/test-utils": "^0.13.3",
63
+ "happy-dom": "^20.8.3"
46
64
  },
47
65
  "peerDependencies": {
48
66
  "typescript": ">=5.0.0"
@@ -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
@@ -26,3 +26,20 @@ export type {
26
26
  TestAuditResult,
27
27
  } from './test-audit'
28
28
  export { auditTestEnvironment, formatTestAudit } from './test-audit'
29
+ export type {
30
+ IslandAuditFormatOptions,
31
+ IslandAuditResult,
32
+ IslandFinding,
33
+ IslandFindingCode,
34
+ IslandLocation,
35
+ } from './island-audit'
36
+ export { auditIslands, formatIslandAudit } from './island-audit'
37
+ // M3.4 — `pyreon doctor --check-ssg` audit.
38
+ export type {
39
+ SsgAuditFormatOptions,
40
+ SsgAuditResult,
41
+ SsgFinding,
42
+ SsgFindingCode,
43
+ SsgLocation,
44
+ } from './ssg-audit'
45
+ export { auditSsg, formatSsgAudit } from './ssg-audit'