@arcote.tech/arc-cli 0.7.20 → 0.7.21

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 CHANGED
@@ -853,7 +853,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
853
853
  this._exitCallback = (err) => {
854
854
  if (err.code !== "commander.executeSubCommandAsync") {
855
855
  throw err;
856
- } else {}
856
+ }
857
857
  };
858
858
  }
859
859
  return this;
@@ -34723,6 +34723,17 @@ function serverExternalsPlugin() {
34723
34723
  }
34724
34724
  };
34725
34725
  }
34726
+ function workspaceSourcePlugin(srcByName) {
34727
+ return {
34728
+ name: "workspace-source",
34729
+ setup(build2) {
34730
+ build2.onResolve({ filter: /^[^./]/ }, (args) => {
34731
+ const src = srcByName.get(args.path);
34732
+ return src ? { path: src, sideEffects: true } : null;
34733
+ });
34734
+ }
34735
+ };
34736
+ }
34726
34737
  function jsxDevShimPlugin() {
34727
34738
  return {
34728
34739
  name: "jsx-dev-runtime-shim",
@@ -34743,9 +34754,10 @@ export { Fragment };
34743
34754
  };
34744
34755
  }
34745
34756
  var CONTEXT_CLIENTS = [
34746
- { name: "server", target: "bun", defines: { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } },
34747
34757
  { name: "browser", target: "browser", defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } }
34748
34758
  ];
34759
+ var SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" };
34760
+ var SERVER_ENTRY_FILE = "_server.js";
34749
34761
  function discoverPackages(rootDir) {
34750
34762
  const rootPkg = JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf-8"));
34751
34763
  const workspaceGlobs = rootPkg.workspaces ?? [];
@@ -34838,9 +34850,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
34838
34850
  console.log(` building: ${pkg.name} (${client.name})`);
34839
34851
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
34840
34852
  const allDeps = pkg.packageJson.dependencies ?? {};
34841
- const isBrowser2 = client.name === "browser";
34842
- const workspaceDeps = isBrowser2 ? Object.keys(allDeps) : Object.entries(allDeps).filter(([, spec]) => !spec.startsWith("workspace:")).map(([name]) => name);
34843
- const externals = [...peerDeps, ...workspaceDeps];
34853
+ const externals = [...peerDeps, ...Object.keys(allDeps)];
34844
34854
  const result = await Bun.build({
34845
34855
  entrypoints: [pkg.entrypoint],
34846
34856
  outdir: join8(outDir, "main"),
@@ -34848,7 +34858,7 @@ async function buildContextClient(pkg, rootDir, client, cache, noCache) {
34848
34858
  format: "esm",
34849
34859
  naming: "index.[ext]",
34850
34860
  external: externals,
34851
- plugins: isBrowser2 ? [jsxDevShimPlugin()] : [jsxDevShimPlugin(), serverExternalsPlugin()],
34861
+ plugins: [jsxDevShimPlugin()],
34852
34862
  define: client.defines
34853
34863
  });
34854
34864
  if (!result.success) {
@@ -34911,6 +34921,80 @@ async function buildContextPackages(rootDir, packages, cache, noCache) {
34911
34921
  }
34912
34922
  return { declarationErrors };
34913
34923
  }
34924
+ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
34925
+ const contexts = packages.filter((p) => isContextPackage(p.packageJson));
34926
+ mkdirSync6(serverDir, { recursive: true });
34927
+ const srcByName = new Map(packages.map((p) => [p.name, p.entrypoint]));
34928
+ const externalSet = new Set(FRAMEWORK_PEERS);
34929
+ for (const p of packages) {
34930
+ for (const name of Object.keys(p.packageJson.peerDependencies ?? {})) {
34931
+ externalSet.add(name);
34932
+ }
34933
+ for (const [name, spec] of Object.entries(p.packageJson.dependencies ?? {})) {
34934
+ if (!spec.startsWith("workspace:"))
34935
+ externalSet.add(name);
34936
+ }
34937
+ }
34938
+ const external = [...externalSet];
34939
+ const unitId = "server-app";
34940
+ const inputHash = sha256OfJson({
34941
+ members: packages.map((p) => ({ name: p.name, src: pkgSourceHash(p) })).sort((a, b) => a.name.localeCompare(b.name)),
34942
+ contexts: contexts.map((p) => p.name).sort(),
34943
+ external: [...external].sort(),
34944
+ defines: SERVER_DEFINES
34945
+ });
34946
+ const entryFileAbs = join8(serverDir, SERVER_ENTRY_FILE);
34947
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
34948
+ console.log(` \u2713 cached: ${unitId}`);
34949
+ return { entryFile: SERVER_ENTRY_FILE, cached: true };
34950
+ }
34951
+ console.log(` building: ${unitId} (${contexts.length} server modules)`);
34952
+ for (const f of readdirSync4(serverDir)) {
34953
+ if (f.endsWith(".js"))
34954
+ rmSync(join8(serverDir, f), { force: true });
34955
+ }
34956
+ const tmpDir = join8(serverDir, "_entries");
34957
+ mkdirSync6(tmpDir, { recursive: true });
34958
+ const entrySrc = join8(tmpDir, SERVER_ENTRY_FILE.replace(/\.js$/, ".ts"));
34959
+ writeFileSync6(entrySrc, contexts.map((p) => `import "${p.name}";`).join(`
34960
+ `) + `
34961
+ `);
34962
+ let result;
34963
+ try {
34964
+ result = await Bun.build({
34965
+ entrypoints: [entrySrc],
34966
+ outdir: serverDir,
34967
+ target: "bun",
34968
+ format: "esm",
34969
+ splitting: true,
34970
+ naming: "[name].[ext]",
34971
+ external,
34972
+ plugins: [
34973
+ jsxDevShimPlugin(),
34974
+ serverExternalsPlugin(),
34975
+ workspaceSourcePlugin(srcByName)
34976
+ ],
34977
+ define: SERVER_DEFINES
34978
+ });
34979
+ } finally {
34980
+ rmSync(tmpDir, { recursive: true, force: true });
34981
+ }
34982
+ if (!result.success) {
34983
+ for (const log2 of result.logs)
34984
+ console.error(log2);
34985
+ throw new Error("Server app build failed");
34986
+ }
34987
+ const entryOut = result.outputs.find((o) => o.kind === "entry-point");
34988
+ if (!entryOut) {
34989
+ throw new Error("Server app build: entry not found in outputs");
34990
+ }
34991
+ if (basename2(entryOut.path) !== SERVER_ENTRY_FILE) {
34992
+ throw new Error(`Server app build: unexpected entry name ${basename2(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`);
34993
+ }
34994
+ const outputHash = sha256OfDir(serverDir);
34995
+ updateCache(cache, unitId, inputHash, { outputHash });
34996
+ return { entryFile: SERVER_ENTRY_FILE, cached: false };
34997
+ }
34914
34998
  async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector) {
34915
34999
  mkdirSync6(outDir, { recursive: true });
34916
35000
  const publicMembers = plan.groups.get("public") ?? [];
@@ -35216,11 +35300,8 @@ import {
35216
35300
  writeFileSync as writeFileSync7
35217
35301
  } from "fs";
35218
35302
  import { join as join9 } from "path";
35219
- async function extractAccessMap(rootDir, packages) {
35220
- const serverBundles = packages.filter((p) => isContextPackage(p.packageJson)).map((p) => ({
35221
- name: p.name,
35222
- path: join9(p.path, "dist", "server", "main", "index.js")
35223
- })).filter((b) => existsSync8(b.path));
35303
+ async function extractAccessMap(rootDir, serverBundlePath) {
35304
+ const serverBundles = existsSync8(serverBundlePath) ? [{ name: "server", path: serverBundlePath }] : [];
35224
35305
  const workerDir = join9(rootDir, ".arc", ".tmp");
35225
35306
  mkdirSync7(workerDir, { recursive: true });
35226
35307
  const workerPath = join9(workerDir, `access-extractor-${Date.now()}.mjs`);
@@ -35549,8 +35630,9 @@ async function buildAll(ws, opts = {}) {
35549
35630
  log2(`Building (concurrency parallel${noCache ? ", no-cache" : ""})...`);
35550
35631
  assertOneModulePerPackage(ws.packages);
35551
35632
  await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
35552
- copyContextServerBundles(ws);
35553
- const accessMap = await extractAccessMap(ws.rootDir, ws.packages);
35633
+ const serverDir = join12(ws.arcDir, "server");
35634
+ const { entryFile: serverEntry } = await buildServerApp(ws.rootDir, serverDir, ws.packages, cache, noCache);
35635
+ const accessMap = await extractAccessMap(ws.rootDir, join12(serverDir, serverEntry));
35554
35636
  mkdirSync9(ws.arcDir, { recursive: true });
35555
35637
  writeFileSync9(join12(ws.arcDir, "access.json"), JSON.stringify(accessMap, null, 2) + `
35556
35638
  `);
@@ -35581,22 +35663,6 @@ function assembleManifest(ws, browser, cache) {
35581
35663
  buildTime: new Date().toISOString()
35582
35664
  };
35583
35665
  }
35584
- function copyContextServerBundles(ws) {
35585
- const outDir = join12(ws.arcDir, "server");
35586
- mkdirSync9(outDir, { recursive: true });
35587
- for (const pkg of ws.packages) {
35588
- if (!isContextPackage(pkg.packageJson))
35589
- continue;
35590
- const src = join12(pkg.path, "dist", "server", "main", "index.js");
35591
- if (!existsSync10(src)) {
35592
- err(`Server bundle missing for ${pkg.name}: ${src}`);
35593
- continue;
35594
- }
35595
- const safeName = pkg.path.split("/").pop();
35596
- const dst = join12(outDir, `${safeName}.js`);
35597
- copyFileSync(src, dst);
35598
- }
35599
- }
35600
35666
  function resolveAssetSource(from, pkgDir, rootDir) {
35601
35667
  if (from.startsWith("./") || from.startsWith("../")) {
35602
35668
  const resolved = join12(pkgDir, from);
@@ -35692,34 +35758,15 @@ async function loadServerContext(ws) {
35692
35758
  const platformPkg = JSON.parse(readFileSync11(join12(platformDir, "package.json"), "utf-8"));
35693
35759
  const platformEntry = join12(platformDir, platformPkg.main ?? "src/index.ts");
35694
35760
  await import(platformEntry);
35695
- const serverDir = join12(ws.arcDir, "server");
35696
- const bundles = existsSync10(serverDir) ? readdirSync6(serverDir).filter((f) => f.endsWith(".js")) : [];
35697
- if (bundles.length > 0) {
35698
- for (const file of bundles) {
35699
- const bundlePath = join12(serverDir, file);
35700
- try {
35701
- await import(bundlePath);
35702
- } catch (e) {
35703
- err(`Failed to load server bundle ${file}: ${e}`);
35704
- }
35705
- }
35706
- } else if (ws.packages.length > 0) {
35707
- const ctxPackages = ws.packages.filter((p) => isContextPackage(p.packageJson));
35708
- for (const ctx of ctxPackages) {
35709
- const serverDist = join12(ctx.path, "dist", "server", "main", "index.js");
35710
- if (!existsSync10(serverDist)) {
35711
- err(`Context server dist not found: ${serverDist}`);
35712
- continue;
35713
- }
35714
- try {
35715
- await import(serverDist);
35716
- } catch (e) {
35717
- err(`Failed to load server context from ${ctx.name}: ${e}`);
35718
- }
35719
- }
35720
- } else {
35761
+ const serverEntry = join12(ws.arcDir, "server", SERVER_ENTRY_FILE);
35762
+ if (!existsSync10(serverEntry)) {
35721
35763
  return { context: null, moduleAccess: new Map };
35722
35764
  }
35765
+ try {
35766
+ await import(serverEntry);
35767
+ } catch (e) {
35768
+ err(`Failed to load server bundle ${SERVER_ENTRY_FILE}: ${e}`);
35769
+ }
35723
35770
  const { getContext, getAllModuleAccess } = await import(platformEntry);
35724
35771
  return {
35725
35772
  context: getContext() ?? null,
@@ -36553,8 +36600,13 @@ ${envNames.map((name) => ` - "https://${cfg.envs[name].domain}"`).joi
36553
36600
 
36554
36601
  # Per-container CPU / memory / network / block-IO + restarts straight from
36555
36602
  # the Docker daemon (socket bind-mounted read-only, see compose).
36603
+ # api_version pinned: the receiver defaults to Docker API 1.25, which modern
36604
+ # daemons (Engine 25+ require >= 1.40) reject \u2014 without this the receiver
36605
+ # fails to start and takes the whole collector down. Quoted so YAML doesn't
36606
+ # parse 1.40 \u2192 1.4. Must be <= the daemon's max; 1.40 is the safe floor.
36556
36607
  docker_stats:
36557
36608
  endpoint: unix:///var/run/docker.sock
36609
+ api_version: "1.40"
36558
36610
  collection_interval: 30s
36559
36611
  metrics:
36560
36612
  container.restarts:
@@ -36824,6 +36876,15 @@ function generateAlloyConfig() {
36824
36876
  discovery.docker "containers" {
36825
36877
  host = "unix:///var/run/docker.sock"
36826
36878
  refresh_interval = "15s"
36879
+
36880
+ // Only containers managed by a compose project (our stack). Ad-hoc / rogue
36881
+ // containers (manual debug runs, other stacks) are excluded \u2014 one bad
36882
+ // stream (e.g. log entries older than Loki's reject window) otherwise 400s
36883
+ // the whole loki.write batch and drops good app logs with it.
36884
+ filter {
36885
+ name = "label"
36886
+ values = ["com.docker.compose.project"]
36887
+ }
36827
36888
  }
36828
36889
 
36829
36890
  discovery.relabel "containers" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.7.20",
3
+ "version": "0.7.21",
4
4
  "description": "CLI tool for Arc framework",
5
5
  "module": "index.ts",
6
6
  "main": "dist/index.js",
@@ -12,13 +12,13 @@
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 --external '@opentelemetry/*' && chmod +x dist/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@arcote.tech/arc": "^0.7.20",
16
- "@arcote.tech/arc-ds": "^0.7.20",
17
- "@arcote.tech/arc-react": "^0.7.20",
18
- "@arcote.tech/arc-host": "^0.7.20",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.7.20",
20
- "@arcote.tech/arc-adapter-db-postgres": "^0.7.20",
21
- "@arcote.tech/arc-otel": "^0.7.20",
15
+ "@arcote.tech/arc": "^0.7.21",
16
+ "@arcote.tech/arc-ds": "^0.7.21",
17
+ "@arcote.tech/arc-react": "^0.7.21",
18
+ "@arcote.tech/arc-host": "^0.7.21",
19
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.21",
20
+ "@arcote.tech/arc-adapter-db-postgres": "^0.7.21",
21
+ "@arcote.tech/arc-otel": "^0.7.21",
22
22
  "@opentelemetry/api": "^1.9.0",
23
23
  "@opentelemetry/api-logs": "^0.57.0",
24
24
  "@opentelemetry/core": "^1.30.0",
@@ -31,7 +31,7 @@
31
31
  "@opentelemetry/sdk-trace-base": "^1.30.0",
32
32
  "@opentelemetry/sdk-trace-node": "^1.30.0",
33
33
  "@opentelemetry/semantic-conventions": "^1.27.0",
34
- "@arcote.tech/platform": "^0.7.20",
34
+ "@arcote.tech/platform": "^0.7.21",
35
35
  "@clack/prompts": "^0.9.0",
36
36
  "commander": "^11.1.0",
37
37
  "chokidar": "^3.5.3",
@@ -7,7 +7,6 @@ import {
7
7
  writeFileSync,
8
8
  } from "fs";
9
9
  import { join } from "path";
10
- import { isContextPackage, type WorkspacePackage } from "./module-builder";
11
10
 
12
11
  // ---------------------------------------------------------------------------
13
12
  // access-extractor — discovers per-module access rules (`protectedBy(...)`)
@@ -27,8 +26,8 @@ import { isContextPackage, type WorkspacePackage } from "./module-builder";
27
26
  // non-context (pure-browser) packages are skipped. This is sensible: access
28
27
  // checks logically belong with server state, not display components.
29
28
  //
30
- // Order requirement: buildContextPackages MUST run before extractAccessMap
31
- // server bundles at `packages/<pkg>/dist/server/main/index.js` must exist.
29
+ // Order requirement: buildServerApp MUST run before extractAccessMap — the
30
+ // combined server bundle at `<arcDir>/server/_server.js` must exist.
32
31
  // ---------------------------------------------------------------------------
33
32
 
34
33
  export interface SerializedAccessRule {
@@ -44,19 +43,16 @@ export type SerializedAccessMap = Record<string, SerializedModuleAccess>;
44
43
 
45
44
  export async function extractAccessMap(
46
45
  rootDir: string,
47
- packages: readonly WorkspacePackage[],
46
+ serverBundlePath: string,
48
47
  ): Promise<SerializedAccessMap> {
49
- // Each entry: a context-package server bundle path that the worker will
50
- // dynamically import. Bun resolves the bundle's internal external imports
51
- // (`@arcote.tech/platform` etc.) via node_modules walking from the bundle
52
- // location workspace symlinks point back to source.
53
- const serverBundles = packages
54
- .filter((p) => isContextPackage(p.packageJson))
55
- .map((p) => ({
56
- name: p.name,
57
- path: join(p.path, "dist", "server", "main", "index.js"),
58
- }))
59
- .filter((b) => existsSync(b.path));
48
+ // The combined server bundle (`<arcDir>/server/_server.js`) side-effect-
49
+ // registers every module via the platform singleton when imported. The
50
+ // worker imports just this entry; Bun resolves its `chunk-<hash>.js` siblings
51
+ // and external peers (`@arcote.tech/platform` etc.) by walking node_modules
52
+ // from the bundle's directory.
53
+ const serverBundles = existsSync(serverBundlePath)
54
+ ? [{ name: "server", path: serverBundlePath }]
55
+ : [];
60
56
 
61
57
  // Worker must live INSIDE the workspace tree so Bun's module resolver can
62
58
  // walk up to <rootDir>/node_modules and find @arcote.tech/platform via the
@@ -18,7 +18,7 @@ import {
18
18
  type BuildCache,
19
19
  } from "./build-cache";
20
20
  import type { ChunkPlan } from "./chunk-planner";
21
- import { SHELL_EXTERNALS } from "./framework-peers";
21
+ import { SHELL_EXTERNALS, FRAMEWORK_PEERS } from "./framework-peers";
22
22
  import {
23
23
  readInstalledVersion,
24
24
  sha256Hex,
@@ -125,6 +125,43 @@ function serverExternalsPlugin(): import("bun").BunPlugin {
125
125
  };
126
126
  }
127
127
 
128
+ /**
129
+ * SERVER bundles inline their workspace (`@ndt/*`) deps so each flattened
130
+ * `.arc/platform/server/<pkg>.js` is self-contained (the deploy image has no
131
+ * `node_modules/@ndt/*` tree). Bun would resolve a bare `@ndt/x` import through
132
+ * package.json `exports` to that dep's PRE-BUILT `dist/server/main/index.js` —
133
+ * an already-fully-inlined bundle. Nesting one fully-inlined bundle inside
134
+ * every consumer is catastrophic:
135
+ * - diamonds duplicate the shared dep per graph edge (2^depth copies),
136
+ * - dist/ is never wiped, so stale bombs from a previous dep graph keep
137
+ * getting re-inlined across rebuilds.
138
+ * Result: bundles ballooned into the hundreds-of-MB / GB range (content =
139
+ * 1.1 GB, sizes lining up as powers of two by graph depth).
140
+ *
141
+ * This plugin pins every workspace package name to its SOURCE entrypoint
142
+ * (`src/index.ts`). The single Bun.build pass then includes each transitive
143
+ * module exactly once (Bun dedups by resolved path), so a server bundle is the
144
+ * size of its UNIQUE source closure — no nesting, no stale re-inlining.
145
+ *
146
+ * `sideEffects: true` keeps the dep's top-level `module(...).build()`
147
+ * registration from being tree-shaken when it's imported only for a named
148
+ * symbol (workspace packages ship `"sideEffects": false`).
149
+ */
150
+ function workspaceSourcePlugin(srcByName: Map<string, string>): import("bun").BunPlugin {
151
+ return {
152
+ name: "workspace-source",
153
+ setup(build) {
154
+ // Bare specifiers only (skip relative `./` and absolute `/`). Non-
155
+ // workspace bare imports (react, @arcote.tech/*, npm deps) miss the map
156
+ // and fall through to the `external` list unchanged.
157
+ build.onResolve({ filter: /^[^./]/ }, (args) => {
158
+ const src = srcByName.get(args.path);
159
+ return src ? { path: src, sideEffects: true } : null;
160
+ });
161
+ },
162
+ };
163
+ }
164
+
128
165
  function jsxDevShimPlugin(): import("bun").BunPlugin {
129
166
  return {
130
167
  name: "jsx-dev-runtime-shim",
@@ -145,12 +182,25 @@ export { Fragment };
145
182
  };
146
183
  }
147
184
 
148
- /** Clients that a context package is built for. */
185
+ // Per-package context build is BROWSER-only. The server side is produced by a
186
+ // single combined `buildServerApp` pass (see below) — bundling each package's
187
+ // server context separately and inlining its deps' pre-built dist caused
188
+ // catastrophic nested duplication (content.js hit 1.1 GB) and turned the
189
+ // consumer's undeclared cyclic `@ndt/*` imports into broken init order. One
190
+ // shared pass dedups every module once and is cycle-safe (same as the browser
191
+ // app build).
149
192
  const CONTEXT_CLIENTS = [
150
- { name: "server", target: "bun" as const, defines: { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } },
151
193
  { name: "browser", target: "browser" as const, defines: { ONLY_SERVER: "false", ONLY_BROWSER: "true", ONLY_CLIENT: "true" } },
152
194
  ];
153
195
 
196
+ /** Server-side build defines — applied by the combined `buildServerApp` pass. */
197
+ const SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } as const;
198
+
199
+ /** Filename of the combined server bundle entry under `<arcDir>/server/`.
200
+ * loadServerContext + the access extractor import exactly this file; Bun's
201
+ * auto-emitted `chunk-<hash>.js` siblings are pulled in transitively. */
202
+ export const SERVER_ENTRY_FILE = "_server.js";
203
+
154
204
  export interface WorkspacePackage {
155
205
  name: string;
156
206
  path: string;
@@ -305,32 +355,16 @@ async function buildContextClient(
305
355
 
306
356
  console.log(` building: ${pkg.name} (${client.name})`);
307
357
 
308
- // Externals depend on the target client:
309
- //
310
- // - BROWSER client: workspace deps MUST be external. Inlining them per
311
- // package would make every context package's dist ship its own copy
312
- // of every workspace dep the top-level browser Bun.build would see
313
- // N pre-inlined copies of context singletons (`WorkspaceContext =
314
- // createContext`) and could not dedupe them, breaking provider lookup.
315
- //
316
- // - SERVER client: workspace deps MUST be bundled inline. The deploy
317
- // image flattens each package's server bundle to
318
- // `.arc/platform/server/<pkg>.js` and runs them via a single
319
- // `loadServerContext()` import loop. There is no `node_modules/@ndt/*`
320
- // tree inside the image, so bare `@ndt/workspace` specifiers would
321
- // fail to resolve at startup. Inline duplication is harmless on the
322
- // server: it's a single Node/Bun process and Arc modules register via
323
- // a shared platform registry singleton (registry.ts), so two physical
324
- // copies of the workspace module still merge into one context.
358
+ // BROWSER client: every dep (workspace + npm + framework peers) is external.
359
+ // Inlining workspace deps per package would make each context package's dist
360
+ // ship its own copy of every workspace dep the top-level browser Bun.build
361
+ // (`buildBrowserApp`) would then see N pre-inlined copies of context
362
+ // singletons (`WorkspaceContext = createContext`) and could not dedupe them,
363
+ // breaking provider lookup. The server side is handled separately by the
364
+ // combined `buildServerApp` pass.
325
365
  const peerDeps = Object.keys(pkg.packageJson.peerDependencies ?? {});
326
366
  const allDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
327
- const isBrowser = client.name === "browser";
328
- const workspaceDeps = isBrowser
329
- ? Object.keys(allDeps)
330
- : Object.entries(allDeps)
331
- .filter(([, spec]) => !spec.startsWith("workspace:"))
332
- .map(([name]) => name);
333
- const externals = [...peerDeps, ...workspaceDeps];
367
+ const externals = [...peerDeps, ...Object.keys(allDeps)];
334
368
 
335
369
  const result = await Bun.build({
336
370
  entrypoints: [pkg.entrypoint],
@@ -339,9 +373,7 @@ async function buildContextClient(
339
373
  format: "esm",
340
374
  naming: "index.[ext]",
341
375
  external: externals,
342
- plugins: isBrowser
343
- ? [jsxDevShimPlugin()]
344
- : [jsxDevShimPlugin(), serverExternalsPlugin()],
376
+ plugins: [jsxDevShimPlugin()],
345
377
  define: client.defines,
346
378
  });
347
379
 
@@ -385,11 +417,13 @@ export async function buildContextPackages(
385
417
  const contexts = packages.filter((p) => isContextPackage(p.packageJson));
386
418
  if (contexts.length === 0) return { declarationErrors: [] };
387
419
 
388
- // Topological order each package's server bundle inlines its workspace
389
- // deps, so those deps must have their `dist/` ready before Bun.build tries
390
- // to resolve them. Without this, a fresh checkout (no dist/ yet) fails the
391
- // first build because content's resolve of @ndt/strategy hits a missing
392
- // file. Inside a topological level, packages are built in parallel.
420
+ // Topological order by declared `workspace:` deps. The browser BUNDLE
421
+ // externalizes every dep (order-independent), but the type-declaration pass
422
+ // (tsc) resolves `@ndt/x` types through that package's emitted
423
+ // `dist/browser/index.d.ts` so a dep's declarations must exist before a
424
+ // dependent's decl build runs, else tsc reports "Cannot find module @ndt/x".
425
+ // The declared dep graph is acyclic; undeclared cyclic imports live in source
426
+ // only and don't affect this ordering. Inside a layer, packages build in parallel.
393
427
  const byName = new Map(contexts.map((p) => [p.name, p]));
394
428
  const remaining = new Set(contexts.map((p) => p.name));
395
429
  const done = new Set<string>();
@@ -440,6 +474,147 @@ export async function buildContextPackages(
440
474
  return { declarationErrors };
441
475
  }
442
476
 
477
+ // ---------------------------------------------------------------------------
478
+ // Combined server bundle — ONE Bun.build for ALL context packages' server side.
479
+ //
480
+ // Replaces N per-package server builds that each inlined their deps' PRE-BUILT
481
+ // dist (`@ndt/x` → `x/dist/server/main/index.js`). That nested an already-
482
+ // fully-inlined bundle inside every consumer: shared deps duplicated per graph
483
+ // edge (content's server bundle reached 1.1 GB) and the consumer's undeclared
484
+ // cyclic `@ndt/*` imports turned into broken module-init order.
485
+ //
486
+ // One shared pass with `splitting: true` — the same recipe as buildBrowserApp:
487
+ // - every module is bundled from SOURCE exactly once (Bun dedups by path),
488
+ // - `@ndt/*` workspace deps resolve to their `src/` entry (workspaceSourcePlugin)
489
+ // so nothing pulls a pre-built dist,
490
+ // - npm deps + framework peers stay external — the deploy image installs them
491
+ // into /app/node_modules via `.arc/platform/package.json` (collectFrameworkDeps),
492
+ // - splitting makes Bun emit eager top-level init, which is cycle-safe; the
493
+ // old non-split per-package build emitted lazy `__esm` wrappers that broke
494
+ // under the consumer's import cycles (`workspace.ids` was undefined).
495
+ //
496
+ // Output under `<arcDir>/server/`:
497
+ // _server.js ← entry: side-effect-imports every context module
498
+ // chunk-<hash>.js × N ← auto-shared module chunks
499
+ // loadServerContext + the access extractor import `_server.js`; chunks ride along.
500
+ // ---------------------------------------------------------------------------
501
+
502
+ export interface ServerAppResult {
503
+ readonly entryFile: string;
504
+ readonly cached: boolean;
505
+ }
506
+
507
+ export async function buildServerApp(
508
+ rootDir: string,
509
+ serverDir: string,
510
+ packages: WorkspacePackage[],
511
+ cache: BuildCache,
512
+ noCache: boolean,
513
+ ): Promise<ServerAppResult> {
514
+ void rootDir; // resolution is source-based; rootDir kept for signature parity
515
+ const contexts = packages.filter((p) => isContextPackage(p.packageJson));
516
+ mkdirSync(serverDir, { recursive: true });
517
+
518
+ // Every workspace package name → its SOURCE entry. Spans ALL packages, not
519
+ // just context ones — a context package imports non-context workspace libs
520
+ // (e.g. content-core) that must inline from source too.
521
+ const srcByName = new Map(packages.map((p) => [p.name, p.entrypoint]));
522
+
523
+ // External = framework peers + every non-workspace npm dep any package
524
+ // declares. NOT bundled; resolved at runtime from /app/node_modules (the
525
+ // deploy image installs exactly this set — collectFrameworkDeps). Only the
526
+ // `@ndt/*` workspace source is inlined.
527
+ const externalSet = new Set<string>(FRAMEWORK_PEERS);
528
+ for (const p of packages) {
529
+ for (const name of Object.keys(p.packageJson.peerDependencies ?? {})) {
530
+ externalSet.add(name);
531
+ }
532
+ for (const [name, spec] of Object.entries(
533
+ (p.packageJson.dependencies ?? {}) as Record<string, string>,
534
+ )) {
535
+ if (!spec.startsWith("workspace:")) externalSet.add(name);
536
+ }
537
+ }
538
+ const external = [...externalSet];
539
+
540
+ const unitId = "server-app";
541
+ const inputHash = sha256OfJson({
542
+ members: packages
543
+ .map((p) => ({ name: p.name, src: pkgSourceHash(p) }))
544
+ .sort((a, b) => a.name.localeCompare(b.name)),
545
+ contexts: contexts.map((p) => p.name).sort(),
546
+ external: [...external].sort(),
547
+ defines: SERVER_DEFINES,
548
+ });
549
+
550
+ const entryFileAbs = join(serverDir, SERVER_ENTRY_FILE);
551
+ if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
552
+ console.log(` ✓ cached: ${unitId}`);
553
+ return { entryFile: SERVER_ENTRY_FILE, cached: true };
554
+ }
555
+
556
+ console.log(` building: ${unitId} (${contexts.length} server modules)`);
557
+
558
+ // Wipe stale .js — old per-package flattened bundles AND previous chunks —
559
+ // so a smaller rebuild never leaves orphaned content-addressed chunks behind.
560
+ for (const f of readdirSync(serverDir)) {
561
+ if (f.endsWith(".js")) rmSync(join(serverDir, f), { force: true });
562
+ }
563
+
564
+ // Entry side-effect-imports every context module so each registers via the
565
+ // platform registry singleton. Written to a tmp subdir so it isn't shipped.
566
+ const tmpDir = join(serverDir, "_entries");
567
+ mkdirSync(tmpDir, { recursive: true });
568
+ const entrySrc = join(tmpDir, SERVER_ENTRY_FILE.replace(/\.js$/, ".ts"));
569
+ writeFileSync(
570
+ entrySrc,
571
+ contexts.map((p) => `import "${p.name}";`).join("\n") + "\n",
572
+ );
573
+
574
+ let result;
575
+ try {
576
+ result = await Bun.build({
577
+ entrypoints: [entrySrc],
578
+ outdir: serverDir,
579
+ target: "bun",
580
+ format: "esm",
581
+ // splitting:true is the whole point — it makes Bun emit eager top-level
582
+ // init (cycle-safe) instead of lazy `__esm` wrappers. Shared modules land
583
+ // in chunk-<hash>.js referenced by the entry.
584
+ splitting: true,
585
+ naming: "[name].[ext]",
586
+ external,
587
+ plugins: [
588
+ jsxDevShimPlugin(),
589
+ serverExternalsPlugin(),
590
+ workspaceSourcePlugin(srcByName),
591
+ ],
592
+ define: SERVER_DEFINES,
593
+ });
594
+ } finally {
595
+ rmSync(tmpDir, { recursive: true, force: true });
596
+ }
597
+
598
+ if (!result.success) {
599
+ for (const log of result.logs) console.error(log);
600
+ throw new Error("Server app build failed");
601
+ }
602
+
603
+ const entryOut = result.outputs.find((o) => o.kind === "entry-point");
604
+ if (!entryOut) {
605
+ throw new Error("Server app build: entry not found in outputs");
606
+ }
607
+ if (basename(entryOut.path) !== SERVER_ENTRY_FILE) {
608
+ throw new Error(
609
+ `Server app build: unexpected entry name ${basename(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`,
610
+ );
611
+ }
612
+
613
+ const outputHash = sha256OfDir(serverDir);
614
+ updateCache(cache, unitId, inputHash, { outputHash });
615
+ return { entryFile: SERVER_ENTRY_FILE, cached: false };
616
+ }
617
+
443
618
 
444
619
  // ---------------------------------------------------------------------------
445
620
  // Browser app build — one Bun.build with multiple entrypoints.
@@ -87,8 +87,13 @@ ${envNames.map((name) => ` - "https://${cfg.envs[name]!.domain}"`).jo
87
87
 
88
88
  # Per-container CPU / memory / network / block-IO + restarts straight from
89
89
  # the Docker daemon (socket bind-mounted read-only, see compose).
90
+ # api_version pinned: the receiver defaults to Docker API 1.25, which modern
91
+ # daemons (Engine 25+ require >= 1.40) reject — without this the receiver
92
+ # fails to start and takes the whole collector down. Quoted so YAML doesn't
93
+ # parse 1.40 → 1.4. Must be <= the daemon's max; 1.40 is the safe floor.
90
94
  docker_stats:
91
95
  endpoint: unix:///var/run/docker.sock
96
+ api_version: "1.40"
92
97
  collection_interval: 30s
93
98
  metrics:
94
99
  container.restarts:
@@ -372,6 +377,15 @@ export function generateAlloyConfig(): string {
372
377
  discovery.docker "containers" {
373
378
  host = "unix:///var/run/docker.sock"
374
379
  refresh_interval = "15s"
380
+
381
+ // Only containers managed by a compose project (our stack). Ad-hoc / rogue
382
+ // containers (manual debug runs, other stacks) are excluded — one bad
383
+ // stream (e.g. log entries older than Loki's reject window) otherwise 400s
384
+ // the whole loki.write batch and drops good app logs with it.
385
+ filter {
386
+ name = "label"
387
+ values = ["com.docker.compose.project"]
388
+ }
375
389
  }
376
390
 
377
391
  discovery.relabel "containers" {
@@ -4,10 +4,12 @@ import { dirname, join } from "path";
4
4
  import {
5
5
  buildBrowserApp,
6
6
  buildContextPackages,
7
+ buildServerApp,
7
8
  buildStyles,
8
9
  buildTranslations,
9
10
  discoverPackages,
10
11
  isContextPackage,
12
+ SERVER_ENTRY_FILE,
11
13
  type BrowserAppResult,
12
14
  type BuildManifest,
13
15
  type ModuleDescriptor,
@@ -161,20 +163,30 @@ export async function buildAll(
161
163
  // else chunk grouping (per-package) silently mis-assigns the extra module.
162
164
  assertOneModulePerPackage(ws.packages);
163
165
 
164
- // Phase 1 — context packages must finish FIRST. The access-extractor
165
- // subprocess imports workspace packages by name, which resolve through
166
- // node_modules to packages' `main` field (typically `dist/server/main/`).
166
+ // Phase 1 — per-package BROWSER context bundles + type declarations.
167
167
  await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
168
168
 
169
- // Phase 1b — relocate per-package server bundles into `.arc/platform/server/`
170
- // so the deploy image can be self-contained (image COPY needs everything
171
- // server-side under one root). loadServerContext reads from here in prod.
172
- copyContextServerBundles(ws);
169
+ // Phase 1b — combined server bundle at `.arc/platform/server/_server.js`.
170
+ // ONE Bun.build for all context modules (deduped from source, cycle-safe)
171
+ // replaces the old per-package server bundles that nested each other's dist
172
+ // into multi-hundred-MB files. The deploy image COPYs this dir wholesale;
173
+ // loadServerContext + the access extractor import the entry, chunks ride along.
174
+ const serverDir = join(ws.arcDir, "server");
175
+ const { entryFile: serverEntry } = await buildServerApp(
176
+ ws.rootDir,
177
+ serverDir,
178
+ ws.packages,
179
+ cache,
180
+ noCache,
181
+ );
173
182
 
174
183
  // Phase 2 — extract access metadata (token name + hasCheck per module) in
175
- // an isolated subprocess. This MUST run before chunk planning so we know
176
- // which token group each module belongs to.
177
- const accessMap = await extractAccessMap(ws.rootDir, ws.packages);
184
+ // an isolated subprocess that imports the combined server bundle. MUST run
185
+ // before chunk planning so we know which token group each module belongs to.
186
+ const accessMap = await extractAccessMap(
187
+ ws.rootDir,
188
+ join(serverDir, serverEntry),
189
+ );
178
190
 
179
191
  // Persist access map for the runtime host (server.ts reads at startup to
180
192
  // wire up moduleAccessMap for filterManifestForTokens / signed URLs).
@@ -242,34 +254,6 @@ function assembleManifest(
242
254
  };
243
255
  }
244
256
 
245
- // ---------------------------------------------------------------------------
246
- // Context server bundles — flatten to `<arcDir>/server/<safeName>.js`
247
- // ---------------------------------------------------------------------------
248
-
249
- /**
250
- * Copy each context package's compiled server bundle from
251
- * `packages/<pkg>/dist/server/main/index.js` to a flat location at
252
- * `<arcDir>/server/<safeName>.js`. The flat layout makes the deploy image
253
- * self-contained — `COPY .arc/platform/` pulls everything server-side, no
254
- * need to drag the entire `packages/` tree into the image.
255
- */
256
- function copyContextServerBundles(ws: WorkspaceInfo): void {
257
- const outDir = join(ws.arcDir, "server");
258
- mkdirSync(outDir, { recursive: true });
259
-
260
- for (const pkg of ws.packages) {
261
- if (!isContextPackage(pkg.packageJson)) continue;
262
- const src = join(pkg.path, "dist", "server", "main", "index.js");
263
- if (!existsSync(src)) {
264
- err(`Server bundle missing for ${pkg.name}: ${src}`);
265
- continue;
266
- }
267
- const safeName = pkg.path.split("/").pop()!;
268
- const dst = join(outDir, `${safeName}.js`);
269
- copyFileSync(src, dst);
270
- }
271
- }
272
-
273
257
  // ---------------------------------------------------------------------------
274
258
  // Browser assets — @arcote.tech/* deps deklarują w `arc.browserAssets` jakie
275
259
  // pliki muszą być dostępne w przeglądarce (np. SQLite WASM worker + .wasm).
@@ -419,44 +403,21 @@ export async function loadServerContext(
419
403
 
420
404
  await import(platformEntry);
421
405
 
422
- // Primary source: flattened server bundles at `<arcDir>/server/<safeName>.js`.
406
+ // The combined server bundle lives at `<arcDir>/server/_server.js` (entry)
407
+ // next to its `chunk-<hash>.js` siblings. Importing the entry pulls the
408
+ // chunks transitively and registers every module via the platform singleton.
423
409
  // The deploy image only has this directory — there's no workspace `packages/`
424
- // tree. In dev, `copyContextServerBundles` populates this same location, so
425
- // both modes go through the same code path.
426
- const serverDir = join(ws.arcDir, "server");
427
- const bundles = existsSync(serverDir)
428
- ? readdirSync(serverDir).filter((f) => f.endsWith(".js"))
429
- : [];
430
-
431
- if (bundles.length > 0) {
432
- for (const file of bundles) {
433
- const bundlePath = join(serverDir, file);
434
- try {
435
- await import(bundlePath);
436
- } catch (e) {
437
- err(`Failed to load server bundle ${file}: ${e}`);
438
- }
439
- }
440
- } else if (ws.packages.length > 0) {
441
- // Fallback for the "no .arc/platform/server/ yet" case (e.g. somebody
442
- // wired up loadServerContext before running the build). This path goes
443
- // through workspace packages directly — only meaningful in dev.
444
- const ctxPackages = ws.packages.filter((p) => isContextPackage(p.packageJson));
445
- for (const ctx of ctxPackages) {
446
- const serverDist = join(ctx.path, "dist", "server", "main", "index.js");
447
- if (!existsSync(serverDist)) {
448
- err(`Context server dist not found: ${serverDist}`);
449
- continue;
450
- }
451
- try {
452
- await import(serverDist);
453
- } catch (e) {
454
- err(`Failed to load server context from ${ctx.name}: ${e}`);
455
- }
456
- }
457
- } else {
410
+ // tree; dev and prod go through the exact same path.
411
+ const serverEntry = join(ws.arcDir, "server", SERVER_ENTRY_FILE);
412
+ if (!existsSync(serverEntry)) {
413
+ // No build yet (or a static-only workspace) — nothing to register.
458
414
  return { context: null, moduleAccess: new Map() };
459
415
  }
416
+ try {
417
+ await import(serverEntry);
418
+ } catch (e) {
419
+ err(`Failed to load server bundle ${SERVER_ENTRY_FILE}: ${e}`);
420
+ }
460
421
 
461
422
  const { getContext, getAllModuleAccess } = await import(platformEntry);
462
423
  return {
@@ -62,8 +62,8 @@ export async function startPlatform(
62
62
  }
63
63
 
64
64
  // 2. Server context — same code path in both modes; loadServerContext
65
- // prefers .arc/platform/server/*.js (the canonical location after the
66
- // copyContextServerBundles step in buildAll).
65
+ // imports the combined server bundle at .arc/platform/server/_server.js
66
+ // (produced by the buildServerApp step in buildAll).
67
67
  log("Loading server context...");
68
68
  const { context, moduleAccess } = await loadServerContext(ws);
69
69
  if (context) {