@chidchanun/bcp 0.1.10 → 0.1.12
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/CHANGELOG.md +43 -2
- package/README.md +131 -9
- package/docs/form-actions.md +314 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +523 -0
- package/packages/bundler/src/server-production-middleware.ts +17 -0
- package/packages/client/src/form.tsx +548 -0
- package/packages/client/src/index.tsx +10 -0
- package/packages/client/src/server.ts +6 -0
- package/packages/server/src/form-action-dev-proxy.ts +601 -0
- package/packages/server/src/form-action-transport.ts +271 -0
- package/packages/server/src/form-action.ts +493 -0
- package/packages/server/src/middleware-dev-server.ts +56 -2
- package/packages/server/src/standalone-production-runtime-v2-action.ts +765 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
fileURLToPath,
|
|
5
|
+
} from "node:url";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
build,
|
|
9
|
+
} from "esbuild";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createServerEnvironmentDefines,
|
|
13
|
+
} from "../../env/src/index.js";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Route,
|
|
17
|
+
} from "../../router/src/index.js";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
findPageGuardFiles,
|
|
21
|
+
} from "../../server/src/page-guard.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
findPageActionFile,
|
|
25
|
+
} from "../../server/src/form-action.js";
|
|
26
|
+
|
|
27
|
+
export async function buildProductionActions(
|
|
28
|
+
pageRoutes: Route[],
|
|
29
|
+
rootDirectory: string,
|
|
30
|
+
serverDirectory: string
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const outputFile =
|
|
33
|
+
path.join(
|
|
34
|
+
serverDirectory,
|
|
35
|
+
"actions.mjs"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
fs.rmSync(
|
|
39
|
+
outputFile,
|
|
40
|
+
{
|
|
41
|
+
force: true,
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const appDirectory =
|
|
46
|
+
path.join(
|
|
47
|
+
rootDirectory,
|
|
48
|
+
"app"
|
|
49
|
+
);
|
|
50
|
+
const actionRoutes =
|
|
51
|
+
pageRoutes
|
|
52
|
+
.map(
|
|
53
|
+
(route) => ({
|
|
54
|
+
route,
|
|
55
|
+
actionFile:
|
|
56
|
+
findPageActionFile(
|
|
57
|
+
route.filePath
|
|
58
|
+
),
|
|
59
|
+
guardFiles:
|
|
60
|
+
findPageGuardFiles(
|
|
61
|
+
appDirectory,
|
|
62
|
+
route.filePath
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
.filter(
|
|
67
|
+
(entry) =>
|
|
68
|
+
entry.actionFile !==
|
|
69
|
+
null
|
|
70
|
+
) as Array<{
|
|
71
|
+
route: Route;
|
|
72
|
+
actionFile: string;
|
|
73
|
+
guardFiles: string[];
|
|
74
|
+
}>;
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
actionRoutes.length ===
|
|
78
|
+
0
|
|
79
|
+
) {
|
|
80
|
+
console.log(
|
|
81
|
+
"[BCP Build] Actions: none"
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entryDirectory =
|
|
87
|
+
path.join(
|
|
88
|
+
rootDirectory,
|
|
89
|
+
".bcp-framework",
|
|
90
|
+
"build",
|
|
91
|
+
".action-entry"
|
|
92
|
+
);
|
|
93
|
+
const entryFile =
|
|
94
|
+
path.join(
|
|
95
|
+
entryDirectory,
|
|
96
|
+
"actions-entry.mjs"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
fs.rmSync(
|
|
100
|
+
entryDirectory,
|
|
101
|
+
{
|
|
102
|
+
recursive: true,
|
|
103
|
+
force: true,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
fs.mkdirSync(
|
|
107
|
+
entryDirectory,
|
|
108
|
+
{
|
|
109
|
+
recursive: true,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(
|
|
114
|
+
entryFile,
|
|
115
|
+
createActionEntry(
|
|
116
|
+
actionRoutes,
|
|
117
|
+
rootDirectory,
|
|
118
|
+
entryDirectory
|
|
119
|
+
),
|
|
120
|
+
"utf8"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const frameworkDirectory =
|
|
124
|
+
path.dirname(
|
|
125
|
+
fileURLToPath(
|
|
126
|
+
import.meta.url
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
const frameworkServerEntry =
|
|
130
|
+
path.resolve(
|
|
131
|
+
frameworkDirectory,
|
|
132
|
+
"../../client/src/server.ts"
|
|
133
|
+
);
|
|
134
|
+
const frameworkServerOnlyEntry =
|
|
135
|
+
path.resolve(
|
|
136
|
+
frameworkDirectory,
|
|
137
|
+
"../../client/src/server-only.mjs"
|
|
138
|
+
);
|
|
139
|
+
const frameworkCacheEntry =
|
|
140
|
+
path.resolve(
|
|
141
|
+
frameworkDirectory,
|
|
142
|
+
"../../cache/src/index.ts"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await build({
|
|
147
|
+
absWorkingDir:
|
|
148
|
+
rootDirectory,
|
|
149
|
+
entryPoints: [
|
|
150
|
+
entryFile,
|
|
151
|
+
],
|
|
152
|
+
outfile:
|
|
153
|
+
outputFile,
|
|
154
|
+
bundle:
|
|
155
|
+
true,
|
|
156
|
+
platform:
|
|
157
|
+
"node",
|
|
158
|
+
format:
|
|
159
|
+
"esm",
|
|
160
|
+
target: [
|
|
161
|
+
"node24",
|
|
162
|
+
],
|
|
163
|
+
minify:
|
|
164
|
+
process.env
|
|
165
|
+
.BCP_BUILD_MINIFY !==
|
|
166
|
+
"0",
|
|
167
|
+
sourcemap:
|
|
168
|
+
process.env
|
|
169
|
+
.BCP_BUILD_SOURCE_MAPS ===
|
|
170
|
+
"1",
|
|
171
|
+
treeShaking:
|
|
172
|
+
true,
|
|
173
|
+
packages:
|
|
174
|
+
"external",
|
|
175
|
+
plugins: [
|
|
176
|
+
{
|
|
177
|
+
name:
|
|
178
|
+
"bcp-action-runtime",
|
|
179
|
+
setup(buildApi) {
|
|
180
|
+
buildApi.onResolve(
|
|
181
|
+
{
|
|
182
|
+
filter:
|
|
183
|
+
/^bcp\/server$/,
|
|
184
|
+
},
|
|
185
|
+
() => ({
|
|
186
|
+
path:
|
|
187
|
+
frameworkServerEntry,
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
buildApi.onResolve(
|
|
191
|
+
{
|
|
192
|
+
filter:
|
|
193
|
+
/^bcp\/server-only$/,
|
|
194
|
+
},
|
|
195
|
+
() => ({
|
|
196
|
+
path:
|
|
197
|
+
frameworkServerOnlyEntry,
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
buildApi.onResolve(
|
|
201
|
+
{
|
|
202
|
+
filter:
|
|
203
|
+
/^bcp\/cache$/,
|
|
204
|
+
},
|
|
205
|
+
() => ({
|
|
206
|
+
path:
|
|
207
|
+
frameworkCacheEntry,
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
define:
|
|
214
|
+
createServerEnvironmentDefines(
|
|
215
|
+
"production"
|
|
216
|
+
),
|
|
217
|
+
legalComments:
|
|
218
|
+
"none",
|
|
219
|
+
logLevel:
|
|
220
|
+
"silent",
|
|
221
|
+
});
|
|
222
|
+
} finally {
|
|
223
|
+
fs.rmSync(
|
|
224
|
+
entryDirectory,
|
|
225
|
+
{
|
|
226
|
+
recursive: true,
|
|
227
|
+
force: true,
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(
|
|
233
|
+
`[BCP Build] Actions: ${actionRoutes.length} route(s) -> ${path.relative(rootDirectory, outputFile).replace(/\\/g, "/")}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createActionEntry(
|
|
238
|
+
actionRoutes: Array<{
|
|
239
|
+
route: Route;
|
|
240
|
+
actionFile: string;
|
|
241
|
+
guardFiles: string[];
|
|
242
|
+
}>,
|
|
243
|
+
rootDirectory: string,
|
|
244
|
+
entryDirectory: string
|
|
245
|
+
): string {
|
|
246
|
+
const frameworkDirectory =
|
|
247
|
+
path.dirname(
|
|
248
|
+
fileURLToPath(
|
|
249
|
+
import.meta.url
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
const actionRuntime =
|
|
253
|
+
path.resolve(
|
|
254
|
+
frameworkDirectory,
|
|
255
|
+
"../../server/src/form-action.ts"
|
|
256
|
+
);
|
|
257
|
+
const actionTransportRuntime =
|
|
258
|
+
path.resolve(
|
|
259
|
+
frameworkDirectory,
|
|
260
|
+
"../../server/src/form-action-transport.ts"
|
|
261
|
+
);
|
|
262
|
+
const guardRuntime =
|
|
263
|
+
path.resolve(
|
|
264
|
+
frameworkDirectory,
|
|
265
|
+
"../../server/src/page-guard.ts"
|
|
266
|
+
);
|
|
267
|
+
const requestContextRuntime =
|
|
268
|
+
path.resolve(
|
|
269
|
+
frameworkDirectory,
|
|
270
|
+
"../../server/src/request-context.ts"
|
|
271
|
+
);
|
|
272
|
+
const cacheRuntime =
|
|
273
|
+
path.resolve(
|
|
274
|
+
frameworkDirectory,
|
|
275
|
+
"../../cache/src/index.ts"
|
|
276
|
+
);
|
|
277
|
+
const routerRuntime =
|
|
278
|
+
path.resolve(
|
|
279
|
+
frameworkDirectory,
|
|
280
|
+
"../../router/src/index.ts"
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const imports: string[] = [
|
|
284
|
+
`import { assertPageActionName, executePageActionFunction, normalizePageActionMethod } from ${JSON.stringify(toImportSpecifier(entryDirectory, actionRuntime))};`,
|
|
285
|
+
`import { completePageActionExecution } from ${JSON.stringify(toImportSpecifier(entryDirectory, actionTransportRuntime))};`,
|
|
286
|
+
`import { executePageGuardFunctions } from ${JSON.stringify(toImportSpecifier(entryDirectory, guardRuntime))};`,
|
|
287
|
+
`import { runWithRequestContext } from ${JSON.stringify(toImportSpecifier(entryDirectory, requestContextRuntime))};`,
|
|
288
|
+
`import { runWithRequestCacheContext } from ${JSON.stringify(toImportSpecifier(entryDirectory, cacheRuntime))};`,
|
|
289
|
+
`import { matchRoute } from ${JSON.stringify(toImportSpecifier(entryDirectory, routerRuntime))};`,
|
|
290
|
+
];
|
|
291
|
+
const guardNameByFile =
|
|
292
|
+
new Map<string, string>();
|
|
293
|
+
let guardIndex =
|
|
294
|
+
0;
|
|
295
|
+
|
|
296
|
+
actionRoutes.forEach(
|
|
297
|
+
(entry, index) => {
|
|
298
|
+
imports.push(
|
|
299
|
+
`import * as Actions${index} from ${JSON.stringify(toImportSpecifier(entryDirectory, entry.actionFile))};`
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
for (const guardFile of entry.guardFiles) {
|
|
303
|
+
if (
|
|
304
|
+
guardNameByFile.has(
|
|
305
|
+
guardFile
|
|
306
|
+
)
|
|
307
|
+
) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const guardName =
|
|
312
|
+
`Guard${guardIndex++}`;
|
|
313
|
+
guardNameByFile.set(
|
|
314
|
+
guardFile,
|
|
315
|
+
guardName
|
|
316
|
+
);
|
|
317
|
+
imports.push(
|
|
318
|
+
`import { guard as ${guardName} } from ${JSON.stringify(toImportSpecifier(entryDirectory, guardFile))};`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const definitions =
|
|
325
|
+
actionRoutes.map(
|
|
326
|
+
(entry, index) => {
|
|
327
|
+
const guards =
|
|
328
|
+
entry.guardFiles.map(
|
|
329
|
+
(guardFile) => {
|
|
330
|
+
const guardName =
|
|
331
|
+
guardNameByFile.get(
|
|
332
|
+
guardFile
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (!guardName) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`BCP Framework: action guard import was not generated for "${guardFile}".`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return `{
|
|
342
|
+
filePath: ${JSON.stringify(path.relative(rootDirectory, guardFile).replace(/\\/g, "/"))},
|
|
343
|
+
guard: ${guardName}
|
|
344
|
+
}`;
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return `{
|
|
349
|
+
pathname: ${JSON.stringify(entry.route.pathname)},
|
|
350
|
+
segments: ${JSON.stringify(entry.route.segments)},
|
|
351
|
+
actions: Actions${index},
|
|
352
|
+
actionFile: ${JSON.stringify(path.relative(rootDirectory, entry.actionFile).replace(/\\/g, "/"))},
|
|
353
|
+
guards: [${guards.join(", ")}]
|
|
354
|
+
}`;
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return `${imports.join("\n")}
|
|
359
|
+
|
|
360
|
+
const definitions = [
|
|
361
|
+
${definitions.join(",\n ")}
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const routePatterns =
|
|
365
|
+
definitions.map(
|
|
366
|
+
(definition) => ({
|
|
367
|
+
pathname: definition.pathname,
|
|
368
|
+
segments: definition.segments,
|
|
369
|
+
filePath: definition.pathname,
|
|
370
|
+
layouts: []
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const definitionByPathname =
|
|
375
|
+
new Map(
|
|
376
|
+
definitions.map(
|
|
377
|
+
(definition) => [
|
|
378
|
+
definition.pathname,
|
|
379
|
+
definition
|
|
380
|
+
]
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
export const actionPathnames =
|
|
385
|
+
definitions.map(
|
|
386
|
+
(definition) =>
|
|
387
|
+
definition.pathname
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
export async function evaluateRouteAction(input) {
|
|
391
|
+
assertPageActionName(
|
|
392
|
+
input.name
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const target =
|
|
396
|
+
new URL(
|
|
397
|
+
input.url
|
|
398
|
+
);
|
|
399
|
+
const match =
|
|
400
|
+
matchRoute(
|
|
401
|
+
routePatterns,
|
|
402
|
+
target.pathname
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (!match) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const definition =
|
|
410
|
+
definitionByPathname.get(
|
|
411
|
+
match.route.pathname
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (!definition) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const candidate =
|
|
419
|
+
definition.actions[
|
|
420
|
+
input.name
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
typeof candidate !==
|
|
425
|
+
"function"
|
|
426
|
+
) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const method =
|
|
431
|
+
normalizePageActionMethod(
|
|
432
|
+
input.method
|
|
433
|
+
);
|
|
434
|
+
const request =
|
|
435
|
+
new Request(
|
|
436
|
+
target,
|
|
437
|
+
{
|
|
438
|
+
method,
|
|
439
|
+
headers:
|
|
440
|
+
new Headers(
|
|
441
|
+
input.headers ||
|
|
442
|
+
[]
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
return runWithRequestContext(
|
|
448
|
+
request,
|
|
449
|
+
() =>
|
|
450
|
+
runWithRequestCacheContext(
|
|
451
|
+
target.pathname,
|
|
452
|
+
async () => {
|
|
453
|
+
const guardExecution =
|
|
454
|
+
await executePageGuardFunctions(
|
|
455
|
+
definition.guards,
|
|
456
|
+
match.params,
|
|
457
|
+
target
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
if (guardExecution.response) {
|
|
461
|
+
return completePageActionExecution({
|
|
462
|
+
hasGuard:
|
|
463
|
+
guardExecution.hasGuard,
|
|
464
|
+
guardData:
|
|
465
|
+
guardExecution.data,
|
|
466
|
+
data:
|
|
467
|
+
null,
|
|
468
|
+
response:
|
|
469
|
+
guardExecution.response
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const execution =
|
|
474
|
+
await executePageActionFunction(
|
|
475
|
+
candidate,
|
|
476
|
+
input.name,
|
|
477
|
+
method,
|
|
478
|
+
input.formData,
|
|
479
|
+
match.params,
|
|
480
|
+
target,
|
|
481
|
+
guardExecution.data,
|
|
482
|
+
guardExecution.hasGuard,
|
|
483
|
+
definition.actionFile
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return completePageActionExecution(
|
|
487
|
+
execution
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
),
|
|
491
|
+
{
|
|
492
|
+
remoteAddress:
|
|
493
|
+
input.remoteAddress ||
|
|
494
|
+
null
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function toImportSpecifier(
|
|
502
|
+
fromDirectory: string,
|
|
503
|
+
filePath: string
|
|
504
|
+
): string {
|
|
505
|
+
let relative =
|
|
506
|
+
path.relative(
|
|
507
|
+
fromDirectory,
|
|
508
|
+
filePath
|
|
509
|
+
)
|
|
510
|
+
.replace(
|
|
511
|
+
/\\/g,
|
|
512
|
+
"/"
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (
|
|
516
|
+
!relative.startsWith(".")
|
|
517
|
+
) {
|
|
518
|
+
relative =
|
|
519
|
+
`./${relative}`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return relative;
|
|
523
|
+
}
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
createServerEnvironmentDefines,
|
|
13
13
|
} from "../../env/src/index.js";
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
routeHasPageActions,
|
|
17
|
+
} from "../../server/src/form-action.js";
|
|
18
|
+
|
|
15
19
|
import {
|
|
16
20
|
findProjectMiddlewareFile,
|
|
17
21
|
} from "../../server/src/middleware-loader.js";
|
|
@@ -33,6 +37,10 @@ import type {
|
|
|
33
37
|
PartialHydrationBuildManifest,
|
|
34
38
|
} from "./partial-hydration.js";
|
|
35
39
|
|
|
40
|
+
import {
|
|
41
|
+
buildProductionActions,
|
|
42
|
+
} from "./server-production-actions.js";
|
|
43
|
+
|
|
36
44
|
import {
|
|
37
45
|
buildProductionGuards,
|
|
38
46
|
} from "./server-production-guards.js";
|
|
@@ -96,6 +104,12 @@ export async function buildProductionServer(
|
|
|
96
104
|
serverDirectory
|
|
97
105
|
);
|
|
98
106
|
|
|
107
|
+
await buildProductionActions(
|
|
108
|
+
pageRoutes,
|
|
109
|
+
rootDirectory,
|
|
110
|
+
serverDirectory
|
|
111
|
+
);
|
|
112
|
+
|
|
99
113
|
return {
|
|
100
114
|
...baseResult,
|
|
101
115
|
size:
|
|
@@ -228,6 +242,9 @@ function writeCacheManifest(
|
|
|
228
242
|
!routeHasPageGuard(
|
|
229
243
|
appDirectory,
|
|
230
244
|
route.filePath
|
|
245
|
+
) &&
|
|
246
|
+
!routeHasPageActions(
|
|
247
|
+
route.filePath
|
|
231
248
|
)
|
|
232
249
|
);
|
|
233
250
|
|