@arcote.tech/arc-cli 0.8.2 → 0.8.4

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.
@@ -24,6 +24,15 @@ export type RemoteState =
24
24
  export interface RemoteStateMarker {
25
25
  cliVersion: string;
26
26
  configHash: string;
27
+ /**
28
+ * sha256 wyrenderowanych artefaktów stacku (Caddyfile + docker-compose).
29
+ * Wykrywa zmianę GENERATORA w obrębie tej samej wersji CLI — bez tego
30
+ * republish tej samej wersji z poprawionym Caddyfile (np. dodane `encode`)
31
+ * nie odświeżał configu na hoście (needUpStack patrzył tylko na cliVersion
32
+ * i configHash). Opcjonalny: starsze markery go nie mają → traktowane jako
33
+ * zmiana (wymusza jednorazowy upStack, po którym marker dostaje hash).
34
+ */
35
+ artifactsHash?: string;
27
36
  updatedAt: string;
28
37
  }
29
38
 
package/src/index.ts CHANGED
@@ -52,8 +52,12 @@ platform
52
52
  .command("build")
53
53
  .description("Build platform for production")
54
54
  .option("--no-cache", "Force full rebuild")
55
- .action((opts: { cache?: boolean }) =>
56
- platformBuild({ noCache: opts.cache === false }),
55
+ .option(
56
+ "--stats",
57
+ "Wypisz rozbicie chunków na biblioteki (per-wejście raw/gzip + najcięższe liby, ext/wewn). Wymusza pełny build.",
58
+ )
59
+ .action((opts: { cache?: boolean; stats?: boolean }) =>
60
+ platformBuild({ noCache: opts.cache === false, stats: opts.stats }),
57
61
  );
58
62
 
59
63
  platform
@@ -25,7 +25,11 @@ import {
25
25
  import { extractAccessMap } from "../builder/access-extractor";
26
26
  import { buildPeerShell, type PeerShellResult } from "../builder/peer-shell";
27
27
  import type { FederationConfig } from "@arcote.tech/platform";
28
- import { assertOneModulePerPackage, planChunks } from "../builder/chunk-planner";
28
+ import {
29
+ assertOneModulePerPackage,
30
+ packageDeclaresModule,
31
+ planChunks,
32
+ } from "../builder/chunk-planner";
29
33
  import { collectFrameworkDeps } from "../builder/dependency-collector";
30
34
  import {
31
35
  mtimeOf,
@@ -144,6 +148,11 @@ export function resolveWorkspace(): WorkspaceInfo {
144
148
 
145
149
  export interface BuildOptions {
146
150
  noCache?: boolean;
151
+ /**
152
+ * Minifikacja bundli przeglądarki (domyślnie true — build/deploy).
153
+ * Dev watcher podaje false: czytelny kod w devtools, szybszy rebuild.
154
+ */
155
+ minify?: boolean;
147
156
  }
148
157
 
149
158
  /**
@@ -157,6 +166,7 @@ export async function buildAll(
157
166
  ): Promise<BuildManifest> {
158
167
  const cache = loadBuildCache(ws.arcDir);
159
168
  const noCache = opts.noCache ?? false;
169
+ const minify = opts.minify ?? true;
160
170
  const themePath = ws.rootPkg.arc?.theme as string | undefined;
161
171
  const federation = getFederationConfig(ws);
162
172
 
@@ -201,13 +211,20 @@ export async function buildAll(
201
211
  const moduleNameOf = (name: string) => name.split("/").pop() ?? name;
202
212
  const exposureOf = (pkgName: string): "local" | "external" | "both" =>
203
213
  accessMap.exposure[moduleNameOf(pkgName)]?.exposure ?? "local";
214
+ // Tylko pakiety DEKLARUJĄCE moduł Arc są członami wchodzącymi do initial/grup.
215
+ // Czyste biblioteki (np. @ndt/email — szablony maili) NIE mogą być tu, bo
216
+ // initial robiłoby `import "@pkg"` i przy `sideEffects:true` (patch buildu)
217
+ // wciągałoby całe ich drzewo zależności do pierwszego wczytania. Taka libka
218
+ // trafia do bundla NORMALNIE — dopiero gdy realnie zaimportuje ją moduł
219
+ // (wtedy podlega tree-shakingowi / lazy-split).
220
+ const modulePackages = ws.packages.filter(packageDeclaresModule);
204
221
  // Normalna platforma: moduły local/both (external-only ukryte w macierzystej
205
222
  // platformie). Host bierze wszystko (nie ukrywa niczego przed sobą).
206
223
  const localPackages = isHost
207
- ? ws.packages
208
- : ws.packages.filter((p) => exposureOf(p.name) !== "external");
224
+ ? modulePackages
225
+ : modulePackages.filter((p) => exposureOf(p.name) !== "external");
209
226
  // Build federowany: moduły external/both → browser-fed/.
210
- const fedPackages = ws.packages.filter((p) =>
227
+ const fedPackages = modulePackages.filter((p) =>
211
228
  ["external", "both"].includes(exposureOf(p.name)),
212
229
  );
213
230
  const hasFederatedModules = fedPackages.length > 0;
@@ -238,31 +255,47 @@ export async function buildAll(
238
255
  // zawsze external peers) + peer-shell.
239
256
  const i18nCollector = new Map<string, Set<string>>();
240
257
 
241
- const [browserResult, , , , shellResult, fedResult] = await Promise.all([
258
+ // Dwa buildy przeglądarkowe (bundled + federated) biegną SZEREGOWO, nie w
259
+ // Promise.all: oba mutują TE SAME package.json (patch sideEffects na czas
260
+ // builda) — przy równoległym biegu `finally` szybszego builda przywraca
261
+ // oryginał, zanim wolniejszy skończy bundlować, i tree-shaking wolniejszego
262
+ // biegnie na złych metadanych. Koszt: +czas builda fed; zysk: deterministyka.
263
+ const browserSequence = (async () => {
242
264
  // HOST buduje się BUNDLED (React inline, eager — bez lazy-race), a initial
243
265
  // eksponuje runtime na globalThis; gościa obsłuży peer-shell (globalThis).
244
- buildBrowserApp(ws.rootDir, ws.browserDir, plan, cache, noCache, i18nCollector, {
245
- federated: false,
246
- exposeRuntime: isHost,
247
- }),
248
- buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
249
- copyBrowserAssets(ws, cache, noCache),
250
- buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
251
- needsPeerShell
252
- ? buildPeerShell(ws.rootDir, join(ws.arcDir, "shell"), cache, noCache)
253
- : Promise.resolve(undefined),
254
- fedPlan
255
- ? buildBrowserApp(
266
+ const bundled = await buildBrowserApp(
267
+ ws.rootDir,
268
+ ws.browserDir,
269
+ plan,
270
+ cache,
271
+ noCache,
272
+ i18nCollector,
273
+ { federated: false, exposeRuntime: isHost, minify },
274
+ );
275
+ const fed = fedPlan
276
+ ? await buildBrowserApp(
256
277
  ws.rootDir,
257
278
  join(ws.arcDir, "browser-fed"),
258
279
  fedPlan,
259
280
  cache,
260
281
  noCache,
261
282
  i18nCollector,
262
- { federated: true, unitId: "browser-app-fed" },
283
+ { federated: true, unitId: "browser-app-fed", minify },
263
284
  )
264
- : Promise.resolve(undefined),
265
- ]);
285
+ : undefined;
286
+ return { bundled, fed };
287
+ })();
288
+
289
+ const [{ bundled: browserResult, fed: fedResult }, , , , shellResult] =
290
+ await Promise.all([
291
+ browserSequence,
292
+ buildStyles(ws.rootDir, ws.arcDir, ws.packages, themePath, cache, noCache),
293
+ copyBrowserAssets(ws, cache, noCache),
294
+ buildTranslations(ws.rootDir, ws.arcDir, cache, noCache),
295
+ needsPeerShell
296
+ ? buildPeerShell(ws.rootDir, join(ws.arcDir, "shell"), cache, noCache)
297
+ : Promise.resolve(undefined),
298
+ ]);
266
299
 
267
300
  // Finalize i18n catalogs once after all chunks + initial bundle collected.
268
301
  const { finalizeTranslations } = await import("../i18n");
@@ -493,7 +526,18 @@ async function copyBrowserAssets(
493
526
 
494
527
  export async function loadServerContext(
495
528
  ws: WorkspaceInfo,
496
- ): Promise<{ context: any | null; moduleAccess: Map<string, any> }> {
529
+ ): Promise<{
530
+ context: any | null;
531
+ moduleAccess: Map<string, any>;
532
+ /**
533
+ * Set gdy import bundla serwerowego RZUCIŁ (np. fail-fast na brakującym env).
534
+ * Kontekst jest wtedy CZĘŚCIOWY — część modułów się nie zarejestrowała, więc
535
+ * ich query rzucają `Element not found`. Caller uruchamiający serwer MUSI to
536
+ * potraktować jako twardy błąd startu (patrz startup.ts), inaczej aplikacja
537
+ * wstaje połowiczna i cicho. Build/access-extractor może to zignorować.
538
+ */
539
+ serverError?: unknown;
540
+ }> {
497
541
  // Set globals for server context — framework packages (arc-auth etc.)
498
542
  // use these at runtime to tree-shake browser/server code paths.
499
543
  (globalThis as any).ONLY_SERVER = true;
@@ -519,15 +563,18 @@ export async function loadServerContext(
519
563
  // No build yet (or a static-only workspace) — nothing to register.
520
564
  return { context: null, moduleAccess: new Map() };
521
565
  }
566
+ let serverError: unknown;
522
567
  try {
523
568
  await import(serverEntry);
524
569
  } catch (e) {
525
570
  err(`Failed to load server bundle ${SERVER_ENTRY_FILE}: ${e}`);
571
+ serverError = e;
526
572
  }
527
573
 
528
574
  const { getContext, getAllModuleAccess } = await import(platformEntry);
529
575
  return {
530
576
  context: getContext() ?? null,
531
577
  moduleAccess: getAllModuleAccess(),
578
+ serverError,
532
579
  };
533
580
  }
@@ -60,7 +60,7 @@ export async function startPlatform(
60
60
  // missing manifest is a hard error.
61
61
  let manifest: BuildManifest;
62
62
  if (devMode) {
63
- manifest = await buildAll(ws);
63
+ manifest = await buildAll(ws, { minify: false });
64
64
  } else {
65
65
  const manifestPath = join(ws.arcDir, "manifest.json");
66
66
  if (!existsSync(manifestPath)) {
@@ -88,7 +88,21 @@ export async function startPlatform(
88
88
  // imports the combined server bundle at .arc/platform/server/_server.js
89
89
  // (produced by the buildServerApp step in buildAll).
90
90
  log("Loading server context...");
91
- const { context, moduleAccess } = await loadServerContext(ws);
91
+ const { context, moduleAccess, serverError } = await loadServerContext(ws);
92
+ // TWARDY STOP: import bundla serwerowego rzucił (np. fail-fast na brakującym
93
+ // env). Kontekst jest wtedy CZĘŚCIOWY — część agregatów się nie
94
+ // zarejestrowała, więc ich query rzucałyby `Element not found`, a aplikacja
95
+ // wyglądałaby na działającą (login OK), tyle że połowiczną. Przerywamy start
96
+ // GŁOŚNO, zamiast wstawać po cichu — to była realna produkcyjna pułapka.
97
+ if (serverError) {
98
+ err(
99
+ "Serwer NIE wstanie: moduł serwerowy nie załadował się (błąd wyżej). " +
100
+ "Częściowy kontekst = połowiczna aplikacja (query rzucają " +
101
+ "'Element not found'). Napraw przyczynę (np. brakujące zmienne " +
102
+ "środowiskowe) i wystartuj ponownie.",
103
+ );
104
+ process.exit(1);
105
+ }
92
106
  if (context) {
93
107
  ok("Context loaded");
94
108
  } else {
@@ -381,7 +395,7 @@ function attachDevWatcher(
381
395
  isRebuilding = true;
382
396
  log("Rebuilding...");
383
397
  try {
384
- const next = await buildAll(ws);
398
+ const next = await buildAll(ws, { minify: false });
385
399
  platform.setManifest(next);
386
400
  platform.notifyReload(next);
387
401
  onReload?.();