@reckona/mreact-router 0.0.75 → 0.0.77

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reckona/mreact-router",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
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.75",
109
- "@reckona/mreact-compat": "0.0.75",
110
- "@reckona/mreact-compiler": "0.0.75",
111
- "@reckona/mreact-devtools": "0.0.75",
112
- "@reckona/mreact-reactive-core": "0.0.75",
113
- "@reckona/mreact-query": "0.0.75",
114
- "@reckona/mreact-reactive-dom": "0.0.75",
115
- "@reckona/mreact-server": "0.0.75",
116
- "@reckona/mreact-shared": "0.0.75"
108
+ "@reckona/mreact": "0.0.77",
109
+ "@reckona/mreact-devtools": "0.0.77",
110
+ "@reckona/mreact-compat": "0.0.77",
111
+ "@reckona/mreact-query": "0.0.77",
112
+ "@reckona/mreact-compiler": "0.0.77",
113
+ "@reckona/mreact-reactive-core": "0.0.77",
114
+ "@reckona/mreact-reactive-dom": "0.0.77",
115
+ "@reckona/mreact-server": "0.0.77",
116
+ "@reckona/mreact-shared": "0.0.77"
117
117
  },
118
118
  "peerDependencies": {
119
119
  "vite": ">=8 <9"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@reckona/mreact-router-native": "0.0.75"
122
+ "@reckona/mreact-router-native": "0.0.77"
123
123
  }
124
124
  }
package/src/actions.ts CHANGED
@@ -176,6 +176,11 @@ export async function prepareRouteServerActions(options: {
176
176
  pageFile: string;
177
177
  request?: Request | undefined;
178
178
  }): Promise<PreparedRouteActions> {
179
+ if (options.formActionReferences !== undefined && options.formActionReferences.length === 0) {
180
+ replaceInferredServerActionReferences(options.appDir, options.pageFile, new Map());
181
+ return { code: options.code, hasFormActions: false };
182
+ }
183
+
179
184
  if (!hasFormActionCandidate(options.code, options.pageFile)) {
180
185
  replaceInferredServerActionReferences(options.appDir, options.pageFile, new Map());
181
186
  return { code: options.code, hasFormActions: false };
package/src/build.ts CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import type { Dirent } from "node:fs";
3
3
  import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { builtinModules } from "node:module";
5
- import { dirname, join, relative, sep } from "node:path";
5
+ import { dirname, join, relative, resolve, sep } from "node:path";
6
6
  import {
7
7
  collectStaticImportReferences,
8
8
  collectTopLevelValueExportNames,
@@ -100,11 +100,24 @@ export interface AwsLambdaArtifactManifest {
100
100
  version: 1;
101
101
  }
102
102
 
103
+ export interface CloudflarePagesArtifactManifest {
104
+ files: Array<{ bytes: number; path: string }>;
105
+ runtime: "cloudflare-pages";
106
+ totalBytes: number;
107
+ version: 1;
108
+ worker: "_worker.js";
109
+ }
110
+
103
111
  export interface PackageAwsLambdaArtifactOptions {
104
112
  fromDir: string;
105
113
  outDir: string;
106
114
  }
107
115
 
116
+ export interface PackageCloudflarePagesArtifactOptions {
117
+ fromDir: string;
118
+ outDir: string;
119
+ }
120
+
108
121
  export interface BuiltServerManifest {
109
122
  allowedSourceDirs?: readonly string[];
110
123
  assetBaseUrl?: string;
@@ -2473,6 +2486,10 @@ function cloudflareWorkspaceRuntimePlugin(): RouterCompatPlugin {
2473
2486
  ["@reckona/mreact-compat/scheduler", packageFile("react-compat", "@reckona/mreact-compat", "scheduler")],
2474
2487
  ["@reckona/mreact-query", packageFile("query", "@reckona/mreact-query", "index")],
2475
2488
  ["@reckona/mreact-reactive-core", packageFile("reactive-core", "@reckona/mreact-reactive-core", "index")],
2489
+ [
2490
+ "@reckona/mreact-router/adapters/cloudflare",
2491
+ packageFile("router", "@reckona/mreact-router", "adapters/cloudflare"),
2492
+ ],
2476
2493
  ["@reckona/mreact-router/link", routerLinkPath],
2477
2494
  ["@reckona/mreact-router/runtime-state", routerRuntimeStatePath],
2478
2495
  ["@reckona/mreact-router/session", packageFile("router", "@reckona/mreact-router", "session")],
@@ -2913,7 +2930,7 @@ export async function packageAwsLambdaArtifact(
2913
2930
  });
2914
2931
  await writeFile(join(options.outDir, "mreact-handler.mjs"), awsLambdaHandlerSource(".mreact"));
2915
2932
 
2916
- const files = await collectAwsLambdaArtifactFiles(options.outDir, "");
2933
+ const files = await collectArtifactFiles(options.outDir, "");
2917
2934
  const manifest = {
2918
2935
  files,
2919
2936
  handler: "mreact-handler.handler",
@@ -2930,6 +2947,63 @@ export async function packageAwsLambdaArtifact(
2930
2947
  return manifest;
2931
2948
  }
2932
2949
 
2950
+ export async function packageCloudflarePagesArtifact(
2951
+ options: PackageCloudflarePagesArtifactOptions,
2952
+ ): Promise<CloudflarePagesArtifactManifest> {
2953
+ const clientManifestPath = join(options.fromDir, "client", "manifest.json");
2954
+ const workerPath = join(options.fromDir, "cloudflare", "worker.mjs");
2955
+
2956
+ await assertRequiredBuildFile(clientManifestPath);
2957
+ await assertRequiredBuildFile(workerPath);
2958
+ await assertRequiredBuildFile(join(options.fromDir, "cloudflare", "route-modules.mjs"));
2959
+ assertPackageOutputDoesNotReplaceBuildOutput(options);
2960
+
2961
+ const clientManifest = JSON.parse(await readFile(clientManifestPath, "utf8")) as {
2962
+ publicAssets?: readonly string[] | undefined;
2963
+ };
2964
+
2965
+ await rm(options.outDir, { force: true, recursive: true });
2966
+ await mkdir(options.outDir, { recursive: true });
2967
+ await cp(join(options.fromDir, "client"), join(options.outDir, "_mreact", "client"), {
2968
+ force: true,
2969
+ recursive: true,
2970
+ });
2971
+ await copyCloudflarePagesPublicAssets({
2972
+ clientDir: join(options.fromDir, "client"),
2973
+ outDir: options.outDir,
2974
+ publicAssets: clientManifest.publicAssets ?? [],
2975
+ });
2976
+
2977
+ const bundledWorker = await bundleRouterModule({
2978
+ code: await readFile(workerPath, "utf8"),
2979
+ filename: workerPath,
2980
+ minify: true,
2981
+ modulePreload: false,
2982
+ outfile: "_worker.js",
2983
+ platform: "browser",
2984
+ plugins: [cloudflareWorkspaceRuntimePlugin()],
2985
+ preserveExports: true,
2986
+ target: "es2022",
2987
+ });
2988
+ await writeFile(join(options.outDir, "_worker.js"), bundledWorker.code);
2989
+
2990
+ const files = await collectArtifactFiles(options.outDir, "");
2991
+ const manifest = {
2992
+ files,
2993
+ runtime: "cloudflare-pages",
2994
+ totalBytes: files.reduce((total, file) => total + file.bytes, 0),
2995
+ version: 1,
2996
+ worker: "_worker.js",
2997
+ } satisfies CloudflarePagesArtifactManifest;
2998
+
2999
+ await writeFile(
3000
+ join(options.outDir, "mreact-cloudflare-pages-artifact.json"),
3001
+ JSON.stringify(manifest, null, 2),
3002
+ );
3003
+
3004
+ return manifest;
3005
+ }
3006
+
2933
3007
  async function assertRequiredBuildFile(path: string): Promise<void> {
2934
3008
  try {
2935
3009
  const info = await stat(path);
@@ -2943,6 +3017,31 @@ async function assertRequiredBuildFile(path: string): Promise<void> {
2943
3017
  throw new Error(`Missing required mreact build artifact: ${path}`);
2944
3018
  }
2945
3019
 
3020
+ function assertPackageOutputDoesNotReplaceBuildOutput(options: {
3021
+ fromDir: string;
3022
+ outDir: string;
3023
+ }): void {
3024
+ if (resolve(options.fromDir) === resolve(options.outDir)) {
3025
+ throw new Error("Package output directory must be different from the mreact build output directory.");
3026
+ }
3027
+ }
3028
+
3029
+ async function copyCloudflarePagesPublicAssets(options: {
3030
+ clientDir: string;
3031
+ outDir: string;
3032
+ publicAssets: readonly string[];
3033
+ }): Promise<void> {
3034
+ for (const asset of options.publicAssets) {
3035
+ if (!asset.startsWith("/") || asset.startsWith("//") || asset.includes("..")) {
3036
+ continue;
3037
+ }
3038
+
3039
+ const relativeAsset = asset.slice(1);
3040
+ await mkdir(dirname(join(options.outDir, relativeAsset)), { recursive: true });
3041
+ await copyFile(join(options.clientDir, relativeAsset), join(options.outDir, relativeAsset));
3042
+ }
3043
+ }
3044
+
2946
3045
  async function copyAwsLambdaProjectMetadata(options: {
2947
3046
  fromDir: string;
2948
3047
  outDir: string;
@@ -2965,7 +3064,7 @@ async function copyAwsLambdaProjectMetadata(options: {
2965
3064
  }
2966
3065
  }
2967
3066
 
2968
- async function collectAwsLambdaArtifactFiles(
3067
+ async function collectArtifactFiles(
2969
3068
  rootDir: string,
2970
3069
  relativeDir: string,
2971
3070
  ): Promise<Array<{ bytes: number; path: string }>> {
@@ -2977,7 +3076,7 @@ async function collectAwsLambdaArtifactFiles(
2977
3076
  const absolutePath = join(rootDir, relativePath);
2978
3077
 
2979
3078
  if (entry.isDirectory()) {
2980
- files.push(...(await collectAwsLambdaArtifactFiles(rootDir, relativePath)));
3079
+ files.push(...(await collectArtifactFiles(rootDir, relativePath)));
2981
3080
  continue;
2982
3081
  }
2983
3082
 
@@ -14,6 +14,7 @@ export interface RouterBundleOptions {
14
14
  define?: Record<string, string> | undefined;
15
15
  filename: string;
16
16
  minify?: boolean | undefined;
17
+ modulePreload?: boolean | undefined;
17
18
  outfile?: string | undefined;
18
19
  platform: "browser" | "node";
19
20
  preserveExports?: boolean | undefined;
@@ -157,6 +158,7 @@ export async function bundleRouterModule(options: RouterBundleOptions): Promise<
157
158
  }
158
159
  : false,
159
160
  minify: options.minify === true,
161
+ modulePreload: options.modulePreload ?? true,
160
162
  sourcemap: options.sourceMap === true,
161
163
  ssr: options.platform === "node",
162
164
  target: options.target ?? "es2022",
@@ -18,6 +18,7 @@ export interface ParsedCliArguments {
18
18
  hostPolicy?: RequestHostPolicy | undefined;
19
19
  log?: CliRequestLogMode | undefined;
20
20
  out?: string | undefined;
21
+ port?: number | undefined;
21
22
  routeArg?: string | undefined;
22
23
  target?: CliBuildTarget | undefined;
23
24
  }
@@ -47,6 +48,17 @@ export function parseCliArguments(argv: readonly string[]): ParsedCliArguments {
47
48
  continue;
48
49
  }
49
50
 
51
+ if (value === "--port") {
52
+ parsed.port = parseCliPort(readOptionValue(argv, index, "port"));
53
+ index += 1;
54
+ continue;
55
+ }
56
+
57
+ if (value.startsWith("--port=")) {
58
+ parsed.port = parseCliPort(value.slice("--port=".length));
59
+ continue;
60
+ }
61
+
50
62
  if (value === "--host") {
51
63
  parsed.host = readOptionValue(argv, index, "host");
52
64
  index += 1;
@@ -171,17 +183,22 @@ export function formatCliHelp(command?: string | undefined): string {
171
183
 
172
184
  if (command === "package") {
173
185
  return [
174
- "Usage: mreact-router package aws-lambda [options]",
186
+ "Usage: mreact-router package <target> [options]",
187
+ "",
188
+ "Package generated build output into a deployable artifact directory.",
175
189
  "",
176
- "Package generated build output into a minimal AWS Lambda asset directory.",
190
+ "Targets:",
191
+ " aws-lambda Minimal AWS Lambda asset directory.",
192
+ " cloudflare-pages Cloudflare Pages advanced mode output with _worker.js.",
177
193
  "",
178
194
  "Options:",
179
- " --from <dir> Build output directory. Default: .mreact",
180
- " --out <dir> Lambda asset output directory. Default: .lambda",
181
- " -h, --help Show this help message.",
195
+ " --from <dir> Build output directory. Default: .mreact",
196
+ " --out <dir> Output directory. Defaults to .lambda for aws-lambda and .mreact/pages for cloudflare-pages.",
197
+ " -h, --help Show this help message.",
182
198
  "",
183
- "Example:",
199
+ "Examples:",
184
200
  " mreact-router package aws-lambda --from .mreact --out .lambda",
201
+ " mreact-router package cloudflare-pages --from .mreact --out .mreact/pages",
185
202
  ].join("\n");
186
203
  }
187
204
 
@@ -217,8 +234,12 @@ export function formatCliHelp(command?: string | undefined): string {
217
234
  "Start the mreact app router development server.",
218
235
  "",
219
236
  "Options:",
237
+ " --port <port> TCP port. Overrides PORT and vite.config.ts server.port.",
220
238
  " --log=requests Print request summaries.",
221
239
  " -h, --help Show this help message.",
240
+ "",
241
+ "Environment:",
242
+ " PORT TCP port when --port is not set.",
222
243
  ].join("\n");
223
244
  }
224
245
 
@@ -232,6 +253,8 @@ export function formatCliHelp(command?: string | undefined): string {
232
253
  " start [outDir] Serve built Node output.",
233
254
  " package aws-lambda --from .mreact --out .lambda",
234
255
  " Package a minimal AWS Lambda asset directory.",
256
+ " package cloudflare-pages --from .mreact --out .mreact/pages",
257
+ " Package Cloudflare Pages advanced mode output.",
235
258
  " help [command] Show help.",
236
259
  "",
237
260
  "Options:",
@@ -284,6 +307,19 @@ export function resolveCliHost(
284
307
  return envValue === undefined || envValue === "" ? "127.0.0.1" : envValue;
285
308
  }
286
309
 
310
+ export function resolveCliDevPort(
311
+ flagValue: number | undefined,
312
+ env: { PORT?: string | undefined },
313
+ viteConfigPort: number | undefined,
314
+ ): number | undefined {
315
+ if (flagValue !== undefined) {
316
+ return flagValue;
317
+ }
318
+
319
+ const envValue = env.PORT;
320
+ return envValue === undefined || envValue === "" ? viteConfigPort : parseCliPort(envValue);
321
+ }
322
+
287
323
  export function resolveCliHostPolicy(
288
324
  flagValue: RequestHostPolicy | undefined,
289
325
  env: { MREACT_ROUTER_HOST_POLICY?: string | undefined },
@@ -331,6 +367,16 @@ function parseCliRequestLogMode(value: string): CliRequestLogMode {
331
367
  throw new Error(`Unsupported log mode ${JSON.stringify(value)}. Expected "requests".`);
332
368
  }
333
369
 
370
+ function parseCliPort(value: string): number {
371
+ const port = Number(value);
372
+
373
+ if (Number.isInteger(port) && port >= 0 && port <= 65535) {
374
+ return port;
375
+ }
376
+
377
+ throw new Error(`Unsupported port ${JSON.stringify(value)}. Expected an integer from 0 to 65535.`);
378
+ }
379
+
334
380
  function parseCliHostPolicy(value: string): RequestHostPolicy {
335
381
  if (value === "strict" || value === "trusted-proxy") {
336
382
  return value;
package/src/cli.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { resolve } from "node:path";
4
- import { buildApp, packageAwsLambdaArtifact } from "./build.js";
4
+ import { buildApp, packageAwsLambdaArtifact, packageCloudflarePagesArtifact } from "./build.js";
5
5
  import {
6
6
  buildTargetsFromCliTarget,
7
7
  createCliRequestLogger,
8
8
  formatCliHelp,
9
9
  parseCliArguments,
10
10
  resolveCliAllowedHosts,
11
+ resolveCliDevPort,
11
12
  resolveCliHost,
12
13
  resolveCliHostPolicy,
13
14
  resolveCliRequestLogMode,
@@ -54,18 +55,27 @@ if (parsed !== undefined) {
54
55
  });
55
56
  console.log(`Built ${result.routes.length} routes.`);
56
57
  } else if (command === "package") {
57
- if (routeArg !== "aws-lambda") {
58
+ if (routeArg === "aws-lambda") {
59
+ const manifest = await packageAwsLambdaArtifact({
60
+ fromDir: resolve(parsed.from ?? ".mreact"),
61
+ outDir: resolve(parsed.out ?? ".lambda"),
62
+ });
63
+ console.log(
64
+ `Packaged AWS Lambda artifact with ${manifest.files.length} files (${manifest.totalBytes} bytes).`,
65
+ );
66
+ } else if (routeArg === "cloudflare-pages") {
67
+ const manifest = await packageCloudflarePagesArtifact({
68
+ fromDir: resolve(parsed.from ?? ".mreact"),
69
+ outDir: resolve(parsed.out ?? ".mreact/pages"),
70
+ });
71
+ console.log(
72
+ `Packaged Cloudflare Pages artifact with ${manifest.files.length} files (${manifest.totalBytes} bytes).`,
73
+ );
74
+ } else {
58
75
  throw new Error(
59
- `Unsupported package target ${JSON.stringify(routeArg)}. Expected "aws-lambda".`,
76
+ `Unsupported package target ${JSON.stringify(routeArg)}. Expected "aws-lambda" or "cloudflare-pages".`,
60
77
  );
61
78
  }
62
- const manifest = await packageAwsLambdaArtifact({
63
- fromDir: resolve(parsed.from ?? ".mreact"),
64
- outDir: resolve(parsed.out ?? ".lambda"),
65
- });
66
- console.log(
67
- `Packaged AWS Lambda artifact with ${manifest.files.length} files (${manifest.totalBytes} bytes).`,
68
- );
69
79
  } else if (command === "dev") {
70
80
  const loaded =
71
81
  routeArg === undefined
@@ -74,7 +84,7 @@ if (parsed !== undefined) {
74
84
  const server = await startDevServer({
75
85
  ...loaded.project,
76
86
  logger,
77
- port: process.env.PORT === undefined ? loaded.serverPort : Number(process.env.PORT),
87
+ port: resolveCliDevPort(parsed.port, process.env, loaded.serverPort),
78
88
  viteConfig: loaded.viteConfig,
79
89
  });
80
90
  console.log(`mreact app router ready at ${server.url}`);
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { buildApp, packageAwsLambdaArtifact } from "./build.js";
1
+ export { buildApp, packageAwsLambdaArtifact, packageCloudflarePagesArtifact } from "./build.js";
2
2
  export { assetHref, assetPreloadLinks } from "./assets.js";
3
3
  export { cacheControl, createMemoryRouteCache, revalidatePath } from "./cache.js";
4
4
  export { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from "./cookies.js";
@@ -74,7 +74,9 @@ export type {
74
74
  BuildAppOptions,
75
75
  BuildAppResult,
76
76
  BuiltImportPolicyArtifact,
77
+ CloudflarePagesArtifactManifest,
77
78
  PackageAwsLambdaArtifactOptions,
79
+ PackageCloudflarePagesArtifactOptions,
78
80
  } from "./build.js";
79
81
  export type {
80
82
  InferLoaderData,