@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,317 @@
1
+ import path from "node:path";
2
+ import type { Metafile } from "esbuild";
3
+
4
+ export interface ModuleNode {
5
+ id: string;
6
+ bytes: number;
7
+
8
+ /**
9
+ * Modules ที่ module นี้ import
10
+ */
11
+ dependencies: Set<string>;
12
+
13
+ /**
14
+ * Modules ที่ import module นี้
15
+ */
16
+ importers: Set<string>;
17
+ }
18
+
19
+ export interface ModuleGraphStats {
20
+ modules: number;
21
+ edges: number;
22
+ }
23
+
24
+ export class ModuleGraph {
25
+ private readonly rootDir: string;
26
+
27
+ private nodes = new Map<string, ModuleNode>();
28
+
29
+ constructor(rootDir: string) {
30
+ this.rootDir = path.resolve(rootDir);
31
+ }
32
+
33
+ /**
34
+ * Update graph จาก esbuild metafile
35
+ */
36
+ update(metafile: Metafile): void {
37
+ const nextNodes = new Map<string, ModuleNode>();
38
+
39
+ /**
40
+ * ----------------------------------------
41
+ * 1. Create all nodes
42
+ * ----------------------------------------
43
+ */
44
+
45
+ for (const [inputPath, input] of Object.entries(metafile.inputs)) {
46
+ const id = this.normalizeInputPath(inputPath);
47
+
48
+ nextNodes.set(id, {
49
+ id,
50
+ bytes: input.bytes,
51
+ dependencies: new Set<string>(),
52
+ importers: new Set<string>(),
53
+ });
54
+ }
55
+
56
+ /**
57
+ * ----------------------------------------
58
+ * 2. Build dependency graph
59
+ * ----------------------------------------
60
+ */
61
+
62
+ for (const [inputPath, input] of Object.entries(metafile.inputs)) {
63
+ const importer = this.normalizeInputPath(inputPath);
64
+
65
+ const importerNode = nextNodes.get(importer);
66
+
67
+ if (!importerNode) {
68
+ continue;
69
+ }
70
+
71
+ for (const imported of input.imports) {
72
+ /**
73
+ * External module ไม่อยู่ใน graph ของ application
74
+ */
75
+ if (imported.external) {
76
+ continue;
77
+ }
78
+
79
+ const dependency = this.resolveDependency(
80
+ importer,
81
+ imported.path,
82
+ nextNodes,
83
+ );
84
+
85
+ if (!dependency) {
86
+ continue;
87
+ }
88
+
89
+ importerNode.dependencies.add(dependency);
90
+
91
+ const dependencyNode = nextNodes.get(dependency);
92
+
93
+ if (dependencyNode) {
94
+ dependencyNode.importers.add(importer);
95
+ }
96
+ }
97
+ }
98
+
99
+ this.nodes = nextNodes;
100
+ }
101
+
102
+ /**
103
+ * หา module ที่ได้รับผลกระทบจาก file ที่เปลี่ยน
104
+ *
105
+ * ตัวอย่าง:
106
+ *
107
+ * page.tsx
108
+ * ↑
109
+ * layout.tsx
110
+ * ↑
111
+ * app.tsx
112
+ *
113
+ * ถ้า page.tsx เปลี่ยน
114
+ * affected จะเป็น:
115
+ *
116
+ * page.tsx
117
+ * layout.tsx
118
+ * app.tsx
119
+ */
120
+ getAffectedModules(filePath: string): string[] {
121
+ const start = this.normalizeAbsolutePath(filePath);
122
+
123
+ if (!this.nodes.has(start)) {
124
+ return [];
125
+ }
126
+
127
+ const visited = new Set<string>();
128
+ const queue: string[] = [start];
129
+
130
+ while (queue.length > 0) {
131
+ const current = queue.shift();
132
+
133
+ if (!current) {
134
+ continue;
135
+ }
136
+
137
+ if (visited.has(current)) {
138
+ continue;
139
+ }
140
+
141
+ visited.add(current);
142
+
143
+ const node = this.nodes.get(current);
144
+
145
+ if (!node) {
146
+ continue;
147
+ }
148
+
149
+ for (const importer of node.importers) {
150
+ if (!visited.has(importer)) {
151
+ queue.push(importer);
152
+ }
153
+ }
154
+ }
155
+
156
+ return [...visited];
157
+ }
158
+
159
+ /**
160
+ * ตรวจสอบว่ามี module นี้หรือไม่
161
+ */
162
+ has(filePath: string): boolean {
163
+ return this.nodes.has(this.normalizeAbsolutePath(filePath));
164
+ }
165
+
166
+ /**
167
+ * ดึง module
168
+ */
169
+ get(filePath: string): ModuleNode | undefined {
170
+ return this.nodes.get(this.normalizeAbsolutePath(filePath));
171
+ }
172
+
173
+ /**
174
+ * ดึงทุก module
175
+ */
176
+ getAll(): ModuleNode[] {
177
+ return [...this.nodes.values()];
178
+ }
179
+
180
+ /**
181
+ * จำนวน module / dependency
182
+ */
183
+ getStats(): ModuleGraphStats {
184
+ let edges = 0;
185
+
186
+ for (const node of this.nodes.values()) {
187
+ edges += node.dependencies.size;
188
+ }
189
+
190
+ return {
191
+ modules: this.nodes.size,
192
+ edges,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Debug graph
198
+ */
199
+ print(): void {
200
+ console.log("\n[BCP] Module Graph");
201
+
202
+ for (const node of this.nodes.values()) {
203
+ const relative = path.relative(this.rootDir, node.id);
204
+
205
+ console.log(` ${relative}`);
206
+
207
+ for (const dependency of node.dependencies) {
208
+ const dependencyRelative = path.relative(
209
+ this.rootDir,
210
+ dependency,
211
+ );
212
+
213
+ console.log(` └─ ${dependencyRelative}`);
214
+ }
215
+ }
216
+
217
+ const stats = this.getStats();
218
+
219
+ console.log(
220
+ `[BCP] ${stats.modules} modules / ${stats.edges} dependencies\n`,
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Normalize path จาก metafile
226
+ */
227
+ private normalizeInputPath(inputPath: string): string {
228
+ if (path.isAbsolute(inputPath)) {
229
+ return this.normalizeAbsolutePath(inputPath);
230
+ }
231
+
232
+ return this.normalizeAbsolutePath(
233
+ path.resolve(this.rootDir, inputPath),
234
+ );
235
+ }
236
+
237
+ /**
238
+ * Normalize absolute path
239
+ */
240
+ private normalizeAbsolutePath(filePath: string): string {
241
+ return path.normalize(path.resolve(filePath));
242
+ }
243
+
244
+ /**
245
+ * Resolve import จาก metafile
246
+ */
247
+ private resolveDependency(
248
+ importer: string,
249
+ importedPath: string,
250
+ nodes: Map<string, ModuleNode>,
251
+ ): string | undefined {
252
+ const normalizedImported = importedPath.replaceAll("/", path.sep);
253
+
254
+ const candidates: string[] = [];
255
+
256
+ /**
257
+ * absolute
258
+ */
259
+ if (path.isAbsolute(normalizedImported)) {
260
+ candidates.push(
261
+ this.normalizeAbsolutePath(normalizedImported),
262
+ );
263
+ }
264
+
265
+ /**
266
+ * relative to importer
267
+ */
268
+ candidates.push(
269
+ this.normalizeAbsolutePath(
270
+ path.resolve(
271
+ path.dirname(importer),
272
+ normalizedImported,
273
+ ),
274
+ ),
275
+ );
276
+
277
+ /**
278
+ * relative to project root
279
+ */
280
+ candidates.push(
281
+ this.normalizeAbsolutePath(
282
+ path.resolve(
283
+ this.rootDir,
284
+ normalizedImported,
285
+ ),
286
+ ),
287
+ );
288
+
289
+ for (const candidate of candidates) {
290
+ if (nodes.has(candidate)) {
291
+ return candidate;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * metafile บางกรณีใช้ path relative
297
+ */
298
+ const normalizedRelative = path
299
+ .normalize(normalizedImported);
300
+
301
+ for (const nodePath of nodes.keys()) {
302
+ const relative = path
303
+ .relative(this.rootDir, nodePath)
304
+ .split(path.sep)
305
+ .join("/");
306
+
307
+ if (
308
+ relative === normalizedImported ||
309
+ path.normalize(relative) === normalizedRelative
310
+ ) {
311
+ return nodePath;
312
+ }
313
+ }
314
+
315
+ return undefined;
316
+ }
317
+ }
@@ -0,0 +1,414 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import type {
6
+ Route,
7
+ } from "../../router/src/index.js";
8
+
9
+ import {
10
+ buildClientIslands,
11
+ type ProductionIslandAsset,
12
+ } from "./client-islands.js";
13
+
14
+ import {
15
+ buildProductionClient as buildBaseProductionClient,
16
+ type ProductionAssetInfo,
17
+ type ProductionBuildManifest,
18
+ } from "./production.js";
19
+
20
+ export type HydrationMode =
21
+ | "full"
22
+ | "none";
23
+
24
+ export interface PartialHydrationBuildManifest
25
+ extends ProductionBuildManifest {
26
+ hydration: {
27
+ serverOnlyRoutes: string[];
28
+ };
29
+ islands: Record<
30
+ string,
31
+ ProductionIslandAsset
32
+ >;
33
+ }
34
+
35
+ export async function buildProductionClient(
36
+ routes: Route[],
37
+ rootDirectory: string
38
+ ): Promise<PartialHydrationBuildManifest> {
39
+ const manifest =
40
+ await buildBaseProductionClient(
41
+ routes,
42
+ rootDirectory
43
+ );
44
+
45
+ const islandsEnabled =
46
+ process.env
47
+ .BCP_EXPERIMENTAL_ISLANDS !==
48
+ "0";
49
+
50
+ const partialHydrationEnabled =
51
+ process.env
52
+ .BCP_EXPERIMENTAL_PARTIAL_HYDRATION !==
53
+ "0";
54
+
55
+ const islands =
56
+ islandsEnabled
57
+ ? await buildClientIslands(
58
+ manifest,
59
+ rootDirectory
60
+ )
61
+ : {};
62
+
63
+ const serverOnlyRoutes =
64
+ partialHydrationEnabled
65
+ ? applyPartialHydration(
66
+ manifest,
67
+ routes,
68
+ rootDirectory
69
+ )
70
+ : [];
71
+
72
+ const result:
73
+ PartialHydrationBuildManifest = {
74
+ ...manifest,
75
+ hydration: {
76
+ serverOnlyRoutes,
77
+ },
78
+ islands,
79
+ };
80
+
81
+ writeManifest(
82
+ result,
83
+ rootDirectory
84
+ );
85
+
86
+ return result;
87
+ }
88
+
89
+ function applyPartialHydration(
90
+ manifest: ProductionBuildManifest,
91
+ routes: Route[],
92
+ rootDirectory: string
93
+ ): string[] {
94
+ const buildRoot =
95
+ path.join(
96
+ rootDirectory,
97
+ ".bcp-framework",
98
+ "build"
99
+ );
100
+
101
+ const clientDirectory =
102
+ path.join(
103
+ buildRoot,
104
+ "client"
105
+ );
106
+
107
+ const serverOnlyRoutes:
108
+ string[] = [];
109
+
110
+ for (
111
+ const route
112
+ of routes
113
+ ) {
114
+ const hydration =
115
+ readHydrationMode(
116
+ route.filePath
117
+ );
118
+
119
+ if (
120
+ hydration !== "none"
121
+ ) {
122
+ continue;
123
+ }
124
+
125
+ const current =
126
+ manifest.routes[
127
+ route.pathname
128
+ ];
129
+
130
+ if (!current) {
131
+ throw new Error(
132
+ `BCP Framework: production client entry missing for route "${route.pathname}".`
133
+ );
134
+ }
135
+
136
+ removeUnusedRouteEntry(
137
+ current.entry,
138
+ manifest,
139
+ clientDirectory
140
+ );
141
+
142
+ const staticRuntime =
143
+ createServerOnlyRuntime();
144
+
145
+ const hash =
146
+ crypto
147
+ .createHash("sha256")
148
+ .update(
149
+ route.pathname
150
+ )
151
+ .update("\0")
152
+ .update(
153
+ staticRuntime
154
+ )
155
+ .digest("hex")
156
+ .slice(0, 10)
157
+ .toUpperCase();
158
+
159
+ const fileName =
160
+ `${routeToEntryName(
161
+ route.pathname
162
+ )}-server-only-${hash}.js`;
163
+
164
+ const relativePath =
165
+ path.join(
166
+ "routes",
167
+ fileName
168
+ );
169
+
170
+ const outputPath =
171
+ path.join(
172
+ clientDirectory,
173
+ relativePath
174
+ );
175
+
176
+ fs.mkdirSync(
177
+ path.dirname(
178
+ outputPath
179
+ ),
180
+ {
181
+ recursive: true,
182
+ }
183
+ );
184
+
185
+ fs.writeFileSync(
186
+ outputPath,
187
+ staticRuntime,
188
+ "utf8"
189
+ );
190
+
191
+ const publicPath =
192
+ `/_bcp/client/${relativePath.replace(
193
+ /\\/g,
194
+ "/"
195
+ )}`;
196
+
197
+ const size =
198
+ Buffer.byteLength(
199
+ staticRuntime
200
+ );
201
+
202
+ manifest.routes[
203
+ route.pathname
204
+ ] = {
205
+ entry:
206
+ publicPath,
207
+ imports: [],
208
+ size: {
209
+ raw:
210
+ size,
211
+ gzip:
212
+ size,
213
+ brotli:
214
+ size,
215
+ },
216
+ };
217
+
218
+ manifest.assets.push({
219
+ path:
220
+ publicPath,
221
+ size: {
222
+ raw:
223
+ size,
224
+ gzip:
225
+ size,
226
+ brotli:
227
+ size,
228
+ },
229
+ });
230
+
231
+ serverOnlyRoutes.push(
232
+ route.pathname
233
+ );
234
+ }
235
+
236
+ manifest.assets.sort(
237
+ (left, right) =>
238
+ left.path.localeCompare(
239
+ right.path
240
+ )
241
+ );
242
+
243
+ return serverOnlyRoutes.sort();
244
+ }
245
+
246
+ function writeManifest(
247
+ manifest: PartialHydrationBuildManifest,
248
+ rootDirectory: string
249
+ ): void {
250
+ const manifestPath =
251
+ path.join(
252
+ rootDirectory,
253
+ ".bcp-framework",
254
+ "build",
255
+ "manifest.json"
256
+ );
257
+
258
+ fs.writeFileSync(
259
+ manifestPath,
260
+ JSON.stringify(
261
+ manifest,
262
+ null,
263
+ 2
264
+ ),
265
+ "utf8"
266
+ );
267
+ }
268
+
269
+ function readHydrationMode(
270
+ filePath: string
271
+ ): HydrationMode {
272
+ const source =
273
+ fs.readFileSync(
274
+ filePath,
275
+ "utf8"
276
+ );
277
+
278
+ const match =
279
+ /\bexport\s+const\s+hydration\s*(?::[^=;]+)?=\s*["']([^"']+)["']/.exec(
280
+ source
281
+ );
282
+
283
+ if (!match) {
284
+ return "full";
285
+ }
286
+
287
+ const value =
288
+ match[1];
289
+
290
+ if (
291
+ value === "full" ||
292
+ value === "none"
293
+ ) {
294
+ return value;
295
+ }
296
+
297
+ throw new Error(
298
+ `BCP Framework: invalid hydration mode "${value}" in "${filePath}". Use "full" or "none".`
299
+ );
300
+ }
301
+
302
+ function removeUnusedRouteEntry(
303
+ publicPath: string,
304
+ manifest: ProductionBuildManifest,
305
+ clientDirectory: string
306
+ ): void {
307
+ const prefix =
308
+ "/_bcp/client/";
309
+
310
+ if (
311
+ !publicPath.startsWith(
312
+ prefix
313
+ )
314
+ ) {
315
+ return;
316
+ }
317
+
318
+ const relativePath =
319
+ publicPath.slice(
320
+ prefix.length
321
+ );
322
+
323
+ const outputPath =
324
+ path.resolve(
325
+ clientDirectory,
326
+ relativePath.replaceAll(
327
+ "/",
328
+ path.sep
329
+ )
330
+ );
331
+
332
+ const clientRoot =
333
+ path.resolve(
334
+ clientDirectory
335
+ );
336
+
337
+ const relative =
338
+ path.relative(
339
+ clientRoot,
340
+ outputPath
341
+ );
342
+
343
+ if (
344
+ relative.startsWith("..") ||
345
+ path.isAbsolute(
346
+ relative
347
+ )
348
+ ) {
349
+ return;
350
+ }
351
+
352
+ for (
353
+ const candidate
354
+ of [
355
+ outputPath,
356
+ `${outputPath}.gz`,
357
+ `${outputPath}.br`,
358
+ ]
359
+ ) {
360
+ fs.rmSync(
361
+ candidate,
362
+ {
363
+ force: true,
364
+ }
365
+ );
366
+ }
367
+
368
+ manifest.assets =
369
+ manifest.assets.filter(
370
+ (asset) =>
371
+ asset.path !==
372
+ publicPath
373
+ ) as ProductionAssetInfo[];
374
+ }
375
+
376
+ function createServerOnlyRuntime(): string {
377
+ return `if(globalThis.__BCP_REACT_ROOT__){window.location.reload();}`;
378
+ }
379
+
380
+ function routeToEntryName(
381
+ pathname: string
382
+ ): string {
383
+ if (
384
+ pathname === "/"
385
+ ) {
386
+ return "index";
387
+ }
388
+
389
+ return pathname
390
+ .slice(1)
391
+ .split("/")
392
+ .map(
393
+ sanitizeSegment
394
+ )
395
+ .join("__");
396
+ }
397
+
398
+ function sanitizeSegment(
399
+ segment: string
400
+ ): string {
401
+ const dynamicMatch =
402
+ /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
403
+ segment
404
+ );
405
+
406
+ if (dynamicMatch) {
407
+ return `param-${dynamicMatch[1]}`;
408
+ }
409
+
410
+ return segment.replace(
411
+ /[^A-Za-z0-9._-]/g,
412
+ "_"
413
+ );
414
+ }