@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,1457 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ brotliCompressSync,
5
+ constants,
6
+ gzipSync,
7
+ } from "node:zlib";
8
+
9
+ import {
10
+ build,
11
+ type Metafile,
12
+ } from "esbuild";
13
+
14
+ import {
15
+ createClientEnvironmentDefines,
16
+ } from "../../env/src/index.js";
17
+
18
+ import type {
19
+ ProductionAssetInfo,
20
+ ProductionAssetSize,
21
+ ProductionBuildManifest,
22
+ } from "./production.js";
23
+
24
+ export type IslandHydrationStrategy =
25
+ | "load"
26
+ | "idle"
27
+ | "visible"
28
+ | "media";
29
+
30
+ export type IslandPrefetchStrategy =
31
+ | "none"
32
+ | "idle"
33
+ | "visible"
34
+ | "interaction";
35
+
36
+ export interface ProductionIslandAsset {
37
+ entry: string;
38
+ imports: string[];
39
+ size: ProductionAssetSize;
40
+ hydrate: IslandHydrationStrategy;
41
+ media: string | null;
42
+ prefetch: IslandPrefetchStrategy;
43
+ preload: boolean;
44
+ }
45
+
46
+ interface IslandSource {
47
+ id: string;
48
+ filePath: string;
49
+ hydrate: IslandHydrationStrategy;
50
+ media: string | null;
51
+ prefetch: IslandPrefetchStrategy;
52
+ preload: boolean;
53
+ }
54
+
55
+ interface OutputNode {
56
+ absolutePath: string;
57
+ publicPath: string;
58
+ metadata:
59
+ Metafile["outputs"][string];
60
+ }
61
+
62
+ const PRECOMPRESS_MIN_BYTES =
63
+ 1024;
64
+
65
+ export async function buildClientIslands(
66
+ manifest: ProductionBuildManifest,
67
+ rootDirectory: string
68
+ ): Promise<
69
+ Record<string, ProductionIslandAsset>
70
+ > {
71
+ const appDirectory =
72
+ path.join(
73
+ rootDirectory,
74
+ "app"
75
+ );
76
+
77
+ const islands =
78
+ discoverIslandSources(
79
+ appDirectory
80
+ );
81
+
82
+ if (islands.length === 0) {
83
+ return {};
84
+ }
85
+
86
+ const buildRoot =
87
+ path.join(
88
+ rootDirectory,
89
+ ".bcp-framework",
90
+ "build"
91
+ );
92
+
93
+ const clientDirectory =
94
+ path.join(
95
+ buildRoot,
96
+ "client"
97
+ );
98
+
99
+ const entryDirectory =
100
+ path.join(
101
+ buildRoot,
102
+ ".island-entries"
103
+ );
104
+
105
+ fs.rmSync(
106
+ entryDirectory,
107
+ {
108
+ recursive: true,
109
+ force: true,
110
+ }
111
+ );
112
+
113
+ fs.mkdirSync(
114
+ entryDirectory,
115
+ {
116
+ recursive: true,
117
+ }
118
+ );
119
+
120
+ const runtimeFile =
121
+ path.join(
122
+ entryDirectory,
123
+ "island-runtime.mjs"
124
+ );
125
+
126
+ fs.writeFileSync(
127
+ runtimeFile,
128
+ createSharedIslandRuntimeModule(),
129
+ "utf8"
130
+ );
131
+
132
+ const entryPoints:
133
+ Record<string, string> = {};
134
+
135
+ const entryToIsland =
136
+ new Map<string, string>();
137
+
138
+ for (const island of islands) {
139
+ const entryName =
140
+ sanitizeName(
141
+ island.id
142
+ );
143
+
144
+ const entryFile =
145
+ path.join(
146
+ entryDirectory,
147
+ `${entryName}.mjs`
148
+ );
149
+
150
+ const payloadFile =
151
+ path.join(
152
+ entryDirectory,
153
+ `${entryName}-payload.mjs`
154
+ );
155
+
156
+ fs.writeFileSync(
157
+ payloadFile,
158
+ createIslandPayloadModule(
159
+ island,
160
+ entryDirectory
161
+ ),
162
+ "utf8"
163
+ );
164
+
165
+ fs.writeFileSync(
166
+ entryFile,
167
+ createIslandSchedulerEntry(
168
+ island,
169
+ entryFile,
170
+ payloadFile,
171
+ runtimeFile
172
+ ),
173
+ "utf8"
174
+ );
175
+
176
+ entryPoints[
177
+ entryName
178
+ ] =
179
+ entryFile;
180
+
181
+ entryToIsland.set(
182
+ normalizePath(
183
+ entryFile
184
+ ),
185
+ island.id
186
+ );
187
+ }
188
+
189
+ const result =
190
+ await build({
191
+ absWorkingDir:
192
+ rootDirectory,
193
+ entryPoints,
194
+ outdir:
195
+ clientDirectory,
196
+ bundle:
197
+ true,
198
+ splitting:
199
+ true,
200
+ format:
201
+ "esm",
202
+ platform:
203
+ "browser",
204
+ target: [
205
+ "es2022",
206
+ ],
207
+ minify:
208
+ true,
209
+ treeShaking:
210
+ true,
211
+ sourcemap:
212
+ false,
213
+ metafile:
214
+ true,
215
+ jsx:
216
+ "automatic",
217
+ entryNames:
218
+ "islands/[name]-[hash]",
219
+ chunkNames:
220
+ "island-chunks/[name]-[hash]",
221
+ assetNames:
222
+ "island-assets/[name]-[hash]",
223
+ define:
224
+ createClientEnvironmentDefines(
225
+ "production"
226
+ ),
227
+ legalComments:
228
+ "none",
229
+ logLevel:
230
+ "silent",
231
+ });
232
+
233
+ if (!result.metafile) {
234
+ throw new Error(
235
+ "BCP Framework: esbuild did not return client island metadata."
236
+ );
237
+ }
238
+
239
+ const outputNodes =
240
+ createOutputNodeMap(
241
+ result.metafile,
242
+ rootDirectory,
243
+ clientDirectory
244
+ );
245
+
246
+ const assetInfoByPath =
247
+ new Map<
248
+ string,
249
+ ProductionAssetInfo
250
+ >();
251
+
252
+ for (const node of outputNodes.values()) {
253
+ const assetInfo =
254
+ precompressOutput(
255
+ node.absolutePath,
256
+ node.publicPath
257
+ );
258
+
259
+ assetInfoByPath.set(
260
+ node.publicPath,
261
+ assetInfo
262
+ );
263
+ }
264
+
265
+ const islandById =
266
+ new Map(
267
+ islands.map(
268
+ (island) => [
269
+ island.id,
270
+ island,
271
+ ]
272
+ )
273
+ );
274
+
275
+ const islandManifest:
276
+ Record<
277
+ string,
278
+ ProductionIslandAsset
279
+ > = {};
280
+
281
+ for (const node of outputNodes.values()) {
282
+ if (!node.metadata.entryPoint) {
283
+ continue;
284
+ }
285
+
286
+ const absoluteEntry =
287
+ path.isAbsolute(
288
+ node.metadata.entryPoint
289
+ )
290
+ ? node.metadata.entryPoint
291
+ : path.resolve(
292
+ rootDirectory,
293
+ node.metadata.entryPoint
294
+ );
295
+
296
+ const islandId =
297
+ entryToIsland.get(
298
+ normalizePath(
299
+ absoluteEntry
300
+ )
301
+ );
302
+
303
+ if (!islandId) {
304
+ continue;
305
+ }
306
+
307
+ const island =
308
+ islandById.get(
309
+ islandId
310
+ );
311
+
312
+ if (!island) {
313
+ continue;
314
+ }
315
+
316
+ const dependencies =
317
+ collectOutputImports(
318
+ node,
319
+ outputNodes,
320
+ rootDirectory
321
+ );
322
+
323
+ islandManifest[
324
+ islandId
325
+ ] = {
326
+ entry:
327
+ node.publicPath,
328
+ imports:
329
+ island.preload
330
+ ? dependencies
331
+ : [],
332
+ size:
333
+ sumAssetSizes(
334
+ [
335
+ node.publicPath,
336
+ ...dependencies,
337
+ ],
338
+ assetInfoByPath
339
+ ),
340
+ hydrate:
341
+ island.hydrate,
342
+ media:
343
+ island.media,
344
+ prefetch:
345
+ island.prefetch,
346
+ preload:
347
+ island.preload,
348
+ };
349
+ }
350
+
351
+ for (const island of islands) {
352
+ if (
353
+ !islandManifest[
354
+ island.id
355
+ ]
356
+ ) {
357
+ throw new Error(
358
+ `BCP Framework: client island output was not found for "${island.id}".`
359
+ );
360
+ }
361
+ }
362
+
363
+ const existingAssets =
364
+ new Set(
365
+ manifest.assets.map(
366
+ (asset) =>
367
+ asset.path
368
+ )
369
+ );
370
+
371
+ for (const asset of assetInfoByPath.values()) {
372
+ if (
373
+ existingAssets.has(
374
+ asset.path
375
+ )
376
+ ) {
377
+ continue;
378
+ }
379
+
380
+ manifest.assets.push(
381
+ asset
382
+ );
383
+ }
384
+
385
+ manifest.assets.sort(
386
+ (left, right) =>
387
+ left.path.localeCompare(
388
+ right.path
389
+ )
390
+ );
391
+
392
+ fs.rmSync(
393
+ entryDirectory,
394
+ {
395
+ recursive: true,
396
+ force: true,
397
+ }
398
+ );
399
+
400
+ return islandManifest;
401
+ }
402
+
403
+ function discoverIslandSources(
404
+ appDirectory: string
405
+ ): IslandSource[] {
406
+ if (
407
+ !fs.existsSync(
408
+ appDirectory
409
+ )
410
+ ) {
411
+ return [];
412
+ }
413
+
414
+ const islands:
415
+ IslandSource[] = [];
416
+
417
+ const seenIds =
418
+ new Map<string, string>();
419
+
420
+ walkDirectory(
421
+ appDirectory,
422
+ (filePath) => {
423
+ if (
424
+ !/\.island\.(?:ts|tsx)$/.test(
425
+ filePath
426
+ )
427
+ ) {
428
+ return;
429
+ }
430
+
431
+ const source =
432
+ fs.readFileSync(
433
+ filePath,
434
+ "utf8"
435
+ );
436
+
437
+ const island =
438
+ parseIslandSource(
439
+ source,
440
+ filePath
441
+ );
442
+
443
+ const duplicate =
444
+ seenIds.get(
445
+ island.id
446
+ );
447
+
448
+ if (duplicate) {
449
+ throw new Error(
450
+ `BCP Framework: duplicate island id "${island.id}" in "${duplicate}" and "${filePath}".`
451
+ );
452
+ }
453
+
454
+ seenIds.set(
455
+ island.id,
456
+ filePath
457
+ );
458
+
459
+ islands.push(
460
+ island
461
+ );
462
+ }
463
+ );
464
+
465
+ return islands.sort(
466
+ (left, right) =>
467
+ left.id.localeCompare(
468
+ right.id
469
+ )
470
+ );
471
+ }
472
+
473
+ function parseIslandSource(
474
+ source: string,
475
+ filePath: string
476
+ ): IslandSource {
477
+ const match =
478
+ /\bcreateIsland(?:\s*<[^>]+>)?\s*\(\s*["']([A-Za-z][A-Za-z0-9._-]*)["']\s*,/.exec(
479
+ source
480
+ );
481
+
482
+ if (!match) {
483
+ throw new Error(
484
+ `BCP Framework: island file "${filePath}" must export createIsland("literal-id", Component).`
485
+ );
486
+ }
487
+
488
+ const id =
489
+ match[1];
490
+
491
+ const configurationSource =
492
+ source.slice(
493
+ match.index
494
+ );
495
+
496
+ const hydrateMatch =
497
+ /\bhydrate\s*:\s*["']([^"']+)["']/.exec(
498
+ configurationSource
499
+ );
500
+
501
+ const rawHydrate =
502
+ hydrateMatch?.[1] ??
503
+ "load";
504
+
505
+ if (
506
+ rawHydrate !== "load" &&
507
+ rawHydrate !== "idle" &&
508
+ rawHydrate !== "visible" &&
509
+ rawHydrate !== "media"
510
+ ) {
511
+ throw new Error(
512
+ `BCP Framework: island "${id}" in "${filePath}" has invalid hydrate strategy "${rawHydrate}".`
513
+ );
514
+ }
515
+
516
+ const hydrate =
517
+ rawHydrate as
518
+ IslandHydrationStrategy;
519
+
520
+ const mediaMatch =
521
+ /\bmedia\s*:\s*(["'])(.*?)\1/.exec(
522
+ configurationSource
523
+ );
524
+
525
+ const media =
526
+ mediaMatch?.[2]?.trim() ??
527
+ "";
528
+
529
+ if (
530
+ hydrate === "media" &&
531
+ !media
532
+ ) {
533
+ throw new Error(
534
+ `BCP Framework: island "${id}" in "${filePath}" uses hydrate: "media" and requires a literal media query.`
535
+ );
536
+ }
537
+
538
+ if (
539
+ hydrate !== "media" &&
540
+ media
541
+ ) {
542
+ throw new Error(
543
+ `BCP Framework: island "${id}" in "${filePath}" defines media but hydrate is "${hydrate}".`
544
+ );
545
+ }
546
+
547
+ const prefetchMatch =
548
+ /\bprefetch\s*:\s*["']([^"']+)["']/.exec(
549
+ configurationSource
550
+ );
551
+
552
+ const rawPrefetch =
553
+ prefetchMatch?.[1] ??
554
+ "none";
555
+
556
+ if (
557
+ rawPrefetch !== "none" &&
558
+ rawPrefetch !== "idle" &&
559
+ rawPrefetch !== "visible" &&
560
+ rawPrefetch !== "interaction"
561
+ ) {
562
+ throw new Error(
563
+ `BCP Framework: island "${id}" in "${filePath}" has invalid prefetch strategy "${rawPrefetch}".`
564
+ );
565
+ }
566
+
567
+ const preloadProperty =
568
+ /\bpreload\s*:/.test(
569
+ configurationSource
570
+ );
571
+
572
+ const preloadMatch =
573
+ /\bpreload\s*:\s*(true|false)\b/.exec(
574
+ configurationSource
575
+ );
576
+
577
+ if (
578
+ preloadProperty &&
579
+ !preloadMatch
580
+ ) {
581
+ throw new Error(
582
+ `BCP Framework: island "${id}" in "${filePath}" requires preload to be a literal boolean.`
583
+ );
584
+ }
585
+
586
+ return {
587
+ id,
588
+ filePath,
589
+ hydrate,
590
+ media:
591
+ hydrate === "media"
592
+ ? media
593
+ : null,
594
+ prefetch:
595
+ rawPrefetch as
596
+ IslandPrefetchStrategy,
597
+ preload:
598
+ preloadMatch
599
+ ? preloadMatch[1] ===
600
+ "true"
601
+ : hydrate === "load",
602
+ };
603
+ }
604
+
605
+ function walkDirectory(
606
+ directory: string,
607
+ callback: (
608
+ filePath: string
609
+ ) => void
610
+ ): void {
611
+ for (
612
+ const entry
613
+ of fs.readdirSync(
614
+ directory,
615
+ {
616
+ withFileTypes: true,
617
+ }
618
+ )
619
+ ) {
620
+ const fullPath =
621
+ path.join(
622
+ directory,
623
+ entry.name
624
+ );
625
+
626
+ if (entry.isDirectory()) {
627
+ walkDirectory(
628
+ fullPath,
629
+ callback
630
+ );
631
+ continue;
632
+ }
633
+
634
+ if (entry.isFile()) {
635
+ callback(
636
+ fullPath
637
+ );
638
+ }
639
+ }
640
+ }
641
+
642
+ function createIslandPayloadModule(
643
+ island: IslandSource,
644
+ entryDirectory: string
645
+ ): string {
646
+ const islandImport =
647
+ toImportSpecifier(
648
+ entryDirectory,
649
+ island.filePath
650
+ );
651
+
652
+ return `
653
+ import {
654
+ createElement
655
+ } from "react";
656
+
657
+ import {
658
+ hydrateRoot
659
+ } from "react-dom/client";
660
+
661
+ import Island from ${JSON.stringify(
662
+ islandImport
663
+ )};
664
+
665
+ const metadata =
666
+ Island.__BCP_ISLAND__;
667
+
668
+ if (
669
+ !metadata ||
670
+ metadata.id !== ${JSON.stringify(
671
+ island.id
672
+ )}
673
+ ) {
674
+ throw new Error(
675
+ "BCP Framework: invalid client island metadata for ${escapeJavaScriptString(
676
+ island.id
677
+ )}."
678
+ );
679
+ }
680
+
681
+ export function hydrateTargets(
682
+ elements
683
+ ) {
684
+ for (
685
+ const element
686
+ of elements
687
+ ) {
688
+ if (
689
+ element.getAttribute(
690
+ "data-bcp-island-hydrated"
691
+ ) === "true"
692
+ ) {
693
+ continue;
694
+ }
695
+
696
+ const serializedProps =
697
+ element.getAttribute(
698
+ "data-bcp-island-props"
699
+ ) ||
700
+ "{}";
701
+
702
+ const props =
703
+ JSON.parse(
704
+ serializedProps
705
+ );
706
+
707
+ hydrateRoot(
708
+ element,
709
+ createElement(
710
+ metadata.component,
711
+ props
712
+ )
713
+ );
714
+
715
+ element.setAttribute(
716
+ "data-bcp-island-hydrated",
717
+ "true"
718
+ );
719
+ }
720
+ }
721
+ `.trim();
722
+ }
723
+
724
+ function createIslandSchedulerEntry(
725
+ island: IslandSource,
726
+ entryFile: string,
727
+ payloadFile: string,
728
+ runtimeFile: string
729
+ ): string {
730
+ const entryDirectory =
731
+ path.dirname(
732
+ entryFile
733
+ );
734
+
735
+ const payloadImport =
736
+ toImportSpecifier(
737
+ entryDirectory,
738
+ payloadFile
739
+ );
740
+
741
+ const runtimeImport =
742
+ toImportSpecifier(
743
+ entryDirectory,
744
+ runtimeFile
745
+ );
746
+
747
+ return `
748
+ import {
749
+ scheduleIsland
750
+ } from ${JSON.stringify(
751
+ runtimeImport
752
+ )};
753
+
754
+ scheduleIsland(
755
+ {
756
+ id: ${JSON.stringify(island.id)},
757
+ hydrate: ${JSON.stringify(island.hydrate)},
758
+ media: ${JSON.stringify(island.media)},
759
+ prefetch: ${JSON.stringify(island.prefetch)}
760
+ },
761
+ () => import(${JSON.stringify(
762
+ payloadImport
763
+ )})
764
+ );
765
+ `.trim();
766
+ }
767
+
768
+ function createSharedIslandRuntimeModule(): string {
769
+ return `
770
+ export function scheduleIsland(
771
+ options,
772
+ importPayload
773
+ ) {
774
+ const {
775
+ id,
776
+ hydrate,
777
+ media,
778
+ prefetch
779
+ } = options;
780
+
781
+ let payloadPromise;
782
+
783
+ function getTargets() {
784
+ return Array.from(
785
+ document.querySelectorAll(
786
+ '[data-bcp-island="' +
787
+ escapeSelectorValue(id) +
788
+ '"]'
789
+ )
790
+ );
791
+ }
792
+
793
+ function loadPayload() {
794
+ if (!payloadPromise) {
795
+ payloadPromise =
796
+ importPayload();
797
+ }
798
+
799
+ return payloadPromise;
800
+ }
801
+
802
+ function prefetchIsland() {
803
+ void loadPayload()
804
+ .catch(
805
+ (error) => {
806
+ console.error(
807
+ "[BCP Island] " +
808
+ id +
809
+ " prefetch failed:",
810
+ error
811
+ );
812
+ }
813
+ );
814
+ }
815
+
816
+ async function hydrateIsland() {
817
+ const payload =
818
+ await loadPayload();
819
+
820
+ if (
821
+ !payload ||
822
+ typeof payload.hydrateTargets !==
823
+ "function"
824
+ ) {
825
+ throw new Error(
826
+ 'BCP Framework: island "' +
827
+ id +
828
+ '" payload is missing hydrateTargets().'
829
+ );
830
+ }
831
+
832
+ payload.hydrateTargets(
833
+ getTargets()
834
+ );
835
+ }
836
+
837
+ function runHydration() {
838
+ void hydrateIsland()
839
+ .catch(
840
+ (error) => {
841
+ console.error(
842
+ "[BCP Island] " +
843
+ id +
844
+ " hydration failed:",
845
+ error
846
+ );
847
+ }
848
+ );
849
+ }
850
+
851
+ schedulePrefetch(
852
+ prefetch,
853
+ getTargets,
854
+ prefetchIsland
855
+ );
856
+
857
+ scheduleHydration(
858
+ hydrate,
859
+ media,
860
+ getTargets,
861
+ runHydration
862
+ );
863
+ }
864
+
865
+ function schedulePrefetch(
866
+ strategy,
867
+ getTargets,
868
+ prefetchIsland
869
+ ) {
870
+ if (strategy === "idle") {
871
+ if (
872
+ "requestIdleCallback" in window
873
+ ) {
874
+ window.requestIdleCallback(
875
+ prefetchIsland,
876
+ { timeout: 1500 }
877
+ );
878
+ } else {
879
+ window.setTimeout(
880
+ prefetchIsland,
881
+ 50
882
+ );
883
+ }
884
+
885
+ return;
886
+ }
887
+
888
+ if (strategy === "visible") {
889
+ const targets =
890
+ getTargets();
891
+
892
+ if (
893
+ targets.length === 0 ||
894
+ !("IntersectionObserver" in window)
895
+ ) {
896
+ return;
897
+ }
898
+
899
+ const observer =
900
+ new IntersectionObserver(
901
+ (entries) => {
902
+ if (
903
+ !entries.some(
904
+ (entry) =>
905
+ entry.isIntersecting
906
+ )
907
+ ) {
908
+ return;
909
+ }
910
+
911
+ observer.disconnect();
912
+ prefetchIsland();
913
+ },
914
+ {
915
+ rootMargin:
916
+ "800px 0px"
917
+ }
918
+ );
919
+
920
+ for (const target of targets) {
921
+ observer.observe(target);
922
+ }
923
+
924
+ return;
925
+ }
926
+
927
+ if (strategy === "interaction") {
928
+ for (const target of getTargets()) {
929
+ let prefetched = false;
930
+
931
+ const onInteraction =
932
+ () => {
933
+ if (prefetched) {
934
+ return;
935
+ }
936
+
937
+ prefetched = true;
938
+
939
+ target.removeEventListener(
940
+ "pointerenter",
941
+ onInteraction
942
+ );
943
+ target.removeEventListener(
944
+ "focusin",
945
+ onInteraction
946
+ );
947
+ target.removeEventListener(
948
+ "touchstart",
949
+ onInteraction
950
+ );
951
+
952
+ prefetchIsland();
953
+ };
954
+
955
+ target.addEventListener(
956
+ "pointerenter",
957
+ onInteraction,
958
+ { passive: true }
959
+ );
960
+ target.addEventListener(
961
+ "focusin",
962
+ onInteraction
963
+ );
964
+ target.addEventListener(
965
+ "touchstart",
966
+ onInteraction,
967
+ { passive: true }
968
+ );
969
+ }
970
+ }
971
+ }
972
+
973
+ function scheduleHydration(
974
+ strategy,
975
+ mediaQuery,
976
+ getTargets,
977
+ runHydration
978
+ ) {
979
+ if (strategy === "idle") {
980
+ if (
981
+ "requestIdleCallback" in window
982
+ ) {
983
+ window.requestIdleCallback(
984
+ runHydration,
985
+ { timeout: 2000 }
986
+ );
987
+ } else {
988
+ window.setTimeout(
989
+ runHydration,
990
+ 1
991
+ );
992
+ }
993
+
994
+ return;
995
+ }
996
+
997
+ if (strategy === "visible") {
998
+ const targets =
999
+ getTargets();
1000
+
1001
+ if (targets.length === 0) {
1002
+ return;
1003
+ }
1004
+
1005
+ if (
1006
+ !("IntersectionObserver" in window)
1007
+ ) {
1008
+ runHydration();
1009
+ return;
1010
+ }
1011
+
1012
+ const observer =
1013
+ new IntersectionObserver(
1014
+ (entries) => {
1015
+ if (
1016
+ !entries.some(
1017
+ (entry) =>
1018
+ entry.isIntersecting
1019
+ )
1020
+ ) {
1021
+ return;
1022
+ }
1023
+
1024
+ observer.disconnect();
1025
+ runHydration();
1026
+ },
1027
+ {
1028
+ rootMargin:
1029
+ "200px 0px"
1030
+ }
1031
+ );
1032
+
1033
+ for (const target of targets) {
1034
+ observer.observe(target);
1035
+ }
1036
+
1037
+ return;
1038
+ }
1039
+
1040
+ if (strategy === "media") {
1041
+ if (
1042
+ !("matchMedia" in window)
1043
+ ) {
1044
+ runHydration();
1045
+ return;
1046
+ }
1047
+
1048
+ const media =
1049
+ window.matchMedia(
1050
+ mediaQuery
1051
+ );
1052
+
1053
+ if (media.matches) {
1054
+ runHydration();
1055
+ return;
1056
+ }
1057
+
1058
+ const onChange =
1059
+ (event) => {
1060
+ if (!event.matches) {
1061
+ return;
1062
+ }
1063
+
1064
+ media.removeEventListener(
1065
+ "change",
1066
+ onChange
1067
+ );
1068
+ runHydration();
1069
+ };
1070
+
1071
+ media.addEventListener(
1072
+ "change",
1073
+ onChange
1074
+ );
1075
+
1076
+ return;
1077
+ }
1078
+
1079
+ runHydration();
1080
+ }
1081
+
1082
+ function escapeSelectorValue(
1083
+ value
1084
+ ) {
1085
+ if (
1086
+ globalThis.CSS &&
1087
+ typeof globalThis.CSS.escape ===
1088
+ "function"
1089
+ ) {
1090
+ return globalThis.CSS.escape(
1091
+ value
1092
+ );
1093
+ }
1094
+
1095
+ return String(value)
1096
+ .replaceAll(
1097
+ "\\\\",
1098
+ "\\\\\\\\"
1099
+ )
1100
+ .replaceAll(
1101
+ '"',
1102
+ '\\\\"'
1103
+ );
1104
+ }
1105
+ `.trim();
1106
+ }
1107
+
1108
+ function createOutputNodeMap(
1109
+ metafile: Metafile,
1110
+ rootDirectory: string,
1111
+ clientDirectory: string
1112
+ ): Map<string, OutputNode> {
1113
+ const nodes =
1114
+ new Map<string, OutputNode>();
1115
+
1116
+ for (
1117
+ const [
1118
+ outputName,
1119
+ metadata,
1120
+ ]
1121
+ of Object.entries(
1122
+ metafile.outputs
1123
+ )
1124
+ ) {
1125
+ const absolutePath =
1126
+ path.isAbsolute(
1127
+ outputName
1128
+ )
1129
+ ? outputName
1130
+ : path.resolve(
1131
+ rootDirectory,
1132
+ outputName
1133
+ );
1134
+
1135
+ const relativePath =
1136
+ path.relative(
1137
+ clientDirectory,
1138
+ absolutePath
1139
+ );
1140
+
1141
+ if (
1142
+ relativePath.startsWith(
1143
+ ".."
1144
+ ) ||
1145
+ path.isAbsolute(
1146
+ relativePath
1147
+ )
1148
+ ) {
1149
+ continue;
1150
+ }
1151
+
1152
+ const publicPath =
1153
+ `/_bcp/client/${relativePath.replace(
1154
+ /\\/g,
1155
+ "/"
1156
+ )}`;
1157
+
1158
+ nodes.set(
1159
+ normalizePath(
1160
+ absolutePath
1161
+ ),
1162
+ {
1163
+ absolutePath,
1164
+ publicPath,
1165
+ metadata,
1166
+ }
1167
+ );
1168
+ }
1169
+
1170
+ return nodes;
1171
+ }
1172
+
1173
+ function collectOutputImports(
1174
+ entryNode: OutputNode,
1175
+ nodes: Map<string, OutputNode>,
1176
+ rootDirectory: string
1177
+ ): string[] {
1178
+ const imports =
1179
+ new Set<string>();
1180
+
1181
+ const visited =
1182
+ new Set<string>();
1183
+
1184
+ function visit(
1185
+ node: OutputNode
1186
+ ): void {
1187
+ const key =
1188
+ normalizePath(
1189
+ node.absolutePath
1190
+ );
1191
+
1192
+ if (visited.has(key)) {
1193
+ return;
1194
+ }
1195
+
1196
+ visited.add(key);
1197
+
1198
+ for (
1199
+ const imported
1200
+ of node.metadata.imports
1201
+ ) {
1202
+ if (imported.external) {
1203
+ continue;
1204
+ }
1205
+
1206
+ const target =
1207
+ resolveImportedOutput(
1208
+ node,
1209
+ imported.path,
1210
+ nodes,
1211
+ rootDirectory
1212
+ );
1213
+
1214
+ if (!target) {
1215
+ continue;
1216
+ }
1217
+
1218
+ if (
1219
+ target.publicPath !==
1220
+ entryNode.publicPath &&
1221
+ /\.(?:m?js)$/i.test(
1222
+ target.publicPath
1223
+ )
1224
+ ) {
1225
+ imports.add(
1226
+ target.publicPath
1227
+ );
1228
+ }
1229
+
1230
+ visit(target);
1231
+ }
1232
+ }
1233
+
1234
+ visit(entryNode);
1235
+
1236
+ return Array.from(
1237
+ imports
1238
+ ).sort();
1239
+ }
1240
+
1241
+ function resolveImportedOutput(
1242
+ source: OutputNode,
1243
+ importPath: string,
1244
+ nodes: Map<string, OutputNode>,
1245
+ rootDirectory: string
1246
+ ): OutputNode | null {
1247
+ const candidates =
1248
+ path.isAbsolute(
1249
+ importPath
1250
+ )
1251
+ ? [
1252
+ importPath,
1253
+ ]
1254
+ : [
1255
+ path.resolve(
1256
+ rootDirectory,
1257
+ importPath
1258
+ ),
1259
+ path.resolve(
1260
+ path.dirname(
1261
+ source.absolutePath
1262
+ ),
1263
+ importPath
1264
+ ),
1265
+ ];
1266
+
1267
+ for (const candidate of candidates) {
1268
+ const node =
1269
+ nodes.get(
1270
+ normalizePath(
1271
+ candidate
1272
+ )
1273
+ );
1274
+
1275
+ if (node) {
1276
+ return node;
1277
+ }
1278
+ }
1279
+
1280
+ return null;
1281
+ }
1282
+
1283
+ function precompressOutput(
1284
+ absolutePath: string,
1285
+ publicPath: string
1286
+ ): ProductionAssetInfo {
1287
+ const content =
1288
+ fs.readFileSync(
1289
+ absolutePath
1290
+ );
1291
+
1292
+ const raw =
1293
+ content.length;
1294
+
1295
+ let gzip = raw;
1296
+ let brotli = raw;
1297
+
1298
+ if (
1299
+ raw >=
1300
+ PRECOMPRESS_MIN_BYTES &&
1301
+ /\.(?:m?js|css|json|html?|svg|txt|xml)$/i.test(
1302
+ absolutePath
1303
+ )
1304
+ ) {
1305
+ const gzipContent =
1306
+ gzipSync(
1307
+ content,
1308
+ {
1309
+ level: 9,
1310
+ }
1311
+ );
1312
+
1313
+ if (
1314
+ gzipContent.length < raw
1315
+ ) {
1316
+ fs.writeFileSync(
1317
+ `${absolutePath}.gz`,
1318
+ gzipContent
1319
+ );
1320
+ gzip =
1321
+ gzipContent.length;
1322
+ }
1323
+
1324
+ const brotliContent =
1325
+ brotliCompressSync(
1326
+ content,
1327
+ {
1328
+ params: {
1329
+ [constants.BROTLI_PARAM_QUALITY]:
1330
+ 8,
1331
+ },
1332
+ }
1333
+ );
1334
+
1335
+ if (
1336
+ brotliContent.length < raw
1337
+ ) {
1338
+ fs.writeFileSync(
1339
+ `${absolutePath}.br`,
1340
+ brotliContent
1341
+ );
1342
+ brotli =
1343
+ brotliContent.length;
1344
+ }
1345
+ }
1346
+
1347
+ return {
1348
+ path:
1349
+ publicPath,
1350
+ size: {
1351
+ raw,
1352
+ gzip,
1353
+ brotli,
1354
+ },
1355
+ };
1356
+ }
1357
+
1358
+ function sumAssetSizes(
1359
+ assetPaths: string[],
1360
+ assets: Map<
1361
+ string,
1362
+ ProductionAssetInfo
1363
+ >
1364
+ ): ProductionAssetSize {
1365
+ const total:
1366
+ ProductionAssetSize = {
1367
+ raw: 0,
1368
+ gzip: 0,
1369
+ brotli: 0,
1370
+ };
1371
+
1372
+ for (const assetPath of assetPaths) {
1373
+ const asset =
1374
+ assets.get(
1375
+ assetPath
1376
+ );
1377
+
1378
+ if (!asset) {
1379
+ continue;
1380
+ }
1381
+
1382
+ total.raw +=
1383
+ asset.size.raw;
1384
+ total.gzip +=
1385
+ asset.size.gzip;
1386
+ total.brotli +=
1387
+ asset.size.brotli;
1388
+ }
1389
+
1390
+ return total;
1391
+ }
1392
+
1393
+ function toImportSpecifier(
1394
+ fromDirectory: string,
1395
+ filePath: string
1396
+ ): string {
1397
+ let relative =
1398
+ path.relative(
1399
+ fromDirectory,
1400
+ filePath
1401
+ )
1402
+ .replace(
1403
+ /\\/g,
1404
+ "/"
1405
+ );
1406
+
1407
+ if (!relative.startsWith(".")) {
1408
+ relative =
1409
+ `./${relative}`;
1410
+ }
1411
+
1412
+ return relative;
1413
+ }
1414
+
1415
+ function sanitizeName(
1416
+ value: string
1417
+ ): string {
1418
+ return value.replace(
1419
+ /[^A-Za-z0-9._-]/g,
1420
+ "_"
1421
+ );
1422
+ }
1423
+
1424
+ function normalizePath(
1425
+ filePath: string
1426
+ ): string {
1427
+ let normalized =
1428
+ path.normalize(
1429
+ path.resolve(
1430
+ filePath
1431
+ )
1432
+ );
1433
+
1434
+ if (
1435
+ process.platform ===
1436
+ "win32"
1437
+ ) {
1438
+ normalized =
1439
+ normalized.toLowerCase();
1440
+ }
1441
+
1442
+ return normalized;
1443
+ }
1444
+
1445
+ function escapeJavaScriptString(
1446
+ value: string
1447
+ ): string {
1448
+ return value
1449
+ .replaceAll(
1450
+ "\\",
1451
+ "\\\\"
1452
+ )
1453
+ .replaceAll(
1454
+ '"',
1455
+ '\\"'
1456
+ );
1457
+ }