@reckona/mreact-router 0.0.50 → 0.0.52
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.
- package/README.md +8 -6
- package/dist/actions.d.ts +1 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +6 -91
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +29 -16
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +232 -10
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/build.d.ts +2 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +266 -13
- package/dist/build.js.map +1 -1
- package/dist/bundle-pipeline.d.ts +2 -0
- package/dist/bundle-pipeline.d.ts.map +1 -1
- package/dist/bundle-pipeline.js +1 -0
- package/dist/bundle-pipeline.js.map +1 -1
- package/dist/cli.js +6 -5
- package/dist/cli.js.map +1 -1
- package/dist/csrf.d.ts +7 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +104 -0
- package/dist/csrf.js.map +1 -0
- package/dist/file-conventions.d.ts.map +1 -1
- package/dist/file-conventions.js +16 -0
- package/dist/file-conventions.js.map +1 -1
- package/dist/import-policy.d.ts.map +1 -1
- package/dist/import-policy.js +2 -0
- package/dist/import-policy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/native-route-matcher.d.ts.map +1 -1
- package/dist/native-route-matcher.js +14 -1
- package/dist/native-route-matcher.js.map +1 -1
- package/dist/render.d.ts +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -9
- package/dist/render.js.map +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +27 -4
- package/dist/routes.js.map +1 -1
- package/dist/types.d.ts +10 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -11
package/dist/build.js
CHANGED
|
@@ -28,6 +28,7 @@ export async function buildApp(options) {
|
|
|
28
28
|
const shouldBuildCloudflare = buildTargets.includes("cloudflare");
|
|
29
29
|
const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
|
|
30
30
|
const routes = await scanAppRoutes({ appDir: project.routesDir });
|
|
31
|
+
const vitePlugins = options.viteConfig?.plugins;
|
|
31
32
|
const files = await collectBuildFiles(project.projectRoot, project.allowedSourceDirs, project.routesDir);
|
|
32
33
|
const serverDir = join(options.outDir, "server");
|
|
33
34
|
const clientDir = join(options.outDir, "client");
|
|
@@ -42,7 +43,7 @@ export async function buildApp(options) {
|
|
|
42
43
|
await mkdir(join(clientDir, ".vite"), { recursive: true });
|
|
43
44
|
await mkdir(join(clientDir, "assets", "routes"), { recursive: true });
|
|
44
45
|
await copyPublicAssets(project.publicDir, join(clientDir, "public"));
|
|
45
|
-
const appConventionPublicAssets = await copyAppFileConventionAssets(project.routesDir,
|
|
46
|
+
const appConventionPublicAssets = await copyAppFileConventionAssets(project.routesDir, clientDir);
|
|
46
47
|
await copyPublicAssets(project.publicDir, clientDir);
|
|
47
48
|
const publicAssets = [
|
|
48
49
|
...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
|
|
@@ -60,6 +61,7 @@ export async function buildApp(options) {
|
|
|
60
61
|
project,
|
|
61
62
|
projectRoot: project.projectRoot,
|
|
62
63
|
routes,
|
|
64
|
+
vitePlugins,
|
|
63
65
|
});
|
|
64
66
|
const generatedImportPolicy = buildGeneratedImportPolicy({
|
|
65
67
|
files,
|
|
@@ -80,6 +82,7 @@ export async function buildApp(options) {
|
|
|
80
82
|
route,
|
|
81
83
|
sourceMapDir: join(options.outDir, "source-maps", "client"),
|
|
82
84
|
sourceMaps: project.clientSourceMaps,
|
|
85
|
+
vitePlugins,
|
|
83
86
|
})));
|
|
84
87
|
const navigationRuntimeScript = clientRoutes.some((route) => route.navigation === true && !route.client)
|
|
85
88
|
? await writeNavigationRuntimeBundle(clientDir)
|
|
@@ -93,7 +96,9 @@ export async function buildApp(options) {
|
|
|
93
96
|
appDir: project.routesDir,
|
|
94
97
|
assetBaseUrl: project.assetBaseUrl,
|
|
95
98
|
clientRoutes: clientManifestRoutes,
|
|
99
|
+
project,
|
|
96
100
|
routes,
|
|
101
|
+
serverModules,
|
|
97
102
|
});
|
|
98
103
|
let cloudflareRouteModules;
|
|
99
104
|
if (shouldBuildCloudflare) {
|
|
@@ -105,6 +110,7 @@ export async function buildApp(options) {
|
|
|
105
110
|
routesDir: project.routesDir,
|
|
106
111
|
routes,
|
|
107
112
|
serverModules,
|
|
113
|
+
vitePlugins,
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
116
|
const serverManifest = {
|
|
@@ -438,6 +444,15 @@ function isNodeError(error) {
|
|
|
438
444
|
async function prerenderStaticRoutes(options) {
|
|
439
445
|
const clientScripts = new Map(options.clientRoutes.flatMap((route) => route.client && route.script !== undefined ? [[route.path, route.script]] : []));
|
|
440
446
|
const prerendered = {};
|
|
447
|
+
const importPolicy = {
|
|
448
|
+
allowedPackages: await readDeclaredProjectPackages(options.project.projectRoot),
|
|
449
|
+
allowedSourceDirs: options.project.allowedSourceDirs,
|
|
450
|
+
projectRoot: options.project.projectRoot,
|
|
451
|
+
};
|
|
452
|
+
const serverModuleMap = new Map(Object.entries(options.serverModules).map(([file, artifact]) => [
|
|
453
|
+
join(options.project.projectRoot, file),
|
|
454
|
+
artifact,
|
|
455
|
+
]));
|
|
441
456
|
for (const route of options.routes) {
|
|
442
457
|
if (route.kind !== "page") {
|
|
443
458
|
continue;
|
|
@@ -451,7 +466,9 @@ async function prerenderStaticRoutes(options) {
|
|
|
451
466
|
appDir: options.appDir,
|
|
452
467
|
assetBaseUrl: options.assetBaseUrl,
|
|
453
468
|
clientScripts,
|
|
469
|
+
importPolicy,
|
|
454
470
|
request: new Request(`http://mreact.local${pathname}`),
|
|
471
|
+
serverModules: serverModuleMap,
|
|
455
472
|
});
|
|
456
473
|
const headers = {};
|
|
457
474
|
response.headers.forEach((value, key) => {
|
|
@@ -542,6 +559,7 @@ async function buildServerModuleArtifacts(options) {
|
|
|
542
559
|
code: stripRouteLoaderOnlyExports(source),
|
|
543
560
|
filename: absoluteFile,
|
|
544
561
|
importPolicy: requestModuleImportPolicy,
|
|
562
|
+
vitePlugins: options.vitePlugins,
|
|
545
563
|
});
|
|
546
564
|
artifact.loader = {
|
|
547
565
|
code,
|
|
@@ -554,6 +572,7 @@ async function buildServerModuleArtifacts(options) {
|
|
|
554
572
|
code: stripRouteMetadataOnlyExports(source),
|
|
555
573
|
filename: absoluteFile,
|
|
556
574
|
importPolicy: requestModuleImportPolicy,
|
|
575
|
+
vitePlugins: options.vitePlugins,
|
|
557
576
|
});
|
|
558
577
|
artifact.routeMetadata = {
|
|
559
578
|
code,
|
|
@@ -568,6 +587,7 @@ async function buildServerModuleArtifacts(options) {
|
|
|
568
587
|
importPolicy: requestModuleImportPolicy,
|
|
569
588
|
routeKind: route?.kind,
|
|
570
589
|
source,
|
|
590
|
+
vitePlugins: options.vitePlugins,
|
|
571
591
|
}),
|
|
572
592
|
sourceHash: hashText(source),
|
|
573
593
|
};
|
|
@@ -728,6 +748,7 @@ async function buildRequestModuleArtifactCode(options) {
|
|
|
728
748
|
code: stripRouteRequestOnlyExports(options.source),
|
|
729
749
|
filename: options.filename,
|
|
730
750
|
importPolicy: options.importPolicy,
|
|
751
|
+
vitePlugins: options.vitePlugins,
|
|
731
752
|
});
|
|
732
753
|
}
|
|
733
754
|
async function bundleRouteLoaderModuleCode(options) {
|
|
@@ -747,6 +768,7 @@ async function bundleRouteRequestModuleCode(options) {
|
|
|
747
768
|
code: options.code,
|
|
748
769
|
filename: options.filename,
|
|
749
770
|
platform: "node",
|
|
771
|
+
vitePlugins: options.vitePlugins,
|
|
750
772
|
plugins: [
|
|
751
773
|
createAppRouterImportPolicyPlugin({
|
|
752
774
|
appDir: options.appDir,
|
|
@@ -794,18 +816,24 @@ async function writeCloudflareRouteModules(options) {
|
|
|
794
816
|
const routeId = routeIdForPath(route.path);
|
|
795
817
|
const routeModuleFile = `routes/${routeId}.mjs`;
|
|
796
818
|
let routeModuleExports;
|
|
797
|
-
if (route.kind === "server") {
|
|
819
|
+
if (route.kind === "server" || route.kind === "metadata") {
|
|
798
820
|
try {
|
|
799
821
|
const routeOutput = await buildCloudflareServerRouteModule({
|
|
800
822
|
filename: route.file,
|
|
823
|
+
vitePlugins: options.vitePlugins,
|
|
801
824
|
});
|
|
802
825
|
const serverRouteFile = `routes/${routeId}.${hashText(routeOutput).slice(0, 8)}.server.mjs`;
|
|
803
826
|
await writeFile(join(options.cloudflareDir, serverRouteFile), routeOutput);
|
|
804
827
|
const serverRouteImport = `./${serverRouteFile.split("/").pop() ?? serverRouteFile}`;
|
|
805
|
-
routeModuleExports = [
|
|
828
|
+
routeModuleExports = [
|
|
829
|
+
`export * from ${JSON.stringify(serverRouteImport)};`,
|
|
830
|
+
...(route.kind === "metadata"
|
|
831
|
+
? [`export { default } from ${JSON.stringify(serverRouteImport)};`]
|
|
832
|
+
: []),
|
|
833
|
+
];
|
|
806
834
|
}
|
|
807
835
|
catch (error) {
|
|
808
|
-
throw new Error(`Failed to build Cloudflare
|
|
836
|
+
throw new Error(`Failed to build Cloudflare ${route.kind} route module for ${routeFile}: ${errorMessage(error)}`);
|
|
809
837
|
}
|
|
810
838
|
await writeFile(join(options.cloudflareDir, routeModuleFile), `${routeModuleExports.join("\n")}\n`);
|
|
811
839
|
registryEntries.push(`${JSON.stringify(routeFile)}: () => import(${JSON.stringify(`./${routeModuleFile}`)})`);
|
|
@@ -827,12 +855,14 @@ async function writeCloudflareRouteModules(options) {
|
|
|
827
855
|
projectRoot: options.projectRoot,
|
|
828
856
|
routesDir: options.routesDir,
|
|
829
857
|
serverModules: options.serverModules,
|
|
858
|
+
vitePlugins: options.vitePlugins,
|
|
830
859
|
})
|
|
831
860
|
: await buildCloudflareStringRouteComponentModule({
|
|
832
861
|
filename: route.file,
|
|
833
862
|
projectRoot: options.projectRoot,
|
|
834
863
|
routesDir: options.routesDir,
|
|
835
864
|
serverModules: options.serverModules,
|
|
865
|
+
vitePlugins: options.vitePlugins,
|
|
836
866
|
});
|
|
837
867
|
const componentFile = `routes/${routeId}.${hashText(componentOutput).slice(0, 8)}.component.mjs`;
|
|
838
868
|
await writeFile(join(options.cloudflareDir, componentFile), componentOutput);
|
|
@@ -849,6 +879,7 @@ async function writeCloudflareRouteModules(options) {
|
|
|
849
879
|
const loaderOutput = await buildCloudflareRouteLoaderModule({
|
|
850
880
|
filename: route.file,
|
|
851
881
|
projectRoot: options.projectRoot,
|
|
882
|
+
vitePlugins: options.vitePlugins,
|
|
852
883
|
});
|
|
853
884
|
const loaderFile = `routes/${routeId}.${hashText(loaderOutput).slice(0, 8)}.loader.mjs`;
|
|
854
885
|
await writeFile(join(options.cloudflareDir, loaderFile), loaderOutput);
|
|
@@ -873,15 +904,25 @@ async function writeCloudflareRouteModules(options) {
|
|
|
873
904
|
return { registryFile: "route-modules.mjs" };
|
|
874
905
|
}
|
|
875
906
|
async function buildCloudflareServerComponentModule(options) {
|
|
907
|
+
const metadataModule = await buildCloudflareRouteMetadataExportModule({
|
|
908
|
+
filename: options.filename,
|
|
909
|
+
vitePlugins: options.vitePlugins,
|
|
910
|
+
});
|
|
876
911
|
const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
|
|
912
|
+
${metadataModule === undefined ? "const metadataModule = {};" : 'import * as metadataModule from "mreact:metadata";'}
|
|
877
913
|
|
|
878
914
|
const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
|
|
879
915
|
export const App = component;
|
|
880
916
|
export default component;
|
|
917
|
+
export const generateMetadata = metadataModule.generateMetadata;
|
|
918
|
+
export const metadata = metadataModule.metadata;
|
|
881
919
|
export const slots = routeModule.slots;`;
|
|
882
|
-
return
|
|
920
|
+
return bundleCloudflareVirtualModule({
|
|
883
921
|
entry,
|
|
884
922
|
filename: `${options.filename}.mreact-cloudflare-component.js`,
|
|
923
|
+
modules: metadataModule === undefined
|
|
924
|
+
? new Map()
|
|
925
|
+
: new Map([["mreact:metadata", metadataModule]]),
|
|
885
926
|
plugins: [
|
|
886
927
|
cloudflareServerSourceTransformPlugin({
|
|
887
928
|
projectRoot: options.projectRoot,
|
|
@@ -891,6 +932,7 @@ export const slots = routeModule.slots;`;
|
|
|
891
932
|
cloudflareWorkspaceRuntimePlugin(),
|
|
892
933
|
],
|
|
893
934
|
resolveDir: dirname(options.filename),
|
|
935
|
+
vitePlugins: options.vitePlugins,
|
|
894
936
|
});
|
|
895
937
|
}
|
|
896
938
|
async function buildCloudflareStringRouteComponentModule(options) {
|
|
@@ -901,6 +943,7 @@ async function buildCloudflareStringRouteComponentModule(options) {
|
|
|
901
943
|
projectRoot: options.projectRoot,
|
|
902
944
|
serverModules: options.serverModules,
|
|
903
945
|
serverOutput: "string",
|
|
946
|
+
vitePlugins: options.vitePlugins,
|
|
904
947
|
});
|
|
905
948
|
}
|
|
906
949
|
const pageModule = await buildCloudflareComponentExportModule({
|
|
@@ -908,15 +951,17 @@ async function buildCloudflareStringRouteComponentModule(options) {
|
|
|
908
951
|
projectRoot: options.projectRoot,
|
|
909
952
|
serverModules: options.serverModules,
|
|
910
953
|
serverOutput: "string",
|
|
954
|
+
vitePlugins: options.vitePlugins,
|
|
911
955
|
});
|
|
912
956
|
const shellModules = await Promise.all(shellFiles.map((shell) => buildCloudflareComponentExportModule({
|
|
913
957
|
filename: shell.file,
|
|
914
958
|
projectRoot: options.projectRoot,
|
|
915
959
|
serverModules: options.serverModules,
|
|
916
960
|
serverOutput: "string",
|
|
961
|
+
vitePlugins: options.vitePlugins,
|
|
917
962
|
})));
|
|
918
963
|
const shellImports = shellFiles.map((_, index) => `import * as shell${index} from "mreact:shell-${index}";`);
|
|
919
|
-
const shellDefinitions = shellFiles.map((shell, index) => `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)} }`);
|
|
964
|
+
const shellDefinitions = shellFiles.map((shell, index) => `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)}, module: shell${index} }`);
|
|
920
965
|
const entry = `import * as pageModule from "mreact:page";
|
|
921
966
|
${shellImports.join("\n")}
|
|
922
967
|
|
|
@@ -929,7 +974,8 @@ export default renderCloudflareStringRoute;
|
|
|
929
974
|
async function renderCloudflareStringRoute(props) {
|
|
930
975
|
const slotHtml = await renderRouteSlots(pageModule.slots, props);
|
|
931
976
|
const layoutShells = await renderLayoutShells(shells, props, slotHtml);
|
|
932
|
-
|
|
977
|
+
const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
|
|
978
|
+
let html = "<!DOCTYPE html>";
|
|
933
979
|
for (const shell of layoutShells) {
|
|
934
980
|
html += shell.prefix;
|
|
935
981
|
}
|
|
@@ -937,6 +983,7 @@ async function renderCloudflareStringRoute(props) {
|
|
|
937
983
|
for (const shell of [...layoutShells].reverse()) {
|
|
938
984
|
html += shell.suffix;
|
|
939
985
|
}
|
|
986
|
+
html = injectCloudflareHead(html, metadata, cloudflareRouteHeadTags(props.clientManifest, props.route.path));
|
|
940
987
|
return new Response(html, {
|
|
941
988
|
headers: {
|
|
942
989
|
"content-type": "text/html; charset=utf-8"
|
|
@@ -973,6 +1020,7 @@ ${cloudflareShellRuntimeSource()}`;
|
|
|
973
1020
|
]),
|
|
974
1021
|
plugins: [cloudflareWorkspaceRuntimePlugin()],
|
|
975
1022
|
resolveDir: dirname(options.filename),
|
|
1023
|
+
vitePlugins: options.vitePlugins,
|
|
976
1024
|
});
|
|
977
1025
|
}
|
|
978
1026
|
async function buildCloudflareStreamRouteComponentModule(options) {
|
|
@@ -981,6 +1029,7 @@ async function buildCloudflareStreamRouteComponentModule(options) {
|
|
|
981
1029
|
projectRoot: options.projectRoot,
|
|
982
1030
|
serverModules: options.serverModules,
|
|
983
1031
|
serverOutput: "stream",
|
|
1032
|
+
vitePlugins: options.vitePlugins,
|
|
984
1033
|
});
|
|
985
1034
|
const shellFiles = await cloudflareShellFilesForPage(options.routesDir, options.filename);
|
|
986
1035
|
const shellModules = await Promise.all(shellFiles.map((shell) => buildCloudflareComponentExportModule({
|
|
@@ -988,9 +1037,10 @@ async function buildCloudflareStreamRouteComponentModule(options) {
|
|
|
988
1037
|
projectRoot: options.projectRoot,
|
|
989
1038
|
serverModules: options.serverModules,
|
|
990
1039
|
serverOutput: "string",
|
|
1040
|
+
vitePlugins: options.vitePlugins,
|
|
991
1041
|
})));
|
|
992
1042
|
const shellImports = shellFiles.map((_, index) => `import * as shell${index} from "mreact:shell-${index}";`);
|
|
993
|
-
const shellDefinitions = shellFiles.map((shell, index) => `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)} }`);
|
|
1043
|
+
const shellDefinitions = shellFiles.map((shell, index) => `{ component: selectComponent(shell${index}, ${JSON.stringify(shell.file)}), id: ${JSON.stringify(shell.id)}, kind: ${JSON.stringify(shell.kind)}, module: shell${index} }`);
|
|
994
1044
|
const entry = `import { createStringSink, renderOutOfOrderReorderScript, renderToReadableStream } from "@reckona/mreact-server";
|
|
995
1045
|
import * as pageModule from "mreact:page";
|
|
996
1046
|
${shellImports.join("\n")}
|
|
@@ -1005,10 +1055,15 @@ function renderCloudflareStreamRoute(props) {
|
|
|
1005
1055
|
const body = renderToReadableStream(async ($sink) => {
|
|
1006
1056
|
const slotHtml = await renderRouteSlots(pageModule.slots, props);
|
|
1007
1057
|
const layoutShells = await renderLayoutShells(shells, props, slotHtml);
|
|
1058
|
+
const metadata = await resolveRouteMetadata([...shells.map((shell) => shell.module), pageModule], props);
|
|
1059
|
+
const routeHeadTags = cloudflareRouteHeadTags(props.clientManifest, props.route.path);
|
|
1008
1060
|
$sink.append("<!DOCTYPE html>");
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1061
|
+
if (layoutShells.length === 0) {
|
|
1062
|
+
$sink.append(injectCloudflareHead("", metadata, routeHeadTags));
|
|
1063
|
+
}
|
|
1064
|
+
for (let index = 0; index < layoutShells.length; index += 1) {
|
|
1065
|
+
const shell = layoutShells[index];
|
|
1066
|
+
$sink.append(index === 0 ? injectCloudflareHead(shell.prefix, metadata, routeHeadTags) : shell.prefix);
|
|
1012
1067
|
}
|
|
1013
1068
|
await pageComponent($sink, props);
|
|
1014
1069
|
renderOutOfOrderReorderScript($sink);
|
|
@@ -1060,6 +1115,7 @@ ${cloudflareShellRuntimeSource()}`;
|
|
|
1060
1115
|
]),
|
|
1061
1116
|
plugins: [cloudflareWorkspaceRuntimePlugin()],
|
|
1062
1117
|
resolveDir: dirname(options.filename),
|
|
1118
|
+
vitePlugins: options.vitePlugins,
|
|
1063
1119
|
});
|
|
1064
1120
|
}
|
|
1065
1121
|
function cloudflareShellRuntimeSource() {
|
|
@@ -1128,6 +1184,160 @@ function readSlotName(attributes) {
|
|
|
1128
1184
|
return match?.[1] ?? match?.[2];
|
|
1129
1185
|
}
|
|
1130
1186
|
|
|
1187
|
+
async function resolveRouteMetadata(modules, props) {
|
|
1188
|
+
const metadata = [];
|
|
1189
|
+
const context = {
|
|
1190
|
+
data: props.data,
|
|
1191
|
+
params: props.params,
|
|
1192
|
+
request: props.request
|
|
1193
|
+
};
|
|
1194
|
+
for (const module of modules) {
|
|
1195
|
+
let next = module.metadata;
|
|
1196
|
+
if (typeof module.generateMetadata === "function") {
|
|
1197
|
+
const generated = await module.generateMetadata(context);
|
|
1198
|
+
next = mergeRouteMetadata([next, generated].filter(Boolean));
|
|
1199
|
+
}
|
|
1200
|
+
if (next !== undefined) {
|
|
1201
|
+
metadata.push(next);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return mergeRouteMetadata(metadata);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function mergeRouteMetadata(metadata) {
|
|
1208
|
+
if (metadata.length === 0) {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
return metadata.reduce((merged, next) => ({
|
|
1212
|
+
...merged,
|
|
1213
|
+
...next,
|
|
1214
|
+
alternates: mergeObject(merged.alternates, next.alternates),
|
|
1215
|
+
csp: mergeObject(merged.csp, next.csp),
|
|
1216
|
+
head: mergeArrays(merged.head, next.head),
|
|
1217
|
+
icons: mergeObject(merged.icons, next.icons),
|
|
1218
|
+
openGraph: {
|
|
1219
|
+
...mergeObject(merged.openGraph, next.openGraph),
|
|
1220
|
+
images: mergeArrays(merged.openGraph?.images, next.openGraph?.images),
|
|
1221
|
+
},
|
|
1222
|
+
}), {});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function mergeObject(left, right) {
|
|
1226
|
+
if (left === undefined) {
|
|
1227
|
+
return right;
|
|
1228
|
+
}
|
|
1229
|
+
if (right === undefined) {
|
|
1230
|
+
return left;
|
|
1231
|
+
}
|
|
1232
|
+
return { ...left, ...right };
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function mergeArrays(left, right) {
|
|
1236
|
+
if (left === undefined || left.length === 0) {
|
|
1237
|
+
return right;
|
|
1238
|
+
}
|
|
1239
|
+
if (right === undefined || right.length === 0) {
|
|
1240
|
+
return left;
|
|
1241
|
+
}
|
|
1242
|
+
return [...left, ...right];
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function injectCloudflareHead(html, metadata, routeHeadTags) {
|
|
1246
|
+
const metadataTags = routeMetadataHeadTags(metadata);
|
|
1247
|
+
const tags = routeHeadTags + metadataTags;
|
|
1248
|
+
let nextHtml = metadata?.lang === undefined ? html : injectHtmlLangAttribute(html, metadataString(metadata.lang));
|
|
1249
|
+
|
|
1250
|
+
if (metadata?.title !== undefined) {
|
|
1251
|
+
nextHtml = nextHtml.replace(/<title\\b[^>]*>[\\s\\S]*?<\\/title\\s*>/i, "");
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (tags === "") {
|
|
1255
|
+
return nextHtml;
|
|
1256
|
+
}
|
|
1257
|
+
if (/<head(?:\\s[^>]*)?>/i.test(nextHtml)) {
|
|
1258
|
+
return nextHtml.replace(/<head(\\s[^>]*)?>/i, (match) => \`\${match}\${tags}\`);
|
|
1259
|
+
}
|
|
1260
|
+
if (/<html(?:\\s[^>]*)?>/i.test(nextHtml)) {
|
|
1261
|
+
return nextHtml.replace(/<html(\\s[^>]*)?>/i, (match) => \`\${match}<head>\${tags}</head>\`);
|
|
1262
|
+
}
|
|
1263
|
+
return \`<head>\${tags}</head>\${nextHtml}\`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function routeMetadataHeadTags(metadata) {
|
|
1267
|
+
if (metadata === undefined) {
|
|
1268
|
+
return "";
|
|
1269
|
+
}
|
|
1270
|
+
const tags = [
|
|
1271
|
+
metadata.title === undefined ? undefined : \`<title>\${escapeHtml(metadataString(metadata.title))}</title>\`,
|
|
1272
|
+
metadata.description === undefined ? undefined : \`<meta name="description" content="\${escapeHtmlAttribute(metadataString(metadata.description))}">\`,
|
|
1273
|
+
metadata.alternates?.canonical === undefined ? undefined : \`<link rel="canonical" href="\${escapeHtmlAttribute(metadataString(metadata.alternates.canonical))}">\`,
|
|
1274
|
+
metadata.openGraph?.title === undefined ? undefined : \`<meta property="og:title" content="\${escapeHtmlAttribute(metadataString(metadata.openGraph.title))}">\`,
|
|
1275
|
+
metadata.openGraph?.description === undefined ? undefined : \`<meta property="og:description" content="\${escapeHtmlAttribute(metadataString(metadata.openGraph.description))}">\`,
|
|
1276
|
+
...openGraphImages(metadata.openGraph).map((image) => \`<meta property="og:image" content="\${escapeHtmlAttribute(image)}">\`),
|
|
1277
|
+
metadata.icons?.icon === undefined ? undefined : \`<link rel="icon" href="\${escapeHtmlAttribute(metadataString(metadata.icons.icon))}">\`,
|
|
1278
|
+
metadata.icons?.apple === undefined ? undefined : \`<link rel="apple-touch-icon" href="\${escapeHtmlAttribute(metadataString(metadata.icons.apple))}">\`,
|
|
1279
|
+
...headDescriptorTags(metadata.head, metadata.csp?.nonce),
|
|
1280
|
+
];
|
|
1281
|
+
return tags.filter((tag) => tag !== undefined).join("");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function openGraphImages(openGraph) {
|
|
1285
|
+
if (openGraph?.images !== undefined && openGraph.images.length > 0) {
|
|
1286
|
+
return openGraph.images.map(metadataImageUrl);
|
|
1287
|
+
}
|
|
1288
|
+
return openGraph?.image === undefined ? [] : [metadataImageUrl(openGraph.image)];
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function headDescriptorTags(descriptors, nonce) {
|
|
1292
|
+
return (descriptors ?? []).flatMap((descriptor) => {
|
|
1293
|
+
const descriptorNonce = descriptor.nonce === true ? nonce : descriptor.nonce || undefined;
|
|
1294
|
+
const attrs = {
|
|
1295
|
+
...descriptor.attrs,
|
|
1296
|
+
...(descriptorNonce === undefined ? {} : { nonce: descriptorNonce }),
|
|
1297
|
+
};
|
|
1298
|
+
const attrText = Object.entries(attrs)
|
|
1299
|
+
.flatMap(([name, value]) => {
|
|
1300
|
+
if (value === undefined || value === false) {
|
|
1301
|
+
return [];
|
|
1302
|
+
}
|
|
1303
|
+
return value === true
|
|
1304
|
+
? [escapeHtmlAttribute(name)]
|
|
1305
|
+
: [\`\${escapeHtmlAttribute(name)}="\${escapeHtmlAttribute(String(value))}"\`];
|
|
1306
|
+
})
|
|
1307
|
+
.join(" ");
|
|
1308
|
+
const open = attrText === "" ? \`<\${descriptor.tag}>\` : \`<\${descriptor.tag} \${attrText}>\`;
|
|
1309
|
+
if (descriptor.tag === "meta" || descriptor.tag === "link" || descriptor.tag === "base") {
|
|
1310
|
+
return [open.slice(0, -1) + ">"];
|
|
1311
|
+
}
|
|
1312
|
+
return [\`\${open}\${String(descriptor.content ?? "").replaceAll("<", "\\\\u003c")}</\${descriptor.tag}>\`];
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function injectHtmlLangAttribute(html, lang) {
|
|
1317
|
+
const escapedLang = escapeHtmlAttribute(lang);
|
|
1318
|
+
if (!/<html(?:\\s[^>]*)?>/i.test(html)) {
|
|
1319
|
+
return html;
|
|
1320
|
+
}
|
|
1321
|
+
return html.replace(/<html(\\s[^>]*)?>/i, (_match, attrs = "") => {
|
|
1322
|
+
const strippedAttrs = String(attrs).replace(/\\s+lang=(?:"[^"]*"|'[^']*'|[^\\s>]+)/i, "");
|
|
1323
|
+
return \`<html lang="\${escapedLang}"\${strippedAttrs}>\`;
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function metadataString(value) {
|
|
1328
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1329
|
+
return String(value);
|
|
1330
|
+
}
|
|
1331
|
+
throw new Error("Invalid metadata field: expected string, number, or boolean.");
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function metadataImageUrl(value) {
|
|
1335
|
+
if (typeof value === "object" && value !== null && "url" in value) {
|
|
1336
|
+
return metadataString(value.url);
|
|
1337
|
+
}
|
|
1338
|
+
return metadataString(value);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1131
1341
|
function cloudflareRouteHeadTags(manifest, routePath) {
|
|
1132
1342
|
const route = manifest.routes.find((route) => route.path === routePath);
|
|
1133
1343
|
const css = route?.css ?? [];
|
|
@@ -1146,18 +1356,35 @@ function escapeHtmlAttribute(value) {
|
|
|
1146
1356
|
.replaceAll("&", "&")
|
|
1147
1357
|
.replaceAll('"', """)
|
|
1148
1358
|
.replaceAll("<", "<");
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function escapeHtml(value) {
|
|
1362
|
+
return String(value ?? "")
|
|
1363
|
+
.replaceAll("&", "&")
|
|
1364
|
+
.replaceAll("<", "<")
|
|
1365
|
+
.replaceAll(">", ">");
|
|
1149
1366
|
}`;
|
|
1150
1367
|
}
|
|
1151
1368
|
async function buildCloudflareComponentExportModule(options) {
|
|
1369
|
+
const metadataModule = await buildCloudflareRouteMetadataExportModule({
|
|
1370
|
+
filename: options.filename,
|
|
1371
|
+
vitePlugins: options.vitePlugins,
|
|
1372
|
+
});
|
|
1152
1373
|
const entry = `import * as routeModule from ${JSON.stringify(options.filename)};
|
|
1374
|
+
${metadataModule === undefined ? "const metadataModule = {};" : 'import * as metadataModule from "mreact:metadata";'}
|
|
1153
1375
|
|
|
1154
1376
|
const component = routeModule.default ?? routeModule.App ?? Object.values(routeModule).find((value) => typeof value === "function");
|
|
1155
1377
|
export const App = component;
|
|
1156
1378
|
export default component;
|
|
1379
|
+
export const generateMetadata = metadataModule.generateMetadata;
|
|
1380
|
+
export const metadata = metadataModule.metadata;
|
|
1157
1381
|
export const slots = routeModule.slots;`;
|
|
1158
|
-
return
|
|
1382
|
+
return bundleCloudflareVirtualModule({
|
|
1159
1383
|
entry,
|
|
1160
1384
|
filename: `${options.filename}.mreact-cloudflare-${options.serverOutput}-component.js`,
|
|
1385
|
+
modules: metadataModule === undefined
|
|
1386
|
+
? new Map()
|
|
1387
|
+
: new Map([["mreact:metadata", metadataModule]]),
|
|
1161
1388
|
plugins: [
|
|
1162
1389
|
cloudflareServerSourceTransformPlugin({
|
|
1163
1390
|
projectRoot: options.projectRoot,
|
|
@@ -1167,6 +1394,7 @@ export const slots = routeModule.slots;`;
|
|
|
1167
1394
|
cloudflareWorkspaceRuntimePlugin(),
|
|
1168
1395
|
],
|
|
1169
1396
|
resolveDir: dirname(options.filename),
|
|
1397
|
+
vitePlugins: options.vitePlugins,
|
|
1170
1398
|
});
|
|
1171
1399
|
}
|
|
1172
1400
|
async function buildCloudflareRouteLoaderModule(options) {
|
|
@@ -1176,6 +1404,23 @@ async function buildCloudflareRouteLoaderModule(options) {
|
|
|
1176
1404
|
filename: `${options.filename}.mreact-cloudflare-loader.js`,
|
|
1177
1405
|
plugins: [cloudflareWorkspaceRuntimePlugin()],
|
|
1178
1406
|
resolveDir: dirname(options.filename),
|
|
1407
|
+
vitePlugins: options.vitePlugins,
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
async function buildCloudflareRouteMetadataExportModule(options) {
|
|
1411
|
+
const source = await readFile(options.filename, "utf8");
|
|
1412
|
+
if (!hasMetadataExport(source)) {
|
|
1413
|
+
return undefined;
|
|
1414
|
+
}
|
|
1415
|
+
const entry = `import * as routeMetadataModule from ${JSON.stringify(options.filename)};
|
|
1416
|
+
export const generateMetadata = routeMetadataModule.generateMetadata;
|
|
1417
|
+
export const metadata = routeMetadataModule.metadata;`;
|
|
1418
|
+
return bundleCloudflareModule({
|
|
1419
|
+
entry,
|
|
1420
|
+
filename: `${options.filename}.mreact-cloudflare-metadata.js`,
|
|
1421
|
+
plugins: [cloudflareWorkspaceRuntimePlugin()],
|
|
1422
|
+
resolveDir: dirname(options.filename),
|
|
1423
|
+
vitePlugins: options.vitePlugins,
|
|
1179
1424
|
});
|
|
1180
1425
|
}
|
|
1181
1426
|
async function buildCloudflareServerRouteModule(options) {
|
|
@@ -1196,6 +1441,7 @@ export default defaultHandler;`;
|
|
|
1196
1441
|
filename: `${options.filename}.mreact-cloudflare-server-route.js`,
|
|
1197
1442
|
plugins: [cloudflareWorkspaceRuntimePlugin()],
|
|
1198
1443
|
resolveDir: dirname(options.filename),
|
|
1444
|
+
vitePlugins: options.vitePlugins,
|
|
1199
1445
|
});
|
|
1200
1446
|
}
|
|
1201
1447
|
async function bundleCloudflareModule(options) {
|
|
@@ -1207,6 +1453,7 @@ async function bundleCloudflareModule(options) {
|
|
|
1207
1453
|
preserveExports: true,
|
|
1208
1454
|
plugins: options.plugins,
|
|
1209
1455
|
target: "es2022",
|
|
1456
|
+
vitePlugins: options.vitePlugins,
|
|
1210
1457
|
});
|
|
1211
1458
|
const code = output.code;
|
|
1212
1459
|
if (code === undefined) {
|
|
@@ -1242,6 +1489,7 @@ async function bundleCloudflareVirtualModule(options) {
|
|
|
1242
1489
|
...options.plugins,
|
|
1243
1490
|
],
|
|
1244
1491
|
resolveDir: options.resolveDir,
|
|
1492
|
+
vitePlugins: options.vitePlugins,
|
|
1245
1493
|
});
|
|
1246
1494
|
}
|
|
1247
1495
|
async function cloudflareShellFilesForPage(routesDir, pageFile) {
|
|
@@ -1337,6 +1585,7 @@ function cloudflareWorkspaceRuntimePlugin() {
|
|
|
1337
1585
|
});
|
|
1338
1586
|
const routerCachePath = packageFile("router", "@reckona/mreact-router", "cache");
|
|
1339
1587
|
const routerCookiesPath = packageFile("router", "@reckona/mreact-router", "cookies");
|
|
1588
|
+
const routerCsrfPath = packageFile("router", "@reckona/mreact-router", "csrf");
|
|
1340
1589
|
const routerDeferredPath = packageFile("router", "@reckona/mreact-router", "deferred");
|
|
1341
1590
|
const routerI18nPath = packageFile("router", "@reckona/mreact-router", "i18n");
|
|
1342
1591
|
const routerLinkPath = packageFile("router", "@reckona/mreact-router", "link");
|
|
@@ -1403,6 +1652,7 @@ export function escapeHtmlBatch(values) {
|
|
|
1403
1652
|
buildApi.onLoad({ filter: /^index$/, namespace: "mreact-cloudflare-router-index" }, () => ({
|
|
1404
1653
|
contents: `export { cacheControl, revalidatePath } from ${JSON.stringify(routerCachePath)};
|
|
1405
1654
|
export { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from ${JSON.stringify(routerCookiesPath)};
|
|
1655
|
+
export { createFormCsrfToken, formCsrfCookie, formCsrfFieldName, serverActionCookie, validateFormCsrf } from ${JSON.stringify(routerCsrfPath)};
|
|
1406
1656
|
export { defer, isDeferredLoaderData } from ${JSON.stringify(routerDeferredPath)};
|
|
1407
1657
|
export { defineMessages, detectLocale } from ${JSON.stringify(routerI18nPath)};
|
|
1408
1658
|
export { Link, linkProps } from ${JSON.stringify(routerLinkPath)};
|
|
@@ -1420,7 +1670,8 @@ export { cookies, headers, html, json, next, notFound, redirect, redirectExterna
|
|
|
1420
1670
|
};
|
|
1421
1671
|
}
|
|
1422
1672
|
function cloudflareRouteRequiresGeneratedModule(route, prerenderedRoutes) {
|
|
1423
|
-
return (route.kind === "
|
|
1673
|
+
return (route.kind === "metadata" ||
|
|
1674
|
+
route.kind === "server" ||
|
|
1424
1675
|
(route.kind === "page" &&
|
|
1425
1676
|
(route.segments.some((segment) => segment.kind !== "static") ||
|
|
1426
1677
|
prerenderedRoutes[route.path] === undefined)));
|
|
@@ -1455,6 +1706,7 @@ async function writeClientRouteBundle(options) {
|
|
|
1455
1706
|
pageFile: route.file,
|
|
1456
1707
|
projectRoot: options.projectRoot,
|
|
1457
1708
|
routeId: routeIdForPath(route.path),
|
|
1709
|
+
vitePlugins: options.vitePlugins,
|
|
1458
1710
|
});
|
|
1459
1711
|
const source = await readFile(route.file, "utf8");
|
|
1460
1712
|
const clientSource = stripRouteClientOnlyExports(source);
|
|
@@ -1545,6 +1797,7 @@ async function writeRouteCssAssets(options) {
|
|
|
1545
1797
|
filename: options.pageFile,
|
|
1546
1798
|
minify: true,
|
|
1547
1799
|
platform: "browser",
|
|
1800
|
+
vitePlugins: options.vitePlugins,
|
|
1548
1801
|
});
|
|
1549
1802
|
const cssAssets = (output.assets ?? []).filter((asset) => asset.fileName.endsWith(".css"));
|
|
1550
1803
|
const written = [];
|