@reckona/mreact-router 0.0.73 → 0.0.75

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 (47) hide show
  1. package/README.md +3 -1
  2. package/dist/actions.d.ts +14 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +55 -44
  5. package/dist/actions.js.map +1 -1
  6. package/dist/build.d.ts +12 -0
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +151 -52
  9. package/dist/build.js.map +1 -1
  10. package/dist/cli-options.d.ts +13 -0
  11. package/dist/cli-options.d.ts.map +1 -1
  12. package/dist/cli-options.js +73 -0
  13. package/dist/cli-options.js.map +1 -1
  14. package/dist/cli.js +4 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +17 -8
  18. package/dist/client.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/render.d.ts +2 -1
  23. package/dist/render.d.ts.map +1 -1
  24. package/dist/render.js +4 -0
  25. package/dist/render.js.map +1 -1
  26. package/dist/route-source.d.ts +8 -0
  27. package/dist/route-source.d.ts.map +1 -1
  28. package/dist/route-source.js +15 -1
  29. package/dist/route-source.js.map +1 -1
  30. package/dist/serve.d.ts.map +1 -1
  31. package/dist/serve.js +6 -0
  32. package/dist/serve.js.map +1 -1
  33. package/dist/server-action-inference.d.ts +50 -0
  34. package/dist/server-action-inference.d.ts.map +1 -0
  35. package/dist/server-action-inference.js +488 -0
  36. package/dist/server-action-inference.js.map +1 -0
  37. package/package.json +12 -11
  38. package/src/actions.ts +92 -62
  39. package/src/build.ts +231 -71
  40. package/src/cli-options.ts +103 -0
  41. package/src/cli.ts +6 -0
  42. package/src/client.ts +14 -11
  43. package/src/index.ts +1 -0
  44. package/src/render.ts +9 -0
  45. package/src/route-source.ts +25 -0
  46. package/src/serve.ts +22 -0
  47. package/src/server-action-inference.ts +804 -0
package/dist/build.js CHANGED
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { builtinModules } from "node:module";
4
4
  import { dirname, join, relative, sep } from "node:path";
5
- import { collectFormActionReferenceNames, collectStaticImportReferences, collectTopLevelValueExportNames, formatDiagnostic, hasModuleDirective, transform, } from "@reckona/mreact-compiler";
5
+ import { collectStaticImportReferences, collectTopLevelValueExportNames, formatDiagnostic, hasModuleDirective, transform, } from "@reckona/mreact-compiler";
6
6
  import { transformCompilerModuleContext } from "@reckona/mreact-compiler/internal";
7
7
  import { buildClientRouteBatchOutput, buildNavigationRuntimeBundle, clientScriptForPath, compilerModuleContextForSource, collectClientRouteReferences, createClientRouteInferenceCache, detectClientNavigationHint, detectNavigationRuntimeHint, formatClientRouteInferenceDiagnostic, inferClientRouteModule, routeIdForPath, } from "./client.js";
8
8
  import { bundleAppRouterSourceModule, importAppRouterSourceModule } from "./module-runner.js";
@@ -12,11 +12,12 @@ import { resolveAppRouterProjectOptions, resolveBuildTargets, } from "./config.j
12
12
  import { routeCachePolicyFromSource } from "./cache.js";
13
13
  import { bundleMiddlewareModuleCode, renderAppRequest } from "./render.js";
14
14
  import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
15
- import { hasGenerateStaticParamsExport, hasLoaderExport, hasPrerenderExport, isStreamRouteSource, routeClosureMayUseAwaitBoundary, stripRouteBuildExports, stripRouteClientOnlyExports, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, stripRouteRequestOnlyExports, } from "./route-source.js";
15
+ import { hasGenerateStaticParamsExport, hasLoaderExport, hasPrerenderExport, isStreamRouteSource, routeClosureMayUseAwaitBoundary, stripRouteBuildExports, stripRouteClientOnlyExports, stripRouteClientSource, stripRouteLoaderOnlyExports, stripRouteMetadataOnlyExports, stripRouteRequestOnlyExports, } from "./route-source.js";
16
16
  import { bundleRouterModule, } from "./bundle-pipeline.js";
17
17
  import { collectRouteCssFiles } from "./route-styles.js";
18
18
  import { existingRouteShellCandidates } from "./route-shells.js";
19
19
  import { sourceModuleCandidates } from "./source-modules.js";
20
+ import { collectBuildInferredServerActions } from "./server-action-inference.js";
20
21
  import { workspacePackageFile } from "./workspace-packages.js";
21
22
  const nativeEscapeTransform = {
22
23
  batchImportName: "escapeHtmlBatch",
@@ -48,7 +49,7 @@ export async function buildApp(options) {
48
49
  const publicAssets = [
49
50
  ...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
50
51
  ].sort();
51
- const serverActionManifest = collectBuildServerActionManifest({
52
+ const serverActionManifest = await collectBuildServerActionManifest({
52
53
  files,
53
54
  projectRoot: project.projectRoot,
54
55
  routes,
@@ -64,7 +65,7 @@ export async function buildApp(options) {
64
65
  routes,
65
66
  vitePlugins,
66
67
  });
67
- const generatedImportPolicy = buildGeneratedImportPolicy({
68
+ const generatedImportPolicy = await buildGeneratedImportPolicy({
68
69
  files,
69
70
  projectRoot: project.projectRoot,
70
71
  routes,
@@ -116,6 +117,7 @@ export async function buildApp(options) {
116
117
  vitePlugins,
117
118
  });
118
119
  }
120
+ const routeServerActionReferences = Object.fromEntries([...serverActionManifest.routeReferences.entries()].sort(([left], [right]) => left.localeCompare(right)));
119
121
  const serverManifest = {
120
122
  allowedSourceDirs: project.allowedSourceDirs.map((directory) => relative(project.projectRoot, directory)),
121
123
  ...(project.assetBaseUrl === undefined ? {} : { assetBaseUrl: project.assetBaseUrl }),
@@ -127,7 +129,12 @@ export async function buildApp(options) {
127
129
  ...(project.publicAssetBaseUrl === undefined
128
130
  ? {}
129
131
  : { publicAssetBaseUrl: project.publicAssetBaseUrl }),
130
- ...(serverActionManifest.length === 0 ? {} : { serverActionManifest }),
132
+ ...(Object.keys(routeServerActionReferences).length === 0
133
+ ? {}
134
+ : { routeServerActionReferences }),
135
+ ...(serverActionManifest.allowedActions.length === 0
136
+ ? {}
137
+ : { serverActionManifest: serverActionManifest.allowedActions }),
131
138
  ...(Object.keys(serverModuleArtifacts.files).length === 0
132
139
  ? {}
133
140
  : { serverModuleFiles: serverModuleArtifacts.files }),
@@ -250,15 +257,18 @@ const frameworkRuntimePackages = new Set([
250
257
  "@reckona/mreact-router",
251
258
  "@reckona/mreact-server",
252
259
  ]);
253
- function buildGeneratedImportPolicy(options) {
260
+ const maxRuntimePackageManifestReads = 1000;
261
+ async function buildGeneratedImportPolicy(options) {
254
262
  const routePackages = new Map();
255
263
  const allPackages = new Set();
256
264
  const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
265
+ const packageJsonLookupCache = new Map();
257
266
  for (const route of options.routes) {
258
267
  const file = relative(options.projectRoot, route.file);
259
- const packages = collectRuntimePackagesForFile({
268
+ const packages = await collectRuntimePackagesForFile({
260
269
  file,
261
270
  files: options.files,
271
+ packageJsonLookupCache,
262
272
  projectRoot: options.projectRoot,
263
273
  seen: new Set(),
264
274
  });
@@ -273,9 +283,10 @@ function buildGeneratedImportPolicy(options) {
273
283
  .map((file) => (relativeRoutesDir === "" ? file : `${relativeRoutesDir}/${file}`))
274
284
  .find((file) => options.files[file] !== undefined);
275
285
  if (middlewareFile !== undefined) {
276
- const packages = collectRuntimePackagesForFile({
286
+ const packages = await collectRuntimePackagesForFile({
277
287
  file: middlewareFile,
278
288
  files: options.files,
289
+ packageJsonLookupCache,
279
290
  projectRoot: options.projectRoot,
280
291
  seen: new Set(),
281
292
  });
@@ -292,7 +303,7 @@ function buildGeneratedImportPolicy(options) {
292
303
  version: 1,
293
304
  };
294
305
  }
295
- function collectRuntimePackagesForFile(options) {
306
+ async function collectRuntimePackagesForFile(options) {
296
307
  if (options.seen.has(options.file)) {
297
308
  return [];
298
309
  }
@@ -312,14 +323,22 @@ function collectRuntimePackagesForFile(options) {
312
323
  filename: join(options.projectRoot, options.file),
313
324
  })) {
314
325
  if (isRuntimePackageSpecifier(reference.source)) {
315
- packages.add(runtimePackageNameForSpecifier(reference.source));
326
+ const packageName = runtimePackageNameForSpecifier(reference.source);
327
+ packages.add(packageName);
328
+ for (const optionalPackageName of await collectRuntimeOptionalPackages({
329
+ packageJsonLookupCache: options.packageJsonLookupCache,
330
+ packageName,
331
+ projectRoot: options.projectRoot,
332
+ })) {
333
+ packages.add(optionalPackageName);
334
+ }
316
335
  continue;
317
336
  }
318
337
  const localFile = resolveBuildLocalSourceImport(options.files, options.file, reference.source);
319
338
  if (localFile === undefined) {
320
339
  continue;
321
340
  }
322
- for (const packageName of collectRuntimePackagesForFile({
341
+ for (const packageName of await collectRuntimePackagesForFile({
323
342
  ...options,
324
343
  file: localFile,
325
344
  })) {
@@ -329,12 +348,99 @@ function collectRuntimePackagesForFile(options) {
329
348
  options.seen.delete(options.file);
330
349
  return [...packages].sort();
331
350
  }
351
+ async function collectRuntimeOptionalPackages(options) {
352
+ const optionalPackages = new Set();
353
+ const seenPackageJson = new Set();
354
+ const queue = [
355
+ { packageName: options.packageName, optional: false, startDir: options.projectRoot },
356
+ ];
357
+ for (let index = 0; index < queue.length && seenPackageJson.size < maxRuntimePackageManifestReads; index += 1) {
358
+ const item = queue[index];
359
+ if (item === undefined || !isValidRuntimePackageName(item.packageName)) {
360
+ continue;
361
+ }
362
+ const manifest = await readRuntimePackageManifest({
363
+ cache: options.packageJsonLookupCache,
364
+ packageName: item.packageName,
365
+ startDir: item.startDir,
366
+ });
367
+ if (manifest === undefined || seenPackageJson.has(manifest.packageJsonPath)) {
368
+ continue;
369
+ }
370
+ seenPackageJson.add(manifest.packageJsonPath);
371
+ if (item.optional && isRuntimePackageName(item.packageName)) {
372
+ optionalPackages.add(item.packageName);
373
+ }
374
+ const packageDir = dirname(manifest.packageJsonPath);
375
+ for (const dependencyName of Object.keys(manifest.dependencies ?? {})) {
376
+ if (isValidRuntimePackageName(dependencyName)) {
377
+ queue.push({ packageName: dependencyName, optional: false, startDir: packageDir });
378
+ }
379
+ }
380
+ for (const optionalDependencyName of Object.keys(manifest.optionalDependencies ?? {})) {
381
+ if (isValidRuntimePackageName(optionalDependencyName)) {
382
+ queue.push({ packageName: optionalDependencyName, optional: true, startDir: packageDir });
383
+ }
384
+ }
385
+ }
386
+ return [...optionalPackages].sort();
387
+ }
388
+ async function readRuntimePackageManifest(options) {
389
+ const key = `${options.startDir}\0${options.packageName}`;
390
+ const cached = options.cache.get(key);
391
+ const manifest = cached ??
392
+ findRuntimePackageJson(options.packageName, options.startDir).then(async (packageJsonPath) => {
393
+ if (packageJsonPath === undefined) {
394
+ return undefined;
395
+ }
396
+ try {
397
+ const json = JSON.parse(await readFile(packageJsonPath, "utf8"));
398
+ return { ...json, packageJsonPath };
399
+ }
400
+ catch {
401
+ return undefined;
402
+ }
403
+ });
404
+ if (cached === undefined) {
405
+ options.cache.set(key, manifest);
406
+ }
407
+ return (await manifest);
408
+ }
409
+ async function findRuntimePackageJson(packageName, startDir) {
410
+ if (!isValidRuntimePackageName(packageName)) {
411
+ return undefined;
412
+ }
413
+ let current = startDir;
414
+ while (true) {
415
+ const candidate = join(current, "node_modules", ...packageName.split("/"), "package.json");
416
+ try {
417
+ if ((await stat(candidate)).isFile()) {
418
+ return candidate;
419
+ }
420
+ }
421
+ catch {
422
+ // Keep walking toward the filesystem root.
423
+ }
424
+ const parent = dirname(current);
425
+ if (parent === current) {
426
+ return undefined;
427
+ }
428
+ current = parent;
429
+ }
430
+ }
332
431
  function isRuntimePackageSpecifier(specifier) {
333
432
  if (specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(specifier)) {
334
433
  return false;
335
434
  }
336
435
  const packageName = runtimePackageNameForSpecifier(specifier);
337
- return !nodeBuiltinPackages.has(specifier) && !frameworkRuntimePackages.has(packageName);
436
+ return !nodeBuiltinPackages.has(specifier) && isRuntimePackageName(packageName);
437
+ }
438
+ function isRuntimePackageName(packageName) {
439
+ return !frameworkRuntimePackages.has(packageName);
440
+ }
441
+ function isValidRuntimePackageName(packageName) {
442
+ return (/^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/u.test(packageName) &&
443
+ !packageName.includes(".."));
338
444
  }
339
445
  function runtimePackageNameForSpecifier(specifier) {
340
446
  if (!specifier.startsWith("@")) {
@@ -343,8 +449,9 @@ function runtimePackageNameForSpecifier(specifier) {
343
449
  const [scope, name] = specifier.split("/");
344
450
  return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
345
451
  }
346
- function collectBuildServerActionManifest(options) {
452
+ async function collectBuildServerActionManifest(options) {
347
453
  const entries = new Map();
454
+ const routeReferences = new Map();
348
455
  const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
349
456
  const routeSourceFiles = new Set(options.routes
350
457
  .filter((route) => route.kind === "page")
@@ -360,56 +467,46 @@ function collectBuildServerActionManifest(options) {
360
467
  }
361
468
  }
362
469
  if (routeSourceFiles.has(file)) {
363
- for (const reference of collectBuildInferredServerActionReferences({
470
+ const inference = await collectBuildInferredServerActionReferences({
364
471
  file,
365
472
  files: options.files,
366
473
  relativeRoutesDir,
367
474
  source: code,
368
- })) {
475
+ });
476
+ for (const diagnostic of inference.diagnostics) {
477
+ console.warn(formatServerActionInferenceDiagnostic(diagnostic));
478
+ }
479
+ routeReferences.set(file, inference.references);
480
+ for (const reference of inference.references) {
369
481
  const key = `${reference.moduleId}#${reference.exportName}`;
370
482
  if (!entries.has(key)) {
371
- entries.set(key, reference);
483
+ entries.set(key, {
484
+ exportName: reference.exportName,
485
+ inferred: reference.inferred,
486
+ moduleId: reference.moduleId,
487
+ });
372
488
  }
373
489
  }
374
490
  }
375
491
  }
376
- return [...entries.values()].sort((left, right) => left.moduleId === right.moduleId
377
- ? left.exportName.localeCompare(right.exportName)
378
- : left.moduleId.localeCompare(right.moduleId));
492
+ return {
493
+ allowedActions: [...entries.values()].sort((left, right) => left.moduleId === right.moduleId
494
+ ? left.exportName.localeCompare(right.exportName)
495
+ : left.moduleId.localeCompare(right.moduleId)),
496
+ routeReferences,
497
+ };
379
498
  }
380
499
  function collectBuildInferredServerActionReferences(options) {
381
- const actionNames = collectFormActionNames(options.source);
382
- if (actionNames.size === 0) {
383
- return [];
384
- }
385
- const references = [];
386
- for (const match of options.source.matchAll(/^import\s+\{\s*(?<specifiers>[^}]+)\s*\}\s+from\s+["'](?<source>[^"']+)["'];?/gm)) {
387
- const source = match.groups?.source;
388
- const specifiers = match.groups?.specifiers;
389
- if (source === undefined || specifiers === undefined || !source.startsWith(".")) {
390
- continue;
391
- }
392
- const localFile = resolveBuildLocalSourceImport(options.files, options.file, source);
393
- if (localFile === undefined) {
394
- continue;
395
- }
396
- const moduleId = moduleIdForBuildFile(localFile, options.relativeRoutesDir);
397
- for (const specifier of specifiers.split(",")) {
398
- const [exportName, localName] = specifier.trim().split(/\s+as\s+/);
399
- const imported = exportName?.trim();
400
- const local = localName?.trim() ?? imported;
401
- if (imported !== undefined &&
402
- imported.length > 0 &&
403
- local !== undefined &&
404
- actionNames.has(local)) {
405
- references.push({ moduleId, exportName: imported, inferred: true });
406
- }
407
- }
408
- }
409
- return references;
500
+ return collectBuildInferredServerActions({
501
+ file: options.file,
502
+ files: options.files,
503
+ relativeRoutesDir: options.relativeRoutesDir,
504
+ resolveSourceImport: (importer, source) => resolveBuildLocalSourceImport(options.files, importer, source),
505
+ source: options.source,
506
+ });
410
507
  }
411
- function collectFormActionNames(code) {
412
- return new Set(collectFormActionReferenceNames({ code }));
508
+ function formatServerActionInferenceDiagnostic(diagnostic) {
509
+ return `${diagnostic.code}: ${diagnostic.message}`;
413
510
  }
414
511
  function isSourceModuleFile(file) {
415
512
  return /\.(?:mreact\.tsx|tsx?|jsx?|mjs|mts|cjs|cts)$/.test(file);
@@ -667,7 +764,9 @@ async function buildServerModuleArtifacts(options) {
667
764
  const clientInference = await inferClientRouteModule({
668
765
  ...(route === undefined ? {} : { appDir: options.project.routesDir }),
669
766
  cache: options.clientRouteInferenceCache,
670
- code: stripRouteClientOnlyExports(source),
767
+ code: route === undefined
768
+ ? stripRouteClientOnlyExports(source)
769
+ : stripRouteClientSource({ code: source, filename: route.file }),
671
770
  filename: join(options.projectRoot, file),
672
771
  ...(route === undefined ? {} : { routePath: route.path }),
673
772
  vitePlugins: options.vitePlugins,
@@ -1781,7 +1880,7 @@ async function writeClientRouteBundles(options) {
1781
1880
  vitePlugins: options.vitePlugins,
1782
1881
  });
1783
1882
  const source = await readFile(route.file, "utf8");
1784
- const clientSource = stripRouteClientOnlyExports(source);
1883
+ const clientSource = stripRouteClientSource({ code: source, filename: route.file });
1785
1884
  const navigation = detectNavigationRuntimeHint(source);
1786
1885
  const references = await collectClientRouteReferences({
1787
1886
  appDir: options.appDir,