@rangojs/router 0.0.0-experimental.10
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/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize path to forward slashes
|
|
8
|
+
*/
|
|
9
|
+
function normalizePath(p: string): string {
|
|
10
|
+
return p.split(path.sep).join("/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a short hash for a location state key
|
|
15
|
+
* Uses first 8 chars of SHA-256 hash for uniqueness while keeping keys short
|
|
16
|
+
* Appends export name for easier debugging: "abc123#ProductState"
|
|
17
|
+
*/
|
|
18
|
+
function hashLocationStateKey(filePath: string, exportName: string): string {
|
|
19
|
+
const input = `${filePath}#${exportName}`;
|
|
20
|
+
const hash = crypto.createHash("sha256").update(input).digest("hex");
|
|
21
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if file imports createLocationState from rsc-router
|
|
26
|
+
*/
|
|
27
|
+
function hasCreateLocationStateImport(code: string): boolean {
|
|
28
|
+
// Match: import { createLocationState } from "@rangojs/router" or "@rangojs/router/client"
|
|
29
|
+
const pattern =
|
|
30
|
+
/import\s*\{[^}]*\bcreateLocationState\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
|
|
31
|
+
return pattern.test(code);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Transform export const X = createLocationState<...>() patterns to inject key
|
|
36
|
+
*
|
|
37
|
+
* The key is injected as the first parameter if not present:
|
|
38
|
+
* - createLocationState() -> createLocationState("id")
|
|
39
|
+
* - createLocationState<T>() -> createLocationState<T>("id")
|
|
40
|
+
*/
|
|
41
|
+
function transformLocationStateExports(
|
|
42
|
+
code: string,
|
|
43
|
+
filePath: string,
|
|
44
|
+
sourceId?: string,
|
|
45
|
+
isBuild: boolean = false
|
|
46
|
+
): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
|
|
47
|
+
// Quick bail-out
|
|
48
|
+
if (!code.includes("createLocationState")) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Must have direct import from rsc-router
|
|
53
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Match: export const X = createLocationState<...>(
|
|
58
|
+
// Captures the export name (X)
|
|
59
|
+
const pattern = /export\s+const\s+(\w+)\s*=\s*createLocationState\s*(?:<[^>]*>)?\s*\(/g;
|
|
60
|
+
|
|
61
|
+
const s = new MagicString(code);
|
|
62
|
+
let hasChanges = false;
|
|
63
|
+
let match: RegExpExecArray | null;
|
|
64
|
+
|
|
65
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
66
|
+
const exportName = match[1];
|
|
67
|
+
const matchEnd = match.index + match[0].length;
|
|
68
|
+
|
|
69
|
+
// Find the end of the createLocationState(...) call
|
|
70
|
+
let parenDepth = 1;
|
|
71
|
+
let i = matchEnd;
|
|
72
|
+
while (i < code.length && parenDepth > 0) {
|
|
73
|
+
if (code[i] === "(") parenDepth++;
|
|
74
|
+
if (code[i] === ")") parenDepth--;
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// i now points just after the closing )
|
|
79
|
+
const closeParenPos = i - 1;
|
|
80
|
+
|
|
81
|
+
// Check if there are any arguments (content between open and close paren)
|
|
82
|
+
const content = code.slice(matchEnd, closeParenPos).trim();
|
|
83
|
+
const hasArgs = content.length > 0;
|
|
84
|
+
|
|
85
|
+
// Find the semicolon or end of statement
|
|
86
|
+
let statementEnd = i;
|
|
87
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
88
|
+
statementEnd++;
|
|
89
|
+
}
|
|
90
|
+
if (code[statementEnd] === ";") {
|
|
91
|
+
statementEnd++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Generate key: hashed in production, readable in dev
|
|
95
|
+
const stateKey = isBuild
|
|
96
|
+
? hashLocationStateKey(filePath, exportName)
|
|
97
|
+
: `${filePath}#${exportName}`;
|
|
98
|
+
|
|
99
|
+
// Inject key as the first (and only) parameter
|
|
100
|
+
// createLocationState() -> createLocationState("id")
|
|
101
|
+
if (!hasArgs) {
|
|
102
|
+
s.appendLeft(closeParenPos, `"${stateKey}"`);
|
|
103
|
+
} else {
|
|
104
|
+
// Already has a key, skip (shouldn't happen with new API, but be safe)
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Also set __rsc_ls_key property for verification
|
|
109
|
+
const propInjection = `\n${exportName}.__rsc_ls_key = "__rsc_ls_${stateKey}";`;
|
|
110
|
+
s.appendRight(statementEnd, propInjection);
|
|
111
|
+
hasChanges = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!hasChanges) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
code: s.toString(),
|
|
120
|
+
map: s.generateMap({ source: sourceId, includeContent: true }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Vite plugin that exposes location state keys on createLocationState calls.
|
|
126
|
+
*
|
|
127
|
+
* When users create location states with createLocationState(), this plugin:
|
|
128
|
+
* 1. Injects an auto-generated key as the first parameter
|
|
129
|
+
* 2. Sets __rsc_ls_key property for verification
|
|
130
|
+
*
|
|
131
|
+
* This allows location states to be created without explicit keys:
|
|
132
|
+
* - Before: export const ProductState = createLocationState<Product>("product")
|
|
133
|
+
* - After: export const ProductState = createLocationState<Product>()
|
|
134
|
+
*
|
|
135
|
+
* The key is auto-generated from file path + export name.
|
|
136
|
+
*
|
|
137
|
+
* Requirements:
|
|
138
|
+
* - Must use direct import: import { createLocationState } from "@rangojs/router"
|
|
139
|
+
* - Must use named export: export const MyState = createLocationState(...)
|
|
140
|
+
*/
|
|
141
|
+
export function exposeLocationStateId(): Plugin {
|
|
142
|
+
let config: ResolvedConfig;
|
|
143
|
+
let isBuild = false;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
name: "@rangojs/router:expose-location-state-id",
|
|
147
|
+
enforce: "post",
|
|
148
|
+
|
|
149
|
+
configResolved(resolvedConfig) {
|
|
150
|
+
config = resolvedConfig;
|
|
151
|
+
isBuild = config.command === "build";
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
transform(code, id) {
|
|
155
|
+
// Skip node_modules
|
|
156
|
+
if (id.includes("/node_modules/")) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Quick bail-out
|
|
161
|
+
if (!code.includes("createLocationState")) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Must have direct import from rsc-router
|
|
166
|
+
if (!hasCreateLocationStateImport(code)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Get relative path for the key
|
|
171
|
+
const relativePath = normalizePath(path.relative(config.root, id));
|
|
172
|
+
|
|
173
|
+
// Transform: inject key
|
|
174
|
+
return transformLocationStateExports(code, relativePath, id, isBuild);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type { Plugin, ResolvedConfig } from "vite";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize path to forward slashes
|
|
8
|
+
*/
|
|
9
|
+
function normalizePath(p: string): string {
|
|
10
|
+
return p.split(path.sep).join("/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a short hash for a prerender handler ID.
|
|
15
|
+
* Uses first 8 chars of SHA-256 hash for uniqueness while keeping IDs short.
|
|
16
|
+
* Appends export name for easier debugging: "abc123#DocsPage"
|
|
17
|
+
*/
|
|
18
|
+
function hashPrerenderHandlerId(filePath: string, exportName: string): string {
|
|
19
|
+
const input = `${filePath}#${exportName}`;
|
|
20
|
+
const hash = crypto.createHash("sha256").update(input).digest("hex");
|
|
21
|
+
return `${hash.slice(0, 8)}#${exportName}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if file imports createPrerenderHandler from @rangojs/router
|
|
26
|
+
*/
|
|
27
|
+
function hasCreatePrerenderHandlerImport(code: string): boolean {
|
|
28
|
+
const pattern =
|
|
29
|
+
/import\s*\{[^}]*\bcreatePrerenderHandler\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
|
|
30
|
+
return pattern.test(code);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Skip past a string literal, template literal, or comment starting at pos.
|
|
35
|
+
* Returns the index after the closing delimiter, or pos if not at a
|
|
36
|
+
* string/comment start. Handles escape sequences and nested ${} in templates.
|
|
37
|
+
*/
|
|
38
|
+
function skipStringOrComment(code: string, pos: number): number {
|
|
39
|
+
const ch = code[pos];
|
|
40
|
+
|
|
41
|
+
if (ch === '"' || ch === "'") {
|
|
42
|
+
for (let j = pos + 1; j < code.length; j++) {
|
|
43
|
+
if (code[j] === "\\") { j++; continue; }
|
|
44
|
+
if (code[j] === ch) return j + 1;
|
|
45
|
+
}
|
|
46
|
+
return code.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ch === "`") {
|
|
50
|
+
let j = pos + 1;
|
|
51
|
+
while (j < code.length) {
|
|
52
|
+
if (code[j] === "\\") { j += 2; continue; }
|
|
53
|
+
if (code[j] === "`") return j + 1;
|
|
54
|
+
if (code[j] === "$" && j + 1 < code.length && code[j + 1] === "{") {
|
|
55
|
+
j += 2;
|
|
56
|
+
let braceDepth = 1;
|
|
57
|
+
while (j < code.length && braceDepth > 0) {
|
|
58
|
+
const inner = skipStringOrComment(code, j);
|
|
59
|
+
if (inner > j) { j = inner; continue; }
|
|
60
|
+
if (code[j] === "{") braceDepth++;
|
|
61
|
+
else if (code[j] === "}") braceDepth--;
|
|
62
|
+
if (braceDepth > 0) j++;
|
|
63
|
+
}
|
|
64
|
+
if (braceDepth === 0) j++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
j++;
|
|
68
|
+
}
|
|
69
|
+
return j;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ch === "/" && pos + 1 < code.length) {
|
|
73
|
+
if (code[pos + 1] === "/") {
|
|
74
|
+
const eol = code.indexOf("\n", pos + 2);
|
|
75
|
+
return eol === -1 ? code.length : eol + 1;
|
|
76
|
+
}
|
|
77
|
+
if (code[pos + 1] === "*") {
|
|
78
|
+
const end = code.indexOf("*/", pos + 2);
|
|
79
|
+
return end === -1 ? code.length : end + 2;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return pos;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find the matching closing paren starting after an already-opened paren.
|
|
88
|
+
* Skips strings, template literals, and comments so parens inside them
|
|
89
|
+
* don't affect depth tracking. Returns the index after the closing paren.
|
|
90
|
+
*/
|
|
91
|
+
function findMatchingParen(code: string, startPos: number): number {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let i = startPos;
|
|
94
|
+
while (i < code.length && depth > 0) {
|
|
95
|
+
const skipped = skipStringOrComment(code, i);
|
|
96
|
+
if (skipped > i) { i = skipped; continue; }
|
|
97
|
+
if (code[i] === "(") depth++;
|
|
98
|
+
if (code[i] === ")") depth--;
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
return i;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Count the number of top-level arguments in a function call.
|
|
106
|
+
* Skips nested parens, brackets, braces, strings, and comments.
|
|
107
|
+
*/
|
|
108
|
+
function countArgs(code: string, startPos: number, endPos: number): number {
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let argCount = 0;
|
|
111
|
+
let hasContent = false;
|
|
112
|
+
let i = startPos;
|
|
113
|
+
|
|
114
|
+
while (i < endPos) {
|
|
115
|
+
const skipped = skipStringOrComment(code, i);
|
|
116
|
+
if (skipped > i) { hasContent = true; i = skipped; continue; }
|
|
117
|
+
|
|
118
|
+
const char = code[i];
|
|
119
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
120
|
+
depth++;
|
|
121
|
+
hasContent = true;
|
|
122
|
+
} else if (char === ")" || char === "]" || char === "}") {
|
|
123
|
+
depth--;
|
|
124
|
+
} else if (char === "," && depth === 0) {
|
|
125
|
+
argCount++;
|
|
126
|
+
} else if (!/\s/.test(char)) {
|
|
127
|
+
hasContent = true;
|
|
128
|
+
}
|
|
129
|
+
i++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return hasContent ? argCount + 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Transform export const X = createPrerenderHandler(...) patterns to inject $$id.
|
|
137
|
+
*
|
|
138
|
+
* Overload shapes:
|
|
139
|
+
* 1 arg (handler) -> inject undefined, "id" (pad for options + id)
|
|
140
|
+
* 2 args (getParams+handler OR handler+options) -> inject , "id"
|
|
141
|
+
* 3 args (getParams+handler+options) -> inject , "id"
|
|
142
|
+
*
|
|
143
|
+
* The __injectedId is always the LAST parameter.
|
|
144
|
+
*/
|
|
145
|
+
function transformPrerenderHandlerExports(
|
|
146
|
+
code: string,
|
|
147
|
+
filePath: string,
|
|
148
|
+
sourceId?: string,
|
|
149
|
+
isBuild: boolean = false,
|
|
150
|
+
): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
|
|
151
|
+
if (!code.includes("createPrerenderHandler")) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!hasCreatePrerenderHandlerImport(code)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Match: export const X = createPrerenderHandler<...>(
|
|
160
|
+
const pattern =
|
|
161
|
+
/export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
162
|
+
|
|
163
|
+
const s = new MagicString(code);
|
|
164
|
+
let hasChanges = false;
|
|
165
|
+
let match: RegExpExecArray | null;
|
|
166
|
+
|
|
167
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
168
|
+
const exportName = match[1];
|
|
169
|
+
const matchEnd = match.index + match[0].length;
|
|
170
|
+
|
|
171
|
+
// Find the matching closing paren (string/comment aware)
|
|
172
|
+
const afterClose = findMatchingParen(code, matchEnd);
|
|
173
|
+
const closeParenPos = afterClose - 1;
|
|
174
|
+
const argCount = countArgs(code, matchEnd, closeParenPos);
|
|
175
|
+
|
|
176
|
+
// Find statement end (after ; or whitespace)
|
|
177
|
+
let statementEnd = afterClose;
|
|
178
|
+
while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
|
|
179
|
+
statementEnd++;
|
|
180
|
+
}
|
|
181
|
+
if (code[statementEnd] === ";") {
|
|
182
|
+
statementEnd++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const handlerId = isBuild
|
|
186
|
+
? hashPrerenderHandlerId(filePath, exportName)
|
|
187
|
+
: `${filePath}#${exportName}`;
|
|
188
|
+
|
|
189
|
+
// Inject $$id as the last parameter.
|
|
190
|
+
// createPrerenderHandler(handler) -> createPrerenderHandler(handler, undefined, "id")
|
|
191
|
+
// createPrerenderHandler(handler, opts) -> createPrerenderHandler(handler, opts, "id")
|
|
192
|
+
// createPrerenderHandler(getP, handler) -> createPrerenderHandler(getP, handler, undefined, "id")
|
|
193
|
+
// createPrerenderHandler(getP, handler, opts) -> createPrerenderHandler(getP, handler, opts, "id")
|
|
194
|
+
//
|
|
195
|
+
// The runtime implementation accepts __injectedId as:
|
|
196
|
+
// Overload 1 (1 fn): 3rd param (after handler, options)
|
|
197
|
+
// Overload 2 (2 fn): 4th param (after getParams, handler, options)
|
|
198
|
+
//
|
|
199
|
+
// We cannot statically distinguish between (handler, options) and (getParams, handler)
|
|
200
|
+
// when there are 2 args. However the runtime resolves this by checking typeof of the
|
|
201
|
+
// second arg. The __injectedId is always a string, so it can appear in either the
|
|
202
|
+
// options or id slot — the runtime handles both via typeof checks.
|
|
203
|
+
let paramInjection: string;
|
|
204
|
+
if (argCount === 0) {
|
|
205
|
+
paramInjection = `undefined, "${handlerId}"`;
|
|
206
|
+
} else if (argCount === 1) {
|
|
207
|
+
// 1 arg (handler only): need to pad for options slot
|
|
208
|
+
paramInjection = `, undefined, "${handlerId}"`;
|
|
209
|
+
} else {
|
|
210
|
+
// 2+ args: just append id
|
|
211
|
+
paramInjection = `, "${handlerId}"`;
|
|
212
|
+
}
|
|
213
|
+
s.appendLeft(closeParenPos, paramInjection);
|
|
214
|
+
|
|
215
|
+
// Set $$id property for external access
|
|
216
|
+
const propInjection = `\n${exportName}.$$id = "${handlerId}";`;
|
|
217
|
+
s.appendRight(statementEnd, propInjection);
|
|
218
|
+
hasChanges = true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!hasChanges) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
code: s.toString(),
|
|
227
|
+
map: s.generateMap({ source: sourceId, includeContent: true, hires: "boundary" }),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Replace the entire file with lightweight stubs when ALL non-type exports are
|
|
233
|
+
* createPrerenderHandler calls. This is the most aggressive eviction — all imports,
|
|
234
|
+
* module-level data, and handler bodies are removed from non-RSC bundles.
|
|
235
|
+
*
|
|
236
|
+
* Returns null for files with mixed exports (non-handler value exports),
|
|
237
|
+
* falling back to per-expression replacement.
|
|
238
|
+
*/
|
|
239
|
+
function generateWholeFileHandlerStubs(
|
|
240
|
+
code: string,
|
|
241
|
+
filePath: string,
|
|
242
|
+
isBuild: boolean,
|
|
243
|
+
): { code: string; map: null } | null {
|
|
244
|
+
const handlerPattern =
|
|
245
|
+
/export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
246
|
+
const handlers: string[] = [];
|
|
247
|
+
let match: RegExpExecArray | null;
|
|
248
|
+
|
|
249
|
+
while ((match = handlerPattern.exec(code)) !== null) {
|
|
250
|
+
handlers.push(match[1]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (handlers.length === 0) return null;
|
|
254
|
+
|
|
255
|
+
// Bail out if the file has re-exports or destructured exports that
|
|
256
|
+
// the declaration pattern below wouldn't catch. Replacing the whole
|
|
257
|
+
// file would silently drop these exports.
|
|
258
|
+
if (/export\s*\{/.test(code) || /export\s*\*/.test(code)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check that every non-type export declaration is a createPrerenderHandler call.
|
|
263
|
+
const allExports =
|
|
264
|
+
/export\s+(const|let|var|function|class|default)\s+(\w+)/g;
|
|
265
|
+
let exportMatch: RegExpExecArray | null;
|
|
266
|
+
|
|
267
|
+
while ((exportMatch = allExports.exec(code)) !== null) {
|
|
268
|
+
const name = exportMatch[2];
|
|
269
|
+
if (!handlers.includes(name)) {
|
|
270
|
+
// Mixed exports — can't replace the whole file
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const stubs = handlers.map((name) => {
|
|
276
|
+
const handlerId = isBuild
|
|
277
|
+
? hashPrerenderHandlerId(filePath, name)
|
|
278
|
+
: `${filePath}#${name}`;
|
|
279
|
+
return `export const ${name} = { __brand: "prerenderHandler", $$id: "${handlerId}" };`;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return { code: stubs.join("\n") + "\n", map: null };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Replace createPrerenderHandler(...) call expressions with lightweight stub objects
|
|
287
|
+
* in non-RSC environments. Other exports, imports, and module-level code remain
|
|
288
|
+
* untouched — only the call expression is replaced.
|
|
289
|
+
*
|
|
290
|
+
* This prevents handler rendering code and build-only dependencies from shipping
|
|
291
|
+
* to client/SSR bundles where handlers never execute.
|
|
292
|
+
*/
|
|
293
|
+
function generatePrerenderHandlerStubs(
|
|
294
|
+
code: string,
|
|
295
|
+
filePath: string,
|
|
296
|
+
sourceId?: string,
|
|
297
|
+
isBuild: boolean = false,
|
|
298
|
+
): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
|
|
299
|
+
// Match: export const X = createPrerenderHandler<...>(
|
|
300
|
+
const pattern =
|
|
301
|
+
/export\s+const\s+(\w+)\s*=\s*(createPrerenderHandler\s*(?:<[^>]*>)?\s*\()/g;
|
|
302
|
+
|
|
303
|
+
const s = new MagicString(code);
|
|
304
|
+
let hasChanges = false;
|
|
305
|
+
let match: RegExpExecArray | null;
|
|
306
|
+
|
|
307
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
308
|
+
const exportName = match[1];
|
|
309
|
+
// callStart points to 'c' in 'createPrerenderHandler'
|
|
310
|
+
const callStart = match.index + match[0].length - match[2].length;
|
|
311
|
+
|
|
312
|
+
// Find the matching closing paren (string/comment aware)
|
|
313
|
+
const openParenPos = match.index + match[0].length;
|
|
314
|
+
const afterCloseParen = findMatchingParen(code, openParenPos);
|
|
315
|
+
|
|
316
|
+
const handlerId = isBuild
|
|
317
|
+
? hashPrerenderHandlerId(filePath, exportName)
|
|
318
|
+
: `${filePath}#${exportName}`;
|
|
319
|
+
|
|
320
|
+
// Replace createPrerenderHandler<...>(...) with stub object
|
|
321
|
+
s.overwrite(
|
|
322
|
+
callStart,
|
|
323
|
+
afterCloseParen,
|
|
324
|
+
`{ __brand: "prerenderHandler", $$id: "${handlerId}" }`,
|
|
325
|
+
);
|
|
326
|
+
hasChanges = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!hasChanges) return null;
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
code: s.toString(),
|
|
333
|
+
map: s.generateMap({ source: sourceId, includeContent: true, hires: "boundary" }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Plugin API type for expose-prerender-handler-id.
|
|
339
|
+
* Accessed by other plugins via config.plugins.find(p => p.name === "...").api
|
|
340
|
+
*/
|
|
341
|
+
export interface ExposePrerenderHandlerIdApi {
|
|
342
|
+
/** Tracks absolute module IDs that contain prerender handler exports.
|
|
343
|
+
* key: absolute module ID (filesystem path)
|
|
344
|
+
* value: array of export names (e.g., ["ArticlesIndex", "ArticleDetail"]) */
|
|
345
|
+
prerenderHandlerModules: Map<string, string[]>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Vite plugin that exposes $$id on createPrerenderHandler calls.
|
|
350
|
+
*
|
|
351
|
+
* When users create prerender handlers with createPrerenderHandler(), this plugin:
|
|
352
|
+
* - RSC environment: Injects $$id into the call and sets a $$id property on the export
|
|
353
|
+
* - Non-RSC environments (client/SSR): Replaces createPrerenderHandler(...) call
|
|
354
|
+
* expressions with lightweight { __brand, $$id } stubs, keeping other exports intact
|
|
355
|
+
*
|
|
356
|
+
* Requirements:
|
|
357
|
+
* - Must use direct import: import { createPrerenderHandler } from "@rangojs/router"
|
|
358
|
+
* - Must use named export: export const MyPage = createPrerenderHandler(...)
|
|
359
|
+
*
|
|
360
|
+
* Other plugins can read handler module data via the plugin API:
|
|
361
|
+
* config.plugins.find(p => p.name === "@rangojs/router:expose-prerender-handler-id")?.api
|
|
362
|
+
*/
|
|
363
|
+
export function exposePrerenderHandlerId(): Plugin {
|
|
364
|
+
let config: ResolvedConfig;
|
|
365
|
+
let isBuild = false;
|
|
366
|
+
const prerenderHandlerModules: Map<string, string[]> = new Map();
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
name: "@rangojs/router:expose-prerender-handler-id",
|
|
370
|
+
enforce: "post",
|
|
371
|
+
|
|
372
|
+
api: {
|
|
373
|
+
prerenderHandlerModules,
|
|
374
|
+
} satisfies ExposePrerenderHandlerIdApi,
|
|
375
|
+
|
|
376
|
+
configResolved(resolvedConfig) {
|
|
377
|
+
config = resolvedConfig;
|
|
378
|
+
isBuild = config.command === "build";
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
transform(code, id) {
|
|
382
|
+
// Skip node_modules
|
|
383
|
+
if (id.includes("/node_modules/")) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Quick bail-out
|
|
388
|
+
if (!code.includes("createPrerenderHandler")) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Must have direct import from @rangojs/router
|
|
393
|
+
if (!hasCreatePrerenderHandlerImport(code)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Get relative path for the ID
|
|
398
|
+
const relativePath = normalizePath(path.relative(config.root, id));
|
|
399
|
+
|
|
400
|
+
const isRscEnv = this.environment?.name === "rsc";
|
|
401
|
+
|
|
402
|
+
if (!isRscEnv) {
|
|
403
|
+
// Non-RSC: try whole-file replacement first (all exports are handlers),
|
|
404
|
+
// fall back to per-expression replacement for mixed-export files
|
|
405
|
+
return (
|
|
406
|
+
generateWholeFileHandlerStubs(code, relativePath, isBuild) ??
|
|
407
|
+
generatePrerenderHandlerStubs(code, relativePath, id, isBuild)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// RSC build: record this module and its handler export names for chunk isolation
|
|
412
|
+
if (isBuild) {
|
|
413
|
+
const handlerPattern =
|
|
414
|
+
/export\s+const\s+(\w+)\s*=\s*createPrerenderHandler\s*(?:<[^>]*>)?\s*\(/g;
|
|
415
|
+
const exportNames: string[] = [];
|
|
416
|
+
let m: RegExpExecArray | null;
|
|
417
|
+
while ((m = handlerPattern.exec(code)) !== null) {
|
|
418
|
+
exportNames.push(m[1]);
|
|
419
|
+
}
|
|
420
|
+
if (exportNames.length > 0) {
|
|
421
|
+
prerenderHandlerModules.set(id, exportNames);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// RSC: inject $$id into calls (existing behavior)
|
|
426
|
+
return transformPrerenderHandlerExports(code, relativePath, id, isBuild);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|