@reckona/mreact-router 0.0.65 → 0.0.67
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/package.json +13 -12
- package/src/actions.ts +1130 -0
- package/src/adapters/aws-lambda.ts +993 -0
- package/src/adapters/cloudflare.ts +1286 -0
- package/src/adapters/devtools.ts +5 -0
- package/src/adapters/edge.ts +70 -0
- package/src/adapters/node.ts +126 -0
- package/src/adapters/static.ts +61 -0
- package/src/app-router-globals.ts +19 -0
- package/src/assets.ts +113 -0
- package/src/build.ts +2948 -0
- package/src/bundle-pipeline.ts +496 -0
- package/src/cache-config.ts +35 -0
- package/src/cache-stats.ts +54 -0
- package/src/cache.ts +418 -0
- package/src/cli-options.ts +296 -0
- package/src/cli.ts +94 -0
- package/src/client.ts +3398 -0
- package/src/config.ts +146 -0
- package/src/cookies.ts +113 -0
- package/src/csp.ts +103 -0
- package/src/csrf.ts +132 -0
- package/src/deferred.ts +52 -0
- package/src/dev-server.ts +262 -0
- package/src/file-conventions.ts +88 -0
- package/src/http.ts +128 -0
- package/src/i18n.ts +98 -0
- package/src/import-policy.ts +261 -0
- package/src/index.ts +221 -0
- package/src/link.ts +47 -0
- package/src/logger.ts +149 -0
- package/src/module-runner.ts +554 -0
- package/src/multipart.ts +577 -0
- package/src/native-escape.ts +53 -0
- package/src/native-route-matcher.ts +173 -0
- package/src/navigation-state.ts +98 -0
- package/src/navigation.ts +215 -0
- package/src/prerender-store.ts +233 -0
- package/src/render.ts +5187 -0
- package/src/route-path.ts +5 -0
- package/src/route-shells.ts +47 -0
- package/src/route-source.ts +224 -0
- package/src/route-styles.ts +91 -0
- package/src/routes.ts +362 -0
- package/src/runtime-cache.ts +8 -0
- package/src/runtime-state.ts +37 -0
- package/src/security-headers.ts +134 -0
- package/src/serve.ts +1265 -0
- package/src/session.ts +238 -0
- package/src/source-jsx.ts +30 -0
- package/src/source-modules.ts +69 -0
- package/src/stream-list.ts +45 -0
- package/src/trace.ts +114 -0
- package/src/types.ts +155 -0
- package/src/upgrade.ts +8 -0
- package/src/vite-config.ts +63 -0
- package/src/vite-plugin-cache-key.ts +53 -0
- package/src/vite.ts +690 -0
- package/src/workspace-packages.ts +67 -0
package/src/cache.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
export interface RouteCachePolicy {
|
|
2
|
+
cacheControl: string;
|
|
3
|
+
revalidateSeconds: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CacheControlOptions {
|
|
7
|
+
maxAge?: number | undefined;
|
|
8
|
+
sMaxAge?: number | undefined;
|
|
9
|
+
staleWhileRevalidate?: boolean | number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AppRouterCacheEntry {
|
|
13
|
+
body: string;
|
|
14
|
+
cacheControl: string;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
path: string;
|
|
17
|
+
status: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AppRouterCache {
|
|
21
|
+
deleteByPath(path: string): void | Promise<void>;
|
|
22
|
+
get(
|
|
23
|
+
key: string,
|
|
24
|
+
now?: number,
|
|
25
|
+
): AppRouterCacheEntry | undefined | Promise<AppRouterCacheEntry | undefined>;
|
|
26
|
+
set(key: string, entry: AppRouterCacheEntry): void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MemoryRouteCacheOptions {
|
|
30
|
+
maxEntries?: number;
|
|
31
|
+
sweepIntervalMs?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AppRouterCacheState {
|
|
35
|
+
activeContexts: RouteCacheContext[];
|
|
36
|
+
invalidatedPaths: Set<string>;
|
|
37
|
+
memoryCache: AppRouterCache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface RouteCacheContext {
|
|
41
|
+
cache: AppRouterCache;
|
|
42
|
+
cachePolicy?: RouteCachePolicy | undefined;
|
|
43
|
+
revalidatedPaths: Set<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cacheState = ((
|
|
47
|
+
globalThis as { __mreactAppRouterCache?: AppRouterCacheState }
|
|
48
|
+
).__mreactAppRouterCache ??= {
|
|
49
|
+
activeContexts: [],
|
|
50
|
+
invalidatedPaths: new Set(),
|
|
51
|
+
memoryCache: createMemoryRouteCache(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export function createMemoryRouteCache(options: MemoryRouteCacheOptions = {}): AppRouterCache {
|
|
55
|
+
const maxEntries = positiveIntegerOrDefault(options.maxEntries, 10_000);
|
|
56
|
+
const sweepIntervalMs = nonNegativeIntegerOrDefault(options.sweepIntervalMs, 60_000);
|
|
57
|
+
const cachedRoutes = new Map<string, AppRouterCacheEntry>();
|
|
58
|
+
const keysByPath = new Map<string, Set<string>>();
|
|
59
|
+
let nextSweepAt = 0;
|
|
60
|
+
|
|
61
|
+
function unindexKey(key: string, path: string): void {
|
|
62
|
+
const keys = keysByPath.get(path);
|
|
63
|
+
keys?.delete(key);
|
|
64
|
+
|
|
65
|
+
if (keys?.size === 0) {
|
|
66
|
+
keysByPath.delete(path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function deleteEntry(key: string): void {
|
|
71
|
+
const entry = cachedRoutes.get(key);
|
|
72
|
+
cachedRoutes.delete(key);
|
|
73
|
+
|
|
74
|
+
if (entry !== undefined) {
|
|
75
|
+
unindexKey(key, entry.path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function indexKey(key: string, path: string): void {
|
|
80
|
+
const keys = keysByPath.get(path);
|
|
81
|
+
|
|
82
|
+
if (keys === undefined) {
|
|
83
|
+
keysByPath.set(path, new Set([key]));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
keys.add(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sweepExpired(now: number): void {
|
|
91
|
+
for (const [key, entry] of cachedRoutes) {
|
|
92
|
+
if (entry.expiresAt <= now) {
|
|
93
|
+
deleteEntry(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
nextSweepAt = now + sweepIntervalMs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function maybeSweepExpired(now: number): void {
|
|
101
|
+
if (sweepIntervalMs === 0 || now >= nextSweepAt) {
|
|
102
|
+
sweepExpired(now);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function evictOldestEntries(): void {
|
|
107
|
+
while (cachedRoutes.size > maxEntries) {
|
|
108
|
+
const oldestKey = cachedRoutes.keys().next().value;
|
|
109
|
+
|
|
110
|
+
if (oldestKey === undefined) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
deleteEntry(oldestKey);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
deleteByPath(path) {
|
|
120
|
+
const normalizedPath = normalizeRevalidationPath(path);
|
|
121
|
+
const keys = keysByPath.get(normalizedPath);
|
|
122
|
+
|
|
123
|
+
if (keys === undefined) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
cachedRoutes.delete(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
keysByPath.delete(normalizedPath);
|
|
132
|
+
},
|
|
133
|
+
get(key, now = Date.now()) {
|
|
134
|
+
const entry = cachedRoutes.get(key);
|
|
135
|
+
|
|
136
|
+
if (entry === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (entry.expiresAt <= now) {
|
|
141
|
+
deleteEntry(key);
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return entry;
|
|
146
|
+
},
|
|
147
|
+
set(key, entry) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
maybeSweepExpired(now);
|
|
150
|
+
const previous = cachedRoutes.get(key);
|
|
151
|
+
|
|
152
|
+
if (previous !== undefined) {
|
|
153
|
+
cachedRoutes.delete(key);
|
|
154
|
+
unindexKey(key, previous.path);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
cachedRoutes.set(key, entry);
|
|
158
|
+
indexKey(key, entry.path);
|
|
159
|
+
|
|
160
|
+
if (cachedRoutes.size > maxEntries) {
|
|
161
|
+
sweepExpired(now);
|
|
162
|
+
evictOldestEntries();
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function positiveIntegerOrDefault(value: number | undefined, fallback: number): number {
|
|
169
|
+
return value === undefined || !Number.isInteger(value) || value < 1 ? fallback : value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function nonNegativeIntegerOrDefault(value: number | undefined, fallback: number): number {
|
|
173
|
+
return value === undefined || !Number.isInteger(value) || value < 0 ? fallback : value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function routeCachePolicyFromSource(code: string): RouteCachePolicy | undefined {
|
|
177
|
+
const match = /^\s*export\s+const\s+revalidate\s*=\s*(?<seconds>\d+)\s*;?\s*$/m.exec(code);
|
|
178
|
+
const seconds = match?.groups?.seconds === undefined ? undefined : Number(match.groups.seconds);
|
|
179
|
+
|
|
180
|
+
if (seconds === undefined || !Number.isFinite(seconds)) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
cacheControl: seconds === 0 ? "no-store" : `s-maxage=${seconds}, stale-while-revalidate`,
|
|
186
|
+
revalidateSeconds: seconds,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function cacheControl(options: CacheControlOptions): void {
|
|
191
|
+
const activeContext = cacheState.activeContexts.at(-1);
|
|
192
|
+
|
|
193
|
+
if (activeContext === undefined) {
|
|
194
|
+
throw new Error("cacheControl() must be called during an app router request.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
activeContext.cachePolicy = routeCachePolicyFromOptions(options);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function routeCachePolicyFromOptions(options: CacheControlOptions): RouteCachePolicy {
|
|
201
|
+
const directives: string[] = [];
|
|
202
|
+
const maxAge = cacheControlSeconds(options.maxAge, "maxAge");
|
|
203
|
+
const sMaxAge = cacheControlSeconds(options.sMaxAge, "sMaxAge");
|
|
204
|
+
|
|
205
|
+
if (maxAge !== undefined) {
|
|
206
|
+
directives.push(`max-age=${maxAge}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (sMaxAge !== undefined) {
|
|
210
|
+
directives.push(`s-maxage=${sMaxAge}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.staleWhileRevalidate !== undefined) {
|
|
214
|
+
directives.push(staleWhileRevalidateDirective(options.staleWhileRevalidate));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (directives.length === 0) {
|
|
218
|
+
throw new Error("cacheControl() requires at least one cache directive.");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
cacheControl: directives.join(", "),
|
|
223
|
+
revalidateSeconds: sMaxAge ?? 0,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function cachedRouteResponse(options: {
|
|
228
|
+
cache?: AppRouterCache | undefined;
|
|
229
|
+
key: string;
|
|
230
|
+
now?: number;
|
|
231
|
+
}): Promise<Response | undefined> {
|
|
232
|
+
return Promise.resolve().then(async () => {
|
|
233
|
+
const cache = options.cache ?? cacheState.memoryCache;
|
|
234
|
+
|
|
235
|
+
await consumeInvalidations(cache);
|
|
236
|
+
const now = options.now ?? Date.now();
|
|
237
|
+
const cached = await cache.get(options.key, now);
|
|
238
|
+
|
|
239
|
+
if (cached === undefined || cached.expiresAt <= now) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return new Response(cached.body, {
|
|
244
|
+
headers: {
|
|
245
|
+
"cache-control": cached.cacheControl,
|
|
246
|
+
"content-type": "text/html; charset=utf-8",
|
|
247
|
+
"x-mreact-cache": "HIT",
|
|
248
|
+
},
|
|
249
|
+
status: cached.status,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function cacheRouteResponse(options: {
|
|
255
|
+
cache?: AppRouterCache | undefined;
|
|
256
|
+
key: string;
|
|
257
|
+
now?: number;
|
|
258
|
+
path: string;
|
|
259
|
+
policy: RouteCachePolicy | undefined;
|
|
260
|
+
response: Response;
|
|
261
|
+
}): Promise<Response> {
|
|
262
|
+
if (options.policy === undefined) {
|
|
263
|
+
return options.response;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (options.policy.revalidateSeconds === 0) {
|
|
267
|
+
options.response.headers.set("cache-control", options.policy.cacheControl);
|
|
268
|
+
return options.response;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const body = await options.response.text();
|
|
272
|
+
const cacheControl = options.policy.cacheControl;
|
|
273
|
+
const status = options.response.status;
|
|
274
|
+
await (options.cache ?? cacheState.memoryCache).set(options.key, {
|
|
275
|
+
body,
|
|
276
|
+
cacheControl,
|
|
277
|
+
expiresAt: (options.now ?? Date.now()) + options.policy.revalidateSeconds * 1000,
|
|
278
|
+
path: normalizeRevalidationPath(options.path),
|
|
279
|
+
status,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return new Response(body, {
|
|
283
|
+
headers: {
|
|
284
|
+
"cache-control": cacheControl,
|
|
285
|
+
"content-type": options.response.headers.get("content-type") ?? "text/html; charset=utf-8",
|
|
286
|
+
"x-mreact-cache": "MISS",
|
|
287
|
+
},
|
|
288
|
+
status,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function revalidatePath(path: string): void {
|
|
293
|
+
const normalizedPath = normalizeRevalidationPath(path);
|
|
294
|
+
const activeContext = cacheState.activeContexts.at(-1);
|
|
295
|
+
|
|
296
|
+
if (activeContext !== undefined) {
|
|
297
|
+
activeContext.revalidatedPaths.add(normalizedPath);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
cacheState.invalidatedPaths.add(normalizedPath);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function consumeInvalidations(
|
|
305
|
+
cache: AppRouterCache = cacheState.memoryCache,
|
|
306
|
+
): Promise<void> {
|
|
307
|
+
if (cacheState.invalidatedPaths.size === 0) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const path of cacheState.invalidatedPaths) {
|
|
312
|
+
await cache.deleteByPath(path);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
cacheState.invalidatedPaths.clear();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function withRouteCacheContext<T>(
|
|
319
|
+
cache: AppRouterCache | undefined,
|
|
320
|
+
fn: () => T | Promise<T>,
|
|
321
|
+
): Promise<{ cachePolicy: RouteCachePolicy | undefined; revalidatedPaths: string[]; value: T }> {
|
|
322
|
+
const context: RouteCacheContext = {
|
|
323
|
+
cache: cache ?? cacheState.memoryCache,
|
|
324
|
+
revalidatedPaths: new Set(),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
cacheState.activeContexts.push(context);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const value = await fn();
|
|
331
|
+
const cachePolicy = context.cachePolicy;
|
|
332
|
+
const revalidatedPaths = Array.from(context.revalidatedPaths);
|
|
333
|
+
|
|
334
|
+
for (const path of revalidatedPaths) {
|
|
335
|
+
await context.cache.deleteByPath(path);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { cachePolicy, revalidatedPaths, value };
|
|
339
|
+
} finally {
|
|
340
|
+
cacheState.activeContexts.pop();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function beginRouteCacheContext(cache: AppRouterCache | undefined): {
|
|
345
|
+
readonly cachePolicy: RouteCachePolicy | undefined;
|
|
346
|
+
dispose(): Promise<{ revalidatedPaths: string[] }>;
|
|
347
|
+
} {
|
|
348
|
+
const context: RouteCacheContext = {
|
|
349
|
+
cache: cache ?? cacheState.memoryCache,
|
|
350
|
+
revalidatedPaths: new Set(),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
cacheState.activeContexts.push(context);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
get cachePolicy() {
|
|
357
|
+
return context.cachePolicy;
|
|
358
|
+
},
|
|
359
|
+
async dispose() {
|
|
360
|
+
const revalidatedPaths = Array.from(context.revalidatedPaths);
|
|
361
|
+
|
|
362
|
+
for (const path of revalidatedPaths) {
|
|
363
|
+
await context.cache.deleteByPath(path);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const index = cacheState.activeContexts.lastIndexOf(context);
|
|
367
|
+
if (index !== -1) {
|
|
368
|
+
cacheState.activeContexts.splice(index, 1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { revalidatedPaths };
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Host is excluded from the cache key to prevent attacker-supplied Host
|
|
377
|
+
// headers from fragmenting / poisoning the cache (Issue 068). The Vary
|
|
378
|
+
// dimension is the request path + query; same-origin reverse proxies are
|
|
379
|
+
// expected to handle vhost separation at their layer.
|
|
380
|
+
export function routeCacheKey(appDir: string, routePath: string, url: URL): string {
|
|
381
|
+
return `${appDir}\0${normalizeRevalidationPath(routePath)}\0${url.pathname}${url.search}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function stripRevalidateExport(code: string): string {
|
|
385
|
+
return code.replace(/^\s*export\s+const\s+revalidate\s*=\s*\d+\s*;?\s*$/m, "");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeRevalidationPath(path: string): string {
|
|
389
|
+
const pathname = path.startsWith("/") ? path : `/${path}`;
|
|
390
|
+
const withoutTrailing = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
391
|
+
|
|
392
|
+
return withoutTrailing === "" ? "/" : withoutTrailing;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function cacheControlSeconds(value: number | undefined, name: string): number | undefined {
|
|
396
|
+
if (value === undefined) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
401
|
+
throw new Error(`cacheControl() ${name} must be a non-negative integer.`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function staleWhileRevalidateDirective(value: boolean | number): string {
|
|
408
|
+
if (value === true) {
|
|
409
|
+
return "stale-while-revalidate";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (value === false) {
|
|
413
|
+
throw new Error("cacheControl() staleWhileRevalidate must be true or a non-negative integer.");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const seconds = cacheControlSeconds(value, "staleWhileRevalidate");
|
|
417
|
+
return `stale-while-revalidate=${seconds}`;
|
|
418
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { AppRouterLogger, AppRouterLogEvent } from "./logger.js";
|
|
2
|
+
import type {
|
|
3
|
+
AppRouterBuildTarget,
|
|
4
|
+
AppRouterClientSourceMapMode,
|
|
5
|
+
} from "./config.js";
|
|
6
|
+
|
|
7
|
+
export type CliRequestLogMode = "requests";
|
|
8
|
+
export type CliBuildTarget = AppRouterBuildTarget | "all";
|
|
9
|
+
|
|
10
|
+
export interface ParsedCliArguments {
|
|
11
|
+
clientSourceMaps?: AppRouterClientSourceMapMode | undefined;
|
|
12
|
+
command: string;
|
|
13
|
+
from?: string | undefined;
|
|
14
|
+
help?: boolean | undefined;
|
|
15
|
+
log?: CliRequestLogMode | undefined;
|
|
16
|
+
out?: string | undefined;
|
|
17
|
+
routeArg?: string | undefined;
|
|
18
|
+
target?: CliBuildTarget | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCliArguments(argv: readonly string[]): ParsedCliArguments {
|
|
22
|
+
const first = argv[0];
|
|
23
|
+
const rootHelp = first === "--help" || first === "-h";
|
|
24
|
+
const command = rootHelp ? "help" : first === undefined || first.startsWith("-") ? "dev" : first;
|
|
25
|
+
const parsed: ParsedCliArguments = rootHelp ? { command, help: true } : { command };
|
|
26
|
+
const startIndex = rootHelp || command === first ? 1 : 0;
|
|
27
|
+
|
|
28
|
+
for (let index = startIndex; index < argv.length; index += 1) {
|
|
29
|
+
const value = argv[index];
|
|
30
|
+
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value === "--help" || value === "-h") {
|
|
36
|
+
parsed.help = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value === "--log") {
|
|
41
|
+
parsed.log = parseCliRequestLogMode(readOptionValue(argv, index, "log"));
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value === "--from") {
|
|
47
|
+
parsed.from = readOptionValue(argv, index, "from");
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (value.startsWith("--from=")) {
|
|
53
|
+
parsed.from = value.slice("--from=".length);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (value === "--out") {
|
|
58
|
+
parsed.out = readOptionValue(argv, index, "out");
|
|
59
|
+
index += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (value.startsWith("--out=")) {
|
|
64
|
+
parsed.out = value.slice("--out=".length);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (value.startsWith("--log=")) {
|
|
69
|
+
parsed.log = parseCliRequestLogMode(value.slice("--log=".length));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (value === "--target") {
|
|
74
|
+
parsed.target = parseCliBuildTarget(readOptionValue(argv, index, "target"));
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value.startsWith("--target=")) {
|
|
80
|
+
parsed.target = parseCliBuildTarget(value.slice("--target=".length));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (value === "--client-source-maps") {
|
|
85
|
+
parsed.clientSourceMaps = parseCliClientSourceMapMode(
|
|
86
|
+
readOptionValue(argv, index, "client-source-maps"),
|
|
87
|
+
);
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (value.startsWith("--client-source-maps=")) {
|
|
93
|
+
parsed.clientSourceMaps = parseCliClientSourceMapMode(
|
|
94
|
+
value.slice("--client-source-maps=".length),
|
|
95
|
+
);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (value.startsWith("-")) {
|
|
100
|
+
throw new Error(`Unknown option ${value}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (parsed.routeArg !== undefined) {
|
|
104
|
+
throw new Error(`Unexpected argument ${value}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parsed.routeArg = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatCliHelp(command?: string | undefined): string {
|
|
114
|
+
if (command === "build") {
|
|
115
|
+
return [
|
|
116
|
+
"Usage: mreact-router build [appDir] [options]",
|
|
117
|
+
"",
|
|
118
|
+
"Build an mreact app router project.",
|
|
119
|
+
"",
|
|
120
|
+
"Options:",
|
|
121
|
+
" --target=node|cloudflare|aws-lambda|all",
|
|
122
|
+
" Select build artifacts. aws-lambda writes .mreact/aws-lambda/mreact-handler.mjs and .mreact/server/import-policy.json.",
|
|
123
|
+
" --client-source-maps=none|hidden",
|
|
124
|
+
" Control production client source map output.",
|
|
125
|
+
" -h, --help",
|
|
126
|
+
" Show this help message.",
|
|
127
|
+
"",
|
|
128
|
+
"Examples:",
|
|
129
|
+
" mreact-router build --target=node",
|
|
130
|
+
" mreact-router build --target=cloudflare",
|
|
131
|
+
" mreact-router build --target=aws-lambda",
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (command === "package") {
|
|
136
|
+
return [
|
|
137
|
+
"Usage: mreact-router package aws-lambda [options]",
|
|
138
|
+
"",
|
|
139
|
+
"Package generated build output into a minimal AWS Lambda asset directory.",
|
|
140
|
+
"",
|
|
141
|
+
"Options:",
|
|
142
|
+
" --from <dir> Build output directory. Default: .mreact",
|
|
143
|
+
" --out <dir> Lambda asset output directory. Default: .lambda",
|
|
144
|
+
" -h, --help Show this help message.",
|
|
145
|
+
"",
|
|
146
|
+
"Example:",
|
|
147
|
+
" mreact-router package aws-lambda --from .mreact --out .lambda",
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (command === "start") {
|
|
152
|
+
return [
|
|
153
|
+
"Usage: mreact-router start [outDir] [options]",
|
|
154
|
+
"",
|
|
155
|
+
"Serve built mreact app router output with the Node adapter.",
|
|
156
|
+
"",
|
|
157
|
+
"Options:",
|
|
158
|
+
" --log=requests Print request summaries.",
|
|
159
|
+
" -h, --help Show this help message.",
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (command === "dev") {
|
|
164
|
+
return [
|
|
165
|
+
"Usage: mreact-router dev [appDir] [options]",
|
|
166
|
+
"",
|
|
167
|
+
"Start the mreact app router development server.",
|
|
168
|
+
"",
|
|
169
|
+
"Options:",
|
|
170
|
+
" --log=requests Print request summaries.",
|
|
171
|
+
" -h, --help Show this help message.",
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
"Usage: mreact-router <command> [options]",
|
|
177
|
+
"",
|
|
178
|
+
"Commands:",
|
|
179
|
+
" dev [appDir] Start the development server.",
|
|
180
|
+
" build [appDir] Build Node and Cloudflare artifacts by default.",
|
|
181
|
+
" build --target=aws-lambda Build Lambda artifacts including generated handler and import policy.",
|
|
182
|
+
" start [outDir] Serve built Node output.",
|
|
183
|
+
" package aws-lambda --from .mreact --out .lambda",
|
|
184
|
+
" Package a minimal AWS Lambda asset directory.",
|
|
185
|
+
" help [command] Show help.",
|
|
186
|
+
"",
|
|
187
|
+
"Options:",
|
|
188
|
+
" -h, --help Show this help message.",
|
|
189
|
+
" --log=requests Print request summaries for dev/start.",
|
|
190
|
+
"",
|
|
191
|
+
"Examples:",
|
|
192
|
+
" mreact-router build --target=cloudflare",
|
|
193
|
+
" mreact-router build --target=aws-lambda",
|
|
194
|
+
" mreact-router package aws-lambda --from .mreact --out .lambda",
|
|
195
|
+
" mreact-router build --help",
|
|
196
|
+
].join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function buildTargetsFromCliTarget(
|
|
200
|
+
target: CliBuildTarget | undefined,
|
|
201
|
+
): readonly AppRouterBuildTarget[] | undefined {
|
|
202
|
+
if (target === undefined) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return target === "all" ? ["node", "cloudflare", "aws-lambda"] : [target];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveCliRequestLogMode(
|
|
210
|
+
flagValue: CliRequestLogMode | undefined,
|
|
211
|
+
env: { MREACT_ROUTER_LOG?: string | undefined },
|
|
212
|
+
): CliRequestLogMode | undefined {
|
|
213
|
+
if (flagValue !== undefined) {
|
|
214
|
+
return flagValue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const envValue = env.MREACT_ROUTER_LOG;
|
|
218
|
+
if (envValue === undefined || envValue === "") {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return parseCliRequestLogMode(envValue);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createCliRequestLogger(): AppRouterLogger {
|
|
226
|
+
return {
|
|
227
|
+
error(event) {
|
|
228
|
+
if (event.type === "router:request:error") {
|
|
229
|
+
console.error(formatCliRequestLogEvent(event));
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
info(event) {
|
|
233
|
+
if (event.type === "router:request:end") {
|
|
234
|
+
console.log(formatCliRequestLogEvent(event));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseCliRequestLogMode(value: string): CliRequestLogMode {
|
|
241
|
+
if (value === "requests") {
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw new Error(`Unsupported log mode ${JSON.stringify(value)}. Expected "requests".`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseCliBuildTarget(value: string): CliBuildTarget {
|
|
249
|
+
if (value === "node" || value === "cloudflare" || value === "aws-lambda" || value === "all") {
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Unsupported build target ${JSON.stringify(value)}. Expected "node", "cloudflare", "aws-lambda", or "all".`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseCliClientSourceMapMode(value: string): AppRouterClientSourceMapMode {
|
|
259
|
+
if (value === "none" || value === "hidden" || value === "linked") {
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Unsupported client source map mode ${JSON.stringify(value)} for --client-source-maps. Expected "none", "hidden", or "linked".`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function readOptionValue(values: readonly string[], index: number, name: string): string {
|
|
269
|
+
const value = values[index + 1];
|
|
270
|
+
|
|
271
|
+
if (value === undefined || value.startsWith("-")) {
|
|
272
|
+
throw new Error(`Missing value for --${name}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function formatCliRequestLogEvent(event: AppRouterLogEvent): string {
|
|
279
|
+
if (event.type === "router:request:error") {
|
|
280
|
+
return `[mreact] ${event.method} ${event.path} error ${event.durationMs}ms ${event.runtime} ${event.error.name}: ${event.error.message}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (event.type === "router:request:end") {
|
|
284
|
+
return `[mreact] ${event.method} ${event.path} ${event.status} ${event.durationMs}ms ${event.runtime}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (event.type === "router:request:timing") {
|
|
288
|
+
return `[mreact] ${event.method} ${event.path} ${event.status} ${event.durationMs}ms ${event.runtime}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (event.type === "router:render:timing") {
|
|
292
|
+
return `[mreact] ${event.method} ${event.path} ${event.status} render timing`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return `[mreact] ${event.method} ${event.path} ${event.runtime}`;
|
|
296
|
+
}
|