@checkstack/dev-server 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/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@checkstack/dev-server",
3
+ "version": "0.2.0",
4
+ "description": "Local Checkstack dev server for plugin authors. Spawns the platform backend with the cwd plugin loaded, runs Vite for the frontend, and reloads on save. Used as a devDependency in plugin repos (run via `bun run dev` → `checkstack-dev`).",
5
+ "license": "Elastic-2.0",
6
+ "type": "module",
7
+ "checkstack": {
8
+ "type": "tooling"
9
+ },
10
+ "main": "src/dev-server.ts",
11
+ "bin": {
12
+ "checkstack-dev": "./src/dev-server.ts"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "typecheck": "tsgo -b",
23
+ "lint": "bun run lint:code",
24
+ "lint:code": "eslint . --max-warnings 0"
25
+ },
26
+ "dependencies": {
27
+ "@checkstack/common": "0.8.0",
28
+ "@vitejs/plugin-react": "^4.3.4",
29
+ "vite": "^5.4.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@checkstack/backend": "*",
33
+ "@checkstack/frontend": "*"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@checkstack/backend": {
37
+ "optional": true
38
+ },
39
+ "@checkstack/frontend": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@checkstack/tsconfig": "0.0.7",
45
+ "@checkstack/scripts": "0.2.0",
46
+ "@checkstack/backend": "0.9.0",
47
+ "@checkstack/frontend": "0.5.0",
48
+ "typescript": "^5.0.0"
49
+ }
50
+ }
@@ -0,0 +1,411 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import path from "node:path";
3
+ import { resolveCorePluginDeps } from "./dev-deps-resolver";
4
+
5
+ /**
6
+ * In-memory fixture for a virtual node_modules tree. Maps a fully
7
+ * qualified file path to its file contents (typically package.json
8
+ * stringified JSON).
9
+ */
10
+ type Fs = Record<string, string>;
11
+
12
+ const ROOT = "/plugin-author/repo";
13
+ const NM = `${ROOT}/node_modules`;
14
+
15
+ function makeFs(packages: Record<string, unknown>): Fs {
16
+ const out: Fs = {};
17
+ for (const [name, pkg] of Object.entries(packages)) {
18
+ out[`${NM}/${name}/package.json`] = JSON.stringify(pkg);
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function makeResolver(fs: Fs) {
24
+ return (_from: string, request: string): string | undefined => {
25
+ // Only handle requests of the form `<pkg>/package.json` — that's all
26
+ // resolveCorePluginDeps issues. A real `createRequire` resolves any
27
+ // entry, but we don't need that fidelity here.
28
+ if (!request.endsWith("/package.json")) return undefined;
29
+ const pkgName = request.replace(/\/package\.json$/, "");
30
+ const candidate = `${NM}/${pkgName}/package.json`;
31
+ return fs[candidate] === undefined ? undefined : candidate;
32
+ };
33
+ }
34
+
35
+ function readFileFromFs(fs: Fs) {
36
+ return (p: string): string => {
37
+ const text = fs[p];
38
+ if (text === undefined) throw new Error(`ENOENT: ${p}`);
39
+ return text;
40
+ };
41
+ }
42
+
43
+ describe("resolveCorePluginDeps", () => {
44
+ it("returns only the auto-included providers for a plugin with no @checkstack/* backend deps", () => {
45
+ const fs: Fs = {
46
+ [`${ROOT}/package.json`]: JSON.stringify({
47
+ name: "@my-org/widget-backend",
48
+ dependencies: {
49
+ "@checkstack/backend-api": "^1.0.0",
50
+ lodash: "^4.0.0",
51
+ },
52
+ checkstack: { type: "backend", pluginId: "widget" },
53
+ }),
54
+ ...makeFs({
55
+ "@checkstack/backend-api": {
56
+ name: "@checkstack/backend-api",
57
+ checkstack: { type: "common" },
58
+ },
59
+ "@checkstack/queue-memory-backend": {
60
+ name: "@checkstack/queue-memory-backend",
61
+ main: "src/index.ts",
62
+ checkstack: { type: "backend", pluginId: "queue-memory" },
63
+ },
64
+ "@checkstack/cache-memory-backend": {
65
+ name: "@checkstack/cache-memory-backend",
66
+ main: "src/index.ts",
67
+ checkstack: { type: "backend", pluginId: "cache-memory" },
68
+ },
69
+ }),
70
+ };
71
+
72
+ const resolved = resolveCorePluginDeps({
73
+ pluginDir: ROOT,
74
+ readFile: readFileFromFs(fs),
75
+ resolveFrom: makeResolver(fs),
76
+ });
77
+
78
+ const names = resolved.map((r) => r.name).toSorted();
79
+ expect(names).toEqual([
80
+ "@checkstack/cache-memory-backend",
81
+ "@checkstack/queue-memory-backend",
82
+ ]);
83
+ });
84
+
85
+ it("includes a single backend dep declared in dependencies", () => {
86
+ const fs: Fs = {
87
+ [`${ROOT}/package.json`]: JSON.stringify({
88
+ name: "@my-org/healthcheck-rcon-backend",
89
+ dependencies: {
90
+ "@checkstack/healthcheck-backend": "^1.0.0",
91
+ },
92
+ checkstack: { type: "backend", pluginId: "healthcheck-rcon" },
93
+ }),
94
+ ...makeFs({
95
+ "@checkstack/healthcheck-backend": {
96
+ name: "@checkstack/healthcheck-backend",
97
+ main: "src/index.ts",
98
+ checkstack: { type: "backend", pluginId: "healthcheck" },
99
+ dependencies: {},
100
+ },
101
+ "@checkstack/queue-memory-backend": {
102
+ name: "@checkstack/queue-memory-backend",
103
+ main: "src/index.ts",
104
+ checkstack: { type: "backend", pluginId: "queue-memory" },
105
+ },
106
+ "@checkstack/cache-memory-backend": {
107
+ name: "@checkstack/cache-memory-backend",
108
+ main: "src/index.ts",
109
+ checkstack: { type: "backend", pluginId: "cache-memory" },
110
+ },
111
+ }),
112
+ };
113
+
114
+ const resolved = resolveCorePluginDeps({
115
+ pluginDir: ROOT,
116
+ readFile: readFileFromFs(fs),
117
+ resolveFrom: makeResolver(fs),
118
+ });
119
+
120
+ expect(resolved.map((r) => r.name).toSorted()).toEqual([
121
+ "@checkstack/cache-memory-backend",
122
+ "@checkstack/healthcheck-backend",
123
+ "@checkstack/queue-memory-backend",
124
+ ]);
125
+
126
+ const hc = resolved.find((r) => r.name === "@checkstack/healthcheck-backend");
127
+ expect(hc?.modulePath).toBe(
128
+ path.resolve(`${NM}/@checkstack/healthcheck-backend/src/index.ts`),
129
+ );
130
+ });
131
+
132
+ it("walks transitive @checkstack/* backend deps", () => {
133
+ // Plugin → notification-discord-backend → notification-backend → catalog-backend
134
+ const fs: Fs = {
135
+ [`${ROOT}/package.json`]: JSON.stringify({
136
+ name: "@my-org/widget-backend",
137
+ dependencies: {
138
+ "@checkstack/notification-discord-backend": "^1.0.0",
139
+ },
140
+ checkstack: { type: "backend", pluginId: "widget" },
141
+ }),
142
+ ...makeFs({
143
+ "@checkstack/notification-discord-backend": {
144
+ name: "@checkstack/notification-discord-backend",
145
+ main: "src/index.ts",
146
+ checkstack: { type: "backend", pluginId: "notification-discord" },
147
+ dependencies: {
148
+ "@checkstack/notification-backend": "^1.0.0",
149
+ },
150
+ },
151
+ "@checkstack/notification-backend": {
152
+ name: "@checkstack/notification-backend",
153
+ main: "src/index.ts",
154
+ checkstack: { type: "backend", pluginId: "notification" },
155
+ dependencies: {
156
+ "@checkstack/catalog-backend": "^1.0.0",
157
+ },
158
+ },
159
+ "@checkstack/catalog-backend": {
160
+ name: "@checkstack/catalog-backend",
161
+ main: "src/index.ts",
162
+ checkstack: { type: "backend", pluginId: "catalog" },
163
+ },
164
+ "@checkstack/queue-memory-backend": {
165
+ name: "@checkstack/queue-memory-backend",
166
+ main: "src/index.ts",
167
+ checkstack: { type: "backend", pluginId: "queue-memory" },
168
+ },
169
+ "@checkstack/cache-memory-backend": {
170
+ name: "@checkstack/cache-memory-backend",
171
+ main: "src/index.ts",
172
+ checkstack: { type: "backend", pluginId: "cache-memory" },
173
+ },
174
+ }),
175
+ };
176
+
177
+ const resolved = resolveCorePluginDeps({
178
+ pluginDir: ROOT,
179
+ readFile: readFileFromFs(fs),
180
+ resolveFrom: makeResolver(fs),
181
+ });
182
+
183
+ expect(resolved.map((r) => r.name).toSorted()).toEqual([
184
+ "@checkstack/cache-memory-backend",
185
+ "@checkstack/catalog-backend",
186
+ "@checkstack/notification-backend",
187
+ "@checkstack/notification-discord-backend",
188
+ "@checkstack/queue-memory-backend",
189
+ ]);
190
+ });
191
+
192
+ it("walks through common-type packages to find transitive backend deps", () => {
193
+ // Plugin → my-shared-common (type: common) → healthcheck-backend
194
+ // (Unusual but possible; common packages can list runtime deps.)
195
+ const fs: Fs = {
196
+ [`${ROOT}/package.json`]: JSON.stringify({
197
+ name: "@my-org/widget-backend",
198
+ dependencies: {
199
+ "@checkstack/widget-common": "^1.0.0",
200
+ },
201
+ checkstack: { type: "backend", pluginId: "widget" },
202
+ }),
203
+ ...makeFs({
204
+ "@checkstack/widget-common": {
205
+ name: "@checkstack/widget-common",
206
+ checkstack: { type: "common" },
207
+ dependencies: {
208
+ "@checkstack/healthcheck-backend": "^1.0.0",
209
+ },
210
+ },
211
+ "@checkstack/healthcheck-backend": {
212
+ name: "@checkstack/healthcheck-backend",
213
+ main: "src/index.ts",
214
+ checkstack: { type: "backend", pluginId: "healthcheck" },
215
+ },
216
+ "@checkstack/queue-memory-backend": {
217
+ name: "@checkstack/queue-memory-backend",
218
+ main: "src/index.ts",
219
+ checkstack: { type: "backend", pluginId: "queue-memory" },
220
+ },
221
+ "@checkstack/cache-memory-backend": {
222
+ name: "@checkstack/cache-memory-backend",
223
+ main: "src/index.ts",
224
+ checkstack: { type: "backend", pluginId: "cache-memory" },
225
+ },
226
+ }),
227
+ };
228
+
229
+ const resolved = resolveCorePluginDeps({
230
+ pluginDir: ROOT,
231
+ readFile: readFileFromFs(fs),
232
+ resolveFrom: makeResolver(fs),
233
+ });
234
+
235
+ expect(resolved.map((r) => r.name)).toContain(
236
+ "@checkstack/healthcheck-backend",
237
+ );
238
+ // The common pkg itself isn't loaded as a plugin
239
+ expect(resolved.map((r) => r.name)).not.toContain(
240
+ "@checkstack/widget-common",
241
+ );
242
+ });
243
+
244
+ it("excludes the plugin under dev itself even when listed transitively", () => {
245
+ const fs: Fs = {
246
+ [`${ROOT}/package.json`]: JSON.stringify({
247
+ name: "@my-org/widget-backend",
248
+ dependencies: {
249
+ "@checkstack/healthcheck-backend": "^1.0.0",
250
+ },
251
+ checkstack: { type: "backend", pluginId: "widget" },
252
+ }),
253
+ ...makeFs({
254
+ "@checkstack/healthcheck-backend": {
255
+ name: "@checkstack/healthcheck-backend",
256
+ main: "src/index.ts",
257
+ checkstack: { type: "backend", pluginId: "healthcheck" },
258
+ // Pretend the platform plugin pulls back into the user's plugin
259
+ // (this would never happen in practice, but the resolver must
260
+ // never try to load the plugin under dev as a sibling).
261
+ dependencies: {
262
+ "@my-org/widget-backend": "^1.0.0",
263
+ },
264
+ },
265
+ "@checkstack/queue-memory-backend": {
266
+ name: "@checkstack/queue-memory-backend",
267
+ main: "src/index.ts",
268
+ checkstack: { type: "backend", pluginId: "queue-memory" },
269
+ },
270
+ "@checkstack/cache-memory-backend": {
271
+ name: "@checkstack/cache-memory-backend",
272
+ main: "src/index.ts",
273
+ checkstack: { type: "backend", pluginId: "cache-memory" },
274
+ },
275
+ }),
276
+ };
277
+
278
+ const resolved = resolveCorePluginDeps({
279
+ pluginDir: ROOT,
280
+ readFile: readFileFromFs(fs),
281
+ resolveFrom: makeResolver(fs),
282
+ });
283
+ expect(resolved.map((r) => r.name)).not.toContain("@my-org/widget-backend");
284
+ });
285
+
286
+ it("skips queue-memory auto-include when bullmq is already in the dep graph", () => {
287
+ const fs: Fs = {
288
+ [`${ROOT}/package.json`]: JSON.stringify({
289
+ name: "@my-org/widget-backend",
290
+ dependencies: {
291
+ "@checkstack/queue-bullmq-backend": "^1.0.0",
292
+ },
293
+ checkstack: { type: "backend", pluginId: "widget" },
294
+ }),
295
+ ...makeFs({
296
+ "@checkstack/queue-bullmq-backend": {
297
+ name: "@checkstack/queue-bullmq-backend",
298
+ main: "src/index.ts",
299
+ checkstack: { type: "backend", pluginId: "queue-bullmq" },
300
+ },
301
+ "@checkstack/queue-memory-backend": {
302
+ name: "@checkstack/queue-memory-backend",
303
+ main: "src/index.ts",
304
+ checkstack: { type: "backend", pluginId: "queue-memory" },
305
+ },
306
+ "@checkstack/cache-memory-backend": {
307
+ name: "@checkstack/cache-memory-backend",
308
+ main: "src/index.ts",
309
+ checkstack: { type: "backend", pluginId: "cache-memory" },
310
+ },
311
+ }),
312
+ };
313
+
314
+ const resolved = resolveCorePluginDeps({
315
+ pluginDir: ROOT,
316
+ readFile: readFileFromFs(fs),
317
+ resolveFrom: makeResolver(fs),
318
+ });
319
+ const names = resolved.map((r) => r.name);
320
+ expect(names).toContain("@checkstack/queue-bullmq-backend");
321
+ expect(names).not.toContain("@checkstack/queue-memory-backend");
322
+ });
323
+
324
+ it("does not include @checkstack/* frontend or tooling packages", () => {
325
+ const fs: Fs = {
326
+ [`${ROOT}/package.json`]: JSON.stringify({
327
+ name: "@my-org/widget-backend",
328
+ dependencies: {
329
+ "@checkstack/healthcheck-backend": "^1.0.0",
330
+ "@checkstack/healthcheck-frontend": "^1.0.0",
331
+ "@checkstack/scripts": "^0.1.0",
332
+ },
333
+ checkstack: { type: "backend", pluginId: "widget" },
334
+ }),
335
+ ...makeFs({
336
+ "@checkstack/healthcheck-backend": {
337
+ name: "@checkstack/healthcheck-backend",
338
+ main: "src/index.ts",
339
+ checkstack: { type: "backend", pluginId: "healthcheck" },
340
+ },
341
+ "@checkstack/healthcheck-frontend": {
342
+ name: "@checkstack/healthcheck-frontend",
343
+ main: "src/index.tsx",
344
+ checkstack: { type: "frontend", pluginId: "healthcheck" },
345
+ },
346
+ "@checkstack/scripts": {
347
+ name: "@checkstack/scripts",
348
+ checkstack: { type: "tooling" },
349
+ },
350
+ "@checkstack/queue-memory-backend": {
351
+ name: "@checkstack/queue-memory-backend",
352
+ main: "src/index.ts",
353
+ checkstack: { type: "backend", pluginId: "queue-memory" },
354
+ },
355
+ "@checkstack/cache-memory-backend": {
356
+ name: "@checkstack/cache-memory-backend",
357
+ main: "src/index.ts",
358
+ checkstack: { type: "backend", pluginId: "cache-memory" },
359
+ },
360
+ }),
361
+ };
362
+
363
+ const resolved = resolveCorePluginDeps({
364
+ pluginDir: ROOT,
365
+ readFile: readFileFromFs(fs),
366
+ resolveFrom: makeResolver(fs),
367
+ });
368
+ const names = resolved.map((r) => r.name);
369
+ expect(names).toContain("@checkstack/healthcheck-backend");
370
+ expect(names).not.toContain("@checkstack/healthcheck-frontend");
371
+ expect(names).not.toContain("@checkstack/scripts");
372
+ });
373
+
374
+ it("silently skips a declared dep that isn't actually installed", () => {
375
+ const fs: Fs = {
376
+ [`${ROOT}/package.json`]: JSON.stringify({
377
+ name: "@my-org/widget-backend",
378
+ dependencies: {
379
+ "@checkstack/missing-backend": "^1.0.0",
380
+ },
381
+ checkstack: { type: "backend", pluginId: "widget" },
382
+ }),
383
+ ...makeFs({
384
+ "@checkstack/queue-memory-backend": {
385
+ name: "@checkstack/queue-memory-backend",
386
+ main: "src/index.ts",
387
+ checkstack: { type: "backend", pluginId: "queue-memory" },
388
+ },
389
+ "@checkstack/cache-memory-backend": {
390
+ name: "@checkstack/cache-memory-backend",
391
+ main: "src/index.ts",
392
+ checkstack: { type: "backend", pluginId: "cache-memory" },
393
+ },
394
+ }),
395
+ };
396
+
397
+ const resolved = resolveCorePluginDeps({
398
+ pluginDir: ROOT,
399
+ readFile: readFileFromFs(fs),
400
+ resolveFrom: makeResolver(fs),
401
+ });
402
+ expect(resolved.map((r) => r.name)).not.toContain(
403
+ "@checkstack/missing-backend",
404
+ );
405
+ // Auto-included providers still present — boot needs them.
406
+ expect(resolved.map((r) => r.name).toSorted()).toEqual([
407
+ "@checkstack/cache-memory-backend",
408
+ "@checkstack/queue-memory-backend",
409
+ ]);
410
+ });
411
+ });
@@ -0,0 +1,215 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+
5
+ /**
6
+ * Walks the plugin's `package.json#dependencies` and returns the file paths
7
+ * of every `@checkstack/*` backend plugin module that should be co-loaded
8
+ * by the dev server.
9
+ *
10
+ * Why this exists: the dev server boots `core/backend` with
11
+ * `skipDiscovery: true` and a single manual plugin (the one under
12
+ * development). Real plugins almost always depend on platform plugins
13
+ * (`@checkstack/healthcheck-backend`, `@checkstack/notification-backend`,
14
+ * `@checkstack/catalog-backend`, …) — without those, the host plugin's
15
+ * `init()` calls into unregistered services and the boot deadlocks or
16
+ * crashes.
17
+ *
18
+ * Resolution rules:
19
+ * 1. Recursively walk `dependencies` (and `peerDependencies`) of the
20
+ * plugin under dev.
21
+ * 2. Only follow packages whose name starts with `@checkstack/`.
22
+ * 3. Only include packages whose `package.json#checkstack.type === "backend"`.
23
+ * Common-type packages provide types and are pulled in automatically
24
+ * by the TS importer; frontend-type packages are loaded by the Vite
25
+ * dev server, not the backend.
26
+ * 4. Skip the plugin under dev itself (its module path is passed
27
+ * separately as the primary).
28
+ * 5. Auto-include `@checkstack/queue-memory-backend` +
29
+ * `@checkstack/cache-memory-backend` when no other queue/cache
30
+ * strategy is in the resolved set, so `coreServices.queueManager` /
31
+ * `coreServices.cacheManager` have a registered strategy on boot.
32
+ * These are the cheapest, zero-config providers — fine for dev.
33
+ *
34
+ * Returns absolute paths suitable for a child process to `bun run` /
35
+ * `import()`. Order is *not* topological — the platform's own dependency
36
+ * sorter inside `loadPlugins` handles that.
37
+ */
38
+
39
+ interface PackageJson {
40
+ name?: string;
41
+ dependencies?: Record<string, string>;
42
+ peerDependencies?: Record<string, string>;
43
+ checkstack?: {
44
+ type?: "backend" | "frontend" | "common" | "tooling";
45
+ pluginId?: string;
46
+ };
47
+ }
48
+
49
+ interface ResolvedPlugin {
50
+ name: string;
51
+ packageDir: string;
52
+ /** Path to import (the package.json `main` resolved to absolute). */
53
+ modulePath: string;
54
+ }
55
+
56
+ /**
57
+ * Resolve the set of backend plugins the dev server should load alongside
58
+ * the plugin under development.
59
+ *
60
+ * @param input.pluginDir The plugin author's repo root (the cwd of the
61
+ * dev command).
62
+ * @param input.readFile Injectable for tests; defaults to
63
+ * `fs.readFileSync`.
64
+ * @param input.resolveFrom Injectable for tests; defaults to Node's
65
+ * `createRequire(...).resolve(...)`.
66
+ */
67
+ export function resolveCorePluginDeps({
68
+ pluginDir,
69
+ readFile = (p) => fs.readFileSync(p, "utf8"),
70
+ resolveFrom,
71
+ }: {
72
+ pluginDir: string;
73
+ readFile?: (p: string) => string;
74
+ resolveFrom?: (from: string, request: string) => string | undefined;
75
+ }): ResolvedPlugin[] {
76
+ const pluginPkgPath = path.join(pluginDir, "package.json");
77
+ const pluginPkg = JSON.parse(readFile(pluginPkgPath)) as PackageJson;
78
+ const pluginUnderDevName = pluginPkg.name;
79
+
80
+ // Default resolver uses createRequire from the plugin's package.json so
81
+ // node_modules lookup matches what `bun run` would do at runtime.
82
+ const defaultResolve =
83
+ resolveFrom ??
84
+ ((from: string, request: string): string | undefined => {
85
+ try {
86
+ const req = createRequire(from);
87
+ return req.resolve(request);
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ });
92
+
93
+ const resolved = new Map<string, ResolvedPlugin>();
94
+ const queue: string[] = [];
95
+
96
+ const seedFromPkg = (pkg: PackageJson) => {
97
+ for (const block of [pkg.dependencies, pkg.peerDependencies]) {
98
+ if (!block) continue;
99
+ for (const dep of Object.keys(block)) {
100
+ if (!dep.startsWith("@checkstack/")) continue;
101
+ if (dep === pluginUnderDevName) continue;
102
+ queue.push(dep);
103
+ }
104
+ }
105
+ };
106
+
107
+ seedFromPkg(pluginPkg);
108
+
109
+ while (queue.length > 0) {
110
+ const depName = queue.shift()!;
111
+ if (resolved.has(depName)) continue;
112
+
113
+ // Resolve the package's package.json from the plugin dir's perspective.
114
+ const pkgJsonPath = defaultResolve(
115
+ path.join(pluginDir, "package.json"),
116
+ `${depName}/package.json`,
117
+ );
118
+ if (!pkgJsonPath) {
119
+ // Dep declared but not actually installed — surface during boot,
120
+ // not here. The `loadPlugins` import will throw with a clear msg.
121
+ continue;
122
+ }
123
+
124
+ const pkg = JSON.parse(readFile(pkgJsonPath)) as PackageJson;
125
+
126
+ // Only enqueue further deps once we've decided to (or not to) load
127
+ // this package — but always walk the graph. A common-type package
128
+ // can transitively depend on a backend-type package.
129
+ seedFromPkg(pkg);
130
+
131
+ if (pkg.checkstack?.type !== "backend") continue;
132
+
133
+ const packageDir = path.dirname(pkgJsonPath);
134
+ const main = readMain(pkg, pkgJsonPath, readFile);
135
+ const modulePath = path.resolve(packageDir, main);
136
+
137
+ resolved.set(depName, {
138
+ name: depName,
139
+ packageDir,
140
+ modulePath,
141
+ });
142
+ }
143
+
144
+ // Auto-include in-memory queue + cache providers if no provider was
145
+ // already pulled in via the dep graph. These are the dev-mode defaults
146
+ // — operators wire BullMQ / Redis in production.
147
+ ensureProvider({
148
+ needle: "queue-memory-backend",
149
+ siblings: ["queue-bullmq-backend"],
150
+ resolved,
151
+ pluginDir,
152
+ readFile,
153
+ resolveFrom: defaultResolve,
154
+ });
155
+ ensureProvider({
156
+ needle: "cache-memory-backend",
157
+ siblings: [], // no other cache provider exists yet
158
+ resolved,
159
+ pluginDir,
160
+ readFile,
161
+ resolveFrom: defaultResolve,
162
+ });
163
+
164
+ return [...resolved.values()];
165
+ }
166
+
167
+ function readMain(
168
+ pkg: PackageJson,
169
+ pkgJsonPath: string,
170
+ readFile: (p: string) => string,
171
+ ): string {
172
+ const raw = JSON.parse(readFile(pkgJsonPath)) as { main?: string };
173
+ return raw.main ?? "src/index.ts";
174
+ }
175
+
176
+ /**
177
+ * If none of `siblings` is already in the resolved set, attempt to add
178
+ * `needle` (a known in-memory dev provider). Silently no-op if `needle`
179
+ * isn't installed — operators may have a different provider wired up.
180
+ */
181
+ function ensureProvider({
182
+ needle,
183
+ siblings,
184
+ resolved,
185
+ pluginDir,
186
+ readFile,
187
+ resolveFrom,
188
+ }: {
189
+ needle: string;
190
+ siblings: string[];
191
+ resolved: Map<string, ResolvedPlugin>;
192
+ pluginDir: string;
193
+ readFile: (p: string) => string;
194
+ resolveFrom: (from: string, request: string) => string | undefined;
195
+ }): void {
196
+ const fqNeedle = `@checkstack/${needle}`;
197
+ if (resolved.has(fqNeedle)) return;
198
+ for (const sibling of siblings) {
199
+ if (resolved.has(`@checkstack/${sibling}`)) return;
200
+ }
201
+ const pkgJsonPath = resolveFrom(
202
+ path.join(pluginDir, "package.json"),
203
+ `${fqNeedle}/package.json`,
204
+ );
205
+ if (!pkgJsonPath) return;
206
+ const pkg = JSON.parse(readFile(pkgJsonPath)) as PackageJson;
207
+ if (pkg.checkstack?.type !== "backend") return;
208
+ const packageDir = path.dirname(pkgJsonPath);
209
+ const main = readMain(pkg, pkgJsonPath, readFile);
210
+ resolved.set(fqNeedle, {
211
+ name: fqNeedle,
212
+ packageDir,
213
+ modulePath: path.resolve(packageDir, main),
214
+ });
215
+ }