@arcote.tech/arc-cli 0.6.1 → 0.7.0
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 +1214 -1217
- package/package.json +7 -7
- package/src/builder/access-extractor.ts +79 -47
- package/src/builder/build-cache.ts +3 -1
- package/src/builder/chunk-planner.ts +107 -0
- package/src/builder/dependency-collector.ts +86 -32
- package/src/builder/framework-peers.ts +81 -0
- package/src/builder/module-builder.ts +186 -110
- package/src/commands/platform-deploy.ts +103 -55
- package/src/commands/platform-dev.ts +11 -100
- package/src/commands/platform-start.ts +4 -90
- package/src/deploy/bootstrap.ts +157 -6
- package/src/deploy/caddyfile.ts +19 -23
- package/src/deploy/compose.ts +43 -27
- package/src/deploy/config.ts +29 -0
- package/src/deploy/deploy-env.ts +129 -0
- package/src/deploy/htpasswd.ts +28 -0
- package/src/deploy/image-template.ts +74 -0
- package/src/deploy/image.ts +237 -0
- package/src/deploy/registry.ts +79 -0
- package/src/deploy/ssh.ts +5 -124
- package/src/deploy/survey.ts +64 -0
- package/src/index.ts +15 -13
- package/src/platform/server.ts +69 -44
- package/src/platform/shared.ts +124 -65
- package/src/platform/startup.ts +160 -0
- package/runtime/Dockerfile +0 -29
- package/runtime/build-and-push.sh +0 -23
- package/runtime/entrypoint.sh +0 -58
- package/src/commands/build-shell.ts +0 -152
- package/src/deploy/remote-sync.ts +0 -323
- package/src/platform/deploy-api.ts +0 -396
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,12 +12,12 @@
|
|
|
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 && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.
|
|
20
|
-
"@arcote.tech/platform": "^0.
|
|
15
|
+
"@arcote.tech/arc": "^0.7.0",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.7.0",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.7.0",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.7.0",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.7.0",
|
|
20
|
+
"@arcote.tech/platform": "^0.7.0",
|
|
21
21
|
"@clack/prompts": "^0.9.0",
|
|
22
22
|
"commander": "^11.1.0",
|
|
23
23
|
"chokidar": "^3.5.3",
|
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
import { spawn } from "bun";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "fs";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { isContextPackage, type WorkspacePackage } from "./module-builder";
|
|
13
|
+
|
|
14
|
+
// Locate platform's server entry by walking up from the CLI bundle location.
|
|
15
|
+
// `import.meta.url` at runtime points to the bundled `dist/index.js` (the CLI
|
|
16
|
+
// is shipped as a single Bun.build artifact), so the path is:
|
|
17
|
+
// <arcRoot>/packages/cli/dist/index.js
|
|
18
|
+
// Four `dirname` applications reach <arcRoot>. This works regardless of how
|
|
19
|
+
// the user invoked the CLI — symlink, npm install, or direct path — as long
|
|
20
|
+
// as the CLI binary lives at packages/cli/dist/ inside the arc workspace.
|
|
21
|
+
function locatePlatformServerEntry(): string {
|
|
22
|
+
const here = fileURLToPath(import.meta.url);
|
|
23
|
+
const arcRoot = realpathSync(dirname(dirname(dirname(dirname(here)))));
|
|
24
|
+
return join(arcRoot, "packages", "platform", "src", "index.server.ts");
|
|
25
|
+
}
|
|
7
26
|
|
|
8
27
|
// ---------------------------------------------------------------------------
|
|
9
|
-
// access-extractor —
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
28
|
+
// access-extractor — discovers per-module access rules (`protectedBy(...)`)
|
|
29
|
+
// from already-built server bundles, in an ISOLATED subprocess.
|
|
30
|
+
//
|
|
31
|
+
// Why subprocess: the global module registry in @arcote.tech/platform is a
|
|
32
|
+
// singleton. Importing user packages from the main CLI process would pollute
|
|
33
|
+
// the registry across builds.
|
|
34
|
+
//
|
|
35
|
+
// Why server bundles (not source): user package source files may import
|
|
36
|
+
// browser-only code at top level (React, JSX runtime, DOM globals). The
|
|
37
|
+
// subprocess is target=bun — those imports crash. Server bundles are built
|
|
38
|
+
// with ONLY_SERVER=true defines that tree-shake the browser branches, so
|
|
39
|
+
// they're safe to load in a bare Bun process.
|
|
40
|
+
//
|
|
41
|
+
// Constraint: only CONTEXT packages can have `protectedBy(...)` rules —
|
|
42
|
+
// non-context (pure-browser) packages are skipped. This is sensible: access
|
|
43
|
+
// checks logically belong with server state, not display components.
|
|
14
44
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// Runtime resolves the actual check via the loaded server bundle at request
|
|
18
|
-
// time.
|
|
45
|
+
// Order requirement: buildContextPackages MUST run before extractAccessMap
|
|
46
|
+
// — server bundles at `packages/<pkg>/dist/server/main/index.js` must exist.
|
|
19
47
|
// ---------------------------------------------------------------------------
|
|
20
48
|
|
|
21
49
|
export interface SerializedAccessRule {
|
|
@@ -30,37 +58,40 @@ export interface SerializedModuleAccess {
|
|
|
30
58
|
export type SerializedAccessMap = Record<string, SerializedModuleAccess>;
|
|
31
59
|
|
|
32
60
|
export async function extractAccessMap(
|
|
33
|
-
|
|
34
|
-
packages: WorkspacePackage[],
|
|
61
|
+
rootDir: string,
|
|
62
|
+
packages: readonly WorkspacePackage[],
|
|
35
63
|
): Promise<SerializedAccessMap> {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
64
|
+
// Each entry: a context-package server bundle path that the worker will
|
|
65
|
+
// dynamically import. Bun resolves the bundle's internal external imports
|
|
66
|
+
// (`@arcote.tech/platform` etc.) via node_modules walking from the bundle
|
|
67
|
+
// location — workspace symlinks point back to source, conditional exports
|
|
68
|
+
// pick the server entry.
|
|
40
69
|
const serverBundles = packages
|
|
41
70
|
.filter((p) => isContextPackage(p.packageJson))
|
|
42
|
-
.map((p) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
71
|
+
.map((p) => ({
|
|
72
|
+
name: p.name,
|
|
73
|
+
path: join(p.path, "dist", "server", "main", "index.js"),
|
|
74
|
+
}))
|
|
75
|
+
.filter((b) => existsSync(b.path));
|
|
76
|
+
|
|
77
|
+
// Worker must live INSIDE the workspace tree so Bun's module resolver can
|
|
78
|
+
// walk up to <rootDir>/node_modules and find @arcote.tech/platform/server.
|
|
79
|
+
// A tmpfile in /tmp/ would fail bare-specifier resolution.
|
|
80
|
+
const workerDir = join(rootDir, ".arc", ".tmp");
|
|
81
|
+
mkdirSync(workerDir, { recursive: true });
|
|
82
|
+
const workerPath = join(workerDir, `access-extractor-${Date.now()}.mjs`);
|
|
83
|
+
const outPath = join(workerDir, `access-${Date.now()}.json`);
|
|
55
84
|
writeFileSync(workerPath, WORKER_SOURCE);
|
|
56
85
|
|
|
57
86
|
try {
|
|
58
87
|
const proc = spawn({
|
|
59
88
|
cmd: ["bun", "run", workerPath],
|
|
89
|
+
cwd: rootDir,
|
|
60
90
|
env: {
|
|
61
91
|
...process.env,
|
|
62
92
|
ARC_ACCESS_BUNDLES: JSON.stringify(serverBundles),
|
|
63
93
|
ARC_ACCESS_OUT: outPath,
|
|
94
|
+
ARC_PLATFORM_ENTRY: locatePlatformServerEntry(),
|
|
64
95
|
},
|
|
65
96
|
stdout: "pipe",
|
|
66
97
|
stderr: "inherit",
|
|
@@ -76,17 +107,22 @@ export async function extractAccessMap(
|
|
|
76
107
|
} catch {
|
|
77
108
|
// best effort
|
|
78
109
|
}
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(outPath);
|
|
112
|
+
} catch {
|
|
113
|
+
// best effort
|
|
114
|
+
}
|
|
79
115
|
}
|
|
80
116
|
}
|
|
81
117
|
|
|
82
118
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// Worker source (inlined — runs in fresh Bun process)
|
|
119
|
+
// Worker source (inlined — runs in fresh Bun process at user workspace cwd)
|
|
84
120
|
// ---------------------------------------------------------------------------
|
|
85
121
|
|
|
86
122
|
const WORKER_SOURCE = `
|
|
87
|
-
import { existsSync } from "node:fs";
|
|
88
|
-
|
|
89
123
|
globalThis.ONLY_SERVER = true;
|
|
124
|
+
globalThis.ONLY_BROWSER = false;
|
|
125
|
+
globalThis.ONLY_CLIENT = false;
|
|
90
126
|
|
|
91
127
|
const bundles = JSON.parse(process.env.ARC_ACCESS_BUNDLES || "[]");
|
|
92
128
|
const out = process.env.ARC_ACCESS_OUT;
|
|
@@ -95,20 +131,16 @@ if (!out) {
|
|
|
95
131
|
process.exit(2);
|
|
96
132
|
}
|
|
97
133
|
|
|
98
|
-
|
|
134
|
+
// Direct file-path import — bypasses node_modules resolution entirely.
|
|
135
|
+
// Avoids quirks with bun-link snapshots holding stale package.json exports.
|
|
136
|
+
const platform = await import(process.env.ARC_PLATFORM_ENTRY);
|
|
99
137
|
|
|
100
|
-
for (const { name, path
|
|
101
|
-
const target = existsSync(path) ? path : fallback;
|
|
102
|
-
if (!target || !existsSync(target)) {
|
|
103
|
-
// No server bundle on either path — module has no protected access rules
|
|
104
|
-
// to discover. Skip silently rather than logging a misleading error.
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
138
|
+
for (const { name, path } of bundles) {
|
|
107
139
|
try {
|
|
108
|
-
await import(
|
|
140
|
+
await import(path);
|
|
109
141
|
} catch (e) {
|
|
110
|
-
console.error("[access-extractor-worker] failed to import", name, "
|
|
111
|
-
//
|
|
142
|
+
console.error("[access-extractor-worker] failed to import", name, ":", e.message);
|
|
143
|
+
// Partial map is better than total failure.
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// v2: switched from `modules-bundle` (one unit) to `modules-chunk:<name>`
|
|
5
|
+
// (one unit per chunk group). Old v1 entries are irrelevant.
|
|
6
|
+
const CACHE_VERSION = 2;
|
|
5
7
|
const CACHE_FILE = ".build-cache.json";
|
|
6
8
|
|
|
7
9
|
export interface CacheEntry {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
import type { SerializedAccessMap } from "./access-extractor";
|
|
3
|
+
import type { WorkspacePackage } from "./module-builder";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// chunk-planner — decides which chunk group each workspace package belongs to,
|
|
7
|
+
// based on its `protectedBy(...)` declarations.
|
|
8
|
+
//
|
|
9
|
+
// Rules:
|
|
10
|
+
// - 0 access rules → "public"
|
|
11
|
+
// - 1 rule → rule.token.name
|
|
12
|
+
// - N rules, all same token.name → that token name
|
|
13
|
+
// - N rules, mixed token.names → throw (multi-token modules are not
|
|
14
|
+
// supported; user must split or use
|
|
15
|
+
// a single token type)
|
|
16
|
+
//
|
|
17
|
+
// Output: a per-chunk grouping consumed by buildModulesByChunks() to emit
|
|
18
|
+
// independent Bun.build per chunk group. Chunks NEVER share code across
|
|
19
|
+
// groups (a public chunk file can be served unauthenticated; a per-token
|
|
20
|
+
// chunk file is signed and requires the matching token).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const PUBLIC_CHUNK = "public" as const;
|
|
24
|
+
|
|
25
|
+
export interface PackageChunk {
|
|
26
|
+
/** Workspace package being assigned. */
|
|
27
|
+
readonly pkg: WorkspacePackage;
|
|
28
|
+
/** Chunk group: "public" or a token.name from `protectedBy(...)`. */
|
|
29
|
+
readonly chunk: string;
|
|
30
|
+
/** Module name as it appears in the runtime registry (last path segment of pkg.name). */
|
|
31
|
+
readonly moduleName: string;
|
|
32
|
+
/** Filesystem-safe directory name within `<arcDir>/modules/<chunk>/`. */
|
|
33
|
+
readonly safeName: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ChunkPlan {
|
|
37
|
+
/** Module-name → assignment, one entry per workspace package. */
|
|
38
|
+
readonly assignments: ReadonlyMap<string, PackageChunk>;
|
|
39
|
+
/** Chunk group → packages in that group, used by builder to spawn N parallel Bun.build. */
|
|
40
|
+
readonly groups: ReadonlyMap<string, readonly PackageChunk[]>;
|
|
41
|
+
/** Sorted chunk names (always starts with "public" if present). */
|
|
42
|
+
readonly chunks: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function planChunks(
|
|
46
|
+
packages: readonly WorkspacePackage[],
|
|
47
|
+
accessMap: SerializedAccessMap,
|
|
48
|
+
): ChunkPlan {
|
|
49
|
+
const assignments = new Map<string, PackageChunk>();
|
|
50
|
+
const groups = new Map<string, PackageChunk[]>();
|
|
51
|
+
|
|
52
|
+
for (const pkg of packages) {
|
|
53
|
+
const moduleName = moduleNameOf(pkg.name);
|
|
54
|
+
const access = accessMap[moduleName];
|
|
55
|
+
const chunk = resolveChunk(moduleName, access);
|
|
56
|
+
|
|
57
|
+
const entry: PackageChunk = {
|
|
58
|
+
pkg,
|
|
59
|
+
chunk,
|
|
60
|
+
moduleName,
|
|
61
|
+
safeName: basename(pkg.path),
|
|
62
|
+
};
|
|
63
|
+
assignments.set(moduleName, entry);
|
|
64
|
+
|
|
65
|
+
const bucket = groups.get(chunk);
|
|
66
|
+
if (bucket) {
|
|
67
|
+
bucket.push(entry);
|
|
68
|
+
} else {
|
|
69
|
+
groups.set(chunk, [entry]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chunks = [...groups.keys()].sort((a, b) => {
|
|
74
|
+
if (a === PUBLIC_CHUNK) return -1;
|
|
75
|
+
if (b === PUBLIC_CHUNK) return 1;
|
|
76
|
+
return a.localeCompare(b);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return { assignments, groups, chunks };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** Match arc.ts: scoped pkg "@my-app/auth" → module name "auth". */
|
|
87
|
+
function moduleNameOf(pkgName: string): string {
|
|
88
|
+
return pkgName.includes("/") ? pkgName.split("/").pop()! : pkgName;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveChunk(
|
|
92
|
+
moduleName: string,
|
|
93
|
+
access: SerializedAccessMap[string] | undefined,
|
|
94
|
+
): string {
|
|
95
|
+
if (!access || access.rules.length === 0) return PUBLIC_CHUNK;
|
|
96
|
+
|
|
97
|
+
const tokenNames = new Set(access.rules.map((r) => r.token.name));
|
|
98
|
+
if (tokenNames.size === 1) {
|
|
99
|
+
return [...tokenNames][0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const list = [...tokenNames].sort().join(", ");
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Module "${moduleName}" has access rules for multiple tokens [${list}]. ` +
|
|
105
|
+
`Multi-token modules are not supported — split the module or unify on a single token type.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
|
+
import { FRAMEWORK_PEERS, FRAMEWORK_PEER_SET } from "./framework-peers";
|
|
4
5
|
import type { WorkspacePackage } from "./module-builder";
|
|
5
6
|
|
|
7
|
+
export { FRAMEWORK_PEERS };
|
|
8
|
+
|
|
6
9
|
// ---------------------------------------------------------------------------
|
|
7
10
|
// dependency-collector — generates per-module + global framework dep manifests
|
|
8
11
|
// that the runtime container uses to `bun install`.
|
|
@@ -15,20 +18,6 @@ import type { WorkspacePackage } from "./module-builder";
|
|
|
15
18
|
// into the module's own node_modules at deploy time.
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
|
|
18
|
-
/**
|
|
19
|
-
* Packages that MUST be singleton across all modules — they define classes
|
|
20
|
-
* checked with `instanceof` cross-module. Container resolves these from the
|
|
21
|
-
* platform-level `node_modules/`, not per-module.
|
|
22
|
-
*/
|
|
23
|
-
export const FRAMEWORK_PEERS = [
|
|
24
|
-
"@arcote.tech/arc",
|
|
25
|
-
"@arcote.tech/arc-ds",
|
|
26
|
-
"@arcote.tech/arc-react",
|
|
27
|
-
"@arcote.tech/platform",
|
|
28
|
-
"react",
|
|
29
|
-
"react-dom",
|
|
30
|
-
] as const;
|
|
31
|
-
|
|
32
21
|
export type FrameworkPeer = (typeof FRAMEWORK_PEERS)[number];
|
|
33
22
|
|
|
34
23
|
export interface CollectedDeps {
|
|
@@ -46,10 +35,17 @@ export function collectFrameworkDeps(
|
|
|
46
35
|
arcDir: string,
|
|
47
36
|
rootDir: string,
|
|
48
37
|
packages: WorkspacePackage[],
|
|
38
|
+
sharedDeps: ReadonlyArray<{ name: string; version: string }> = [],
|
|
49
39
|
): CollectedDeps {
|
|
50
40
|
mkdirSync(arcDir, { recursive: true });
|
|
51
41
|
|
|
52
42
|
const versions = resolveFrameworkVersions(rootDir, packages);
|
|
43
|
+
// Shared deps (discovered across ≥ 2 workspace packages) join the framework
|
|
44
|
+
// in the global manifest — installed once into <arcDir>/node_modules/ and
|
|
45
|
+
// served via the shell so each module's browser bundle leaves them external.
|
|
46
|
+
for (const { name, version } of sharedDeps) {
|
|
47
|
+
versions[name] = version;
|
|
48
|
+
}
|
|
53
49
|
const manifest = {
|
|
54
50
|
name: "arc-platform-framework",
|
|
55
51
|
private: true,
|
|
@@ -59,20 +55,18 @@ export function collectFrameworkDeps(
|
|
|
59
55
|
const manifestPath = join(arcDir, "package.json");
|
|
60
56
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
61
57
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
const targetLock = join(arcDir, "bun.lock");
|
|
67
|
-
if (existsSync(rootLock)) {
|
|
68
|
-
copyFileSync(rootLock, targetLock);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const hash = sha256OfFiles([manifestPath, targetLock]);
|
|
58
|
+
// Hash over package.json bytes alone. No lockfile in the hash: newer bun
|
|
59
|
+
// rejects empty `{}` lockfiles, and the framework dep set is small enough
|
|
60
|
+
// to let bun generate its own lockfile during install in the image build.
|
|
61
|
+
const hash = sha256Hex(readFileSync(manifestPath));
|
|
72
62
|
writeFileSync(join(arcDir, ".deps-hash"), hash + "\n");
|
|
73
63
|
return { hash, manifestPath };
|
|
74
64
|
}
|
|
75
65
|
|
|
66
|
+
function sha256Hex(bytes: Buffer): string {
|
|
67
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
68
|
+
}
|
|
69
|
+
|
|
76
70
|
// ---------------------------------------------------------------------------
|
|
77
71
|
// Per-module deps
|
|
78
72
|
// ---------------------------------------------------------------------------
|
|
@@ -80,19 +74,37 @@ export function collectFrameworkDeps(
|
|
|
80
74
|
export function collectModuleDeps(
|
|
81
75
|
arcDir: string,
|
|
82
76
|
pkg: WorkspacePackage,
|
|
77
|
+
sharedDepNames: ReadonlySet<string> = new Set(),
|
|
83
78
|
): CollectedDeps {
|
|
84
79
|
const safeName = basename(pkg.path);
|
|
85
80
|
const moduleDir = join(arcDir, "modules", safeName);
|
|
86
81
|
mkdirSync(moduleDir, { recursive: true });
|
|
87
82
|
|
|
88
83
|
const pkgDeps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
84
|
+
const peerDeps = (pkg.packageJson.peerDependencies ?? {}) as Record<
|
|
85
|
+
string,
|
|
86
|
+
string
|
|
87
|
+
>;
|
|
89
88
|
const filtered: Record<string, string> = {};
|
|
90
|
-
|
|
89
|
+
|
|
90
|
+
// Regular deps, excluding:
|
|
91
|
+
// - framework peers (global install via collectFrameworkDeps)
|
|
92
|
+
// - shared deps (also global — promoted to shell across ≥ 2 modules)
|
|
93
|
+
// - workspace links (bundled inline by buildModules; not on npm)
|
|
91
94
|
for (const [name, spec] of Object.entries(pkgDeps)) {
|
|
92
|
-
if (
|
|
95
|
+
if (FRAMEWORK_PEER_SET.has(name)) continue;
|
|
96
|
+
if (sharedDepNames.has(name)) continue;
|
|
93
97
|
if (spec.startsWith("workspace:")) continue;
|
|
94
98
|
filtered[name] = spec;
|
|
95
99
|
}
|
|
100
|
+
// Peer deps minus framework peers + shared deps — e.g. `@arcote.tech/arc-auth`
|
|
101
|
+
// is declared peer of `@ndt/auth` and lives in framework peers; user peers
|
|
102
|
+
// not in either set go per-module.
|
|
103
|
+
for (const [name, spec] of Object.entries(peerDeps)) {
|
|
104
|
+
if (FRAMEWORK_PEER_SET.has(name)) continue;
|
|
105
|
+
if (sharedDepNames.has(name)) continue;
|
|
106
|
+
filtered[name] = spec;
|
|
107
|
+
}
|
|
96
108
|
|
|
97
109
|
const manifest = {
|
|
98
110
|
name: `arc-module-${safeName}`,
|
|
@@ -116,22 +128,51 @@ function resolveFrameworkVersions(
|
|
|
116
128
|
rootDir: string,
|
|
117
129
|
packages: WorkspacePackage[],
|
|
118
130
|
): Record<string, string> {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
131
|
+
// The framework manifest installs into /app/node_modules in the deploy
|
|
132
|
+
// image. It needs to cover:
|
|
133
|
+
// 1. Static framework peers (FRAMEWORK_PEERS) — always present
|
|
134
|
+
// 2. User-extension @arcote.tech/* peers (e.g. arc-ai-openai) declared
|
|
135
|
+
// as peerDeps on user context packages
|
|
136
|
+
// 3. npm runtime deps that server bundles leave external (non-workspace
|
|
137
|
+
// deps not yet bundled inline)
|
|
138
|
+
// Resolution order for each: root package.json wins, then any workspace
|
|
139
|
+
// package's deps/peerDeps, then "*" as last resort.
|
|
121
140
|
const rootPkg = JSON.parse(
|
|
122
141
|
readFileSync(join(rootDir, "package.json"), "utf-8"),
|
|
123
142
|
);
|
|
124
143
|
const rootDeps = (rootPkg.dependencies ?? {}) as Record<string, string>;
|
|
144
|
+
const rootDevDeps = (rootPkg.devDependencies ?? {}) as Record<string, string>;
|
|
145
|
+
|
|
146
|
+
// Build the full required name set.
|
|
147
|
+
const required = new Set<string>(FRAMEWORK_PEERS);
|
|
148
|
+
for (const pkg of packages) {
|
|
149
|
+
const peers = pkg.packageJson.peerDependencies ?? {};
|
|
150
|
+
const deps = (pkg.packageJson.dependencies ?? {}) as Record<string, string>;
|
|
151
|
+
// Any @arcote.tech/* peer beyond the static list (e.g. arc-ai adapters).
|
|
152
|
+
for (const name of Object.keys(peers)) {
|
|
153
|
+
if (name.startsWith("@arcote.tech/")) required.add(name);
|
|
154
|
+
}
|
|
155
|
+
// npm deps (non-workspace) — server bundles leave them external, so the
|
|
156
|
+
// image needs them in /app/node_modules.
|
|
157
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
158
|
+
if (spec.startsWith("workspace:")) continue;
|
|
159
|
+
if (name.startsWith("@arcote.tech/") || !isFrameworkExternal(name)) continue;
|
|
160
|
+
required.add(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
125
164
|
const out: Record<string, string> = {};
|
|
126
|
-
for (const name of
|
|
127
|
-
const rootSpec = rootDeps[name];
|
|
165
|
+
for (const name of required) {
|
|
166
|
+
const rootSpec = rootDeps[name] ?? rootDevDeps[name];
|
|
128
167
|
if (rootSpec) {
|
|
129
168
|
out[name] = rootSpec;
|
|
130
169
|
continue;
|
|
131
170
|
}
|
|
132
171
|
let found: string | undefined;
|
|
133
172
|
for (const pkg of packages) {
|
|
134
|
-
const spec =
|
|
173
|
+
const spec =
|
|
174
|
+
(pkg.packageJson.dependencies ?? {})[name] ??
|
|
175
|
+
(pkg.packageJson.peerDependencies ?? {})[name];
|
|
135
176
|
if (spec) {
|
|
136
177
|
found = spec;
|
|
137
178
|
break;
|
|
@@ -142,6 +183,19 @@ function resolveFrameworkVersions(
|
|
|
142
183
|
return out;
|
|
143
184
|
}
|
|
144
185
|
|
|
186
|
+
/**
|
|
187
|
+
* True if a non-arc npm dep is one the image must install globally for
|
|
188
|
+
* runtime resolution. Today we include everything that user context packages
|
|
189
|
+
* declare as deps but isn't `workspace:` — server bundles leave them external
|
|
190
|
+
* so they MUST be present in /app/node_modules.
|
|
191
|
+
*
|
|
192
|
+
* Future optimization: bundle these inline into the server bundle to avoid
|
|
193
|
+
* the install layer entirely. Left as a follow-up.
|
|
194
|
+
*/
|
|
195
|
+
function isFrameworkExternal(_name: string): boolean {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
145
199
|
function sha256OfFiles(paths: string[]): string {
|
|
146
200
|
const hash = createHash("sha256");
|
|
147
201
|
for (const p of paths.sort()) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// framework-peers — single source of truth for packages that MUST be
|
|
3
|
+
// singleton across all modules. Used by:
|
|
4
|
+
//
|
|
5
|
+
// - dependency-collector.ts (decides which deps go to the global
|
|
6
|
+
// framework manifest vs per-module install)
|
|
7
|
+
// - module-builder.ts (decides which imports stay external in
|
|
8
|
+
// browser bundles, mapped via importmap to
|
|
9
|
+
// /shell/<short>.js)
|
|
10
|
+
// - shared.ts (collectArcPeerDeps — runtime shell entry
|
|
11
|
+
// discovery, importmap generation)
|
|
12
|
+
// - commands/build-shell.ts (runtime shell builder, fallback for user-
|
|
13
|
+
// extension @arcote.tech/* packages)
|
|
14
|
+
//
|
|
15
|
+
// Two reasons a package needs to be singleton:
|
|
16
|
+
//
|
|
17
|
+
// 1. Core framework — `instanceof` checks for ArcAggregate / ArcCommand /
|
|
18
|
+
// ArcFunction base classes require a single class identity across module
|
|
19
|
+
// boundaries.
|
|
20
|
+
// 2. Browser-side React Context — AuthProvider / WorkspaceProvider /
|
|
21
|
+
// ChatProvider etc.; duplicate Context objects across modules cause
|
|
22
|
+
// `useAuth must be used within AuthProvider` even when the provider IS
|
|
23
|
+
// mounted (just from a different module's React.Context instance).
|
|
24
|
+
//
|
|
25
|
+
// The runtime container installs these into <arcDir>/node_modules/ (global)
|
|
26
|
+
// and the shell builder emits /shell/<short>.js per peer.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Core framework packages — `instanceof` consumers. */
|
|
30
|
+
export const CORE_PEERS = [
|
|
31
|
+
"@arcote.tech/arc",
|
|
32
|
+
"@arcote.tech/arc-ds",
|
|
33
|
+
"@arcote.tech/arc-react",
|
|
34
|
+
"@arcote.tech/platform",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
/** Browser-side fragments with React Context — singleton identity required. */
|
|
38
|
+
export const BROWSER_FRAGMENT_PEERS = [
|
|
39
|
+
"@arcote.tech/arc-auth",
|
|
40
|
+
"@arcote.tech/arc-workspace",
|
|
41
|
+
"@arcote.tech/arc-utils",
|
|
42
|
+
"@arcote.tech/arc-chat",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
/** All @arcote.tech/* packages that must be singleton. */
|
|
46
|
+
export const ARC_PEERS = [...CORE_PEERS, ...BROWSER_FRAGMENT_PEERS] as const;
|
|
47
|
+
|
|
48
|
+
/** React + ReactDOM — separate from arc peers but treated identically. */
|
|
49
|
+
export const REACT_PEERS = ["react", "react-dom"] as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Full set of npm packages that the runtime container installs globally
|
|
53
|
+
* (one copy in <arcDir>/node_modules/, shared across all per-module bundles).
|
|
54
|
+
* Filter applied by dependency-collector to keep them out of per-module deps.
|
|
55
|
+
*/
|
|
56
|
+
export const FRAMEWORK_PEERS = [...ARC_PEERS, ...REACT_PEERS] as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bare specifiers that browser bundles leave external — resolved via the
|
|
60
|
+
* importmap in the shell HTML. Superset of FRAMEWORK_PEERS because React's
|
|
61
|
+
* automatic JSX transform emits `react/jsx-runtime` and `react/jsx-dev-runtime`
|
|
62
|
+
* sub-path imports that also need to point at the shared shell bundle.
|
|
63
|
+
*/
|
|
64
|
+
export const SHELL_EXTERNALS = [
|
|
65
|
+
...FRAMEWORK_PEERS,
|
|
66
|
+
"react/jsx-runtime",
|
|
67
|
+
"react/jsx-dev-runtime",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
export const FRAMEWORK_PEER_SET = new Set<string>(FRAMEWORK_PEERS);
|
|
71
|
+
export const SHELL_EXTERNAL_SET = new Set<string>(SHELL_EXTERNALS);
|
|
72
|
+
|
|
73
|
+
/** Short identifier used as the shell bundle filename (`/shell/<short>.js`). */
|
|
74
|
+
export function shortNameOf(pkg: string): string {
|
|
75
|
+
// `@arcote.tech/platform` is special-cased historically to `platform`
|
|
76
|
+
// (rather than `@arcote.tech/platform`) — keep that mapping stable so
|
|
77
|
+
// existing consumers don't break.
|
|
78
|
+
if (pkg === "@arcote.tech/platform") return "platform";
|
|
79
|
+
if (pkg.startsWith("@arcote.tech/")) return pkg.slice("@arcote.tech/".length);
|
|
80
|
+
return pkg;
|
|
81
|
+
}
|