@reckona/mreact-router 0.0.72 → 0.0.74
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 +1 -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 +44 -44
- package/dist/build.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 +78 -61
- 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: {
|
package/src/build.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { copyFile, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "nod
|
|
|
4
4
|
import { builtinModules } from "node:module";
|
|
5
5
|
import { dirname, join, relative, sep } from "node:path";
|
|
6
6
|
import {
|
|
7
|
-
collectFormActionReferenceNames,
|
|
8
7
|
collectStaticImportReferences,
|
|
9
8
|
collectTopLevelValueExportNames,
|
|
10
9
|
formatDiagnostic,
|
|
@@ -56,6 +55,7 @@ import {
|
|
|
56
55
|
routeClosureMayUseAwaitBoundary,
|
|
57
56
|
stripRouteBuildExports,
|
|
58
57
|
stripRouteClientOnlyExports,
|
|
58
|
+
stripRouteClientSource,
|
|
59
59
|
stripRouteLoaderOnlyExports,
|
|
60
60
|
stripRouteMetadataOnlyExports,
|
|
61
61
|
stripRouteRequestOnlyExports,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
import { collectRouteCssFiles } from "./route-styles.js";
|
|
68
68
|
import { existingRouteShellCandidates } from "./route-shells.js";
|
|
69
69
|
import { sourceModuleCandidates } from "./source-modules.js";
|
|
70
|
+
import { collectBuildInferredServerActions } from "./server-action-inference.js";
|
|
70
71
|
import { workspacePackageFile } from "./workspace-packages.js";
|
|
71
72
|
import type { PluginOption, UserConfig } from "vite";
|
|
72
73
|
|
|
@@ -112,6 +113,7 @@ export interface BuiltServerManifest {
|
|
|
112
113
|
prerenderedRoutes?: Record<string, BuiltPrerenderedRoute>;
|
|
113
114
|
publicAssetBaseUrl?: string;
|
|
114
115
|
routesDir?: string;
|
|
116
|
+
routeServerActionReferences?: Record<string, BuiltServerActionExpressionReference[]>;
|
|
115
117
|
serverActionManifest?: BuiltServerActionReference[];
|
|
116
118
|
serverModuleFiles?: Record<string, string>;
|
|
117
119
|
serverModuleRenderFiles?: Record<string, string>;
|
|
@@ -126,6 +128,18 @@ export interface BuiltServerActionReference {
|
|
|
126
128
|
moduleId: string;
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
export interface BuiltServerActionExpressionReference {
|
|
132
|
+
end: number;
|
|
133
|
+
exportName: string;
|
|
134
|
+
expression: string;
|
|
135
|
+
expressionEnd: number;
|
|
136
|
+
expressionStart: number;
|
|
137
|
+
inferred: boolean;
|
|
138
|
+
moduleId: string;
|
|
139
|
+
sourceHash: string;
|
|
140
|
+
start: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
export interface BuiltServerModuleArtifact {
|
|
130
144
|
analysis?: BuiltRouteSourceAnalysisSummary;
|
|
131
145
|
loader?: BuiltServerModuleOutput;
|
|
@@ -200,7 +214,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
200
214
|
...new Set([...(await collectPublicAssetPaths(project.publicDir)), ...appConventionPublicAssets]),
|
|
201
215
|
].sort();
|
|
202
216
|
|
|
203
|
-
const serverActionManifest = collectBuildServerActionManifest({
|
|
217
|
+
const serverActionManifest = await collectBuildServerActionManifest({
|
|
204
218
|
files,
|
|
205
219
|
projectRoot: project.projectRoot,
|
|
206
220
|
routes,
|
|
@@ -274,6 +288,11 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
274
288
|
});
|
|
275
289
|
}
|
|
276
290
|
|
|
291
|
+
const routeServerActionReferences = Object.fromEntries(
|
|
292
|
+
[...serverActionManifest.routeReferences.entries()].sort(([left], [right]) =>
|
|
293
|
+
left.localeCompare(right),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
277
296
|
const serverManifest = {
|
|
278
297
|
allowedSourceDirs: project.allowedSourceDirs.map((directory) =>
|
|
279
298
|
relative(project.projectRoot, directory),
|
|
@@ -287,7 +306,12 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
|
|
|
287
306
|
...(project.publicAssetBaseUrl === undefined
|
|
288
307
|
? {}
|
|
289
308
|
: { publicAssetBaseUrl: project.publicAssetBaseUrl }),
|
|
290
|
-
...(
|
|
309
|
+
...(Object.keys(routeServerActionReferences).length === 0
|
|
310
|
+
? {}
|
|
311
|
+
: { routeServerActionReferences }),
|
|
312
|
+
...(serverActionManifest.allowedActions.length === 0
|
|
313
|
+
? {}
|
|
314
|
+
: { serverActionManifest: serverActionManifest.allowedActions }),
|
|
291
315
|
...(Object.keys(serverModuleArtifacts.files).length === 0
|
|
292
316
|
? {}
|
|
293
317
|
: { serverModuleFiles: serverModuleArtifacts.files }),
|
|
@@ -588,13 +612,17 @@ function runtimePackageNameForSpecifier(specifier: string): string {
|
|
|
588
612
|
return scope !== undefined && name !== undefined ? `${scope}/${name}` : specifier;
|
|
589
613
|
}
|
|
590
614
|
|
|
591
|
-
function collectBuildServerActionManifest(options: {
|
|
615
|
+
async function collectBuildServerActionManifest(options: {
|
|
592
616
|
files: Record<string, string>;
|
|
593
617
|
projectRoot: string;
|
|
594
618
|
routes: readonly AppRoute[];
|
|
595
619
|
routesDir: string;
|
|
596
|
-
}):
|
|
620
|
+
}): Promise<{
|
|
621
|
+
allowedActions: BuiltServerActionReference[];
|
|
622
|
+
routeReferences: Map<string, BuiltServerActionExpressionReference[]>;
|
|
623
|
+
}> {
|
|
597
624
|
const entries = new Map<string, BuiltServerActionReference>();
|
|
625
|
+
const routeReferences = new Map<string, BuiltServerActionExpressionReference[]>();
|
|
598
626
|
const relativeRoutesDir = relative(options.projectRoot, options.routesDir);
|
|
599
627
|
const routeSourceFiles = new Set(
|
|
600
628
|
options.routes
|
|
@@ -616,26 +644,41 @@ function collectBuildServerActionManifest(options: {
|
|
|
616
644
|
}
|
|
617
645
|
|
|
618
646
|
if (routeSourceFiles.has(file)) {
|
|
619
|
-
|
|
647
|
+
const inference = await collectBuildInferredServerActionReferences({
|
|
620
648
|
file,
|
|
621
649
|
files: options.files,
|
|
622
650
|
relativeRoutesDir,
|
|
623
651
|
source: code,
|
|
624
|
-
})
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
for (const diagnostic of inference.diagnostics) {
|
|
655
|
+
console.warn(formatServerActionInferenceDiagnostic(diagnostic));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
routeReferences.set(file, inference.references);
|
|
659
|
+
|
|
660
|
+
for (const reference of inference.references) {
|
|
625
661
|
const key = `${reference.moduleId}#${reference.exportName}`;
|
|
626
662
|
|
|
627
663
|
if (!entries.has(key)) {
|
|
628
|
-
entries.set(key,
|
|
664
|
+
entries.set(key, {
|
|
665
|
+
exportName: reference.exportName,
|
|
666
|
+
inferred: reference.inferred,
|
|
667
|
+
moduleId: reference.moduleId,
|
|
668
|
+
});
|
|
629
669
|
}
|
|
630
670
|
}
|
|
631
671
|
}
|
|
632
672
|
}
|
|
633
673
|
|
|
634
|
-
return
|
|
635
|
-
left
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
674
|
+
return {
|
|
675
|
+
allowedActions: [...entries.values()].sort((left, right) =>
|
|
676
|
+
left.moduleId === right.moduleId
|
|
677
|
+
? left.exportName.localeCompare(right.exportName)
|
|
678
|
+
: left.moduleId.localeCompare(right.moduleId),
|
|
679
|
+
),
|
|
680
|
+
routeReferences,
|
|
681
|
+
};
|
|
639
682
|
}
|
|
640
683
|
|
|
641
684
|
function collectBuildInferredServerActionReferences(options: {
|
|
@@ -643,54 +686,25 @@ function collectBuildInferredServerActionReferences(options: {
|
|
|
643
686
|
files: Record<string, string>;
|
|
644
687
|
relativeRoutesDir: string;
|
|
645
688
|
source: string;
|
|
646
|
-
}):
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
)
|
|
658
|
-
const source = match.groups?.source;
|
|
659
|
-
const specifiers = match.groups?.specifiers;
|
|
660
|
-
|
|
661
|
-
if (source === undefined || specifiers === undefined || !source.startsWith(".")) {
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const localFile = resolveBuildLocalSourceImport(options.files, options.file, source);
|
|
666
|
-
|
|
667
|
-
if (localFile === undefined) {
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const moduleId = moduleIdForBuildFile(localFile, options.relativeRoutesDir);
|
|
672
|
-
|
|
673
|
-
for (const specifier of specifiers.split(",")) {
|
|
674
|
-
const [exportName, localName] = specifier.trim().split(/\s+as\s+/);
|
|
675
|
-
const imported = exportName?.trim();
|
|
676
|
-
const local = localName?.trim() ?? imported;
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
imported !== undefined &&
|
|
680
|
-
imported.length > 0 &&
|
|
681
|
-
local !== undefined &&
|
|
682
|
-
actionNames.has(local)
|
|
683
|
-
) {
|
|
684
|
-
references.push({ moduleId, exportName: imported, inferred: true });
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return references;
|
|
689
|
+
}): Promise<{
|
|
690
|
+
diagnostics: { code: string; message: string }[];
|
|
691
|
+
references: BuiltServerActionExpressionReference[];
|
|
692
|
+
}> {
|
|
693
|
+
return collectBuildInferredServerActions({
|
|
694
|
+
file: options.file,
|
|
695
|
+
files: options.files,
|
|
696
|
+
relativeRoutesDir: options.relativeRoutesDir,
|
|
697
|
+
resolveSourceImport: (importer, source) =>
|
|
698
|
+
resolveBuildLocalSourceImport(options.files, importer, source),
|
|
699
|
+
source: options.source,
|
|
700
|
+
});
|
|
690
701
|
}
|
|
691
702
|
|
|
692
|
-
function
|
|
693
|
-
|
|
703
|
+
function formatServerActionInferenceDiagnostic(diagnostic: {
|
|
704
|
+
code: string;
|
|
705
|
+
message: string;
|
|
706
|
+
}): string {
|
|
707
|
+
return `${diagnostic.code}: ${diagnostic.message}`;
|
|
694
708
|
}
|
|
695
709
|
|
|
696
710
|
function isSourceModuleFile(file: string): boolean {
|
|
@@ -1038,7 +1052,10 @@ async function buildServerModuleArtifacts(options: {
|
|
|
1038
1052
|
const clientInference = await inferClientRouteModule({
|
|
1039
1053
|
...(route === undefined ? {} : { appDir: options.project.routesDir }),
|
|
1040
1054
|
cache: options.clientRouteInferenceCache,
|
|
1041
|
-
code:
|
|
1055
|
+
code:
|
|
1056
|
+
route === undefined
|
|
1057
|
+
? stripRouteClientOnlyExports(source)
|
|
1058
|
+
: stripRouteClientSource({ code: source, filename: route.file }),
|
|
1042
1059
|
filename: join(options.projectRoot, file),
|
|
1043
1060
|
...(route === undefined ? {} : { routePath: route.path }),
|
|
1044
1061
|
vitePlugins: options.vitePlugins,
|
|
@@ -2467,7 +2484,7 @@ async function writeClientRouteBundles(options: {
|
|
|
2467
2484
|
vitePlugins: options.vitePlugins,
|
|
2468
2485
|
});
|
|
2469
2486
|
const source = await readFile(route.file, "utf8");
|
|
2470
|
-
const clientSource =
|
|
2487
|
+
const clientSource = stripRouteClientSource({ code: source, filename: route.file });
|
|
2471
2488
|
const navigation = detectNavigationRuntimeHint(source);
|
|
2472
2489
|
const references = await collectClientRouteReferences({
|
|
2473
2490
|
appDir: options.appDir,
|
package/src/client.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "./bundle-pipeline.js";
|
|
30
30
|
import type { AppRoute } from "./routes.js";
|
|
31
31
|
import { existingRouteShellCandidates } from "./route-shells.js";
|
|
32
|
-
import {
|
|
32
|
+
import { stripRouteClientSource } from "./route-source.js";
|
|
33
33
|
import { hasJsxSyntax } from "./source-jsx.js";
|
|
34
34
|
import { sourceModuleCandidates } from "./source-modules.js";
|
|
35
35
|
import { escapeHtmlQuotedAttribute as escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
|
|
@@ -147,7 +147,7 @@ export async function routeToClientManifestEntry(
|
|
|
147
147
|
|
|
148
148
|
const code = await readFile(route.file, "utf8");
|
|
149
149
|
const inference = await inferClientRouteModule({
|
|
150
|
-
code:
|
|
150
|
+
code: stripRouteClientSource({ code, filename: route.file }),
|
|
151
151
|
filename: route.file,
|
|
152
152
|
routePath: route.path,
|
|
153
153
|
});
|
|
@@ -311,9 +311,10 @@ export async function collectClientRouteReferences(options: {
|
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
for (const referenceFile of inference.clientReferenceSourceFiles) {
|
|
314
|
-
const code =
|
|
315
|
-
await readClientRouteSource({ cache, filename: referenceFile, sourceTransform }),
|
|
316
|
-
|
|
314
|
+
const code = stripRouteClientSource({
|
|
315
|
+
code: await readClientRouteSource({ cache, filename: referenceFile, sourceTransform }),
|
|
316
|
+
filename: referenceFile,
|
|
317
|
+
});
|
|
317
318
|
await addSource({ code, filename: referenceFile });
|
|
318
319
|
}
|
|
319
320
|
};
|
|
@@ -327,9 +328,10 @@ export async function collectClientRouteReferences(options: {
|
|
|
327
328
|
|
|
328
329
|
if (options.appDir !== undefined) {
|
|
329
330
|
for (const shell of await clientShellFilesForPage(options.appDir, options.filename)) {
|
|
330
|
-
const code =
|
|
331
|
-
await readClientRouteSource({ cache, filename: shell, sourceTransform }),
|
|
332
|
-
|
|
331
|
+
const code = stripRouteClientSource({
|
|
332
|
+
code: await readClientRouteSource({ cache, filename: shell, sourceTransform }),
|
|
333
|
+
filename: shell,
|
|
334
|
+
});
|
|
333
335
|
const moduleContext = await compilerModuleContextForSource({
|
|
334
336
|
cache,
|
|
335
337
|
code,
|
|
@@ -419,13 +421,14 @@ async function inferClientRouteShellModules(options: {
|
|
|
419
421
|
}): Promise<ClientRouteInferenceResult[]> {
|
|
420
422
|
return Promise.all(
|
|
421
423
|
(await clientShellFilesForPage(options.appDir, options.filename)).map(async (shell) => {
|
|
422
|
-
const code =
|
|
423
|
-
await readClientRouteSource({
|
|
424
|
+
const code = stripRouteClientSource({
|
|
425
|
+
code: await readClientRouteSource({
|
|
424
426
|
cache: options.cache,
|
|
425
427
|
filename: shell,
|
|
426
428
|
sourceTransform: options.sourceTransform,
|
|
427
429
|
}),
|
|
428
|
-
|
|
430
|
+
filename: shell,
|
|
431
|
+
});
|
|
429
432
|
return await inferClientRouteModuleSource({
|
|
430
433
|
cache: options.cache,
|
|
431
434
|
code,
|
package/src/index.ts
CHANGED
package/src/render.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type { AppRoute, RouteMatcher } from "./routes.js";
|
|
|
45
45
|
import { appFileConventionContentType, type AppFileConvention } from "./file-conventions.js";
|
|
46
46
|
import {
|
|
47
47
|
type AppRouterServerActionOptions,
|
|
48
|
+
type PreparedFormActionReference,
|
|
48
49
|
dispatchServerActionRequest,
|
|
49
50
|
prepareRouteServerActions,
|
|
50
51
|
} from "./actions.js";
|
|
@@ -148,6 +149,10 @@ export interface RenderAppRequestOptions {
|
|
|
148
149
|
serverModuleCacheVersion?: string | undefined;
|
|
149
150
|
serverSourceFiles?: ReadonlyMap<string, string> | undefined;
|
|
150
151
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
152
|
+
serverActionReferencesByFile?: ReadonlyMap<
|
|
153
|
+
string,
|
|
154
|
+
readonly PreparedFormActionReference[]
|
|
155
|
+
> | undefined;
|
|
151
156
|
skipMiddleware?: boolean | undefined;
|
|
152
157
|
preload?: AppRouterRenderPreload | undefined;
|
|
153
158
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
@@ -904,9 +909,13 @@ async function renderAppRequestInternal(options: RenderAppRequestOptions): Promi
|
|
|
904
909
|
const preparedActions = await prepareRouteServerActions({
|
|
905
910
|
appDir: options.appDir,
|
|
906
911
|
code: originalCode,
|
|
912
|
+
formActionReferences: options.serverActionReferencesByFile?.get(matched.route.file),
|
|
907
913
|
pageFile: matched.route.file,
|
|
908
914
|
request: options.request,
|
|
909
915
|
});
|
|
916
|
+
for (const diagnostic of preparedActions.diagnostics ?? []) {
|
|
917
|
+
console.warn(`${diagnostic.code}: ${diagnostic.message}`);
|
|
918
|
+
}
|
|
910
919
|
finishRenderTimingPhase(timing, phaseStartedAt, "serverActionsMs");
|
|
911
920
|
const code = preparedActions.code;
|
|
912
921
|
phaseStartedAt = renderTimingPhaseStartedAt(timing);
|
package/src/route-source.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, isAbsolute, join } from "node:path";
|
|
2
2
|
import {
|
|
3
|
+
collectFormActionExpressionReferences,
|
|
3
4
|
collectTopLevelValueExportNames,
|
|
4
5
|
collectJsxComponentRootNames,
|
|
5
6
|
collectStaticImportReferences,
|
|
@@ -49,6 +50,30 @@ export function stripRouteClientOnlyExports(code: string): string {
|
|
|
49
50
|
}));
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export function stripRouteClientSource(input: {
|
|
54
|
+
code: string;
|
|
55
|
+
filename?: string | undefined;
|
|
56
|
+
}): string {
|
|
57
|
+
return stripRouteClientFormActionExpressions({
|
|
58
|
+
code: stripRouteClientOnlyExports(input.code),
|
|
59
|
+
filename: input.filename,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function stripRouteClientFormActionExpressions(input: {
|
|
64
|
+
code: string;
|
|
65
|
+
filename?: string | undefined;
|
|
66
|
+
}): string {
|
|
67
|
+
const references = collectFormActionExpressionReferences(input);
|
|
68
|
+
let code = input.code;
|
|
69
|
+
|
|
70
|
+
for (const reference of [...references].reverse()) {
|
|
71
|
+
code = `${code.slice(0, reference.expressionStart)}undefined${code.slice(reference.expressionEnd)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return code;
|
|
75
|
+
}
|
|
76
|
+
|
|
52
77
|
export function stripRouteBuildExports(code: string): string {
|
|
53
78
|
return stripRouteClientOnlyExports(code);
|
|
54
79
|
}
|
package/src/serve.ts
CHANGED
|
@@ -50,6 +50,20 @@ interface BuiltRuntime {
|
|
|
50
50
|
prerenderedRoutes: Map<string, BuiltPrerenderedRoute>;
|
|
51
51
|
routeMatcher: RouteMatcher;
|
|
52
52
|
routes: readonly AppRoute[];
|
|
53
|
+
serverActionReferencesByFile: ReadonlyMap<
|
|
54
|
+
string,
|
|
55
|
+
readonly {
|
|
56
|
+
end: number;
|
|
57
|
+
expression: string;
|
|
58
|
+
expressionEnd: number;
|
|
59
|
+
expressionStart: number;
|
|
60
|
+
moduleId: string;
|
|
61
|
+
exportName: string;
|
|
62
|
+
inferred: boolean;
|
|
63
|
+
sourceHash: string;
|
|
64
|
+
start: number;
|
|
65
|
+
}[]
|
|
66
|
+
>;
|
|
53
67
|
serverActionManifest?: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined;
|
|
54
68
|
serverModuleArtifactLoads: Map<string, Promise<void>>;
|
|
55
69
|
serverModuleFiles: ReadonlyMap<string, string>;
|
|
@@ -701,6 +715,12 @@ async function materializeBuiltRuntime(options: {
|
|
|
701
715
|
const serverSourceFiles = new Map(
|
|
702
716
|
Object.entries(serverManifest.files).map(([file, source]) => [join(appDir, file), source]),
|
|
703
717
|
);
|
|
718
|
+
const serverActionReferencesByFile = new Map(
|
|
719
|
+
Object.entries(serverManifest.routeServerActionReferences ?? {}).map(([file, references]) => [
|
|
720
|
+
join(appDir, file),
|
|
721
|
+
references,
|
|
722
|
+
]),
|
|
723
|
+
);
|
|
704
724
|
const routeMatcher = createRouteMatcher(routes);
|
|
705
725
|
const clientScripts = new Map(
|
|
706
726
|
clientManifest.routes.flatMap((route) =>
|
|
@@ -752,6 +772,7 @@ async function materializeBuiltRuntime(options: {
|
|
|
752
772
|
prerenderedRoutes,
|
|
753
773
|
routeMatcher,
|
|
754
774
|
routes,
|
|
775
|
+
serverActionReferencesByFile,
|
|
755
776
|
...(serverManifest.serverActionManifest === undefined
|
|
756
777
|
? {}
|
|
757
778
|
: { serverActionManifest: serverManifest.serverActionManifest }),
|
|
@@ -1070,6 +1091,7 @@ function builtRenderAppRequestOptions(
|
|
|
1070
1091
|
serverModules: options.runtime.serverModules,
|
|
1071
1092
|
serverModuleCacheVersion: options.runtime.serverModuleCacheVersion,
|
|
1072
1093
|
serverSourceFiles: options.runtime.serverSourceFiles,
|
|
1094
|
+
serverActionReferencesByFile: options.runtime.serverActionReferencesByFile,
|
|
1073
1095
|
serverActions: mergeBuiltServerActionOptions(
|
|
1074
1096
|
options.serverActions,
|
|
1075
1097
|
options.runtime.serverActionManifest,
|