@arcote.tech/arc-cli 0.7.24 → 0.7.26
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 +94 -32
- package/package.json +9 -9
- package/src/builder/dependency-collector.ts +59 -32
- package/src/builder/module-builder.ts +144 -23
- package/src/platform/shared.ts +3 -8
package/dist/index.js
CHANGED
|
@@ -34512,6 +34512,7 @@ import {
|
|
|
34512
34512
|
writeFileSync as writeFileSync6
|
|
34513
34513
|
} from "fs";
|
|
34514
34514
|
import { basename as basename2, dirname as dirname6, join as join8, relative as relative3 } from "path";
|
|
34515
|
+
import { builtinModules } from "module";
|
|
34515
34516
|
init_i18n();
|
|
34516
34517
|
init_compile();
|
|
34517
34518
|
|
|
@@ -34712,14 +34713,54 @@ function singleReactPlugin(rootDir) {
|
|
|
34712
34713
|
}
|
|
34713
34714
|
};
|
|
34714
34715
|
}
|
|
34715
|
-
|
|
34716
|
+
var BUILTIN_SET = new Set(builtinModules);
|
|
34717
|
+
function pkgNameOf(spec) {
|
|
34718
|
+
if (spec.startsWith("@"))
|
|
34719
|
+
return spec.split("/").slice(0, 2).join("/");
|
|
34720
|
+
return spec.split("/")[0];
|
|
34721
|
+
}
|
|
34722
|
+
function isBuiltinSpec(spec) {
|
|
34723
|
+
if (spec.startsWith("node:") || spec.startsWith("bun:") || spec === "bun")
|
|
34724
|
+
return true;
|
|
34725
|
+
return BUILTIN_SET.has(spec) || BUILTIN_SET.has(pkgNameOf(spec));
|
|
34726
|
+
}
|
|
34727
|
+
function versionFromImporter(importer, name) {
|
|
34728
|
+
let dir = importer ? dirname6(importer) : "";
|
|
34729
|
+
while (dir && dir !== dirname6(dir)) {
|
|
34730
|
+
const pj = join8(dir, "package.json");
|
|
34731
|
+
if (existsSync7(pj)) {
|
|
34732
|
+
try {
|
|
34733
|
+
const json = JSON.parse(readFileSync7(pj, "utf-8"));
|
|
34734
|
+
const spec = json.dependencies?.[name] ?? json.peerDependencies?.[name] ?? json.devDependencies?.[name];
|
|
34735
|
+
if (typeof spec === "string")
|
|
34736
|
+
return spec;
|
|
34737
|
+
} catch {}
|
|
34738
|
+
}
|
|
34739
|
+
dir = dirname6(dir);
|
|
34740
|
+
}
|
|
34741
|
+
return;
|
|
34742
|
+
}
|
|
34743
|
+
function npmExternalsPlugin(workspaceNames, recorded) {
|
|
34716
34744
|
return {
|
|
34717
|
-
name: "
|
|
34745
|
+
name: "npm-externals",
|
|
34718
34746
|
setup(build2) {
|
|
34719
|
-
build2.onResolve({ filter: /^
|
|
34720
|
-
|
|
34721
|
-
|
|
34722
|
-
|
|
34747
|
+
build2.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
34748
|
+
if (isBuiltinSpec(args.path)) {
|
|
34749
|
+
return { path: args.path, external: true };
|
|
34750
|
+
}
|
|
34751
|
+
const name = pkgNameOf(args.path);
|
|
34752
|
+
if (workspaceNames.has(name))
|
|
34753
|
+
return null;
|
|
34754
|
+
if (name.startsWith("@arcote.tech/") && !FRAMEWORK_PEER_SET.has(name)) {
|
|
34755
|
+
return null;
|
|
34756
|
+
}
|
|
34757
|
+
const version = versionFromImporter(args.importer, name);
|
|
34758
|
+
if (version?.startsWith("workspace:"))
|
|
34759
|
+
return null;
|
|
34760
|
+
if (!recorded.has(name))
|
|
34761
|
+
recorded.set(name, version);
|
|
34762
|
+
return { path: args.path, external: true };
|
|
34763
|
+
});
|
|
34723
34764
|
}
|
|
34724
34765
|
};
|
|
34725
34766
|
}
|
|
@@ -34758,6 +34799,7 @@ var CONTEXT_CLIENTS = [
|
|
|
34758
34799
|
];
|
|
34759
34800
|
var SERVER_DEFINES = { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" };
|
|
34760
34801
|
var SERVER_ENTRY_FILE = "_server.js";
|
|
34802
|
+
var SERVER_EXTERNALS_FILE = "_externals.json";
|
|
34761
34803
|
function discoverPackages(rootDir) {
|
|
34762
34804
|
const rootPkg = JSON.parse(readFileSync7(join8(rootDir, "package.json"), "utf-8"));
|
|
34763
34805
|
const workspaceGlobs = rootPkg.workspaces ?? [];
|
|
@@ -34944,9 +34986,14 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
|
|
|
34944
34986
|
defines: SERVER_DEFINES
|
|
34945
34987
|
});
|
|
34946
34988
|
const entryFileAbs = join8(serverDir, SERVER_ENTRY_FILE);
|
|
34989
|
+
const externalsFileAbs = join8(serverDir, SERVER_EXTERNALS_FILE);
|
|
34947
34990
|
if (!noCache && isCacheHit(cache, unitId, inputHash, [entryFileAbs])) {
|
|
34948
34991
|
console.log(` \u2713 cached: ${unitId}`);
|
|
34949
|
-
return {
|
|
34992
|
+
return {
|
|
34993
|
+
entryFile: SERVER_ENTRY_FILE,
|
|
34994
|
+
cached: true,
|
|
34995
|
+
externals: readServerExternals(externalsFileAbs)
|
|
34996
|
+
};
|
|
34950
34997
|
}
|
|
34951
34998
|
console.log(` building: ${unitId} (${contexts.length} server modules)`);
|
|
34952
34999
|
for (const f of readdirSync4(serverDir)) {
|
|
@@ -34959,6 +35006,7 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
|
|
|
34959
35006
|
writeFileSync6(entrySrc, contexts.map((p) => `import "${p.name}";`).join(`
|
|
34960
35007
|
`) + `
|
|
34961
35008
|
`);
|
|
35009
|
+
const recorded = new Map;
|
|
34962
35010
|
let result;
|
|
34963
35011
|
try {
|
|
34964
35012
|
result = await Bun.build({
|
|
@@ -34971,8 +35019,8 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
|
|
|
34971
35019
|
external,
|
|
34972
35020
|
plugins: [
|
|
34973
35021
|
jsxDevShimPlugin(),
|
|
34974
|
-
|
|
34975
|
-
|
|
35022
|
+
workspaceSourcePlugin(srcByName),
|
|
35023
|
+
npmExternalsPlugin(new Set(srcByName.keys()), recorded)
|
|
34976
35024
|
],
|
|
34977
35025
|
define: SERVER_DEFINES
|
|
34978
35026
|
});
|
|
@@ -34991,9 +35039,22 @@ async function buildServerApp(rootDir, serverDir, packages, cache, noCache) {
|
|
|
34991
35039
|
if (basename2(entryOut.path) !== SERVER_ENTRY_FILE) {
|
|
34992
35040
|
throw new Error(`Server app build: unexpected entry name ${basename2(entryOut.path)} (wanted ${SERVER_ENTRY_FILE})`);
|
|
34993
35041
|
}
|
|
35042
|
+
const externals = [...recorded].filter(([name]) => !FRAMEWORK_PEER_SET.has(name)).map(([name, version]) => ({ name, version })).sort((a, b) => a.name.localeCompare(b.name));
|
|
35043
|
+
writeFileSync6(externalsFileAbs, JSON.stringify(externals, null, 2) + `
|
|
35044
|
+
`);
|
|
34994
35045
|
const outputHash = sha256OfDir(serverDir);
|
|
34995
35046
|
updateCache(cache, unitId, inputHash, { outputHash });
|
|
34996
|
-
return { entryFile: SERVER_ENTRY_FILE, cached: false };
|
|
35047
|
+
return { entryFile: SERVER_ENTRY_FILE, cached: false, externals };
|
|
35048
|
+
}
|
|
35049
|
+
function readServerExternals(path4) {
|
|
35050
|
+
if (!existsSync7(path4))
|
|
35051
|
+
return [];
|
|
35052
|
+
try {
|
|
35053
|
+
const parsed = JSON.parse(readFileSync7(path4, "utf-8"));
|
|
35054
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
35055
|
+
} catch {
|
|
35056
|
+
return [];
|
|
35057
|
+
}
|
|
34997
35058
|
}
|
|
34998
35059
|
async function buildBrowserApp(rootDir, outDir, plan, cache, noCache, i18nCollector) {
|
|
34999
35060
|
mkdirSync6(outDir, { recursive: true });
|
|
@@ -35476,9 +35537,10 @@ import { createHash } from "crypto";
|
|
|
35476
35537
|
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
35477
35538
|
import { basename as basename4, dirname as dirname7, join as join11 } from "path";
|
|
35478
35539
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
35479
|
-
function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = []) {
|
|
35540
|
+
function collectFrameworkDeps(arcDir, rootDir, packages, sharedDeps = [], serverExternals = []) {
|
|
35480
35541
|
mkdirSync8(arcDir, { recursive: true });
|
|
35481
35542
|
const versions = resolveFrameworkVersions(rootDir, packages);
|
|
35543
|
+
mergeServerExternals(versions, rootDir, packages, serverExternals);
|
|
35482
35544
|
for (const { name, version } of sharedDeps) {
|
|
35483
35545
|
versions[name] = version;
|
|
35484
35546
|
}
|
|
@@ -35529,18 +35591,10 @@ function resolveFrameworkVersions(rootDir, packages) {
|
|
|
35529
35591
|
const required = new Set(FRAMEWORK_PEERS);
|
|
35530
35592
|
for (const pkg of packages) {
|
|
35531
35593
|
const peers = pkg.packageJson.peerDependencies ?? {};
|
|
35532
|
-
const deps = pkg.packageJson.dependencies ?? {};
|
|
35533
35594
|
for (const name of Object.keys(peers)) {
|
|
35534
35595
|
if (name.startsWith("@arcote.tech/"))
|
|
35535
35596
|
required.add(name);
|
|
35536
35597
|
}
|
|
35537
|
-
for (const [name, spec] of Object.entries(deps)) {
|
|
35538
|
-
if (spec.startsWith("workspace:"))
|
|
35539
|
-
continue;
|
|
35540
|
-
if (name.startsWith("@arcote.tech/") || !isFrameworkExternal(name))
|
|
35541
|
-
continue;
|
|
35542
|
-
required.add(name);
|
|
35543
|
-
}
|
|
35544
35598
|
}
|
|
35545
35599
|
const out = {};
|
|
35546
35600
|
for (const name of required) {
|
|
@@ -35549,20 +35603,28 @@ function resolveFrameworkVersions(rootDir, packages) {
|
|
|
35549
35603
|
out[name] = rootSpec;
|
|
35550
35604
|
continue;
|
|
35551
35605
|
}
|
|
35552
|
-
|
|
35553
|
-
for (const pkg of packages) {
|
|
35554
|
-
const spec = (pkg.packageJson.dependencies ?? {})[name] ?? (pkg.packageJson.peerDependencies ?? {})[name];
|
|
35555
|
-
if (spec) {
|
|
35556
|
-
found = spec;
|
|
35557
|
-
break;
|
|
35558
|
-
}
|
|
35559
|
-
}
|
|
35560
|
-
out[name] = found ?? "*";
|
|
35606
|
+
out[name] = findWorkspaceSpec(packages, name) ?? "*";
|
|
35561
35607
|
}
|
|
35562
35608
|
return out;
|
|
35563
35609
|
}
|
|
35564
|
-
function
|
|
35565
|
-
|
|
35610
|
+
function findWorkspaceSpec(packages, name) {
|
|
35611
|
+
for (const pkg of packages) {
|
|
35612
|
+
const spec = (pkg.packageJson.dependencies ?? {})[name] ?? (pkg.packageJson.peerDependencies ?? {})[name];
|
|
35613
|
+
if (spec)
|
|
35614
|
+
return spec;
|
|
35615
|
+
}
|
|
35616
|
+
return;
|
|
35617
|
+
}
|
|
35618
|
+
function mergeServerExternals(versions, rootDir, packages, externals) {
|
|
35619
|
+
const rootPkg = JSON.parse(readFileSync10(join11(rootDir, "package.json"), "utf-8"));
|
|
35620
|
+
const rootDeps = rootPkg.dependencies ?? {};
|
|
35621
|
+
const rootDevDeps = rootPkg.devDependencies ?? {};
|
|
35622
|
+
for (const { name, version } of externals) {
|
|
35623
|
+
if (versions[name])
|
|
35624
|
+
continue;
|
|
35625
|
+
const installed = readInstalledVersion(rootDir, name);
|
|
35626
|
+
versions[name] = rootDeps[name] ?? rootDevDeps[name] ?? version ?? findWorkspaceSpec(packages, name) ?? (installed ? `^${installed}` : "*");
|
|
35627
|
+
}
|
|
35566
35628
|
}
|
|
35567
35629
|
|
|
35568
35630
|
// src/platform/shared.ts
|
|
@@ -35631,7 +35693,7 @@ async function buildAll(ws, opts = {}) {
|
|
|
35631
35693
|
assertOneModulePerPackage(ws.packages);
|
|
35632
35694
|
await buildContextPackages(ws.rootDir, ws.packages, cache, noCache);
|
|
35633
35695
|
const serverDir = join12(ws.arcDir, "server");
|
|
35634
|
-
const { entryFile: serverEntry } = await buildServerApp(ws.rootDir, serverDir, ws.packages, cache, noCache);
|
|
35696
|
+
const { entryFile: serverEntry, externals: serverExternals } = await buildServerApp(ws.rootDir, serverDir, ws.packages, cache, noCache);
|
|
35635
35697
|
const accessMap = await extractAccessMap(ws.rootDir, join12(serverDir, serverEntry));
|
|
35636
35698
|
mkdirSync9(ws.arcDir, { recursive: true });
|
|
35637
35699
|
writeFileSync9(join12(ws.arcDir, "access.json"), JSON.stringify(accessMap, null, 2) + `
|
|
@@ -35647,7 +35709,7 @@ async function buildAll(ws, opts = {}) {
|
|
|
35647
35709
|
]);
|
|
35648
35710
|
const { finalizeTranslations: finalizeTranslations3 } = await Promise.resolve().then(() => (init_i18n(), exports_i18n));
|
|
35649
35711
|
await finalizeTranslations3(ws.rootDir, ws.arcDir, i18nCollector);
|
|
35650
|
-
collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages);
|
|
35712
|
+
collectFrameworkDeps(ws.arcDir, ws.rootDir, ws.packages, [], serverExternals);
|
|
35651
35713
|
saveBuildCache(ws.arcDir, cache);
|
|
35652
35714
|
const finalManifest = assembleManifest(ws, browserResult, cache);
|
|
35653
35715
|
writeFileSync9(join12(ws.arcDir, "manifest.json"), JSON.stringify(finalManifest, null, 2));
|
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.26",
|
|
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.
|
|
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.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",
|
|
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.
|
|
34
|
+
"@arcote.tech/platform": "^0.7.26",
|
|
35
35
|
"@clack/prompts": "^0.9.0",
|
|
36
36
|
"commander": "^11.1.0",
|
|
37
37
|
"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
|
|
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
|
|