@chidchanun/bcp 0.1.0

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 (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/docs/caching.md +76 -0
  5. package/docs/configuration.md +97 -0
  6. package/docs/deployment.md +74 -0
  7. package/docs/getting-started.md +82 -0
  8. package/docs/middleware.md +58 -0
  9. package/docs/releasing.md +299 -0
  10. package/docs/routing.md +103 -0
  11. package/docs/security.md +57 -0
  12. package/package.json +68 -0
  13. package/packages/bundler/src/client-islands.ts +1457 -0
  14. package/packages/bundler/src/incremental-context.ts +206 -0
  15. package/packages/bundler/src/index.ts +1991 -0
  16. package/packages/bundler/src/module-graph.ts +317 -0
  17. package/packages/bundler/src/partial-hydration.ts +414 -0
  18. package/packages/bundler/src/production.ts +974 -0
  19. package/packages/bundler/src/server-production-middleware.ts +447 -0
  20. package/packages/bundler/src/server-production.ts +1193 -0
  21. package/packages/bundler/src/special-files.ts +131 -0
  22. package/packages/cache/src/index.ts +761 -0
  23. package/packages/cli/bin/bcp.mjs +93 -0
  24. package/packages/cli/src/args.ts +305 -0
  25. package/packages/cli/src/bootstrap.ts +514 -0
  26. package/packages/cli/src/index.ts +504 -0
  27. package/packages/cli/src/version.ts +45 -0
  28. package/packages/client/src/cache.ts +11 -0
  29. package/packages/client/src/config.ts +18 -0
  30. package/packages/client/src/error-boundary.tsx +149 -0
  31. package/packages/client/src/hydration.ts +3 -0
  32. package/packages/client/src/index.tsx +57 -0
  33. package/packages/client/src/islands.tsx +315 -0
  34. package/packages/client/src/metadata.ts +281 -0
  35. package/packages/client/src/navigation-loading.ts +52 -0
  36. package/packages/client/src/navigation-state.ts +80 -0
  37. package/packages/client/src/not-found.ts +34 -0
  38. package/packages/client/src/persistent-layout-runtime.ts +273 -0
  39. package/packages/client/src/router-v2.tsx +969 -0
  40. package/packages/client/src/router.tsx +1 -0
  41. package/packages/config/src/index.ts +1038 -0
  42. package/packages/env/src/index.ts +593 -0
  43. package/packages/router/src/advanced-router.ts +1032 -0
  44. package/packages/router/src/index.ts +1 -0
  45. package/packages/server/src/compression.ts +249 -0
  46. package/packages/server/src/dev-document-metadata.ts +154 -0
  47. package/packages/server/src/dev-hmr.ts +225 -0
  48. package/packages/server/src/index.ts +2265 -0
  49. package/packages/server/src/metadata.ts +478 -0
  50. package/packages/server/src/middleware-dev-server.ts +260 -0
  51. package/packages/server/src/middleware-loader.ts +140 -0
  52. package/packages/server/src/middleware-proxy.ts +516 -0
  53. package/packages/server/src/middleware.ts +704 -0
  54. package/packages/server/src/navigation-payload.ts +471 -0
  55. package/packages/server/src/production-server.ts +1746 -0
  56. package/packages/server/src/response-cache-proxy.ts +828 -0
  57. package/packages/server/src/security-proxy.ts +406 -0
  58. package/packages/server/src/security.ts +451 -0
  59. package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
  60. package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
  61. package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
  62. package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
  63. package/packages/server/src/standalone-production-runtime.ts +1951 -0
  64. package/packages/server/src/standalone-production-server.ts +6 -0
  65. package/packages/server/src/static-assets.ts +262 -0
  66. package/packages/server/src/static-dev-server.ts +854 -0
@@ -0,0 +1,1193 @@
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
+ readResolvedBcpConfig,
13
+ type ResolvedBcpConfig,
14
+ } from "../../config/src/index.js";
15
+
16
+ import {
17
+ createServerEnvironmentDefines,
18
+ } from "../../env/src/index.js";
19
+
20
+ import type {
21
+ ApiRoute,
22
+ Route,
23
+ } from "../../router/src/index.js";
24
+
25
+ import type {
26
+ PartialHydrationBuildManifest,
27
+ } from "./partial-hydration.js";
28
+
29
+ import {
30
+ findNearestSpecialFile,
31
+ findSpecialFileInDirectory,
32
+ } from "./special-files.js";
33
+
34
+ export interface ProductionServerBuildResult {
35
+ serverFile: string;
36
+ size: number;
37
+ pageRoutes: number;
38
+ apiRoutes: number;
39
+ }
40
+
41
+ export async function buildProductionServer(
42
+ pageRoutes: Route[],
43
+ apiRoutes: ApiRoute[],
44
+ rootDirectory: string,
45
+ clientManifest: PartialHydrationBuildManifest
46
+ ): Promise<ProductionServerBuildResult> {
47
+ const frameworkConfig =
48
+ readResolvedBcpConfig();
49
+
50
+ const buildRoot =
51
+ path.join(
52
+ rootDirectory,
53
+ ".bcp-framework",
54
+ "build"
55
+ );
56
+
57
+ const serverDirectory =
58
+ path.join(
59
+ buildRoot,
60
+ "server"
61
+ );
62
+
63
+ const entryDirectory =
64
+ path.join(
65
+ buildRoot,
66
+ ".server-entry"
67
+ );
68
+
69
+ const publicSource =
70
+ path.join(
71
+ rootDirectory,
72
+ "public"
73
+ );
74
+
75
+ const publicOutput =
76
+ path.join(
77
+ buildRoot,
78
+ "public"
79
+ );
80
+
81
+ fs.rmSync(
82
+ serverDirectory,
83
+ {
84
+ recursive: true,
85
+ force: true,
86
+ }
87
+ );
88
+ fs.rmSync(
89
+ entryDirectory,
90
+ {
91
+ recursive: true,
92
+ force: true,
93
+ }
94
+ );
95
+ fs.rmSync(
96
+ publicOutput,
97
+ {
98
+ recursive: true,
99
+ force: true,
100
+ }
101
+ );
102
+
103
+ fs.mkdirSync(
104
+ serverDirectory,
105
+ {
106
+ recursive: true,
107
+ }
108
+ );
109
+ fs.mkdirSync(
110
+ entryDirectory,
111
+ {
112
+ recursive: true,
113
+ }
114
+ );
115
+
116
+ writeResolvedConfigManifest(
117
+ serverDirectory,
118
+ frameworkConfig
119
+ );
120
+
121
+ if (fs.existsSync(publicSource)) {
122
+ fs.cpSync(
123
+ publicSource,
124
+ publicOutput,
125
+ {
126
+ recursive: true,
127
+ }
128
+ );
129
+ }
130
+
131
+ const entryFile =
132
+ path.join(
133
+ entryDirectory,
134
+ "server-entry.mjs"
135
+ );
136
+
137
+ fs.writeFileSync(
138
+ entryFile,
139
+ createServerEntry(
140
+ pageRoutes,
141
+ apiRoutes,
142
+ rootDirectory,
143
+ entryDirectory,
144
+ clientManifest,
145
+ frameworkConfig
146
+ ),
147
+ "utf8"
148
+ );
149
+
150
+ const serverFile =
151
+ path.join(
152
+ serverDirectory,
153
+ "server.mjs"
154
+ );
155
+
156
+ const frameworkDirectory =
157
+ path.dirname(
158
+ fileURLToPath(
159
+ import.meta.url
160
+ )
161
+ );
162
+
163
+ const frameworkClientEntry =
164
+ path.resolve(
165
+ frameworkDirectory,
166
+ "../../client/src/index.tsx"
167
+ );
168
+
169
+ const frameworkIslandEntry =
170
+ path.resolve(
171
+ frameworkDirectory,
172
+ "../../client/src/islands.tsx"
173
+ );
174
+
175
+ await build({
176
+ absWorkingDir:
177
+ rootDirectory,
178
+ entryPoints: [
179
+ entryFile,
180
+ ],
181
+ outfile:
182
+ serverFile,
183
+ bundle: true,
184
+ platform: "node",
185
+ format: "esm",
186
+ target: [
187
+ "node24",
188
+ ],
189
+ minify:
190
+ frameworkConfig.build.minify,
191
+ sourcemap:
192
+ frameworkConfig.build.sourceMaps,
193
+ treeShaking: true,
194
+ packages: "external",
195
+ external: [
196
+ "react",
197
+ "react-dom",
198
+ "react-dom/*",
199
+ ],
200
+ plugins: [
201
+ {
202
+ name:
203
+ "bcp-standalone-client-runtime",
204
+ setup(buildApi) {
205
+ buildApi.onResolve(
206
+ {
207
+ filter:
208
+ /^bcp$/,
209
+ },
210
+ () => ({
211
+ path:
212
+ frameworkClientEntry,
213
+ })
214
+ );
215
+
216
+ buildApi.onResolve(
217
+ {
218
+ filter:
219
+ /^bcp\/island$/,
220
+ },
221
+ () => ({
222
+ path:
223
+ frameworkIslandEntry,
224
+ })
225
+ );
226
+ },
227
+ },
228
+ ],
229
+ define:
230
+ createServerEnvironmentDefines(
231
+ "production"
232
+ ),
233
+ legalComments: "none",
234
+ logLevel: "silent",
235
+ });
236
+
237
+ fs.rmSync(
238
+ entryDirectory,
239
+ {
240
+ recursive: true,
241
+ force: true,
242
+ }
243
+ );
244
+
245
+ const stat =
246
+ fs.statSync(
247
+ serverFile
248
+ );
249
+
250
+ return {
251
+ serverFile,
252
+ size:
253
+ stat.size,
254
+ pageRoutes:
255
+ pageRoutes.length,
256
+ apiRoutes:
257
+ apiRoutes.length,
258
+ };
259
+ }
260
+
261
+ function writeResolvedConfigManifest(
262
+ serverDirectory: string,
263
+ frameworkConfig: ResolvedBcpConfig
264
+ ): void {
265
+ fs.writeFileSync(
266
+ path.join(
267
+ serverDirectory,
268
+ "config.json"
269
+ ),
270
+ JSON.stringify(
271
+ {
272
+ version: 1,
273
+ ...frameworkConfig,
274
+ },
275
+ null,
276
+ 2
277
+ ),
278
+ "utf8"
279
+ );
280
+ }
281
+
282
+ function createServerEntry(
283
+ pageRoutes: Route[],
284
+ apiRoutes: ApiRoute[],
285
+ rootDirectory: string,
286
+ entryDirectory: string,
287
+ clientManifest: PartialHydrationBuildManifest,
288
+ frameworkConfig: ResolvedBcpConfig
289
+ ): string {
290
+ const frameworkDirectory =
291
+ path.dirname(
292
+ fileURLToPath(
293
+ import.meta.url
294
+ )
295
+ );
296
+
297
+ const standaloneServerFile =
298
+ path.resolve(
299
+ frameworkDirectory,
300
+ "../../server/src/standalone-production-server.ts"
301
+ );
302
+
303
+ const appDirectory =
304
+ path.join(
305
+ rootDirectory,
306
+ "app"
307
+ );
308
+
309
+ const imports: string[] = [
310
+ `import path from "node:path";`,
311
+ `import { fileURLToPath } from "node:url";`,
312
+ `import { createElement, Fragment } from "react";`,
313
+ `import { createStandaloneProductionServer } from ${JSON.stringify(
314
+ toImportSpecifier(
315
+ entryDirectory,
316
+ standaloneServerFile
317
+ )
318
+ )};`,
319
+ ];
320
+
321
+ const componentDefinitions:
322
+ string[] = [];
323
+ const routeDefinitions:
324
+ string[] = [];
325
+ const apiDefinitions:
326
+ string[] = [];
327
+
328
+ const serverOnlyRoutes =
329
+ new Set(
330
+ clientManifest
331
+ .hydration
332
+ .serverOnlyRoutes
333
+ );
334
+
335
+ const rootNotFoundFile =
336
+ findSpecialFileInDirectory(
337
+ appDirectory,
338
+ "not-found"
339
+ );
340
+
341
+ let globalNotFoundName =
342
+ "null";
343
+
344
+ if (rootNotFoundFile) {
345
+ globalNotFoundName =
346
+ "GlobalNotFound";
347
+
348
+ imports.push(
349
+ `import GlobalNotFound from ${JSON.stringify(
350
+ toImportSpecifier(
351
+ entryDirectory,
352
+ rootNotFoundFile
353
+ )
354
+ )};`
355
+ );
356
+ }
357
+
358
+ const rootLayoutFile =
359
+ findRootLayoutFile(
360
+ appDirectory
361
+ );
362
+
363
+ let rootLayoutName =
364
+ "null";
365
+
366
+ if (rootLayoutFile) {
367
+ rootLayoutName =
368
+ "GlobalNotFoundLayout";
369
+
370
+ imports.push(
371
+ `import GlobalNotFoundLayout from ${JSON.stringify(
372
+ toImportSpecifier(
373
+ entryDirectory,
374
+ rootLayoutFile
375
+ )
376
+ )};`
377
+ );
378
+ }
379
+
380
+ pageRoutes.forEach(
381
+ (
382
+ route,
383
+ routeIndex
384
+ ) => {
385
+ const pageModuleName =
386
+ `PageModule${routeIndex}`;
387
+ const pageName =
388
+ `Page${routeIndex}`;
389
+
390
+ imports.push(
391
+ `import * as ${pageModuleName} from ${JSON.stringify(
392
+ toImportSpecifier(
393
+ entryDirectory,
394
+ route.filePath
395
+ )
396
+ )};`
397
+ );
398
+ componentDefinitions.push(
399
+ `const ${pageName} = ${pageModuleName}.default;`
400
+ );
401
+
402
+ const layoutModules:
403
+ Array<{
404
+ name: string;
405
+ moduleName: string;
406
+ filePath: string;
407
+ }> = [];
408
+
409
+ route.layouts.forEach(
410
+ (
411
+ layoutFile,
412
+ layoutIndex
413
+ ) => {
414
+ const layoutName =
415
+ `Layout${routeIndex}_${layoutIndex}`;
416
+ const moduleName =
417
+ `LayoutModule${routeIndex}_${layoutIndex}`;
418
+
419
+ imports.push(
420
+ `import * as ${moduleName} from ${JSON.stringify(
421
+ toImportSpecifier(
422
+ entryDirectory,
423
+ layoutFile
424
+ )
425
+ )};`
426
+ );
427
+ componentDefinitions.push(
428
+ `const ${layoutName} = ${moduleName}.default;`
429
+ );
430
+
431
+ layoutModules.push({
432
+ name:
433
+ layoutName,
434
+ moduleName,
435
+ filePath:
436
+ layoutFile,
437
+ });
438
+ }
439
+ );
440
+
441
+ const layoutNames =
442
+ layoutModules.map(
443
+ (layout) =>
444
+ layout.name
445
+ );
446
+
447
+ const loadingFile =
448
+ findNearestLoadingFile(
449
+ appDirectory,
450
+ route.filePath
451
+ );
452
+
453
+ let loadingName =
454
+ "null";
455
+
456
+ if (loadingFile) {
457
+ loadingName =
458
+ `Loading${routeIndex}`;
459
+ imports.push(
460
+ `import ${loadingName} from ${JSON.stringify(
461
+ toImportSpecifier(
462
+ entryDirectory,
463
+ loadingFile
464
+ )
465
+ )};`
466
+ );
467
+ }
468
+
469
+ const errorFile =
470
+ findNearestSpecialFile(
471
+ appDirectory,
472
+ route.filePath,
473
+ "error"
474
+ );
475
+
476
+ let errorName =
477
+ "null";
478
+
479
+ if (errorFile) {
480
+ errorName =
481
+ `Error${routeIndex}`;
482
+ imports.push(
483
+ `import ${errorName} from ${JSON.stringify(
484
+ toImportSpecifier(
485
+ entryDirectory,
486
+ errorFile
487
+ )
488
+ )};`
489
+ );
490
+ }
491
+
492
+ const notFoundFile =
493
+ findNearestSpecialFile(
494
+ appDirectory,
495
+ route.filePath,
496
+ "not-found"
497
+ );
498
+
499
+ let notFoundName =
500
+ "null";
501
+
502
+ if (notFoundFile) {
503
+ notFoundName =
504
+ `NotFound${routeIndex}`;
505
+ imports.push(
506
+ `import ${notFoundName} from ${JSON.stringify(
507
+ toImportSpecifier(
508
+ entryDirectory,
509
+ notFoundFile
510
+ )
511
+ )};`
512
+ );
513
+ }
514
+
515
+ const client =
516
+ clientManifest.routes[
517
+ route.pathname
518
+ ];
519
+
520
+ if (!client) {
521
+ throw new Error(
522
+ `BCP Framework: client manifest entry missing for route "${route.pathname}".`
523
+ );
524
+ }
525
+
526
+ const hydration =
527
+ serverOnlyRoutes.has(
528
+ route.pathname
529
+ )
530
+ ? "none"
531
+ : "full";
532
+
533
+ let renderedPageName =
534
+ pageName;
535
+
536
+ if (
537
+ hydration === "none" &&
538
+ frameworkConfig.experimental.islands
539
+ ) {
540
+ const islandIds =
541
+ collectRouteIslandIds(
542
+ route,
543
+ rootDirectory,
544
+ [
545
+ errorFile,
546
+ notFoundFile,
547
+ ].filter(
548
+ (
549
+ value
550
+ ): value is string =>
551
+ Boolean(value)
552
+ )
553
+ );
554
+
555
+ const islandAssets =
556
+ islandIds.map(
557
+ (islandId) => {
558
+ const asset =
559
+ clientManifest
560
+ .islands[
561
+ islandId
562
+ ];
563
+
564
+ if (!asset) {
565
+ throw new Error(
566
+ `BCP Framework: island "${islandId}" used by route "${route.pathname}" was not found in the client island manifest.`
567
+ );
568
+ }
569
+
570
+ return {
571
+ id:
572
+ islandId,
573
+ asset,
574
+ };
575
+ }
576
+ );
577
+
578
+ if (islandAssets.length > 0) {
579
+ renderedPageName =
580
+ `Page${routeIndex}WithIslands`;
581
+ componentDefinitions.push(
582
+ createIslandPageWrapper(
583
+ renderedPageName,
584
+ pageName,
585
+ islandAssets
586
+ )
587
+ );
588
+ }
589
+ }
590
+
591
+ const metadataSources = [
592
+ ...layoutModules.map(
593
+ (layout) =>
594
+ createMetadataSourceDefinition(
595
+ layout.moduleName,
596
+ layout.filePath,
597
+ rootDirectory
598
+ )
599
+ ),
600
+ createMetadataSourceDefinition(
601
+ pageModuleName,
602
+ route.filePath,
603
+ rootDirectory
604
+ ),
605
+ ];
606
+
607
+ routeDefinitions.push(`{
608
+ pathname: ${JSON.stringify(route.pathname)},
609
+ segments: ${JSON.stringify(route.segments)},
610
+ page: ${renderedPageName},
611
+ layouts: [${layoutNames.join(", ")}],
612
+ loading: ${loadingName},
613
+ error: ${errorName},
614
+ notFound: ${notFoundName},
615
+ metadataSources: [${metadataSources.join(", ")}],
616
+ hydration: ${JSON.stringify(hydration)},
617
+ client: ${JSON.stringify(client)}
618
+ }`);
619
+ }
620
+ );
621
+
622
+ apiRoutes.forEach(
623
+ (
624
+ route,
625
+ routeIndex
626
+ ) => {
627
+ const moduleName =
628
+ `Api${routeIndex}`;
629
+
630
+ imports.push(
631
+ `import * as ${moduleName} from ${JSON.stringify(
632
+ toImportSpecifier(
633
+ entryDirectory,
634
+ route.filePath
635
+ )
636
+ )};`
637
+ );
638
+
639
+ apiDefinitions.push(`{
640
+ pathname: ${JSON.stringify(route.pathname)},
641
+ segments: ${JSON.stringify(route.segments)},
642
+ handlers: ${moduleName}
643
+ }`);
644
+ }
645
+ );
646
+
647
+ const standaloneDefaults = {
648
+ server:
649
+ frameworkConfig.server,
650
+ compression:
651
+ frameworkConfig.compression,
652
+ responseCache:
653
+ frameworkConfig.cache.response,
654
+ partialHydration:
655
+ frameworkConfig.experimental.partialHydration,
656
+ islands:
657
+ frameworkConfig.experimental.islands,
658
+ };
659
+
660
+ return `${imports.join("\n")}
661
+
662
+ ${componentDefinitions.join("\n\n")}
663
+
664
+ process.env.NODE_ENV = "production";
665
+
666
+ const bcpDefaults =
667
+ ${JSON.stringify(standaloneDefaults)};
668
+
669
+ process.env.BCP_COMPRESSION ??=
670
+ bcpDefaults.compression
671
+ ? "1"
672
+ : "0";
673
+ process.env.BCP_RESPONSE_CACHE ??=
674
+ bcpDefaults.responseCache
675
+ ? "1"
676
+ : "0";
677
+ process.env.BCP_EXPERIMENTAL_PARTIAL_HYDRATION ??=
678
+ bcpDefaults.partialHydration
679
+ ? "1"
680
+ : "0";
681
+ process.env.BCP_EXPERIMENTAL_ISLANDS ??=
682
+ bcpDefaults.islands
683
+ ? "1"
684
+ : "0";
685
+
686
+ const serverDirectory =
687
+ path.dirname(
688
+ fileURLToPath(
689
+ import.meta.url
690
+ )
691
+ );
692
+
693
+ const buildDirectory =
694
+ path.resolve(
695
+ serverDirectory,
696
+ ".."
697
+ );
698
+
699
+ const server =
700
+ createStandaloneProductionServer({
701
+ port:
702
+ Number(
703
+ process.env.BCP_PORT ||
704
+ process.env.PORT ||
705
+ bcpDefaults.server.port
706
+ ),
707
+ hostname:
708
+ process.env.BCP_HOSTNAME ||
709
+ process.env.HOST ||
710
+ bcpDefaults.server.hostname,
711
+ buildDirectory,
712
+ buildId:
713
+ ${JSON.stringify(clientManifest.buildId)},
714
+ version:
715
+ ${JSON.stringify(clientManifest.version)},
716
+ routes: [
717
+ ${routeDefinitions.join(",\n ")}
718
+ ],
719
+ apiRoutes: [
720
+ ${apiDefinitions.join(",\n ")}
721
+ ],
722
+ globalNotFound:
723
+ ${globalNotFoundName},
724
+ globalNotFoundLayouts:
725
+ ${rootLayoutName === "null"
726
+ ? "[]"
727
+ : `[${rootLayoutName}]`}
728
+ });
729
+
730
+ process.once(
731
+ "SIGINT",
732
+ async () => {
733
+ await server.stop();
734
+ process.exit(0);
735
+ }
736
+ );
737
+
738
+ process.once(
739
+ "SIGTERM",
740
+ async () => {
741
+ await server.stop();
742
+ process.exit(0);
743
+ }
744
+ );
745
+
746
+ await server.start();
747
+ `;
748
+ }
749
+
750
+ function createMetadataSourceDefinition(
751
+ moduleName: string,
752
+ filePath: string,
753
+ rootDirectory: string
754
+ ): string {
755
+ const label =
756
+ path.relative(
757
+ rootDirectory,
758
+ filePath
759
+ )
760
+ .replace(
761
+ /\\/g,
762
+ "/"
763
+ );
764
+
765
+ return `{
766
+ label: ${JSON.stringify(label)},
767
+ metadata: ${moduleName}.metadata,
768
+ generateMetadata: ${moduleName}.generateMetadata
769
+ }`;
770
+ }
771
+
772
+ function createIslandPageWrapper(
773
+ wrapperName: string,
774
+ pageName: string,
775
+ islands: Array<{
776
+ id: string;
777
+ asset: {
778
+ entry: string;
779
+ imports: string[];
780
+ };
781
+ }>
782
+ ): string {
783
+ const preloadPaths =
784
+ Array.from(
785
+ new Set(
786
+ islands.flatMap(
787
+ (island) =>
788
+ island.asset.imports
789
+ )
790
+ )
791
+ );
792
+
793
+ const preloadElements =
794
+ preloadPaths.map(
795
+ (
796
+ importPath,
797
+ index
798
+ ) =>
799
+ `createElement("link", { key: "bcp-island-preload-${index}", rel: "modulepreload", href: ${JSON.stringify(importPath)}, "data-bcp-island-preload": "true" })`
800
+ );
801
+
802
+ const islandScripts =
803
+ islands.map(
804
+ (
805
+ island,
806
+ index
807
+ ) =>
808
+ `createElement("script", { key: "bcp-island-${index}", type: "module", src: ${JSON.stringify(island.asset.entry)}, "data-bcp-island-entry": ${JSON.stringify(island.id)} })`
809
+ );
810
+
811
+ return `const ${wrapperName} = (props) =>
812
+ createElement(
813
+ Fragment,
814
+ null,
815
+ createElement(
816
+ ${pageName},
817
+ props
818
+ ),
819
+ ${[
820
+ ...preloadElements,
821
+ ...islandScripts,
822
+ ].join(",\n ")}
823
+ );`;
824
+ }
825
+
826
+ function collectRouteIslandIds(
827
+ route: Route,
828
+ rootDirectory: string,
829
+ extraFiles: string[] = []
830
+ ): string[] {
831
+ const appDirectory =
832
+ path.resolve(
833
+ rootDirectory,
834
+ "app"
835
+ );
836
+
837
+ const visited =
838
+ new Set<string>();
839
+ const islandIds =
840
+ new Set<string>();
841
+
842
+ for (
843
+ const entryFile
844
+ of [
845
+ route.filePath,
846
+ ...route.layouts,
847
+ ...extraFiles,
848
+ ]
849
+ ) {
850
+ visitSourceFile(
851
+ entryFile,
852
+ appDirectory,
853
+ visited,
854
+ islandIds
855
+ );
856
+ }
857
+
858
+ return Array.from(
859
+ islandIds
860
+ ).sort();
861
+ }
862
+
863
+ function visitSourceFile(
864
+ filePath: string,
865
+ appDirectory: string,
866
+ visited: Set<string>,
867
+ islandIds: Set<string>
868
+ ): void {
869
+ const resolved =
870
+ path.resolve(
871
+ filePath
872
+ );
873
+
874
+ if (visited.has(resolved)) {
875
+ return;
876
+ }
877
+
878
+ visited.add(
879
+ resolved
880
+ );
881
+
882
+ const source =
883
+ fs.readFileSync(
884
+ resolved,
885
+ "utf8"
886
+ );
887
+
888
+ if (
889
+ /\.island\.(?:ts|tsx)$/.test(
890
+ resolved
891
+ )
892
+ ) {
893
+ const match =
894
+ /\bcreateIsland\s*\(\s*["']([A-Za-z][A-Za-z0-9._-]*)["']\s*,/.exec(
895
+ source
896
+ );
897
+
898
+ if (!match) {
899
+ throw new Error(
900
+ `BCP Framework: island file "${resolved}" must export createIsland("literal-id", Component).`
901
+ );
902
+ }
903
+
904
+ islandIds.add(
905
+ match[1]
906
+ );
907
+ return;
908
+ }
909
+
910
+ for (
911
+ const specifier
912
+ of collectRelativeImports(
913
+ source
914
+ )
915
+ ) {
916
+ const importedFile =
917
+ resolveSourceImport(
918
+ resolved,
919
+ specifier
920
+ );
921
+
922
+ if (!importedFile) {
923
+ continue;
924
+ }
925
+
926
+ const relative =
927
+ path.relative(
928
+ appDirectory,
929
+ importedFile
930
+ );
931
+
932
+ if (
933
+ relative.startsWith("..") ||
934
+ path.isAbsolute(
935
+ relative
936
+ )
937
+ ) {
938
+ continue;
939
+ }
940
+
941
+ visitSourceFile(
942
+ importedFile,
943
+ appDirectory,
944
+ visited,
945
+ islandIds
946
+ );
947
+ }
948
+ }
949
+
950
+ function collectRelativeImports(
951
+ source: string
952
+ ): string[] {
953
+ const imports =
954
+ new Set<string>();
955
+
956
+ const pattern =
957
+ /(?:\bimport\s+(?:[^"']*?\s+from\s+)?|\bexport\s+[^"']*?\s+from\s+)["']([^"']+)["']/g;
958
+
959
+ let match:
960
+ RegExpExecArray | null;
961
+
962
+ while (
963
+ (
964
+ match =
965
+ pattern.exec(
966
+ source
967
+ )
968
+ ) !== null
969
+ ) {
970
+ if (
971
+ match[1].startsWith(".")
972
+ ) {
973
+ imports.add(
974
+ match[1]
975
+ );
976
+ }
977
+ }
978
+
979
+ return Array.from(
980
+ imports
981
+ );
982
+ }
983
+
984
+ function resolveSourceImport(
985
+ fromFile: string,
986
+ specifier: string
987
+ ): string | null {
988
+ const basePath =
989
+ path.resolve(
990
+ path.dirname(
991
+ fromFile
992
+ ),
993
+ specifier
994
+ );
995
+
996
+ const candidates = [
997
+ basePath,
998
+ `${basePath}.ts`,
999
+ `${basePath}.tsx`,
1000
+ `${basePath}.js`,
1001
+ `${basePath}.jsx`,
1002
+ path.join(
1003
+ basePath,
1004
+ "index.ts"
1005
+ ),
1006
+ path.join(
1007
+ basePath,
1008
+ "index.tsx"
1009
+ ),
1010
+ path.join(
1011
+ basePath,
1012
+ "index.js"
1013
+ ),
1014
+ path.join(
1015
+ basePath,
1016
+ "index.jsx"
1017
+ ),
1018
+ ];
1019
+
1020
+ for (const candidate of candidates) {
1021
+ if (!fs.existsSync(candidate)) {
1022
+ continue;
1023
+ }
1024
+
1025
+ const stat =
1026
+ fs.statSync(
1027
+ candidate
1028
+ );
1029
+
1030
+ if (stat.isFile()) {
1031
+ return candidate;
1032
+ }
1033
+ }
1034
+
1035
+ return null;
1036
+ }
1037
+
1038
+ function findNearestLoadingFile(
1039
+ appDirectory: string,
1040
+ pageFilePath: string
1041
+ ): string | null {
1042
+ const appRoot =
1043
+ path.resolve(
1044
+ appDirectory
1045
+ );
1046
+
1047
+ let currentDirectory =
1048
+ path.dirname(
1049
+ path.resolve(
1050
+ pageFilePath
1051
+ )
1052
+ );
1053
+
1054
+ while (true) {
1055
+ const tsxFile =
1056
+ path.join(
1057
+ currentDirectory,
1058
+ "loading.tsx"
1059
+ );
1060
+ const tsFile =
1061
+ path.join(
1062
+ currentDirectory,
1063
+ "loading.ts"
1064
+ );
1065
+
1066
+ const hasTsx =
1067
+ fs.existsSync(
1068
+ tsxFile
1069
+ );
1070
+ const hasTs =
1071
+ fs.existsSync(
1072
+ tsFile
1073
+ );
1074
+
1075
+ if (hasTsx && hasTs) {
1076
+ throw new Error(
1077
+ `BCP Framework: both loading.ts and loading.tsx exist in "${currentDirectory}".`
1078
+ );
1079
+ }
1080
+
1081
+ if (hasTsx) {
1082
+ return tsxFile;
1083
+ }
1084
+ if (hasTs) {
1085
+ return tsFile;
1086
+ }
1087
+
1088
+ if (currentDirectory === appRoot) {
1089
+ break;
1090
+ }
1091
+
1092
+ const parent =
1093
+ path.dirname(
1094
+ currentDirectory
1095
+ );
1096
+
1097
+ if (
1098
+ parent === currentDirectory ||
1099
+ !isInsideDirectory(
1100
+ appRoot,
1101
+ parent
1102
+ )
1103
+ ) {
1104
+ break;
1105
+ }
1106
+
1107
+ currentDirectory =
1108
+ parent;
1109
+ }
1110
+
1111
+ return null;
1112
+ }
1113
+
1114
+ function findRootLayoutFile(
1115
+ appDirectory: string
1116
+ ): string | null {
1117
+ const tsxFile =
1118
+ path.join(
1119
+ appDirectory,
1120
+ "layout.tsx"
1121
+ );
1122
+ const tsFile =
1123
+ path.join(
1124
+ appDirectory,
1125
+ "layout.ts"
1126
+ );
1127
+
1128
+ const hasTsx =
1129
+ fs.existsSync(
1130
+ tsxFile
1131
+ );
1132
+ const hasTs =
1133
+ fs.existsSync(
1134
+ tsFile
1135
+ );
1136
+
1137
+ if (hasTsx && hasTs) {
1138
+ throw new Error(
1139
+ `BCP Framework: both layout.ts and layout.tsx exist in "${appDirectory}".`
1140
+ );
1141
+ }
1142
+
1143
+ if (hasTsx) {
1144
+ return tsxFile;
1145
+ }
1146
+ if (hasTs) {
1147
+ return tsFile;
1148
+ }
1149
+
1150
+ return null;
1151
+ }
1152
+
1153
+ function isInsideDirectory(
1154
+ rootDirectory: string,
1155
+ candidate: string
1156
+ ): boolean {
1157
+ const relative =
1158
+ path.relative(
1159
+ rootDirectory,
1160
+ candidate
1161
+ );
1162
+
1163
+ return (
1164
+ relative === "" ||
1165
+ (
1166
+ !relative.startsWith(
1167
+ ".."
1168
+ ) &&
1169
+ !path.isAbsolute(
1170
+ relative
1171
+ )
1172
+ )
1173
+ );
1174
+ }
1175
+
1176
+ function toImportSpecifier(
1177
+ fromDirectory: string,
1178
+ filePath: string
1179
+ ): string {
1180
+ const relative =
1181
+ path.relative(
1182
+ fromDirectory,
1183
+ filePath
1184
+ )
1185
+ .replace(
1186
+ /\\/g,
1187
+ "/"
1188
+ );
1189
+
1190
+ return relative.startsWith(".")
1191
+ ? relative
1192
+ : `./${relative}`;
1193
+ }