@arcote.tech/arc-cli 0.7.25 → 0.7.27
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 +937 -4152
- package/package.json +11 -10
- package/src/builder/dependency-collector.ts +59 -32
- package/src/builder/module-builder.ts +144 -23
- package/src/commands/platform-dev.ts +2 -3
- package/src/index.ts +3 -2
- package/src/platform/shared.ts +3 -8
- package/src/platform/startup.ts +47 -2
- package/src/utils/build.ts +42 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.27",
|
|
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.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.7.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.7.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.
|
|
20
|
-
"@arcote.tech/arc-adapter-db-postgres": "^0.7.
|
|
21
|
-
"@arcote.tech/arc-otel": "^0.7.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.27",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.27",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.27",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.27",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.27",
|
|
20
|
+
"@arcote.tech/arc-adapter-db-postgres": "^0.7.27",
|
|
21
|
+
"@arcote.tech/arc-otel": "^0.7.27",
|
|
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.
|
|
34
|
+
"@arcote.tech/platform": "^0.7.27",
|
|
35
|
+
"@arcote.tech/arc-map": "^0.7.27",
|
|
35
36
|
"@clack/prompts": "^0.9.0",
|
|
36
37
|
"commander": "^11.1.0",
|
|
37
38
|
"chokidar": "^3.5.3",
|
|
@@ -3,7 +3,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { basename, dirname, join } from "path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { FRAMEWORK_PEERS, FRAMEWORK_PEER_SET } from "./framework-peers";
|
|
6
|
-
import
|
|
6
|
+
import { readInstalledVersion } from "./hash";
|
|
7
|
+
import type { ServerExternal, WorkspacePackage } from "./module-builder";
|
|
7
8
|
|
|
8
9
|
export { FRAMEWORK_PEERS };
|
|
9
10
|
|
|
@@ -37,10 +38,17 @@ export function collectFrameworkDeps(
|
|
|
37
38
|
rootDir: string,
|
|
38
39
|
packages: WorkspacePackage[],
|
|
39
40
|
sharedDeps: ReadonlyArray<{ name: string; version: string }> = [],
|
|
41
|
+
serverExternals: ReadonlyArray<ServerExternal> = [],
|
|
40
42
|
): CollectedDeps {
|
|
41
43
|
mkdirSync(arcDir, { recursive: true });
|
|
42
44
|
|
|
43
45
|
const versions = resolveFrameworkVersions(rootDir, packages);
|
|
46
|
+
// npm packages the server bundle left external — the runtime image must
|
|
47
|
+
// install them into /app/node_modules. This is the ONLY source for deps
|
|
48
|
+
// reached through a fragment the consumer never declares (e.g.
|
|
49
|
+
// `@arcote.tech/arc-files` → `@aws-sdk/client-s3`): the bundle records what
|
|
50
|
+
// it actually externalized, so the manifest tracks real usage, no whitelist.
|
|
51
|
+
mergeServerExternals(versions, rootDir, packages, serverExternals);
|
|
44
52
|
// Shared deps (discovered across ≥ 2 workspace packages) join the framework
|
|
45
53
|
// in the global manifest — installed once into <arcDir>/node_modules/ and
|
|
46
54
|
// served via the shell so each module's browser bundle leaves them external.
|
|
@@ -178,12 +186,13 @@ function resolveFrameworkVersions(
|
|
|
178
186
|
packages: WorkspacePackage[],
|
|
179
187
|
): Record<string, string> {
|
|
180
188
|
// The framework manifest installs into /app/node_modules in the deploy
|
|
181
|
-
// image.
|
|
189
|
+
// image. resolveFrameworkVersions covers the singleton @arcote.tech/* set:
|
|
182
190
|
// 1. Static framework peers (FRAMEWORK_PEERS) — always present
|
|
183
191
|
// 2. User-extension @arcote.tech/* peers (e.g. arc-ai-openai) declared
|
|
184
192
|
// as peerDeps on user context packages
|
|
185
|
-
//
|
|
186
|
-
//
|
|
193
|
+
// Plain npm runtime deps the server bundle leaves external are added
|
|
194
|
+
// separately by mergeServerExternals (driven by what the bundle actually
|
|
195
|
+
// externalized, not by what workspace packages happen to declare).
|
|
187
196
|
// Resolution order for each: root package.json wins, then any workspace
|
|
188
197
|
// package's deps/peerDeps, then "*" as last resort.
|
|
189
198
|
const rootPkg = JSON.parse(
|
|
@@ -196,18 +205,10 @@ function resolveFrameworkVersions(
|
|
|
196
205
|
const required = new Set<string>(FRAMEWORK_PEERS);
|
|
197
206
|
for (const pkg of packages) {
|
|
198
207
|
const peers = pkg.packageJson.peerDependencies ?? {};
|
|
199
|
-
const deps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
200
208
|
// Any @arcote.tech/* peer beyond the static list (e.g. arc-ai adapters).
|
|
201
209
|
for (const name of Object.keys(peers)) {
|
|
202
210
|
if (name.startsWith("@arcote.tech/")) required.add(name);
|
|
203
211
|
}
|
|
204
|
-
// npm deps (non-workspace) — server bundles leave them external, so the
|
|
205
|
-
// image needs them in /app/node_modules.
|
|
206
|
-
for (const [name, spec] of Object.entries(deps)) {
|
|
207
|
-
if (spec.startsWith("workspace:")) continue;
|
|
208
|
-
if (name.startsWith("@arcote.tech/") || !isFrameworkExternal(name)) continue;
|
|
209
|
-
required.add(name);
|
|
210
|
-
}
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
const out: Record<string, string> = {};
|
|
@@ -217,32 +218,58 @@ function resolveFrameworkVersions(
|
|
|
217
218
|
out[name] = rootSpec;
|
|
218
219
|
continue;
|
|
219
220
|
}
|
|
220
|
-
|
|
221
|
-
for (const pkg of packages) {
|
|
222
|
-
const spec =
|
|
223
|
-
(pkg.packageJson.dependencies ?? {})[name] ??
|
|
224
|
-
(pkg.packageJson.peerDependencies ?? {})[name];
|
|
225
|
-
if (spec) {
|
|
226
|
-
found = spec;
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
out[name] = found ?? "*";
|
|
221
|
+
out[name] = findWorkspaceSpec(packages, name) ?? "*";
|
|
231
222
|
}
|
|
232
223
|
return out;
|
|
233
224
|
}
|
|
234
225
|
|
|
226
|
+
/** Version spec for `name` declared by any workspace package, if any. */
|
|
227
|
+
function findWorkspaceSpec(
|
|
228
|
+
packages: WorkspacePackage[],
|
|
229
|
+
name: string,
|
|
230
|
+
): string | undefined {
|
|
231
|
+
for (const pkg of packages) {
|
|
232
|
+
const spec =
|
|
233
|
+
(pkg.packageJson.dependencies ?? {})[name] ??
|
|
234
|
+
(pkg.packageJson.peerDependencies ?? {})[name];
|
|
235
|
+
if (spec) return spec;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
235
240
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* the
|
|
241
|
+
* Add server-bundle externals to the install manifest. For each, resolve a
|
|
242
|
+
* version spec by cascade so the image installs something compatible even when
|
|
243
|
+
* the consumer never declared the dep:
|
|
244
|
+
* 1. consumer root deps/devDeps (their explicit pin — wins)
|
|
245
|
+
* 2. spec recorded from the importing package (e.g. arc-files peerDeps)
|
|
246
|
+
* 3. any workspace package's declaration
|
|
247
|
+
* 4. the version actually installed in node_modules (caret-ranged)
|
|
248
|
+
* 5. "*" as last resort
|
|
249
|
+
* Already-resolved names (framework peers) are left untouched.
|
|
243
250
|
*/
|
|
244
|
-
function
|
|
245
|
-
|
|
251
|
+
function mergeServerExternals(
|
|
252
|
+
versions: Record<string, string>,
|
|
253
|
+
rootDir: string,
|
|
254
|
+
packages: WorkspacePackage[],
|
|
255
|
+
externals: ReadonlyArray<ServerExternal>,
|
|
256
|
+
): void {
|
|
257
|
+
const rootPkg = JSON.parse(
|
|
258
|
+
readFileSync(join(rootDir, "package.json"), "utf-8"),
|
|
259
|
+
);
|
|
260
|
+
const rootDeps = (rootPkg.dependencies ?? {}) as Record<string, string>;
|
|
261
|
+
const rootDevDeps = (rootPkg.devDependencies ?? {}) as Record<string, string>;
|
|
262
|
+
|
|
263
|
+
for (const { name, version } of externals) {
|
|
264
|
+
if (versions[name]) continue;
|
|
265
|
+
const installed = readInstalledVersion(rootDir, name);
|
|
266
|
+
versions[name] =
|
|
267
|
+
rootDeps[name] ??
|
|
268
|
+
rootDevDeps[name] ??
|
|
269
|
+
version ??
|
|
270
|
+
findWorkspaceSpec(packages, name) ??
|
|
271
|
+
(installed ? `^${installed}` : "*");
|
|
272
|
+
}
|
|
246
273
|
}
|
|
247
274
|
|
|
248
275
|
function sha256OfFiles(paths: string[]): string {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from "fs";
|
|
10
10
|
import { basename, dirname, join, relative } from "path";
|
|
11
|
+
import { builtinModules } from "node:module";
|
|
11
12
|
import type { BuildManifest, ModuleDescriptor } from "@arcote.tech/platform";
|
|
12
13
|
import { buildTypeDeclarations } from "../utils/build";
|
|
13
14
|
import { i18nExtractPlugin, finalizeTranslations } from "../i18n";
|
|
@@ -18,7 +19,11 @@ import {
|
|
|
18
19
|
type BuildCache,
|
|
19
20
|
} from "./build-cache";
|
|
20
21
|
import type { ChunkPlan } from "./chunk-planner";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
SHELL_EXTERNALS,
|
|
24
|
+
FRAMEWORK_PEERS,
|
|
25
|
+
FRAMEWORK_PEER_SET,
|
|
26
|
+
} from "./framework-peers";
|
|
22
27
|
import {
|
|
23
28
|
readInstalledVersion,
|
|
24
29
|
sha256Hex,
|
|
@@ -97,30 +102,101 @@ function singleReactPlugin(rootDir: string): import("bun").BunPlugin {
|
|
|
97
102
|
};
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/** One external the server bundle leaves for the deploy image to install. */
|
|
106
|
+
export interface ServerExternal {
|
|
107
|
+
/** Package name (subpath stripped): `@aws-sdk/client-s3`, `react`, … */
|
|
108
|
+
readonly name: string;
|
|
109
|
+
/** Version spec read from the importing package's package.json, if found. */
|
|
110
|
+
readonly version?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const BUILTIN_SET = new Set<string>(builtinModules);
|
|
114
|
+
|
|
115
|
+
/** Package name from a bare specifier, dropping any subpath. */
|
|
116
|
+
function pkgNameOf(spec: string): string {
|
|
117
|
+
if (spec.startsWith("@")) return spec.split("/").slice(0, 2).join("/");
|
|
118
|
+
return spec.split("/")[0];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** node:/bun: builtins — external by `target: "bun"`, never installed. */
|
|
122
|
+
function isBuiltinSpec(spec: string): boolean {
|
|
123
|
+
if (spec.startsWith("node:") || spec.startsWith("bun:") || spec === "bun")
|
|
124
|
+
return true;
|
|
125
|
+
return BUILTIN_SET.has(spec) || BUILTIN_SET.has(pkgNameOf(spec));
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
* Best-effort version spec for `name` as declared by the package that imports
|
|
130
|
+
* it. Walks up from the importer to the nearest package.json that lists `name`
|
|
131
|
+
* in deps/peerDeps/devDeps — e.g. `@aws-sdk/client-s3` resolves to the
|
|
132
|
+
* `^3.x` spec in `@arcote.tech/arc-files`'s peerDependencies. The collector
|
|
133
|
+
* falls back to root deps / installed version when this returns undefined.
|
|
134
|
+
*/
|
|
135
|
+
function versionFromImporter(importer: string, name: string): string | undefined {
|
|
136
|
+
let dir = importer ? dirname(importer) : "";
|
|
137
|
+
while (dir && dir !== dirname(dir)) {
|
|
138
|
+
const pj = join(dir, "package.json");
|
|
139
|
+
if (existsSync(pj)) {
|
|
140
|
+
try {
|
|
141
|
+
const json = JSON.parse(readFileSync(pj, "utf-8"));
|
|
142
|
+
const spec =
|
|
143
|
+
json.dependencies?.[name] ??
|
|
144
|
+
json.peerDependencies?.[name] ??
|
|
145
|
+
json.devDependencies?.[name];
|
|
146
|
+
if (typeof spec === "string") return spec;
|
|
147
|
+
} catch {
|
|
148
|
+
// unreadable package.json — keep walking
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
dir = dirname(dir);
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Structural externalization for the combined server bundle: inline only
|
|
158
|
+
* first-party code (workspace `@ndt/*` source — handled earlier by
|
|
159
|
+
* `workspaceSourcePlugin` — plus non-peer `@arcote.tech/*` fragments like
|
|
160
|
+
* `arc-files`), and leave EVERY third-party npm package external. The deploy
|
|
161
|
+
* image installs externals into `/app/node_modules` anyway, so inlining them
|
|
162
|
+
* only bloats the bundle. Replaces the old hardcoded `@aws-sdk|@smithy` regex
|
|
163
|
+
* (a band-aid from the 1.1 GB nested-dist era, since fixed by the single
|
|
164
|
+
* source-based build) — no package-name whitelist to keep in sync.
|
|
108
165
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
166
|
+
* Records each externalized package + the version spec of the package that
|
|
167
|
+
* imports it, so `collectFrameworkDeps` can emit them into the install
|
|
168
|
+
* manifest. This is what closes the gap for deps reached through a fragment
|
|
169
|
+
* the consumer never declares (arc-files → aws-sdk).
|
|
112
170
|
*/
|
|
113
|
-
function
|
|
171
|
+
function npmExternalsPlugin(
|
|
172
|
+
workspaceNames: ReadonlySet<string>,
|
|
173
|
+
recorded: Map<string, string | undefined>,
|
|
174
|
+
): import("bun").BunPlugin {
|
|
114
175
|
return {
|
|
115
|
-
name: "
|
|
176
|
+
name: "npm-externals",
|
|
116
177
|
setup(build) {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
build.onResolve({ filter: /^
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
178
|
+
// Bare specifiers only. An exact workspace name (`@ndt/foo`) is resolved
|
|
179
|
+
// to source earlier by workspaceSourcePlugin and never reaches here; a
|
|
180
|
+
// workspace SUBPATH (`@ndt/foo/bar`) does, and must NOT be externalized.
|
|
181
|
+
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
182
|
+
if (isBuiltinSpec(args.path)) {
|
|
183
|
+
return { path: args.path, external: true };
|
|
184
|
+
}
|
|
185
|
+
const name = pkgNameOf(args.path);
|
|
186
|
+
// First-party workspace package (subpath import) — let Bun resolve and
|
|
187
|
+
// inline it; `workspace:*` is not installable from npm.
|
|
188
|
+
if (workspaceNames.has(name)) return null;
|
|
189
|
+
// First-party non-peer fragments inline from source/dist so
|
|
190
|
+
// SERVER_DEFINES (ONLY_SERVER…) apply and import cycles stay safe.
|
|
191
|
+
if (name.startsWith("@arcote.tech/") && !FRAMEWORK_PEER_SET.has(name)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const version = versionFromImporter(args.importer, name);
|
|
195
|
+
// Defensive: any `workspace:` dep is first-party — never externalize it.
|
|
196
|
+
if (version?.startsWith("workspace:")) return null;
|
|
197
|
+
if (!recorded.has(name)) recorded.set(name, version);
|
|
198
|
+
return { path: args.path, external: true };
|
|
199
|
+
});
|
|
124
200
|
},
|
|
125
201
|
};
|
|
126
202
|
}
|
|
@@ -201,6 +277,11 @@ const SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT
|
|
|
201
277
|
* auto-emitted `chunk-<hash>.js` siblings are pulled in transitively. */
|
|
202
278
|
export const SERVER_ENTRY_FILE = "_server.js";
|
|
203
279
|
|
|
280
|
+
/** Sidecar listing third-party packages the server bundle left external.
|
|
281
|
+
* Written next to the bundle so a cached rebuild can still feed the install
|
|
282
|
+
* manifest (collectFrameworkDeps) without re-running Bun.build. */
|
|
283
|
+
export const SERVER_EXTERNALS_FILE = "_externals.json";
|
|
284
|
+
|
|
204
285
|
export interface WorkspacePackage {
|
|
205
286
|
name: string;
|
|
206
287
|
path: string;
|
|
@@ -502,6 +583,12 @@ export async function buildContextPackages(
|
|
|
502
583
|
export interface ServerAppResult {
|
|
503
584
|
readonly entryFile: string;
|
|
504
585
|
readonly cached: boolean;
|
|
586
|
+
/**
|
|
587
|
+
* Third-party npm packages the bundle left external — the deploy image must
|
|
588
|
+
* install exactly these. Fed to `collectFrameworkDeps`. Empty on a cache hit
|
|
589
|
+
* (the manifest from the cached build is already on disk).
|
|
590
|
+
*/
|
|
591
|
+
readonly externals: ReadonlyArray<ServerExternal>;
|
|
505
592
|
}
|
|
506
593
|
|
|
507
594
|
export async function buildServerApp(
|
|
@@ -548,9 +635,17 @@ export async function buildServerApp(
|
|
|
548
635
|
});
|
|
549
636
|
|
|
550
637
|
const entryFileAbs = join(serverDir, SERVER_ENTRY_FILE);
|
|
638
|
+
const externalsFileAbs = join(serverDir, SERVER_EXTERNALS_FILE);
|
|
551
639
|
if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
|
|
552
640
|
console.log(` ✓ cached: ${unitId}`);
|
|
553
|
-
|
|
641
|
+
// collectFrameworkDeps runs every build (uncached) and needs the external
|
|
642
|
+
// set to build the install manifest — reload it from the sidecar written
|
|
643
|
+
// by the last real build so a cache hit doesn't drop deps from the manifest.
|
|
644
|
+
return {
|
|
645
|
+
entryFile: SERVER_ENTRY_FILE,
|
|
646
|
+
cached: true,
|
|
647
|
+
externals: readServerExternals(externalsFileAbs),
|
|
648
|
+
};
|
|
554
649
|
}
|
|
555
650
|
|
|
556
651
|
console.log(` building: ${unitId} (${contexts.length} server modules)`);
|
|
@@ -571,6 +666,10 @@ export async function buildServerApp(
|
|
|
571
666
|
contexts.map((p) => `import "${p.name}";`).join("\n") + "\n",
|
|
572
667
|
);
|
|
573
668
|
|
|
669
|
+
// npmExternalsPlugin records every third-party package it leaves external,
|
|
670
|
+
// plus the version spec of whatever imports it (best-effort).
|
|
671
|
+
const recorded = new Map<string, string | undefined>();
|
|
672
|
+
|
|
574
673
|
let result;
|
|
575
674
|
try {
|
|
576
675
|
result = await Bun.build({
|
|
@@ -586,8 +685,11 @@ export async function buildServerApp(
|
|
|
586
685
|
external,
|
|
587
686
|
plugins: [
|
|
588
687
|
jsxDevShimPlugin(),
|
|
589
|
-
|
|
688
|
+
// workspaceSourcePlugin MUST precede npmExternalsPlugin: the first
|
|
689
|
+
// onResolve to return a result wins, so `@ndt/*` source is pinned
|
|
690
|
+
// before the catch-all externalizer can mark it external.
|
|
590
691
|
workspaceSourcePlugin(srcByName),
|
|
692
|
+
npmExternalsPlugin(new Set(srcByName.keys()), recorded),
|
|
591
693
|
],
|
|
592
694
|
define: SERVER_DEFINES,
|
|
593
695
|
});
|
|
@@ -610,9 +712,28 @@ export async function buildServerApp(
|
|
|
610
712
|
);
|
|
611
713
|
}
|
|
612
714
|
|
|
715
|
+
// Third-party externals only — framework peers carry their own resolution in
|
|
716
|
+
// collectFrameworkDeps, so drop them here to avoid double-sourcing versions.
|
|
717
|
+
const externals: ServerExternal[] = [...recorded]
|
|
718
|
+
.filter(([name]) => !FRAMEWORK_PEER_SET.has(name))
|
|
719
|
+
.map(([name, version]) => ({ name, version }))
|
|
720
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
721
|
+
writeFileSync(externalsFileAbs, JSON.stringify(externals, null, 2) + "\n");
|
|
722
|
+
|
|
613
723
|
const outputHash = sha256OfDir(serverDir);
|
|
614
724
|
updateCache(cache, unitId, inputHash, { outputHash });
|
|
615
|
-
return { entryFile: SERVER_ENTRY_FILE, cached: false };
|
|
725
|
+
return { entryFile: SERVER_ENTRY_FILE, cached: false, externals };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Read the externals sidecar written by the last real server build. */
|
|
729
|
+
function readServerExternals(path: string): ServerExternal[] {
|
|
730
|
+
if (!existsSync(path)) return [];
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
733
|
+
return Array.isArray(parsed) ? (parsed as ServerExternal[]) : [];
|
|
734
|
+
} catch {
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
616
737
|
}
|
|
617
738
|
|
|
618
739
|
|
|
@@ -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
|
-
|
|
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
|
-
.
|
|
38
|
-
|
|
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
|
package/src/platform/shared.ts
CHANGED
|
@@ -172,13 +172,8 @@ export async function buildAll(
|
|
|
172
172
|
// into multi-hundred-MB files. The deploy image COPYs this dir wholesale;
|
|
173
173
|
// loadServerContext + the access extractor import the entry, chunks ride along.
|
|
174
174
|
const serverDir = join(ws.arcDir, "server");
|
|
175
|
-
const { entryFile: serverEntry } =
|
|
176
|
-
ws.rootDir,
|
|
177
|
-
serverDir,
|
|
178
|
-
ws.packages,
|
|
179
|
-
cache,
|
|
180
|
-
noCache,
|
|
181
|
-
);
|
|
175
|
+
const { entryFile: serverEntry, externals: serverExternals } =
|
|
176
|
+
await buildServerApp(ws.rootDir, serverDir, ws.packages, cache, noCache);
|
|
182
177
|
|
|
183
178
|
// Phase 2 — extract access metadata (token name + hasCheck per module) in
|
|
184
179
|
// an isolated subprocess that imports the combined server bundle. MUST run
|
|
@@ -223,7 +218,7 @@ export async function buildAll(
|
|
|
223
218
|
// Phase 5 — framework peer manifest at `<arcDir>/package.json`. Used by
|
|
224
219
|
// the deploy image build (single `bun install` for all peers, one copy
|
|
225
220
|
// shared across module bundles).
|
|
226
|
-
collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages);
|
|
221
|
+
collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages, [], serverExternals);
|
|
227
222
|
|
|
228
223
|
saveBuildCache(ws.arcDir, cache);
|
|
229
224
|
|
package/src/platform/startup.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
);
|
package/src/utils/build.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|