@chidchanun/bcp 0.1.8 → 0.1.10

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