@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.
- package/README.md +17 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1189 -30
- package/lib/types/index.d.ts +109 -2
- package/package.json +20 -2
- package/src/event-names.ts +65 -0
- package/src/index.ts +17 -0
- package/src/island-audit.ts +675 -0
- package/src/jsx.ts +162 -39
- package/src/load-native.ts +155 -0
- package/src/pyreon-intercept.ts +352 -2
- package/src/ssg-audit.ts +513 -0
- package/src/tests/detector-tag-consistency.test.ts +31 -15
- package/src/tests/island-audit.test.ts +524 -0
- package/src/tests/jsx.test.ts +236 -4
- package/src/tests/load-native.test.ts +53 -0
- package/src/tests/native-equivalence.test.ts +77 -0
- package/src/tests/pyreon-intercept.test.ts +296 -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/ssg-audit.test.ts +402 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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'
|