@reckona/mreact-router 0.0.96 → 0.0.98

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 (100) hide show
  1. package/README.md +4 -0
  2. package/dist/actions.d.ts +7 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +26 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  7. package/dist/adapters/aws-lambda.js +10 -7
  8. package/dist/adapters/aws-lambda.js.map +1 -1
  9. package/dist/adapters/static.d.ts.map +1 -1
  10. package/dist/adapters/static.js +13 -0
  11. package/dist/adapters/static.js.map +1 -1
  12. package/dist/build.d.ts +38 -1
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +1093 -307
  15. package/dist/build.js.map +1 -1
  16. package/dist/bundle-pipeline.d.ts +7 -0
  17. package/dist/bundle-pipeline.d.ts.map +1 -1
  18. package/dist/bundle-pipeline.js +121 -69
  19. package/dist/bundle-pipeline.js.map +1 -1
  20. package/dist/cache.d.ts.map +1 -1
  21. package/dist/cache.js +10 -8
  22. package/dist/cache.js.map +1 -1
  23. package/dist/cli-options.d.ts +1 -0
  24. package/dist/cli-options.d.ts.map +1 -1
  25. package/dist/cli-options.js +16 -0
  26. package/dist/cli-options.js.map +1 -1
  27. package/dist/cli.js +1 -0
  28. package/dist/cli.js.map +1 -1
  29. package/dist/client.d.ts +6 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +120 -9
  32. package/dist/client.js.map +1 -1
  33. package/dist/csrf.d.ts.map +1 -1
  34. package/dist/csrf.js +7 -3
  35. package/dist/csrf.js.map +1 -1
  36. package/dist/http.d.ts.map +1 -1
  37. package/dist/http.js +18 -1
  38. package/dist/http.js.map +1 -1
  39. package/dist/import-policy.d.ts.map +1 -1
  40. package/dist/import-policy.js +7 -3
  41. package/dist/import-policy.js.map +1 -1
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/link.d.ts +5 -3
  46. package/dist/link.d.ts.map +1 -1
  47. package/dist/link.js +92 -3
  48. package/dist/link.js.map +1 -1
  49. package/dist/logger.d.ts +7 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js.map +1 -1
  52. package/dist/module-runner.d.ts +2 -0
  53. package/dist/module-runner.d.ts.map +1 -1
  54. package/dist/module-runner.js +1 -0
  55. package/dist/module-runner.js.map +1 -1
  56. package/dist/native-route-matcher.d.ts +2 -1
  57. package/dist/native-route-matcher.d.ts.map +1 -1
  58. package/dist/native-route-matcher.js +13 -2
  59. package/dist/native-route-matcher.js.map +1 -1
  60. package/dist/render.d.ts.map +1 -1
  61. package/dist/render.js +121 -6
  62. package/dist/render.js.map +1 -1
  63. package/dist/route-source.d.ts +11 -11
  64. package/dist/route-source.d.ts.map +1 -1
  65. package/dist/route-source.js +32 -25
  66. package/dist/route-source.js.map +1 -1
  67. package/dist/route-styles.d.ts +6 -0
  68. package/dist/route-styles.d.ts.map +1 -1
  69. package/dist/route-styles.js +10 -1
  70. package/dist/route-styles.js.map +1 -1
  71. package/dist/serve.d.ts +5 -0
  72. package/dist/serve.d.ts.map +1 -1
  73. package/dist/serve.js +7 -4
  74. package/dist/serve.js.map +1 -1
  75. package/dist/vite.d.ts.map +1 -1
  76. package/dist/vite.js +32 -5
  77. package/dist/vite.js.map +1 -1
  78. package/package.json +11 -11
  79. package/src/actions.ts +42 -2
  80. package/src/adapters/aws-lambda.ts +33 -16
  81. package/src/adapters/static.ts +25 -0
  82. package/src/build.ts +1605 -421
  83. package/src/bundle-pipeline.ts +136 -70
  84. package/src/cache.ts +13 -8
  85. package/src/cli-options.ts +20 -0
  86. package/src/cli.ts +1 -0
  87. package/src/client.ts +152 -15
  88. package/src/csrf.ts +8 -3
  89. package/src/http.ts +21 -1
  90. package/src/import-policy.ts +8 -3
  91. package/src/index.ts +5 -0
  92. package/src/link.ts +135 -8
  93. package/src/logger.ts +9 -1
  94. package/src/module-runner.ts +4 -0
  95. package/src/native-route-matcher.ts +13 -2
  96. package/src/render.ts +181 -6
  97. package/src/route-source.ts +33 -20
  98. package/src/route-styles.ts +17 -1
  99. package/src/serve.ts +11 -6
  100. package/src/vite.ts +41 -5
package/src/client.ts CHANGED
@@ -67,6 +67,7 @@ export interface ClientRouteManifestEntry {
67
67
  }
68
68
 
69
69
  export interface BuildClientRouteOutputOptions {
70
+ cacheDir?: string | undefined;
70
71
  code: string;
71
72
  clientBoundaryImports?: readonly string[] | undefined;
72
73
  clientReferenceImports?: readonly ClientReferenceImport[] | undefined;
@@ -145,16 +146,20 @@ export interface ClientRouteReferenceResult extends ClientRouteInferenceResult {
145
146
  export interface ClientRouteInferenceDiagnostic {
146
147
  code:
147
148
  | typeof clientBoundaryInferenceServerOnlyReferenceCode
149
+ | typeof clientBoundaryInferenceFunctionCallInteractiveCode
148
150
  | typeof clientBoundaryInferenceUnsupportedReferenceCode;
149
151
  filename: string;
150
152
  level: "warn";
151
153
  localNames: string[];
152
154
  message: string;
155
+ routePath?: string | undefined;
153
156
  source: string;
154
157
  }
155
158
 
156
159
  const clientBoundaryInferenceServerOnlyReferenceCode =
157
160
  "MR_CLIENT_BOUNDARY_INFERENCE_SERVER_ONLY_REFERENCE";
161
+ const clientBoundaryInferenceFunctionCallInteractiveCode =
162
+ "MR_CLIENT_BOUNDARY_INFERENCE_FUNCTION_CALL_INTERACTIVE";
158
163
  const clientBoundaryInferenceUnsupportedReferenceCode =
159
164
  "MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE";
160
165
 
@@ -244,7 +249,7 @@ export async function inferClientRouteModule(options: {
244
249
  : routeInference;
245
250
 
246
251
  if (options.appDir === undefined) {
247
- return mergedRouteInference;
252
+ return withClientRouteDiagnosticPath(mergedRouteInference, options.routePath);
248
253
  }
249
254
 
250
255
  const shellInferences = await inferClientRouteShellModules({
@@ -254,14 +259,18 @@ export async function inferClientRouteModule(options: {
254
259
  sourceTransform,
255
260
  });
256
261
 
257
- return {
258
- client: mergedRouteInference.client || shellInferences.some((inference) => inference.client),
259
- clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
260
- diagnostics: [
261
- ...mergedRouteInference.diagnostics,
262
- ...shellInferences.flatMap((inference) => inference.diagnostics),
263
- ],
264
- };
262
+ return withClientRouteDiagnosticPath(
263
+ {
264
+ client:
265
+ mergedRouteInference.client || shellInferences.some((inference) => inference.client),
266
+ clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
267
+ diagnostics: [
268
+ ...mergedRouteInference.diagnostics,
269
+ ...shellInferences.flatMap((inference) => inference.diagnostics),
270
+ ],
271
+ },
272
+ options.routePath,
273
+ );
265
274
  } catch (error) {
266
275
  throw new Error(
267
276
  `Failed to infer client route for ${options.routePath ?? "<unknown>"} (${options.filename}).\n${errorMessage(error)}`,
@@ -337,6 +346,7 @@ export async function collectClientRouteReferences(options: {
337
346
  cache?: ClientRouteInferenceCache | undefined;
338
347
  code: string;
339
348
  filename: string;
349
+ routePath?: string | undefined;
340
350
  vitePlugins?: readonly PluginOption[] | undefined;
341
351
  }): Promise<ClientRouteReferenceResult> {
342
352
  const cache = options.cache ?? createClientRouteInferenceCache();
@@ -502,7 +512,9 @@ export async function collectClientRouteReferences(options: {
502
512
  clientBoundaryImports: routeInference.clientBoundaryImports,
503
513
  clientReferenceImports,
504
514
  clientReferenceManifest,
505
- diagnostics: sources.flatMap((source) => source.inference.diagnostics),
515
+ diagnostics: sources
516
+ .flatMap((source) => source.inference.diagnostics)
517
+ .map((diagnostic) => withClientRouteDiagnosticPath(diagnostic, options.routePath)),
506
518
  usesNavigationLink:
507
519
  routeInference.usesNavigationLink ||
508
520
  sources.some(
@@ -520,11 +532,17 @@ export async function resolveNavigationRuntime(options: {
520
532
  vitePlugins?: readonly PluginOption[] | undefined;
521
533
  }): Promise<boolean> {
522
534
  const cache = options.cache ?? createClientRouteInferenceCache();
535
+ const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
536
+ const code = await transformClientRouteSource({
537
+ code: options.code,
538
+ filename: options.filename,
539
+ sourceTransform,
540
+ });
523
541
  // Read the override from the cached module context so the dev server does not
524
542
  // re-parse every page route on each request.
525
543
  const moduleContext = await compilerModuleContextForSource({
526
544
  cache,
527
- code: options.code,
545
+ code,
528
546
  filename: options.filename,
529
547
  });
530
548
  const override = readTopLevelBooleanExportFromContext(moduleContext, "navigationRuntime");
@@ -807,6 +825,12 @@ async function inferClientRouteModuleSource(options: {
807
825
  }
808
826
 
809
827
  if (renderedByCall && !renderedByJsx) {
828
+ diagnostics.push(
829
+ functionCallInteractiveImportDiagnostic({
830
+ filename: options.filename,
831
+ reference,
832
+ }),
833
+ );
810
834
  continue;
811
835
  }
812
836
 
@@ -1139,6 +1163,28 @@ function serverOnlyClientImportReferenceDiagnostic(options: {
1139
1163
  };
1140
1164
  }
1141
1165
 
1166
+ function functionCallInteractiveImportDiagnostic(options: {
1167
+ filename: string;
1168
+ reference: StaticImportReference;
1169
+ }): ClientRouteInferenceDiagnostic {
1170
+ const localNames = options.reference.localNames.filter(startsUppercase);
1171
+ const component = localNames[0] ?? options.reference.localNames[0] ?? options.reference.source;
1172
+ const componentUsage = startsUppercase(component) ? `<${component} />` : "JSX";
1173
+
1174
+ return {
1175
+ code: clientBoundaryInferenceFunctionCallInteractiveCode,
1176
+ filename: options.filename,
1177
+ level: "warn",
1178
+ localNames: localNames.length === 0 ? options.reference.localNames : localNames,
1179
+ message:
1180
+ `${options.filename}: interactive component import ${JSON.stringify(options.reference.source)} ` +
1181
+ `is rendered through a function call as ${component}(), so its event handlers or cell() state ` +
1182
+ "will not hydrate as a client boundary. Render it through JSX such as " +
1183
+ `${componentUsage}, or move the call behind an explicit client boundary.`,
1184
+ source: options.reference.source,
1185
+ };
1186
+ }
1187
+
1142
1188
  function unsupportedClientImportReferenceDiagnostic(options: {
1143
1189
  filename: string;
1144
1190
  identifierReferences: ReadonlySet<string>;
@@ -1180,7 +1226,31 @@ function startsUppercase(value: string): boolean {
1180
1226
  export function formatClientRouteInferenceDiagnostic(
1181
1227
  diagnostic: ClientRouteInferenceDiagnostic,
1182
1228
  ): string {
1183
- return `${diagnostic.code}: ${diagnostic.message}`;
1229
+ const route = diagnostic.routePath === undefined ? "" : ` on route ${JSON.stringify(diagnostic.routePath)}`;
1230
+ return `${diagnostic.code}: ${diagnostic.message}${route}`;
1231
+ }
1232
+
1233
+ function withClientRouteDiagnosticPath<T extends ClientRouteInferenceDiagnostic | ClientRouteInferenceResult>(
1234
+ value: T,
1235
+ routePath: string | undefined,
1236
+ ): T {
1237
+ if (routePath === undefined) {
1238
+ return value;
1239
+ }
1240
+
1241
+ if ("diagnostics" in value) {
1242
+ return {
1243
+ ...value,
1244
+ diagnostics: value.diagnostics.map((diagnostic) =>
1245
+ withClientRouteDiagnosticPath(diagnostic, routePath),
1246
+ ),
1247
+ };
1248
+ }
1249
+
1250
+ return {
1251
+ ...value,
1252
+ routePath: value.routePath ?? routePath,
1253
+ };
1184
1254
  }
1185
1255
 
1186
1256
  async function resolveAppLocalModule(options: {
@@ -1573,6 +1643,7 @@ export async function buildClientRouteOutput(
1573
1643
  options.dropConsoleFunctions ?? resolveClientConsolePureFunctions(options.dropClientConsole);
1574
1644
  const bundled = await bundleRouterModule({
1575
1645
  code: entry.code,
1646
+ cacheDir: options.cacheDir,
1576
1647
  define: {
1577
1648
  __MREACT_CLIENT_DEVTOOLS__: "false",
1578
1649
  },
@@ -1594,6 +1665,7 @@ export async function buildClientRouteOutput(
1594
1665
 
1595
1666
  export async function buildClientRouteBatchOutput(options: {
1596
1667
  assetBaseUrl?: string | undefined;
1668
+ cacheDir?: string | undefined;
1597
1669
  dropConsoleFunctions?: readonly string[] | undefined;
1598
1670
  minify?: boolean;
1599
1671
  projectRoot?: string | undefined;
@@ -1616,6 +1688,7 @@ export async function buildClientRouteBatchOutput(options: {
1616
1688
  );
1617
1689
  const bundled = await bundleRouterModules({
1618
1690
  base: options.assetBaseUrl ?? "/_mreact/client/",
1691
+ cacheDir: options.cacheDir,
1619
1692
  define: {
1620
1693
  __MREACT_CLIENT_DEVTOOLS__: "false",
1621
1694
  },
@@ -2085,6 +2158,10 @@ export async function __mreactPrefetch(url) {
2085
2158
  return false;
2086
2159
  }
2087
2160
 
2161
+ if (__mreactIsCurrentLocationNavigationHref(href)) {
2162
+ return false;
2163
+ }
2164
+
2088
2165
  const script = __mreactRouteScriptForNavigationUrl(href);
2089
2166
 
2090
2167
  if (script === undefined) {
@@ -2104,11 +2181,43 @@ function __mreactPrefetchNavigationHtml(href) {
2104
2181
  return false;
2105
2182
  }
2106
2183
 
2107
- __mreactNavigationState.cache.set(href, html);
2184
+ __mreactRememberNavigationHtml(href, html);
2108
2185
  return true;
2109
2186
  }).catch(() => false);
2110
2187
  }
2111
2188
 
2189
+ const __mreactNavigationHtmlCacheMaxEntries = 64;
2190
+
2191
+ function __mreactCachedNavigationHtml(href) {
2192
+ const html = __mreactNavigationState.cache.get(href);
2193
+
2194
+ if (html === undefined) {
2195
+ return undefined;
2196
+ }
2197
+
2198
+ __mreactNavigationState.cache.delete(href);
2199
+ __mreactNavigationState.cache.set(href, html);
2200
+ return html;
2201
+ }
2202
+
2203
+ function __mreactRememberNavigationHtml(href, html) {
2204
+ if (__mreactNavigationState.cache.has(href)) {
2205
+ __mreactNavigationState.cache.delete(href);
2206
+ }
2207
+
2208
+ __mreactNavigationState.cache.set(href, html);
2209
+
2210
+ while (__mreactNavigationState.cache.size > __mreactNavigationHtmlCacheMaxEntries) {
2211
+ const oldestHref = __mreactNavigationState.cache.keys().next().value;
2212
+
2213
+ if (oldestHref === undefined) {
2214
+ return;
2215
+ }
2216
+
2217
+ __mreactNavigationState.cache.delete(oldestHref);
2218
+ }
2219
+ }
2220
+
2112
2221
  function __mreactPrefetchRouteScript(script) {
2113
2222
  if (typeof document === "undefined") {
2114
2223
  return false;
@@ -2149,14 +2258,14 @@ export async function __mreactNavigate(url, options = {}) {
2149
2258
  __mreactSetNavigationState(__mreactPendingNavigationState(href, options.type ?? "push"));
2150
2259
 
2151
2260
  try {
2152
- const cachedHtml = __mreactNavigationState.cache.get(href);
2261
+ const cachedHtml = __mreactCachedNavigationHtml(href);
2153
2262
  const html = cachedHtml ?? await __mreactFetchNavigationHtml(href);
2154
2263
 
2155
2264
  if (typeof html !== "string") {
2156
2265
  return false;
2157
2266
  }
2158
2267
 
2159
- __mreactNavigationState.cache.set(href, html);
2268
+ __mreactRememberNavigationHtml(href, html);
2160
2269
  return await __mreactApplyNavigationHtmlWithOptionalTransition(html, href, options);
2161
2270
  } finally {
2162
2271
  __mreactSetNavigationState(__mreactIdleNavigationState());
@@ -2822,6 +2931,24 @@ function __mreactIsHashOnlyNavigation(nextUrl) {
2822
2931
  nextUrl.hash !== location.hash;
2823
2932
  }
2824
2933
 
2934
+ function __mreactIsCurrentLocationNavigationHref(href) {
2935
+ if (typeof location === "undefined") {
2936
+ return false;
2937
+ }
2938
+
2939
+ return __mreactIsCurrentLocationNavigation(new URL(href, location.href));
2940
+ }
2941
+
2942
+ function __mreactIsCurrentLocationNavigation(nextUrl) {
2943
+ if (typeof location === "undefined") {
2944
+ return false;
2945
+ }
2946
+
2947
+ return nextUrl.origin === location.origin &&
2948
+ nextUrl.pathname === location.pathname &&
2949
+ nextUrl.search === location.search;
2950
+ }
2951
+
2825
2952
  function __mreactInstallNavigation() {
2826
2953
  if (__mreactNavigationState.installed || typeof document === "undefined") {
2827
2954
  return;
@@ -2884,6 +3011,16 @@ function __mreactInstallNavigation() {
2884
3011
  return;
2885
3012
  }
2886
3013
 
3014
+ if (__mreactIsCurrentLocationNavigation(nextUrl)) {
3015
+ event.preventDefault();
3016
+
3017
+ if (__mreactAnchorScrollMode(anchor) !== false && nextUrl.hash === "") {
3018
+ __mreactScrollTo(0, 0);
3019
+ }
3020
+
3021
+ return;
3022
+ }
3023
+
2887
3024
  event.preventDefault();
2888
3025
  void __mreactNavigate(nextUrl.href, {
2889
3026
  scroll: __mreactAnchorScrollMode(anchor),
package/src/csrf.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  const csrfCookieNameProduction = "__Host-mreact.csrf";
2
2
  const csrfCookieNameDevelopment = "mreact.csrf";
3
- const csrfCookieNamesRead = [csrfCookieNameProduction, csrfCookieNameDevelopment];
4
3
  const formFieldCsrf = "__mreact_csrf";
5
4
 
6
5
  export const formCsrfFieldName = formFieldCsrf;
@@ -32,7 +31,7 @@ export function formCsrfCookie(csrfToken: string): string {
32
31
  export function validateFormCsrf(request: Request, formData: FormData): Response | undefined {
33
32
  const formToken = stringFormValue(formData.get(formFieldCsrf));
34
33
  const cookieHeader = request.headers.get("cookie");
35
- const cookieToken = csrfCookieNamesRead
34
+ const cookieToken = csrfCookieNamesRead()
36
35
  .map((name) => readCookie(cookieHeader, name))
37
36
  .find((token) => token !== undefined);
38
37
 
@@ -48,7 +47,7 @@ export function validateFormCsrf(request: Request, formData: FormData): Response
48
47
  export function readExistingFormCsrfToken(request: Request | undefined): string | undefined {
49
48
  const cookieHeader = request?.headers.get("cookie") ?? null;
50
49
 
51
- for (const name of csrfCookieNamesRead) {
50
+ for (const name of csrfCookieNamesRead()) {
52
51
  const token = readCookie(cookieHeader, name);
53
52
 
54
53
  if (token !== undefined && isCsrfTokenShape(token)) {
@@ -90,6 +89,12 @@ function currentCsrfCookieName(): string {
90
89
  return isProductionEnvironment() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
91
90
  }
92
91
 
92
+ function csrfCookieNamesRead(): readonly string[] {
93
+ return isProductionEnvironment()
94
+ ? [csrfCookieNameProduction]
95
+ : [csrfCookieNameProduction, csrfCookieNameDevelopment];
96
+ }
97
+
93
98
  function timingSafeStringEqual(left: string, right: string): boolean {
94
99
  let diff = left.length ^ right.length;
95
100
  const length = Math.max(left.length, right.length);
package/src/http.ts CHANGED
@@ -88,7 +88,16 @@ export async function sendResponse(
88
88
  response: Response,
89
89
  ): Promise<void> {
90
90
  outgoing.statusCode = response.status;
91
- response.headers.forEach((value, key) => outgoing.setHeader(key, value));
91
+ const setCookieHeaders = responseSetCookieHeaders(response.headers);
92
+ response.headers.forEach((value, key) => {
93
+ if (key.toLowerCase() === "set-cookie" && setCookieHeaders.length > 0) {
94
+ return;
95
+ }
96
+ outgoing.setHeader(key, value);
97
+ });
98
+ if (setCookieHeaders.length > 0) {
99
+ outgoing.setHeader("set-cookie", setCookieHeaders);
100
+ }
92
101
 
93
102
  if (response.body === null) {
94
103
  outgoing.end();
@@ -126,3 +135,14 @@ export async function sendResponse(
126
135
  reader.releaseLock();
127
136
  }
128
137
  }
138
+
139
+ function responseSetCookieHeaders(headers: Headers): string[] {
140
+ const getSetCookie = (headers as Headers & { getSetCookie?: () => string[] }).getSetCookie;
141
+
142
+ if (typeof getSetCookie === "function") {
143
+ return getSetCookie.call(headers);
144
+ }
145
+
146
+ const value = headers.get("set-cookie");
147
+ return value === null ? [] : [value];
148
+ }
@@ -106,9 +106,14 @@ export function createAppRouterImportPolicyPlugin(options: AppRouterImportPolicy
106
106
 
107
107
  const resolvedPackage = resolvePackageSpecifier(args.path, args.resolveDir, options.appDir);
108
108
 
109
- return resolvedPackage === undefined
110
- ? undefined
111
- : { external: true, path: pathToFileURL(resolvedPackage).href };
109
+ if (resolvedPackage === undefined) {
110
+ return undefined;
111
+ }
112
+
113
+ return {
114
+ external: true,
115
+ path: pathToFileURL(resolvedPackage).href,
116
+ };
112
117
  });
113
118
  },
114
119
  };
package/src/index.ts CHANGED
@@ -71,6 +71,8 @@ export const getSession = getSessionInternal;
71
71
  export const rotateSession = rotateSessionInternal;
72
72
  export type {
73
73
  AwsLambdaArtifactManifest,
74
+ BuildAppPhase,
75
+ BuildAppPhaseTiming,
74
76
  BuildAppOptions,
75
77
  BuildAppResult,
76
78
  BuiltImportPolicyArtifact,
@@ -78,6 +80,7 @@ export type {
78
80
  PackageAwsLambdaArtifactOptions,
79
81
  PackageCloudflarePagesArtifactOptions,
80
82
  } from "./build.js";
83
+ export type { ServerActionContext } from "./actions.js";
81
84
  export type {
82
85
  InferLoaderData,
83
86
  LayoutProps,
@@ -126,6 +129,7 @@ export type {
126
129
  export type { CookieOptions } from "./cookies.js";
127
130
  export type { AppRouterImportPolicy } from "./import-policy.js";
128
131
  export type {
132
+ LinkChild,
129
133
  LinkOptions,
130
134
  LinkPrefetch,
131
135
  LinkProps,
@@ -140,6 +144,7 @@ export type {
140
144
  export type { RouterRuntimeCacheStat } from "./runtime-cache.js";
141
145
  export type { MemorySessionStoreOptionsInternal as MemorySessionStoreOptions };
142
146
  export type {
147
+ AppRouterCspInlineNonceWarningLogEvent,
143
148
  AppRouterLogError,
144
149
  AppRouterLogEvent,
145
150
  AppRouterLogger,
package/src/link.ts CHANGED
@@ -1,12 +1,18 @@
1
- import {
2
- createElement,
3
- type ReactCompatElement,
4
- type ReactCompatNode,
5
- } from "@reckona/mreact-compat";
1
+ import { escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
2
+ import type { HtmlSink } from "@reckona/mreact-shared/compiler-contract";
3
+ import { safeUrlAttributeValue } from "@reckona/mreact-shared/url-safety";
6
4
 
7
5
  export type LinkPrefetch = "intent" | "viewport" | "none" | false;
8
6
  export type LinkScroll = "top" | "preserve";
9
7
  export type LinkTransition = "auto" | "none" | false;
8
+ export type LinkChild =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | undefined
14
+ | Node
15
+ | readonly LinkChild[];
10
16
 
11
17
  export interface LinkOptions {
12
18
  href: string;
@@ -17,7 +23,7 @@ export interface LinkOptions {
17
23
  }
18
24
 
19
25
  export interface LinkProps extends LinkOptions {
20
- children?: ReactCompatNode;
26
+ children?: LinkChild;
21
27
  [attribute: string]: unknown;
22
28
  }
23
29
 
@@ -37,11 +43,132 @@ export function linkProps(options: LinkOptions): Record<string, string> {
37
43
  };
38
44
  }
39
45
 
40
- export function Link(props: LinkProps): ReactCompatElement {
46
+ export function Link(props: LinkProps): string | HTMLAnchorElement;
47
+ export function Link(sink: HtmlSink, props: LinkProps): void;
48
+ export function Link(
49
+ sinkOrProps: HtmlSink | LinkProps,
50
+ maybeProps?: LinkProps,
51
+ ): string | HTMLAnchorElement | void {
52
+ if (maybeProps !== undefined) {
53
+ (sinkOrProps as HtmlSink).append(renderLinkString(maybeProps));
54
+ return;
55
+ }
56
+
57
+ return renderLink(sinkOrProps as LinkProps);
58
+ }
59
+
60
+ function renderLink(props: LinkProps): string | HTMLAnchorElement {
61
+ const { href, prefetch, reload, scroll, transition, ...rest } = props;
62
+ const propsWithLinkAttrs = {
63
+ ...rest,
64
+ ...linkProps({ href, prefetch, reload, scroll, transition }),
65
+ };
66
+
67
+ if (typeof document !== "undefined" && typeof document.createElement === "function") {
68
+ return createAnchorElement(propsWithLinkAttrs);
69
+ }
70
+
71
+ return renderAnchorString(propsWithLinkAttrs);
72
+ }
73
+
74
+ function renderLinkString(props: LinkProps): string {
41
75
  const { href, prefetch, reload, scroll, transition, ...rest } = props;
42
76
 
43
- return createElement("a", {
77
+ return renderAnchorString({
44
78
  ...rest,
45
79
  ...linkProps({ href, prefetch, reload, scroll, transition }),
46
80
  });
47
81
  }
82
+
83
+ function createAnchorElement(props: Record<string, unknown>): HTMLAnchorElement {
84
+ const anchor = document.createElement("a");
85
+
86
+ for (const [name, value] of Object.entries(props)) {
87
+ if (name === "children") {
88
+ appendLinkChild(anchor, value as LinkChild);
89
+ continue;
90
+ }
91
+
92
+ if (!shouldSetAttribute(name, value)) {
93
+ continue;
94
+ }
95
+
96
+ anchor.setAttribute(attributeName(name), String(value));
97
+ }
98
+
99
+ return anchor;
100
+ }
101
+
102
+ function appendLinkChild(parent: Node, child: LinkChild): void {
103
+ if (child === null || child === undefined || typeof child === "boolean") {
104
+ return;
105
+ }
106
+
107
+ if (Array.isArray(child)) {
108
+ for (const item of child) {
109
+ appendLinkChild(parent, item);
110
+ }
111
+ return;
112
+ }
113
+
114
+ if (child instanceof Node) {
115
+ parent.appendChild(child);
116
+ return;
117
+ }
118
+
119
+ parent.appendChild(document.createTextNode(String(child)));
120
+ }
121
+
122
+ function renderAnchorString(props: Record<string, unknown>): string {
123
+ return `<a${renderAnchorAttributes(props)}>${renderChildren(props.children as LinkChild)}</a>`;
124
+ }
125
+
126
+ function renderAnchorAttributes(props: Record<string, unknown>): string {
127
+ const attrs: string[] = [];
128
+
129
+ for (const [name, value] of Object.entries(props)) {
130
+ if (name === "children" || !shouldSetAttribute(name, value)) {
131
+ continue;
132
+ }
133
+
134
+ const attrName = attributeName(name);
135
+ const attrValue = String(value);
136
+ const safeValue = safeUrlAttributeValue(attrName, attrValue);
137
+
138
+ if (safeValue === undefined) {
139
+ continue;
140
+ }
141
+
142
+ attrs.push(`${escapeHtmlAttribute(attrName)}="${escapeHtmlAttribute(safeValue)}"`);
143
+ }
144
+
145
+ return attrs.length === 0 ? "" : ` ${attrs.join(" ")}`;
146
+ }
147
+
148
+ function shouldSetAttribute(name: string, value: unknown): boolean {
149
+ return (
150
+ name !== "key" &&
151
+ name !== "ref" &&
152
+ value !== null &&
153
+ value !== undefined &&
154
+ value !== false &&
155
+ typeof value !== "function" &&
156
+ typeof value !== "symbol"
157
+ );
158
+ }
159
+
160
+ function attributeName(name: string): string {
161
+ return name === "className" ? "class" : name;
162
+ }
163
+
164
+ function renderChildren(child: LinkChild): string {
165
+ if (child === null || child === undefined || typeof child === "boolean") {
166
+ return "";
167
+ }
168
+
169
+ if (Array.isArray(child)) {
170
+ return child.map(renderChildren).join("");
171
+ }
172
+
173
+ return String(child);
174
+ }
package/src/logger.ts CHANGED
@@ -14,7 +14,8 @@ export type AppRouterLogEvent =
14
14
  | AppRouterRequestEndLogEvent
15
15
  | AppRouterRequestErrorLogEvent
16
16
  | AppRouterRequestTimingLogEvent
17
- | AppRouterRenderTimingLogEvent;
17
+ | AppRouterRenderTimingLogEvent
18
+ | AppRouterCspInlineNonceWarningLogEvent;
18
19
 
19
20
  export interface AppRouterRequestStartLogEvent {
20
21
  method: string;
@@ -59,6 +60,13 @@ export interface AppRouterRenderTimingLogEvent {
59
60
  type: "router:render:timing";
60
61
  }
61
62
 
63
+ export interface AppRouterCspInlineNonceWarningLogEvent {
64
+ directive: "script-src" | "style-src";
65
+ path: string;
66
+ tag: "script" | "style";
67
+ type: "router:csp:inline-nonce-warning";
68
+ }
69
+
62
70
  export interface AppRouterLogError {
63
71
  message: string;
64
72
  name: string;
@@ -85,6 +85,7 @@ export async function importAppRouterSourceModule<T>(options: {
85
85
  label: string;
86
86
  plugins?: readonly RouterCompatPlugin[] | undefined;
87
87
  resolveDir?: string | undefined;
88
+ root?: string | undefined;
88
89
  serverSourceTransform?: ServerSourceTransformOptions | undefined;
89
90
  sourcefile?: string | undefined;
90
91
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -125,6 +126,7 @@ async function importAppRouterSourceModuleWithoutCache<T>(options: {
125
126
  label: string;
126
127
  plugins?: readonly RouterCompatPlugin[] | undefined;
127
128
  resolveDir?: string | undefined;
129
+ root?: string | undefined;
128
130
  serverSourceTransform?: ServerSourceTransformOptions | undefined;
129
131
  sourcefile?: string | undefined;
130
132
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -275,6 +277,7 @@ export async function bundleAppRouterSourceModule(options: {
275
277
  label: string;
276
278
  plugins?: readonly RouterCompatPlugin[] | undefined;
277
279
  resolveDir?: string | undefined;
280
+ root?: string | undefined;
278
281
  serverSourceTransform?: ServerSourceTransformOptions | undefined;
279
282
  sourcefile?: string | undefined;
280
283
  vitePlugins?: readonly PluginOption[] | undefined;
@@ -284,6 +287,7 @@ export async function bundleAppRouterSourceModule(options: {
284
287
  externalizeAppSourceModuleDirs: options.externalizeAppSourceModuleDirs,
285
288
  filename: options.sourcefile ?? join(options.resolveDir ?? process.cwd(), "module.js"),
286
289
  platform: "node",
290
+ root: options.root,
287
291
  vitePlugins: options.vitePlugins,
288
292
  plugins: [
289
293
  workspacePackageResolutionPlugin(),
@@ -70,7 +70,7 @@ export function createNativeRouteMatcher(
70
70
  };
71
71
  }
72
72
 
73
- function normalizeNativeParams(
73
+ export function normalizeNativeParams(
74
74
  route: AppRoute,
75
75
  params: Record<string, string>,
76
76
  ): MatchedRoute["params"] {
@@ -82,13 +82,24 @@ function normalizeNativeParams(
82
82
  }
83
83
  const value = normalized[segment.name];
84
84
  if (typeof value === "string") {
85
- normalized[segment.name] = value.split("/").filter((part: string) => part !== "");
85
+ normalized[segment.name] = value
86
+ .split("/")
87
+ .filter((part: string) => part !== "")
88
+ .map((part: string) => safeDecodeURIComponent(part) ?? part);
86
89
  }
87
90
  }
88
91
 
89
92
  return normalized;
90
93
  }
91
94
 
95
+ function safeDecodeURIComponent(value: string): string | undefined {
96
+ try {
97
+ return decodeURIComponent(value);
98
+ } catch {
99
+ return undefined;
100
+ }
101
+ }
102
+
92
103
  export function shouldUseNativeRouteMatcher(
93
104
  routeCount: number,
94
105
  mode: string | undefined,