@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/src/actions.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
1
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { access, readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import {
6
- collectFormActionReferenceNames,
7
- collectFormActionReferences,
6
+ collectFormActionExpressionReferences,
8
7
  hasModuleDirective,
9
8
  } from "@reckona/mreact-compiler";
10
9
  import {
@@ -31,6 +30,10 @@ import {
31
30
  readExistingFormCsrfToken,
32
31
  validateFormCsrf,
33
32
  } from "./csrf.js";
33
+ import {
34
+ collectRuntimeInferredServerActions,
35
+ type ServerActionInferenceDiagnostic,
36
+ } from "./server-action-inference.js";
34
37
 
35
38
  function isProductionEnvironment(): boolean {
36
39
  return process.env.NODE_ENV === "production";
@@ -137,6 +140,7 @@ export interface PreparedRouteActions {
137
140
  actionNonce?: string;
138
141
  code: string;
139
142
  csrfToken?: string;
143
+ diagnostics?: ServerActionInferenceDiagnostic[] | undefined;
140
144
  // True when the cookie should be (re)set on the response. False means
141
145
  // the incoming request already carried a valid CSRF cookie that the
142
146
  // render is reusing -- skipping Set-Cookie avoids cookie thrash across
@@ -151,11 +155,24 @@ interface ActionReference {
151
155
  moduleId: string;
152
156
  }
153
157
 
158
+ export interface PreparedFormActionReference {
159
+ end: number;
160
+ expression: string;
161
+ expressionEnd: number;
162
+ expressionStart: number;
163
+ exportName: string;
164
+ inferred: boolean;
165
+ moduleId: string;
166
+ sourceHash: string;
167
+ start: number;
168
+ }
169
+
154
170
  const inferredServerActionReferences = new Map<string, Map<string, Map<string, ActionReference>>>();
155
171
 
156
172
  export async function prepareRouteServerActions(options: {
157
173
  appDir: string;
158
174
  code: string;
175
+ formActionReferences?: readonly PreparedFormActionReference[] | undefined;
159
176
  pageFile: string;
160
177
  request?: Request | undefined;
161
178
  }): Promise<PreparedRouteActions> {
@@ -164,11 +181,20 @@ export async function prepareRouteServerActions(options: {
164
181
  return { code: options.code, hasFormActions: false };
165
182
  }
166
183
 
167
- const references = await collectImportedServerActions(options);
184
+ const inference =
185
+ options.formActionReferences === undefined
186
+ ? await collectImportedServerActions(options)
187
+ : {
188
+ diagnostics: [],
189
+ references: formActionReferenceMap(options.code, options.formActionReferences),
190
+ };
191
+ const { diagnostics, references } = inference;
168
192
 
169
193
  if (references.size === 0) {
170
194
  replaceInferredServerActionReferences(options.appDir, options.pageFile, new Map());
171
- return { code: options.code, hasFormActions: false };
195
+ return diagnostics.length === 0
196
+ ? { code: options.code, hasFormActions: false }
197
+ : { code: options.code, diagnostics, hasFormActions: false };
172
198
  }
173
199
 
174
200
  // Reuse the existing CSRF token when the browser already sent one.
@@ -189,7 +215,9 @@ export async function prepareRouteServerActions(options: {
189
215
 
190
216
  if (lowered === options.code) {
191
217
  replaceInferredServerActionReferences(options.appDir, options.pageFile, new Map());
192
- return { code: options.code, hasFormActions: false };
218
+ return diagnostics.length === 0
219
+ ? { code: options.code, hasFormActions: false }
220
+ : { code: options.code, diagnostics, hasFormActions: false };
193
221
  }
194
222
 
195
223
  replaceInferredServerActionReferences(options.appDir, options.pageFile, references);
@@ -199,12 +227,33 @@ export async function prepareRouteServerActions(options: {
199
227
  code: lowered,
200
228
  csrfToken,
201
229
  csrfTokenIsNew,
230
+ diagnostics,
202
231
  hasFormActions: true,
203
232
  };
204
233
  }
205
234
 
235
+ function formActionReferenceMap(
236
+ code: string,
237
+ references: readonly PreparedFormActionReference[],
238
+ ): Map<string, ActionReference> {
239
+ const sourceHash = formActionSourceHash(code);
240
+
241
+ return new Map(
242
+ references
243
+ .filter((reference) => reference.sourceHash === sourceHash)
244
+ .map((reference) => [
245
+ formActionOccurrenceKey(reference),
246
+ {
247
+ exportName: reference.exportName,
248
+ inferred: reference.inferred,
249
+ moduleId: reference.moduleId,
250
+ },
251
+ ]),
252
+ );
253
+ }
254
+
206
255
  function hasFormActionCandidate(code: string, filename: string): boolean {
207
- return collectFormActionReferenceNames({ code, filename }).length > 0;
256
+ return collectFormActionExpressionReferences({ code, filename }).length > 0;
208
257
  }
209
258
 
210
259
  export async function dispatchServerActionRequest(options: {
@@ -571,10 +620,10 @@ function lowerFormActions(options: {
571
620
  references: Map<string, ActionReference>;
572
621
  }): string {
573
622
  let code = options.code;
574
- const formReferences = collectFormActionReferences({ code: options.code });
623
+ const formReferences = collectFormActionExpressionReferences({ code: options.code });
575
624
 
576
625
  for (const formReference of [...formReferences].reverse()) {
577
- const reference = options.references.get(formReference.name);
626
+ const reference = options.references.get(formActionOccurrenceKey(formReference));
578
627
 
579
628
  if (reference === undefined) {
580
629
  continue;
@@ -586,7 +635,7 @@ function lowerFormActions(options: {
586
635
  csrfToken: options.csrfToken,
587
636
  opening,
588
637
  reference,
589
- referenceName: formReference.name,
638
+ referenceName: formReference.expression,
590
639
  });
591
640
 
592
641
  if (loweredOpening === opening) {
@@ -599,6 +648,26 @@ function lowerFormActions(options: {
599
648
  return code;
600
649
  }
601
650
 
651
+ function formActionOccurrenceKey(reference: {
652
+ end: number;
653
+ expression: string;
654
+ expressionEnd: number;
655
+ expressionStart: number;
656
+ start: number;
657
+ }): string {
658
+ return [
659
+ reference.start,
660
+ reference.end,
661
+ reference.expressionStart,
662
+ reference.expressionEnd,
663
+ reference.expression,
664
+ ].join(":");
665
+ }
666
+
667
+ function formActionSourceHash(code: string): string {
668
+ return createHash("sha256").update(code).digest("base64url");
669
+ }
670
+
602
671
  function lowerFormActionOpening(options: {
603
672
  actionNonce: string;
604
673
  csrfToken: string;
@@ -653,58 +722,19 @@ async function collectImportedServerActions(options: {
653
722
  appDir: string;
654
723
  code: string;
655
724
  pageFile: string;
656
- }): Promise<Map<string, ActionReference>> {
657
- const references = new Map<string, ActionReference>();
658
- const actionNames = new Set(
659
- collectFormActionReferenceNames({ code: options.code, filename: options.pageFile }),
660
- );
661
-
662
- if (actionNames.size === 0) {
663
- return references;
664
- }
665
-
666
- const imports = options.code.matchAll(
667
- /^import\s+\{\s*(?<specifiers>[^}]+)\s*\}\s+from\s+["'](?<source>[^"']+)["'];?/gm,
668
- );
669
-
670
- for (const match of imports) {
671
- const source = match.groups?.source;
672
- const specifiers = match.groups?.specifiers;
673
-
674
- if (source === undefined || specifiers === undefined || !source.startsWith(".")) {
675
- continue;
676
- }
677
-
678
- const file = await resolveSourceFile(dirname(options.pageFile), source);
679
-
680
- if (file === undefined) {
681
- continue;
682
- }
683
-
684
- const moduleId = moduleIdForFile(options.appDir, file);
685
- const inferred = !(await isUseServerFile(file));
686
-
687
- for (const specifier of specifiers.split(",")) {
688
- const [exportName, localName] = specifier.trim().split(/\s+as\s+/);
689
- const imported = exportName?.trim();
690
- const local = localName?.trim() ?? imported;
691
-
692
- if (
693
- imported !== undefined &&
694
- imported.length > 0 &&
695
- local !== undefined &&
696
- actionNames.has(local)
697
- ) {
698
- references.set(local, {
699
- exportName: imported,
700
- inferred,
701
- moduleId,
702
- });
703
- }
704
- }
705
- }
706
-
707
- return references;
725
+ }): Promise<{
726
+ diagnostics: ServerActionInferenceDiagnostic[];
727
+ references: Map<string, ActionReference>;
728
+ }> {
729
+ return await collectRuntimeInferredServerActions({
730
+ appDir: options.appDir,
731
+ code: options.code,
732
+ fileSystem: {
733
+ isUseServerFile,
734
+ resolveSourceFile,
735
+ },
736
+ pageFile: options.pageFile,
737
+ });
708
738
  }
709
739
 
710
740
  function isInferredServerActionReference(options: {