@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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/cloudflare.d.ts +3 -3
  3. package/dist/adapters/cloudflare.d.ts.map +1 -1
  4. package/dist/adapters/cloudflare.js +11 -4
  5. package/dist/adapters/cloudflare.js.map +1 -1
  6. package/dist/boundaries.d.ts +37 -1
  7. package/dist/boundaries.d.ts.map +1 -1
  8. package/dist/boundaries.js +125 -4
  9. package/dist/boundaries.js.map +1 -1
  10. package/dist/build.d.ts +6 -0
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/build.js +269 -24
  13. package/dist/build.js.map +1 -1
  14. package/dist/bundle-pipeline.d.ts +1 -0
  15. package/dist/bundle-pipeline.d.ts.map +1 -1
  16. package/dist/bundle-pipeline.js +4 -1
  17. package/dist/bundle-pipeline.js.map +1 -1
  18. package/dist/client.d.ts +1 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +67 -7
  21. package/dist/client.js.map +1 -1
  22. package/dist/config.d.ts +7 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +17 -0
  25. package/dist/config.js.map +1 -1
  26. package/dist/index.d.ts +5 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/module-runner.js +9 -1
  31. package/dist/module-runner.js.map +1 -1
  32. package/dist/render.d.ts.map +1 -1
  33. package/dist/render.js +23 -23
  34. package/dist/render.js.map +1 -1
  35. package/dist/request-header-tracking.d.ts +19 -0
  36. package/dist/request-header-tracking.d.ts.map +1 -1
  37. package/dist/request-header-tracking.js +48 -0
  38. package/dist/request-header-tracking.js.map +1 -1
  39. package/dist/tailwind-source.d.ts.map +1 -1
  40. package/dist/tailwind-source.js +147 -1
  41. package/dist/tailwind-source.js.map +1 -1
  42. package/dist/types.d.ts +14 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/vite.d.ts +1 -0
  46. package/dist/vite.d.ts.map +1 -1
  47. package/dist/vite.js +4 -0
  48. package/dist/vite.js.map +1 -1
  49. package/package.json +11 -11
  50. package/src/adapters/cloudflare.ts +14 -5
  51. package/src/boundaries.ts +224 -4
  52. package/src/build.ts +383 -24
  53. package/src/bundle-pipeline.ts +9 -4
  54. package/src/client.ts +104 -10
  55. package/src/config.ts +32 -0
  56. package/src/index.ts +10 -0
  57. package/src/module-runner.ts +9 -1
  58. package/src/render.ts +30 -16
  59. package/src/request-header-tracking.ts +61 -0
  60. package/src/tailwind-source.ts +164 -1
  61. package/src/types.ts +15 -2
  62. 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 { resolveAppRouterProjectOptions, type AppRouterProjectOptions } from "./config.js";
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
- return createBoundaryReport({
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(input.projectRoot, entry, route.components);
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