@arcote.tech/arc-cli 0.7.0 → 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 +1382 -1343
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +10 -24
- package/src/builder/module-builder.ts +293 -153
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.7.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.7.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.
|
|
20
|
-
"@arcote.tech/platform": "^0.7.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.1",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.1",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.1",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.1",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.1",
|
|
20
|
+
"@arcote.tech/platform": "^0.7.1",
|
|
21
21
|
"@clack/prompts": "^0.9.0",
|
|
22
22
|
"commander": "^11.1.0",
|
|
23
23
|
"chokidar": "^3.5.3",
|
|
@@ -3,27 +3,12 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
-
realpathSync,
|
|
7
6
|
unlinkSync,
|
|
8
7
|
writeFileSync,
|
|
9
8
|
} from "fs";
|
|
10
|
-
import {
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
9
|
+
import { join } from "path";
|
|
12
10
|
import { isContextPackage, type WorkspacePackage } from "./module-builder";
|
|
13
11
|
|
|
14
|
-
// Locate platform's server entry by walking up from the CLI bundle location.
|
|
15
|
-
// `import.meta.url` at runtime points to the bundled `dist/index.js` (the CLI
|
|
16
|
-
// is shipped as a single Bun.build artifact), so the path is:
|
|
17
|
-
// <arcRoot>/packages/cli/dist/index.js
|
|
18
|
-
// Four `dirname` applications reach <arcRoot>. This works regardless of how
|
|
19
|
-
// the user invoked the CLI — symlink, npm install, or direct path — as long
|
|
20
|
-
// as the CLI binary lives at packages/cli/dist/ inside the arc workspace.
|
|
21
|
-
function locatePlatformServerEntry(): string {
|
|
22
|
-
const here = fileURLToPath(import.meta.url);
|
|
23
|
-
const arcRoot = realpathSync(dirname(dirname(dirname(dirname(here)))));
|
|
24
|
-
return join(arcRoot, "packages", "platform", "src", "index.server.ts");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
12
|
// ---------------------------------------------------------------------------
|
|
28
13
|
// access-extractor — discovers per-module access rules (`protectedBy(...)`)
|
|
29
14
|
// from already-built server bundles, in an ISOLATED subprocess.
|
|
@@ -64,8 +49,7 @@ export async function extractAccessMap(
|
|
|
64
49
|
// Each entry: a context-package server bundle path that the worker will
|
|
65
50
|
// dynamically import. Bun resolves the bundle's internal external imports
|
|
66
51
|
// (`@arcote.tech/platform` etc.) via node_modules walking from the bundle
|
|
67
|
-
// location — workspace symlinks point back to source
|
|
68
|
-
// pick the server entry.
|
|
52
|
+
// location — workspace symlinks point back to source.
|
|
69
53
|
const serverBundles = packages
|
|
70
54
|
.filter((p) => isContextPackage(p.packageJson))
|
|
71
55
|
.map((p) => ({
|
|
@@ -75,8 +59,9 @@ export async function extractAccessMap(
|
|
|
75
59
|
.filter((b) => existsSync(b.path));
|
|
76
60
|
|
|
77
61
|
// Worker must live INSIDE the workspace tree so Bun's module resolver can
|
|
78
|
-
// walk up to <rootDir>/node_modules and find @arcote.tech/platform
|
|
79
|
-
// A tmpfile in /tmp/ would fail
|
|
62
|
+
// walk up to <rootDir>/node_modules and find @arcote.tech/platform via the
|
|
63
|
+
// bare specifier `@arcote.tech/platform`. A tmpfile in /tmp/ would fail
|
|
64
|
+
// bare-specifier resolution.
|
|
80
65
|
const workerDir = join(rootDir, ".arc", ".tmp");
|
|
81
66
|
mkdirSync(workerDir, { recursive: true });
|
|
82
67
|
const workerPath = join(workerDir, `access-extractor-${Date.now()}.mjs`);
|
|
@@ -91,7 +76,6 @@ export async function extractAccessMap(
|
|
|
91
76
|
...process.env,
|
|
92
77
|
ARC_ACCESS_BUNDLES: JSON.stringify(serverBundles),
|
|
93
78
|
ARC_ACCESS_OUT: outPath,
|
|
94
|
-
ARC_PLATFORM_ENTRY: locatePlatformServerEntry(),
|
|
95
79
|
},
|
|
96
80
|
stdout: "pipe",
|
|
97
81
|
stderr: "inherit",
|
|
@@ -131,9 +115,11 @@ if (!out) {
|
|
|
131
115
|
process.exit(2);
|
|
132
116
|
}
|
|
133
117
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
118
|
+
// Bare-specifier import — Bun walks up from this worker's location
|
|
119
|
+
// (<rootDir>/.arc/.tmp/) to <rootDir>/node_modules and finds the package.
|
|
120
|
+
// Single entry (./src/index.ts) — React imports on top level are benign
|
|
121
|
+
// (createContext, function defs); no DOM access until actual render.
|
|
122
|
+
const platform = await import("@arcote.tech/platform");
|
|
137
123
|
|
|
138
124
|
for (const { name, path } of bundles) {
|
|
139
125
|
try {
|
|
@@ -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,26 @@ async function buildContextClient(
|
|
|
227
277
|
|
|
228
278
|
console.log(` building: ${pkg.name} (${client.name})`);
|
|
229
279
|
|
|
230
|
-
// Externals: framework peers + npm dependencies
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
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).
|
|
234
297
|
const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
|
|
235
298
|
const allDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
236
|
-
const
|
|
237
|
-
.filter(([, spec]) => !spec.startsWith("workspace:"))
|
|
238
|
-
.map(([name]) => name);
|
|
239
|
-
const externals = [...peerDeps, ...npmDeps];
|
|
299
|
+
const externals = [...peerDeps, ...Object.keys(allDeps)];
|
|
240
300
|
|
|
241
301
|
const result = await Bun.build({
|
|
242
302
|
entrypoints: [pkg.entrypoint],
|
|
@@ -307,187 +367,267 @@ export async function buildContextPackages(
|
|
|
307
367
|
return { declarationErrors };
|
|
308
368
|
}
|
|
309
369
|
|
|
370
|
+
|
|
310
371
|
// ---------------------------------------------------------------------------
|
|
311
|
-
//
|
|
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.)
|
|
312
394
|
// ---------------------------------------------------------------------------
|
|
313
395
|
|
|
314
|
-
interface
|
|
315
|
-
|
|
316
|
-
|
|
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[];
|
|
317
402
|
}
|
|
318
403
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
*/
|
|
330
|
-
export async function buildModulesByChunks(
|
|
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(
|
|
331
414
|
rootDir: string,
|
|
332
415
|
outDir: string,
|
|
333
416
|
plan: ChunkPlan,
|
|
334
417
|
cache: BuildCache,
|
|
335
418
|
noCache: boolean,
|
|
336
|
-
|
|
419
|
+
i18nCollector: Map<string, Set<string>>,
|
|
420
|
+
): Promise<BrowserAppResult> {
|
|
337
421
|
mkdirSync(outDir, { recursive: true });
|
|
338
422
|
|
|
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);
|
|
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);
|
|
366
428
|
|
|
367
|
-
|
|
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}`;
|
|
380
|
-
|
|
381
|
-
const pkgHashes = members.map((m) => ({
|
|
382
|
-
name: m.pkg.name,
|
|
383
|
-
safeName: m.safeName,
|
|
384
|
-
moduleName: m.moduleName,
|
|
385
|
-
srcHash: pkgSourceHash(m.pkg),
|
|
386
|
-
}));
|
|
429
|
+
const unitId = "browser-app";
|
|
387
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
|
+
}
|
|
388
442
|
const inputHash = sha256OfJson({
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
members: allMembers,
|
|
444
|
+
groups: [
|
|
445
|
+
"initial",
|
|
446
|
+
...protectedGroups.map((g) => g.name).sort(),
|
|
447
|
+
],
|
|
392
448
|
define: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" },
|
|
393
449
|
});
|
|
394
450
|
|
|
395
451
|
if (!noCache && isCacheHit(cache, unitId, inputHash)) {
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
405
467
|
}
|
|
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
468
|
}
|
|
417
|
-
console.log(` rebuilding ${unitId}: output file missing`);
|
|
418
469
|
}
|
|
419
470
|
|
|
420
|
-
console.log(
|
|
471
|
+
console.log(
|
|
472
|
+
` building: ${unitId} (initial: ${publicMembers.length} modules, groups: ${protectedGroups
|
|
473
|
+
.map((g) => `${g.name}=${g.members.length}`)
|
|
474
|
+
.join(",") || "none"})`,
|
|
475
|
+
);
|
|
476
|
+
|
|
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
|
+
}
|
|
421
483
|
|
|
422
|
-
const tmpDir = join(
|
|
484
|
+
const tmpDir = join(outDir, "_entries");
|
|
423
485
|
mkdirSync(tmpDir, { recursive: true });
|
|
424
486
|
|
|
425
|
-
const
|
|
426
|
-
|
|
487
|
+
const importLines = (pkgs: { pkg: { name: string } }[]): string =>
|
|
488
|
+
pkgs.map((m) => `import "${m.pkg.name}";`).join("\n");
|
|
427
489
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
490
|
+
const initialEntry = join(tmpDir, "initial.ts");
|
|
491
|
+
writeFileSync(
|
|
492
|
+
initialEntry,
|
|
493
|
+
`${importLines(publicMembers)}\nexport { startApp } from "@arcote.tech/platform";\n`,
|
|
494
|
+
);
|
|
495
|
+
|
|
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));
|
|
433
503
|
}
|
|
434
504
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
}
|
|
443
536
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
537
|
+
let result;
|
|
538
|
+
try {
|
|
539
|
+
result = await Bun.build({
|
|
540
|
+
entrypoints: entryPaths,
|
|
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.
|
|
545
|
+
splitting: true,
|
|
546
|
+
format: "esm",
|
|
547
|
+
target: "browser",
|
|
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
|
+
],
|
|
556
|
+
naming: "[name].[ext]",
|
|
557
|
+
define: {
|
|
558
|
+
ONLY_SERVER: "false",
|
|
559
|
+
ONLY_BROWSER: "true",
|
|
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"',
|
|
564
|
+
},
|
|
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
|
+
}
|
|
571
|
+
|
|
572
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
463
573
|
|
|
464
574
|
if (!result.success) {
|
|
465
|
-
console.error(`Chunk "${chunk}" build failed:`);
|
|
466
575
|
for (const log of result.logs) console.error(log);
|
|
467
|
-
throw new Error(
|
|
576
|
+
throw new Error("Browser app build failed");
|
|
468
577
|
}
|
|
469
578
|
|
|
470
|
-
|
|
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,
|
|
606
|
+
hash,
|
|
607
|
+
modules: groupModuleMap.get(stem) ?? [],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
} else if (out.kind === "chunk") {
|
|
611
|
+
sharedChunks.push(name);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
471
614
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
};
|
|
487
|
-
});
|
|
615
|
+
if (!initialFile) {
|
|
616
|
+
throw new Error("Browser app build: initial entry not found in outputs");
|
|
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
|
+
});
|
|
488
629
|
|
|
489
|
-
|
|
490
|
-
return { modules, cached: false };
|
|
630
|
+
return manifest;
|
|
491
631
|
}
|
|
492
632
|
|
|
493
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
|
}
|