@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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/docs/caching.md +76 -0
- package/docs/configuration.md +97 -0
- package/docs/deployment.md +74 -0
- package/docs/getting-started.md +82 -0
- package/docs/middleware.md +58 -0
- package/docs/releasing.md +299 -0
- package/docs/routing.md +103 -0
- package/docs/security.md +57 -0
- package/package.json +68 -0
- package/packages/bundler/src/client-islands.ts +1457 -0
- package/packages/bundler/src/incremental-context.ts +206 -0
- package/packages/bundler/src/index.ts +1991 -0
- package/packages/bundler/src/module-graph.ts +317 -0
- package/packages/bundler/src/partial-hydration.ts +414 -0
- package/packages/bundler/src/production.ts +974 -0
- package/packages/bundler/src/server-production-middleware.ts +447 -0
- package/packages/bundler/src/server-production.ts +1193 -0
- package/packages/bundler/src/special-files.ts +131 -0
- package/packages/cache/src/index.ts +761 -0
- package/packages/cli/bin/bcp.mjs +93 -0
- package/packages/cli/src/args.ts +305 -0
- package/packages/cli/src/bootstrap.ts +514 -0
- package/packages/cli/src/index.ts +504 -0
- package/packages/cli/src/version.ts +45 -0
- package/packages/client/src/cache.ts +11 -0
- package/packages/client/src/config.ts +18 -0
- package/packages/client/src/error-boundary.tsx +149 -0
- package/packages/client/src/hydration.ts +3 -0
- package/packages/client/src/index.tsx +57 -0
- package/packages/client/src/islands.tsx +315 -0
- package/packages/client/src/metadata.ts +281 -0
- package/packages/client/src/navigation-loading.ts +52 -0
- package/packages/client/src/navigation-state.ts +80 -0
- package/packages/client/src/not-found.ts +34 -0
- package/packages/client/src/persistent-layout-runtime.ts +273 -0
- package/packages/client/src/router-v2.tsx +969 -0
- package/packages/client/src/router.tsx +1 -0
- package/packages/config/src/index.ts +1038 -0
- package/packages/env/src/index.ts +593 -0
- package/packages/router/src/advanced-router.ts +1032 -0
- package/packages/router/src/index.ts +1 -0
- package/packages/server/src/compression.ts +249 -0
- package/packages/server/src/dev-document-metadata.ts +154 -0
- package/packages/server/src/dev-hmr.ts +225 -0
- package/packages/server/src/index.ts +2265 -0
- package/packages/server/src/metadata.ts +478 -0
- package/packages/server/src/middleware-dev-server.ts +260 -0
- package/packages/server/src/middleware-loader.ts +140 -0
- package/packages/server/src/middleware-proxy.ts +516 -0
- package/packages/server/src/middleware.ts +704 -0
- package/packages/server/src/navigation-payload.ts +471 -0
- package/packages/server/src/production-server.ts +1746 -0
- package/packages/server/src/response-cache-proxy.ts +828 -0
- package/packages/server/src/security-proxy.ts +406 -0
- package/packages/server/src/security.ts +451 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
- package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
- package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
- package/packages/server/src/standalone-production-runtime.ts +1951 -0
- package/packages/server/src/standalone-production-server.ts +6 -0
- package/packages/server/src/static-assets.ts +262 -0
- 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
|
+
}
|