@arcote.tech/arc-cli 0.7.26 → 0.7.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.7.26",
3
+ "version": "0.7.28",
4
4
  "description": "CLI tool for Arc framework",
5
5
  "module": "index.ts",
6
6
  "main": "dist/index.js",
@@ -9,16 +9,16 @@
9
9
  "arc": "./dist/index.js"
10
10
  },
11
11
  "scripts": {
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"
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 @arcote.tech/arc-map --external '@opentelemetry/*' && chmod +x dist/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@arcote.tech/arc": "^0.7.26",
16
- "@arcote.tech/arc-ds": "^0.7.26",
17
- "@arcote.tech/arc-react": "^0.7.26",
18
- "@arcote.tech/arc-host": "^0.7.26",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.7.26",
20
- "@arcote.tech/arc-adapter-db-postgres": "^0.7.26",
21
- "@arcote.tech/arc-otel": "^0.7.26",
15
+ "@arcote.tech/arc": "^0.7.28",
16
+ "@arcote.tech/arc-ds": "^0.7.28",
17
+ "@arcote.tech/arc-react": "^0.7.28",
18
+ "@arcote.tech/arc-host": "^0.7.28",
19
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.28",
20
+ "@arcote.tech/arc-adapter-db-postgres": "^0.7.28",
21
+ "@arcote.tech/arc-otel": "^0.7.28",
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,8 @@
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.26",
34
+ "@arcote.tech/platform": "^0.7.28",
35
+ "@arcote.tech/arc-map": "^0.7.28",
35
36
  "@clack/prompts": "^0.9.0",
36
37
  "commander": "^11.1.0",
37
38
  "chokidar": "^3.5.3",
@@ -3,12 +3,11 @@ import { resolveWorkspace } from "../platform/shared";
3
3
 
4
4
  /** `arc platform dev` — dev mode (watcher + SSE reload + no-cache headers). */
5
5
  export async function platformDev(
6
- opts: { noCache?: boolean } = {},
6
+ opts: { noCache?: boolean; map?: boolean } = {},
7
7
  ): Promise<void> {
8
8
  const ws = resolveWorkspace();
9
9
  // noCache is consumed by buildAll inside startPlatform when devMode=true.
10
10
  // For now the dev startup always uses the cache after the first build;
11
11
  // explicit --no-cache here only matters if we wire it through later.
12
- void opts;
13
- await startPlatform({ ws, devMode: true });
12
+ await startPlatform({ ws, devMode: true, map: opts.map });
14
13
  }
package/src/index.ts CHANGED
@@ -34,8 +34,9 @@ platform
34
34
  .command("dev")
35
35
  .description("Start platform in dev mode (Bun server + Vite HMR)")
36
36
  .option("--no-cache", "Force full rebuild on startup")
37
- .action((opts: { cache?: boolean }) =>
38
- platformDev({ noCache: opts.cache === false }),
37
+ .option("--map", "Also start the Arc Context Map dev tool on a separate port")
38
+ .action((opts: { cache?: boolean; map?: boolean }) =>
39
+ platformDev({ noCache: opts.cache === false, map: opts.map }),
39
40
  );
40
41
 
41
42
  platform
@@ -134,8 +134,8 @@ export function generateShellHtml(
134
134
  <html lang="en">
135
135
  <head>
136
136
  <meta charset="UTF-8" />
137
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
138
- <title>${manifest?.title ?? appName}</title>${manifest?.favicon ? `\n <link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `\n <link rel="manifest" href="/manifest.json">` : ""}
137
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
138
+ <title>${manifest?.title ?? appName}</title>${manifest?.favicon ? `\n <link rel="icon" href="${manifest.favicon}">` : ""}${manifest ? `\n <link rel="manifest" href="/manifest.json${stylesQs}">` : ""}
139
139
  <link rel="stylesheet" href="/styles.css${stylesQs}" />
140
140
  <link rel="stylesheet" href="/theme.css${stylesQs}" />
141
141
  <link rel="modulepreload" href="${initialUrl}" />${otelTag}
@@ -403,9 +403,15 @@ function staticFilesHandler(
403
403
  "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
404
404
  });
405
405
 
406
- // Serve manifest.json from root dir
406
+ // Serve manifest.json from root dir. Stable URL, content changes between
407
+ // builds — HTML references it with `?v=<stylesHash>` (generateShellHtml),
408
+ // so the URL changes on rebuild and immutable caching is safe. Handler
409
+ // ignores the query string itself (same pattern as /styles.css).
407
410
  if ((path === "/manifest.json" || path === "/manifest.webmanifest") && ws.manifest) {
408
- return serveFile(ws.manifest.path, ctx.corsHeaders);
411
+ return serveFile(ws.manifest.path, {
412
+ ...ctx.corsHeaders,
413
+ "Cache-Control": devMode ? "no-cache" : "max-age=31536000,immutable",
414
+ });
409
415
  }
410
416
 
411
417
  // Public files (files with extensions)
@@ -436,7 +442,18 @@ function apiEndpointsHandler(
436
442
  payloads = [ctx.tokenPayload];
437
443
  }
438
444
  return filterManifestForTokens(getManifest(), moduleAccessMap, payloads)
439
- .then((filtered) => Response.json(filtered, { headers: ctx.corsHeaders }));
445
+ .then((filtered) =>
446
+ Response.json(filtered, {
447
+ headers: {
448
+ ...ctx.corsHeaders,
449
+ // Treść zależy od tokenu (filterManifestForTokens) i zmienia się
450
+ // co deploy. Nie jest artefaktem buildu — `private`, by pośredni
451
+ // cache nie podał manifestu jednego usera drugiemu, `no-cache`,
452
+ // by po deployu klient dostał świeże hashe/podpisy chunków.
453
+ "Cache-Control": "no-cache, private",
454
+ },
455
+ }),
456
+ );
440
457
  }
441
458
 
442
459
  if (url.pathname === "/api/translations") {
@@ -501,7 +518,15 @@ function devReloadHandler(
501
518
  function spaFallbackHandler(getShellHtml: () => string): ArcHttpHandler {
502
519
  return (_req, _url, ctx) => {
503
520
  return new Response(getShellHtml(), {
504
- headers: { ...ctx.corsHeaders, "Content-Type": "text/html" },
521
+ headers: {
522
+ ...ctx.corsHeaders,
523
+ "Content-Type": "text/html",
524
+ // Stabilny URL, zmienna treść (embeduje initial.<hash>.js + styles ?v=).
525
+ // Kotwica całego łańcucha — musi być zawsze rewalidowana, inaczej po
526
+ // deployu przeglądarka/CDN podaje stary shell wskazujący na nieistniejące
527
+ // już bundle. Hash nie pomoże: to URL trasy, którego nie da się zahashować.
528
+ "Cache-Control": "no-cache, must-revalidate",
529
+ },
505
530
  });
506
531
  };
507
532
  }
@@ -35,6 +35,8 @@ export interface StartPlatformOptions {
35
35
  dbPath?: string;
36
36
  /** Dev mode: rebuild on startup, watch for changes, SSE-notify clients on rebuild. */
37
37
  devMode: boolean;
38
+ /** Dev-only: also start the Arc Context Map tool on a separate port. */
39
+ map?: boolean;
38
40
  }
39
41
 
40
42
  export async function startPlatform(
@@ -111,9 +113,16 @@ export async function startPlatform(
111
113
  ok("Commands, queries, WebSocket — all on same port");
112
114
  }
113
115
 
116
+ // 4a. Dev-only: optional Arc Context Map tool on a separate port, starting
117
+ // just above the platform's actual port (auto-increments if busy).
118
+ let onReload: (() => void) | undefined;
119
+ if (devMode && opts.map) {
120
+ onReload = await startMapTool(ws, actualPort + 1);
121
+ }
122
+
114
123
  // 4. Dev-only: file watcher + debounced rebuild + SSE notify.
115
124
  if (devMode) {
116
- attachDevWatcher(ws, platform);
125
+ attachDevWatcher(ws, platform, onReload);
117
126
  }
118
127
 
119
128
  // 5. Graceful shutdown for both modes.
@@ -125,7 +134,42 @@ export async function startPlatform(
125
134
  process.on("SIGINT", cleanup);
126
135
  }
127
136
 
128
- function attachDevWatcher(ws: WorkspaceInfo, platform: PlatformServer): void {
137
+ /**
138
+ * Start the Arc Context Map dev tool on a separate port. Loaded lazily so the
139
+ * ts-morph dependency stays out of the normal `platform dev` path. Returns a
140
+ * reload hook to wire into the file watcher, or undefined on failure.
141
+ */
142
+ async function startMapTool(
143
+ ws: WorkspaceInfo,
144
+ port: number,
145
+ ): Promise<(() => void) | undefined> {
146
+ try {
147
+ const { startMapServer } = await import("@arcote.tech/arc-map");
148
+ const { getContext } = await import("@arcote.tech/platform");
149
+ const globs = ws.packages
150
+ .map((p) => join(p.path, "src", "**", "*.{ts,tsx}"));
151
+ const packageOf = (absFile: string): string | undefined =>
152
+ ws.packages.find((p) => absFile.startsWith(join(p.path, "src")))?.name;
153
+
154
+ const map = await startMapServer({
155
+ getContext: () => getContext(),
156
+ scan: { globs, workspaceRoot: ws.rootDir, packageOf },
157
+ useCasePackages: ws.packages.map((p) => ({ name: p.name, path: p.path })),
158
+ port,
159
+ });
160
+ ok(`Context map → ${map.url}`);
161
+ return map.notifyReload;
162
+ } catch (e) {
163
+ err(`Context map failed to start: ${(e as Error).message}`);
164
+ return undefined;
165
+ }
166
+ }
167
+
168
+ function attachDevWatcher(
169
+ ws: WorkspaceInfo,
170
+ platform: PlatformServer,
171
+ onReload?: () => void,
172
+ ): void {
129
173
  log("Watching for changes...");
130
174
  let rebuildTimer: ReturnType<typeof setTimeout> | null = null;
131
175
  let isRebuilding = false;
@@ -140,6 +184,7 @@ function attachDevWatcher(ws: WorkspaceInfo, platform: PlatformServer): void {
140
184
  const next = await buildAll(ws);
141
185
  platform.setManifest(next);
142
186
  platform.notifyReload(next);
187
+ onReload?.();
143
188
  ok(
144
189
  `Rebuilt — initial + ${Object.keys(next.groups).length} group(s)`,
145
190
  );
@@ -1,5 +1,13 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "fs";
3
11
  import { dirname, join } from "path";
4
12
  import type { ArcConfig, ContextInfo } from "./config";
5
13
  import { generateClientTypes } from "./config";
@@ -152,6 +160,37 @@ export interface DeclarationResult {
152
160
  * @param globalsContent Optional ambient declarations (ONLY_SERVER etc.) —
153
161
  * written to a temp .d.ts and included in compilation.
154
162
  */
163
+ /**
164
+ * Emituje deklaracje do katalogu TYMCZASOWEGO (siostra `outDir`), a po sukcesie
165
+ * scala je do `outDir`. Zapobiega TS5055 ("Cannot write file … would overwrite
166
+ * input file") gdy SAMO-REFERENCYJNY cykl importów pakietu (np. workspace →
167
+ * billing → workspace) wciąga własny `outDir/*.d.ts` do programu jako input —
168
+ * wtedy emisja wprost do `outDir` kolidowałaby z tym plikiem-inputem. Program ma
169
+ * `outDir` = temp, więc nie pisze po pliku czytanym jako wejście; świeże
170
+ * deklaracje przenosimy na miejsce dopiero po emisji.
171
+ */
172
+ async function runDeclEmit(
173
+ files: string[],
174
+ outDir: string,
175
+ cwd: string,
176
+ ): Promise<DeclarationResult> {
177
+ const tmpOut = `${outDir}.decltmp`;
178
+ rmSync(tmpOut, { recursive: true, force: true });
179
+ try {
180
+ let result = await runTsgo(files, tmpOut, cwd);
181
+ if (result === null) {
182
+ result = await runTsc(files, tmpOut, cwd);
183
+ }
184
+ if (result.success) {
185
+ mkdirSync(outDir, { recursive: true });
186
+ cpSync(tmpOut, outDir, { recursive: true });
187
+ }
188
+ return result;
189
+ } finally {
190
+ rmSync(tmpOut, { recursive: true, force: true });
191
+ }
192
+ }
193
+
155
194
  export async function buildTypeDeclarations(
156
195
  files: string[],
157
196
  outDir: string,
@@ -170,10 +209,7 @@ export async function buildTypeDeclarations(
170
209
  allFiles.push(globalsPath);
171
210
  }
172
211
 
173
- let result = await runTsgo(allFiles, outDir, rootDir);
174
- if (result === null) {
175
- result = await runTsc(allFiles, outDir, rootDir);
176
- }
212
+ const result = await runDeclEmit(allFiles, outDir, rootDir);
177
213
 
178
214
  // Clean up temp globals
179
215
  if (globalsPath && existsSync(globalsPath)) {
@@ -204,11 +240,7 @@ export async function buildContextDeclarations(
204
240
  ? [context.fullPath, arcTypesPath]
205
241
  : [context.fullPath];
206
242
 
207
- let result = await runTsgo(files, outDir, configDir);
208
- if (result === null) {
209
- result = await runTsc(files, outDir, configDir);
210
- }
211
- return result;
243
+ return runDeclEmit(files, outDir, configDir);
212
244
  }
213
245
 
214
246
  // ---------------------------------------------------------------------------