@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.
@@ -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