@reactra/vite-plugin 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akhil Shastri and the Reactra contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @reactra/vite-plugin
2
+
3
+ > Reactra Vite plugin — wires the Reactra compiler and file-based route manifest into Vite.
4
+
5
+ **Alpha** — published under the npm dist-tag `alpha`. APIs may change before 1.0.
6
+
7
+ ```bash
8
+ npm install @reactra/vite-plugin@alpha
9
+ ```
10
+
11
+ Part of [Reactra](https://github.com/akhilshastri/reactra) — a compiler-first,
12
+ React-19-compatible framework. See the [documentation](https://reactra-docs.vercel.app) to get
13
+ started.
14
+
15
+ ## License
16
+
17
+ MIT
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal structural type for esbuild's `Plugin` interface — we don't
3
+ * import esbuild at type-position to keep this file resolvable under
4
+ * Bun without esbuild's full type tree.
5
+ */
6
+ export interface EsbuildPluginBuild {
7
+ onLoad(opts: {
8
+ filter: RegExp;
9
+ namespace?: string;
10
+ }, cb: (args: {
11
+ path: string;
12
+ }) => {
13
+ contents: string;
14
+ loader: string;
15
+ } | null): void;
16
+ }
17
+ export interface EsbuildPlugin {
18
+ readonly name: string;
19
+ setup(build: EsbuildPluginBuild): void;
20
+ }
21
+ /**
22
+ * esbuild plugin factory — registers an `onLoad` callback that
23
+ * preprocesses Reactra DSL files before esbuild's parser sees them.
24
+ *
25
+ * The Vite plugin's `config()` hook injects this into
26
+ * `config.optimizeDeps.esbuildOptions.plugins` so the dep-scan phase
27
+ * runs through it. Once Vite's dev server is up, the regular
28
+ * `transform()` hook takes over for per-request compiles.
29
+ */
30
+ export declare const reactraEsbuildPlugin: EsbuildPlugin;
31
+ //# sourceMappingURL=esbuild-prescan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"esbuild-prescan.d.ts","sourceRoot":"","sources":["../src/esbuild-prescan.ts"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,CACJ,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,EAC5C,EAAE,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAC1E,IAAI,CAAA;CACR;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAA;CACvC;AAID;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAmClC,CAAA"}
@@ -0,0 +1,75 @@
1
+ // esbuild plugin for Vite's dep-scan pre-bundling phase (Day 24 / `#15`).
2
+ //
3
+ // Vite's `optimizeDeps` runs esbuild over the entry's import graph
4
+ // BEFORE the dev server starts serving requests — so the Vite
5
+ // `transform()` hook hasn't fired yet. esbuild's scanner sees raw
6
+ // DSL keywords (`export component Foo {`, `route store X {`, etc.)
7
+ // and bails with "Unexpected component" / "Expected ';' but found
8
+ // 'store'" parse errors. Result: the dreaded "Failed to run
9
+ // dependency scan" warning at first dev-server boot. On-demand
10
+ // serving still works because the transform pipeline does fire on
11
+ // each request, but pre-bundling is skipped and the user sees a
12
+ // scary-looking stack trace they have to learn to ignore.
13
+ //
14
+ // Fix: register an esbuild `onLoad` callback for `.tsx/.ts/.jsx/.js`
15
+ // files. When the file's source contains a Reactra container keyword,
16
+ // run `compile()` to produce React-19 TSX before esbuild's parser
17
+ // sees it. Otherwise return `null` so esbuild's default loaders take
18
+ // over. Cheap regex gate — same `containsReactraDsl` predicate the
19
+ // Vite `transform` hook uses, so the per-file cost is the same.
20
+ //
21
+ // This module exports a plain esbuild Plugin (not a Vite plugin);
22
+ // the Vite plugin's `config()` hook wires it into
23
+ // `config.optimizeDeps.esbuildOptions.plugins`.
24
+ import { readFileSync } from "node:fs";
25
+ import { compile, containsReactraDsl } from "@reactra/babel-plugin";
26
+ const SUPPORTED_RE = /\.(?:tsx|jsx|ts|js)$/;
27
+ /**
28
+ * esbuild plugin factory — registers an `onLoad` callback that
29
+ * preprocesses Reactra DSL files before esbuild's parser sees them.
30
+ *
31
+ * The Vite plugin's `config()` hook injects this into
32
+ * `config.optimizeDeps.esbuildOptions.plugins` so the dep-scan phase
33
+ * runs through it. Once Vite's dev server is up, the regular
34
+ * `transform()` hook takes over for per-request compiles.
35
+ */
36
+ export const reactraEsbuildPlugin = {
37
+ name: "@reactra/esbuild-prescan",
38
+ setup(build) {
39
+ build.onLoad({ filter: SUPPORTED_RE }, (args) => {
40
+ let source;
41
+ try {
42
+ source = readFileSync(args.path, "utf8");
43
+ }
44
+ catch {
45
+ // File unreadable — let esbuild's default loader try (and
46
+ // emit its own clearer error).
47
+ return null;
48
+ }
49
+ if (!containsReactraDsl(source))
50
+ return null;
51
+ // The pre-scan phase only needs source good enough for esbuild
52
+ // to discover IMPORTS — it doesn't actually execute the code.
53
+ // We still run the full compile() because:
54
+ // (1) it's already cheap on Reactra files (no IR, no Babel
55
+ // round-trip — single-pass preprocess + template emit);
56
+ // (2) the resulting React-19 TSX has the same import-set as
57
+ // the original source, so esbuild scans the right deps;
58
+ // (3) handing esbuild anything less risks subtle scan-vs-
59
+ // transform divergence at runtime (different deps pre-
60
+ // bundled than the page actually uses).
61
+ try {
62
+ const { code } = compile(source, { sourceFile: args.path });
63
+ return { contents: code, loader: "tsx" };
64
+ }
65
+ catch {
66
+ // If our compile fails here, fall through to esbuild's
67
+ // default loader — it'll produce a similar error but at the
68
+ // correct phase (the regular transform also runs compile,
69
+ // so a real syntax error surfaces there with our diagnostics).
70
+ return null;
71
+ }
72
+ });
73
+ },
74
+ };
75
+ //# sourceMappingURL=esbuild-prescan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"esbuild-prescan.js","sourceRoot":"","sources":["../src/esbuild-prescan.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,EAAE;AACF,mEAAmE;AACnE,8DAA8D;AAC9D,kEAAkE;AAClE,mEAAmE;AACnE,kEAAkE;AAClE,4DAA4D;AAC5D,+DAA+D;AAC/D,kEAAkE;AAClE,gEAAgE;AAChE,0DAA0D;AAC1D,EAAE;AACF,qEAAqE;AACrE,sEAAsE;AACtE,kEAAkE;AAClE,qEAAqE;AACrE,mEAAmE;AACnE,gEAAgE;AAChE,EAAE;AACF,kEAAkE;AAClE,kDAAkD;AAClD,gDAAgD;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAmBnE,MAAM,YAAY,GAAG,sBAAsB,CAAA;AAE3C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAkB;IACjD,IAAI,EAAE,0BAA0B;IAChC,KAAK,CAAC,KAAK;QACT,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9C,IAAI,MAAc,CAAA;YAClB,IAAI,CAAC;gBACH,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,0DAA0D;gBAC1D,+BAA+B;gBAC/B,OAAO,IAAI,CAAA;YACb,CAAC;YACD,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAA;YAC5C,+DAA+D;YAC/D,8DAA8D;YAC9D,2CAA2C;YAC3C,6DAA6D;YAC7D,8DAA8D;YAC9D,8DAA8D;YAC9D,8DAA8D;YAC9D,4DAA4D;YAC5D,6DAA6D;YAC7D,8CAA8C;YAC9C,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;gBAC3D,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;YAC1C,CAAC;YAAC,MAAM,CAAC;gBACP,uDAAuD;gBACvD,4DAA4D;gBAC5D,0DAA0D;gBAC1D,+DAA+D;gBAC/D,OAAO,IAAI,CAAA;YACb,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;CACF,CAAA"}
@@ -0,0 +1,128 @@
1
+ import type { ContainerNode } from "@reactra/babel-plugin";
2
+ /**
3
+ * One discovered container (component / store / service) extracted from a
4
+ * single source file via {@link extractContainers}.
5
+ */
6
+ export interface ContainerInfo {
7
+ /** `"component" | "store" | "service"` — derived from the marker name. */
8
+ readonly kind: "component" | "store" | "service";
9
+ readonly name: string;
10
+ /** True when the declaration carries `export` (v2 independent export token). */
11
+ readonly exported: boolean;
12
+ /**
13
+ * Service modifier (`"scoped" | "server"`) when present; undefined for
14
+ * plain `service X {}` and for stores/components.
15
+ */
16
+ readonly serviceModifier?: string;
17
+ /**
18
+ * Names of every `inject service X` site inside this container's body —
19
+ * used by {@link collectServiceInjectionDiagnostics} to find injection
20
+ * sites that reference undeclared services.
21
+ */
22
+ readonly injectedServiceNames: string[];
23
+ }
24
+ /**
25
+ * Project the already-compiled container graph into the three-way
26
+ * {@link ContainerInfo}. The pure half of {@link extractContainers}, split out
27
+ * so the A3 graph cache can derive from a single per-file compile (the scanner
28
+ * passes a cached `ContainerNode[]` instead of re-reading + re-compiling).
29
+ */
30
+ export declare const containersToInfo: (containers: readonly ContainerNode[]) => ContainerInfo[];
31
+ /**
32
+ * Extract all containers (components, stores, services) from a raw DSL source
33
+ * file via the compiled graph. `injectedServiceNames` is read from each
34
+ * container's `injects` list (no body-boundary guessing — a mis-attribution-
35
+ * proof replacement for the old "next top-level const" slice heuristic).
36
+ *
37
+ * Returns `[]` on malformed source — callers must treat an empty result as
38
+ * "no containers" rather than an error.
39
+ */
40
+ export declare const extractContainers: (source: string) => ContainerInfo[];
41
+ /**
42
+ * Extract the first `export component <Name>` declaration name from `source`.
43
+ * Returns `null` when no exported component is found (plain React TSX, no
44
+ * DSL container, or a malformed file).
45
+ *
46
+ * Replaces `extractPageComponentName` in walker.ts — fixes the R2 regression
47
+ * where the old regex missed `export component Foo uses traceable {` (the
48
+ * header-`uses` form). The graph normalises all header forms before we read.
49
+ */
50
+ export declare const extractComponentName: (source: string) => string | null;
51
+ /** Container-graph half of {@link extractComponentName} (A3 cache path). */
52
+ export declare const componentNameFromContainers: (containers: readonly ContainerNode[]) => string | null;
53
+ /**
54
+ * Extract the FIRST component's `prefetch` trigger from `source` via the
55
+ * compiled graph (`container.prefetch.trigger`). Returns the trigger word, or
56
+ * `undefined` when the file has no component, no `prefetch` declaration, or is
57
+ * malformed.
58
+ *
59
+ * First-component-wins matches the existing manifest semantics (one prefetch
60
+ * policy per page). Reads the graph rather than line-regexing the raw DSL, so
61
+ * the header-`uses` form (`export component X uses traceable { prefetch … }`)
62
+ * is recognised — the same regression class that produced the header-`uses`
63
+ * walker bug (B5).
64
+ */
65
+ export declare const extractPrefetch: (source: string) => "hover" | "visible" | "mount" | "none" | undefined;
66
+ /** Container-graph half of {@link extractPrefetch} (A3 cache path). */
67
+ export declare const prefetchFromContainers: (containers: readonly ContainerNode[]) => "hover" | "visible" | "mount" | "none" | undefined;
68
+ /**
69
+ * One `query` declaration extracted from a page component. Shape is unchanged
70
+ * from the existing `RouteQueryDecl` in walker.ts — only the extraction
71
+ * mechanism changes.
72
+ */
73
+ export interface QueryDecl {
74
+ readonly name: string;
75
+ readonly tsType: string;
76
+ }
77
+ /**
78
+ * Extract `query` declarations from `source` via the compiled graph
79
+ * (`container.queries`, source-ordered). Returns `[]` for files with no
80
+ * queries, malformed files, or non-component files.
81
+ *
82
+ * C5 (B5): folded off `QUERY_MARKER_RE` onto the graph — `QueryNode.tsType` is
83
+ * the already-decoded StringLiteral value from Pass 2, so the v2 semicolon leak
84
+ * and the manual `\"`-unescaping the old regex carried are both gone for free.
85
+ * `tsType` defaults to `"string"` in Pass 2, matching the prior fallback. Inline
86
+ * `inject store s(query foo)` args never produce a `QueryNode`, so they're
87
+ * excluded as before.
88
+ */
89
+ export declare const extractQueries: (source: string) => QueryDecl[];
90
+ /** Container-graph half of {@link extractQueries} (A3 cache path). */
91
+ export declare const queriesFromContainers: (containers: readonly ContainerNode[]) => QueryDecl[];
92
+ /**
93
+ * Extract every store declaration from `source`. Returns `{ name, exported }`
94
+ * pairs. Non-exported stores are included so callers can filter by `exported`.
95
+ *
96
+ * Replaces `STORE_DECL_RE` / `extractStoreNames` in walker.ts with a
97
+ * graph-based approach that correctly handles:
98
+ * - v2 `export app store X` / `export subtree store X "/p"` forms
99
+ * - header-`uses` `export store X uses undoable`
100
+ * - non-exported `app store X` / `route store X`
101
+ * - keyed `route store X(key: string)`
102
+ */
103
+ export declare const extractStoreNames: (source: string) => {
104
+ name: string;
105
+ exported: boolean;
106
+ }[];
107
+ /** Container-graph half of {@link extractStoreNames} (A3 cache path). */
108
+ export declare const storeNamesFromContainers: (containers: readonly ContainerNode[]) => {
109
+ name: string;
110
+ exported: boolean;
111
+ }[];
112
+ /**
113
+ * Extract every service declaration from `source`. Returns
114
+ * `{ name, exported, serviceModifier? }` triples. Non-exported services are
115
+ * included so callers can filter by `exported`.
116
+ */
117
+ export declare const extractServiceNames: (source: string) => {
118
+ name: string;
119
+ exported: boolean;
120
+ serviceModifier?: string;
121
+ }[];
122
+ /** Container-graph half of {@link extractServiceNames} (A3 cache path). */
123
+ export declare const serviceNamesFromContainers: (containers: readonly ContainerNode[]) => {
124
+ name: string;
125
+ exported: boolean;
126
+ serviceModifier?: string;
127
+ }[];
128
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAIzE;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS,CAAA;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,EAAE,CAAA;CACxC;AAoDD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,YAAY,SAAS,aAAa,EAAE,KACnC,aAAa,EAOX,CAAA;AAEL;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,aAAa,EACtB,CAAA;AAI1C;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,IACV,CAAA;AAErD,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,GACtC,YAAY,SAAS,aAAa,EAAE,KACnC,MAAM,GAAG,IAIX,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,MAAM,KACb,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SACI,CAAA;AAEhD,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,GACjC,YAAY,SAAS,aAAa,EAAE,KACnC,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,SACuB,CAAA;AAEnE;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,KAAG,SAAS,EACV,CAAA;AAE/C,sEAAsE;AACtE,eAAO,MAAM,qBAAqB,GAChC,YAAY,SAAS,aAAa,EAAE,KACnC,SAAS,EAGT,CAAA;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,EACpC,CAAA;AAElD,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,aAAa,EAAE,KACnC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,EAGgB,CAAA;AAEtD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,KACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,EACZ,CAAA;AAEpD,2EAA2E;AAC3E,eAAO,MAAM,0BAA0B,GACrC,YAAY,SAAS,aAAa,EAAE,KACnC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,EAGwB,CAAA"}
@@ -0,0 +1,155 @@
1
+ // @reactra/vite-plugin — parse-based container extraction
2
+ //
3
+ // Owner: reactra-compiler-spec.md §4 Pass 1.1 (preprocess markers) +
4
+ // reactra-router-spec.md §5.1 (manifest format) +
5
+ // Phase 3 ADR-1 (reuse the compiler — single grammar source of truth).
6
+ //
7
+ // Each exported function calls `compileToGraph()` once per file and reads the
8
+ // intermediate FileGraph (Pass 2 extract). This replaces the hand-rolled regex
9
+ // passes in walker.ts AND the marker-regex / body-slice heuristics this file
10
+ // used to carry — the R2/B5 regression mitigation: the walker now shares the
11
+ // compiler's exact grammar for header-`uses` forms, v2 store/service lifetimes,
12
+ // and `inject` site classification (no body-boundary guessing).
13
+ import { compileToGraph } from "@reactra/babel-plugin";
14
+ // ─── graph helper ─────────────────────────────────────────────────────────────
15
+ /**
16
+ * Run `compileToGraph()` on `source` and return its `containers`, or `[]` on
17
+ * any failure (malformed / mid-edit file). The try/catch is intentional: a
18
+ * broken source file must not crash the manifest regen loop — every public
19
+ * function below degrades to its documented empty / null fallback.
20
+ *
21
+ * READ-ONLY (C1): consumers read existing graph fields; codegen is never run.
22
+ */
23
+ const containerNodes = (source) => {
24
+ try {
25
+ return compileToGraph(source).graph.containers;
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ };
31
+ /**
32
+ * Collapse the compiler's five-way {@link ContainerKind} into the three-way
33
+ * `ContainerInfo.kind` the walker consumes. The three store lifetimes
34
+ * (`route-store` / `session-store` / `export-store`) all surface as `"store"`.
35
+ *
36
+ * Exhaustive by construction: a 4th store kind added to {@link ContainerKind}
37
+ * makes the `default` branch a `never`-assignment compile error (C2) — so a
38
+ * future store lifetime can never be silently dropped from the manifest.
39
+ */
40
+ const toInfoKind = (kind) => {
41
+ switch (kind) {
42
+ case "component":
43
+ return "component";
44
+ case "service":
45
+ return "service";
46
+ case "route-store":
47
+ case "session-store":
48
+ case "export-store":
49
+ return "store";
50
+ default: {
51
+ const _exhaustive = kind;
52
+ return _exhaustive;
53
+ }
54
+ }
55
+ };
56
+ /** Service-injection-site names for a container, read straight from the graph. */
57
+ const injectedServiceNamesOf = (c) => c.injects.filter((i) => i.serviceKind === "service").map((i) => i.name);
58
+ // ─── Public API ───────────────────────────────────────────────────────────────
59
+ /**
60
+ * Project the already-compiled container graph into the three-way
61
+ * {@link ContainerInfo}. The pure half of {@link extractContainers}, split out
62
+ * so the A3 graph cache can derive from a single per-file compile (the scanner
63
+ * passes a cached `ContainerNode[]` instead of re-reading + re-compiling).
64
+ */
65
+ export const containersToInfo = (containers) => containers.map((c) => ({
66
+ kind: toInfoKind(c.kind),
67
+ name: c.name,
68
+ exported: c.exported,
69
+ serviceModifier: c.serviceModifier,
70
+ injectedServiceNames: injectedServiceNamesOf(c),
71
+ }));
72
+ /**
73
+ * Extract all containers (components, stores, services) from a raw DSL source
74
+ * file via the compiled graph. `injectedServiceNames` is read from each
75
+ * container's `injects` list (no body-boundary guessing — a mis-attribution-
76
+ * proof replacement for the old "next top-level const" slice heuristic).
77
+ *
78
+ * Returns `[]` on malformed source — callers must treat an empty result as
79
+ * "no containers" rather than an error.
80
+ */
81
+ export const extractContainers = (source) => containersToInfo(containerNodes(source));
82
+ // ─── Focused extract fns (used by walker.ts) ──────────────────────────────────
83
+ /**
84
+ * Extract the first `export component <Name>` declaration name from `source`.
85
+ * Returns `null` when no exported component is found (plain React TSX, no
86
+ * DSL container, or a malformed file).
87
+ *
88
+ * Replaces `extractPageComponentName` in walker.ts — fixes the R2 regression
89
+ * where the old regex missed `export component Foo uses traceable {` (the
90
+ * header-`uses` form). The graph normalises all header forms before we read.
91
+ */
92
+ export const extractComponentName = (source) => componentNameFromContainers(containerNodes(source));
93
+ /** Container-graph half of {@link extractComponentName} (A3 cache path). */
94
+ export const componentNameFromContainers = (containers) => {
95
+ // Route pages are `export component` — only EXPORTED components qualify.
96
+ const component = containers.find((c) => c.kind === "component" && c.exported);
97
+ return component?.name ?? null;
98
+ };
99
+ /**
100
+ * Extract the FIRST component's `prefetch` trigger from `source` via the
101
+ * compiled graph (`container.prefetch.trigger`). Returns the trigger word, or
102
+ * `undefined` when the file has no component, no `prefetch` declaration, or is
103
+ * malformed.
104
+ *
105
+ * First-component-wins matches the existing manifest semantics (one prefetch
106
+ * policy per page). Reads the graph rather than line-regexing the raw DSL, so
107
+ * the header-`uses` form (`export component X uses traceable { prefetch … }`)
108
+ * is recognised — the same regression class that produced the header-`uses`
109
+ * walker bug (B5).
110
+ */
111
+ export const extractPrefetch = (source) => prefetchFromContainers(containerNodes(source));
112
+ /** Container-graph half of {@link extractPrefetch} (A3 cache path). */
113
+ export const prefetchFromContainers = (containers) => containers.find((c) => c.kind === "component")?.prefetch?.trigger;
114
+ /**
115
+ * Extract `query` declarations from `source` via the compiled graph
116
+ * (`container.queries`, source-ordered). Returns `[]` for files with no
117
+ * queries, malformed files, or non-component files.
118
+ *
119
+ * C5 (B5): folded off `QUERY_MARKER_RE` onto the graph — `QueryNode.tsType` is
120
+ * the already-decoded StringLiteral value from Pass 2, so the v2 semicolon leak
121
+ * and the manual `\"`-unescaping the old regex carried are both gone for free.
122
+ * `tsType` defaults to `"string"` in Pass 2, matching the prior fallback. Inline
123
+ * `inject store s(query foo)` args never produce a `QueryNode`, so they're
124
+ * excluded as before.
125
+ */
126
+ export const extractQueries = (source) => queriesFromContainers(containerNodes(source));
127
+ /** Container-graph half of {@link extractQueries} (A3 cache path). */
128
+ export const queriesFromContainers = (containers) => containers.flatMap((c) => c.queries.map((q) => ({ name: q.name, tsType: q.tsType.trim() || "string" })));
129
+ /**
130
+ * Extract every store declaration from `source`. Returns `{ name, exported }`
131
+ * pairs. Non-exported stores are included so callers can filter by `exported`.
132
+ *
133
+ * Replaces `STORE_DECL_RE` / `extractStoreNames` in walker.ts with a
134
+ * graph-based approach that correctly handles:
135
+ * - v2 `export app store X` / `export subtree store X "/p"` forms
136
+ * - header-`uses` `export store X uses undoable`
137
+ * - non-exported `app store X` / `route store X`
138
+ * - keyed `route store X(key: string)`
139
+ */
140
+ export const extractStoreNames = (source) => storeNamesFromContainers(containerNodes(source));
141
+ /** Container-graph half of {@link extractStoreNames} (A3 cache path). */
142
+ export const storeNamesFromContainers = (containers) => containersToInfo(containers)
143
+ .filter((c) => c.kind === "store")
144
+ .map(({ name, exported }) => ({ name, exported }));
145
+ /**
146
+ * Extract every service declaration from `source`. Returns
147
+ * `{ name, exported, serviceModifier? }` triples. Non-exported services are
148
+ * included so callers can filter by `exported`.
149
+ */
150
+ export const extractServiceNames = (source) => serviceNamesFromContainers(containerNodes(source));
151
+ /** Container-graph half of {@link extractServiceNames} (A3 cache path). */
152
+ export const serviceNamesFromContainers = (containers) => containersToInfo(containers)
153
+ .filter((c) => c.kind === "service")
154
+ .map(({ name, exported, serviceModifier }) => ({ name, exported, serviceModifier }));
155
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { ContainerNode } from "@reactra/babel-plugin";
2
+ /**
3
+ * The declaration set of one source file that the manifest + resource-names
4
+ * output depends on. All arrays are sorted for order-insensitive comparison
5
+ * (declaration order in the file doesn't affect the generated output —
6
+ * scanStores/scanServices/collectResourceNames all sort their results).
7
+ */
8
+ export interface DeclFingerprint {
9
+ /** Sorted store binding names declared in the file. */
10
+ readonly stores: string[];
11
+ /** Sorted service binding names declared in the file. */
12
+ readonly services: string[];
13
+ /** Sorted `name:modifier` pairs (modifier = "" when none) — flips a regen. */
14
+ readonly serviceModifiers: string[];
15
+ /** Sorted resource names declared across the file's containers. */
16
+ readonly resources: string[];
17
+ }
18
+ /**
19
+ * Derive a {@link DeclFingerprint} from a file's compiled containers. Pure:
20
+ * same containers → identical fingerprint. Caller supplies the cached
21
+ * `containersFor(path)` result so no extra compile is paid.
22
+ */
23
+ export declare const fingerprintOf: (containers: readonly ContainerNode[]) => DeclFingerprint;
24
+ /** True when two fingerprints declare the same set of stores/services/resources. */
25
+ export declare const fingerprintsEqual: (a: DeclFingerprint, b: DeclFingerprint) => boolean;
26
+ /** True when `path` is a page file (under `src/pages/`) — routes/queries/prefetch live here. */
27
+ export declare const isPagesFile: (projectRoot: string, eventPath: string) => boolean;
28
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;IACnC,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAC7B;AAQD;;;;GAIG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,SAAS,aAAa,EAAE,KACnC,eAoBF,CAAA;AAED,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,GAAI,GAAG,eAAe,EAAE,GAAG,eAAe,KAAG,OAS1E,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,WAAW,GAAI,aAAa,MAAM,EAAE,WAAW,MAAM,KAAG,OAKpE,CAAA"}
@@ -0,0 +1,68 @@
1
+ // @reactra/vite-plugin — per-file declaration fingerprint (A3 increment 2)
2
+ //
3
+ // Owner: backlogs/framework-review-2026-06-12.md §A3 +
4
+ // reactra-compiler-spec.md §7.1 (vite plugin file-watcher hook).
5
+ //
6
+ // A3 increment 2: on a `change` event we want to skip the src/-wide store/
7
+ // service/resource rescan when the edit only touched a body — the declared SET
8
+ // (store names, service names + modifiers, resource names) is what the manifest
9
+ // + resource-names files are derived from, so if that set is unchanged the wide
10
+ // rescan can't change the generated output.
11
+ //
12
+ // We deliberately do NOT implement the backlog's literal "skip store/service
13
+ // rescan on every change" — a `change` CAN rename a store, add/remove one, or
14
+ // flip a service modifier (and a `[id]`→`[customerId]` param rename arrives as
15
+ // `change` too). The fingerprint differ catches all of those: any decl-set
16
+ // change forces a full regen.
17
+ //
18
+ // The fingerprint is cheap strings derived from the already-cached compile —
19
+ // no extra read, no extra compile.
20
+ const STORE_KINDS = new Set([
21
+ "route-store",
22
+ "session-store",
23
+ "export-store",
24
+ ]);
25
+ /**
26
+ * Derive a {@link DeclFingerprint} from a file's compiled containers. Pure:
27
+ * same containers → identical fingerprint. Caller supplies the cached
28
+ * `containersFor(path)` result so no extra compile is paid.
29
+ */
30
+ export const fingerprintOf = (containers) => {
31
+ const stores = [];
32
+ const services = [];
33
+ const serviceModifiers = [];
34
+ const resources = [];
35
+ for (const c of containers) {
36
+ if (STORE_KINDS.has(c.kind))
37
+ stores.push(c.name);
38
+ if (c.kind === "service") {
39
+ services.push(c.name);
40
+ serviceModifiers.push(`${c.name}:${c.serviceModifier ?? ""}`);
41
+ }
42
+ for (const r of c.resources)
43
+ resources.push(r.name);
44
+ }
45
+ const sort = (xs) => xs.sort((a, b) => a.localeCompare(b));
46
+ return {
47
+ stores: sort(stores),
48
+ services: sort(services),
49
+ serviceModifiers: sort(serviceModifiers),
50
+ resources: sort(resources),
51
+ };
52
+ };
53
+ /** True when two fingerprints declare the same set of stores/services/resources. */
54
+ export const fingerprintsEqual = (a, b) => {
55
+ const sameList = (xs, ys) => xs.length === ys.length && xs.every((x, i) => x === ys[i]);
56
+ return (sameList(a.stores, b.stores) &&
57
+ sameList(a.services, b.services) &&
58
+ sameList(a.serviceModifiers, b.serviceModifiers) &&
59
+ sameList(a.resources, b.resources));
60
+ };
61
+ /** True when `path` is a page file (under `src/pages/`) — routes/queries/prefetch live here. */
62
+ export const isPagesFile = (projectRoot, eventPath) => {
63
+ const root = projectRoot.replace(/\\/g, "/");
64
+ const target = eventPath.replace(/\\/g, "/");
65
+ const prefix = (root.endsWith("/") ? root + "src/pages/" : root + "/src/pages/");
66
+ return target.startsWith(prefix);
67
+ };
68
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { ContainerNode } from "@reactra/babel-plugin";
2
+ /**
3
+ * A per-regen cache of compiled container graphs, keyed by absolute file path.
4
+ * One instance lives for the duration of a single manifest regeneration.
5
+ */
6
+ export interface GraphCache {
7
+ /**
8
+ * Read + parse `path` once per regen; returns `graph.containers`.
9
+ * Returns `[]` on any read OR compile failure (malformed / mid-edit file),
10
+ * so callers degrade to their documented empty result rather than throwing.
11
+ *
12
+ * @param path absolute file path (the cache key — NO content-hash / mtime).
13
+ */
14
+ containersFor: (path: string) => readonly ContainerNode[];
15
+ }
16
+ /**
17
+ * Build a fresh {@link GraphCache}. Memoizes by absolute path: the first
18
+ * `containersFor(path)` reads + compiles; subsequent calls for the same path
19
+ * return the stashed result. Discard the instance when the regen completes —
20
+ * it never sees stale content because a new cache is built per regen.
21
+ */
22
+ export declare const createGraphCache: () => GraphCache;
23
+ //# sourceMappingURL=graph-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-cache.d.ts","sourceRoot":"","sources":["../src/graph-cache.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAE1D;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,aAAa,EAAE,CAAA;CAC1D;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,UAqBnC,CAAA"}
@@ -0,0 +1,52 @@
1
+ // @reactra/vite-plugin — per-regen graph cache
2
+ //
3
+ // Owner: reactra-compiler-spec.md §7.1 (vite plugin file-watcher hook) +
4
+ // backlogs/framework-review-2026-06-12.md §A3 (one walk, one preprocess).
5
+ //
6
+ // A3 increment 1: every container-reading scanner (scanPages, scanStores,
7
+ // scanServices, collectServiceInjectionDiagnostics, collectResourceNames) used
8
+ // to read + `compileToGraph` each file independently — after B5 landed, a 200-
9
+ // file regen compiled each file 4–5×. This cache folds the read + compile +
10
+ // try/catch fallback into ONE place, memoized by absolute path, so each file
11
+ // compiles AT MOST once per regen.
12
+ //
13
+ // Lifetime: ONE instance per regen (created at the top of regenerateRouteManifest
14
+ // and the watcher event handler, then thrown away). NO module-level instance,
15
+ // NO content-hash / mtime key, NO eviction (C1) — a regen is short-lived and a
16
+ // fresh cache each time is correct by construction. The fingerprint differ
17
+ // (increment 2) is the only module-level state.
18
+ //
19
+ // READ-ONLY (C3): reads `graph.containers` only; codegen is never run. Returns
20
+ // `[]` on read OR compile failure, preserving every scanner's documented empty
21
+ // degradation. Node-portable (C7): `node:fs` + plain `Map` only.
22
+ import { readFileSync } from "node:fs";
23
+ import { compileToGraph } from "@reactra/babel-plugin";
24
+ /**
25
+ * Build a fresh {@link GraphCache}. Memoizes by absolute path: the first
26
+ * `containersFor(path)` reads + compiles; subsequent calls for the same path
27
+ * return the stashed result. Discard the instance when the regen completes —
28
+ * it never sees stale content because a new cache is built per regen.
29
+ */
30
+ export const createGraphCache = () => {
31
+ const byPath = new Map();
32
+ const containersFor = (path) => {
33
+ const cached = byPath.get(path);
34
+ if (cached)
35
+ return cached;
36
+ let containers;
37
+ try {
38
+ const source = readFileSync(path, "utf8");
39
+ // READ-ONLY: consume only `.graph.containers`; codegen is never run.
40
+ containers = compileToGraph(source).graph.containers;
41
+ }
42
+ catch {
43
+ // Read OR compile failure → degrade to empty (C3). The next regen
44
+ // (a fresh cache) recovers once the file is well-formed / readable.
45
+ containers = [];
46
+ }
47
+ byPath.set(path, containers);
48
+ return containers;
49
+ };
50
+ return { containersFor };
51
+ };
52
+ //# sourceMappingURL=graph-cache.js.map