@arcote.tech/arc-cli 0.6.2 → 0.7.1
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/dist/index.js +1696 -1663
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +64 -46
- package/src/builder/build-cache.ts +3 -1
- package/src/builder/chunk-planner.ts +107 -0
- package/src/builder/dependency-collector.ts +83 -41
- package/src/builder/framework-peers.ts +81 -0
- package/src/builder/module-builder.ts +322 -106
- package/src/commands/platform-build.ts +2 -1
- package/src/commands/platform-deploy.ts +121 -64
- package/src/commands/platform-dev.ts +11 -100
- package/src/commands/platform-start.ts +4 -90
- package/src/deploy/ansible.ts +23 -3
- package/src/deploy/assets/ansible/site.yml +23 -7
- package/src/deploy/assets.ts +23 -7
- package/src/deploy/bootstrap.ts +270 -10
- package/src/deploy/caddyfile.ts +19 -23
- package/src/deploy/compose.ts +44 -27
- package/src/deploy/config.ts +67 -3
- package/src/deploy/deploy-env.ts +129 -0
- package/src/deploy/env-file.ts +103 -0
- package/src/deploy/htpasswd.ts +28 -0
- package/src/deploy/image-template.ts +74 -0
- package/src/deploy/image.ts +243 -0
- package/src/deploy/registry.ts +79 -0
- package/src/deploy/ssh.ts +52 -122
- package/src/deploy/survey.ts +64 -0
- package/src/index.ts +20 -13
- package/src/platform/server.ts +119 -94
- package/src/platform/shared.ts +139 -292
- package/src/platform/startup.ts +159 -0
- package/runtime/Dockerfile +0 -29
- package/runtime/build-and-push.sh +0 -23
- package/runtime/entrypoint.sh +0 -58
- package/src/commands/build-shell.ts +0 -152
- package/src/deploy/remote-sync.ts +0 -321
- package/src/platform/deploy-api.ts +0 -400
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
updateCache,
|
|
18
18
|
type BuildCache,
|
|
19
19
|
} from "./build-cache";
|
|
20
|
+
import type { ChunkPlan } from "./chunk-planner";
|
|
21
|
+
import { SHELL_EXTERNALS } from "./framework-peers";
|
|
20
22
|
import {
|
|
21
23
|
readInstalledVersion,
|
|
22
24
|
sha256Hex,
|
|
@@ -28,8 +30,92 @@ import { pAll } from "./parallel";
|
|
|
28
30
|
|
|
29
31
|
/** Re-export for internal CLI consumers (avoid direct platform dependency in consumers). */
|
|
30
32
|
export type { BuildManifest, ModuleDescriptor };
|
|
31
|
-
/** Re-export hash helpers for backward compatibility (deploy
|
|
33
|
+
/** Re-export hash helpers for backward compatibility (deploy code uses sha256OfFiles). */
|
|
32
34
|
export { sha256Hex, sha256OfFiles };
|
|
35
|
+
/** Re-export externals so callers (shell builder) stay decoupled from framework-peers. */
|
|
36
|
+
export { SHELL_EXTERNALS };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bun's automatic JSX transform emits `import { jsxDEV } from "react/jsx-dev-runtime"`
|
|
40
|
+
* regardless of NODE_ENV. React's production module exports `jsxDEV` as undefined
|
|
41
|
+
* (it's a debug-only symbol). At runtime in a production container this crashes
|
|
42
|
+
* the moment a JSX call is reached. The plugin redirects `react/jsx-dev-runtime`
|
|
43
|
+
* imports to a synthetic module that re-exports `jsx` as `jsxDEV` from the
|
|
44
|
+
* production jsx-runtime — semantically equivalent at runtime, no debug data.
|
|
45
|
+
*
|
|
46
|
+
* Applied to BOTH browser and server bundles. Server bundles may import
|
|
47
|
+
* JSX-typed Trans/translation components from workspace deps; without the
|
|
48
|
+
* shim, loadServerContext fails on any server bundle that touches them.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Force every `react*` and `react-dom*` import to resolve from the workspace
|
|
52
|
+
* root's node_modules. Without this, monorepo symlinks (npm-linked
|
|
53
|
+
* @arcote.tech/platform → arc workspace's react@A; @ndt/* → ndt workspace's
|
|
54
|
+
* react@B) produce two physical React copies in the bundle. Two copies =
|
|
55
|
+
* two `ReactSharedInternals` = null dispatcher = "Invalid hook call".
|
|
56
|
+
*
|
|
57
|
+
* We resolve once from `rootDir` and pin every subsequent React-related
|
|
58
|
+
* specifier to that absolute path, defeating Bun's per-importer resolution.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Override `"sideEffects": false` for workspace + framework packages.
|
|
62
|
+
*
|
|
63
|
+
* Arc modules auto-register via top-level `module(...).build()` — that IS a
|
|
64
|
+
* side effect. But packages typically ship with `"sideEffects": false` (a
|
|
65
|
+
* good default for treeshake-friendly libs). Two failure modes follow:
|
|
66
|
+
*
|
|
67
|
+
* 1. Side-effect-only imports (`import "@ndt/strategy"`) get tree-shaken
|
|
68
|
+
* to nothing — module never registers.
|
|
69
|
+
* 2. Splitting heuristics treat such modules as freely duplicatable, so
|
|
70
|
+
* a context-package singleton (`WorkspaceContext = createContext(...)`)
|
|
71
|
+
* ends up cloned per entry; `useWorkspace()` can't find the provider.
|
|
72
|
+
*
|
|
73
|
+
* Bun.build accepts `sideEffects: true` in `onResolve` return values (same
|
|
74
|
+
* as esbuild) — that flag overrides package.json. We flip the bit for any
|
|
75
|
+
* `@ndt/*` or `@arcote.tech/*` import. React itself is fine; it doesn't
|
|
76
|
+
* use sideEffects:false in a problematic way.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
function singleReactPlugin(rootDir: string): import("bun").BunPlugin {
|
|
80
|
+
const reactPkgs = ["react", "react-dom", "react-dom/client", "react/jsx-runtime"];
|
|
81
|
+
return {
|
|
82
|
+
name: "single-react",
|
|
83
|
+
setup(build) {
|
|
84
|
+
const resolved = new Map<string, string>();
|
|
85
|
+
for (const spec of reactPkgs) {
|
|
86
|
+
try {
|
|
87
|
+
resolved.set(spec, Bun.resolveSync(spec, rootDir));
|
|
88
|
+
} catch {
|
|
89
|
+
// If consumer doesn't have it installed, leave Bun's default behavior.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
build.onResolve({ filter: /^(react|react-dom|react-dom\/client|react\/jsx-runtime)$/ }, (args) => {
|
|
93
|
+
const path = resolved.get(args.path);
|
|
94
|
+
return path ? { path } : null;
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function jsxDevShimPlugin(): import("bun").BunPlugin {
|
|
101
|
+
return {
|
|
102
|
+
name: "jsx-dev-runtime-shim",
|
|
103
|
+
setup(build) {
|
|
104
|
+
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
105
|
+
path: "react-jsx-dev-runtime-shim",
|
|
106
|
+
namespace: "jsx-dev-shim",
|
|
107
|
+
}));
|
|
108
|
+
build.onLoad({ filter: /.*/, namespace: "jsx-dev-shim" }, () => ({
|
|
109
|
+
contents: `import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
110
|
+
export const jsxDEV = jsx;
|
|
111
|
+
export const jsxsDEV = jsxs;
|
|
112
|
+
export { Fragment };
|
|
113
|
+
`,
|
|
114
|
+
loader: "ts",
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
33
119
|
|
|
34
120
|
/** Clients that a context package is built for. */
|
|
35
121
|
const CONTEXT_CLIENTS = [
|
|
@@ -37,21 +123,6 @@ const CONTEXT_CLIENTS = [
|
|
|
37
123
|
{ name: "browser", target: "browser" as const, defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } },
|
|
38
124
|
];
|
|
39
125
|
|
|
40
|
-
/** Packages that shell provides — modules import them but don't bundle them. */
|
|
41
|
-
export const SHELL_EXTERNALS = [
|
|
42
|
-
"react",
|
|
43
|
-
"react-dom",
|
|
44
|
-
"react/jsx-runtime",
|
|
45
|
-
"react/jsx-dev-runtime",
|
|
46
|
-
"@arcote.tech/arc",
|
|
47
|
-
"@arcote.tech/arc-ds",
|
|
48
|
-
"@arcote.tech/arc-react",
|
|
49
|
-
"@arcote.tech/arc-auth",
|
|
50
|
-
"@arcote.tech/arc-utils",
|
|
51
|
-
"@arcote.tech/arc-workspace",
|
|
52
|
-
"@arcote.tech/platform",
|
|
53
|
-
];
|
|
54
|
-
|
|
55
126
|
export interface WorkspacePackage {
|
|
56
127
|
name: string;
|
|
57
128
|
path: string;
|
|
@@ -206,9 +277,26 @@ async function buildContextClient(
|
|
|
206
277
|
|
|
207
278
|
console.log(` building: ${pkg.name} (${client.name})`);
|
|
208
279
|
|
|
280
|
+
// Externals: framework peers + npm dependencies + workspace deps.
|
|
281
|
+
//
|
|
282
|
+
// Workspace deps used to be bundled inline (because the deploy image has
|
|
283
|
+
// no workspace symlinks). The consequence: every context package's dist
|
|
284
|
+
// carried its own copy of every workspace dep — `@ndt/strategy/dist`
|
|
285
|
+
// shipped `@ndt/workspace` inlined, `@ndt/content/dist` shipped another
|
|
286
|
+
// copy, etc. At deploy-build time the top-level Bun.build saw N
|
|
287
|
+
// pre-inlined copies and could no longer dedupe them — context-package
|
|
288
|
+
// singletons (e.g. `WorkspaceContext = createContext`) duplicated per
|
|
289
|
+
// entry, breaking `useWorkspace()` provider lookup.
|
|
290
|
+
//
|
|
291
|
+
// Treating workspace deps as `external` makes per-package dist emit bare
|
|
292
|
+
// specifiers (`import { workspace } from "@ndt/workspace"`), which the
|
|
293
|
+
// browser-side Bun.build then resolves ONCE across all entries → single
|
|
294
|
+
// module instance, splitting hoists into a shared chunk. The deploy
|
|
295
|
+
// image is unaffected: the browser bundle is self-contained at that
|
|
296
|
+
// layer (we only ship the final chunks, not per-package dist/browser).
|
|
209
297
|
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
210
|
-
const
|
|
211
|
-
const externals = [...peerDeps, ...
|
|
298
|
+
const allDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
299
|
+
const externals = [...peerDeps, ...Object.keys(allDeps)];
|
|
212
300
|
|
|
213
301
|
const result = await Bun.build({
|
|
214
302
|
entrypoints: [pkg.entrypoint],
|
|
@@ -217,6 +305,7 @@ async function buildContextClient(
|
|
|
217
305
|
format: "esm",
|
|
218
306
|
naming: "index.[ext]",
|
|
219
307
|
external: externals,
|
|
308
|
+
plugins: [jsxDevShimPlugin()],
|
|
220
309
|
define: client.defines,
|
|
221
310
|
});
|
|
222
311
|
|
|
@@ -278,140 +367,267 @@ export async function buildContextPackages(
|
|
|
278
367
|
return { declarationErrors };
|
|
279
368
|
}
|
|
280
369
|
|
|
370
|
+
|
|
281
371
|
// ---------------------------------------------------------------------------
|
|
282
|
-
//
|
|
372
|
+
// Browser app build — one Bun.build with multiple entrypoints.
|
|
373
|
+
//
|
|
374
|
+
// Replaces the previous architecture of 25+ separate Bun.build calls (per
|
|
375
|
+
// shell peer + per context package × client + per token chunk group + initial
|
|
376
|
+
// bundle). The big win is `splitting: true` across ALL entries:
|
|
377
|
+
//
|
|
378
|
+
// - workspace context, framework, common deps land in ONE shared chunk
|
|
379
|
+
// referenced by both `initial.*.js` and `<token>.*.js`. No more multi-
|
|
380
|
+
// instance bugs (e.g. createWorkspace().build() running twice, each
|
|
381
|
+
// producing its own aggregate refs).
|
|
382
|
+
// - No importmap. Nothing is external — Bun bundles everything inline,
|
|
383
|
+
// dedups via shared chunks. The HTML loads `initial.<hash>.js` and that
|
|
384
|
+
// pulls shared chunks as needed.
|
|
385
|
+
// - Token chunks are first-class entries (`<token-name>.<hash>.js`) that
|
|
386
|
+
// side-effect-import all their member modules. Server signs the URL for
|
|
387
|
+
// the entry; shared chunks ride along unsigned (filenames are content-
|
|
388
|
+
// addressed so they're not enumerable without the manifest).
|
|
389
|
+
//
|
|
390
|
+
// Output layout under <outDir> = `<arcDir>/browser/`:
|
|
391
|
+
// initial.<hash>.js ← public modules + bootstrap entry
|
|
392
|
+
// <tokenName>.<hash>.js × N ← one per token group
|
|
393
|
+
// chunk-<hash>.js × N ← auto-shared (workspace ctx, framework, etc.)
|
|
283
394
|
// ---------------------------------------------------------------------------
|
|
284
395
|
|
|
285
|
-
interface
|
|
286
|
-
|
|
287
|
-
|
|
396
|
+
export interface BrowserGroupEntry {
|
|
397
|
+
/** Filename relative to outDir (`<token>.<hash>.js`). */
|
|
398
|
+
readonly file: string;
|
|
399
|
+
readonly hash: string;
|
|
400
|
+
/** Module names registered by this group (for manifest filterability). */
|
|
401
|
+
readonly modules: readonly string[];
|
|
288
402
|
}
|
|
289
403
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
404
|
+
export interface BrowserAppResult {
|
|
405
|
+
readonly initial: { file: string; hash: string };
|
|
406
|
+
/** Keyed by token.name. `initial` is NOT here — it's separate. */
|
|
407
|
+
readonly groups: Record<string, BrowserGroupEntry>;
|
|
408
|
+
/** Auto-shared chunks emitted by Bun.build splitting. Public, unsigned. */
|
|
409
|
+
readonly sharedChunks: readonly string[];
|
|
410
|
+
readonly cached: boolean;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function buildBrowserApp(
|
|
295
414
|
rootDir: string,
|
|
296
415
|
outDir: string,
|
|
297
|
-
|
|
416
|
+
plan: ChunkPlan,
|
|
298
417
|
cache: BuildCache,
|
|
299
418
|
noCache: boolean,
|
|
300
|
-
|
|
419
|
+
i18nCollector: Map<string, Set<string>>,
|
|
420
|
+
): Promise<BrowserAppResult> {
|
|
301
421
|
mkdirSync(outDir, { recursive: true });
|
|
302
422
|
|
|
303
|
-
const
|
|
423
|
+
const publicMembers = plan.groups.get("public") ?? [];
|
|
424
|
+
const protectedGroups = plan.chunks
|
|
425
|
+
.filter((c) => c !== "public")
|
|
426
|
+
.map((c) => ({ name: c, members: plan.groups.get(c) ?? [] }))
|
|
427
|
+
.filter((g) => g.members.length > 0);
|
|
304
428
|
|
|
305
|
-
|
|
306
|
-
// we run a single Bun.build for the whole bundle (chunk sharing wins).
|
|
307
|
-
const pkgHashes = packages.map((p) => ({
|
|
308
|
-
name: p.name,
|
|
309
|
-
safeName: basename(p.path),
|
|
310
|
-
srcHash: pkgSourceHash(p),
|
|
311
|
-
}));
|
|
429
|
+
const unitId = "browser-app";
|
|
312
430
|
|
|
431
|
+
// Cache key spans every package's source plus the build config that
|
|
432
|
+
// matters. If anything changes, full rebuild.
|
|
433
|
+
const allMembers: { name: string; group: string; srcHash: string }[] = [];
|
|
434
|
+
for (const m of publicMembers) {
|
|
435
|
+
allMembers.push({ name: m.pkg.name, group: "public", srcHash: pkgSourceHash(m.pkg) });
|
|
436
|
+
}
|
|
437
|
+
for (const g of protectedGroups) {
|
|
438
|
+
for (const m of g.members) {
|
|
439
|
+
allMembers.push({ name: m.pkg.name, group: g.name, srcHash: pkgSourceHash(m.pkg) });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
313
442
|
const inputHash = sha256OfJson({
|
|
314
|
-
|
|
315
|
-
|
|
443
|
+
members: allMembers,
|
|
444
|
+
groups: [
|
|
445
|
+
"initial",
|
|
446
|
+
...protectedGroups.map((g) => g.name).sort(),
|
|
447
|
+
],
|
|
316
448
|
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
317
449
|
});
|
|
318
450
|
|
|
319
|
-
// Cache hit → reconstruct module descriptors from existing files in outDir.
|
|
320
451
|
if (!noCache && isCacheHit(cache, unitId, inputHash)) {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
452
|
+
const cached = cache.units[unitId]?.outputHashes;
|
|
453
|
+
if (cached?._manifest) {
|
|
454
|
+
try {
|
|
455
|
+
const m = JSON.parse(cached._manifest) as BrowserAppResult;
|
|
456
|
+
const allFiles = [
|
|
457
|
+
m.initial.file,
|
|
458
|
+
...Object.values(m.groups).map((g) => g.file),
|
|
459
|
+
...m.sharedChunks,
|
|
460
|
+
];
|
|
461
|
+
if (allFiles.every((f) => existsSync(join(outDir, f)))) {
|
|
462
|
+
console.log(` ✓ cached: ${unitId}`);
|
|
463
|
+
return { ...m, cached: true };
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// fall through to rebuild
|
|
330
467
|
}
|
|
331
|
-
modules.push({ file, name, hash: existing[safeName] ?? sha256Hex(readFileSync(filePath)) });
|
|
332
468
|
}
|
|
333
|
-
console.log(` ✓ cached: modules-bundle (${modules.length} module(s))`);
|
|
334
|
-
return { modules, cached: true };
|
|
335
469
|
}
|
|
336
470
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
471
|
+
console.log(
|
|
472
|
+
` building: ${unitId} (initial: ${publicMembers.length} modules, groups: ${protectedGroups
|
|
473
|
+
.map((g) => `${g.name}=${g.members.length}`)
|
|
474
|
+
.join(",") || "none"})`,
|
|
475
|
+
);
|
|
341
476
|
|
|
342
|
-
|
|
343
|
-
|
|
477
|
+
// Wipe outDir so stale-hash files don't linger.
|
|
478
|
+
if (existsSync(outDir)) {
|
|
479
|
+
for (const f of readdirSync(outDir)) {
|
|
480
|
+
if (f.endsWith(".js")) rmSync(join(outDir, f), { force: true });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
344
483
|
|
|
345
|
-
|
|
346
|
-
|
|
484
|
+
const tmpDir = join(outDir, "_entries");
|
|
485
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
347
486
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const moduleName = pkg.name.includes("/") ? pkg.name.split("/").pop()! : pkg.name;
|
|
351
|
-
fileToName.set(safeName, moduleName);
|
|
487
|
+
const importLines = (pkgs: { pkg: { name: string } }[]): string =>
|
|
488
|
+
pkgs.map((m) => `import "${m.pkg.name}";`).join("\n");
|
|
352
489
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
490
|
+
const initialEntry = join(tmpDir, "initial.ts");
|
|
491
|
+
writeFileSync(
|
|
492
|
+
initialEntry,
|
|
493
|
+
`${importLines(publicMembers)}\nexport { startApp } from "@arcote.tech/platform";\n`,
|
|
494
|
+
);
|
|
357
495
|
|
|
358
|
-
|
|
496
|
+
const entryPaths: string[] = [initialEntry];
|
|
497
|
+
const groupModuleMap = new Map<string, string[]>();
|
|
498
|
+
for (const g of protectedGroups) {
|
|
499
|
+
const entry = join(tmpDir, `${g.name}.ts`);
|
|
500
|
+
writeFileSync(entry, `${importLines(g.members)}\n`);
|
|
501
|
+
entryPaths.push(entry);
|
|
502
|
+
groupModuleMap.set(g.name, g.members.map((m) => m.moduleName));
|
|
503
|
+
}
|
|
359
504
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
505
|
+
// ---------------------------------------------------------------------
|
|
506
|
+
// Temporarily flip `"sideEffects"` on every workspace package's
|
|
507
|
+
// package.json from `false` to `true` for the duration of the build.
|
|
508
|
+
//
|
|
509
|
+
// Why: Arc modules auto-register via top-level `module(...).build()` —
|
|
510
|
+
// a real side effect. But packages ship with `"sideEffects": false` (a
|
|
511
|
+
// good ESM-library default), which makes Bun.build:
|
|
512
|
+
// (1) tree-shake side-effect-only imports → module().build() never runs,
|
|
513
|
+
// (2) freely duplicate the module across entry chunks → context
|
|
514
|
+
// singletons (`createContext`) get cloned, breaking provider lookup.
|
|
515
|
+
//
|
|
516
|
+
// Bun has no plugin hook to override package.json `sideEffects` and
|
|
517
|
+
// `build.resolve()` is not implemented (Bun#2771), so the cleanest
|
|
518
|
+
// reliable lever is a tiny on-disk patch with guaranteed restore.
|
|
519
|
+
// ---------------------------------------------------------------------
|
|
520
|
+
const allMemberPkgs = new Map<string, WorkspacePackage>();
|
|
521
|
+
for (const m of publicMembers) allMemberPkgs.set(m.pkg.name, m.pkg);
|
|
522
|
+
for (const g of protectedGroups)
|
|
523
|
+
for (const m of g.members) allMemberPkgs.set(m.pkg.name, m.pkg);
|
|
524
|
+
|
|
525
|
+
const patchedPkgJsons: { path: string; original: string }[] = [];
|
|
526
|
+
for (const pkg of allMemberPkgs.values()) {
|
|
527
|
+
const pkgJsonPath = join(pkg.path, "package.json");
|
|
528
|
+
if (!existsSync(pkgJsonPath)) continue;
|
|
529
|
+
const original = readFileSync(pkgJsonPath, "utf-8");
|
|
530
|
+
const parsed = JSON.parse(original);
|
|
531
|
+
if (parsed.sideEffects === true) continue; // already correct
|
|
532
|
+
parsed.sideEffects = true;
|
|
533
|
+
writeFileSync(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
534
|
+
patchedPkgJsons.push({ path: pkgJsonPath, original });
|
|
535
|
+
}
|
|
368
536
|
|
|
369
|
-
|
|
370
|
-
|
|
537
|
+
let result;
|
|
538
|
+
try {
|
|
539
|
+
result = await Bun.build({
|
|
540
|
+
entrypoints: entryPaths,
|
|
371
541
|
outdir: outDir,
|
|
542
|
+
// splitting:true is the whole point: shared deps (workspace context,
|
|
543
|
+
// framework, lucide, etc.) land in chunk-<hash>.js, referenced by both
|
|
544
|
+
// initial and token-group entries. One instance, no provider duplication.
|
|
372
545
|
splitting: true,
|
|
373
546
|
format: "esm",
|
|
374
547
|
target: "browser",
|
|
375
|
-
|
|
376
|
-
|
|
548
|
+
// No externals. Framework peers (react, @arcote.tech/*) get bundled and
|
|
549
|
+
// deduped into shared chunks. No importmap needed in the HTML.
|
|
550
|
+
external: [],
|
|
551
|
+
plugins: [
|
|
552
|
+
singleReactPlugin(rootDir),
|
|
553
|
+
jsxDevShimPlugin(),
|
|
554
|
+
i18nExtractPlugin(i18nCollector, rootDir),
|
|
555
|
+
],
|
|
377
556
|
naming: "[name].[ext]",
|
|
378
557
|
define: {
|
|
379
558
|
ONLY_SERVER: "false",
|
|
380
559
|
ONLY_BROWSER: "true",
|
|
381
560
|
ONLY_CLIENT: "true",
|
|
561
|
+
// CRITICAL: React's index.js does `if (process.env.NODE_ENV === 'production') require('./cjs/react.production.js') else require('./cjs/react.development.js')`.
|
|
562
|
+
// Without inlining NODE_ENV at build time Bun bundles BOTH branches → TWO ReactSharedInternals → multi-instance "Invalid hook call" crash.
|
|
563
|
+
"process.env.NODE_ENV": '"production"',
|
|
382
564
|
},
|
|
383
565
|
});
|
|
566
|
+
} finally {
|
|
567
|
+
// Always restore — a crash here MUST NOT leave the user with mutated
|
|
568
|
+
// workspace package.json files.
|
|
569
|
+
for (const p of patchedPkgJsons) writeFileSync(p.path, p.original);
|
|
570
|
+
}
|
|
384
571
|
|
|
385
|
-
|
|
386
|
-
console.error("Modules bundle build failed:");
|
|
387
|
-
for (const log of result.logs) console.error(log);
|
|
388
|
-
throw new Error("Module build failed");
|
|
389
|
-
}
|
|
572
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
390
573
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
574
|
+
if (!result.success) {
|
|
575
|
+
for (const log of result.logs) console.error(log);
|
|
576
|
+
throw new Error("Browser app build failed");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Bun's `[name]` in naming preserves entry filename without hash. We add
|
|
580
|
+
// content hashes ourselves so identical bytes produce identical URLs across
|
|
581
|
+
// deploys (good for browser caching). Shared chunks already have a hash in
|
|
582
|
+
// their name (Bun auto-emits `chunk-<hash>.js`); we leave those alone.
|
|
583
|
+
let initialFile = "";
|
|
584
|
+
let initialHash = "";
|
|
585
|
+
const groups: Record<string, BrowserGroupEntry> = {};
|
|
586
|
+
const sharedChunks: string[] = [];
|
|
587
|
+
|
|
588
|
+
for (const out of result.outputs) {
|
|
589
|
+
const name = basename(out.path);
|
|
590
|
+
if (out.kind === "entry-point") {
|
|
591
|
+
const bytes = readFileSync(out.path);
|
|
592
|
+
const hash = sha256Hex(bytes).slice(0, 16);
|
|
593
|
+
const stem = name.replace(/\.js$/, "");
|
|
594
|
+
const finalName = `${stem}.${hash}.js`;
|
|
595
|
+
const finalPath = join(outDir, finalName);
|
|
596
|
+
rmSync(finalPath, { force: true });
|
|
597
|
+
writeFileSync(finalPath, bytes);
|
|
598
|
+
rmSync(out.path, { force: true });
|
|
599
|
+
|
|
600
|
+
if (stem === "initial") {
|
|
601
|
+
initialFile = finalName;
|
|
602
|
+
initialHash = hash;
|
|
603
|
+
} else {
|
|
604
|
+
groups[stem] = {
|
|
605
|
+
file: finalName,
|
|
408
606
|
hash,
|
|
607
|
+
modules: groupModuleMap.get(stem) ?? [],
|
|
409
608
|
};
|
|
410
|
-
}
|
|
609
|
+
}
|
|
610
|
+
} else if (out.kind === "chunk") {
|
|
611
|
+
sharedChunks.push(name);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
411
614
|
|
|
412
|
-
|
|
413
|
-
|
|
615
|
+
if (!initialFile) {
|
|
616
|
+
throw new Error("Browser app build: initial entry not found in outputs");
|
|
414
617
|
}
|
|
618
|
+
|
|
619
|
+
const manifest: BrowserAppResult = {
|
|
620
|
+
initial: { file: initialFile, hash: initialHash },
|
|
621
|
+
groups,
|
|
622
|
+
sharedChunks,
|
|
623
|
+
cached: false,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
updateCache(cache, unitId, inputHash, {
|
|
627
|
+
outputHashes: { _manifest: JSON.stringify(manifest) },
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
return manifest;
|
|
415
631
|
}
|
|
416
632
|
|
|
417
633
|
// ---------------------------------------------------------------------------
|
|
@@ -3,5 +3,6 @@ import { buildAll, ok, resolveWorkspace } from "../platform/shared";
|
|
|
3
3
|
export async function platformBuild(opts: { noCache?: boolean } = {}): Promise<void> {
|
|
4
4
|
const ws = resolveWorkspace();
|
|
5
5
|
const manifest = await buildAll(ws, { noCache: opts.noCache });
|
|
6
|
-
|
|
6
|
+
const groupCount = Object.keys(manifest.groups).length;
|
|
7
|
+
ok(`Platform built — initial + ${groupCount} group(s)`);
|
|
7
8
|
}
|