@reckona/mreact-router 0.0.131 → 0.0.134

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 (48) hide show
  1. package/dist/adapters/cloudflare.js +1 -1
  2. package/dist/adapters/cloudflare.js.map +1 -1
  3. package/dist/adapters/static.js +5 -1
  4. package/dist/adapters/static.js.map +1 -1
  5. package/dist/build.d.ts +1 -0
  6. package/dist/build.d.ts.map +1 -1
  7. package/dist/build.js +136 -41
  8. package/dist/build.js.map +1 -1
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +188 -6
  11. package/dist/client.js.map +1 -1
  12. package/dist/http.d.ts +1 -0
  13. package/dist/http.d.ts.map +1 -1
  14. package/dist/http.js +7 -1
  15. package/dist/http.js.map +1 -1
  16. package/dist/module-runner.d.ts +4 -1
  17. package/dist/module-runner.d.ts.map +1 -1
  18. package/dist/module-runner.js +3 -2
  19. package/dist/module-runner.js.map +1 -1
  20. package/dist/render.d.ts +6 -1
  21. package/dist/render.d.ts.map +1 -1
  22. package/dist/render.js +84 -46
  23. package/dist/render.js.map +1 -1
  24. package/dist/routes.js +1 -1
  25. package/dist/routes.js.map +1 -1
  26. package/dist/serve.d.ts.map +1 -1
  27. package/dist/serve.js +110 -17
  28. package/dist/serve.js.map +1 -1
  29. package/dist/vite-plugin-cache-key.d.ts +2 -1
  30. package/dist/vite-plugin-cache-key.d.ts.map +1 -1
  31. package/dist/vite-plugin-cache-key.js +9 -0
  32. package/dist/vite-plugin-cache-key.js.map +1 -1
  33. package/dist/vite.d.ts +2 -1
  34. package/dist/vite.d.ts.map +1 -1
  35. package/dist/vite.js +2 -0
  36. package/dist/vite.js.map +1 -1
  37. package/package.json +11 -11
  38. package/src/adapters/cloudflare.ts +1 -1
  39. package/src/adapters/static.ts +7 -1
  40. package/src/build.ts +201 -53
  41. package/src/client.ts +329 -8
  42. package/src/http.ts +9 -1
  43. package/src/module-runner.ts +8 -2
  44. package/src/render.ts +117 -21
  45. package/src/routes.ts +1 -1
  46. package/src/serve.ts +167 -18
  47. package/src/vite-plugin-cache-key.ts +12 -1
  48. package/src/vite.ts +4 -0
package/src/routes.ts CHANGED
@@ -262,7 +262,7 @@ async function collectRouteFiles(directory: string, rootDirectory = directory):
262
262
  }
263
263
 
264
264
  function shouldSkipRouteScanDirectory(name: string): boolean {
265
- return name === ".vite" || name === "node_modules";
265
+ return name === ".vite" || name === "__tests__" || name === "node_modules";
266
266
  }
267
267
 
268
268
  function appFileConventionForRelativeFile(
package/src/serve.ts CHANGED
@@ -21,7 +21,13 @@ import {
21
21
  type RenderAppRequestOptions,
22
22
  } from "./render.js";
23
23
  import type { RouterInstrumentation } from "./trace.js";
24
- import { bytesResponse, htmlResponse, nodeRequestToWebRequest, sendResponse } from "./http.js";
24
+ import {
25
+ bytesResponse,
26
+ htmlResponse,
27
+ nodeRequestToWebRequest,
28
+ rawNodeRequestUrl,
29
+ sendResponse,
30
+ } from "./http.js";
25
31
  import {
26
32
  emitRouterLog,
27
33
  logDurationMs,
@@ -40,6 +46,7 @@ interface BuiltRuntime {
40
46
  appDir: string;
41
47
  allowedSourceDirs: readonly string[];
42
48
  assetBaseUrl?: string | undefined;
49
+ clientAssetPaths: ReadonlySet<string>;
43
50
  clientScripts: ReadonlyMap<string, string>;
44
51
  clientStylesByFile: ReadonlyMap<string, readonly string[]>;
45
52
  clientStyles: ReadonlyMap<string, readonly string[]>;
@@ -373,8 +380,14 @@ async function renderBuiltAppRequestWithRuntime(
373
380
  const url = new URL(options.request.url);
374
381
  const timing = createBuiltRenderTiming(options.logger);
375
382
 
376
- if (url.pathname.startsWith("/_mreact/client/")) {
377
- return readBuiltClientAsset(options.outDir, url.pathname);
383
+ const clientAssetPathname = builtClientAssetPathname(options.request, url);
384
+
385
+ if (clientAssetPathname !== undefined) {
386
+ return readBuiltClientAsset(
387
+ options.outDir,
388
+ clientAssetPathname,
389
+ options.runtime.clientAssetPaths,
390
+ );
378
391
  }
379
392
 
380
393
  if (options.request.method === "GET" || options.request.method === "HEAD") {
@@ -650,12 +663,15 @@ export async function startServer(
650
663
  };
651
664
  }
652
665
 
653
- async function readBuiltClientAsset(outDir: string, pathname: string): Promise<Response> {
666
+ async function readBuiltClientAsset(
667
+ outDir: string,
668
+ pathname: string,
669
+ allowedPaths: ReadonlySet<string>,
670
+ ): Promise<Response> {
654
671
  const clientPrefix = "/_mreact/client/";
655
- const relativePath = pathname.slice(clientPrefix.length);
656
- const normalized = normalize(relativePath);
672
+ const normalized = safeBuiltClientAssetPath(pathname.slice(clientPrefix.length));
657
673
 
658
- if (normalized.startsWith("..")) {
674
+ if (normalized === undefined || !allowedPaths.has(normalized)) {
659
675
  return new Response("Not Found", { status: 404 });
660
676
  }
661
677
 
@@ -670,6 +686,86 @@ async function readBuiltClientAsset(outDir: string, pathname: string): Promise<R
670
686
  }
671
687
  }
672
688
 
689
+ function builtClientAssetPathname(request: Request, url: URL): string | undefined {
690
+ const rawUrl = rawNodeRequestUrl(request);
691
+ const rawPathname = rawUrl?.split(/[?#]/, 1)[0];
692
+ const pathname = rawPathname === undefined || rawPathname === "" ? url.pathname : rawPathname;
693
+ const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
694
+
695
+ return normalizedPathname.startsWith("/_mreact/client/") ? normalizedPathname : undefined;
696
+ }
697
+
698
+ function safeBuiltClientAssetPath(relativePath: string): string | undefined {
699
+ let decoded: string;
700
+
701
+ try {
702
+ decoded = decodeURIComponent(relativePath);
703
+ } catch {
704
+ return undefined;
705
+ }
706
+
707
+ if (decoded.includes("\\") || decoded.includes("\0")) {
708
+ return undefined;
709
+ }
710
+
711
+ if (decoded.split("/").some((segment) => segment === "..")) {
712
+ return undefined;
713
+ }
714
+
715
+ const normalized = normalize(decoded);
716
+
717
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${sep}`)) {
718
+ return undefined;
719
+ }
720
+
721
+ return normalized;
722
+ }
723
+
724
+ function builtClientAssetPaths(manifest: {
725
+ assets?: readonly string[] | undefined;
726
+ routes: readonly ClientRouteManifestEntry[];
727
+ }): ReadonlySet<string> {
728
+ const paths = new Set<string>(["manifest.json"]);
729
+
730
+ for (const route of manifest.routes) {
731
+ for (const asset of [
732
+ route.script,
733
+ route.sourceMap,
734
+ route.navigationScript,
735
+ ...(route.css ?? []),
736
+ ...(route.imports ?? []),
737
+ ]) {
738
+ const path = safeClientManifestAssetPath(asset);
739
+
740
+ if (path !== undefined) {
741
+ paths.add(path);
742
+ }
743
+ }
744
+ }
745
+
746
+ for (const asset of manifest.assets ?? []) {
747
+ const path = safeClientManifestAssetPath(asset);
748
+
749
+ if (path !== undefined) {
750
+ paths.add(path);
751
+ }
752
+ }
753
+
754
+ return paths;
755
+ }
756
+
757
+ function safeClientManifestAssetPath(asset: string | undefined): string | undefined {
758
+ if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
759
+ return undefined;
760
+ }
761
+
762
+ const segments = asset.split("/");
763
+
764
+ return segments.some((segment) => segment === "" || segment === "." || segment === "..")
765
+ ? undefined
766
+ : segments.join("/");
767
+ }
768
+
673
769
  async function readBuiltPublicAsset(
674
770
  outDir: string,
675
771
  pathname: string,
@@ -734,9 +830,12 @@ async function readBuiltRuntime(options: {
734
830
  return cached.runtime;
735
831
  }
736
832
 
833
+ const serverManifestPath = join(outDir, "server", "manifest.json");
834
+ const clientManifestPath = join(outDir, "client", "manifest.json");
835
+ const importPolicyPath = join(outDir, "server", "import-policy.json");
737
836
  const [serverManifestText, clientManifestText, importPolicyText] = await Promise.all([
738
- readFile(join(outDir, "server", "manifest.json"), "utf8"),
739
- readFile(join(outDir, "client", "manifest.json"), "utf8"),
837
+ readRequiredBuiltArtifactText(serverManifestPath, "built app server manifest"),
838
+ readRequiredBuiltArtifactText(clientManifestPath, "built app client manifest"),
740
839
  readBuiltImportPolicyText(outDir),
741
840
  ]);
742
841
 
@@ -751,10 +850,13 @@ async function readBuiltRuntime(options: {
751
850
 
752
851
  const runtime = materializeBuiltRuntime({
753
852
  clientManifestText,
853
+ clientManifestPath,
754
854
  importPolicyText,
855
+ importPolicyPath,
755
856
  outDir,
756
857
  runtimeDir,
757
858
  serverManifestText,
859
+ serverManifestPath,
758
860
  });
759
861
 
760
862
  builtRuntimeCache.set(cacheKey, {
@@ -769,16 +871,24 @@ async function readBuiltRuntime(options: {
769
871
 
770
872
  async function materializeBuiltRuntime(options: {
771
873
  clientManifestText: string;
874
+ clientManifestPath: string;
875
+ importPolicyPath: string;
772
876
  importPolicyText: string | undefined;
773
877
  outDir: string;
774
878
  runtimeDir: string;
775
879
  serverManifestText: string;
880
+ serverManifestPath: string;
776
881
  }): Promise<BuiltRuntime> {
777
- const serverManifest = JSON.parse(options.serverManifestText) as BuiltServerManifest;
778
- const clientManifest = JSON.parse(options.clientManifestText) as {
882
+ const serverManifest = parseBuiltJsonArtifact<BuiltServerManifest>(
883
+ options.serverManifestText,
884
+ options.serverManifestPath,
885
+ "built app server manifest",
886
+ );
887
+ const clientManifest = parseBuiltJsonArtifact<{
888
+ assets?: readonly string[];
779
889
  routes: ClientRouteManifestEntry[];
780
890
  styles?: Array<{ css?: readonly string[]; file: string }>;
781
- };
891
+ }>(options.clientManifestText, options.clientManifestPath, "built app client manifest");
782
892
  const appDir = await materializeBuiltServerApp(options.runtimeDir, serverManifest);
783
893
  const projectRoot = appDir;
784
894
  const routesDir = join(projectRoot, serverManifest.routesDir ?? "");
@@ -860,7 +970,10 @@ async function materializeBuiltRuntime(options: {
860
970
  const allowedSourceDirs = (serverManifest.allowedSourceDirs ?? [""]).map((directory) =>
861
971
  join(projectRoot, directory),
862
972
  );
863
- const generatedImportPolicy = builtGeneratedImportPolicy(options.importPolicyText);
973
+ const generatedImportPolicy = builtGeneratedImportPolicy(
974
+ options.importPolicyText,
975
+ options.importPolicyPath,
976
+ );
864
977
 
865
978
  return {
866
979
  appDir: routesDir,
@@ -868,6 +981,7 @@ async function materializeBuiltRuntime(options: {
868
981
  ...(serverManifest.assetBaseUrl === undefined
869
982
  ? {}
870
983
  : { assetBaseUrl: serverManifest.assetBaseUrl }),
984
+ clientAssetPaths: builtClientAssetPaths(clientManifest),
871
985
  clientScripts,
872
986
  clientStylesByFile,
873
987
  clientStyles,
@@ -898,27 +1012,30 @@ async function materializeBuiltRuntime(options: {
898
1012
  }
899
1013
 
900
1014
  async function readBuiltImportPolicyText(outDir: string): Promise<string | undefined> {
1015
+ const policyPath = join(outDir, "server", "import-policy.json");
1016
+
901
1017
  try {
902
- return await readFile(join(outDir, "server", "import-policy.json"), "utf8");
1018
+ return await readFile(policyPath, "utf8");
903
1019
  } catch (error) {
904
1020
  if (isMissingFileError(error)) {
905
1021
  return undefined;
906
1022
  }
907
1023
 
908
- throw error;
1024
+ throw builtArtifactReadError("built app import policy", policyPath, error);
909
1025
  }
910
1026
  }
911
1027
 
912
1028
  function builtGeneratedImportPolicy(
913
1029
  importPolicyText: string | undefined,
1030
+ importPolicyPath: string,
914
1031
  ): AppRouterImportPolicy | undefined {
915
1032
  if (importPolicyText === undefined) {
916
1033
  return undefined;
917
1034
  }
918
1035
 
919
- const artifact = JSON.parse(importPolicyText) as {
1036
+ const artifact = parseBuiltJsonArtifact<{
920
1037
  runtimePackages?: unknown;
921
- };
1038
+ }>(importPolicyText, importPolicyPath, "built app import policy");
922
1039
  const runtimePackages = Array.isArray(artifact.runtimePackages)
923
1040
  ? artifact.runtimePackages.filter((name): name is string => typeof name === "string")
924
1041
  : [];
@@ -961,6 +1078,31 @@ function isMissingFileError(error: unknown): boolean {
961
1078
  );
962
1079
  }
963
1080
 
1081
+ async function readRequiredBuiltArtifactText(labelPath: string, label: string): Promise<string> {
1082
+ try {
1083
+ return await readFile(labelPath, "utf8");
1084
+ } catch (error) {
1085
+ throw builtArtifactReadError(label, labelPath, error);
1086
+ }
1087
+ }
1088
+
1089
+ function builtArtifactReadError(label: string, artifactPath: string, error: unknown): Error {
1090
+ const prefix = isMissingFileError(error) ? "Missing" : "Unable to read";
1091
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
1092
+
1093
+ return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
1094
+ }
1095
+
1096
+ function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
1097
+ try {
1098
+ return JSON.parse(text) as T;
1099
+ } catch (error) {
1100
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
1101
+
1102
+ throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
1103
+ }
1104
+ }
1105
+
964
1106
  async function loadBuiltServerModuleArtifacts(
965
1107
  runtime: BuiltRuntime,
966
1108
  files: Iterable<string>,
@@ -1008,7 +1150,11 @@ async function loadBuiltServerModuleArtifact(
1008
1150
  mergeBuiltServerModuleArtifacts(
1009
1151
  existing,
1010
1152
  hydrateBuiltServerModuleArtifact(
1011
- JSON.parse(text) as BuiltServerModuleArtifact,
1153
+ parseBuiltJsonArtifact<BuiltServerModuleArtifact>(
1154
+ text,
1155
+ artifactPath,
1156
+ `built server module artifact for ${file}`,
1157
+ ),
1012
1158
  builtServerDirForArtifactPath(artifactPath),
1013
1159
  ),
1014
1160
  ),
@@ -1016,6 +1162,9 @@ async function loadBuiltServerModuleArtifact(
1016
1162
  })
1017
1163
  .catch((error) => {
1018
1164
  runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
1165
+ if (isMissingFileError(error)) {
1166
+ throw builtArtifactReadError(`built server module artifact for ${file}`, artifactPath, error);
1167
+ }
1019
1168
  throw error;
1020
1169
  });
1021
1170
  runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
@@ -1,4 +1,4 @@
1
- import type { PluginOption } from "vite";
1
+ import type { PluginOption, UserConfig } from "vite";
2
2
 
3
3
  const pluginObjectIds = new WeakMap<object, number>();
4
4
  let nextPluginObjectId = 1;
@@ -13,6 +13,17 @@ export function vitePluginsCacheKey(plugins: readonly PluginOption[] | undefined
13
13
  .join("\0");
14
14
  }
15
15
 
16
+ export function viteDefineCacheKey(define: UserConfig["define"] | undefined): string {
17
+ if (define === undefined) {
18
+ return "";
19
+ }
20
+
21
+ return Object.entries(define)
22
+ .sort(([left], [right]) => left.localeCompare(right))
23
+ .map(([key, value]) => `${key}:${String(value)}`)
24
+ .join("\0");
25
+ }
26
+
16
27
  function flattenVitePluginOptions(
17
28
  options: readonly PluginOption[],
18
29
  ): Array<{ name?: string; objectId: number }> {
package/src/vite.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  type Connect,
12
12
  type Plugin,
13
13
  type PluginOption,
14
+ type UserConfig,
14
15
  type ViteDevServer,
15
16
  } from "vite";
16
17
  import type { AppRouterServerActionOptions } from "./actions.js";
@@ -46,6 +47,7 @@ import { workspacePackageFile } from "./workspace-packages.js";
46
47
 
47
48
  export interface AppRouterViteMiddlewareOptions extends AppRouterProjectOptions {
48
49
  allowedHosts?: readonly string[] | undefined;
50
+ define?: UserConfig["define"] | undefined;
49
51
  hostPolicy?: RequestHostPolicy | undefined;
50
52
  importPolicy?: AppRouterImportPolicy | undefined;
51
53
  routeCache?: AppRouterCache | undefined;
@@ -203,6 +205,7 @@ export function createAppRouterVitePlugin(options: AppRouterVitePluginOptions):
203
205
  return () => {
204
206
  const middlewareOptions: AppRouterViteRuntimeMiddlewareOptions = {
205
207
  ...options,
208
+ define: server.config.define,
206
209
  navigationScanVitePlugins: userVitePlugins ?? [],
207
210
  viteDevServer: server,
208
211
  vitePlugins: server.config.plugins,
@@ -503,6 +506,7 @@ async function handleAppRouterViteRequest(
503
506
  outgoing,
504
507
  await renderAppRequest({
505
508
  appDir: project.routesDir,
509
+ define: options.define,
506
510
  importPolicy: {
507
511
  ...options.importPolicy,
508
512
  allowedSourceDirs: project.allowedSourceDirs,