@arcote.tech/arc-cli 0.7.0 → 0.7.2
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 +1410 -1339
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +10 -24
- package/src/builder/module-builder.ts +343 -160
- package/src/commands/platform-build.ts +2 -1
- package/src/commands/platform-deploy.ts +30 -21
- 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 +137 -28
- package/src/deploy/compose.ts +4 -3
- package/src/deploy/config.ts +38 -3
- package/src/deploy/deploy-env.ts +1 -1
- package/src/deploy/env-file.ts +103 -0
- package/src/deploy/image.ts +7 -1
- package/src/deploy/ssh.ts +51 -2
- package/src/index.ts +5 -0
- package/src/platform/server.ts +99 -99
- package/src/platform/shared.ts +28 -240
- package/src/platform/startup.ts +4 -5
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
updateCache,
|
|
18
18
|
type BuildCache,
|
|
19
19
|
} from "./build-cache";
|
|
20
|
-
import type { ChunkPlan
|
|
20
|
+
import type { ChunkPlan } from "./chunk-planner";
|
|
21
21
|
import { SHELL_EXTERNALS } from "./framework-peers";
|
|
22
22
|
import {
|
|
23
23
|
readInstalledVersion,
|
|
@@ -47,6 +47,56 @@ export { SHELL_EXTERNALS };
|
|
|
47
47
|
* JSX-typed Trans/translation components from workspace deps; without the
|
|
48
48
|
* shim, loadServerContext fails on any server bundle that touches them.
|
|
49
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
|
+
|
|
50
100
|
function jsxDevShimPlugin(): import("bun").BunPlugin {
|
|
51
101
|
return {
|
|
52
102
|
name: "jsx-dev-runtime-shim",
|
|
@@ -227,16 +277,32 @@ async function buildContextClient(
|
|
|
227
277
|
|
|
228
278
|
console.log(` building: ${pkg.name} (${client.name})`);
|
|
229
279
|
|
|
230
|
-
// Externals
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
280
|
+
// Externals depend on the target client:
|
|
281
|
+
//
|
|
282
|
+
// - BROWSER client: workspace deps MUST be external. Inlining them per
|
|
283
|
+
// package would make every context package's dist ship its own copy
|
|
284
|
+
// of every workspace dep — the top-level browser Bun.build would see
|
|
285
|
+
// N pre-inlined copies of context singletons (`WorkspaceContext =
|
|
286
|
+
// createContext`) and could not dedupe them, breaking provider lookup.
|
|
287
|
+
//
|
|
288
|
+
// - SERVER client: workspace deps MUST be bundled inline. The deploy
|
|
289
|
+
// image flattens each package's server bundle to
|
|
290
|
+
// `.arc/platform/server/<pkg>.js` and runs them via a single
|
|
291
|
+
// `loadServerContext()` import loop. There is no `node_modules/@ndt/*`
|
|
292
|
+
// tree inside the image, so bare `@ndt/workspace` specifiers would
|
|
293
|
+
// fail to resolve at startup. Inline duplication is harmless on the
|
|
294
|
+
// server: it's a single Node/Bun process and Arc modules register via
|
|
295
|
+
// a shared platform registry singleton (registry.ts), so two physical
|
|
296
|
+
// copies of the workspace module still merge into one context.
|
|
234
297
|
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
235
298
|
const allDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
.
|
|
239
|
-
|
|
299
|
+
const isBrowser = client.name === "browser";
|
|
300
|
+
const workspaceDeps = isBrowser
|
|
301
|
+
? Object.keys(allDeps)
|
|
302
|
+
: Object.entries(allDeps)
|
|
303
|
+
.filter(([, spec]) => !spec.startsWith("workspace:"))
|
|
304
|
+
.map(([name]) => name);
|
|
305
|
+
const externals = [...peerDeps, ...workspaceDeps];
|
|
240
306
|
|
|
241
307
|
const result = await Bun.build({
|
|
242
308
|
entrypoints: [pkg.entrypoint],
|
|
@@ -289,14 +355,51 @@ export async function buildContextPackages(
|
|
|
289
355
|
const contexts = packages.filter((p) => isContextPackage(p.packageJson));
|
|
290
356
|
if (contexts.length === 0) return { declarationErrors: [] };
|
|
291
357
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
358
|
+
// Topological order — each package's server bundle inlines its workspace
|
|
359
|
+
// deps, so those deps must have their `dist/` ready before Bun.build tries
|
|
360
|
+
// to resolve them. Without this, a fresh checkout (no dist/ yet) fails the
|
|
361
|
+
// first build because content's resolve of @ndt/strategy hits a missing
|
|
362
|
+
// file. Inside a topological level, packages are built in parallel.
|
|
363
|
+
const byName = new Map(contexts.map((p) => [p.name, p]));
|
|
364
|
+
const remaining = new Set(contexts.map((p) => p.name));
|
|
365
|
+
const done = new Set<string>();
|
|
366
|
+
const ordered: WorkspacePackage[][] = [];
|
|
367
|
+
|
|
368
|
+
const workspaceDepsOf = (pkg: WorkspacePackage): string[] => {
|
|
369
|
+
const deps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
370
|
+
return Object.entries(deps)
|
|
371
|
+
.filter(([name, spec]) => spec.startsWith("workspace:") && byName.has(name))
|
|
372
|
+
.map(([name]) => name);
|
|
373
|
+
};
|
|
297
374
|
|
|
298
|
-
|
|
299
|
-
|
|
375
|
+
while (remaining.size > 0) {
|
|
376
|
+
const layer: WorkspacePackage[] = [];
|
|
377
|
+
for (const name of remaining) {
|
|
378
|
+
const pkg = byName.get(name)!;
|
|
379
|
+
const unmetDeps = workspaceDepsOf(pkg).filter((d) => !done.has(d));
|
|
380
|
+
if (unmetDeps.length === 0) layer.push(pkg);
|
|
381
|
+
}
|
|
382
|
+
if (layer.length === 0) {
|
|
383
|
+
const cycle = [...remaining].join(", ");
|
|
384
|
+
throw new Error(`Workspace dependency cycle detected: ${cycle}`);
|
|
385
|
+
}
|
|
386
|
+
ordered.push(layer);
|
|
387
|
+
for (const pkg of layer) {
|
|
388
|
+
done.add(pkg.name);
|
|
389
|
+
remaining.delete(pkg.name);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const declarationErrors: string[] = [];
|
|
394
|
+
for (const layer of ordered) {
|
|
395
|
+
const tasks = layer.flatMap((pkg) =>
|
|
396
|
+
CONTEXT_CLIENTS.map((client) => () =>
|
|
397
|
+
buildContextClient(pkg, rootDir, client, cache, noCache),
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
const results = await pAll(tasks);
|
|
401
|
+
for (const r of results) declarationErrors.push(...r.declarationErrors);
|
|
402
|
+
}
|
|
300
403
|
|
|
301
404
|
if (declarationErrors.length > 0) {
|
|
302
405
|
console.warn("\n\x1b[33mType declaration errors:\x1b[0m");
|
|
@@ -307,187 +410,267 @@ export async function buildContextPackages(
|
|
|
307
410
|
return { declarationErrors };
|
|
308
411
|
}
|
|
309
412
|
|
|
413
|
+
|
|
310
414
|
// ---------------------------------------------------------------------------
|
|
311
|
-
//
|
|
415
|
+
// Browser app build — one Bun.build with multiple entrypoints.
|
|
416
|
+
//
|
|
417
|
+
// Replaces the previous architecture of 25+ separate Bun.build calls (per
|
|
418
|
+
// shell peer + per context package × client + per token chunk group + initial
|
|
419
|
+
// bundle). The big win is `splitting: true` across ALL entries:
|
|
420
|
+
//
|
|
421
|
+
// - workspace context, framework, common deps land in ONE shared chunk
|
|
422
|
+
// referenced by both `initial.*.js` and `<token>.*.js`. No more multi-
|
|
423
|
+
// instance bugs (e.g. createWorkspace().build() running twice, each
|
|
424
|
+
// producing its own aggregate refs).
|
|
425
|
+
// - No importmap. Nothing is external — Bun bundles everything inline,
|
|
426
|
+
// dedups via shared chunks. The HTML loads `initial.<hash>.js` and that
|
|
427
|
+
// pulls shared chunks as needed.
|
|
428
|
+
// - Token chunks are first-class entries (`<token-name>.<hash>.js`) that
|
|
429
|
+
// side-effect-import all their member modules. Server signs the URL for
|
|
430
|
+
// the entry; shared chunks ride along unsigned (filenames are content-
|
|
431
|
+
// addressed so they're not enumerable without the manifest).
|
|
432
|
+
//
|
|
433
|
+
// Output layout under <outDir> = `<arcDir>/browser/`:
|
|
434
|
+
// initial.<hash>.js ← public modules + bootstrap entry
|
|
435
|
+
// <tokenName>.<hash>.js × N ← one per token group
|
|
436
|
+
// chunk-<hash>.js × N ← auto-shared (workspace ctx, framework, etc.)
|
|
312
437
|
// ---------------------------------------------------------------------------
|
|
313
438
|
|
|
314
|
-
interface
|
|
315
|
-
|
|
316
|
-
|
|
439
|
+
export interface BrowserGroupEntry {
|
|
440
|
+
/** Filename relative to outDir (`<token>.<hash>.js`). */
|
|
441
|
+
readonly file: string;
|
|
442
|
+
readonly hash: string;
|
|
443
|
+
/** Module names registered by this group (for manifest filterability). */
|
|
444
|
+
readonly modules: readonly string[];
|
|
317
445
|
}
|
|
318
446
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
*/
|
|
330
|
-
export async function buildModulesByChunks(
|
|
447
|
+
export interface BrowserAppResult {
|
|
448
|
+
readonly initial: { file: string; hash: string };
|
|
449
|
+
/** Keyed by token.name. `initial` is NOT here — it's separate. */
|
|
450
|
+
readonly groups: Record<string, BrowserGroupEntry>;
|
|
451
|
+
/** Auto-shared chunks emitted by Bun.build splitting. Public, unsigned. */
|
|
452
|
+
readonly sharedChunks: readonly string[];
|
|
453
|
+
readonly cached: boolean;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function buildBrowserApp(
|
|
331
457
|
rootDir: string,
|
|
332
458
|
outDir: string,
|
|
333
459
|
plan: ChunkPlan,
|
|
334
460
|
cache: BuildCache,
|
|
335
461
|
noCache: boolean,
|
|
336
|
-
|
|
462
|
+
i18nCollector: Map<string, Set<string>>,
|
|
463
|
+
): Promise<BrowserAppResult> {
|
|
337
464
|
mkdirSync(outDir, { recursive: true });
|
|
338
465
|
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const members = plan.groups.get(chunk) ?? [];
|
|
345
|
-
if (members.length === 0) continue;
|
|
346
|
-
|
|
347
|
-
const chunkOutDir = join(outDir, chunk);
|
|
348
|
-
mkdirSync(chunkOutDir, { recursive: true });
|
|
349
|
-
|
|
350
|
-
const result = await buildChunkGroup(
|
|
351
|
-
rootDir,
|
|
352
|
-
chunkOutDir,
|
|
353
|
-
chunk,
|
|
354
|
-
members,
|
|
355
|
-
cache,
|
|
356
|
-
noCache,
|
|
357
|
-
i18nCollector,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
aggregateModules.push(...result.modules);
|
|
361
|
-
if (!result.cached) allCached = false;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Single workspace-wide i18n write — keeps msgid bookkeeping atomic.
|
|
365
|
-
await finalizeTranslations(rootDir, join(outDir, ".."), i18nCollector);
|
|
366
|
-
|
|
367
|
-
return { modules: aggregateModules, cached: allCached };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
async function buildChunkGroup(
|
|
371
|
-
rootDir: string,
|
|
372
|
-
chunkOutDir: string,
|
|
373
|
-
chunk: string,
|
|
374
|
-
members: readonly PackageChunk[],
|
|
375
|
-
cache: BuildCache,
|
|
376
|
-
noCache: boolean,
|
|
377
|
-
i18nCollector: Map<string, Set<string>>,
|
|
378
|
-
): Promise<ModulesBundleResult> {
|
|
379
|
-
const unitId = `modules-chunk:${chunk}`;
|
|
466
|
+
const publicMembers = plan.groups.get("public") ?? [];
|
|
467
|
+
const protectedGroups = plan.chunks
|
|
468
|
+
.filter((c) => c !== "public")
|
|
469
|
+
.map((c) => ({ name: c, members: plan.groups.get(c) ?? [] }))
|
|
470
|
+
.filter((g) => g.members.length > 0);
|
|
380
471
|
|
|
381
|
-
const
|
|
382
|
-
name: m.pkg.name,
|
|
383
|
-
safeName: m.safeName,
|
|
384
|
-
moduleName: m.moduleName,
|
|
385
|
-
srcHash: pkgSourceHash(m.pkg),
|
|
386
|
-
}));
|
|
472
|
+
const unitId = "browser-app";
|
|
387
473
|
|
|
474
|
+
// Cache key spans every package's source plus the build config that
|
|
475
|
+
// matters. If anything changes, full rebuild.
|
|
476
|
+
const allMembers: { name: string; group: string; srcHash: string }[] = [];
|
|
477
|
+
for (const m of publicMembers) {
|
|
478
|
+
allMembers.push({ name: m.pkg.name, group: "public", srcHash: pkgSourceHash(m.pkg) });
|
|
479
|
+
}
|
|
480
|
+
for (const g of protectedGroups) {
|
|
481
|
+
for (const m of g.members) {
|
|
482
|
+
allMembers.push({ name: m.pkg.name, group: g.name, srcHash: pkgSourceHash(m.pkg) });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
388
485
|
const inputHash = sha256OfJson({
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
486
|
+
members: allMembers,
|
|
487
|
+
groups: [
|
|
488
|
+
"initial",
|
|
489
|
+
...protectedGroups.map((g) => g.name).sort(),
|
|
490
|
+
],
|
|
392
491
|
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
393
492
|
});
|
|
394
493
|
|
|
395
494
|
if (!noCache && isCacheHit(cache, unitId, inputHash)) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
495
|
+
const cached = cache.units[unitId]?.outputHashes;
|
|
496
|
+
if (cached?._manifest) {
|
|
497
|
+
try {
|
|
498
|
+
const m = JSON.parse(cached._manifest) as BrowserAppResult;
|
|
499
|
+
const allFiles = [
|
|
500
|
+
m.initial.file,
|
|
501
|
+
...Object.values(m.groups).map((g) => g.file),
|
|
502
|
+
...m.sharedChunks,
|
|
503
|
+
];
|
|
504
|
+
if (allFiles.every((f) => existsSync(join(outDir, f)))) {
|
|
505
|
+
console.log(` ✓ cached: ${unitId}`);
|
|
506
|
+
return { ...m, cached: true };
|
|
507
|
+
}
|
|
508
|
+
} catch {
|
|
509
|
+
// fall through to rebuild
|
|
405
510
|
}
|
|
406
|
-
modules.push({
|
|
407
|
-
file,
|
|
408
|
-
name: h.moduleName,
|
|
409
|
-
chunk,
|
|
410
|
-
hash: existing[h.safeName] ?? sha256Hex(readFileSync(filePath)),
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
if (!missing) {
|
|
414
|
-
console.log(` ✓ cached: ${unitId} (${modules.length} module(s))`);
|
|
415
|
-
return { modules, cached: true };
|
|
416
511
|
}
|
|
417
|
-
console.log(` rebuilding ${unitId}: output file missing`);
|
|
418
512
|
}
|
|
419
513
|
|
|
420
|
-
console.log(
|
|
514
|
+
console.log(
|
|
515
|
+
` building: ${unitId} (initial: ${publicMembers.length} modules, groups: ${protectedGroups
|
|
516
|
+
.map((g) => `${g.name}=${g.members.length}`)
|
|
517
|
+
.join(",") || "none"})`,
|
|
518
|
+
);
|
|
421
519
|
|
|
422
|
-
|
|
520
|
+
// Wipe outDir so stale-hash files don't linger.
|
|
521
|
+
if (existsSync(outDir)) {
|
|
522
|
+
for (const f of readdirSync(outDir)) {
|
|
523
|
+
if (f.endsWith(".js")) rmSync(join(outDir, f), { force: true });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const tmpDir = join(outDir, "_entries");
|
|
423
528
|
mkdirSync(tmpDir, { recursive: true });
|
|
424
529
|
|
|
425
|
-
const
|
|
426
|
-
|
|
530
|
+
const importLines = (pkgs: { pkg: { name: string } }[]): string =>
|
|
531
|
+
pkgs.map((m) => `import "${m.pkg.name}";`).join("\n");
|
|
532
|
+
|
|
533
|
+
const initialEntry = join(tmpDir, "initial.ts");
|
|
534
|
+
writeFileSync(
|
|
535
|
+
initialEntry,
|
|
536
|
+
`${importLines(publicMembers)}\nexport { startApp } from "@arcote.tech/platform";\n`,
|
|
537
|
+
);
|
|
427
538
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
539
|
+
const entryPaths: string[] = [initialEntry];
|
|
540
|
+
const groupModuleMap = new Map<string, string[]>();
|
|
541
|
+
for (const g of protectedGroups) {
|
|
542
|
+
const entry = join(tmpDir, `${g.name}.ts`);
|
|
543
|
+
writeFileSync(entry, `${importLines(g.members)}\n`);
|
|
544
|
+
entryPaths.push(entry);
|
|
545
|
+
groupModuleMap.set(g.name, g.members.map((m) => m.moduleName));
|
|
433
546
|
}
|
|
434
547
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
548
|
+
// ---------------------------------------------------------------------
|
|
549
|
+
// Temporarily flip `"sideEffects"` on every workspace package's
|
|
550
|
+
// package.json from `false` to `true` for the duration of the build.
|
|
551
|
+
//
|
|
552
|
+
// Why: Arc modules auto-register via top-level `module(...).build()` —
|
|
553
|
+
// a real side effect. But packages ship with `"sideEffects": false` (a
|
|
554
|
+
// good ESM-library default), which makes Bun.build:
|
|
555
|
+
// (1) tree-shake side-effect-only imports → module().build() never runs,
|
|
556
|
+
// (2) freely duplicate the module across entry chunks → context
|
|
557
|
+
// singletons (`createContext`) get cloned, breaking provider lookup.
|
|
558
|
+
//
|
|
559
|
+
// Bun has no plugin hook to override package.json `sideEffects` and
|
|
560
|
+
// `build.resolve()` is not implemented (Bun#2771), so the cleanest
|
|
561
|
+
// reliable lever is a tiny on-disk patch with guaranteed restore.
|
|
562
|
+
// ---------------------------------------------------------------------
|
|
563
|
+
const allMemberPkgs = new Map<string, WorkspacePackage>();
|
|
564
|
+
for (const m of publicMembers) allMemberPkgs.set(m.pkg.name, m.pkg);
|
|
565
|
+
for (const g of protectedGroups)
|
|
566
|
+
for (const m of g.members) allMemberPkgs.set(m.pkg.name, m.pkg);
|
|
567
|
+
|
|
568
|
+
const patchedPkgJsons: { path: string; original: string }[] = [];
|
|
569
|
+
for (const pkg of allMemberPkgs.values()) {
|
|
570
|
+
const pkgJsonPath = join(pkg.path, "package.json");
|
|
571
|
+
if (!existsSync(pkgJsonPath)) continue;
|
|
572
|
+
const original = readFileSync(pkgJsonPath, "utf-8");
|
|
573
|
+
const parsed = JSON.parse(original);
|
|
574
|
+
if (parsed.sideEffects === true) continue; // already correct
|
|
575
|
+
parsed.sideEffects = true;
|
|
576
|
+
writeFileSync(pkgJsonPath, JSON.stringify(parsed, null, 2) + "\n");
|
|
577
|
+
patchedPkgJsons.push({ path: pkgJsonPath, original });
|
|
578
|
+
}
|
|
443
579
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
580
|
+
let result;
|
|
581
|
+
try {
|
|
582
|
+
result = await Bun.build({
|
|
583
|
+
entrypoints: entryPaths,
|
|
584
|
+
outdir: outDir,
|
|
585
|
+
// splitting:true is the whole point: shared deps (workspace context,
|
|
586
|
+
// framework, lucide, etc.) land in chunk-<hash>.js, referenced by both
|
|
587
|
+
// initial and token-group entries. One instance, no provider duplication.
|
|
588
|
+
splitting: true,
|
|
589
|
+
format: "esm",
|
|
590
|
+
target: "browser",
|
|
591
|
+
// No externals. Framework peers (react, @arcote.tech/*) get bundled and
|
|
592
|
+
// deduped into shared chunks. No importmap needed in the HTML.
|
|
593
|
+
external: [],
|
|
594
|
+
plugins: [
|
|
595
|
+
singleReactPlugin(rootDir),
|
|
596
|
+
jsxDevShimPlugin(),
|
|
597
|
+
i18nExtractPlugin(i18nCollector, rootDir),
|
|
598
|
+
],
|
|
599
|
+
naming: "[name].[ext]",
|
|
600
|
+
define: {
|
|
601
|
+
ONLY_SERVER: "false",
|
|
602
|
+
ONLY_BROWSER: "true",
|
|
603
|
+
ONLY_CLIENT: "true",
|
|
604
|
+
// CRITICAL: React's index.js does `if (process.env.NODE_ENV === 'production') require('./cjs/react.production.js') else require('./cjs/react.development.js')`.
|
|
605
|
+
// Without inlining NODE_ENV at build time Bun bundles BOTH branches → TWO ReactSharedInternals → multi-instance "Invalid hook call" crash.
|
|
606
|
+
"process.env.NODE_ENV": '"production"',
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
} finally {
|
|
610
|
+
// Always restore — a crash here MUST NOT leave the user with mutated
|
|
611
|
+
// workspace package.json files.
|
|
612
|
+
for (const p of patchedPkgJsons) writeFileSync(p.path, p.original);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
463
616
|
|
|
464
617
|
if (!result.success) {
|
|
465
|
-
console.error(`Chunk "${chunk}" build failed:`);
|
|
466
618
|
for (const log of result.logs) console.error(log);
|
|
467
|
-
throw new Error(
|
|
619
|
+
throw new Error("Browser app build failed");
|
|
468
620
|
}
|
|
469
621
|
|
|
470
|
-
|
|
622
|
+
// Bun's `[name]` in naming preserves entry filename without hash. We add
|
|
623
|
+
// content hashes ourselves so identical bytes produce identical URLs across
|
|
624
|
+
// deploys (good for browser caching). Shared chunks already have a hash in
|
|
625
|
+
// their name (Bun auto-emits `chunk-<hash>.js`); we leave those alone.
|
|
626
|
+
let initialFile = "";
|
|
627
|
+
let initialHash = "";
|
|
628
|
+
const groups: Record<string, BrowserGroupEntry> = {};
|
|
629
|
+
const sharedChunks: string[] = [];
|
|
630
|
+
|
|
631
|
+
for (const out of result.outputs) {
|
|
632
|
+
const name = basename(out.path);
|
|
633
|
+
if (out.kind === "entry-point") {
|
|
634
|
+
const bytes = readFileSync(out.path);
|
|
635
|
+
const hash = sha256Hex(bytes).slice(0, 16);
|
|
636
|
+
const stem = name.replace(/\.js$/, "");
|
|
637
|
+
const finalName = `${stem}.${hash}.js`;
|
|
638
|
+
const finalPath = join(outDir, finalName);
|
|
639
|
+
rmSync(finalPath, { force: true });
|
|
640
|
+
writeFileSync(finalPath, bytes);
|
|
641
|
+
rmSync(out.path, { force: true });
|
|
642
|
+
|
|
643
|
+
if (stem === "initial") {
|
|
644
|
+
initialFile = finalName;
|
|
645
|
+
initialHash = hash;
|
|
646
|
+
} else {
|
|
647
|
+
groups[stem] = {
|
|
648
|
+
file: finalName,
|
|
649
|
+
hash,
|
|
650
|
+
modules: groupModuleMap.get(stem) ?? [],
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
} else if (out.kind === "chunk") {
|
|
654
|
+
sharedChunks.push(name);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
471
657
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
};
|
|
487
|
-
});
|
|
658
|
+
if (!initialFile) {
|
|
659
|
+
throw new Error("Browser app build: initial entry not found in outputs");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const manifest: BrowserAppResult = {
|
|
663
|
+
initial: { file: initialFile, hash: initialHash },
|
|
664
|
+
groups,
|
|
665
|
+
sharedChunks,
|
|
666
|
+
cached: false,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
updateCache(cache, unitId, inputHash, {
|
|
670
|
+
outputHashes: { _manifest: JSON.stringify(manifest) },
|
|
671
|
+
});
|
|
488
672
|
|
|
489
|
-
|
|
490
|
-
return { modules, cached: false };
|
|
673
|
+
return manifest;
|
|
491
674
|
}
|
|
492
675
|
|
|
493
676
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
}
|