@reckona/mreact-router 0.0.151 → 0.0.153

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/actions.d.ts +10 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +6 -0
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +18 -0
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +18 -0
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +33 -0
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +33 -0
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +5 -0
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js +5 -0
  17. package/dist/adapters/edge.js.map +1 -1
  18. package/dist/adapters/node.d.ts +5 -0
  19. package/dist/adapters/node.d.ts.map +1 -1
  20. package/dist/adapters/node.js +5 -0
  21. package/dist/adapters/node.js.map +1 -1
  22. package/dist/adapters/static.d.ts +5 -0
  23. package/dist/adapters/static.d.ts.map +1 -1
  24. package/dist/adapters/static.js +5 -0
  25. package/dist/adapters/static.js.map +1 -1
  26. package/dist/build.d.ts +15 -0
  27. package/dist/build.d.ts.map +1 -1
  28. package/dist/build.js +71 -1
  29. package/dist/build.js.map +1 -1
  30. package/dist/cache.d.ts +15 -0
  31. package/dist/cache.d.ts.map +1 -1
  32. package/dist/cache.js +15 -0
  33. package/dist/cache.js.map +1 -1
  34. package/dist/cookies.d.ts +16 -0
  35. package/dist/cookies.d.ts.map +1 -1
  36. package/dist/cookies.js +16 -0
  37. package/dist/cookies.js.map +1 -1
  38. package/dist/dev-server.d.ts +5 -0
  39. package/dist/dev-server.d.ts.map +1 -1
  40. package/dist/dev-server.js +5 -0
  41. package/dist/dev-server.js.map +1 -1
  42. package/dist/link.d.ts +10 -0
  43. package/dist/link.d.ts.map +1 -1
  44. package/dist/link.js +5 -0
  45. package/dist/link.js.map +1 -1
  46. package/dist/middleware.d.ts.map +1 -1
  47. package/dist/middleware.js +39 -4
  48. package/dist/middleware.js.map +1 -1
  49. package/dist/navigation.d.ts +53 -0
  50. package/dist/navigation.d.ts.map +1 -1
  51. package/dist/navigation.js +48 -0
  52. package/dist/navigation.js.map +1 -1
  53. package/dist/render.d.ts +28 -0
  54. package/dist/render.d.ts.map +1 -1
  55. package/dist/render.js +127 -27
  56. package/dist/render.js.map +1 -1
  57. package/dist/route-module-loader.d.ts +6 -1
  58. package/dist/route-module-loader.d.ts.map +1 -1
  59. package/dist/route-module-loader.js +8 -1
  60. package/dist/route-module-loader.js.map +1 -1
  61. package/dist/serve.d.ts +5 -0
  62. package/dist/serve.d.ts.map +1 -1
  63. package/dist/serve.js +5 -0
  64. package/dist/serve.js.map +1 -1
  65. package/dist/session.d.ts +19 -0
  66. package/dist/session.d.ts.map +1 -1
  67. package/dist/session.js +19 -0
  68. package/dist/session.js.map +1 -1
  69. package/dist/tailwind-source.d.ts +6 -0
  70. package/dist/tailwind-source.d.ts.map +1 -0
  71. package/dist/tailwind-source.js +25 -0
  72. package/dist/tailwind-source.js.map +1 -0
  73. package/dist/typed-routes.d.ts +5 -0
  74. package/dist/typed-routes.d.ts.map +1 -1
  75. package/dist/typed-routes.js +5 -0
  76. package/dist/typed-routes.js.map +1 -1
  77. package/dist/types.d.ts +15 -0
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/types.js +5 -0
  80. package/dist/types.js.map +1 -1
  81. package/dist/vite.d.ts.map +1 -1
  82. package/dist/vite.js +3 -25
  83. package/dist/vite.js.map +1 -1
  84. package/package.json +11 -11
  85. package/src/actions.ts +18 -0
  86. package/src/adapters/aws-lambda.ts +18 -0
  87. package/src/adapters/cloudflare.ts +33 -0
  88. package/src/adapters/edge.ts +5 -0
  89. package/src/adapters/node.ts +5 -0
  90. package/src/adapters/static.ts +5 -0
  91. package/src/build.ts +92 -2
  92. package/src/cache.ts +15 -0
  93. package/src/cookies.ts +16 -0
  94. package/src/dev-server.ts +5 -0
  95. package/src/link.ts +10 -0
  96. package/src/middleware.ts +54 -4
  97. package/src/navigation.ts +53 -0
  98. package/src/render.ts +164 -25
  99. package/src/route-module-loader.ts +15 -0
  100. package/src/serve.ts +5 -0
  101. package/src/session.ts +19 -0
  102. package/src/tailwind-source.ts +37 -0
  103. package/src/typed-routes.ts +5 -0
  104. package/src/types.ts +15 -0
  105. package/src/vite.ts +3 -38
package/src/build.ts CHANGED
@@ -90,12 +90,13 @@ import { sourceModuleCandidates } from "./source-modules.js";
90
90
  import { collectBuildInferredServerActions } from "./server-action-inference.js";
91
91
  import { viteDefineCacheKey, vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
92
92
  import { workspacePackageFile } from "./workspace-packages.js";
93
+ import { prependTailwindSourceDirectives } from "./tailwind-source.js";
93
94
  import {
94
95
  parseRouteMiddlewareControl,
95
96
  parseStaticMiddlewareConfig,
96
97
  validateRouteMiddlewareControl,
97
98
  } from "./middleware.js";
98
- import type { PluginOption, UserConfig } from "vite";
99
+ import type { Plugin, PluginOption, UserConfig } from "vite";
99
100
 
100
101
  const nativeEscapeTransform = {
101
102
  batchImportName: "escapeHtmlBatch",
@@ -333,6 +334,11 @@ export interface BuiltPrerenderedRoute {
333
334
 
334
335
  type StaticParams = Record<string, string | number | boolean | readonly string[]>;
335
336
 
337
+ /**
338
+ * Builds an app-router project into server, client, and optional deployment-target artifacts.
339
+ *
340
+ * Use this from custom build scripts when the CLI is too coarse-grained; the returned manifest paths describe the files written under `outDir`. The build reads route files, loaders, middleware, metadata, server actions, and client references, so callers should pass the same project root and source allow-list they expect to deploy.
341
+ */
336
342
  export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
337
343
  const timingSink = options.onBuildPhaseTiming;
338
344
  const progressSink = options.onBuildProgress;
@@ -530,6 +536,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
530
536
  sourceAnalysis,
531
537
  sourceMapDir: join(options.outDir, "source-maps", "client"),
532
538
  sourceMaps: project.clientSourceMaps,
539
+ sourceDirs: project.allowedSourceDirs,
533
540
  vitePlugins,
534
541
  })
535
542
  : timeBuildPhase(timingSink, progressSink, "clientBundles", () =>
@@ -544,6 +551,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
544
551
  sourceAnalysis,
545
552
  sourceMapDir: join(options.outDir, "source-maps", "client"),
546
553
  sourceMaps: project.clientSourceMaps,
554
+ sourceDirs: project.allowedSourceDirs,
547
555
  vitePlugins,
548
556
  }),
549
557
  ),
@@ -1759,6 +1767,12 @@ async function prerenderStaticRoutes(options: {
1759
1767
  artifact,
1760
1768
  ]),
1761
1769
  );
1770
+ const serverModuleCacheVersion = buildPrerenderServerModuleCacheVersion({
1771
+ define: options.define,
1772
+ project: options.project,
1773
+ sourceAnalysis: options.sourceAnalysis,
1774
+ vitePlugins: options.vitePlugins,
1775
+ });
1762
1776
 
1763
1777
  const prerenderedEntries = await mapWithBuildConcurrency(
1764
1778
  options.routes.filter((route): route is AppRoute & { kind: "page" } => route.kind === "page"),
@@ -1780,6 +1794,7 @@ async function prerenderStaticRoutes(options: {
1780
1794
  define: options.define,
1781
1795
  importPolicy,
1782
1796
  request: new Request(`http://mreact.local${pathname}`),
1797
+ serverModuleCacheVersion,
1783
1798
  serverModules: serverModuleMap,
1784
1799
  vitePlugins: options.vitePlugins,
1785
1800
  });
@@ -1810,6 +1825,28 @@ async function prerenderStaticRoutes(options: {
1810
1825
  return prerendered;
1811
1826
  }
1812
1827
 
1828
+ function buildPrerenderServerModuleCacheVersion(options: {
1829
+ define?: UserConfig["define"] | undefined;
1830
+ project: ResolvedAppRouterProject;
1831
+ sourceAnalysis: BuildSourceAnalysisScope;
1832
+ vitePlugins?: readonly PluginOption[] | undefined;
1833
+ }): string {
1834
+ const sourceHashes = Array.from(options.sourceAnalysis.byFile.entries())
1835
+ .map(([file, analysis]) => [file, analysis.sourceHash] as const)
1836
+ .sort(([left], [right]) => left.localeCompare(right));
1837
+
1838
+ return `build-prerender:${hashText(
1839
+ JSON.stringify({
1840
+ allowedSourceDirs: [...options.project.allowedSourceDirs].sort(),
1841
+ projectRoot: options.project.projectRoot,
1842
+ routesDir: options.project.routesDir,
1843
+ sourceHashes,
1844
+ viteDefine: viteDefineCacheKey(options.define),
1845
+ vitePlugins: vitePluginsCacheKey(options.vitePlugins),
1846
+ }),
1847
+ ).slice(0, 16)}`;
1848
+ }
1849
+
1813
1850
  async function prerenderPathsForRoute(
1814
1851
  route: AppRoute,
1815
1852
  analysis: BuildRouteSourceAnalysis,
@@ -2102,6 +2139,10 @@ async function buildServerModuleArtifacts(options: {
2102
2139
  filename: join(options.projectRoot, file),
2103
2140
  moduleContextCache: options.clientRouteInferenceCache,
2104
2141
  serverOutput,
2142
+ ...(serverOutput === "stream" &&
2143
+ (routeAnalysis?.clientRoute ?? clientInference?.client) === true
2144
+ ? { serverAwaitHydration: true as const }
2145
+ : {}),
2105
2146
  });
2106
2147
  const fatalDiagnostics = output.diagnostics.filter(
2107
2148
  (diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
@@ -5012,6 +5053,7 @@ async function writeClientRouteBundles(options: {
5012
5053
  sourceAnalysis: BuildSourceAnalysisScope;
5013
5054
  sourceMapDir: string;
5014
5055
  sourceMaps: AppRouterClientSourceMapMode;
5056
+ sourceDirs: readonly string[];
5015
5057
  vitePlugins?: readonly PluginOption[] | undefined;
5016
5058
  }): Promise<ClientRouteBundleManifest> {
5017
5059
  type PreparedClientRouteEntry = {
@@ -5033,6 +5075,7 @@ async function writeClientRouteBundles(options: {
5033
5075
  pageRoutes,
5034
5076
  projectRoot: options.projectRoot,
5035
5077
  sourceAnalysis: options.sourceAnalysis,
5078
+ sourceDirs: options.sourceDirs,
5036
5079
  vitePlugins: options.vitePlugins,
5037
5080
  });
5038
5081
  const specialCssAssets = await writeSpecialRouteCssAssetBatches({
@@ -5042,6 +5085,7 @@ async function writeClientRouteBundles(options: {
5042
5085
  clientDir: options.clientDir,
5043
5086
  projectRoot: options.projectRoot,
5044
5087
  sourceAnalysis: options.sourceAnalysis,
5088
+ sourceDirs: options.sourceDirs,
5045
5089
  vitePlugins: options.vitePlugins,
5046
5090
  });
5047
5091
  const entries: PreparedRouteEntry[] = await Promise.all(
@@ -5271,6 +5315,7 @@ async function writeSpecialRouteCssAssetBatches(options: {
5271
5315
  clientDir: string;
5272
5316
  projectRoot: string;
5273
5317
  sourceAnalysis: BuildSourceAnalysisScope;
5318
+ sourceDirs: readonly string[];
5274
5319
  vitePlugins?: readonly PluginOption[] | undefined;
5275
5320
  }): Promise<{ assets: string[]; styles: ClientStyleManifestEntry[] }> {
5276
5321
  const boundaryFiles = await collectSpecialBoundaryFiles(options.appDir);
@@ -5295,6 +5340,7 @@ async function writeSpecialRouteCssAssetBatches(options: {
5295
5340
  pageFile: file,
5296
5341
  projectRoot: options.projectRoot,
5297
5342
  routeIds: [specialBoundaryRouteId(options.appDir, file)],
5343
+ sourceDirs: options.sourceDirs,
5298
5344
  vitePlugins: options.vitePlugins,
5299
5345
  });
5300
5346
 
@@ -5335,6 +5381,7 @@ async function writeRouteCssAssetBatches(options: {
5335
5381
  pageRoutes: readonly (AppRoute & { kind: "page" })[];
5336
5382
  projectRoot: string;
5337
5383
  sourceAnalysis: BuildSourceAnalysisScope;
5384
+ sourceDirs: readonly string[];
5338
5385
  vitePlugins?: readonly PluginOption[] | undefined;
5339
5386
  }): Promise<{ assets: string[]; byRoute: Map<string, string[]> }> {
5340
5387
  const cssInputs = await mapWithBuildConcurrency(options.pageRoutes, async (route) => {
@@ -5378,6 +5425,7 @@ async function writeRouteCssAssetBatches(options: {
5378
5425
  pageFile: group.routeFiles[0] ?? options.appDir,
5379
5426
  projectRoot: options.projectRoot,
5380
5427
  routeIds: group.routeIds,
5428
+ sourceDirs: options.sourceDirs,
5381
5429
  vitePlugins: options.vitePlugins,
5382
5430
  }),
5383
5431
  ] as const,
@@ -5410,6 +5458,7 @@ async function writeRouteCssAssetsForFiles(options: {
5410
5458
  pageFile: string;
5411
5459
  projectRoot: string;
5412
5460
  routeIds: readonly string[];
5461
+ sourceDirs: readonly string[];
5413
5462
  vitePlugins?: readonly PluginOption[] | undefined;
5414
5463
  }): Promise<RouteCssAssetBatch> {
5415
5464
  const cssFiles = [...options.cssFiles];
@@ -5429,7 +5478,13 @@ async function writeRouteCssAssetsForFiles(options: {
5429
5478
  minify: true,
5430
5479
  platform: "browser",
5431
5480
  root: options.projectRoot,
5432
- vitePlugins: options.vitePlugins,
5481
+ vitePlugins: [
5482
+ productionRouteCssTailwindSourcePlugin({
5483
+ cssFiles,
5484
+ sourceDirs: options.sourceDirs,
5485
+ }),
5486
+ ...(options.vitePlugins ?? []),
5487
+ ],
5433
5488
  });
5434
5489
  const cssAssets = (output.assets ?? []).filter((asset) => asset.fileName.endsWith(".css"));
5435
5490
  const nonCssAssets = (output.assets ?? []).filter((asset) => !asset.fileName.endsWith(".css"));
@@ -5463,6 +5518,31 @@ async function writeRouteCssAssetsForFiles(options: {
5463
5518
  return { assets: writtenAssets.sort(), css: written };
5464
5519
  }
5465
5520
 
5521
+ function productionRouteCssTailwindSourcePlugin(options: {
5522
+ cssFiles: readonly string[];
5523
+ sourceDirs: readonly string[];
5524
+ }): Plugin {
5525
+ const cssFiles = new Set(options.cssFiles);
5526
+
5527
+ return {
5528
+ name: "mreact-production-route-css-tailwind-source",
5529
+ enforce: "pre",
5530
+ transform(code, id) {
5531
+ const [filename] = id.split(/[?#]/, 1);
5532
+
5533
+ if (filename === undefined || !cssFiles.has(filename)) {
5534
+ return;
5535
+ }
5536
+
5537
+ return prependTailwindSourceDirectives({
5538
+ code,
5539
+ cssFile: filename,
5540
+ sourceDirs: options.sourceDirs,
5541
+ });
5542
+ },
5543
+ };
5544
+ }
5545
+
5466
5546
  function applyClientSourceMapReference(options: {
5467
5547
  code: string;
5468
5548
  scriptBasename: string;
@@ -5541,6 +5621,11 @@ async function writeCloudflareWorkerArtifact(options: {
5541
5621
  );
5542
5622
  }
5543
5623
 
5624
+ /**
5625
+ * Packages a built app-router output directory into the minimal AWS Lambda artifact layout.
5626
+ *
5627
+ * The package contains `.mreact`, a Lambda handler, project package metadata, production dependencies, and `mreact-lambda-artifact.json`; it intentionally excludes source files and development tooling that are not needed at runtime.
5628
+ */
5544
5629
  export async function packageAwsLambdaArtifact(
5545
5630
  options: PackageAwsLambdaArtifactOptions,
5546
5631
  ): Promise<AwsLambdaArtifactManifest> {
@@ -5648,6 +5733,11 @@ async function assertAwsLambdaRuntimeDependencies(outDir: string): Promise<void>
5648
5733
  );
5649
5734
  }
5650
5735
 
5736
+ /**
5737
+ * Packages a Cloudflare-target build for Cloudflare Pages advanced mode.
5738
+ *
5739
+ * The package writes `_worker.js`, generated client assets, public assets, and `mreact-cloudflare-pages-artifact.json`; deploy the output directory rather than the raw `.mreact` build tree.
5740
+ */
5651
5741
  export async function packageCloudflarePagesArtifact(
5652
5742
  options: PackageCloudflarePagesArtifactOptions,
5653
5743
  ): Promise<CloudflarePagesArtifactManifest> {
package/src/cache.ts CHANGED
@@ -56,6 +56,11 @@ const cacheState = ((
56
56
  });
57
57
  cacheState.storage ??= new AsyncLocalStorage<RouteCacheContext>();
58
58
 
59
+ /**
60
+ * Creates an in-memory route response cache for app-router rendering.
61
+ *
62
+ * The cache is process-local, evicts expired entries during reads/writes, and is suitable for development, tests, or single-process deployments that do not need shared invalidation.
63
+ */
59
64
  export function createMemoryRouteCache(options: MemoryRouteCacheOptions = {}): AppRouterCache {
60
65
  const maxEntries = positiveIntegerOrDefault(options.maxEntries, 10_000);
61
66
  const sweepIntervalMs = nonNegativeIntegerOrDefault(options.sweepIntervalMs, 60_000);
@@ -192,6 +197,11 @@ export function routeCachePolicyFromSource(code: string): RouteCachePolicy | und
192
197
  };
193
198
  }
194
199
 
200
+ /**
201
+ * Sets the cache policy for the current app-router render.
202
+ *
203
+ * Call this during a loader or route render to override the response `Cache-Control` policy; it throws outside an active app-router request.
204
+ */
195
205
  export function cacheControl(options: CacheControlOptions): void {
196
206
  const activeContext = activeRouteCacheContext();
197
207
 
@@ -326,6 +336,11 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
326
336
  return request.headers.has("authorization") || request.headers.has("cookie");
327
337
  }
328
338
 
339
+ /**
340
+ * Invalidates cached route responses for a normalized app-router path.
341
+ *
342
+ * During a request the invalidation is scoped to that route cache context; outside a request it is queued for the next cache access.
343
+ */
329
344
  export function revalidatePath(path: string): void {
330
345
  const normalizedPath = normalizeRevalidationPath(path);
331
346
  const activeContext = activeRouteCacheContext();
package/src/cookies.ts CHANGED
@@ -22,6 +22,11 @@ function assertAttributeValue(value: string): void {
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ * Parses a raw `Cookie` header into decoded name/value pairs.
27
+ *
28
+ * Malformed percent-encoded values are skipped so one bad cookie does not abort request handling.
29
+ */
25
30
  export function parseCookieHeader(
26
31
  cookieHeader: string | null | undefined,
27
32
  ): Map<string, string> {
@@ -50,6 +55,11 @@ export function parseCookieHeader(
50
55
  return values;
51
56
  }
52
57
 
58
+ /**
59
+ * Serializes a cookie name, value, and attributes for a `Set-Cookie` header.
60
+ *
61
+ * Cookie names and attribute values are validated, and `SameSite=None` requires `Secure`.
62
+ */
53
63
  export function serializeCookie(
54
64
  name: string,
55
65
  value: string,
@@ -91,6 +101,9 @@ export function serializeCookie(
91
101
  return parts.join("; ");
92
102
  }
93
103
 
104
+ /**
105
+ * Appends a serialized cookie to a response's `Set-Cookie` headers and returns the same response.
106
+ */
94
107
  export function setCookie(
95
108
  response: Response,
96
109
  name: string,
@@ -101,6 +114,9 @@ export function setCookie(
101
114
  return response;
102
115
  }
103
116
 
117
+ /**
118
+ * Appends an expiring `Set-Cookie` header that removes a cookie in the browser.
119
+ */
104
120
  export function deleteCookie(
105
121
  response: Response,
106
122
  name: string,
package/src/dev-server.ts CHANGED
@@ -33,6 +33,11 @@ export interface StartDevServerOptions extends AppRouterProjectOptions {
33
33
  onUpgrade?: HttpUpgradeHandler | undefined;
34
34
  }
35
35
 
36
+ /**
37
+ * Starts the local app-router development server with Vite middleware and route compilation.
38
+ *
39
+ * The server trusts only the configured host settings, merges declared application packages into the development import policy, and returns a `close()` method that should be awaited by tests and scripts.
40
+ */
36
41
  export async function startDevServer(
37
42
  options: StartDevServerOptions,
38
43
  ): Promise<{ close(): Promise<void>; server: Server; url: string }> {
package/src/link.ts CHANGED
@@ -39,6 +39,11 @@ export interface LinkProps<Href extends string = LinkHref> extends LinkOptions<H
39
39
  [attribute: string]: unknown;
40
40
  }
41
41
 
42
+ /**
43
+ * Converts router link options into anchor attributes consumed by the client navigation runtime.
44
+ *
45
+ * Use this when rendering a custom anchor component that should still opt into mreact prefetch, scroll, reload, or transition behavior.
46
+ */
42
47
  export function linkProps(options: LinkOptions<string>): Record<string, string> {
43
48
  return {
44
49
  href: options.href,
@@ -61,6 +66,11 @@ export type ConcreteLinkHrefGuard<Href extends string> = [RegisteredAppRoutePath
61
66
  ? { readonly __mreactRoutePatternHrefError__: never }
62
67
  : unknown;
63
68
 
69
+ /**
70
+ * Renders an app-router anchor with typed `href` support and navigation runtime attributes.
71
+ *
72
+ * In JSX it returns an anchor element compatible with the mreact runtime; during server streaming it can also write directly to an `HtmlSink`. Unsafe URL attribute values are dropped during rendering.
73
+ */
64
74
  export function Link<const Href extends LinkHref>(
65
75
  props: LinkProps<Href> & ConcreteLinkHrefGuard<Href>,
66
76
  ): ReactCompatElement;
package/src/middleware.ts CHANGED
@@ -110,28 +110,78 @@ export function middlewareMatches(config: MiddlewareModule["config"], pathname:
110
110
  }
111
111
 
112
112
  export function parseRouteMiddlewareControl(code: string): RouteMiddlewareControl | undefined {
113
- if (!/\bexport\s+const\s+middleware\s*=/.test(code)) {
113
+ const masked = maskCodeLiterals(code);
114
+ const declaration = /\bexport\s+const\s+middleware\s*=\s*\{/.exec(masked);
115
+
116
+ if (declaration === null) {
117
+ return undefined;
118
+ }
119
+
120
+ const objectStart = masked.indexOf("{", declaration.index);
121
+ const objectEnd = findMatchingBrace(masked, objectStart);
122
+
123
+ if (objectStart < 0 || objectEnd < 0) {
114
124
  return undefined;
115
125
  }
116
126
 
117
- if (/\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*true\b/.test(code)) {
127
+ const maskedBody = masked.slice(objectStart + 1, objectEnd);
128
+ const body = code.slice(objectStart + 1, objectEnd);
129
+
130
+ if (/\bskip\s*:\s*true\b/.test(maskedBody)) {
118
131
  return { skip: true };
119
132
  }
120
133
 
121
- const skipArray = /\bmiddleware\s*=\s*\{[\s\S]*?\bskip\s*:\s*\[([\s\S]*?)\]/.exec(code);
134
+ const skipArray = /\bskip\s*:\s*\[([\s\S]*?)\]/.exec(maskedBody);
122
135
 
123
136
  if (skipArray === null) {
124
137
  return undefined;
125
138
  }
126
139
 
140
+ const arrayStart = skipArray.index + skipArray[0].indexOf("[") + 1;
141
+ const arrayEnd = skipArray.index + skipArray[0].lastIndexOf("]");
142
+ const arrayBody = body.slice(arrayStart, arrayEnd);
127
143
  const ids = Array.from(
128
- skipArray[1]?.matchAll(/["']([^"']+)["']/g) ?? [],
144
+ arrayBody.matchAll(/["']([^"']+)["']/g),
129
145
  (match) => match[1],
130
146
  ).filter((id) => id !== undefined);
131
147
 
132
148
  return ids.length === 0 ? undefined : { skip: ids };
133
149
  }
134
150
 
151
+ function maskCodeLiterals(code: string): string {
152
+ return code.replace(
153
+ /\/\*[\s\S]*?\*\/|\/\/[^\n\r]*|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`/g,
154
+ (match) => " ".repeat(match.length),
155
+ );
156
+ }
157
+
158
+ function findMatchingBrace(code: string, openIndex: number): number {
159
+ if (openIndex < 0) {
160
+ return -1;
161
+ }
162
+
163
+ let depth = 0;
164
+
165
+ for (let index = openIndex; index < code.length; index += 1) {
166
+ const char = code[index];
167
+
168
+ if (char === "{") {
169
+ depth += 1;
170
+ continue;
171
+ }
172
+
173
+ if (char === "}") {
174
+ depth -= 1;
175
+
176
+ if (depth === 0) {
177
+ return index;
178
+ }
179
+ }
180
+ }
181
+
182
+ return -1;
183
+ }
184
+
135
185
  export function mergeRouteMiddlewareControls(
136
186
  controls: readonly (RouteMiddlewareControl | undefined)[],
137
187
  ): RouteMiddlewareControl | undefined {
package/src/navigation.ts CHANGED
@@ -61,6 +61,11 @@ function throwUnsafeRewrite(location: string): never {
61
61
  throw new TypeError(`unsafe rewrite target: ${JSON.stringify(location)}`);
62
62
  }
63
63
 
64
+ /**
65
+ * Throws an internal redirect for loaders, middleware, route handlers, or server actions.
66
+ *
67
+ * The target must be same-origin or relative; use `redirectExternal()` for trusted `http` or `https` destinations. The default status is `303`.
68
+ */
64
69
  export function redirect(location: string, options: RedirectOptions = {}): never {
65
70
  if (!isSafeInternalRedirect(location)) {
66
71
  throwUnsafeRedirect(location);
@@ -72,6 +77,11 @@ export function redirect(location: string, options: RedirectOptions = {}): never
72
77
  });
73
78
  }
74
79
 
80
+ /**
81
+ * Throws an external redirect for trusted `http` or `https` destinations.
82
+ *
83
+ * This helper is intentionally separate from `redirect()` so off-site navigation is explicit. The default status is `307`.
84
+ */
75
85
  export function redirectExternal(
76
86
  location: string,
77
87
  options: RedirectOptions = {},
@@ -86,6 +96,11 @@ export function redirectExternal(
86
96
  });
87
97
  }
88
98
 
99
+ /**
100
+ * Creates a same-origin `303 See Other` redirect response.
101
+ *
102
+ * Use this from route handlers when returning a `Response` is more convenient than throwing through the app-router control flow.
103
+ */
89
104
  export function redirect303(location: string, init: ResponseInit = {}): Response {
90
105
  if (!isSafeInternalRedirect(location)) {
91
106
  throwUnsafeRedirect(location);
@@ -101,6 +116,9 @@ export function redirect303(location: string, init: ResponseInit = {}): Response
101
116
  });
102
117
  }
103
118
 
119
+ /**
120
+ * Creates a plain-text error response with a default `text/plain` content type.
121
+ */
104
122
  export function textError(message: string, status = 400, init: ResponseInit = {}): Response {
105
123
  const headers = new Headers(init.headers);
106
124
 
@@ -115,6 +133,11 @@ export function textError(message: string, status = 400, init: ResponseInit = {}
115
133
  });
116
134
  }
117
135
 
136
+ /**
137
+ * Reads `request.formData()` and optionally validates it with a schema-like parser.
138
+ *
139
+ * Pass a schema object with `parse(FormData)` when a route handler or server action should receive typed form data.
140
+ */
118
141
  export async function parseForm(request: Request): Promise<FormData>;
119
142
  export async function parseForm<T>(request: Request, schema: ParseSchema<T>): Promise<T>;
120
143
  export async function parseForm<T>(
@@ -126,6 +149,11 @@ export async function parseForm<T>(
126
149
  return schema === undefined ? form : schema.parse(form);
127
150
  }
128
151
 
152
+ /**
153
+ * Throws a route-level 404 control error.
154
+ *
155
+ * Use this from loaders, metadata, middleware, or route handlers when the matched route is valid but the requested resource is absent.
156
+ */
129
157
  export function notFound(): never {
130
158
  throw Object.assign(new Error("Not Found"), {
131
159
  name: notFoundErrorName,
@@ -133,14 +161,25 @@ export function notFound(): never {
133
161
  });
134
162
  }
135
163
 
164
+ /**
165
+ * Alias for `notFound()` for codebases that prefer an explicit throwing helper name.
166
+ */
136
167
  export function throwNotFound(): never {
137
168
  return notFound();
138
169
  }
139
170
 
171
+ /**
172
+ * Continues middleware processing without changing the request.
173
+ */
140
174
  export function next(): MiddlewareNext {
141
175
  return undefined;
142
176
  }
143
177
 
178
+ /**
179
+ * Creates an internal rewrite response consumed by app-router middleware.
180
+ *
181
+ * The target must be a same-origin route path, query, hash, or relative URL; protocol-relative, external, and control-character targets are rejected.
182
+ */
144
183
  export function rewrite(location: string, init: ResponseInit = {}): Response {
145
184
  if (!isSafeInternalRedirect(location)) {
146
185
  throwUnsafeRewrite(location);
@@ -160,10 +199,16 @@ export function rewrite(location: string, init: ResponseInit = {}): Response {
160
199
  return response;
161
200
  }
162
201
 
202
+ /**
203
+ * Creates a JSON response using the platform `Response.json()` implementation.
204
+ */
163
205
  export function json(value: unknown, init?: ResponseInit): Response {
164
206
  return Response.json(value, init);
165
207
  }
166
208
 
209
+ /**
210
+ * Creates an HTML response and defaults the content type to `text/html; charset=utf-8`.
211
+ */
167
212
  export function html(value: string, init: ResponseInit = {}): Response {
168
213
  if (init.headers === undefined) {
169
214
  return new Response(value, {
@@ -184,6 +229,9 @@ export function html(value: string, init: ResponseInit = {}): Response {
184
229
  });
185
230
  }
186
231
 
232
+ /**
233
+ * Returns the request headers object for route code that follows app-router helper naming.
234
+ */
187
235
  export function headers(request: Request): Headers {
188
236
  return request.headers;
189
237
  }
@@ -194,6 +242,11 @@ export interface RequestCookies {
194
242
  has(name: string): boolean;
195
243
  }
196
244
 
245
+ /**
246
+ * Parses request cookies into a small read-only helper.
247
+ *
248
+ * Values are decoded with `decodeURIComponent`; malformed encoded values are ignored rather than throwing during request handling.
249
+ */
197
250
  export function cookies(request: Request): RequestCookies {
198
251
  let values: ReadonlyMap<string, string> | undefined;
199
252
  const cookieValues = () => {