@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.
- package/README.md +3 -1
- package/dist/actions.d.ts +14 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +55 -44
- package/dist/actions.js.map +1 -1
- package/dist/build.d.ts +12 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +151 -52
- package/dist/build.js.map +1 -1
- package/dist/cli-options.d.ts +13 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +73 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -8
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +2 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +4 -0
- package/dist/render.js.map +1 -1
- package/dist/route-source.d.ts +8 -0
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +15 -1
- package/dist/route-source.js.map +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +6 -0
- package/dist/serve.js.map +1 -1
- package/dist/server-action-inference.d.ts +50 -0
- package/dist/server-action-inference.d.ts.map +1 -0
- package/dist/server-action-inference.js +488 -0
- package/dist/server-action-inference.js.map +1 -0
- package/package.json +12 -11
- package/src/actions.ts +92 -62
- package/src/build.ts +231 -71
- package/src/cli-options.ts +103 -0
- package/src/cli.ts +6 -0
- package/src/client.ts +14 -11
- package/src/index.ts +1 -0
- package/src/render.ts +9 -0
- package/src/route-source.ts +25 -0
- package/src/serve.ts +22 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
623
|
+
const formReferences = collectFormActionExpressionReferences({ code: options.code });
|
|
575
624
|
|
|
576
625
|
for (const formReference of [...formReferences].reverse()) {
|
|
577
|
-
const reference = options.references.get(formReference
|
|
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.
|
|
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<
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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: {
|