@checkstack/script-packages-backend 0.2.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/CHANGELOG.md +273 -0
- package/drizzle/0000_flashy_squadron_supreme.sql +63 -0
- package/drizzle/0001_flawless_drax.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +395 -0
- package/drizzle/meta/0001_snapshot.json +491 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +7 -0
- package/package.json +32 -0
- package/src/atomic-symlink.test.ts +47 -0
- package/src/atomic-symlink.ts +66 -0
- package/src/blob-gc-runner.test.ts +120 -0
- package/src/blob-gc-runner.ts +139 -0
- package/src/blob-gc.test.ts +182 -0
- package/src/blob-gc.ts +161 -0
- package/src/blob-hash.test.ts +70 -0
- package/src/blob-hash.ts +56 -0
- package/src/blob-store-registry.test.ts +78 -0
- package/src/blob-store-registry.ts +75 -0
- package/src/blob-store.ts +51 -0
- package/src/cache-archive.test.ts +164 -0
- package/src/cache-archive.ts +192 -0
- package/src/cache-layout.ts +64 -0
- package/src/data-dir.test.ts +41 -0
- package/src/data-dir.ts +42 -0
- package/src/e2e-install-reconcile.test.ts +121 -0
- package/src/hooks.ts +20 -0
- package/src/index.ts +594 -0
- package/src/install-controller.test.ts +257 -0
- package/src/install-controller.ts +144 -0
- package/src/install-service.test.ts +104 -0
- package/src/install-service.ts +116 -0
- package/src/install-state-store.ts +131 -0
- package/src/lockfile.test.ts +60 -0
- package/src/lockfile.ts +0 -0
- package/src/npmrc.test.ts +48 -0
- package/src/npmrc.ts +42 -0
- package/src/package-types.test.ts +293 -0
- package/src/package-types.ts +408 -0
- package/src/parse-bun-lock.test.ts +62 -0
- package/src/parse-bun-lock.ts +59 -0
- package/src/reconcile-diff.test.ts +41 -0
- package/src/reconcile-diff.ts +26 -0
- package/src/reconcile-fs.ts +199 -0
- package/src/reconciler.test.ts +289 -0
- package/src/reconciler.ts +81 -0
- package/src/registry-client.test.ts +314 -0
- package/src/registry-client.ts +0 -0
- package/src/registry-request-config.ts +63 -0
- package/src/registry-token.test.ts +124 -0
- package/src/registry-token.ts +104 -0
- package/src/resolution-root.test.ts +82 -0
- package/src/resolution-root.ts +127 -0
- package/src/resolver.test.ts +133 -0
- package/src/resolver.ts +132 -0
- package/src/router.ts +273 -0
- package/src/schema.ts +166 -0
- package/src/size-cap.test.ts +32 -0
- package/src/size-cap.ts +40 -0
- package/src/storage-migration.test.ts +318 -0
- package/src/storage-migration.ts +213 -0
- package/src/stores.ts +533 -0
- package/src/tree-gc.test.ts +184 -0
- package/src/tree-gc.ts +160 -0
- package/src/tree-retirement.ts +81 -0
- package/src/type-acquisition-route.ts +178 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
resolveDataDir,
|
|
5
|
+
resolveScriptPackagesDir,
|
|
6
|
+
storePaths,
|
|
7
|
+
} from "./data-dir";
|
|
8
|
+
|
|
9
|
+
describe("resolveDataDir", () => {
|
|
10
|
+
test("uses CHECKSTACK_DATA_DIR when set", () => {
|
|
11
|
+
expect(resolveDataDir({ CHECKSTACK_DATA_DIR: "/srv/cs" })).toBe("/srv/cs");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("defaults to .data under cwd when unset", () => {
|
|
15
|
+
expect(resolveDataDir({})).toBe(path.resolve(process.cwd(), ".data"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("ignores a blank value", () => {
|
|
19
|
+
expect(resolveDataDir({ CHECKSTACK_DATA_DIR: " " })).toBe(
|
|
20
|
+
path.resolve(process.cwd(), ".data"),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("resolveScriptPackagesDir", () => {
|
|
26
|
+
test("nests script-packages under the data dir", () => {
|
|
27
|
+
expect(resolveScriptPackagesDir({ CHECKSTACK_DATA_DIR: "/srv/cs" })).toBe(
|
|
28
|
+
"/srv/cs/script-packages",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("storePaths", () => {
|
|
34
|
+
test("derives the cache / trees / current / runs subpaths", () => {
|
|
35
|
+
const p = storePaths("/srv/cs/script-packages");
|
|
36
|
+
expect(p.cache).toBe("/srv/cs/script-packages/cache");
|
|
37
|
+
expect(p.trees).toBe("/srv/cs/script-packages/trees");
|
|
38
|
+
expect(p.current).toBe("/srv/cs/script-packages/current");
|
|
39
|
+
expect(p.runs).toBe("/srv/cs/script-packages/.runs");
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/data-dir.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the on-disk root for the managed package store on this host.
|
|
5
|
+
*
|
|
6
|
+
* No general backend data-dir convention exists in the repo yet, so we
|
|
7
|
+
* introduce `CHECKSTACK_DATA_DIR` (env), defaulting to a `.data/` dir under
|
|
8
|
+
* the backend's CWD. Script packages live at `<dataDir>/script-packages/`.
|
|
9
|
+
*
|
|
10
|
+
* The store contains a generated `package.json`, `bun.lock`, the
|
|
11
|
+
* content-addressed cache, versioned `trees/<lockfileHash>/`, the `current`
|
|
12
|
+
* symlink, and `.runs/<uuid>/` scratch dirs for runner resolution.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveDataDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
15
|
+
const configured = env.CHECKSTACK_DATA_DIR;
|
|
16
|
+
if (configured && configured.trim().length > 0) {
|
|
17
|
+
return path.resolve(configured);
|
|
18
|
+
}
|
|
19
|
+
return path.resolve(process.cwd(), ".data");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `<dataDir>/script-packages` - the managed package store root. */
|
|
23
|
+
export function resolveScriptPackagesDir(
|
|
24
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
25
|
+
): string {
|
|
26
|
+
return path.join(resolveDataDir(env), "script-packages");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Subpaths inside the package store. */
|
|
30
|
+
export function storePaths(storeRoot: string) {
|
|
31
|
+
return {
|
|
32
|
+
root: storeRoot,
|
|
33
|
+
/** Content-addressed blob cache (compressed blobs keyed by integrity). */
|
|
34
|
+
cache: path.join(storeRoot, "cache"),
|
|
35
|
+
/** Versioned materialized trees: `trees/<lockfileHash>/node_modules`. */
|
|
36
|
+
trees: path.join(storeRoot, "trees"),
|
|
37
|
+
/** Atomically-flipped symlink to the active tree. */
|
|
38
|
+
current: path.join(storeRoot, "current"),
|
|
39
|
+
/** Per-run scratch dirs for runner resolution. */
|
|
40
|
+
runs: path.join(storeRoot, ".runs"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { defaultEsmScriptRunner } from "@checkstack/backend-api";
|
|
6
|
+
import { performInstall, type BlobIndex } from "./install-service";
|
|
7
|
+
import { createCentralResolver } from "./resolver";
|
|
8
|
+
import { reconcileToHash } from "./reconciler";
|
|
9
|
+
import { createReconcileFsDeps } from "./reconcile-fs";
|
|
10
|
+
import { storePaths } from "./data-dir";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* End-to-end proof of the central-store + core-reconciliation pipeline
|
|
14
|
+
* against a real (tiny, pure-JS) package:
|
|
15
|
+
*
|
|
16
|
+
* resolve+install -> publish per-package blobs -> reconcile (delta pull
|
|
17
|
+
* from the "blob store") -> bun install --offline materialize -> atomic
|
|
18
|
+
* `current` flip -> a user script imports the package via resolutionRoot.
|
|
19
|
+
*
|
|
20
|
+
* Network is only touched once (the central resolve); the reconcile side
|
|
21
|
+
* pulls from the in-memory blob store, never the registry.
|
|
22
|
+
*/
|
|
23
|
+
// This test reaches the public npm registry once (the central resolve), so
|
|
24
|
+
// it's opt-in to avoid flakiness / failures in offline CI. Run with
|
|
25
|
+
// `CHECKSTACK_E2E_NETWORK=1 bun test` to exercise the full pipeline.
|
|
26
|
+
const E2E_ENABLED = process.env.CHECKSTACK_E2E_NETWORK === "1";
|
|
27
|
+
|
|
28
|
+
describe.skipIf(!E2E_ENABLED)("e2e: install + reconcile + import", () => {
|
|
29
|
+
let work: string;
|
|
30
|
+
// In-memory blob store standing in for the active BlobStore.
|
|
31
|
+
const blobs = new Map<string, Uint8Array>();
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
work = await mkdtemp(path.join(tmpdir(), "cs-e2e-"));
|
|
35
|
+
});
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await rm(work, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test(
|
|
41
|
+
"resolves leftpad centrally, reconciles on a fresh host, and runs an import",
|
|
42
|
+
async () => {
|
|
43
|
+
const resolver = createCentralResolver({
|
|
44
|
+
scratchDir: path.join(work, "install-scratch"),
|
|
45
|
+
cacheDir: path.join(work, "install-cache"),
|
|
46
|
+
registry: {
|
|
47
|
+
registryUrl: "https://registry.npmjs.org/",
|
|
48
|
+
scopedRegistries: [],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const index: BlobIndex = { record: async () => {} };
|
|
53
|
+
const blobStore = {
|
|
54
|
+
id: "memory",
|
|
55
|
+
has: async ({ integrity }: { integrity: string }) =>
|
|
56
|
+
blobs.has(integrity),
|
|
57
|
+
put: async ({
|
|
58
|
+
integrity,
|
|
59
|
+
bytes,
|
|
60
|
+
}: {
|
|
61
|
+
integrity: string;
|
|
62
|
+
bytes: Uint8Array;
|
|
63
|
+
}) => {
|
|
64
|
+
blobs.set(integrity, bytes);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const installResult = await performInstall({
|
|
69
|
+
packages: [{ name: "leftpad", version: "0.0.1", enabled: true }],
|
|
70
|
+
ignoreScripts: true,
|
|
71
|
+
resolver,
|
|
72
|
+
blobStore,
|
|
73
|
+
blobIndex: index,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(installResult.manifest.length).toBeGreaterThanOrEqual(1);
|
|
77
|
+
expect(installResult.lockfileHash.length).toBe(64);
|
|
78
|
+
expect(blobs.size).toBeGreaterThanOrEqual(1);
|
|
79
|
+
|
|
80
|
+
// ── Fresh host reconcile (cold cache, pulls all blobs from the store) ──
|
|
81
|
+
const storeRoot = path.join(work, "host-store");
|
|
82
|
+
const deps = createReconcileFsDeps({
|
|
83
|
+
storeRoot,
|
|
84
|
+
fetchBlob: async ({ integrity }) => {
|
|
85
|
+
const bytes = blobs.get(integrity);
|
|
86
|
+
if (!bytes) throw new Error(`blob ${integrity} not in store`);
|
|
87
|
+
return bytes;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const recRes = await reconcileToHash({
|
|
92
|
+
lockfileHash: installResult.lockfileHash,
|
|
93
|
+
manifest: installResult.manifest,
|
|
94
|
+
deps,
|
|
95
|
+
});
|
|
96
|
+
expect(recRes.alreadyConverged).toBe(false);
|
|
97
|
+
expect(recRes.pulledIntegrities.length).toBe(blobs.size);
|
|
98
|
+
|
|
99
|
+
// A second reconcile is a no-op (idempotent convergence).
|
|
100
|
+
const again = await reconcileToHash({
|
|
101
|
+
lockfileHash: installResult.lockfileHash,
|
|
102
|
+
manifest: installResult.manifest,
|
|
103
|
+
deps,
|
|
104
|
+
});
|
|
105
|
+
expect(again.alreadyConverged).toBe(true);
|
|
106
|
+
|
|
107
|
+
// ── Run a user script that imports the materialized package ──
|
|
108
|
+
const current = storePaths(storeRoot).current;
|
|
109
|
+
const run = await defaultEsmScriptRunner.run({
|
|
110
|
+
script: `import leftpad from "leftpad";\nexport default leftpad("7", 3);`,
|
|
111
|
+
context: {},
|
|
112
|
+
timeoutMs: 20_000,
|
|
113
|
+
resolutionRoot: current,
|
|
114
|
+
});
|
|
115
|
+
expect(run.error).toBeUndefined();
|
|
116
|
+
// leftpad pads with "0" by default.
|
|
117
|
+
expect(run.result).toBe("007");
|
|
118
|
+
},
|
|
119
|
+
60_000,
|
|
120
|
+
);
|
|
121
|
+
});
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHook } from "@checkstack/backend-api";
|
|
2
|
+
import {
|
|
3
|
+
SCRIPT_PACKAGES_CHANGED_HOOK_ID,
|
|
4
|
+
type ScriptPackagesChangedPayload,
|
|
5
|
+
} from "@checkstack/script-packages-common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Backend hook fired by the elected installer after a successful install.
|
|
9
|
+
*
|
|
10
|
+
* Core instances subscribe in `broadcast` mode (every instance receives it,
|
|
11
|
+
* the deliberate inverse of installer-election) and kick their reconciler
|
|
12
|
+
* to delta-sync to the new `lockfileHash`. Each instance's broadcast
|
|
13
|
+
* handler also pushes a `RefreshScriptPackages` control message to its
|
|
14
|
+
* connected satellites (Phase 3). The hook is best-effort liveness; the
|
|
15
|
+
* durable desired state (`install_state.lockfileHash`) drives idempotent
|
|
16
|
+
* convergence on the next startup / assignment-sync regardless.
|
|
17
|
+
*/
|
|
18
|
+
export const scriptPackagesChangedHook = createHook<ScriptPackagesChangedPayload>(
|
|
19
|
+
SCRIPT_PACKAGES_CHANGED_HOOK_ID,
|
|
20
|
+
);
|