@reckona/mreact-router 0.0.207 → 0.0.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/cloudflare.d.ts +3 -3
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +11 -4
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/boundaries.d.ts +37 -1
- package/dist/boundaries.d.ts.map +1 -1
- package/dist/boundaries.js +125 -4
- package/dist/boundaries.js.map +1 -1
- package/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +269 -24
- package/dist/build.js.map +1 -1
- package/dist/bundle-pipeline.d.ts +1 -0
- package/dist/bundle-pipeline.d.ts.map +1 -1
- package/dist/bundle-pipeline.js +4 -1
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +67 -7
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module-runner.js +9 -1
- package/dist/module-runner.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +23 -23
- package/dist/render.js.map +1 -1
- package/dist/request-header-tracking.d.ts +19 -0
- package/dist/request-header-tracking.d.ts.map +1 -1
- package/dist/request-header-tracking.js +48 -0
- package/dist/request-header-tracking.js.map +1 -1
- package/dist/tailwind-source.d.ts.map +1 -1
- package/dist/tailwind-source.js +147 -1
- package/dist/tailwind-source.js.map +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +4 -0
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/adapters/cloudflare.ts +14 -5
- package/src/boundaries.ts +224 -4
- package/src/build.ts +383 -24
- package/src/bundle-pipeline.ts +9 -4
- package/src/client.ts +104 -10
- package/src/config.ts +32 -0
- package/src/index.ts +10 -0
- package/src/module-runner.ts +9 -1
- package/src/render.ts +30 -16
- package/src/request-header-tracking.ts +61 -0
- package/src/tailwind-source.ts +164 -1
- package/src/types.ts +15 -2
- package/src/vite.ts +5 -0
package/src/boundaries.ts
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
createClientRouteInferenceCache,
|
|
12
12
|
inferClientRouteModule,
|
|
13
13
|
} from "./client-route-inference.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
resolveAppRouterProjectOptions,
|
|
16
|
+
type AppRouterExecutionContracts,
|
|
17
|
+
type AppRouterProjectOptions,
|
|
18
|
+
} from "./config.js";
|
|
15
19
|
import { stripRouteClientSource } from "./route-source.js";
|
|
16
20
|
import { scanAppRoutes } from "./routes.js";
|
|
17
21
|
|
|
@@ -20,18 +24,58 @@ import { scanAppRoutes } from "./routes.js";
|
|
|
20
24
|
*/
|
|
21
25
|
export interface BoundaryReportComponent {
|
|
22
26
|
classification: ClientRouteComponentClassification;
|
|
27
|
+
decision: BoundaryReportDecision;
|
|
23
28
|
exportName: string;
|
|
24
29
|
file: string;
|
|
25
30
|
origin: ClientRouteComponentOrigin;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
export type BoundaryExecutionMode = "client" | "server" | "shared" | "unknown";
|
|
34
|
+
|
|
35
|
+
export interface BoundaryReportDecision {
|
|
36
|
+
executionMode: BoundaryExecutionMode;
|
|
37
|
+
reasonChain: readonly string[];
|
|
38
|
+
sourceRange?: BoundarySourceRange | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BoundarySourcePosition {
|
|
42
|
+
column: number;
|
|
43
|
+
line: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BoundarySourceRange {
|
|
47
|
+
end: BoundarySourcePosition;
|
|
48
|
+
start: BoundarySourcePosition;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BoundaryReportByteCost {
|
|
52
|
+
gzipEstimateBytes: number;
|
|
53
|
+
observedTransferBytes?: number | undefined;
|
|
54
|
+
rawBytes: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BoundaryReportCost {
|
|
58
|
+
baseline?:
|
|
59
|
+
| {
|
|
60
|
+
initialGzipDeltaBytes?: number | undefined;
|
|
61
|
+
navigationGzipDeltaBytes?: number | undefined;
|
|
62
|
+
}
|
|
63
|
+
| undefined;
|
|
64
|
+
initial?: BoundaryReportByteCost | undefined;
|
|
65
|
+
navigation?: BoundaryReportByteCost | undefined;
|
|
66
|
+
reason?: string | undefined;
|
|
67
|
+
status: "available" | "unavailable";
|
|
68
|
+
}
|
|
69
|
+
|
|
28
70
|
/**
|
|
29
71
|
* Describes the rendered component boundary graph for one page route.
|
|
30
72
|
*/
|
|
31
73
|
export interface BoundaryReportRoute {
|
|
32
74
|
classification: "client-route" | "server-render";
|
|
33
75
|
components: readonly BoundaryReportComponent[];
|
|
76
|
+
cost: BoundaryReportCost;
|
|
34
77
|
entry: string;
|
|
78
|
+
executionModes: readonly BoundaryExecutionMode[];
|
|
35
79
|
path: string;
|
|
36
80
|
}
|
|
37
81
|
|
|
@@ -60,9 +104,11 @@ export interface BoundaryReport {
|
|
|
60
104
|
|
|
61
105
|
export interface CreateBoundaryReportRouteInput {
|
|
62
106
|
components: readonly ClientRouteComponent[];
|
|
107
|
+
cost?: BoundaryReportCost | undefined;
|
|
63
108
|
diagnostics: readonly ClientRouteInferenceDiagnostic[];
|
|
64
109
|
entry: string;
|
|
65
110
|
path: string;
|
|
111
|
+
source?: string | undefined;
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
export interface CreateBoundaryReportInput {
|
|
@@ -106,21 +152,29 @@ export async function analyzeAppBoundaries(
|
|
|
106
152
|
diagnostics: inference.diagnostics,
|
|
107
153
|
entry: route.file,
|
|
108
154
|
path: route.path,
|
|
155
|
+
source,
|
|
109
156
|
};
|
|
110
157
|
}),
|
|
111
158
|
);
|
|
112
159
|
|
|
113
|
-
|
|
160
|
+
const report = createBoundaryReport({
|
|
114
161
|
projectRoot: project.projectRoot,
|
|
115
162
|
routes: analyzedRoutes,
|
|
116
163
|
});
|
|
164
|
+
validateBoundaryExecutionContracts(report, project.executionContracts);
|
|
165
|
+
return report;
|
|
117
166
|
}
|
|
118
167
|
|
|
119
168
|
export function createBoundaryReport(input: CreateBoundaryReportInput): BoundaryReport {
|
|
120
169
|
const routes = input.routes
|
|
121
170
|
.map((route): BoundaryReportRoute => {
|
|
122
171
|
const entry = projectRelativePath(input.projectRoot, route.entry);
|
|
123
|
-
const components = normalizeComponents(
|
|
172
|
+
const components = normalizeComponents(
|
|
173
|
+
input.projectRoot,
|
|
174
|
+
entry,
|
|
175
|
+
route.components,
|
|
176
|
+
route.source,
|
|
177
|
+
);
|
|
124
178
|
|
|
125
179
|
return {
|
|
126
180
|
classification: components.some(
|
|
@@ -132,7 +186,12 @@ export function createBoundaryReport(input: CreateBoundaryReportInput): Boundary
|
|
|
132
186
|
? "client-route"
|
|
133
187
|
: "server-render",
|
|
134
188
|
components,
|
|
189
|
+
cost: route.cost ?? {
|
|
190
|
+
reason: "No production artifact supplied.",
|
|
191
|
+
status: "unavailable",
|
|
192
|
+
},
|
|
135
193
|
entry,
|
|
194
|
+
executionModes: executionModesForComponents(components),
|
|
136
195
|
path: route.path,
|
|
137
196
|
};
|
|
138
197
|
})
|
|
@@ -178,6 +237,8 @@ export function formatBoundaryReport(report: BoundaryReport): string {
|
|
|
178
237
|
);
|
|
179
238
|
}
|
|
180
239
|
|
|
240
|
+
lines.push(` modes: ${route.executionModes.join(", ")}`);
|
|
241
|
+
|
|
181
242
|
lines.push("");
|
|
182
243
|
}
|
|
183
244
|
|
|
@@ -213,12 +274,21 @@ function normalizeComponents(
|
|
|
213
274
|
projectRoot: string,
|
|
214
275
|
entry: string,
|
|
215
276
|
components: readonly ClientRouteComponent[],
|
|
277
|
+
source: string | undefined,
|
|
216
278
|
): BoundaryReportComponent[] {
|
|
217
279
|
const unique = new Map<string, BoundaryReportComponent>();
|
|
218
280
|
|
|
219
281
|
for (const component of components) {
|
|
220
|
-
const normalized = {
|
|
282
|
+
const normalized: BoundaryReportComponent = {
|
|
221
283
|
...component,
|
|
284
|
+
decision: {
|
|
285
|
+
executionMode: executionModeForClassification(component.classification),
|
|
286
|
+
reasonChain: reasonChainForComponent(component, projectRoot, entry),
|
|
287
|
+
...(source === undefined ||
|
|
288
|
+
component.file !== resolveProjectRelativePath(projectRoot, entry)
|
|
289
|
+
? {}
|
|
290
|
+
: sourceRangeField(source, component.exportName)),
|
|
291
|
+
},
|
|
222
292
|
file: projectRelativePath(projectRoot, component.file),
|
|
223
293
|
};
|
|
224
294
|
unique.set(
|
|
@@ -243,6 +313,156 @@ function normalizeComponents(
|
|
|
243
313
|
});
|
|
244
314
|
}
|
|
245
315
|
|
|
316
|
+
/** Validates opt-in execution constraints against the normalized boundary report. */
|
|
317
|
+
export function validateBoundaryExecutionContracts(
|
|
318
|
+
report: BoundaryReport,
|
|
319
|
+
contracts: AppRouterExecutionContracts | undefined,
|
|
320
|
+
): void {
|
|
321
|
+
if (contracts === undefined) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const violations: string[] = [];
|
|
326
|
+
const serverOnlyRoutes = contracts.serverOnlyRoutes ?? [];
|
|
327
|
+
const noCompatComponents = contracts.noCompatComponents ?? [];
|
|
328
|
+
|
|
329
|
+
for (const route of report.routes) {
|
|
330
|
+
if (serverOnlyRoutes.some((pattern) => globMatches(route.path, pattern))) {
|
|
331
|
+
for (const component of route.components) {
|
|
332
|
+
if (
|
|
333
|
+
component.decision.executionMode === "client" ||
|
|
334
|
+
component.decision.executionMode === "shared" ||
|
|
335
|
+
component.decision.executionMode === "unknown"
|
|
336
|
+
) {
|
|
337
|
+
violations.push(
|
|
338
|
+
`server-only route ${JSON.stringify(route.path)} includes ${component.decision.executionMode} execution at ${component.file}#${component.exportName}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const component of route.components) {
|
|
345
|
+
const componentId = `${component.file}#${component.exportName}`;
|
|
346
|
+
if (
|
|
347
|
+
!noCompatComponents.some(
|
|
348
|
+
(pattern) => globMatches(component.file, pattern) || globMatches(componentId, pattern),
|
|
349
|
+
)
|
|
350
|
+
) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (component.origin === "compat-filename" || isCompatComponentFile(component.file)) {
|
|
355
|
+
violations.push(
|
|
356
|
+
`no-compat component ${componentId} uses compat fallback from its filename`,
|
|
357
|
+
);
|
|
358
|
+
} else if (component.decision.executionMode === "unknown") {
|
|
359
|
+
violations.push(`no-compat component ${componentId} has an unknown compiler decision`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (violations.length > 0) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`mreactRouter execution contract violation(s):\n${violations.map((violation) => `- ${violation}`).join("\n")}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isCompatComponentFile(file: string): boolean {
|
|
372
|
+
return /\.compat(?:\.mreact)?\.[cm]?[jt]sx?$/.test(file.replaceAll("\\", "/"));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function executionModesForComponents(
|
|
376
|
+
components: readonly BoundaryReportComponent[],
|
|
377
|
+
): BoundaryExecutionMode[] {
|
|
378
|
+
const order: readonly BoundaryExecutionMode[] = ["client", "server", "shared", "unknown"];
|
|
379
|
+
const modes = new Set(components.map((component) => component.decision.executionMode));
|
|
380
|
+
return order.filter((mode) => modes.has(mode));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function executionModeForClassification(
|
|
384
|
+
classification: ClientRouteComponentClassification,
|
|
385
|
+
): BoundaryExecutionMode {
|
|
386
|
+
if (classification === "client-boundary" || classification === "client-route") {
|
|
387
|
+
return "client";
|
|
388
|
+
}
|
|
389
|
+
if (classification === "shared") {
|
|
390
|
+
return "shared";
|
|
391
|
+
}
|
|
392
|
+
if (classification === "unknown") {
|
|
393
|
+
return "unknown";
|
|
394
|
+
}
|
|
395
|
+
return "server";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function reasonChainForComponent(
|
|
399
|
+
component: ClientRouteComponent,
|
|
400
|
+
projectRoot: string,
|
|
401
|
+
entry: string,
|
|
402
|
+
): string[] {
|
|
403
|
+
const reasons = [`classification:${component.classification}`, `origin:${component.origin}`];
|
|
404
|
+
|
|
405
|
+
if (component.origin === "inferred-client-runtime") {
|
|
406
|
+
reasons.push("client-runtime-inference");
|
|
407
|
+
} else if (component.origin === "compat-filename") {
|
|
408
|
+
reasons.push("compat-fallback");
|
|
409
|
+
} else if (component.origin === "unresolved-reference") {
|
|
410
|
+
reasons.push("unresolved-reference");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (projectRelativePath(projectRoot, component.file) !== entry) {
|
|
414
|
+
reasons.push(`reachable-from:${entry}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return reasons;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function sourceRangeField(
|
|
421
|
+
source: string,
|
|
422
|
+
exportName: string,
|
|
423
|
+
): { sourceRange?: BoundarySourceRange | undefined } {
|
|
424
|
+
const escapedName = escapeRegExp(exportName);
|
|
425
|
+
const pattern =
|
|
426
|
+
exportName === "default"
|
|
427
|
+
? /\bexport\s+default\b/u
|
|
428
|
+
: new RegExp(
|
|
429
|
+
`\\bexport\\s+(?:(?:async)\\s+)?(?:function|class|const|let|var)\\s+${escapedName}\\b`,
|
|
430
|
+
"u",
|
|
431
|
+
);
|
|
432
|
+
const match = pattern.exec(source);
|
|
433
|
+
|
|
434
|
+
return match === null || match.index === undefined
|
|
435
|
+
? {}
|
|
436
|
+
: {
|
|
437
|
+
sourceRange: {
|
|
438
|
+
end: sourcePosition(source, match.index + match[0].length),
|
|
439
|
+
start: sourcePosition(source, match.index),
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function sourcePosition(source: string, offset: number): BoundarySourcePosition {
|
|
445
|
+
const lines = source.slice(0, offset).split("\n");
|
|
446
|
+
return { column: (lines.at(-1)?.length ?? 0) + 1, line: lines.length };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function escapeRegExp(value: string): string {
|
|
450
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function globMatches(value: string, pattern: string): boolean {
|
|
454
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/gu, "\\$&");
|
|
455
|
+
const expression = escaped
|
|
456
|
+
.replaceAll("**", "\u0000")
|
|
457
|
+
.replaceAll("*", "[^/]*")
|
|
458
|
+
.replaceAll("\u0000", ".*");
|
|
459
|
+
return new RegExp(`^${expression}$`, "u").test(value);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function resolveProjectRelativePath(projectRoot: string, relativePath: string): string {
|
|
463
|
+
return `${projectRoot}/${relativePath}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
246
466
|
function summarizeBoundaryRoutes(routes: readonly BoundaryReportRoute[]): BoundaryReportSummary {
|
|
247
467
|
const components = routes.flatMap((route) => route.components);
|
|
248
468
|
|