@arcote.tech/arc-cli 0.5.6 → 0.5.7

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.
@@ -1,21 +1,37 @@
1
1
  import { findUpSync } from "find-up";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import {
5
- buildPackages,
5
+ buildContextPackages,
6
+ buildModulesBundle,
6
7
  buildStyles,
8
+ buildTranslations,
7
9
  discoverPackages,
8
10
  isContextPackage,
9
- sha256OfFiles,
10
11
  type BuildManifest,
11
12
  type ModuleDescriptor,
12
- type ModuleEntry,
13
13
  type WorkspacePackage,
14
14
  } from "../builder/module-builder";
15
+ import {
16
+ loadBuildCache,
17
+ saveBuildCache,
18
+ isCacheHit,
19
+ updateCache,
20
+ type BuildCache,
21
+ } from "../builder/build-cache";
22
+ import {
23
+ mtimeOf,
24
+ readInstalledVersion,
25
+ sha256Hex,
26
+ sha256OfDir,
27
+ sha256OfFiles,
28
+ sha256OfJson,
29
+ } from "../builder/hash";
30
+ import { pAll } from "../builder/parallel";
15
31
 
16
32
  // Re-export for convenience
17
- export { buildPackages, buildStyles, isContextPackage };
18
- export type { BuildManifest, ModuleDescriptor, ModuleEntry, WorkspacePackage };
33
+ export { buildContextPackages, buildModulesBundle, buildStyles, isContextPackage };
34
+ export type { BuildManifest, ModuleDescriptor, WorkspacePackage };
19
35
 
20
36
  // ---------------------------------------------------------------------------
21
37
  // Logging
@@ -51,6 +67,8 @@ export interface WorkspaceInfo {
51
67
  arcDir: string;
52
68
  modulesDir: string;
53
69
  shellDir: string;
70
+ /** Static assets generated z arc.browserAssets workspace deps. Serwowane pod /assets/*. */
71
+ assetsDir: string;
54
72
  publicDir: string;
55
73
  packages: WorkspacePackage[];
56
74
  manifest?: ManifestInfo;
@@ -107,6 +125,7 @@ export function resolveWorkspace(): WorkspaceInfo {
107
125
  arcDir,
108
126
  modulesDir: join(arcDir, "modules"),
109
127
  shellDir: join(arcDir, "shell"),
128
+ assetsDir: join(arcDir, "assets"),
110
129
  publicDir: join(rootDir, "public"),
111
130
  packages,
112
131
  manifest,
@@ -117,32 +136,39 @@ export function resolveWorkspace(): WorkspaceInfo {
117
136
  // Build pipeline — packages, CSS, theme, shell
118
137
  // ---------------------------------------------------------------------------
119
138
 
120
- export async function buildAll(ws: WorkspaceInfo): Promise<BuildManifest> {
121
- log("Building packages...");
122
- const manifest = await buildPackages(ws.rootDir, ws.modulesDir, ws.packages);
123
- ok(`Built ${manifest.modules.length} module(s)`);
124
-
125
- log("Building styles...");
126
- await buildStyles(ws.rootDir, ws.arcDir);
127
- ok("Styles built");
128
-
129
- // Copy theme if configured
130
- const themePath = ws.rootPkg.arc?.theme;
131
- if (themePath) {
132
- const src = join(ws.rootDir, themePath);
133
- if (existsSync(src)) {
134
- copyFileSync(src, join(ws.arcDir, "theme.css"));
135
- ok("Theme copied");
136
- }
137
- }
138
-
139
- log("Building shell...");
140
- await buildShell(ws.shellDir, ws.packages);
141
- ok("Shell built");
139
+ export interface BuildOptions {
140
+ noCache?: boolean;
141
+ }
142
142
 
143
- // Compute aggregate hashes now that all artifacts are on disk, then rewrite
144
- // manifest.json with full hash data (modules + shellHash + stylesHash).
145
- const finalManifest = finalizeManifest(ws, manifest);
143
+ /**
144
+ * Run the full build pipeline. All units run in parallel; each consults the
145
+ * source-hash cache and skips if its inputs are unchanged. Pass `noCache: true`
146
+ * to force a full rebuild (the cache is still updated).
147
+ */
148
+ export async function buildAll(
149
+ ws: WorkspaceInfo,
150
+ opts: BuildOptions = {},
151
+ ): Promise<BuildManifest> {
152
+ const cache = loadBuildCache(ws.arcDir);
153
+ const noCache = opts.noCache ?? false;
154
+ const themePath = ws.rootPkg.arc?.theme as string | undefined;
155
+
156
+ log(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
157
+
158
+ // Promise.all over independent units. buildModulesBundle is the only one
159
+ // that returns data we need (the manifest). The rest update the cache.
160
+ const [, modulesResult] = await Promise.all([
161
+ buildContextPackages(ws.rootDir, ws.packages, cache, noCache),
162
+ buildModulesBundle(ws.rootDir, ws.modulesDir, ws.packages, cache, noCache),
163
+ buildShell(ws, cache, noCache),
164
+ buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
165
+ copyBrowserAssets(ws, cache, noCache),
166
+ buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
167
+ ]);
168
+
169
+ saveBuildCache(ws.arcDir, cache);
170
+
171
+ const finalManifest = assembleManifest(ws, modulesResult.modules, cache);
146
172
  writeFileSync(
147
173
  join(ws.modulesDir, "manifest.json"),
148
174
  JSON.stringify(finalManifest, null, 2),
@@ -151,38 +177,158 @@ export async function buildAll(ws: WorkspaceInfo): Promise<BuildManifest> {
151
177
  }
152
178
 
153
179
  /**
154
- * Compute shellHash + stylesHash and produce the final manifest.
155
- * Pure function — does not touch disk.
180
+ * Assemble the platform manifest from cached output hashes — no disk reads.
156
181
  */
157
- export function finalizeManifest(
182
+ function assembleManifest(
158
183
  ws: WorkspaceInfo,
159
- manifest: BuildManifest,
184
+ modules: ModuleDescriptor[],
185
+ cache: BuildCache,
160
186
  ): BuildManifest {
161
- const shellFiles = listFilesRec(ws.shellDir);
162
- const stylesFiles = [
163
- join(ws.arcDir, "styles.css"),
164
- join(ws.arcDir, "theme.css"),
165
- ].filter((p) => existsSync(p));
187
+ // Aggregate shellHash from all shell:* unit output hashes.
188
+ const shellEntries: Record<string, string> = {};
189
+ for (const [unitId, entry] of Object.entries(cache.units)) {
190
+ if (unitId.startsWith("shell:") && entry.outputHash) {
191
+ shellEntries[unitId] = entry.outputHash;
192
+ }
193
+ }
194
+ const shellHash = sha256OfJson(shellEntries);
195
+ const stylesHash = cache.units["styles"]?.outputHash ?? "";
166
196
 
167
197
  return {
168
- modules: manifest.modules,
169
- shellHash: sha256OfFiles(shellFiles),
170
- stylesHash: sha256OfFiles(stylesFiles),
171
- buildTime: manifest.buildTime,
198
+ modules,
199
+ shellHash,
200
+ stylesHash,
201
+ buildTime: new Date().toISOString(),
172
202
  };
173
203
  }
174
204
 
175
- function listFilesRec(dir: string): string[] {
176
- if (!existsSync(dir)) return [];
177
- const out: string[] = [];
178
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
179
- const p = join(dir, entry.name);
180
- if (entry.isDirectory()) out.push(...listFilesRec(p));
181
- else if (entry.isFile()) out.push(p);
205
+ // ---------------------------------------------------------------------------
206
+ // Browser assets — @arcote.tech/* deps deklarują w `arc.browserAssets` jakie
207
+ // pliki muszą być dostępne w przeglądarce (np. SQLite WASM worker + .wasm).
208
+ // ---------------------------------------------------------------------------
209
+
210
+ interface BrowserAsset {
211
+ /** Path to source file — relative to package dir or package-resolvable. */
212
+ from: string;
213
+ /** Target filename in /assets/. */
214
+ to: string;
215
+ }
216
+
217
+ interface ResolvedAsset {
218
+ arcPkg: string;
219
+ from: string;
220
+ to: string;
221
+ src: string;
222
+ }
223
+
224
+ function resolveAssetSource(
225
+ from: string,
226
+ pkgDir: string,
227
+ rootDir: string,
228
+ ): string | null {
229
+ if (from.startsWith("./") || from.startsWith("../")) {
230
+ const resolved = join(pkgDir, from);
231
+ return existsSync(resolved) ? resolved : null;
232
+ }
233
+ const candidates = [
234
+ join(rootDir, "node_modules", from),
235
+ join(pkgDir, "node_modules", from),
236
+ ];
237
+ for (const c of candidates) {
238
+ if (existsSync(c)) return c;
239
+ }
240
+ const bunCacheDir = join(rootDir, "node_modules", ".bun");
241
+ if (existsSync(bunCacheDir)) {
242
+ for (const entry of readdirSync(bunCacheDir, { withFileTypes: true })) {
243
+ if (!entry.isDirectory()) continue;
244
+ const candidate = join(
245
+ bunCacheDir,
246
+ entry.name,
247
+ "node_modules",
248
+ from,
249
+ );
250
+ if (existsSync(candidate)) return candidate;
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function readBrowserAssets(pkgDir: string): BrowserAsset[] {
257
+ const pkgJsonPath = join(pkgDir, "package.json");
258
+ if (!existsSync(pkgJsonPath)) return [];
259
+ try {
260
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
261
+ const assets = pkg.arc?.browserAssets;
262
+ if (!Array.isArray(assets)) return [];
263
+ return assets.filter(
264
+ (a): a is BrowserAsset =>
265
+ typeof a?.from === "string" && typeof a?.to === "string",
266
+ );
267
+ } catch {
268
+ return [];
269
+ }
270
+ }
271
+
272
+ function discoverBrowserAssets(ws: WorkspaceInfo): ResolvedAsset[] {
273
+ const arcDir = join(ws.rootDir, "node_modules", "@arcote.tech");
274
+ if (!existsSync(arcDir)) return [];
275
+
276
+ const out: ResolvedAsset[] = [];
277
+ for (const entry of readdirSync(arcDir, { withFileTypes: true })) {
278
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
279
+ const pkgDir = join(arcDir, entry.name);
280
+ const assets = readBrowserAssets(pkgDir);
281
+ for (const asset of assets) {
282
+ const src = resolveAssetSource(asset.from, pkgDir, ws.rootDir);
283
+ if (!src) {
284
+ err(`browserAsset not found: ${asset.from} (from @arcote.tech/${entry.name})`);
285
+ continue;
286
+ }
287
+ out.push({ arcPkg: entry.name, from: asset.from, to: asset.to, src });
288
+ }
182
289
  }
183
290
  return out;
184
291
  }
185
292
 
293
+ /**
294
+ * Copy browser assets as a single cached unit. Hash inputs: every (from, to, src-mtime).
295
+ */
296
+ async function copyBrowserAssets(
297
+ ws: WorkspaceInfo,
298
+ cache: BuildCache,
299
+ noCache: boolean,
300
+ ): Promise<void> {
301
+ mkdirSync(ws.assetsDir, { recursive: true });
302
+ const assets = discoverBrowserAssets(ws);
303
+ if (assets.length === 0) return;
304
+
305
+ const unitId = "browser-assets";
306
+ const inputHash = sha256OfJson(
307
+ assets.map((a) => ({
308
+ arcPkg: a.arcPkg,
309
+ from: a.from,
310
+ to: a.to,
311
+ mtime: mtimeOf(a.src),
312
+ })),
313
+ );
314
+
315
+ const requiredOutputs = assets.map((a) => join(ws.assetsDir, a.to));
316
+ if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
317
+ console.log(` ✓ cached: browser-assets (${assets.length})`);
318
+ return;
319
+ }
320
+
321
+ console.log(` building: browser-assets (${assets.length})`);
322
+ const outputHashes: Record<string, string> = {};
323
+ for (const asset of assets) {
324
+ const dest = join(ws.assetsDir, asset.to);
325
+ mkdirSync(dirname(dest), { recursive: true });
326
+ copyFileSync(asset.src, dest);
327
+ outputHashes[asset.to] = sha256Hex(readFileSync(dest));
328
+ }
329
+ updateCache(cache, unitId, inputHash, { outputHashes });
330
+ }
331
+
186
332
  // ---------------------------------------------------------------------------
187
333
  // Shell builder — framework packages for import map
188
334
  // ---------------------------------------------------------------------------
@@ -190,18 +336,15 @@ function listFilesRec(dir: string): string[] {
190
336
  /** Collect all @arcote.tech/* peerDependencies from workspace packages. */
191
337
  export function collectArcPeerDeps(packages: WorkspacePackage[]): [string, string][] {
192
338
  const seen = new Set<string>();
193
- // Always include core framework packages
194
339
  for (const pkg of ["@arcote.tech/arc", "@arcote.tech/arc-ds", "@arcote.tech/arc-react", "@arcote.tech/platform"]) {
195
340
  seen.add(pkg);
196
341
  }
197
- // Scan all workspace packages for @arcote.tech/* peerDeps
198
342
  for (const wp of packages) {
199
343
  const peerDeps = wp.packageJson.peerDependencies ?? {};
200
344
  for (const dep of Object.keys(peerDeps)) {
201
345
  if (dep.startsWith("@arcote.tech/")) seen.add(dep);
202
346
  }
203
347
  }
204
- // Convert to [shortName, fullName] entries
205
348
  return [...seen].map((pkg) => {
206
349
  const short = pkg === "@arcote.tech/platform"
207
350
  ? "platform"
@@ -210,16 +353,10 @@ export function collectArcPeerDeps(packages: WorkspacePackage[]): [string, strin
210
353
  });
211
354
  }
212
355
 
213
- async function buildShell(outDir: string, packages?: WorkspacePackage[]): Promise<void> {
214
- mkdirSync(outDir, { recursive: true });
215
- const tmpDir = join(outDir, "_tmp");
216
- mkdirSync(tmpDir, { recursive: true });
217
-
218
- // Step 1: Build React layer (bundles React CJS → ESM)
219
- const reactEntries: [string, string][] = [
220
- [
221
- "react",
222
- `import React from "react";
356
+ const REACT_ENTRIES: [string, string][] = [
357
+ [
358
+ "react",
359
+ `import React from "react";
223
360
  export default React;
224
361
  export const {
225
362
  Children, Component, Fragment, Profiler, PureComponent, StrictMode, Suspense,
@@ -229,96 +366,172 @@ export const {
229
366
  useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore,
230
367
  useTransition, version, useActionState, useOptimistic,
231
368
  } = React;`,
232
- ],
233
- ["jsx-runtime", `export { jsx, jsxs, Fragment } from "react/jsx-runtime";`],
234
- [
235
- "jsx-dev-runtime",
236
- `export { jsxDEV, Fragment } from "react/jsx-dev-runtime";`,
237
- ],
238
- [
239
- "react-dom",
240
- `import ReactDOM from "react-dom";
369
+ ],
370
+ ["jsx-runtime", `export { jsx, jsxs, Fragment } from "react/jsx-runtime";`],
371
+ [
372
+ "jsx-dev-runtime",
373
+ `export { jsxDEV, Fragment } from "react/jsx-dev-runtime";`,
374
+ ],
375
+ [
376
+ "react-dom",
377
+ `import ReactDOM from "react-dom";
241
378
  export default ReactDOM;
242
379
  export const { createPortal, flushSync } = ReactDOM;`,
243
- ],
244
- [
245
- "react-dom-client",
246
- `export { createRoot, hydrateRoot } from "react-dom/client";`,
247
- ],
248
- ];
380
+ ],
381
+ [
382
+ "react-dom-client",
383
+ `export { createRoot, hydrateRoot } from "react-dom/client";`,
384
+ ],
385
+ ];
386
+
387
+ const REACT_OUTPUT_FILES = REACT_ENTRIES.map(([n]) => `${n}.js`);
388
+
389
+ const SHELL_BASE_EXTERNAL = [
390
+ "react",
391
+ "react-dom",
392
+ "react/jsx-runtime",
393
+ "react/jsx-dev-runtime",
394
+ "react-dom/client",
395
+ ];
396
+
397
+ const sourceFilter = (rel: string): boolean => {
398
+ if (rel.startsWith("dist/") || rel.startsWith("dist")) return false;
399
+ if (rel.includes("/node_modules/") || rel.startsWith("node_modules")) return false;
400
+ if (rel.startsWith(".arc/") || rel.startsWith(".arc")) return false;
401
+ return true;
402
+ };
403
+
404
+ function arcPkgSrcHash(rootDir: string, pkg: string): string {
405
+ // Prefer src/ tree (workspace links), fallback to whole package dir.
406
+ const srcDir = join(rootDir, "node_modules", pkg, "src");
407
+ if (existsSync(srcDir)) return sha256OfDir(srcDir, sourceFilter);
408
+ return sha256OfDir(join(rootDir, "node_modules", pkg), sourceFilter);
409
+ }
410
+
411
+ async function buildShellReact(
412
+ shellDir: string,
413
+ tmpDir: string,
414
+ rootDir: string,
415
+ cache: BuildCache,
416
+ noCache: boolean,
417
+ ): Promise<void> {
418
+ const unitId = "shell:react";
419
+ const inputHash = sha256OfJson({
420
+ react: readInstalledVersion(rootDir, "react"),
421
+ "react-dom": readInstalledVersion(rootDir, "react-dom"),
422
+ entries: REACT_ENTRIES.map(([k, v]) => [k, v]),
423
+ });
424
+
425
+ const requiredOutputs = REACT_OUTPUT_FILES.map((f) => join(shellDir, f));
426
+ if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
427
+ console.log(` ✓ cached: shell:react`);
428
+ return;
429
+ }
430
+
431
+ console.log(` building: shell:react`);
249
432
 
250
433
  const reactEps: string[] = [];
251
- for (const [name, code] of reactEntries) {
434
+ for (const [name, code] of REACT_ENTRIES) {
252
435
  const f = join(tmpDir, `${name}.ts`);
253
- Bun.write(f, code);
436
+ await Bun.write(f, code);
254
437
  reactEps.push(f);
255
438
  }
256
439
 
257
- const r1 = await Bun.build({
440
+ const r = await Bun.build({
258
441
  entrypoints: reactEps,
259
- outdir: outDir,
442
+ outdir: shellDir,
260
443
  splitting: true,
261
444
  format: "esm",
262
445
  target: "browser",
263
446
  naming: "[name].[ext]",
264
447
  });
265
- if (!r1.success) {
266
- for (const l of r1.logs) console.error(l);
448
+ if (!r.success) {
449
+ for (const l of r.logs) console.error(l);
267
450
  throw new Error("Shell React build failed");
268
451
  }
269
452
 
270
- // Step 2: Build Arc layer (react is EXTERNAL — resolved via import map)
271
- // Dynamically collect only @arcote.tech/* packages actually used by workspace
272
- const arcEntries = packages
273
- ? collectArcPeerDeps(packages)
274
- : [
275
- // Fallback: core packages only (no workspace packages available)
276
- ["arc", "@arcote.tech/arc"],
277
- ["arc-ds", "@arcote.tech/arc-ds"],
278
- ["arc-react", "@arcote.tech/arc-react"],
279
- ["platform", "@arcote.tech/platform"],
280
- ] as [string, string][];
281
-
282
- const baseExternal = [
283
- "react",
284
- "react-dom",
285
- "react/jsx-runtime",
286
- "react/jsx-dev-runtime",
287
- "react-dom/client",
288
- ];
289
- const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
453
+ const outputHash = sha256OfFiles(requiredOutputs);
454
+ updateCache(cache, unitId, inputHash, { outputHash });
455
+ }
290
456
 
291
- // Build each arc entry separately so it can import sibling arc packages
292
- // as externals (resolved via import map) without circular self-reference.
293
- for (const [name, pkg] of arcEntries) {
294
- const f = join(tmpDir, `${name}.ts`);
295
- Bun.write(f, `export * from "${pkg}";\n`);
296
-
297
- const r2 = await Bun.build({
298
- entrypoints: [f],
299
- outdir: outDir,
300
- format: "esm",
301
- target: "browser",
302
- naming: "[name].[ext]",
303
- external: [
304
- ...baseExternal,
305
- // Other arc packages are external (not self)
306
- ...allArcPkgs.filter((p) => p !== pkg),
307
- ],
308
- define: {
309
- ONLY_SERVER: "false",
310
- ONLY_BROWSER: "true",
311
- ONLY_CLIENT: "true",
312
- },
313
- });
314
- if (!r2.success) {
315
- for (const l of r2.logs) console.error(l);
316
- throw new Error(`Shell build failed for ${pkg}`);
317
- }
457
+ async function buildShellArcEntry(
458
+ shortName: string,
459
+ pkg: string,
460
+ allArcPkgs: string[],
461
+ shellDir: string,
462
+ tmpDir: string,
463
+ rootDir: string,
464
+ cache: BuildCache,
465
+ noCache: boolean,
466
+ ): Promise<void> {
467
+ const unitId = `shell:arc:${shortName}`;
468
+ const otherExternals = allArcPkgs.filter((p) => p !== pkg);
469
+ const inputHash = sha256OfJson({
470
+ pkg,
471
+ version: readInstalledVersion(rootDir, pkg),
472
+ src: arcPkgSrcHash(rootDir, pkg),
473
+ base: SHELL_BASE_EXTERNAL,
474
+ others: [...otherExternals].sort(),
475
+ });
476
+
477
+ const outputFile = join(shellDir, `${shortName}.js`);
478
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [outputFile])) {
479
+ console.log(` ✓ cached: ${unitId}`);
480
+ return;
318
481
  }
319
482
 
320
- // Clean tmp
321
- const { rmSync } = await import("fs");
483
+ console.log(` building: ${unitId}`);
484
+
485
+ const f = join(tmpDir, `${shortName}.ts`);
486
+ await Bun.write(f, `export * from "${pkg}";\n`);
487
+
488
+ const r = await Bun.build({
489
+ entrypoints: [f],
490
+ outdir: shellDir,
491
+ format: "esm",
492
+ target: "browser",
493
+ naming: "[name].[ext]",
494
+ external: [...SHELL_BASE_EXTERNAL, ...otherExternals],
495
+ define: {
496
+ ONLY_SERVER: "false",
497
+ ONLY_BROWSER: "true",
498
+ ONLY_CLIENT: "true",
499
+ },
500
+ });
501
+ if (!r.success) {
502
+ for (const l of r.logs) console.error(l);
503
+ throw new Error(`Shell build failed for ${pkg}`);
504
+ }
505
+
506
+ const outputHash = sha256OfFiles([outputFile]);
507
+ updateCache(cache, unitId, inputHash, { outputHash });
508
+ }
509
+
510
+ /**
511
+ * Build the framework shell — react layer + each @arcote.tech/* package as a
512
+ * separate cacheable unit. Tasks run in parallel via pAll.
513
+ */
514
+ export async function buildShell(
515
+ ws: WorkspaceInfo,
516
+ cache: BuildCache,
517
+ noCache: boolean,
518
+ ): Promise<void> {
519
+ mkdirSync(ws.shellDir, { recursive: true });
520
+ const tmpDir = join(ws.shellDir, "_tmp");
521
+ mkdirSync(tmpDir, { recursive: true });
522
+
523
+ const arcEntries = collectArcPeerDeps(ws.packages);
524
+ const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
525
+
526
+ const tasks: Array<() => Promise<void>> = [
527
+ () => buildShellReact(ws.shellDir, tmpDir, ws.rootDir, cache, noCache),
528
+ ...arcEntries.map(([short, pkg]) => () =>
529
+ buildShellArcEntry(short, pkg, allArcPkgs, ws.shellDir, tmpDir, ws.rootDir, cache, noCache),
530
+ ),
531
+ ];
532
+
533
+ await pAll(tasks);
534
+
322
535
  rmSync(tmpDir, { recursive: true, force: true });
323
536
  }
324
537
 
@@ -338,20 +551,14 @@ export async function loadServerContext(
338
551
  (globalThis as any).ONLY_BROWSER = false;
339
552
  (globalThis as any).ONLY_CLIENT = false;
340
553
 
341
- // Import all context packages side effects from module().build()
342
- // register context elements into the global registry via setContext().
343
- // Resolve platform from the project's node_modules using an absolute path.
344
- // When CLI is bun-linked, `import("@arcote.tech/platform")` would resolve to
345
- // the CLI's own copy, creating a separate module instance from what context
346
- // packages use. Using an absolute path ensures a single shared instance.
554
+ // Resolve platform from the project's node_modules using an absolute path
555
+ // (see comment in original implementation for why).
347
556
  const platformDir = join(process.cwd(), "node_modules", "@arcote.tech", "platform");
348
557
  const platformPkg = JSON.parse(readFileSync(join(platformDir, "package.json"), "utf-8"));
349
558
  const platformEntry = join(platformDir, platformPkg.main ?? "src/index.ts");
350
559
 
351
- // Pre-import platform so it's cached with this absolute path
352
560
  await import(platformEntry);
353
561
 
354
- // Import context packages from server dist (has server-only code paths)
355
562
  for (const ctx of ctxPackages) {
356
563
  const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
357
564
  if (!existsSync(serverDist)) {
@@ -366,7 +573,6 @@ export async function loadServerContext(
366
573
  }
367
574
  }
368
575
 
369
- // Import non-context packages from source to capture module().protectedBy() metadata
370
576
  const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
371
577
  for (const pkg of nonCtxPackages) {
372
578
  try {