@reckona/mreact-router 0.0.149 → 0.0.151

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.
@@ -1,7 +1,8 @@
1
1
  export type RouteSearchValue = boolean | number | string | null | undefined;
2
2
  export type RouteSearchParams = Record<string, RouteSearchValue | readonly RouteSearchValue[]>;
3
3
  export type RouteParamsFor<Path extends `/${string}`> = Simplify<ExtractRouteParams<Path>>;
4
- export type AppRouteHref<Path extends `/${string}`> = HasRouteParams<Path> extends true ? (options: DynamicHrefOptions<Path>) => string : (options?: StaticHrefOptions) => string;
4
+ export type AppRouteHref<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never ? (options?: StaticHrefOptions) => string : (options: DynamicHrefOptions<Path>) => string;
5
+ export type AppRouteLinkHref<Path extends `/${string}`> = `${AppRouteLinkPathname<Path>}${AppRouteLinkHrefSuffix}`;
5
6
  export interface StaticHrefOptions {
6
7
  hash?: string | undefined;
7
8
  search?: RouteSearchParams | undefined;
@@ -19,6 +20,10 @@ type SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Na
19
20
  type Simplify<T> = {
20
21
  [Key in keyof T]: T[Key];
21
22
  } & {};
23
+ export type AppRouteLinkHrefSuffix = "" | `?${string}` | `#${string}` | `?${string}#${string}`;
24
+ export type AppRouteLinkPathname<Path extends `/${string}`> = Path extends "/" ? "/" : Path extends `/${infer Segments}` ? `/${AppRouteLinkSegments<Segments>}` : never;
25
+ export type AppRouteLinkSegments<Segments extends string> = Segments extends `${infer Segment}/${infer Rest}` ? `${AppRouteLinkSegment<Segment>}/${AppRouteLinkSegments<Rest>}` : AppRouteLinkSegment<Segments>;
26
+ export type AppRouteLinkSegment<Segment extends string> = Segment extends `:${string}` ? string : Segment;
22
27
  export declare function href<const Path extends `/${string}`>(path: Path, ...args: HasRouteParams<Path> extends true ? [options: DynamicHrefOptions<Path>] : [options?: StaticHrefOptions]): string;
23
28
  export {};
24
29
  //# sourceMappingURL=typed-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typed-routes.d.ts","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAC5E,MAAM,MAAM,iBAAiB,GAAG,MAAM,CACpC,MAAM,EACN,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAC/C,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,QAAQ,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;AAE3F,MAAM,MAAM,YAAY,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC,SAAS,IAAI,GACnF,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,KAAK,MAAM,GAC7C,CAAC,OAAO,CAAC,EAAE,iBAAiB,KAAK,MAAM,CAAC;AAE5C,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,CAAE,SAAQ,iBAAiB;IACtF,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,KAAK,cAAc,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,MAAM,cAAc,CAAC,IAAI,CAAC,SAAS,KAAK,GACrF,KAAK,GACL,IAAI,CAAC;AAET,KAAK,kBAAkB,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,GAAG,MAAM,OAAO,IAAI,MAAM,IAAI,EAAE,GACxF,iBAAiB,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,GACrD,iBAAiB,CAAC,IAAI,CAAC,CAAC;AAE5B,KAAK,iBAAiB,CAAC,OAAO,SAAS,MAAM,IAAI,OAAO,SAAS,OAAO,MAAM,IAAI,EAAE,GAChF;KAAG,GAAG,IAAI,IAAI,GAAG,SAAS,MAAM,EAAE;CAAE,GACpC,OAAO,SAAS,IAAI,MAAM,IAAI,EAAE,GAC9B;KAAG,GAAG,IAAI,IAAI,GAAG,MAAM;CAAE,GACzB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAE3B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;CAAE,GAAG,EAAE,CAAC;AAErD,wBAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,EAClD,IAAI,EAAE,IAAI,EACV,GAAG,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,SAAS,IAAI,GACtC,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,GACnC,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC,GAChC,MAAM,CA0CR"}
1
+ {"version":3,"file":"typed-routes.d.ts","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAC5E,MAAM,MAAM,iBAAiB,GAAG,MAAM,CACpC,MAAM,EACN,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAC/C,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,QAAQ,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;AAE3F,MAAM,MAAM,YAAY,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,MAAM,cAAc,CAAC,IAAI,CAAC,SAAS,KAAK,GAC1F,CAAC,OAAO,CAAC,EAAE,iBAAiB,KAAK,MAAM,GACvC,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC;AAElD,MAAM,MAAM,gBAAgB,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IACpD,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,sBAAsB,EAAE,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,kBAAkB,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,CAAE,SAAQ,iBAAiB;IACtF,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,KAAK,cAAc,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,MAAM,cAAc,CAAC,IAAI,CAAC,SAAS,KAAK,GACrF,KAAK,GACL,IAAI,CAAC;AAET,KAAK,kBAAkB,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,GAAG,MAAM,OAAO,IAAI,MAAM,IAAI,EAAE,GACxF,iBAAiB,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,GACrD,iBAAiB,CAAC,IAAI,CAAC,CAAC;AAE5B,KAAK,iBAAiB,CAAC,OAAO,SAAS,MAAM,IAAI,OAAO,SAAS,OAAO,MAAM,IAAI,EAAE,GAChF;KAAG,GAAG,IAAI,IAAI,GAAG,SAAS,MAAM,EAAE;CAAE,GACpC,OAAO,SAAS,IAAI,MAAM,IAAI,EAAE,GAC9B;KAAG,GAAG,IAAI,IAAI,GAAG,MAAM;CAAE,GACzB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAE3B,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;CAAE,GAAG,EAAE,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;AAE/F,MAAM,MAAM,oBAAoB,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,IAAI,IAAI,SAAS,GAAG,GAC1E,GAAG,GACH,IAAI,SAAS,IAAI,MAAM,QAAQ,EAAE,GAC/B,IAAI,oBAAoB,CAAC,QAAQ,CAAC,EAAE,GACpC,KAAK,CAAC;AAEZ,MAAM,MAAM,oBAAoB,CAAC,QAAQ,SAAS,MAAM,IAAI,QAAQ,SAAS,GAAG,MAAM,OAAO,IAAI,MAAM,IAAI,EAAE,GACzG,GAAG,mBAAmB,CAAC,OAAO,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,EAAE,GAC/D,mBAAmB,CAAC,QAAQ,CAAC,CAAC;AAElC,MAAM,MAAM,mBAAmB,CAAC,OAAO,SAAS,MAAM,IAAI,OAAO,SAAS,IAAI,MAAM,EAAE,GAClF,MAAM,GACN,OAAO,CAAC;AAEZ,wBAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,IAAI,MAAM,EAAE,EAClD,IAAI,EAAE,IAAI,EACV,GAAG,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,SAAS,IAAI,GACtC,CAAC,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,GACnC,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC,GAChC,MAAM,CA0CR"}
@@ -1 +1 @@
1
- {"version":3,"file":"typed-routes.js","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":"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","sourcesContent":["export type RouteSearchValue = boolean | number | string | null | undefined;\nexport type RouteSearchParams = Record<\n string,\n RouteSearchValue | readonly RouteSearchValue[]\n>;\n\nexport type RouteParamsFor<Path extends `/${string}`> = Simplify<ExtractRouteParams<Path>>;\n\nexport type AppRouteHref<Path extends `/${string}`> = HasRouteParams<Path> extends true\n ? (options: DynamicHrefOptions<Path>) => string\n : (options?: StaticHrefOptions) => string;\n\nexport interface StaticHrefOptions {\n hash?: string | undefined;\n search?: RouteSearchParams | undefined;\n}\n\nexport interface DynamicHrefOptions<Path extends `/${string}`> extends StaticHrefOptions {\n params: RouteParamsFor<Path>;\n}\n\ntype HasRouteParams<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never\n ? false\n : true;\n\ntype ExtractRouteParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}`\n ? SegmentRouteParam<Segment> & ExtractRouteParams<Rest>\n : SegmentRouteParam<Path>;\n\ntype SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Name}`\n ? { [Key in Name]: readonly string[] }\n : Segment extends `:${infer Name}`\n ? { [Key in Name]: string }\n : Record<never, never>;\n\ntype Simplify<T> = { [Key in keyof T]: T[Key] } & {};\n\nexport function href<const Path extends `/${string}`>(\n path: Path,\n ...args: HasRouteParams<Path> extends true\n ? [options: DynamicHrefOptions<Path>]\n : [options?: StaticHrefOptions]\n): string {\n assertInternalRoutePath(path);\n\n const options = args[0] as\n | (StaticHrefOptions & { params?: Record<string, readonly string[] | string> | undefined })\n | undefined;\n const params =\n options !== undefined && \"params\" in options && options.params !== undefined\n ? (options.params as Record<string, readonly string[] | string>)\n : {};\n const pathname = path\n .split(\"/\")\n .map((segment) => {\n if (segment.startsWith(\":...\")) {\n const name = segment.slice(4);\n const value = params[name];\n\n if (!Array.isArray(value)) {\n throw new Error(`Missing catch-all route param ${JSON.stringify(name)} for ${path}.`);\n }\n\n return value.map((part) => encodeURIComponent(part)).join(\"/\");\n }\n\n if (segment.startsWith(\":\")) {\n const name = segment.slice(1);\n const value = params[name];\n\n if (typeof value !== \"string\") {\n throw new Error(`Missing route param ${JSON.stringify(name)} for ${path}.`);\n }\n\n return encodeURIComponent(value);\n }\n\n return segment;\n })\n .join(\"/\");\n const search = searchString(options?.search);\n const hash = options?.hash === undefined ? \"\" : `#${encodeURIComponent(options.hash)}`;\n\n return `${pathname}${search}${hash}`;\n}\n\nfunction assertInternalRoutePath(path: string): void {\n if (!path.startsWith(\"/\") || path.startsWith(\"//\")) {\n throw new Error(`href() expected an internal route path, received ${JSON.stringify(path)}.`);\n }\n\n for (let index = 0; index < path.length; index += 1) {\n const code = path.charCodeAt(index);\n\n if (code <= 0x1f || code === 0x7f) {\n throw new Error(\"href() route paths must not contain control characters.\");\n }\n }\n}\n\nfunction searchString(search: RouteSearchParams | undefined): string {\n if (search === undefined) {\n return \"\";\n }\n\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n const values = Array.isArray(value) ? value : [value];\n\n for (const entry of values) {\n if (entry !== undefined && entry !== null) {\n params.append(key, String(entry));\n }\n }\n }\n\n const value = params.toString();\n\n return value === \"\" ? \"\" : `?${value}`;\n}\n"]}
1
+ {"version":3,"file":"typed-routes.js","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":"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","sourcesContent":["export type RouteSearchValue = boolean | number | string | null | undefined;\nexport type RouteSearchParams = Record<\n string,\n RouteSearchValue | readonly RouteSearchValue[]\n>;\n\nexport type RouteParamsFor<Path extends `/${string}`> = Simplify<ExtractRouteParams<Path>>;\n\nexport type AppRouteHref<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never\n ? (options?: StaticHrefOptions) => string\n : (options: DynamicHrefOptions<Path>) => string;\n\nexport type AppRouteLinkHref<Path extends `/${string}`> =\n `${AppRouteLinkPathname<Path>}${AppRouteLinkHrefSuffix}`;\n\nexport interface StaticHrefOptions {\n hash?: string | undefined;\n search?: RouteSearchParams | undefined;\n}\n\nexport interface DynamicHrefOptions<Path extends `/${string}`> extends StaticHrefOptions {\n params: RouteParamsFor<Path>;\n}\n\ntype HasRouteParams<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never\n ? false\n : true;\n\ntype ExtractRouteParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}`\n ? SegmentRouteParam<Segment> & ExtractRouteParams<Rest>\n : SegmentRouteParam<Path>;\n\ntype SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Name}`\n ? { [Key in Name]: readonly string[] }\n : Segment extends `:${infer Name}`\n ? { [Key in Name]: string }\n : Record<never, never>;\n\ntype Simplify<T> = { [Key in keyof T]: T[Key] } & {};\n\nexport type AppRouteLinkHrefSuffix = \"\" | `?${string}` | `#${string}` | `?${string}#${string}`;\n\nexport type AppRouteLinkPathname<Path extends `/${string}`> = Path extends \"/\"\n ? \"/\"\n : Path extends `/${infer Segments}`\n ? `/${AppRouteLinkSegments<Segments>}`\n : never;\n\nexport type AppRouteLinkSegments<Segments extends string> = Segments extends `${infer Segment}/${infer Rest}`\n ? `${AppRouteLinkSegment<Segment>}/${AppRouteLinkSegments<Rest>}`\n : AppRouteLinkSegment<Segments>;\n\nexport type AppRouteLinkSegment<Segment extends string> = Segment extends `:${string}`\n ? string\n : Segment;\n\nexport function href<const Path extends `/${string}`>(\n path: Path,\n ...args: HasRouteParams<Path> extends true\n ? [options: DynamicHrefOptions<Path>]\n : [options?: StaticHrefOptions]\n): string {\n assertInternalRoutePath(path);\n\n const options = args[0] as\n | (StaticHrefOptions & { params?: Record<string, readonly string[] | string> | undefined })\n | undefined;\n const params =\n options !== undefined && \"params\" in options && options.params !== undefined\n ? (options.params as Record<string, readonly string[] | string>)\n : {};\n const pathname = path\n .split(\"/\")\n .map((segment) => {\n if (segment.startsWith(\":...\")) {\n const name = segment.slice(4);\n const value = params[name];\n\n if (!Array.isArray(value)) {\n throw new Error(`Missing catch-all route param ${JSON.stringify(name)} for ${path}.`);\n }\n\n return value.map((part) => encodeURIComponent(part)).join(\"/\");\n }\n\n if (segment.startsWith(\":\")) {\n const name = segment.slice(1);\n const value = params[name];\n\n if (typeof value !== \"string\") {\n throw new Error(`Missing route param ${JSON.stringify(name)} for ${path}.`);\n }\n\n return encodeURIComponent(value);\n }\n\n return segment;\n })\n .join(\"/\");\n const search = searchString(options?.search);\n const hash = options?.hash === undefined ? \"\" : `#${encodeURIComponent(options.hash)}`;\n\n return `${pathname}${search}${hash}`;\n}\n\nfunction assertInternalRoutePath(path: string): void {\n if (!path.startsWith(\"/\") || path.startsWith(\"//\")) {\n throw new Error(`href() expected an internal route path, received ${JSON.stringify(path)}.`);\n }\n\n for (let index = 0; index < path.length; index += 1) {\n const code = path.charCodeAt(index);\n\n if (code <= 0x1f || code === 0x7f) {\n throw new Error(\"href() route paths must not contain control characters.\");\n }\n }\n}\n\nfunction searchString(search: RouteSearchParams | undefined): string {\n if (search === undefined) {\n return \"\";\n }\n\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n const values = Array.isArray(value) ? value : [value];\n\n for (const entry of values) {\n if (entry !== undefined && entry !== null) {\n params.append(key, String(entry));\n }\n }\n }\n\n const value = params.toString();\n\n return value === \"\" ? \"\" : `?${value}`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reckona/mreact-router",
3
- "version": "0.0.149",
3
+ "version": "0.0.151",
4
4
  "description": "File-system app router, SSR, actions, and deployment adapters for mreact.",
5
5
  "keywords": [
6
6
  "jsx",
@@ -105,20 +105,20 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "typescript": "^6.0.3",
108
- "@reckona/mreact": "0.0.149",
109
- "@reckona/mreact-compiler": "0.0.149",
110
- "@reckona/mreact-query": "0.0.149",
111
- "@reckona/mreact-compat": "0.0.149",
112
- "@reckona/mreact-devtools": "0.0.149",
113
- "@reckona/mreact-reactive-core": "0.0.149",
114
- "@reckona/mreact-server": "0.0.149",
115
- "@reckona/mreact-shared": "0.0.149",
116
- "@reckona/mreact-reactive-dom": "0.0.149"
108
+ "@reckona/mreact": "0.0.151",
109
+ "@reckona/mreact-compat": "0.0.151",
110
+ "@reckona/mreact-devtools": "0.0.151",
111
+ "@reckona/mreact-reactive-core": "0.0.151",
112
+ "@reckona/mreact-query": "0.0.151",
113
+ "@reckona/mreact-compiler": "0.0.151",
114
+ "@reckona/mreact-reactive-dom": "0.0.151",
115
+ "@reckona/mreact-shared": "0.0.151",
116
+ "@reckona/mreact-server": "0.0.151"
117
117
  },
118
118
  "peerDependencies": {
119
119
  "vite": ">=8 <9"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@reckona/mreact-router-native": "0.0.149"
122
+ "@reckona/mreact-router-native": "0.0.151"
123
123
  }
124
124
  }
package/src/actions.ts CHANGED
@@ -286,6 +286,11 @@ export async function dispatchServerActionRequest(options: {
286
286
  appDir: string;
287
287
  importPolicy?: AppRouterImportPolicy | undefined;
288
288
  request: Request;
289
+ renderSingleFlightNavigation?: ((options: {
290
+ path: string;
291
+ request: Request;
292
+ revalidatedPaths: readonly string[];
293
+ }) => Promise<Response | undefined>) | undefined;
289
294
  routeCache?: AppRouterCache | undefined;
290
295
  serverActionCacheVersion?: string | undefined;
291
296
  serverActions?: AppRouterServerActionOptions | undefined;
@@ -293,8 +298,14 @@ export async function dispatchServerActionRequest(options: {
293
298
  const { revalidatedPaths, value } = await withRouteCacheContext(options.routeCache, () =>
294
299
  dispatchServerActionRequestWithoutCacheContext(options),
295
300
  );
301
+ const singleFlight = await renderSingleFlightActionResponse({
302
+ render: options.renderSingleFlightNavigation,
303
+ request: options.request,
304
+ response: value,
305
+ revalidatedPaths,
306
+ });
296
307
 
297
- return withRevalidationHeader(value, revalidatedPaths);
308
+ return withRevalidationHeader(singleFlight ?? value, revalidatedPaths);
298
309
  }
299
310
 
300
311
  async function dispatchServerActionRequestWithoutCacheContext(options: {
@@ -568,6 +579,70 @@ function redirectToFormReferer(request: Request): Response {
568
579
  });
569
580
  }
570
581
 
582
+ async function renderSingleFlightActionResponse(options: {
583
+ render:
584
+ | ((options: {
585
+ path: string;
586
+ request: Request;
587
+ revalidatedPaths: readonly string[];
588
+ }) => Promise<Response | undefined>)
589
+ | undefined;
590
+ request: Request;
591
+ response: Response;
592
+ revalidatedPaths: readonly string[];
593
+ }): Promise<Response | undefined> {
594
+ if (
595
+ options.render === undefined ||
596
+ options.revalidatedPaths.length === 0 ||
597
+ options.request.headers.get("x-mreact-action-single-flight") !== "1" ||
598
+ !isFormActionRedirectResponse(options.response)
599
+ ) {
600
+ return undefined;
601
+ }
602
+
603
+ const path = sameOriginResponseLocationPath(options.request, options.response);
604
+
605
+ if (path === undefined || !options.revalidatedPaths.includes(normalizeActionPath(path))) {
606
+ return undefined;
607
+ }
608
+
609
+ const rendered = await options.render({
610
+ path,
611
+ request: options.request,
612
+ revalidatedPaths: options.revalidatedPaths,
613
+ });
614
+
615
+ if (rendered === undefined) {
616
+ return undefined;
617
+ }
618
+
619
+ rendered.headers.set("x-mreact-action-single-flight", "1");
620
+ return rendered;
621
+ }
622
+
623
+ function isFormActionRedirectResponse(response: Response): boolean {
624
+ return response.status >= 300 && response.status < 400 && response.headers.has("location");
625
+ }
626
+
627
+ function sameOriginResponseLocationPath(request: Request, response: Response): string | undefined {
628
+ const location = response.headers.get("location");
629
+
630
+ if (location === null) {
631
+ return undefined;
632
+ }
633
+
634
+ try {
635
+ const requestUrl = new URL(request.url);
636
+ const locationUrl = new URL(location, requestUrl);
637
+
638
+ return locationUrl.origin === requestUrl.origin
639
+ ? `${locationUrl.pathname}${locationUrl.search}`
640
+ : undefined;
641
+ } catch {
642
+ return undefined;
643
+ }
644
+ }
645
+
571
646
  function sameOriginRefererPath(request: Request): string | undefined {
572
647
  const referer = request.headers.get("referer");
573
648
 
@@ -587,6 +662,13 @@ function sameOriginRefererPath(request: Request): string | undefined {
587
662
  }
588
663
  }
589
664
 
665
+ function normalizeActionPath(path: string): string {
666
+ const pathname = path.startsWith("/") ? path : `/${path}`;
667
+ const withoutTrailing = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
668
+
669
+ return withoutTrailing === "" ? "/" : withoutTrailing;
670
+ }
671
+
590
672
  function withRevalidationHeader(response: Response, paths: string[]): Response {
591
673
  if (paths.length > 0) {
592
674
  response.headers.set("x-mreact-revalidate", paths.join(","));
package/src/build.ts CHANGED
@@ -744,17 +744,17 @@ function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
744
744
  return left.localeCompare(right);
745
745
  });
746
746
  const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
747
- const routesObject = routePaths
748
- .map((routePath) => ` readonly ${JSON.stringify(routePath)}: AppRouteHref<${JSON.stringify(routePath)}>;`)
749
- .join("\n");
750
-
751
747
  return [
752
- `import type { AppRouteHref } from "@reckona/mreact-router";`,
748
+ `import type { AppRouteLinkHref as MreactAppRouteLinkHref } from "@reckona/mreact-router";`,
753
749
  ``,
754
750
  `export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
755
- `export declare const routes: {`,
756
- routesObject,
757
- `};`,
751
+ `export type AppRouteHref = MreactAppRouteLinkHref<AppRoutePath>;`,
752
+ ``,
753
+ `declare module "@reckona/mreact-router/link" {`,
754
+ ` interface AppRouteDeclarations {`,
755
+ ` readonly path: AppRoutePath;`,
756
+ ` }`,
757
+ `}`,
758
758
  ``,
759
759
  ].join("\n");
760
760
  }
package/src/client.ts CHANGED
@@ -3512,6 +3512,22 @@ function __mreactInstallNavigation() {
3512
3512
  }
3513
3513
  });
3514
3514
  __mreactObserveViewportPrefetchAnchors(document);
3515
+ document.addEventListener("submit", (event) => {
3516
+ if (event.defaultPrevented || typeof fetch !== "function") {
3517
+ return;
3518
+ }
3519
+
3520
+ const submission = __mreactServerActionSubmissionFromEvent(event);
3521
+
3522
+ if (submission === null) {
3523
+ return;
3524
+ }
3525
+
3526
+ event.preventDefault();
3527
+ void __mreactSubmitServerActionForm(submission).catch(() => {
3528
+ location.href = submission.action;
3529
+ });
3530
+ });
3515
3531
  document.addEventListener("click", (event) => {
3516
3532
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
3517
3533
  return;
@@ -3562,6 +3578,100 @@ function __mreactInstallNavigation() {
3562
3578
  });
3563
3579
  }
3564
3580
 
3581
+ function __mreactServerActionSubmissionFromEvent(event) {
3582
+ const target = event.target;
3583
+ const form = target instanceof Element ? target.closest("form") : null;
3584
+ const submitter = __mreactFormSubmitterFromEvent(event);
3585
+
3586
+ if (!(form instanceof HTMLFormElement)) {
3587
+ return null;
3588
+ }
3589
+
3590
+ const action = submitter?.getAttribute("formaction") ?? form.action;
3591
+ const method = submitter?.getAttribute("formmethod") ?? form.method;
3592
+ const targetName = submitter?.getAttribute("formtarget") ?? form.target;
3593
+ const actionUrl = new URL(action, location.href);
3594
+
3595
+ if (
3596
+ targetName !== "" ||
3597
+ actionUrl.origin !== location.origin ||
3598
+ actionUrl.pathname !== "/_mreact/actions" ||
3599
+ method.toUpperCase() !== "POST"
3600
+ ) {
3601
+ return null;
3602
+ }
3603
+
3604
+ return { action: actionUrl.href, form, submitter };
3605
+ }
3606
+
3607
+ function __mreactFormSubmitterFromEvent(event) {
3608
+ const submitter = event.submitter;
3609
+
3610
+ return submitter instanceof HTMLElement ? submitter : null;
3611
+ }
3612
+
3613
+ async function __mreactSubmitServerActionForm(submission) {
3614
+ const response = await fetch(submission.action, {
3615
+ body: __mreactServerActionFormData(submission.form, submission.submitter),
3616
+ headers: { "x-mreact-action-single-flight": "1" },
3617
+ method: "POST",
3618
+ });
3619
+ __mreactApplyRevalidationHeader(response);
3620
+
3621
+ const contentType = response.headers.get("content-type") ?? "";
3622
+ const singleFlight = response.headers.get("x-mreact-action-single-flight") === "1";
3623
+ if (
3624
+ !singleFlight &&
3625
+ !contentType.includes("text/html")
3626
+ ) {
3627
+ location.href = response.url || submission.action;
3628
+ return;
3629
+ }
3630
+
3631
+ const html = await response.text();
3632
+ const applied = __mreactApplyServerActionHtml(html, singleFlight);
3633
+
3634
+ if (!applied) {
3635
+ location.href = response.url || submission.action;
3636
+ }
3637
+ }
3638
+
3639
+ function __mreactServerActionFormData(form, submitter) {
3640
+ if (submitter !== null) {
3641
+ try {
3642
+ return new FormData(form, submitter);
3643
+ } catch {
3644
+ // Older DOM implementations only accept the form argument.
3645
+ }
3646
+ }
3647
+
3648
+ return new FormData(form);
3649
+ }
3650
+
3651
+ function __mreactApplyServerActionHtml(html, singleFlight) {
3652
+ __mreactSaveCurrentHistoryState();
3653
+ const applied = __mreactApplyNavigationHtml(html, location.href);
3654
+
3655
+ if (!applied) {
3656
+ if (
3657
+ !singleFlight ||
3658
+ typeof document === "undefined" ||
3659
+ typeof document.open !== "function"
3660
+ ) {
3661
+ return false;
3662
+ }
3663
+
3664
+ document.open();
3665
+ document.write(html);
3666
+ document.close();
3667
+ return true;
3668
+ }
3669
+
3670
+ __mreactResetNavigationFocus();
3671
+ __mreactAnnounceNavigation();
3672
+ return true;
3673
+ }
3674
+
3565
3675
  function __mreactInstallNavigationFetchRevalidation() {
3566
3676
  if (
3567
3677
  __mreactNavigationState.fetchRevalidationInstalled ||
package/src/index.ts CHANGED
@@ -87,6 +87,11 @@ export type {
87
87
  export type { ServerActionContext } from "./actions.js";
88
88
  export type {
89
89
  AppRouteHref,
90
+ AppRouteLinkHref,
91
+ AppRouteLinkHrefSuffix,
92
+ AppRouteLinkPathname,
93
+ AppRouteLinkSegment,
94
+ AppRouteLinkSegments,
90
95
  DynamicHrefOptions,
91
96
  RouteParamsFor,
92
97
  RouteSearchParams,
@@ -144,10 +149,14 @@ export type {
144
149
  export type { CookieOptions } from "./cookies.js";
145
150
  export type { AppRouterImportPolicy } from "./import-policy.js";
146
151
  export type {
152
+ AppRouteDeclarations,
153
+ ConcreteLinkHrefGuard,
147
154
  LinkChild,
155
+ LinkHref,
148
156
  LinkOptions,
149
157
  LinkPrefetch,
150
158
  LinkProps,
159
+ RegisteredAppRoutePath,
151
160
  LinkScroll,
152
161
  LinkTransition,
153
162
  TrustedLinkHtml,
package/src/link.ts CHANGED
@@ -2,6 +2,14 @@ import { escapeHtmlAttribute, escapeHtmlText } from "@reckona/mreact-shared/html
2
2
  import type { HtmlSink } from "@reckona/mreact-shared/compiler-contract";
3
3
  import type { ReactCompatElement, ReactCompatNode } from "@reckona/mreact-compat";
4
4
  import { safeUrlAttributeValue } from "@reckona/mreact-shared/url-safety";
5
+ import type { AppRouteLinkHref } from "./typed-routes.js";
6
+ export type {
7
+ AppRouteLinkHref,
8
+ AppRouteLinkHrefSuffix,
9
+ AppRouteLinkPathname,
10
+ AppRouteLinkSegment,
11
+ AppRouteLinkSegments,
12
+ } from "./typed-routes.js";
5
13
 
6
14
  const TRUSTED_LINK_HTML = Symbol.for("modular.react.router.trusted_link_html");
7
15
 
@@ -10,21 +18,28 @@ export type LinkScroll = "top" | "preserve";
10
18
  export type LinkTransition = "auto" | "none" | false;
11
19
  export type TrustedLinkHtml = { readonly [TRUSTED_LINK_HTML]: string };
12
20
  export type LinkChild = ReactCompatNode | Node | TrustedLinkHtml | readonly LinkChild[];
13
-
14
- export interface LinkOptions {
15
- href: string;
21
+ export interface AppRouteDeclarations {}
22
+ export type RegisteredAppRoutePath = AppRouteDeclarations extends { readonly path: infer Path }
23
+ ? Extract<Path, `/${string}`>
24
+ : never;
25
+ export type LinkHref = [RegisteredAppRoutePath] extends [never]
26
+ ? string
27
+ : AppRouteLinkHref<RegisteredAppRoutePath>;
28
+
29
+ export interface LinkOptions<Href extends string = LinkHref> {
30
+ href: Href;
16
31
  prefetch?: LinkPrefetch | undefined;
17
32
  reload?: boolean | undefined;
18
33
  scroll?: LinkScroll | undefined;
19
34
  transition?: LinkTransition | undefined;
20
35
  }
21
36
 
22
- export interface LinkProps extends LinkOptions {
37
+ export interface LinkProps<Href extends string = LinkHref> extends LinkOptions<Href> {
23
38
  children?: LinkChild;
24
39
  [attribute: string]: unknown;
25
40
  }
26
41
 
27
- export function linkProps(options: LinkOptions): Record<string, string> {
42
+ export function linkProps(options: LinkOptions<string>): Record<string, string> {
28
43
  return {
29
44
  href: options.href,
30
45
  ...(options.prefetch === undefined || options.prefetch === "intent"
@@ -40,11 +55,19 @@ export function linkProps(options: LinkOptions): Record<string, string> {
40
55
  };
41
56
  }
42
57
 
43
- export function Link(props: LinkProps): ReactCompatElement;
44
- export function Link(sink: HtmlSink, props: LinkProps): void;
58
+ export type ConcreteLinkHrefGuard<Href extends string> = [RegisteredAppRoutePath] extends [never]
59
+ ? unknown
60
+ : Href extends Extract<RegisteredAppRoutePath, `${string}:${string}`>
61
+ ? { readonly __mreactRoutePatternHrefError__: never }
62
+ : unknown;
63
+
64
+ export function Link<const Href extends LinkHref>(
65
+ props: LinkProps<Href> & ConcreteLinkHrefGuard<Href>,
66
+ ): ReactCompatElement;
67
+ export function Link(sink: HtmlSink, props: LinkProps<string>): void;
45
68
  export function Link(
46
- sinkOrProps: HtmlSink | LinkProps,
47
- maybeProps?: LinkProps,
69
+ sinkOrProps: HtmlSink | LinkProps<string>,
70
+ maybeProps?: LinkProps<string>,
48
71
  ): ReactCompatElement | string | HTMLAnchorElement | void {
49
72
  if (maybeProps !== undefined) {
50
73
  (sinkOrProps as HtmlSink).append(renderLinkString(maybeProps));
@@ -60,7 +83,7 @@ export function Link(
60
83
  [TRUSTED_LINK_HTML]: html,
61
84
  });
62
85
 
63
- function renderLink(props: LinkProps): string | HTMLAnchorElement {
86
+ function renderLink(props: LinkProps<string>): string | HTMLAnchorElement {
64
87
  const { href, prefetch, reload, scroll, transition, ...rest } = props;
65
88
  const propsWithLinkAttrs = {
66
89
  ...rest,
@@ -74,7 +97,7 @@ function renderLink(props: LinkProps): string | HTMLAnchorElement {
74
97
  return renderAnchorString(propsWithLinkAttrs);
75
98
  }
76
99
 
77
- function renderLinkString(props: LinkProps): string {
100
+ function renderLinkString(props: LinkProps<string>): string {
78
101
  const { href, prefetch, reload, scroll, transition, ...rest } = props;
79
102
 
80
103
  return renderAnchorString({
package/src/render.ts CHANGED
@@ -812,6 +812,16 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
812
812
  appDir: options.appDir,
813
813
  importPolicy: options.importPolicy,
814
814
  request: options.request,
815
+ renderSingleFlightNavigation: async (singleFlight) =>
816
+ renderAppRequestInternal({
817
+ ...options,
818
+ matchedRoute: undefined,
819
+ request: singleFlightNavigationRequest({
820
+ path: singleFlight.path,
821
+ request: singleFlight.request,
822
+ }),
823
+ requestUrl: undefined,
824
+ }),
815
825
  routeCache: options.routeCache,
816
826
  ...(options.serverModuleCacheVersion === undefined
817
827
  ? {}
@@ -1672,6 +1682,25 @@ function isNavigationRouteCacheReloadRequest(request: Request): boolean {
1672
1682
  );
1673
1683
  }
1674
1684
 
1685
+ function singleFlightNavigationRequest(options: { path: string; request: Request }): Request {
1686
+ const actionUrl = new URL(options.request.url);
1687
+ const targetUrl = new URL(options.path, actionUrl);
1688
+ const headers = new Headers();
1689
+ const cookie = options.request.headers.get("cookie");
1690
+
1691
+ if (cookie !== null) {
1692
+ headers.set("cookie", cookie);
1693
+ }
1694
+
1695
+ headers.set("x-mreact-navigation", "1");
1696
+ headers.set("x-mreact-navigation-cache", "reload");
1697
+
1698
+ return new Request(targetUrl, {
1699
+ headers,
1700
+ method: "GET",
1701
+ });
1702
+ }
1703
+
1675
1704
  async function nearestBoundaryFileForPage(options: {
1676
1705
  appDir: string;
1677
1706
  filename: string;
@@ -6,9 +6,12 @@ export type RouteSearchParams = Record<
6
6
 
7
7
  export type RouteParamsFor<Path extends `/${string}`> = Simplify<ExtractRouteParams<Path>>;
8
8
 
9
- export type AppRouteHref<Path extends `/${string}`> = HasRouteParams<Path> extends true
10
- ? (options: DynamicHrefOptions<Path>) => string
11
- : (options?: StaticHrefOptions) => string;
9
+ export type AppRouteHref<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never
10
+ ? (options?: StaticHrefOptions) => string
11
+ : (options: DynamicHrefOptions<Path>) => string;
12
+
13
+ export type AppRouteLinkHref<Path extends `/${string}`> =
14
+ `${AppRouteLinkPathname<Path>}${AppRouteLinkHrefSuffix}`;
12
15
 
13
16
  export interface StaticHrefOptions {
14
17
  hash?: string | undefined;
@@ -35,6 +38,22 @@ type SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Na
35
38
 
36
39
  type Simplify<T> = { [Key in keyof T]: T[Key] } & {};
37
40
 
41
+ export type AppRouteLinkHrefSuffix = "" | `?${string}` | `#${string}` | `?${string}#${string}`;
42
+
43
+ export type AppRouteLinkPathname<Path extends `/${string}`> = Path extends "/"
44
+ ? "/"
45
+ : Path extends `/${infer Segments}`
46
+ ? `/${AppRouteLinkSegments<Segments>}`
47
+ : never;
48
+
49
+ export type AppRouteLinkSegments<Segments extends string> = Segments extends `${infer Segment}/${infer Rest}`
50
+ ? `${AppRouteLinkSegment<Segment>}/${AppRouteLinkSegments<Rest>}`
51
+ : AppRouteLinkSegment<Segments>;
52
+
53
+ export type AppRouteLinkSegment<Segment extends string> = Segment extends `:${string}`
54
+ ? string
55
+ : Segment;
56
+
38
57
  export function href<const Path extends `/${string}`>(
39
58
  path: Path,
40
59
  ...args: HasRouteParams<Path> extends true