@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.
Files changed (37) hide show
  1. package/dist/index.js +1696 -1663
  2. package/package.json +7 -7
  3. package/src/builder/access-extractor.ts +64 -46
  4. package/src/builder/build-cache.ts +3 -1
  5. package/src/builder/chunk-planner.ts +107 -0
  6. package/src/builder/dependency-collector.ts +83 -41
  7. package/src/builder/framework-peers.ts +81 -0
  8. package/src/builder/module-builder.ts +322 -106
  9. package/src/commands/platform-build.ts +2 -1
  10. package/src/commands/platform-deploy.ts +121 -64
  11. package/src/commands/platform-dev.ts +11 -100
  12. package/src/commands/platform-start.ts +4 -90
  13. package/src/deploy/ansible.ts +23 -3
  14. package/src/deploy/assets/ansible/site.yml +23 -7
  15. package/src/deploy/assets.ts +23 -7
  16. package/src/deploy/bootstrap.ts +270 -10
  17. package/src/deploy/caddyfile.ts +19 -23
  18. package/src/deploy/compose.ts +44 -27
  19. package/src/deploy/config.ts +67 -3
  20. package/src/deploy/deploy-env.ts +129 -0
  21. package/src/deploy/env-file.ts +103 -0
  22. package/src/deploy/htpasswd.ts +28 -0
  23. package/src/deploy/image-template.ts +74 -0
  24. package/src/deploy/image.ts +243 -0
  25. package/src/deploy/registry.ts +79 -0
  26. package/src/deploy/ssh.ts +52 -122
  27. package/src/deploy/survey.ts +64 -0
  28. package/src/index.ts +20 -13
  29. package/src/platform/server.ts +119 -94
  30. package/src/platform/shared.ts +139 -292
  31. package/src/platform/startup.ts +159 -0
  32. package/runtime/Dockerfile +0 -29
  33. package/runtime/build-and-push.sh +0 -23
  34. package/runtime/entrypoint.sh +0 -58
  35. package/src/commands/build-shell.ts +0 -152
  36. package/src/deploy/remote-sync.ts +0 -321
  37. package/src/platform/deploy-api.ts +0 -400
@@ -1,13 +1,14 @@
1
1
  import { findUpSync } from "find-up";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  import {
5
+ buildBrowserApp,
5
6
  buildContextPackages,
6
- buildModulesBundle,
7
7
  buildStyles,
8
8
  buildTranslations,
9
9
  discoverPackages,
10
10
  isContextPackage,
11
+ type BrowserAppResult,
11
12
  type BuildManifest,
12
13
  type ModuleDescriptor,
13
14
  type WorkspacePackage,
@@ -19,23 +20,18 @@ import {
19
20
  updateCache,
20
21
  type BuildCache,
21
22
  } from "../builder/build-cache";
23
+ import { extractAccessMap } from "../builder/access-extractor";
24
+ import { planChunks } from "../builder/chunk-planner";
25
+ import { collectFrameworkDeps } from "../builder/dependency-collector";
22
26
  import {
23
27
  mtimeOf,
24
- readInstalledVersion,
25
28
  sha256Hex,
26
- sha256OfDir,
27
29
  sha256OfFiles,
28
30
  sha256OfJson,
29
31
  } from "../builder/hash";
30
- import { pAll } from "../builder/parallel";
31
- import {
32
- collectFrameworkDeps,
33
- collectModuleDeps,
34
- } from "../builder/dependency-collector";
35
- import { extractAccessMap } from "../builder/access-extractor";
36
32
 
37
33
  // Re-export for convenience
38
- export { buildContextPackages, buildModulesBundle, buildStyles, isContextPackage };
34
+ export { buildContextPackages, buildStyles, isContextPackage };
39
35
  export type { BuildManifest, ModuleDescriptor, WorkspacePackage };
40
36
 
41
37
  // ---------------------------------------------------------------------------
@@ -70,8 +66,8 @@ export interface WorkspaceInfo {
70
66
  rootPkg: Record<string, any>;
71
67
  appName: string;
72
68
  arcDir: string;
73
- modulesDir: string;
74
- shellDir: string;
69
+ /** Single Bun.build output dir for browser app: `<arcDir>/browser/`. */
70
+ browserDir: string;
75
71
  /** Static assets generated z arc.browserAssets workspace deps. Serwowane pod /assets/*. */
76
72
  assetsDir: string;
77
73
  publicDir: string;
@@ -93,16 +89,15 @@ export function resolveWorkspace(): WorkspaceInfo {
93
89
 
94
90
  log("Scanning workspaces...");
95
91
  const packages = discoverPackages(rootDir);
96
- ok(
97
- `Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`,
98
- );
99
-
100
- // Empty package list is allowed in pre-deploy runtime mode (container with
101
- // freshly-mounted volume, no user code pushed yet). The CLI entry that
102
- // forbids this (arc platform build/dev) checks explicitly.
103
- if (packages.length === 0 && process.env.ARC_DEPLOY_API !== "1") {
104
- err("No workspace packages found.");
105
- process.exit(1);
92
+ if (packages.length > 0) {
93
+ ok(
94
+ `Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`,
95
+ );
96
+ } else {
97
+ // Zero packages is valid for `platform start` in image runtime mode —
98
+ // the workspace tree isn't shipped, only `.arc/platform/` is. Build
99
+ // commands enforce non-empty packages where they need them.
100
+ log("No workspace packages found — assuming image runtime mode.");
106
101
  }
107
102
 
108
103
  // Detect manifest.json or manifest.webmanifest in root dir
@@ -131,8 +126,7 @@ export function resolveWorkspace(): WorkspaceInfo {
131
126
  rootPkg,
132
127
  appName,
133
128
  arcDir,
134
- modulesDir: join(arcDir, "modules"),
135
- shellDir: join(arcDir, "shell"),
129
+ browserDir: join(arcDir, "browser"),
136
130
  assetsDir: join(arcDir, "assets"),
137
131
  publicDir: join(rootDir, "public"),
138
132
  packages,
@@ -163,69 +157,115 @@ export async function buildAll(
163
157
 
164
158
  log(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
165
159
 
166
- // Promise.all over independent units. buildModulesBundle is the only one
167
- // that returns data we need (the manifest). The rest update the cache.
168
- const [, modulesResult] = await Promise.all([
169
- buildContextPackages(ws.rootDir, ws.packages, cache, noCache),
170
- buildModulesBundle(ws.rootDir, ws.modulesDir, ws.packages, cache, noCache),
171
- buildShell(ws, cache, noCache),
160
+ // Phase 1 context packages must finish FIRST. The access-extractor
161
+ // subprocess imports workspace packages by name, which resolve through
162
+ // node_modules to packages' `main` field (typically `dist/server/main/`).
163
+ await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
164
+
165
+ // Phase 1b — relocate per-package server bundles into `.arc/platform/server/`
166
+ // so the deploy image can be self-contained (image COPY needs everything
167
+ // server-side under one root). loadServerContext reads from here in prod.
168
+ copyContextServerBundles(ws);
169
+
170
+ // Phase 2 — extract access metadata (token name + hasCheck per module) in
171
+ // an isolated subprocess. This MUST run before chunk planning so we know
172
+ // which token group each module belongs to.
173
+ const accessMap = await extractAccessMap(ws.rootDir, ws.packages);
174
+
175
+ // Persist access map for the runtime host (server.ts reads at startup to
176
+ // wire up moduleAccessMap for filterManifestForTokens / signed URLs).
177
+ mkdirSync(ws.arcDir, { recursive: true });
178
+ writeFileSync(
179
+ join(ws.arcDir, "access.json"),
180
+ JSON.stringify(accessMap, null, 2) + "\n",
181
+ );
182
+
183
+ // Phase 3 — group modules into chunks (one Bun.build per group).
184
+ const plan = planChunks(ws.packages, accessMap);
185
+ ok(
186
+ `Chunks: ${plan.chunks
187
+ .map((c) => `${c}(${plan.groups.get(c)?.length ?? 0})`)
188
+ .join(", ")}`,
189
+ );
190
+
191
+ // Phase 4 — independent parallel build units. The browser app is a single
192
+ // Bun.build call covering initial + all token groups (replaces ~25 separate
193
+ // Bun.builds for shell + per-chunk + per-package).
194
+ const i18nCollector = new Map<string, Set<string>>();
195
+
196
+ const [browserResult] = await Promise.all([
197
+ buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector),
172
198
  buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
173
199
  copyBrowserAssets(ws, cache, noCache),
174
200
  buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
175
201
  ]);
176
202
 
177
- saveBuildCache(ws.arcDir, cache);
203
+ // Finalize i18n catalogs once after all chunks + initial bundle collected.
204
+ const { finalizeTranslations } = await import("../i18n");
205
+ await finalizeTranslations(ws.rootDir, ws.arcDir, i18nCollector);
178
206
 
179
- // v0.6 deploy artifacts: framework + per-module dependency manifests + access
180
- // map. Generated unconditionally runtime container needs them to bun-install
181
- // and to enforce protectBy rules. Order matters: deps before access (access
182
- // extraction subprocess imports server bundles whose own deps may matter).
207
+ // Phase 5 framework peer manifest at `<arcDir>/package.json`. Used by
208
+ // the deploy image build (single `bun install` for all peers, one copy
209
+ // shared across module bundles).
183
210
  collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages);
184
- for (const pkg of ws.packages) {
185
- collectModuleDeps(ws.arcDir, pkg);
186
- }
187
- try {
188
- await extractAccessMap(ws.arcDir, ws.packages);
189
- } catch (e) {
190
- err(`access-extractor failed: ${(e as Error).message}`);
191
- // Don't fail the build — protection rules will be empty server-side but
192
- // the rest of the deploy can still proceed.
193
- }
194
211
 
195
- const finalManifest = assembleManifest(ws, modulesResult.modules, cache);
212
+ saveBuildCache(ws.arcDir, cache);
213
+
214
+ const finalManifest = assembleManifest(ws, browserResult, cache);
196
215
  writeFileSync(
197
- join(ws.modulesDir, "manifest.json"),
216
+ join(ws.arcDir, "manifest.json"),
198
217
  JSON.stringify(finalManifest, null, 2),
199
218
  );
200
219
  return finalManifest;
201
220
  }
202
221
 
203
222
  /**
204
- * Assemble the platform manifest from cached output hashes no disk reads.
223
+ * Assemble the platform manifest from the browser app result + style hash.
205
224
  */
206
225
  function assembleManifest(
207
226
  ws: WorkspaceInfo,
208
- modules: ModuleDescriptor[],
227
+ browser: BrowserAppResult,
209
228
  cache: BuildCache,
210
229
  ): BuildManifest {
211
- // Aggregate shellHash from all shell:* unit output hashes.
212
- const shellEntries: Record<string, string> = {};
213
- for (const [unitId, entry] of Object.entries(cache.units)) {
214
- if (unitId.startsWith("shell:") && entry.outputHash) {
215
- shellEntries[unitId] = entry.outputHash;
216
- }
217
- }
218
- const shellHash = sha256OfJson(shellEntries);
219
230
  const stylesHash = cache.units["styles"]?.outputHash ?? "";
220
231
 
221
232
  return {
222
- modules,
223
- shellHash,
233
+ initial: browser.initial,
234
+ groups: browser.groups,
235
+ sharedChunks: browser.sharedChunks,
224
236
  stylesHash,
225
237
  buildTime: new Date().toISOString(),
226
238
  };
227
239
  }
228
240
 
241
+ // ---------------------------------------------------------------------------
242
+ // Context server bundles — flatten to `<arcDir>/server/<safeName>.js`
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * Copy each context package's compiled server bundle from
247
+ * `packages/<pkg>/dist/server/main/index.js` to a flat location at
248
+ * `<arcDir>/server/<safeName>.js`. The flat layout makes the deploy image
249
+ * self-contained — `COPY .arc/platform/` pulls everything server-side, no
250
+ * need to drag the entire `packages/` tree into the image.
251
+ */
252
+ function copyContextServerBundles(ws: WorkspaceInfo): void {
253
+ const outDir = join(ws.arcDir, "server");
254
+ mkdirSync(outDir, { recursive: true });
255
+
256
+ for (const pkg of ws.packages) {
257
+ if (!isContextPackage(pkg.packageJson)) continue;
258
+ const src = join(pkg.path, "dist", "server", "main", "index.js");
259
+ if (!existsSync(src)) {
260
+ err(`Server bundle missing for ${pkg.name}: ${src}`);
261
+ continue;
262
+ }
263
+ const safeName = pkg.path.split("/").pop()!;
264
+ const dst = join(outDir, `${safeName}.js`);
265
+ copyFileSync(src, dst);
266
+ }
267
+ }
268
+
229
269
  // ---------------------------------------------------------------------------
230
270
  // Browser assets — @arcote.tech/* deps deklarują w `arc.browserAssets` jakie
231
271
  // pliki muszą być dostępne w przeglądarce (np. SQLite WASM worker + .wasm).
@@ -353,258 +393,65 @@ async function copyBrowserAssets(
353
393
  updateCache(cache, unitId, inputHash, { outputHashes });
354
394
  }
355
395
 
356
- // ---------------------------------------------------------------------------
357
- // Shell builder — framework packages for import map
358
- // ---------------------------------------------------------------------------
359
-
360
- /** Collect all @arcote.tech/* peerDependencies from workspace packages. */
361
- export function collectArcPeerDeps(packages: WorkspacePackage[]): [string, string][] {
362
- const seen = new Set<string>();
363
- for (const pkg of ["@arcote.tech/arc", "@arcote.tech/arc-ds", "@arcote.tech/arc-react", "@arcote.tech/platform"]) {
364
- seen.add(pkg);
365
- }
366
- for (const wp of packages) {
367
- const peerDeps = wp.packageJson.peerDependencies ?? {};
368
- for (const dep of Object.keys(peerDeps)) {
369
- if (dep.startsWith("@arcote.tech/")) seen.add(dep);
370
- }
371
- }
372
- return [...seen].map((pkg) => {
373
- const short = pkg === "@arcote.tech/platform"
374
- ? "platform"
375
- : pkg.replace("@arcote.tech/", "");
376
- return [short, pkg];
377
- });
378
- }
379
-
380
- const REACT_ENTRIES: [string, string][] = [
381
- [
382
- "react",
383
- `import React from "react";
384
- export default React;
385
- export const {
386
- Children, Component, Fragment, Profiler, PureComponent, StrictMode, Suspense,
387
- cloneElement, createContext, createElement, createRef, forwardRef, isValidElement,
388
- lazy, memo, startTransition, use, useCallback, useContext, useDebugValue,
389
- useDeferredValue, useEffect, useId, useImperativeHandle, useInsertionEffect,
390
- useLayoutEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore,
391
- useTransition, version, useActionState, useOptimistic,
392
- } = React;`,
393
- ],
394
- ["jsx-runtime", `export { jsx, jsxs, Fragment } from "react/jsx-runtime";`],
395
- [
396
- "jsx-dev-runtime",
397
- `export { jsxDEV, Fragment } from "react/jsx-dev-runtime";`,
398
- ],
399
- [
400
- "react-dom",
401
- `import ReactDOM from "react-dom";
402
- export default ReactDOM;
403
- export const { createPortal, flushSync } = ReactDOM;`,
404
- ],
405
- [
406
- "react-dom-client",
407
- `export { createRoot, hydrateRoot } from "react-dom/client";`,
408
- ],
409
- ];
410
-
411
- const REACT_OUTPUT_FILES = REACT_ENTRIES.map(([n]) => `${n}.js`);
412
-
413
- const SHELL_BASE_EXTERNAL = [
414
- "react",
415
- "react-dom",
416
- "react/jsx-runtime",
417
- "react/jsx-dev-runtime",
418
- "react-dom/client",
419
- ];
420
-
421
- const sourceFilter = (rel: string): boolean => {
422
- if (rel.startsWith("dist/") || rel.startsWith("dist")) return false;
423
- if (rel.includes("/node_modules/") || rel.startsWith("node_modules")) return false;
424
- if (rel.startsWith(".arc/") || rel.startsWith(".arc")) return false;
425
- return true;
426
- };
427
-
428
- function arcPkgSrcHash(rootDir: string, pkg: string): string {
429
- // Prefer src/ tree (workspace links), fallback to whole package dir.
430
- const srcDir = join(rootDir, "node_modules", pkg, "src");
431
- if (existsSync(srcDir)) return sha256OfDir(srcDir, sourceFilter);
432
- return sha256OfDir(join(rootDir, "node_modules", pkg), sourceFilter);
433
- }
434
-
435
- async function buildShellReact(
436
- shellDir: string,
437
- tmpDir: string,
438
- rootDir: string,
439
- cache: BuildCache,
440
- noCache: boolean,
441
- ): Promise<void> {
442
- const unitId = "shell:react";
443
- const inputHash = sha256OfJson({
444
- react: readInstalledVersion(rootDir, "react"),
445
- "react-dom": readInstalledVersion(rootDir, "react-dom"),
446
- entries: REACT_ENTRIES.map(([k, v]) => [k, v]),
447
- });
448
-
449
- const requiredOutputs = REACT_OUTPUT_FILES.map((f) => join(shellDir, f));
450
- if (!noCache && isCacheHit(cache, unitId, inputHash, requiredOutputs)) {
451
- console.log(` ✓ cached: shell:react`);
452
- return;
453
- }
454
-
455
- console.log(` building: shell:react`);
456
-
457
- const reactEps: string[] = [];
458
- for (const [name, code] of REACT_ENTRIES) {
459
- const f = join(tmpDir, `${name}.ts`);
460
- await Bun.write(f, code);
461
- reactEps.push(f);
462
- }
463
-
464
- const r = await Bun.build({
465
- entrypoints: reactEps,
466
- outdir: shellDir,
467
- splitting: true,
468
- format: "esm",
469
- target: "browser",
470
- naming: "[name].[ext]",
471
- });
472
- if (!r.success) {
473
- for (const l of r.logs) console.error(l);
474
- throw new Error("Shell React build failed");
475
- }
476
-
477
- const outputHash = sha256OfFiles(requiredOutputs);
478
- updateCache(cache, unitId, inputHash, { outputHash });
479
- }
480
-
481
- async function buildShellArcEntry(
482
- shortName: string,
483
- pkg: string,
484
- allArcPkgs: string[],
485
- shellDir: string,
486
- tmpDir: string,
487
- rootDir: string,
488
- cache: BuildCache,
489
- noCache: boolean,
490
- ): Promise<void> {
491
- const unitId = `shell:arc:${shortName}`;
492
- const otherExternals = allArcPkgs.filter((p) => p !== pkg);
493
- const inputHash = sha256OfJson({
494
- pkg,
495
- version: readInstalledVersion(rootDir, pkg),
496
- src: arcPkgSrcHash(rootDir, pkg),
497
- base: SHELL_BASE_EXTERNAL,
498
- others: [...otherExternals].sort(),
499
- });
500
-
501
- const outputFile = join(shellDir, `${shortName}.js`);
502
- if (!noCache && isCacheHit(cache, unitId, inputHash, [outputFile])) {
503
- console.log(` ✓ cached: ${unitId}`);
504
- return;
505
- }
506
-
507
- console.log(` building: ${unitId}`);
508
-
509
- const f = join(tmpDir, `${shortName}.ts`);
510
- await Bun.write(f, `export * from "${pkg}";\n`);
511
-
512
- const r = await Bun.build({
513
- entrypoints: [f],
514
- outdir: shellDir,
515
- format: "esm",
516
- target: "browser",
517
- naming: "[name].[ext]",
518
- external: [...SHELL_BASE_EXTERNAL, ...otherExternals],
519
- define: {
520
- ONLY_SERVER: "false",
521
- ONLY_BROWSER: "true",
522
- ONLY_CLIENT: "true",
523
- },
524
- });
525
- if (!r.success) {
526
- for (const l of r.logs) console.error(l);
527
- throw new Error(`Shell build failed for ${pkg}`);
528
- }
529
-
530
- const outputHash = sha256OfFiles([outputFile]);
531
- updateCache(cache, unitId, inputHash, { outputHash });
532
- }
533
-
534
- /**
535
- * Build the framework shell — react layer + each @arcote.tech/* package as a
536
- * separate cacheable unit. Tasks run in parallel via pAll.
537
- */
538
- export async function buildShell(
539
- ws: WorkspaceInfo,
540
- cache: BuildCache,
541
- noCache: boolean,
542
- ): Promise<void> {
543
- mkdirSync(ws.shellDir, { recursive: true });
544
- const tmpDir = join(ws.shellDir, "_tmp");
545
- mkdirSync(tmpDir, { recursive: true });
546
-
547
- const arcEntries = collectArcPeerDeps(ws.packages);
548
- const allArcPkgs = arcEntries.map(([, pkg]) => pkg);
549
-
550
- const tasks: Array<() => Promise<void>> = [
551
- () => buildShellReact(ws.shellDir, tmpDir, ws.rootDir, cache, noCache),
552
- ...arcEntries.map(([short, pkg]) => () =>
553
- buildShellArcEntry(short, pkg, allArcPkgs, ws.shellDir, tmpDir, ws.rootDir, cache, noCache),
554
- ),
555
- ];
556
-
557
- await pAll(tasks);
558
-
559
- rmSync(tmpDir, { recursive: true, force: true });
560
- }
561
-
562
396
  // ---------------------------------------------------------------------------
563
397
  // Server context loading
564
398
  // ---------------------------------------------------------------------------
565
399
 
566
400
  export async function loadServerContext(
567
- packages: WorkspacePackage[],
401
+ ws: WorkspaceInfo,
568
402
  ): Promise<{ context: any | null; moduleAccess: Map<string, any> }> {
569
- const ctxPackages = packages.filter((p) => isContextPackage(p.packageJson));
570
- if (ctxPackages.length === 0) return { context: null, moduleAccess: new Map() };
571
-
572
403
  // Set globals for server context — framework packages (arc-auth etc.)
573
404
  // use these at runtime to tree-shake browser/server code paths.
574
405
  (globalThis as any).ONLY_SERVER = true;
575
406
  (globalThis as any).ONLY_BROWSER = false;
576
407
  (globalThis as any).ONLY_CLIENT = false;
577
408
 
578
- // Resolve platform from the project's node_modules using an absolute path
579
- // (see comment in original implementation for why).
409
+ // Resolve platform from the project's node_modules. Platform exports a
410
+ // single entry (./src/index.ts) React imports at top level are benign
411
+ // in a non-render context (createContext, function defs only).
580
412
  const platformDir = join(process.cwd(), "node_modules", "@arcote.tech", "platform");
581
413
  const platformPkg = JSON.parse(readFileSync(join(platformDir, "package.json"), "utf-8"));
582
414
  const platformEntry = join(platformDir, platformPkg.main ?? "src/index.ts");
583
415
 
584
416
  await import(platformEntry);
585
417
 
586
- for (const ctx of ctxPackages) {
587
- const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
588
- if (!existsSync(serverDist)) {
589
- err(`Context server dist not found: ${serverDist}`);
590
- continue;
591
- }
592
-
593
- try {
594
- await import(serverDist);
595
- } catch (e) {
596
- err(`Failed to load server context from ${ctx.name}: ${e}`);
418
+ // Primary source: flattened server bundles at `<arcDir>/server/<safeName>.js`.
419
+ // The deploy image only has this directory — there's no workspace `packages/`
420
+ // tree. In dev, `copyContextServerBundles` populates this same location, so
421
+ // both modes go through the same code path.
422
+ const serverDir = join(ws.arcDir, "server");
423
+ const bundles = existsSync(serverDir)
424
+ ? readdirSync(serverDir).filter((f) => f.endsWith(".js"))
425
+ : [];
426
+
427
+ if (bundles.length > 0) {
428
+ for (const file of bundles) {
429
+ const bundlePath = join(serverDir, file);
430
+ try {
431
+ await import(bundlePath);
432
+ } catch (e) {
433
+ err(`Failed to load server bundle ${file}: ${e}`);
434
+ }
597
435
  }
598
- }
599
-
600
- const nonCtxPackages = packages.filter((p) => !isContextPackage(p.packageJson));
601
- for (const pkg of nonCtxPackages) {
602
- try {
603
- await import(pkg.entrypoint);
604
- } catch {
605
- // Non-context packages may fail on server (React components etc.) — that's OK,
606
- // module().protectedBy().build() runs synchronously before any rendering
436
+ } else if (ws.packages.length > 0) {
437
+ // Fallback for the "no .arc/platform/server/ yet" case (e.g. somebody
438
+ // wired up loadServerContext before running the build). This path goes
439
+ // through workspace packages directly — only meaningful in dev.
440
+ const ctxPackages = ws.packages.filter((p) => isContextPackage(p.packageJson));
441
+ for (const ctx of ctxPackages) {
442
+ const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
443
+ if (!existsSync(serverDist)) {
444
+ err(`Context server dist not found: ${serverDist}`);
445
+ continue;
446
+ }
447
+ try {
448
+ await import(serverDist);
449
+ } catch (e) {
450
+ err(`Failed to load server context from ${ctx.name}: ${e}`);
451
+ }
607
452
  }
453
+ } else {
454
+ return { context: null, moduleAccess: new Map() };
608
455
  }
609
456
 
610
457
  const { getContext, getAllModuleAccess } = await import(platformEntry);