@chidchanun/bcp 0.1.4 → 0.1.6
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 +28 -0
- package/README.md +39 -1
- package/docs/application-modules.md +116 -0
- package/docs/development-logging.md +13 -0
- package/package.json +6 -1
- package/packages/bundler/src/application-modules.ts +771 -0
- package/packages/bundler/src/client-boundary.ts +484 -0
- package/packages/cli/src/index.ts +36 -5
- package/packages/client/src/server-only.browser.mjs +7 -0
- package/packages/client/src/server-only.d.ts +1 -0
- package/packages/client/src/server-only.mjs +1 -0
- package/packages/server/src/critical-css-proxy.ts +559 -0
- package/packages/server/src/critical-css.ts +132 -0
- package/packages/server/src/dev-hmr.ts +86 -0
- package/packages/server/src/middleware-dev-server.ts +59 -3
- package/packages/server/src/standalone-production-runtime-v6.ts +205 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Route,
|
|
6
|
+
} from "../../router/src/index.js";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
hasUseClientDirective,
|
|
10
|
+
resolveApplicationModuleAlias,
|
|
11
|
+
usesReactClientHooks,
|
|
12
|
+
} from "./application-modules.js";
|
|
13
|
+
|
|
14
|
+
const MODULE_EXTENSIONS = [
|
|
15
|
+
".ts",
|
|
16
|
+
".tsx",
|
|
17
|
+
".mts",
|
|
18
|
+
".cts",
|
|
19
|
+
".js",
|
|
20
|
+
".jsx",
|
|
21
|
+
".mjs",
|
|
22
|
+
".cjs",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export function validateClientBoundaries(
|
|
26
|
+
routes: Route[],
|
|
27
|
+
rootDirectory: string
|
|
28
|
+
): void {
|
|
29
|
+
const root =
|
|
30
|
+
path.resolve(
|
|
31
|
+
rootDirectory
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const visited =
|
|
35
|
+
new Set<string>();
|
|
36
|
+
|
|
37
|
+
for (
|
|
38
|
+
const route
|
|
39
|
+
of routes
|
|
40
|
+
) {
|
|
41
|
+
visitClientModule(
|
|
42
|
+
route.filePath,
|
|
43
|
+
route.pathname,
|
|
44
|
+
root,
|
|
45
|
+
visited
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
for (
|
|
49
|
+
const layoutFile
|
|
50
|
+
of route.layouts
|
|
51
|
+
) {
|
|
52
|
+
visitClientModule(
|
|
53
|
+
layoutFile,
|
|
54
|
+
route.pathname,
|
|
55
|
+
root,
|
|
56
|
+
visited
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (
|
|
62
|
+
const islandFile
|
|
63
|
+
of findIslandFiles(
|
|
64
|
+
path.join(
|
|
65
|
+
root,
|
|
66
|
+
"app"
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
visitClientModule(
|
|
71
|
+
islandFile,
|
|
72
|
+
"client island",
|
|
73
|
+
root,
|
|
74
|
+
visited,
|
|
75
|
+
true
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function visitClientModule(
|
|
81
|
+
filePath: string,
|
|
82
|
+
routeName: string,
|
|
83
|
+
rootDirectory: string,
|
|
84
|
+
visited: Set<string>,
|
|
85
|
+
islandRoot = false
|
|
86
|
+
): void {
|
|
87
|
+
const normalized =
|
|
88
|
+
normalizePath(
|
|
89
|
+
filePath
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
visited.has(
|
|
94
|
+
normalized
|
|
95
|
+
)
|
|
96
|
+
) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
visited.add(
|
|
101
|
+
normalized
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const source =
|
|
105
|
+
fs.readFileSync(
|
|
106
|
+
filePath,
|
|
107
|
+
"utf8"
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
!islandRoot &&
|
|
112
|
+
usesReactClientHooks(
|
|
113
|
+
source
|
|
114
|
+
) &&
|
|
115
|
+
!hasUseClientDirective(
|
|
116
|
+
source
|
|
117
|
+
)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`BCP Framework: ${formatApplicationPath(rootDirectory, filePath)} uses React client hooks on ${routeName} but is missing the "use client" directive. Add "use client" as the first directive in the module.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (
|
|
125
|
+
const specifier
|
|
126
|
+
of collectRuntimeImports(
|
|
127
|
+
source
|
|
128
|
+
)
|
|
129
|
+
) {
|
|
130
|
+
if (
|
|
131
|
+
specifier ===
|
|
132
|
+
"bcp/server-only"
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`BCP Framework: ${formatApplicationPath(rootDirectory, filePath)} imports bcp/server-only but is reachable from the client bundle for ${routeName}. Move the server dependency behind an API route.`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const dependency =
|
|
140
|
+
resolveLocalDependency(
|
|
141
|
+
rootDirectory,
|
|
142
|
+
filePath,
|
|
143
|
+
specifier
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!dependency) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
visitClientModule(
|
|
151
|
+
dependency,
|
|
152
|
+
routeName,
|
|
153
|
+
rootDirectory,
|
|
154
|
+
visited,
|
|
155
|
+
/\.island\.(?:ts|tsx)$/.test(
|
|
156
|
+
dependency
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectRuntimeImports(
|
|
163
|
+
source: string
|
|
164
|
+
): string[] {
|
|
165
|
+
const imports =
|
|
166
|
+
new Set<string>();
|
|
167
|
+
|
|
168
|
+
const staticImport =
|
|
169
|
+
/\bimport\s+(?!type\b)(?:[^;"']*?\s+from\s+)?["']([^"']+)["']/g;
|
|
170
|
+
const reExport =
|
|
171
|
+
/\bexport\s+(?!type\b)[^;"']*?\s+from\s+["']([^"']+)["']/g;
|
|
172
|
+
const dynamicImport =
|
|
173
|
+
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
174
|
+
|
|
175
|
+
for (
|
|
176
|
+
const pattern
|
|
177
|
+
of [
|
|
178
|
+
staticImport,
|
|
179
|
+
reExport,
|
|
180
|
+
dynamicImport,
|
|
181
|
+
]
|
|
182
|
+
) {
|
|
183
|
+
let match:
|
|
184
|
+
RegExpExecArray | null;
|
|
185
|
+
|
|
186
|
+
while (
|
|
187
|
+
(
|
|
188
|
+
match =
|
|
189
|
+
pattern.exec(
|
|
190
|
+
source
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
imports.add(
|
|
195
|
+
match[1]
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return Array.from(
|
|
201
|
+
imports
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveLocalDependency(
|
|
206
|
+
rootDirectory: string,
|
|
207
|
+
importer: string,
|
|
208
|
+
specifier: string
|
|
209
|
+
): string | null {
|
|
210
|
+
if (
|
|
211
|
+
specifier.startsWith(
|
|
212
|
+
"@/"
|
|
213
|
+
)
|
|
214
|
+
) {
|
|
215
|
+
return resolveApplicationModuleAlias(
|
|
216
|
+
rootDirectory,
|
|
217
|
+
specifier
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
!specifier.startsWith(
|
|
223
|
+
"./"
|
|
224
|
+
) &&
|
|
225
|
+
!specifier.startsWith(
|
|
226
|
+
"../"
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const unresolved =
|
|
233
|
+
path.resolve(
|
|
234
|
+
path.dirname(
|
|
235
|
+
importer
|
|
236
|
+
),
|
|
237
|
+
specifier
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const resolved =
|
|
241
|
+
resolveExistingModulePath(
|
|
242
|
+
unresolved
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!resolved) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const root =
|
|
250
|
+
path.resolve(
|
|
251
|
+
rootDirectory
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
!isPathInside(
|
|
256
|
+
root,
|
|
257
|
+
resolved
|
|
258
|
+
)
|
|
259
|
+
) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return resolved;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function resolveExistingModulePath(
|
|
267
|
+
unresolvedPath: string
|
|
268
|
+
): string | null {
|
|
269
|
+
if (
|
|
270
|
+
isFile(
|
|
271
|
+
unresolvedPath
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
return unresolvedPath;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (
|
|
278
|
+
path.extname(
|
|
279
|
+
unresolvedPath
|
|
280
|
+
) === ""
|
|
281
|
+
) {
|
|
282
|
+
for (
|
|
283
|
+
const extension
|
|
284
|
+
of MODULE_EXTENSIONS
|
|
285
|
+
) {
|
|
286
|
+
const candidate =
|
|
287
|
+
`${unresolvedPath}${extension}`;
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
isFile(
|
|
291
|
+
candidate
|
|
292
|
+
)
|
|
293
|
+
) {
|
|
294
|
+
return candidate;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
isDirectory(
|
|
301
|
+
unresolvedPath
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
304
|
+
for (
|
|
305
|
+
const extension
|
|
306
|
+
of MODULE_EXTENSIONS
|
|
307
|
+
) {
|
|
308
|
+
const candidate =
|
|
309
|
+
path.join(
|
|
310
|
+
unresolvedPath,
|
|
311
|
+
`index${extension}`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
isFile(
|
|
316
|
+
candidate
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
return candidate;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function findIslandFiles(
|
|
328
|
+
appDirectory: string
|
|
329
|
+
): string[] {
|
|
330
|
+
if (
|
|
331
|
+
!fs.existsSync(
|
|
332
|
+
appDirectory
|
|
333
|
+
)
|
|
334
|
+
) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const result:
|
|
339
|
+
string[] = [];
|
|
340
|
+
|
|
341
|
+
walkDirectory(
|
|
342
|
+
appDirectory,
|
|
343
|
+
(filePath) => {
|
|
344
|
+
if (
|
|
345
|
+
/\.island\.(?:ts|tsx)$/.test(
|
|
346
|
+
filePath
|
|
347
|
+
)
|
|
348
|
+
) {
|
|
349
|
+
result.push(
|
|
350
|
+
filePath
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return result.sort();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function walkDirectory(
|
|
360
|
+
directory: string,
|
|
361
|
+
callback: (
|
|
362
|
+
filePath: string
|
|
363
|
+
) => void
|
|
364
|
+
): void {
|
|
365
|
+
for (
|
|
366
|
+
const entry
|
|
367
|
+
of fs.readdirSync(
|
|
368
|
+
directory,
|
|
369
|
+
{
|
|
370
|
+
withFileTypes: true,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
) {
|
|
374
|
+
const fullPath =
|
|
375
|
+
path.join(
|
|
376
|
+
directory,
|
|
377
|
+
entry.name
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
entry.isDirectory()
|
|
382
|
+
) {
|
|
383
|
+
walkDirectory(
|
|
384
|
+
fullPath,
|
|
385
|
+
callback
|
|
386
|
+
);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
entry.isFile()
|
|
392
|
+
) {
|
|
393
|
+
callback(
|
|
394
|
+
fullPath
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function formatApplicationPath(
|
|
401
|
+
rootDirectory: string,
|
|
402
|
+
filePath: string
|
|
403
|
+
): string {
|
|
404
|
+
const relative =
|
|
405
|
+
path.relative(
|
|
406
|
+
rootDirectory,
|
|
407
|
+
filePath
|
|
408
|
+
)
|
|
409
|
+
.replace(
|
|
410
|
+
/\\/g,
|
|
411
|
+
"/"
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return `"${relative || filePath}"`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isPathInside(
|
|
418
|
+
parent: string,
|
|
419
|
+
child: string
|
|
420
|
+
): boolean {
|
|
421
|
+
const relative =
|
|
422
|
+
path.relative(
|
|
423
|
+
parent,
|
|
424
|
+
child
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
relative === "" ||
|
|
429
|
+
(
|
|
430
|
+
relative !== ".." &&
|
|
431
|
+
!relative.startsWith(
|
|
432
|
+
`..${path.sep}`
|
|
433
|
+
) &&
|
|
434
|
+
!path.isAbsolute(
|
|
435
|
+
relative
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function isFile(
|
|
442
|
+
filePath: string
|
|
443
|
+
): boolean {
|
|
444
|
+
try {
|
|
445
|
+
return fs.statSync(
|
|
446
|
+
filePath
|
|
447
|
+
).isFile();
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isDirectory(
|
|
454
|
+
filePath: string
|
|
455
|
+
): boolean {
|
|
456
|
+
try {
|
|
457
|
+
return fs.statSync(
|
|
458
|
+
filePath
|
|
459
|
+
).isDirectory();
|
|
460
|
+
} catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function normalizePath(
|
|
466
|
+
filePath: string
|
|
467
|
+
): string {
|
|
468
|
+
let normalized =
|
|
469
|
+
path.normalize(
|
|
470
|
+
path.resolve(
|
|
471
|
+
filePath
|
|
472
|
+
)
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (
|
|
476
|
+
process.platform ===
|
|
477
|
+
"win32"
|
|
478
|
+
) {
|
|
479
|
+
normalized =
|
|
480
|
+
normalized.toLowerCase();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return normalized;
|
|
484
|
+
}
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
pathToFileURL,
|
|
5
5
|
} from "node:url";
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
installApplicationModuleAlias,
|
|
9
|
+
} from "../../bundler/src/application-modules.js";
|
|
10
|
+
import {
|
|
11
|
+
validateClientBoundaries,
|
|
12
|
+
} from "../../bundler/src/client-boundary.js";
|
|
7
13
|
import {
|
|
8
14
|
readResolvedBcpConfig,
|
|
9
15
|
} from "../../config/src/index.js";
|
|
@@ -72,6 +78,31 @@ async function runDev() {
|
|
|
72
78
|
process.env.NODE_ENV =
|
|
73
79
|
"development";
|
|
74
80
|
|
|
81
|
+
const rootDirectory =
|
|
82
|
+
resolveProjectRoot(
|
|
83
|
+
cliOptions.rootDirectory
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
installApplicationModuleAlias(
|
|
87
|
+
rootDirectory
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const appDirectory =
|
|
91
|
+
path.join(
|
|
92
|
+
rootDirectory,
|
|
93
|
+
"app"
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const pageRoutes =
|
|
97
|
+
scanRoutes(
|
|
98
|
+
appDirectory
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
validateClientBoundaries(
|
|
102
|
+
pageRoutes,
|
|
103
|
+
rootDirectory
|
|
104
|
+
);
|
|
105
|
+
|
|
75
106
|
const {
|
|
76
107
|
createMiddlewareDevServer,
|
|
77
108
|
} =
|
|
@@ -79,11 +110,6 @@ async function runDev() {
|
|
|
79
110
|
"../../server/src/middleware-dev-server.js"
|
|
80
111
|
);
|
|
81
112
|
|
|
82
|
-
const rootDirectory =
|
|
83
|
-
resolveProjectRoot(
|
|
84
|
-
cliOptions.rootDirectory
|
|
85
|
-
);
|
|
86
|
-
|
|
87
113
|
console.log(
|
|
88
114
|
`[BCP CLI] Project: ${rootDirectory}`
|
|
89
115
|
);
|
|
@@ -151,6 +177,11 @@ async function runBuild() {
|
|
|
151
177
|
apiRoutes
|
|
152
178
|
);
|
|
153
179
|
|
|
180
|
+
validateClientBoundaries(
|
|
181
|
+
pageRoutes,
|
|
182
|
+
rootDirectory
|
|
183
|
+
);
|
|
184
|
+
|
|
154
185
|
console.log("");
|
|
155
186
|
console.log(
|
|
156
187
|
`[BCP Build] Project: ${rootDirectory}`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|