@geekmidas/cli 10.0.0-alpha.32 → 10.0.0-alpha.34
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/{config-URVD4bTj.cjs → config-DMpzmsQL.cjs} +2 -2
- package/dist/{config-URVD4bTj.cjs.map → config-DMpzmsQL.cjs.map} +1 -1
- package/dist/{config-BH3Ki69F.mjs → config-Du8AQJRK.mjs} +2 -2
- package/dist/{config-BH3Ki69F.mjs.map → config-Du8AQJRK.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/discover-CMxesskG.mjs.map +1 -1
- package/dist/discover-CroTIm0K.cjs.map +1 -1
- package/dist/{github-BrDmlm1x.mjs → github-DN-92LWP.mjs} +3 -3
- package/dist/{github-BrDmlm1x.mjs.map → github-DN-92LWP.mjs.map} +1 -1
- package/dist/{github-B2fis34P.cjs → github-JhvrmjQp.cjs} +3 -3
- package/dist/{github-B2fis34P.cjs.map → github-JhvrmjQp.cjs.map} +1 -1
- package/dist/{index-p89EzILa.d.mts → index-CAp22ul5.d.cts} +2 -2
- package/dist/{index-p89EzILa.d.mts.map → index-CAp22ul5.d.cts.map} +1 -1
- package/dist/{index-rU9u761a.d.cts → index-DxXMtQNT.d.mts} +2 -2
- package/dist/{index-rU9u761a.d.cts.map → index-DxXMtQNT.d.mts.map} +1 -1
- package/dist/index.cjs +125 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +126 -82
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-CnPtWnkR.mjs → migrate-BbhXxQer.mjs} +4 -4
- package/dist/{migrate-CnPtWnkR.mjs.map → migrate-BbhXxQer.mjs.map} +1 -1
- package/dist/{migrate-BiasgAIN.cjs → migrate-u1oRHz6K.cjs} +4 -4
- package/dist/{migrate-BiasgAIN.cjs.map → migrate-u1oRHz6K.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{ready-DXtNfk7v.cjs → ready-CXhoutix.cjs} +108 -25
- package/dist/ready-CXhoutix.cjs.map +1 -0
- package/dist/{ready-CvP3JVEw.mjs → ready-D1BCiaYU.mjs} +106 -29
- package/dist/ready-D1BCiaYU.mjs.map +1 -0
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{types-CzK6hsXP.d.mts → types-BTiu5kwo.d.mts} +26 -1
- package/dist/{types-CzK6hsXP.d.mts.map → types-BTiu5kwo.d.mts.map} +1 -1
- package/dist/{types-DOOAevjG.d.cts → types-Bj_FpeTc.d.cts} +26 -1
- package/dist/{types-DOOAevjG.d.cts.map → types-Bj_FpeTc.d.cts.map} +1 -1
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Bav3TVrP.cjs → workspace-CAJk6UmC.cjs} +11 -4
- package/dist/{workspace-Bav3TVrP.cjs.map → workspace-CAJk6UmC.cjs.map} +1 -1
- package/dist/{workspace-T4IVGSoB.mjs → workspace-CsL76tpc.mjs} +11 -4
- package/dist/{workspace-T4IVGSoB.mjs.map → workspace-CsL76tpc.mjs.map} +1 -1
- package/dist/{workspace-C97QUCIf.mjs → workspace-DB2YHgOl.mjs} +4 -3
- package/dist/{workspace-C97QUCIf.mjs.map → workspace-DB2YHgOl.mjs.map} +1 -1
- package/dist/{workspace-BhiMkRuA.cjs → workspace-LKFnEBX8.cjs} +4 -3
- package/dist/{workspace-BhiMkRuA.cjs.map → workspace-LKFnEBX8.cjs.map} +1 -1
- package/package.json +14 -14
- package/dist/ready-CvP3JVEw.mjs.map +0 -1
- package/dist/ready-DXtNfk7v.cjs.map +0 -1
|
@@ -4,6 +4,7 @@ let node_fs = require("node:fs");
|
|
|
4
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
7
|
+
let node_url = require("node:url");
|
|
7
8
|
let _geekmidas_envkit = require("@geekmidas/envkit");
|
|
8
9
|
let _geekmidas_services = require("@geekmidas/services");
|
|
9
10
|
//#region src/generators/drivers.ts
|
|
@@ -165,6 +166,64 @@ const CLIENT_PLUGINS = {
|
|
|
165
166
|
"two-factor": "twoFactorClient",
|
|
166
167
|
username: "usernameClient"
|
|
167
168
|
};
|
|
169
|
+
/** Where a project keeps its test factories, relative to its root. */
|
|
170
|
+
const DEFAULT_FACTORIES_DIR = "test/factories";
|
|
171
|
+
/** A file in the factories folder that names no database construct. */
|
|
172
|
+
var UnknownFactoryFile = class extends Error {
|
|
173
|
+
file;
|
|
174
|
+
known;
|
|
175
|
+
constructor(file, known) {
|
|
176
|
+
super(`${file} names no database construct, so no test could be handed it. ` + (known.length > 0 ? `The databases: ${known.join(", ")}. ` : "This project declares no database. ") + `Name a factory after its database: test/factories/<construct>.ts.`);
|
|
177
|
+
this.file = file;
|
|
178
|
+
this.known = known;
|
|
179
|
+
this.name = "UnknownFactoryFile";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** A factory file that exports no `createFactory`. */
|
|
183
|
+
var FactoryHasNoCreate = class extends Error {
|
|
184
|
+
file;
|
|
185
|
+
constructor(file) {
|
|
186
|
+
super(`${file} is a test factory but exports no \`createFactory\`. Export \`function createFactory(db: Kysely<Database>)\` returning the factory.`);
|
|
187
|
+
this.file = file;
|
|
188
|
+
this.name = "FactoryHasNoCreate";
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The factories in a folder, each matched to the database it is named after.
|
|
193
|
+
*
|
|
194
|
+
* A file that names no database, or exports no `createFactory`, is refused
|
|
195
|
+
* rather than skipped: either is a factory no test would ever be handed.
|
|
196
|
+
*/
|
|
197
|
+
async function factorySources(folder, databases) {
|
|
198
|
+
if (!folder) return [];
|
|
199
|
+
const entries = await (0, node_fs_promises.readdir)(folder, { withFileTypes: true }).catch((error) => {
|
|
200
|
+
if (error.code === "ENOENT") return [];
|
|
201
|
+
throw error;
|
|
202
|
+
});
|
|
203
|
+
const found = [];
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (!entry.isFile() || !isFactoryFile(entry.name)) continue;
|
|
206
|
+
const file = (0, node_path.join)(folder, entry.name);
|
|
207
|
+
const id = (0, _geekmidas_manifest.canonicalId)(entry.name.slice(0, -(0, node_path.extname)(entry.name).length));
|
|
208
|
+
if (!databases.includes(id)) throw new UnknownFactoryFile(file, databases);
|
|
209
|
+
if (typeof (await import((0, node_url.pathToFileURL)(file).href)).createFactory !== "function") throw new FactoryHasNoCreate(file);
|
|
210
|
+
found.push({
|
|
211
|
+
file,
|
|
212
|
+
service: (0, _geekmidas_manifest.serviceKey)(id)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return found.sort((a, b) => a.service < b.service ? -1 : 1);
|
|
216
|
+
}
|
|
217
|
+
function isFactoryFile(file) {
|
|
218
|
+
if (file.endsWith(".d.ts")) return false;
|
|
219
|
+
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
220
|
+
return [
|
|
221
|
+
".ts",
|
|
222
|
+
".mts",
|
|
223
|
+
".js",
|
|
224
|
+
".mjs"
|
|
225
|
+
].includes((0, node_path.extname)(file));
|
|
226
|
+
}
|
|
168
227
|
/** Write the manifest, the clients and the harness; return the manifest path. */
|
|
169
228
|
async function writeTestHarness(options) {
|
|
170
229
|
const sources = {};
|
|
@@ -213,18 +272,22 @@ async function writeTestHarness(options) {
|
|
|
213
272
|
appRoot: options.root,
|
|
214
273
|
cache: cacheBackendsIn(declared, options.cacheBackend)
|
|
215
274
|
});
|
|
216
|
-
const
|
|
275
|
+
const owners = new Set(auths.map((auth) => auth.databaseId));
|
|
276
|
+
const databases = databaseSources(declared, sources).filter(({ id, kind }) => !owners.has(id) && kind !== "database-reader");
|
|
277
|
+
const factories = await factorySources(options.factories, databases.map(({ id }) => id));
|
|
217
278
|
const files = [.../* @__PURE__ */ new Set([...Object.values(manifest.constructs).map(({ source }) => source.file), ...manifest.endpoints.map(({ source }) => source.file)])];
|
|
218
279
|
const harnessFor = (dir) => harnessModule({
|
|
219
280
|
dir,
|
|
220
281
|
files,
|
|
221
|
-
|
|
282
|
+
databases,
|
|
222
283
|
surfaces: surfaces.map((id, index) => ({
|
|
223
284
|
id,
|
|
224
285
|
secured: clients[index].content.includes("authStrategies:")
|
|
225
286
|
})),
|
|
226
287
|
auths,
|
|
227
|
-
drivers
|
|
288
|
+
drivers,
|
|
289
|
+
factories,
|
|
290
|
+
mail: Object.values(declared).some(({ kind }) => kind === "email")
|
|
228
291
|
});
|
|
229
292
|
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
230
293
|
return (await Promise.all([...new Set(options.targets)].map(async (target) => {
|
|
@@ -256,7 +319,8 @@ async function authClients(declared, sources, env) {
|
|
|
256
319
|
clients.push({
|
|
257
320
|
id,
|
|
258
321
|
basePath: exported.basePath,
|
|
259
|
-
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? [])
|
|
322
|
+
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),
|
|
323
|
+
databaseId: exported.databaseId
|
|
260
324
|
});
|
|
261
325
|
}
|
|
262
326
|
return clients;
|
|
@@ -269,20 +333,16 @@ function propertyOf(id) {
|
|
|
269
333
|
return id.charAt(0).toLowerCase() + id.slice(1);
|
|
270
334
|
}
|
|
271
335
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* The same rule `featureTest` applies at runtime, applied here so `db` can be
|
|
275
|
-
* typed by the construct's schema.
|
|
336
|
+
* Every database construct the app declares — a database, a schema tenant, a
|
|
337
|
+
* reader — keyed as a test is handed its transaction.
|
|
276
338
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (services.has(exported?.service)) return source;
|
|
285
|
-
}
|
|
339
|
+
function databaseSources(declared, sources) {
|
|
340
|
+
return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && declaration.kind.startsWith("database"))).map(([id, declaration]) => ({
|
|
341
|
+
id,
|
|
342
|
+
kind: declaration.kind,
|
|
343
|
+
service: (0, _geekmidas_manifest.serviceKey)(id),
|
|
344
|
+
source: sources[id]
|
|
345
|
+
}));
|
|
286
346
|
}
|
|
287
347
|
/** An import specifier for `file`, from the harness written into `dir`. */
|
|
288
348
|
function specifierFrom(dir, file) {
|
|
@@ -290,32 +350,49 @@ function specifierFrom(dir, file) {
|
|
|
290
350
|
return path.startsWith(".") ? path : `./${path}`;
|
|
291
351
|
}
|
|
292
352
|
function harnessModule(options) {
|
|
293
|
-
const { surfaces, auths, drivers,
|
|
353
|
+
const { surfaces, auths, drivers, databases, dir, files, factories, mail } = options;
|
|
354
|
+
const magicLinks = mail ? auths.filter(({ plugins }) => plugins.includes("magicLinkClient")) : [];
|
|
355
|
+
const signIn = magicLinks.length === 1 ? magicLinks[0] : void 0;
|
|
294
356
|
const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
|
|
295
357
|
const imports = [
|
|
296
|
-
`import {${
|
|
297
|
-
...
|
|
358
|
+
`import {${databases.length ? " type DatabaseOf," : ""} featureTest, loadTestManifest${signIn ? ", signInWithMagicLink" : ""} } from '@geekmidas/constructs/testing';`,
|
|
359
|
+
...databases.map(({ id, source }) => `import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`),
|
|
298
360
|
`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
|
|
299
361
|
...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${(0, _geekmidas_manifest.kebabCase)(id)}.js';`),
|
|
300
362
|
...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
|
|
301
363
|
...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
|
|
302
364
|
...drivers.imports ? [drivers.imports] : [],
|
|
365
|
+
...factories.map(({ file, service }) => `import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`),
|
|
303
366
|
...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
|
|
304
367
|
];
|
|
305
368
|
const url = (id) => `manifest.env[${JSON.stringify((0, _geekmidas_manifest.provideKey)(id, "url"))}]!`;
|
|
306
|
-
const members = [
|
|
369
|
+
const members = [
|
|
370
|
+
...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${(0, _geekmidas_manifest.provideKey)(id, "url")}. */
|
|
307
371
|
readonly ${propertyOf(id)} = create${id}({
|
|
308
372
|
baseURL: ${url(id)},
|
|
309
373
|
fetch: this.fetch,${secured ? `
|
|
310
374
|
// Served in-process: no authorizer runs, so nothing is signed. A scheme
|
|
311
375
|
// with no strategy sends its request without credentials.
|
|
312
376
|
authStrategies: {} as never,` : ""}
|
|
313
|
-
});`),
|
|
377
|
+
});`),
|
|
378
|
+
...auths.map((auth) => ` /** \`${auth.id}\`, at ${(0, _geekmidas_manifest.provideKey)(auth.id, "url")}. */
|
|
314
379
|
readonly ${propertyOf(auth.id)} = createAuthClient({
|
|
315
380
|
baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
|
|
316
381
|
plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
|
|
317
382
|
fetchOptions: { customFetchImpl: this.fetch },
|
|
318
|
-
});`)
|
|
383
|
+
});`),
|
|
384
|
+
...signIn ? [` /**
|
|
385
|
+
* Sign in as \`email\` through \`${signIn.id}\`'s magic link, the way a person
|
|
386
|
+
* does — the email opened, the link followed. The session it then reports.
|
|
387
|
+
*/
|
|
388
|
+
signIn(email: string) {
|
|
389
|
+
return signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);
|
|
390
|
+
}`] : []
|
|
391
|
+
];
|
|
392
|
+
const factoriesType = factories.length ? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join("; ")} }` : "";
|
|
393
|
+
const factoriesOption = factories.length ? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(", ")} }` : "";
|
|
394
|
+
const databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join("; ")} }`;
|
|
395
|
+
const generics = databases.length ? `<Browser, ${databasesType}${factoriesType ? `, ${factoriesType}` : ""}>` : "";
|
|
319
396
|
return `// Generated by \`gkm test\` from the app's constructs — do not edit.
|
|
320
397
|
${imports.join("\n")}
|
|
321
398
|
|
|
@@ -332,7 +409,7 @@ ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).joi
|
|
|
332
409
|
};
|
|
333
410
|
|
|
334
411
|
/** \`it\`, for a test that drives the app the way it runs deployed. */
|
|
335
|
-
export const it = featureTest${
|
|
412
|
+
export const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption} });
|
|
336
413
|
`;
|
|
337
414
|
}
|
|
338
415
|
//#endregion
|
|
@@ -350,6 +427,12 @@ const TEST_READY_ENV = "GKM_TEST_READY";
|
|
|
350
427
|
/** Where `gkm test` records what it prepared, from where it ran. */
|
|
351
428
|
const TEST_READY_FILE = ".gkm/test-ready.json";
|
|
352
429
|
//#endregion
|
|
430
|
+
Object.defineProperty(exports, "DEFAULT_FACTORIES_DIR", {
|
|
431
|
+
enumerable: true,
|
|
432
|
+
get: function() {
|
|
433
|
+
return DEFAULT_FACTORIES_DIR;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
353
436
|
Object.defineProperty(exports, "TEST_MANIFEST_ENV", {
|
|
354
437
|
enumerable: true,
|
|
355
438
|
get: function() {
|
|
@@ -387,4 +470,4 @@ Object.defineProperty(exports, "writeTestHarness", {
|
|
|
387
470
|
}
|
|
388
471
|
});
|
|
389
472
|
|
|
390
|
-
//# sourceMappingURL=ready-
|
|
473
|
+
//# sourceMappingURL=ready-CXhoutix.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ready-CXhoutix.cjs","names":["DEFAULT_CACHE","join","existsSync","readFileSync","readdir","join","canonicalId","extname","pathToFileURL","serviceKey","discover","EndpointGenerator","OpenApiTsGenerator","kebabCase","rm","mkdir","writeFile","EnvironmentParser","serviceContext","relative","provideKey"],"sources":["../src/generators/drivers.ts","../src/test/harness.ts","../src/test/ready.ts"],"sourcesContent":["/**\n * Driver registration for generated entry points.\n *\n * A construct declares that it needs object storage and is handed one URL; the\n * scheme in that URL picks the driver that builds the client. Which drivers\n * exist is therefore the *entry point's* decision, not the construct's and not\n * the application's — an app that never talks to S3 should never resolve the AWS\n * SDK, and application code that registers a driver has named a provider in the\n * one layer this design keeps provider-free.\n *\n * So the generated entries register: `gkm dev`'s server for local development,\n * and each Lambda handler for its own target. Registration is an explicit call\n * rather than an import side effect, because a side-effecting module is exactly\n * what a bundler is entitled to drop.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type CacheBackend, DEFAULT_CACHE } from '../types.js';\n\n/** The `s3://` driver, which serves MinIO locally and S3 deployed. */\nconst S3 = {\n\timports: `import { registerStorageDriver } from '@geekmidas/storage';\\nimport { s3Driver } from '@geekmidas/storage/aws';`,\n\tsetup: 'registerStorageDriver(s3Driver);',\n};\n\n/** What a generated entry needs in order to register the drivers it uses. */\nexport interface RuntimeDrivers {\n\t/** Import lines, or `''` when there is nothing to register. */\n\timports: string;\n\t/** The registration calls, or `''`. */\n\tsetup: string;\n}\n\n/** @deprecated The same shape, from when only storage had drivers. */\nexport type StorageDrivers = RuntimeDrivers;\n\nconst NONE: RuntimeDrivers = { imports: '', setup: '' };\n\n/**\n * The cache driver for a backend — exactly one, never all three.\n *\n * Keyed off the *backend* rather than off a dependency, because unlike storage\n * the backend already says which driver is right: a project caching in Postgres\n * resolves `pg` and never `ioredis`, and one caching in Upstash resolves\n * neither. That is the whole reason the drivers live behind separate subpaths.\n *\n * Both HTTP schemes are registered for Upstash, because the local proxy speaks\n * `http://` and Upstash speaks `https://`, and an entry that registered one\n * would work in exactly one of the two places.\n */\nconst CACHE_DRIVERS: Record<CacheBackend, RuntimeDrivers> = {\n\tupstash: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(upstashCacheDriver);\\nregisterCacheDriver(upstashInsecureCacheDriver);',\n\t},\n\telasticache: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(redisCacheDriver);\\nregisterCacheDriver(redissCacheDriver);',\n\t},\n\tdb: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,\n\t\tsetup: 'registerCacheDriver(postgresCacheDriver);',\n\t},\n};\n\n/**\n * Everything a generated entry should register: object storage, and the cache.\n *\n * Merged here rather than threaded separately, because a generated file has one\n * import block and one setup block whatever fills them.\n */\nexport function driversFor(options: {\n\tappRoot: string;\n\t/**\n\t * The cache backends actually in play — normally one, and more than one only\n\t * where an app declares two caches that live in different places.\n\t *\n\t * Absent or empty means nothing declared a cache, so no cache driver is\n\t * registered at all.\n\t */\n\tcache?: CacheBackend | readonly CacheBackend[] | false;\n}): RuntimeDrivers {\n\tconst backends =\n\t\toptions.cache === false || options.cache === undefined\n\t\t\t? []\n\t\t\t: typeof options.cache === 'string'\n\t\t\t\t? [options.cache]\n\t\t\t\t: options.cache;\n\n\tconst parts = [\n\t\tstorageDriversFor(options.appRoot),\n\t\t...[...new Set(backends)].map(\n\t\t\t(backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws],\n\t\t),\n\t].filter((part) => part.imports || part.setup);\n\n\tif (parts.length === 0) return NONE;\n\n\treturn {\n\t\timports: parts.map((part) => part.imports).join('\\n'),\n\t\tsetup: parts.map((part) => part.setup).join('\\n'),\n\t};\n}\n\n/**\n * Which cache backends an app's entry actually has to speak.\n *\n * The config answers this for a cache that named nowhere, and the *declaration*\n * answers it for one that named a database — `orders.cache('Sessions')` is a\n * statement about the application, so a deployment cannot move that cache and\n * the entry cannot register a driver for somewhere else.\n *\n * Reading only the config is how an entry ends up registering the Upstash\n * driver for a URL the target composed as `postgres://`, which fails at the\n * first request with `UnregisteredCacheScheme` and a stack that points at the\n * cache rather than at the config that disagreed.\n */\nexport function cacheBackendsIn(\n\tmanifest: Record<string, { kind: string; of?: string }>,\n\tconfigured: CacheBackend,\n): CacheBackend[] {\n\tconst caches = Object.values(manifest).filter((d) => d.kind === 'cache');\n\n\treturn [...new Set(caches.map((c) => (c.of ? 'db' : configured)))];\n}\n\n/**\n * The drivers an app's entry points should register.\n *\n * Keyed off the dependency rather than off a declared bucket, because the entry\n * is generated before discovery has run on the watcher's rebuild path — and an\n * app that installed `@geekmidas/storage` has already paid for the SDK it would\n * otherwise resolve lazily.\n */\nexport function storageDriversFor(appRoot: string): RuntimeDrivers {\n\treturn dependsOnStorage(appRoot) ? S3 : NONE;\n}\n\nfunction dependsOnStorage(appRoot: string): boolean {\n\tconst manifest = join(appRoot, 'package.json');\n\tif (!existsSync(manifest)) return false;\n\n\ttry {\n\t\tconst pkg = JSON.parse(readFileSync(manifest, 'utf8')) as {\n\t\t\tdependencies?: Record<string, string>;\n\t\t\tdevDependencies?: Record<string, string>;\n\t\t};\n\n\t\treturn Boolean(\n\t\t\tpkg.dependencies?.['@geekmidas/storage'] ??\n\t\t\t\tpkg.devDependencies?.['@geekmidas/storage'],\n\t\t);\n\t} catch {\n\t\t// An unreadable package.json is the build's problem, not this function's.\n\t\treturn false;\n\t}\n}\n","/**\n * What `gkm test` writes for a feature test to be built from.\n *\n * `gkm test` already discovers an app's constructs and resolves the test\n * stage's environment; this keeps both, under `.gkm/test/`:\n *\n * - `manifest.json` — which construct and endpoint is exported where, and the\n * environment the test stage resolved, keyed as the constructs derive keys.\n * - `clients/<surface>.ts` — each surface's typed client, from the same\n * generator `gkm build` uses for the app's own.\n * - `index.ts` — the configured `it`, and a `Browser` holding one client per\n * surface and one per auth server, each on the browser's `fetch`.\n *\n * A test imports `it` from there and declares nothing — not a construct, not an\n * environment key, not a client.\n */\n\nimport { mkdir, readdir, rm, writeFile } from 'node:fs/promises';\nimport { extname, join, relative } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { BetterAuth } from '@geekmidas/constructs/auth';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport type { TestManifest } from '@geekmidas/constructs/testing';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\tcanonicalId,\n\tkebabCase,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport { serviceContext } from '@geekmidas/services';\nimport {\n\tcacheBackendsIn,\n\tdriversFor,\n\ttype RuntimeDrivers,\n} from '../generators/drivers.js';\nimport { EndpointGenerator } from '../generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from '../generators/OpenApiTsGenerator.js';\nimport { type ConstructSource, discover } from '../reconcile/discover.js';\nimport type { CacheBackend } from '../types.js';\n\n/**\n * The variable the manifest's path travels in — what\n * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:\n * that entry carries the whole harness (MSW, Hono, Vitest), and importing it\n * here put all of it on the start-up path of every `gkm` command.\n */\nexport const TEST_MANIFEST_ENV = 'GKM_TEST_MANIFEST';\n\n/** Where the harness lives, relative to the workspace root. */\nexport const TEST_HARNESS_DIR = '.gkm/test';\n\n/**\n * Better-auth's client plugin for each server plugin id.\n *\n * Written down rather than derived: the ids and the client names do not follow\n * one rule (`admin` pairs with `adminClient`, `email-otp` with\n * `emailOTPClient`), and a guess that missed would hand a test a client\n * silently lacking every method of that plugin. A server plugin not listed has\n * no client counterpart — `bearer`, `open-api` — or needs one from another\n * package, and is left out.\n */\nconst CLIENT_PLUGINS: Readonly<Record<string, string>> = {\n\tadmin: 'adminClient',\n\tanonymous: 'anonymousClient',\n\t'device-authorization': 'deviceAuthorizationClient',\n\t'email-otp': 'emailOTPClient',\n\tjwt: 'jwtClient',\n\t'last-login-method': 'lastLoginMethodClient',\n\t'magic-link': 'magicLinkClient',\n\t'multi-session': 'multiSessionClient',\n\t'oauth-popup': 'oauthPopupClient',\n\t'one-tap': 'oneTapClient',\n\t'one-time-token': 'oneTimeTokenClient',\n\torganization: 'organizationClient',\n\t'phone-number': 'phoneNumberClient',\n\tsiwe: 'siweClient',\n\t'two-factor': 'twoFactorClient',\n\tusername: 'usernameClient',\n};\n\nexport interface WriteTestHarnessOptions {\n\t/** The workspace root: where discovery runs from. */\n\troot: string;\n\t/**\n\t * The directories to write `.gkm/test/` into — each app's own. A package\n\t * maps its `#test` import only to a path inside itself, so each app gets its\n\t * own copy of the harness rather than one at the root it cannot reach.\n\t */\n\ttargets: string[];\n\t/** The constructs glob — the same patterns reconcile and the build use. */\n\tpatterns: string[];\n\t/** The stage the environment was resolved for. */\n\tstage: string;\n\t/** The environment the suite runs with. */\n\tenv: Record<string, string>;\n\t/**\n\t * Where a cache lives when it names nowhere — the target's default, which\n\t * the build's server entry registers a driver for.\n\t */\n\tcacheBackend: CacheBackend;\n\t/**\n\t * The folder of test factories, absolute: `test/factories` at the root, or\n\t * where `test.factories` in the config points. Absent or empty, a test is\n\t * handed no factories.\n\t */\n\tfactories?: string;\n}\n\n/** Where a project keeps its test factories, relative to its root. */\nexport const DEFAULT_FACTORIES_DIR = 'test/factories';\n\n/** A file in the factories folder that names no database construct. */\nexport class UnknownFactoryFile extends Error {\n\tconstructor(\n\t\treadonly file: string,\n\t\treadonly known: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${file} names no database construct, so no test could be handed it. ` +\n\t\t\t\t(known.length > 0\n\t\t\t\t\t? `The databases: ${known.join(', ')}. `\n\t\t\t\t\t: 'This project declares no database. ') +\n\t\t\t\t`Name a factory after its database: test/factories/<construct>.ts.`,\n\t\t);\n\t\tthis.name = 'UnknownFactoryFile';\n\t}\n}\n\n/** A factory file that exports no `createFactory`. */\nexport class FactoryHasNoCreate extends Error {\n\tconstructor(readonly file: string) {\n\t\tsuper(\n\t\t\t`${file} is a test factory but exports no \\`createFactory\\`. ` +\n\t\t\t\t`Export \\`function createFactory(db: Kysely<Database>)\\` returning ` +\n\t\t\t\t`the factory.`,\n\t\t);\n\t\tthis.name = 'FactoryHasNoCreate';\n\t}\n}\n\n/** One database's factory: the file, and the service name a test keys it by. */\nexport interface FactorySource {\n\tfile: string;\n\tservice: string;\n}\n\n/**\n * The factories in a folder, each matched to the database it is named after.\n *\n * A file that names no database, or exports no `createFactory`, is refused\n * rather than skipped: either is a factory no test would ever be handed.\n */\nexport async function factorySources(\n\tfolder: string | undefined,\n\tdatabases: readonly string[],\n): Promise<FactorySource[]> {\n\tif (!folder) return [];\n\n\tconst entries = await readdir(folder, { withFileTypes: true }).catch(\n\t\t(error: NodeJS.ErrnoException) => {\n\t\t\tif (error.code === 'ENOENT') return [];\n\t\t\tthrow error;\n\t\t},\n\t);\n\n\tconst found: FactorySource[] = [];\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !isFactoryFile(entry.name)) continue;\n\n\t\tconst file = join(folder, entry.name);\n\t\tconst id = canonicalId(entry.name.slice(0, -extname(entry.name).length));\n\t\tif (!databases.includes(id)) {\n\t\t\tthrow new UnknownFactoryFile(file, databases);\n\t\t}\n\n\t\tconst module = (await import(pathToFileURL(file).href)) as {\n\t\t\tcreateFactory?: unknown;\n\t\t};\n\t\tif (typeof module.createFactory !== 'function') {\n\t\t\tthrow new FactoryHasNoCreate(file);\n\t\t}\n\n\t\tfound.push({ file, service: serviceKey(id) });\n\t}\n\n\treturn found.sort((a, b) => (a.service < b.service ? -1 : 1));\n}\n\nfunction isFactoryFile(file: string): boolean {\n\tif (file.endsWith('.d.ts')) return false;\n\tif (/\\.(spec|test)\\.[cm]?[jt]s$/.test(file)) return false;\n\treturn ['.ts', '.mts', '.js', '.mjs'].includes(extname(file));\n}\n\n/** Write the manifest, the clients and the harness; return the manifest path. */\nexport async function writeTestHarness(\n\toptions: WriteTestHarnessOptions,\n): Promise<string> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst declared = await discover({\n\t\tpatterns: options.patterns,\n\t\tcwd: options.root,\n\t\tsources,\n\t});\n\n\tconst loaded = await new EndpointGenerator().load(\n\t\toptions.patterns,\n\t\toptions.root,\n\t);\n\tconst endpoints = loaded\n\t\t.map(({ key, construct, path }) => ({\n\t\t\tsurface: construct.owner ?? construct.surface?.id,\n\t\t\tconstruct,\n\t\t\tsource: { file: path.absolute, export: key },\n\t\t}))\n\t\t.filter(\n\t\t\t(endpoint): endpoint is typeof endpoint & { surface: string } =>\n\t\t\t\tendpoint.surface !== undefined,\n\t\t);\n\n\tconst manifest: TestManifest = {\n\t\tstage: options.stage,\n\t\tconstructs: Object.fromEntries(\n\t\t\tObject.entries(declared).flatMap(([id, declaration]) => {\n\t\t\t\tconst source = sources[id];\n\t\t\t\treturn source\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkind: declaration.kind,\n\t\t\t\t\t\t\t\t\tsource: { file: source.file, export: source.exportName },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t]\n\t\t\t\t\t: [];\n\t\t\t}),\n\t\t),\n\t\tendpoints: endpoints.map(({ surface, source }) => ({ surface, source })),\n\t\tenv: options.env,\n\t};\n\t// One typed client per surface, from the generator the build uses.\n\tconst surfaces = [...new Set(endpoints.map(({ surface }) => surface))];\n\tconst generator = new OpenApiTsGenerator();\n\tconst clients = await Promise.all(\n\t\tsurfaces.map(async (surface) => ({\n\t\t\tfile: `${kebabCase(surface)}.ts`,\n\t\t\tcontent: await generator.generate(\n\t\t\t\tendpoints\n\t\t\t\t\t.filter((endpoint) => endpoint.surface === surface)\n\t\t\t\t\t.map(({ construct }) => construct as Endpoint<any, any, any, any>),\n\t\t\t\t{ title: surface, version: '1.0.0' },\n\t\t\t),\n\t\t})),\n\t);\n\n\tconst auths = await authClients(declared, sources, options.env);\n\t// The drivers the server entry registers — which cache and storage clients\n\t// exist is the entry's decision, and the harness stands where the entry does.\n\tconst drivers = driversFor({\n\t\tappRoot: options.root,\n\t\tcache: cacheBackendsIn(declared, options.cacheBackend),\n\t});\n\n\t// The app's own databases: not a tenant an auth server owns — reached\n\t// through that server — and not a reader of one. What a test is handed.\n\tconst owners = new Set(auths.map((auth) => auth.databaseId));\n\tconst databases = databaseSources(declared, sources).filter(\n\t\t({ id, kind }) => !owners.has(id) && kind !== 'database-reader',\n\t);\n\tconst factories = await factorySources(\n\t\toptions.factories,\n\t\tdatabases.map(({ id }) => id),\n\t);\n\tconst files = [\n\t\t...new Set([\n\t\t\t...Object.values(manifest.constructs).map(({ source }) => source.file),\n\t\t\t...manifest.endpoints.map(({ source }) => source.file),\n\t\t]),\n\t];\n\tconst harnessFor = (dir: string) =>\n\t\tharnessModule({\n\t\t\tdir,\n\t\t\tfiles,\n\t\t\tdatabases,\n\t\t\tsurfaces: surfaces.map((id, index) => ({\n\t\t\t\tid,\n\t\t\t\t// The client the generator writes for a surface with authorizers\n\t\t\t\t// takes a strategy per scheme; one without takes none.\n\t\t\t\tsecured: clients[index]!.content.includes('authStrategies:'),\n\t\t\t})),\n\t\t\tauths,\n\t\t\tdrivers,\n\t\t\tfactories,\n\t\t\tmail: Object.values(declared).some(({ kind }) => kind === 'email'),\n\t\t});\n\tconst json = `${JSON.stringify(manifest, null, 2)}\\n`;\n\n\tconst written = await Promise.all(\n\t\t[...new Set(options.targets)].map(async (target) => {\n\t\t\tconst dir = join(target, TEST_HARNESS_DIR);\n\t\t\t// Rewritten whole on every run: a client for a surface that no longer\n\t\t\t// exists is not something a test should be able to import.\n\t\t\tawait rm(dir, { recursive: true, force: true });\n\t\t\tawait mkdir(join(dir, 'clients'), { recursive: true });\n\t\t\tawait writeFile(join(dir, 'manifest.json'), json);\n\t\t\tawait writeFile(join(dir, 'index.ts'), harnessFor(dir));\n\t\t\tfor (const client of clients) {\n\t\t\t\tawait writeFile(join(dir, 'clients', client.file), client.content);\n\t\t\t}\n\t\t\treturn join(dir, 'manifest.json');\n\t\t}),\n\t);\n\n\treturn written[0]!;\n}\n\ninterface AuthClient {\n\tid: string;\n\tbasePath: string;\n\tplugins: string[];\n\t/** The schema tenant it owns — reached through it, never by a test. */\n\tdatabaseId: string;\n}\n\n/** Each auth server, with the client plugins its server plugins pair with. */\nasync function authClients(\n\tdeclared: Awaited<ReturnType<typeof discover>>,\n\tsources: Record<string, ConstructSource>,\n\tenv: Record<string, string>,\n): Promise<AuthClient[]> {\n\tconst clients: AuthClient[] = [];\n\tconst envParser = new EnvironmentParser({ ...env });\n\n\tfor (const [id, declaration] of Object.entries(declared)) {\n\t\tconst source = sources[id];\n\t\tif (declaration.kind !== 'rest-api' || !source) continue;\n\n\t\tconst exported = (await import(source.file))[source.exportName] as unknown;\n\t\tif (!isBetterAuth(exported) || exported.id !== id) continue;\n\n\t\tconst ids = await exported.pluginIds({\n\t\t\tenvParser,\n\t\t\tcontext: serviceContext,\n\t\t});\n\t\tclients.push({\n\t\t\tid,\n\t\t\tbasePath: exported.basePath,\n\t\t\tplugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),\n\t\t\tdatabaseId: exported.databaseId,\n\t\t});\n\t}\n\n\treturn clients;\n}\n\nfunction isBetterAuth(value: unknown): value is BetterAuth {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'pluginIds' in value &&\n\t\t'basePath' in value\n\t);\n}\n\n/** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */\nfunction propertyOf(id: string): string {\n\treturn id.charAt(0).toLowerCase() + id.slice(1);\n}\n\n/**\n * The database a test's `db` is — the one the endpoints were given with\n * `.database()`, found by the service they hold, which is the construct's own.\n * The same rule `featureTest` applies at runtime, applied here so `db` can be\n * typed by the construct's schema.\n */\n/** A database construct: its id, its service name, and where it is exported. */\ninterface DatabaseSource {\n\tid: string;\n\tkind: string;\n\tservice: string;\n\tsource: ConstructSource;\n}\n\n/**\n * Every database construct the app declares — a database, a schema tenant, a\n * reader — keyed as a test is handed its transaction.\n */\nfunction databaseSources(\n\tdeclared: Awaited<ReturnType<typeof discover>>,\n\tsources: Record<string, ConstructSource>,\n): DatabaseSource[] {\n\treturn Object.entries(declared)\n\t\t.filter(([id, declaration]) =>\n\t\t\tBoolean(sources[id] && declaration.kind.startsWith('database')),\n\t\t)\n\t\t.map(([id, declaration]) => ({\n\t\t\tid,\n\t\t\tkind: declaration.kind,\n\t\t\tservice: serviceKey(id),\n\t\t\tsource: sources[id]!,\n\t\t}));\n}\n\n/** An import specifier for `file`, from the harness written into `dir`. */\nfunction specifierFrom(dir: string, file: string): string {\n\tconst path = relative(dir, file).replace(/\\.tsx?$/, '.js');\n\treturn path.startsWith('.') ? path : `./${path}`;\n}\n\nfunction harnessModule(options: {\n\tdir: string;\n\t/** Every module the manifest points at, by absolute path. */\n\tfiles: string[];\n\tdatabases: DatabaseSource[];\n\tsurfaces: { id: string; secured: boolean }[];\n\tauths: AuthClient[];\n\tdrivers: RuntimeDrivers;\n\tfactories: FactorySource[];\n\t/** Whether the app sends mail — what a magic-link sign-in is read from. */\n\tmail: boolean;\n}): string {\n\tconst { surfaces, auths, drivers, databases, dir, files, factories, mail } =\n\t\toptions;\n\t// One auth server a test can sign in to without a person: a magic link,\n\t// read from the app's inbox. With two, which one \\`signIn\\` means is a\n\t// guess, so neither gets it.\n\tconst magicLinks = mail\n\t\t? auths.filter(({ plugins }) => plugins.includes('magicLinkClient'))\n\t\t: [];\n\tconst signIn = magicLinks.length === 1 ? magicLinks[0] : undefined;\n\tconst plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();\n\n\tconst imports = [\n\t\t`import {${databases.length ? ' type DatabaseOf,' : ''} featureTest, loadTestManifest${signIn ? ', signInWithMagicLink' : ''} } from '@geekmidas/constructs/testing';`,\n\t\t...databases.map(\n\t\t\t({ id, source }) =>\n\t\t\t\t`import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`,\n\t\t),\n\t\t`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,\n\t\t...surfaces.map(\n\t\t\t({ id }) =>\n\t\t\t\t`import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`,\n\t\t),\n\t\t...(auths.length\n\t\t\t? [`import { createAuthClient } from 'better-auth/client';`]\n\t\t\t: []),\n\t\t...(plugins.length\n\t\t\t? [`import { ${plugins.join(', ')} } from 'better-auth/client/plugins';`]\n\t\t\t: []),\n\t\t...(drivers.imports ? [drivers.imports] : []),\n\t\t...factories.map(\n\t\t\t({ file, service }) =>\n\t\t\t\t`import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`,\n\t\t),\n\t\t// Every construct and endpoint module, imported here — inside the app,\n\t\t// where its tsconfig paths resolve — rather than by path from the kit.\n\t\t...files.map(\n\t\t\t(file, index) =>\n\t\t\t\t`import * as __module${index} from '${specifierFrom(dir, file)}';`,\n\t\t),\n\t];\n\n\tconst url = (id: string) =>\n\t\t`manifest.env[${JSON.stringify(provideKey(id, 'url'))}]!`;\n\n\tconst members = [\n\t\t...surfaces.map(\n\t\t\t({ id, secured }) => `\t/** \\`${id}\\`, at ${provideKey(id, 'url')}. */\n\treadonly ${propertyOf(id)} = create${id}({\n\t\tbaseURL: ${url(id)},\n\t\tfetch: this.fetch,${\n\t\t\tsecured\n\t\t\t\t? `\n\t\t// Served in-process: no authorizer runs, so nothing is signed. A scheme\n\t\t// with no strategy sends its request without credentials.\n\t\tauthStrategies: {} as never,`\n\t\t\t\t: ''\n\t\t}\n\t});`,\n\t\t),\n\t\t...auths.map(\n\t\t\t(auth) => `\t/** \\`${auth.id}\\`, at ${provideKey(auth.id, 'url')}. */\n\treadonly ${propertyOf(auth.id)} = createAuthClient({\n\t\tbaseURL: \\`\\${${url(auth.id)}}${auth.basePath}\\`,\n\t\tplugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(', ')}],\n\t\tfetchOptions: { customFetchImpl: this.fetch },\n\t});`,\n\t\t),\n\t\t...(signIn\n\t\t\t? [\n\t\t\t\t\t`\t/**\n\t * Sign in as \\`email\\` through \\`${signIn.id}\\`'s magic link, the way a person\n\t * does — the email opened, the link followed. The session it then reports.\n\t */\n\tsignIn(email: string) {\n\t\treturn signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);\n\t}`,\n\t\t\t\t]\n\t\t\t: []),\n\t];\n\n\t// Each database's factory, keyed as the test is handed it.\n\tconst factoriesType = factories.length\n\t\t? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join('; ')} }`\n\t\t: '';\n\tconst factoriesOption = factories.length\n\t\t? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(', ')} }`\n\t\t: '';\n\t// Each database's schema, keyed as the test is handed its transaction.\n\tconst databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join('; ')} }`;\n\tconst generics = databases.length\n\t\t? `<Browser, ${databasesType}${factoriesType ? `, ${factoriesType}` : ''}>`\n\t\t: '';\n\n\treturn `// Generated by \\`gkm test\\` from the app's constructs — do not edit.\n${imports.join('\\n')}\n\n${drivers.setup ? `// The drivers the app's server entry registers, registered the same way.\\n${drivers.setup}\\n\\n` : ''}const manifest = loadTestManifest(new URL('./manifest.json', import.meta.url));\n\n/** A browser with a client for every surface the app declares. */\nexport class Browser extends TestBrowser {\n${members.join('\\n\\n')}\n}\n\n/** The app's modules, by the path the manifest records for each. */\nconst modules = {\n${files.map((file, index) => `\\t${JSON.stringify(file)}: __module${index},`).join('\\n')}\n};\n\n/** \\`it\\`, for a test that drives the app the way it runs deployed. */\nexport const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption} });\n`;\n}\n","/**\n * The hand-off between `gkm test` and the Vitest global setup.\n *\n * `gkm test` reconciles and migrates the test stage before Vitest starts; the\n * setup, run inside Vitest, only has to load what that left. Started by plain\n * `vitest` instead, the setup runs `gkm test --setup` itself first — so there is\n * one way the stage gets ready, whichever started the suite.\n */\n\n/** Set on the Vitest process by `gkm test`: the stage is ready, here. */\nexport const TEST_READY_ENV = 'GKM_TEST_READY';\n\n/** Where `gkm test` records what it prepared, from where it ran. */\nexport const TEST_READY_FILE = '.gkm/test-ready.json';\n\nexport interface TestReady {\n\t/** The resolved environment, as a JSON object of strings. */\n\tenv: string;\n\t/** The test manifest the `#test` harness reads. */\n\tmanifest?: string;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { a as DEFAULT_CACHE, t as discover } from "./discover-CMxesskG.mjs";
|
|
2
2
|
import { n as EndpointGenerator, t as OpenApiTsGenerator } from "./OpenApiTsGenerator-hDINScnz.mjs";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join, relative } from "node:path";
|
|
6
|
-
import { kebabCase, provideKey } from "@geekmidas/manifest";
|
|
4
|
+
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { extname, join, relative } from "node:path";
|
|
6
|
+
import { canonicalId, kebabCase, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
8
9
|
import { serviceContext } from "@geekmidas/services";
|
|
9
10
|
//#region src/generators/drivers.ts
|
|
@@ -165,6 +166,64 @@ const CLIENT_PLUGINS = {
|
|
|
165
166
|
"two-factor": "twoFactorClient",
|
|
166
167
|
username: "usernameClient"
|
|
167
168
|
};
|
|
169
|
+
/** Where a project keeps its test factories, relative to its root. */
|
|
170
|
+
const DEFAULT_FACTORIES_DIR = "test/factories";
|
|
171
|
+
/** A file in the factories folder that names no database construct. */
|
|
172
|
+
var UnknownFactoryFile = class extends Error {
|
|
173
|
+
file;
|
|
174
|
+
known;
|
|
175
|
+
constructor(file, known) {
|
|
176
|
+
super(`${file} names no database construct, so no test could be handed it. ` + (known.length > 0 ? `The databases: ${known.join(", ")}. ` : "This project declares no database. ") + `Name a factory after its database: test/factories/<construct>.ts.`);
|
|
177
|
+
this.file = file;
|
|
178
|
+
this.known = known;
|
|
179
|
+
this.name = "UnknownFactoryFile";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** A factory file that exports no `createFactory`. */
|
|
183
|
+
var FactoryHasNoCreate = class extends Error {
|
|
184
|
+
file;
|
|
185
|
+
constructor(file) {
|
|
186
|
+
super(`${file} is a test factory but exports no \`createFactory\`. Export \`function createFactory(db: Kysely<Database>)\` returning the factory.`);
|
|
187
|
+
this.file = file;
|
|
188
|
+
this.name = "FactoryHasNoCreate";
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The factories in a folder, each matched to the database it is named after.
|
|
193
|
+
*
|
|
194
|
+
* A file that names no database, or exports no `createFactory`, is refused
|
|
195
|
+
* rather than skipped: either is a factory no test would ever be handed.
|
|
196
|
+
*/
|
|
197
|
+
async function factorySources(folder, databases) {
|
|
198
|
+
if (!folder) return [];
|
|
199
|
+
const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
|
|
200
|
+
if (error.code === "ENOENT") return [];
|
|
201
|
+
throw error;
|
|
202
|
+
});
|
|
203
|
+
const found = [];
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (!entry.isFile() || !isFactoryFile(entry.name)) continue;
|
|
206
|
+
const file = join(folder, entry.name);
|
|
207
|
+
const id = canonicalId(entry.name.slice(0, -extname(entry.name).length));
|
|
208
|
+
if (!databases.includes(id)) throw new UnknownFactoryFile(file, databases);
|
|
209
|
+
if (typeof (await import(pathToFileURL(file).href)).createFactory !== "function") throw new FactoryHasNoCreate(file);
|
|
210
|
+
found.push({
|
|
211
|
+
file,
|
|
212
|
+
service: serviceKey(id)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return found.sort((a, b) => a.service < b.service ? -1 : 1);
|
|
216
|
+
}
|
|
217
|
+
function isFactoryFile(file) {
|
|
218
|
+
if (file.endsWith(".d.ts")) return false;
|
|
219
|
+
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
220
|
+
return [
|
|
221
|
+
".ts",
|
|
222
|
+
".mts",
|
|
223
|
+
".js",
|
|
224
|
+
".mjs"
|
|
225
|
+
].includes(extname(file));
|
|
226
|
+
}
|
|
168
227
|
/** Write the manifest, the clients and the harness; return the manifest path. */
|
|
169
228
|
async function writeTestHarness(options) {
|
|
170
229
|
const sources = {};
|
|
@@ -213,18 +272,22 @@ async function writeTestHarness(options) {
|
|
|
213
272
|
appRoot: options.root,
|
|
214
273
|
cache: cacheBackendsIn(declared, options.cacheBackend)
|
|
215
274
|
});
|
|
216
|
-
const
|
|
275
|
+
const owners = new Set(auths.map((auth) => auth.databaseId));
|
|
276
|
+
const databases = databaseSources(declared, sources).filter(({ id, kind }) => !owners.has(id) && kind !== "database-reader");
|
|
277
|
+
const factories = await factorySources(options.factories, databases.map(({ id }) => id));
|
|
217
278
|
const files = [.../* @__PURE__ */ new Set([...Object.values(manifest.constructs).map(({ source }) => source.file), ...manifest.endpoints.map(({ source }) => source.file)])];
|
|
218
279
|
const harnessFor = (dir) => harnessModule({
|
|
219
280
|
dir,
|
|
220
281
|
files,
|
|
221
|
-
|
|
282
|
+
databases,
|
|
222
283
|
surfaces: surfaces.map((id, index) => ({
|
|
223
284
|
id,
|
|
224
285
|
secured: clients[index].content.includes("authStrategies:")
|
|
225
286
|
})),
|
|
226
287
|
auths,
|
|
227
|
-
drivers
|
|
288
|
+
drivers,
|
|
289
|
+
factories,
|
|
290
|
+
mail: Object.values(declared).some(({ kind }) => kind === "email")
|
|
228
291
|
});
|
|
229
292
|
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
230
293
|
return (await Promise.all([...new Set(options.targets)].map(async (target) => {
|
|
@@ -256,7 +319,8 @@ async function authClients(declared, sources, env) {
|
|
|
256
319
|
clients.push({
|
|
257
320
|
id,
|
|
258
321
|
basePath: exported.basePath,
|
|
259
|
-
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? [])
|
|
322
|
+
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),
|
|
323
|
+
databaseId: exported.databaseId
|
|
260
324
|
});
|
|
261
325
|
}
|
|
262
326
|
return clients;
|
|
@@ -269,20 +333,16 @@ function propertyOf(id) {
|
|
|
269
333
|
return id.charAt(0).toLowerCase() + id.slice(1);
|
|
270
334
|
}
|
|
271
335
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* The same rule `featureTest` applies at runtime, applied here so `db` can be
|
|
275
|
-
* typed by the construct's schema.
|
|
336
|
+
* Every database construct the app declares — a database, a schema tenant, a
|
|
337
|
+
* reader — keyed as a test is handed its transaction.
|
|
276
338
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (services.has(exported?.service)) return source;
|
|
285
|
-
}
|
|
339
|
+
function databaseSources(declared, sources) {
|
|
340
|
+
return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && declaration.kind.startsWith("database"))).map(([id, declaration]) => ({
|
|
341
|
+
id,
|
|
342
|
+
kind: declaration.kind,
|
|
343
|
+
service: serviceKey(id),
|
|
344
|
+
source: sources[id]
|
|
345
|
+
}));
|
|
286
346
|
}
|
|
287
347
|
/** An import specifier for `file`, from the harness written into `dir`. */
|
|
288
348
|
function specifierFrom(dir, file) {
|
|
@@ -290,32 +350,49 @@ function specifierFrom(dir, file) {
|
|
|
290
350
|
return path.startsWith(".") ? path : `./${path}`;
|
|
291
351
|
}
|
|
292
352
|
function harnessModule(options) {
|
|
293
|
-
const { surfaces, auths, drivers,
|
|
353
|
+
const { surfaces, auths, drivers, databases, dir, files, factories, mail } = options;
|
|
354
|
+
const magicLinks = mail ? auths.filter(({ plugins }) => plugins.includes("magicLinkClient")) : [];
|
|
355
|
+
const signIn = magicLinks.length === 1 ? magicLinks[0] : void 0;
|
|
294
356
|
const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
|
|
295
357
|
const imports = [
|
|
296
|
-
`import {${
|
|
297
|
-
...
|
|
358
|
+
`import {${databases.length ? " type DatabaseOf," : ""} featureTest, loadTestManifest${signIn ? ", signInWithMagicLink" : ""} } from '@geekmidas/constructs/testing';`,
|
|
359
|
+
...databases.map(({ id, source }) => `import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`),
|
|
298
360
|
`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
|
|
299
361
|
...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`),
|
|
300
362
|
...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
|
|
301
363
|
...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
|
|
302
364
|
...drivers.imports ? [drivers.imports] : [],
|
|
365
|
+
...factories.map(({ file, service }) => `import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`),
|
|
303
366
|
...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
|
|
304
367
|
];
|
|
305
368
|
const url = (id) => `manifest.env[${JSON.stringify(provideKey(id, "url"))}]!`;
|
|
306
|
-
const members = [
|
|
369
|
+
const members = [
|
|
370
|
+
...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${provideKey(id, "url")}. */
|
|
307
371
|
readonly ${propertyOf(id)} = create${id}({
|
|
308
372
|
baseURL: ${url(id)},
|
|
309
373
|
fetch: this.fetch,${secured ? `
|
|
310
374
|
// Served in-process: no authorizer runs, so nothing is signed. A scheme
|
|
311
375
|
// with no strategy sends its request without credentials.
|
|
312
376
|
authStrategies: {} as never,` : ""}
|
|
313
|
-
});`),
|
|
377
|
+
});`),
|
|
378
|
+
...auths.map((auth) => ` /** \`${auth.id}\`, at ${provideKey(auth.id, "url")}. */
|
|
314
379
|
readonly ${propertyOf(auth.id)} = createAuthClient({
|
|
315
380
|
baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
|
|
316
381
|
plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
|
|
317
382
|
fetchOptions: { customFetchImpl: this.fetch },
|
|
318
|
-
});`)
|
|
383
|
+
});`),
|
|
384
|
+
...signIn ? [` /**
|
|
385
|
+
* Sign in as \`email\` through \`${signIn.id}\`'s magic link, the way a person
|
|
386
|
+
* does — the email opened, the link followed. The session it then reports.
|
|
387
|
+
*/
|
|
388
|
+
signIn(email: string) {
|
|
389
|
+
return signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);
|
|
390
|
+
}`] : []
|
|
391
|
+
];
|
|
392
|
+
const factoriesType = factories.length ? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join("; ")} }` : "";
|
|
393
|
+
const factoriesOption = factories.length ? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(", ")} }` : "";
|
|
394
|
+
const databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join("; ")} }`;
|
|
395
|
+
const generics = databases.length ? `<Browser, ${databasesType}${factoriesType ? `, ${factoriesType}` : ""}>` : "";
|
|
319
396
|
return `// Generated by \`gkm test\` from the app's constructs — do not edit.
|
|
320
397
|
${imports.join("\n")}
|
|
321
398
|
|
|
@@ -332,7 +409,7 @@ ${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).joi
|
|
|
332
409
|
};
|
|
333
410
|
|
|
334
411
|
/** \`it\`, for a test that drives the app the way it runs deployed. */
|
|
335
|
-
export const it = featureTest${
|
|
412
|
+
export const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption} });
|
|
336
413
|
`;
|
|
337
414
|
}
|
|
338
415
|
//#endregion
|
|
@@ -350,6 +427,6 @@ const TEST_READY_ENV = "GKM_TEST_READY";
|
|
|
350
427
|
/** Where `gkm test` records what it prepared, from where it ran. */
|
|
351
428
|
const TEST_READY_FILE = ".gkm/test-ready.json";
|
|
352
429
|
//#endregion
|
|
353
|
-
export {
|
|
430
|
+
export { writeTestHarness as a, TEST_MANIFEST_ENV as i, TEST_READY_FILE as n, cacheBackendsIn as o, DEFAULT_FACTORIES_DIR as r, driversFor as s, TEST_READY_ENV as t };
|
|
354
431
|
|
|
355
|
-
//# sourceMappingURL=ready-
|
|
432
|
+
//# sourceMappingURL=ready-D1BCiaYU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ready-D1BCiaYU.mjs","names":[],"sources":["../src/generators/drivers.ts","../src/test/harness.ts","../src/test/ready.ts"],"sourcesContent":["/**\n * Driver registration for generated entry points.\n *\n * A construct declares that it needs object storage and is handed one URL; the\n * scheme in that URL picks the driver that builds the client. Which drivers\n * exist is therefore the *entry point's* decision, not the construct's and not\n * the application's — an app that never talks to S3 should never resolve the AWS\n * SDK, and application code that registers a driver has named a provider in the\n * one layer this design keeps provider-free.\n *\n * So the generated entries register: `gkm dev`'s server for local development,\n * and each Lambda handler for its own target. Registration is an explicit call\n * rather than an import side effect, because a side-effecting module is exactly\n * what a bundler is entitled to drop.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type CacheBackend, DEFAULT_CACHE } from '../types.js';\n\n/** The `s3://` driver, which serves MinIO locally and S3 deployed. */\nconst S3 = {\n\timports: `import { registerStorageDriver } from '@geekmidas/storage';\\nimport { s3Driver } from '@geekmidas/storage/aws';`,\n\tsetup: 'registerStorageDriver(s3Driver);',\n};\n\n/** What a generated entry needs in order to register the drivers it uses. */\nexport interface RuntimeDrivers {\n\t/** Import lines, or `''` when there is nothing to register. */\n\timports: string;\n\t/** The registration calls, or `''`. */\n\tsetup: string;\n}\n\n/** @deprecated The same shape, from when only storage had drivers. */\nexport type StorageDrivers = RuntimeDrivers;\n\nconst NONE: RuntimeDrivers = { imports: '', setup: '' };\n\n/**\n * The cache driver for a backend — exactly one, never all three.\n *\n * Keyed off the *backend* rather than off a dependency, because unlike storage\n * the backend already says which driver is right: a project caching in Postgres\n * resolves `pg` and never `ioredis`, and one caching in Upstash resolves\n * neither. That is the whole reason the drivers live behind separate subpaths.\n *\n * Both HTTP schemes are registered for Upstash, because the local proxy speaks\n * `http://` and Upstash speaks `https://`, and an entry that registered one\n * would work in exactly one of the two places.\n */\nconst CACHE_DRIVERS: Record<CacheBackend, RuntimeDrivers> = {\n\tupstash: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(upstashCacheDriver);\\nregisterCacheDriver(upstashInsecureCacheDriver);',\n\t},\n\telasticache: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,\n\t\tsetup:\n\t\t\t'registerCacheDriver(redisCacheDriver);\\nregisterCacheDriver(redissCacheDriver);',\n\t},\n\tdb: {\n\t\timports: `import { registerCacheDriver } from '@geekmidas/cache';\\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,\n\t\tsetup: 'registerCacheDriver(postgresCacheDriver);',\n\t},\n};\n\n/**\n * Everything a generated entry should register: object storage, and the cache.\n *\n * Merged here rather than threaded separately, because a generated file has one\n * import block and one setup block whatever fills them.\n */\nexport function driversFor(options: {\n\tappRoot: string;\n\t/**\n\t * The cache backends actually in play — normally one, and more than one only\n\t * where an app declares two caches that live in different places.\n\t *\n\t * Absent or empty means nothing declared a cache, so no cache driver is\n\t * registered at all.\n\t */\n\tcache?: CacheBackend | readonly CacheBackend[] | false;\n}): RuntimeDrivers {\n\tconst backends =\n\t\toptions.cache === false || options.cache === undefined\n\t\t\t? []\n\t\t\t: typeof options.cache === 'string'\n\t\t\t\t? [options.cache]\n\t\t\t\t: options.cache;\n\n\tconst parts = [\n\t\tstorageDriversFor(options.appRoot),\n\t\t...[...new Set(backends)].map(\n\t\t\t(backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws],\n\t\t),\n\t].filter((part) => part.imports || part.setup);\n\n\tif (parts.length === 0) return NONE;\n\n\treturn {\n\t\timports: parts.map((part) => part.imports).join('\\n'),\n\t\tsetup: parts.map((part) => part.setup).join('\\n'),\n\t};\n}\n\n/**\n * Which cache backends an app's entry actually has to speak.\n *\n * The config answers this for a cache that named nowhere, and the *declaration*\n * answers it for one that named a database — `orders.cache('Sessions')` is a\n * statement about the application, so a deployment cannot move that cache and\n * the entry cannot register a driver for somewhere else.\n *\n * Reading only the config is how an entry ends up registering the Upstash\n * driver for a URL the target composed as `postgres://`, which fails at the\n * first request with `UnregisteredCacheScheme` and a stack that points at the\n * cache rather than at the config that disagreed.\n */\nexport function cacheBackendsIn(\n\tmanifest: Record<string, { kind: string; of?: string }>,\n\tconfigured: CacheBackend,\n): CacheBackend[] {\n\tconst caches = Object.values(manifest).filter((d) => d.kind === 'cache');\n\n\treturn [...new Set(caches.map((c) => (c.of ? 'db' : configured)))];\n}\n\n/**\n * The drivers an app's entry points should register.\n *\n * Keyed off the dependency rather than off a declared bucket, because the entry\n * is generated before discovery has run on the watcher's rebuild path — and an\n * app that installed `@geekmidas/storage` has already paid for the SDK it would\n * otherwise resolve lazily.\n */\nexport function storageDriversFor(appRoot: string): RuntimeDrivers {\n\treturn dependsOnStorage(appRoot) ? S3 : NONE;\n}\n\nfunction dependsOnStorage(appRoot: string): boolean {\n\tconst manifest = join(appRoot, 'package.json');\n\tif (!existsSync(manifest)) return false;\n\n\ttry {\n\t\tconst pkg = JSON.parse(readFileSync(manifest, 'utf8')) as {\n\t\t\tdependencies?: Record<string, string>;\n\t\t\tdevDependencies?: Record<string, string>;\n\t\t};\n\n\t\treturn Boolean(\n\t\t\tpkg.dependencies?.['@geekmidas/storage'] ??\n\t\t\t\tpkg.devDependencies?.['@geekmidas/storage'],\n\t\t);\n\t} catch {\n\t\t// An unreadable package.json is the build's problem, not this function's.\n\t\treturn false;\n\t}\n}\n","/**\n * What `gkm test` writes for a feature test to be built from.\n *\n * `gkm test` already discovers an app's constructs and resolves the test\n * stage's environment; this keeps both, under `.gkm/test/`:\n *\n * - `manifest.json` — which construct and endpoint is exported where, and the\n * environment the test stage resolved, keyed as the constructs derive keys.\n * - `clients/<surface>.ts` — each surface's typed client, from the same\n * generator `gkm build` uses for the app's own.\n * - `index.ts` — the configured `it`, and a `Browser` holding one client per\n * surface and one per auth server, each on the browser's `fetch`.\n *\n * A test imports `it` from there and declares nothing — not a construct, not an\n * environment key, not a client.\n */\n\nimport { mkdir, readdir, rm, writeFile } from 'node:fs/promises';\nimport { extname, join, relative } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { BetterAuth } from '@geekmidas/constructs/auth';\nimport type { Endpoint } from '@geekmidas/constructs/endpoints';\nimport type { TestManifest } from '@geekmidas/constructs/testing';\nimport { EnvironmentParser } from '@geekmidas/envkit';\nimport {\n\tcanonicalId,\n\tkebabCase,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport { serviceContext } from '@geekmidas/services';\nimport {\n\tcacheBackendsIn,\n\tdriversFor,\n\ttype RuntimeDrivers,\n} from '../generators/drivers.js';\nimport { EndpointGenerator } from '../generators/EndpointGenerator.js';\nimport { OpenApiTsGenerator } from '../generators/OpenApiTsGenerator.js';\nimport { type ConstructSource, discover } from '../reconcile/discover.js';\nimport type { CacheBackend } from '../types.js';\n\n/**\n * The variable the manifest's path travels in — what\n * `@geekmidas/constructs/testing` reads. Its own constant rather than an import:\n * that entry carries the whole harness (MSW, Hono, Vitest), and importing it\n * here put all of it on the start-up path of every `gkm` command.\n */\nexport const TEST_MANIFEST_ENV = 'GKM_TEST_MANIFEST';\n\n/** Where the harness lives, relative to the workspace root. */\nexport const TEST_HARNESS_DIR = '.gkm/test';\n\n/**\n * Better-auth's client plugin for each server plugin id.\n *\n * Written down rather than derived: the ids and the client names do not follow\n * one rule (`admin` pairs with `adminClient`, `email-otp` with\n * `emailOTPClient`), and a guess that missed would hand a test a client\n * silently lacking every method of that plugin. A server plugin not listed has\n * no client counterpart — `bearer`, `open-api` — or needs one from another\n * package, and is left out.\n */\nconst CLIENT_PLUGINS: Readonly<Record<string, string>> = {\n\tadmin: 'adminClient',\n\tanonymous: 'anonymousClient',\n\t'device-authorization': 'deviceAuthorizationClient',\n\t'email-otp': 'emailOTPClient',\n\tjwt: 'jwtClient',\n\t'last-login-method': 'lastLoginMethodClient',\n\t'magic-link': 'magicLinkClient',\n\t'multi-session': 'multiSessionClient',\n\t'oauth-popup': 'oauthPopupClient',\n\t'one-tap': 'oneTapClient',\n\t'one-time-token': 'oneTimeTokenClient',\n\torganization: 'organizationClient',\n\t'phone-number': 'phoneNumberClient',\n\tsiwe: 'siweClient',\n\t'two-factor': 'twoFactorClient',\n\tusername: 'usernameClient',\n};\n\nexport interface WriteTestHarnessOptions {\n\t/** The workspace root: where discovery runs from. */\n\troot: string;\n\t/**\n\t * The directories to write `.gkm/test/` into — each app's own. A package\n\t * maps its `#test` import only to a path inside itself, so each app gets its\n\t * own copy of the harness rather than one at the root it cannot reach.\n\t */\n\ttargets: string[];\n\t/** The constructs glob — the same patterns reconcile and the build use. */\n\tpatterns: string[];\n\t/** The stage the environment was resolved for. */\n\tstage: string;\n\t/** The environment the suite runs with. */\n\tenv: Record<string, string>;\n\t/**\n\t * Where a cache lives when it names nowhere — the target's default, which\n\t * the build's server entry registers a driver for.\n\t */\n\tcacheBackend: CacheBackend;\n\t/**\n\t * The folder of test factories, absolute: `test/factories` at the root, or\n\t * where `test.factories` in the config points. Absent or empty, a test is\n\t * handed no factories.\n\t */\n\tfactories?: string;\n}\n\n/** Where a project keeps its test factories, relative to its root. */\nexport const DEFAULT_FACTORIES_DIR = 'test/factories';\n\n/** A file in the factories folder that names no database construct. */\nexport class UnknownFactoryFile extends Error {\n\tconstructor(\n\t\treadonly file: string,\n\t\treadonly known: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${file} names no database construct, so no test could be handed it. ` +\n\t\t\t\t(known.length > 0\n\t\t\t\t\t? `The databases: ${known.join(', ')}. `\n\t\t\t\t\t: 'This project declares no database. ') +\n\t\t\t\t`Name a factory after its database: test/factories/<construct>.ts.`,\n\t\t);\n\t\tthis.name = 'UnknownFactoryFile';\n\t}\n}\n\n/** A factory file that exports no `createFactory`. */\nexport class FactoryHasNoCreate extends Error {\n\tconstructor(readonly file: string) {\n\t\tsuper(\n\t\t\t`${file} is a test factory but exports no \\`createFactory\\`. ` +\n\t\t\t\t`Export \\`function createFactory(db: Kysely<Database>)\\` returning ` +\n\t\t\t\t`the factory.`,\n\t\t);\n\t\tthis.name = 'FactoryHasNoCreate';\n\t}\n}\n\n/** One database's factory: the file, and the service name a test keys it by. */\nexport interface FactorySource {\n\tfile: string;\n\tservice: string;\n}\n\n/**\n * The factories in a folder, each matched to the database it is named after.\n *\n * A file that names no database, or exports no `createFactory`, is refused\n * rather than skipped: either is a factory no test would ever be handed.\n */\nexport async function factorySources(\n\tfolder: string | undefined,\n\tdatabases: readonly string[],\n): Promise<FactorySource[]> {\n\tif (!folder) return [];\n\n\tconst entries = await readdir(folder, { withFileTypes: true }).catch(\n\t\t(error: NodeJS.ErrnoException) => {\n\t\t\tif (error.code === 'ENOENT') return [];\n\t\t\tthrow error;\n\t\t},\n\t);\n\n\tconst found: FactorySource[] = [];\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !isFactoryFile(entry.name)) continue;\n\n\t\tconst file = join(folder, entry.name);\n\t\tconst id = canonicalId(entry.name.slice(0, -extname(entry.name).length));\n\t\tif (!databases.includes(id)) {\n\t\t\tthrow new UnknownFactoryFile(file, databases);\n\t\t}\n\n\t\tconst module = (await import(pathToFileURL(file).href)) as {\n\t\t\tcreateFactory?: unknown;\n\t\t};\n\t\tif (typeof module.createFactory !== 'function') {\n\t\t\tthrow new FactoryHasNoCreate(file);\n\t\t}\n\n\t\tfound.push({ file, service: serviceKey(id) });\n\t}\n\n\treturn found.sort((a, b) => (a.service < b.service ? -1 : 1));\n}\n\nfunction isFactoryFile(file: string): boolean {\n\tif (file.endsWith('.d.ts')) return false;\n\tif (/\\.(spec|test)\\.[cm]?[jt]s$/.test(file)) return false;\n\treturn ['.ts', '.mts', '.js', '.mjs'].includes(extname(file));\n}\n\n/** Write the manifest, the clients and the harness; return the manifest path. */\nexport async function writeTestHarness(\n\toptions: WriteTestHarnessOptions,\n): Promise<string> {\n\tconst sources: Record<string, ConstructSource> = {};\n\tconst declared = await discover({\n\t\tpatterns: options.patterns,\n\t\tcwd: options.root,\n\t\tsources,\n\t});\n\n\tconst loaded = await new EndpointGenerator().load(\n\t\toptions.patterns,\n\t\toptions.root,\n\t);\n\tconst endpoints = loaded\n\t\t.map(({ key, construct, path }) => ({\n\t\t\tsurface: construct.owner ?? construct.surface?.id,\n\t\t\tconstruct,\n\t\t\tsource: { file: path.absolute, export: key },\n\t\t}))\n\t\t.filter(\n\t\t\t(endpoint): endpoint is typeof endpoint & { surface: string } =>\n\t\t\t\tendpoint.surface !== undefined,\n\t\t);\n\n\tconst manifest: TestManifest = {\n\t\tstage: options.stage,\n\t\tconstructs: Object.fromEntries(\n\t\t\tObject.entries(declared).flatMap(([id, declaration]) => {\n\t\t\t\tconst source = sources[id];\n\t\t\t\treturn source\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkind: declaration.kind,\n\t\t\t\t\t\t\t\t\tsource: { file: source.file, export: source.exportName },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t]\n\t\t\t\t\t: [];\n\t\t\t}),\n\t\t),\n\t\tendpoints: endpoints.map(({ surface, source }) => ({ surface, source })),\n\t\tenv: options.env,\n\t};\n\t// One typed client per surface, from the generator the build uses.\n\tconst surfaces = [...new Set(endpoints.map(({ surface }) => surface))];\n\tconst generator = new OpenApiTsGenerator();\n\tconst clients = await Promise.all(\n\t\tsurfaces.map(async (surface) => ({\n\t\t\tfile: `${kebabCase(surface)}.ts`,\n\t\t\tcontent: await generator.generate(\n\t\t\t\tendpoints\n\t\t\t\t\t.filter((endpoint) => endpoint.surface === surface)\n\t\t\t\t\t.map(({ construct }) => construct as Endpoint<any, any, any, any>),\n\t\t\t\t{ title: surface, version: '1.0.0' },\n\t\t\t),\n\t\t})),\n\t);\n\n\tconst auths = await authClients(declared, sources, options.env);\n\t// The drivers the server entry registers — which cache and storage clients\n\t// exist is the entry's decision, and the harness stands where the entry does.\n\tconst drivers = driversFor({\n\t\tappRoot: options.root,\n\t\tcache: cacheBackendsIn(declared, options.cacheBackend),\n\t});\n\n\t// The app's own databases: not a tenant an auth server owns — reached\n\t// through that server — and not a reader of one. What a test is handed.\n\tconst owners = new Set(auths.map((auth) => auth.databaseId));\n\tconst databases = databaseSources(declared, sources).filter(\n\t\t({ id, kind }) => !owners.has(id) && kind !== 'database-reader',\n\t);\n\tconst factories = await factorySources(\n\t\toptions.factories,\n\t\tdatabases.map(({ id }) => id),\n\t);\n\tconst files = [\n\t\t...new Set([\n\t\t\t...Object.values(manifest.constructs).map(({ source }) => source.file),\n\t\t\t...manifest.endpoints.map(({ source }) => source.file),\n\t\t]),\n\t];\n\tconst harnessFor = (dir: string) =>\n\t\tharnessModule({\n\t\t\tdir,\n\t\t\tfiles,\n\t\t\tdatabases,\n\t\t\tsurfaces: surfaces.map((id, index) => ({\n\t\t\t\tid,\n\t\t\t\t// The client the generator writes for a surface with authorizers\n\t\t\t\t// takes a strategy per scheme; one without takes none.\n\t\t\t\tsecured: clients[index]!.content.includes('authStrategies:'),\n\t\t\t})),\n\t\t\tauths,\n\t\t\tdrivers,\n\t\t\tfactories,\n\t\t\tmail: Object.values(declared).some(({ kind }) => kind === 'email'),\n\t\t});\n\tconst json = `${JSON.stringify(manifest, null, 2)}\\n`;\n\n\tconst written = await Promise.all(\n\t\t[...new Set(options.targets)].map(async (target) => {\n\t\t\tconst dir = join(target, TEST_HARNESS_DIR);\n\t\t\t// Rewritten whole on every run: a client for a surface that no longer\n\t\t\t// exists is not something a test should be able to import.\n\t\t\tawait rm(dir, { recursive: true, force: true });\n\t\t\tawait mkdir(join(dir, 'clients'), { recursive: true });\n\t\t\tawait writeFile(join(dir, 'manifest.json'), json);\n\t\t\tawait writeFile(join(dir, 'index.ts'), harnessFor(dir));\n\t\t\tfor (const client of clients) {\n\t\t\t\tawait writeFile(join(dir, 'clients', client.file), client.content);\n\t\t\t}\n\t\t\treturn join(dir, 'manifest.json');\n\t\t}),\n\t);\n\n\treturn written[0]!;\n}\n\ninterface AuthClient {\n\tid: string;\n\tbasePath: string;\n\tplugins: string[];\n\t/** The schema tenant it owns — reached through it, never by a test. */\n\tdatabaseId: string;\n}\n\n/** Each auth server, with the client plugins its server plugins pair with. */\nasync function authClients(\n\tdeclared: Awaited<ReturnType<typeof discover>>,\n\tsources: Record<string, ConstructSource>,\n\tenv: Record<string, string>,\n): Promise<AuthClient[]> {\n\tconst clients: AuthClient[] = [];\n\tconst envParser = new EnvironmentParser({ ...env });\n\n\tfor (const [id, declaration] of Object.entries(declared)) {\n\t\tconst source = sources[id];\n\t\tif (declaration.kind !== 'rest-api' || !source) continue;\n\n\t\tconst exported = (await import(source.file))[source.exportName] as unknown;\n\t\tif (!isBetterAuth(exported) || exported.id !== id) continue;\n\n\t\tconst ids = await exported.pluginIds({\n\t\t\tenvParser,\n\t\t\tcontext: serviceContext,\n\t\t});\n\t\tclients.push({\n\t\t\tid,\n\t\t\tbasePath: exported.basePath,\n\t\t\tplugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),\n\t\t\tdatabaseId: exported.databaseId,\n\t\t});\n\t}\n\n\treturn clients;\n}\n\nfunction isBetterAuth(value: unknown): value is BetterAuth {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t'pluginIds' in value &&\n\t\t'basePath' in value\n\t);\n}\n\n/** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */\nfunction propertyOf(id: string): string {\n\treturn id.charAt(0).toLowerCase() + id.slice(1);\n}\n\n/**\n * The database a test's `db` is — the one the endpoints were given with\n * `.database()`, found by the service they hold, which is the construct's own.\n * The same rule `featureTest` applies at runtime, applied here so `db` can be\n * typed by the construct's schema.\n */\n/** A database construct: its id, its service name, and where it is exported. */\ninterface DatabaseSource {\n\tid: string;\n\tkind: string;\n\tservice: string;\n\tsource: ConstructSource;\n}\n\n/**\n * Every database construct the app declares — a database, a schema tenant, a\n * reader — keyed as a test is handed its transaction.\n */\nfunction databaseSources(\n\tdeclared: Awaited<ReturnType<typeof discover>>,\n\tsources: Record<string, ConstructSource>,\n): DatabaseSource[] {\n\treturn Object.entries(declared)\n\t\t.filter(([id, declaration]) =>\n\t\t\tBoolean(sources[id] && declaration.kind.startsWith('database')),\n\t\t)\n\t\t.map(([id, declaration]) => ({\n\t\t\tid,\n\t\t\tkind: declaration.kind,\n\t\t\tservice: serviceKey(id),\n\t\t\tsource: sources[id]!,\n\t\t}));\n}\n\n/** An import specifier for `file`, from the harness written into `dir`. */\nfunction specifierFrom(dir: string, file: string): string {\n\tconst path = relative(dir, file).replace(/\\.tsx?$/, '.js');\n\treturn path.startsWith('.') ? path : `./${path}`;\n}\n\nfunction harnessModule(options: {\n\tdir: string;\n\t/** Every module the manifest points at, by absolute path. */\n\tfiles: string[];\n\tdatabases: DatabaseSource[];\n\tsurfaces: { id: string; secured: boolean }[];\n\tauths: AuthClient[];\n\tdrivers: RuntimeDrivers;\n\tfactories: FactorySource[];\n\t/** Whether the app sends mail — what a magic-link sign-in is read from. */\n\tmail: boolean;\n}): string {\n\tconst { surfaces, auths, drivers, databases, dir, files, factories, mail } =\n\t\toptions;\n\t// One auth server a test can sign in to without a person: a magic link,\n\t// read from the app's inbox. With two, which one \\`signIn\\` means is a\n\t// guess, so neither gets it.\n\tconst magicLinks = mail\n\t\t? auths.filter(({ plugins }) => plugins.includes('magicLinkClient'))\n\t\t: [];\n\tconst signIn = magicLinks.length === 1 ? magicLinks[0] : undefined;\n\tconst plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();\n\n\tconst imports = [\n\t\t`import {${databases.length ? ' type DatabaseOf,' : ''} featureTest, loadTestManifest${signIn ? ', signInWithMagicLink' : ''} } from '@geekmidas/constructs/testing';`,\n\t\t...databases.map(\n\t\t\t({ id, source }) =>\n\t\t\t\t`import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`,\n\t\t),\n\t\t`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,\n\t\t...surfaces.map(\n\t\t\t({ id }) =>\n\t\t\t\t`import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`,\n\t\t),\n\t\t...(auths.length\n\t\t\t? [`import { createAuthClient } from 'better-auth/client';`]\n\t\t\t: []),\n\t\t...(plugins.length\n\t\t\t? [`import { ${plugins.join(', ')} } from 'better-auth/client/plugins';`]\n\t\t\t: []),\n\t\t...(drivers.imports ? [drivers.imports] : []),\n\t\t...factories.map(\n\t\t\t({ file, service }) =>\n\t\t\t\t`import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`,\n\t\t),\n\t\t// Every construct and endpoint module, imported here — inside the app,\n\t\t// where its tsconfig paths resolve — rather than by path from the kit.\n\t\t...files.map(\n\t\t\t(file, index) =>\n\t\t\t\t`import * as __module${index} from '${specifierFrom(dir, file)}';`,\n\t\t),\n\t];\n\n\tconst url = (id: string) =>\n\t\t`manifest.env[${JSON.stringify(provideKey(id, 'url'))}]!`;\n\n\tconst members = [\n\t\t...surfaces.map(\n\t\t\t({ id, secured }) => `\t/** \\`${id}\\`, at ${provideKey(id, 'url')}. */\n\treadonly ${propertyOf(id)} = create${id}({\n\t\tbaseURL: ${url(id)},\n\t\tfetch: this.fetch,${\n\t\t\tsecured\n\t\t\t\t? `\n\t\t// Served in-process: no authorizer runs, so nothing is signed. A scheme\n\t\t// with no strategy sends its request without credentials.\n\t\tauthStrategies: {} as never,`\n\t\t\t\t: ''\n\t\t}\n\t});`,\n\t\t),\n\t\t...auths.map(\n\t\t\t(auth) => `\t/** \\`${auth.id}\\`, at ${provideKey(auth.id, 'url')}. */\n\treadonly ${propertyOf(auth.id)} = createAuthClient({\n\t\tbaseURL: \\`\\${${url(auth.id)}}${auth.basePath}\\`,\n\t\tplugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(', ')}],\n\t\tfetchOptions: { customFetchImpl: this.fetch },\n\t});`,\n\t\t),\n\t\t...(signIn\n\t\t\t? [\n\t\t\t\t\t`\t/**\n\t * Sign in as \\`email\\` through \\`${signIn.id}\\`'s magic link, the way a person\n\t * does — the email opened, the link followed. The session it then reports.\n\t */\n\tsignIn(email: string) {\n\t\treturn signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);\n\t}`,\n\t\t\t\t]\n\t\t\t: []),\n\t];\n\n\t// Each database's factory, keyed as the test is handed it.\n\tconst factoriesType = factories.length\n\t\t? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join('; ')} }`\n\t\t: '';\n\tconst factoriesOption = factories.length\n\t\t? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(', ')} }`\n\t\t: '';\n\t// Each database's schema, keyed as the test is handed its transaction.\n\tconst databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join('; ')} }`;\n\tconst generics = databases.length\n\t\t? `<Browser, ${databasesType}${factoriesType ? `, ${factoriesType}` : ''}>`\n\t\t: '';\n\n\treturn `// Generated by \\`gkm test\\` from the app's constructs — do not edit.\n${imports.join('\\n')}\n\n${drivers.setup ? `// The drivers the app's server entry registers, registered the same way.\\n${drivers.setup}\\n\\n` : ''}const manifest = loadTestManifest(new URL('./manifest.json', import.meta.url));\n\n/** A browser with a client for every surface the app declares. */\nexport class Browser extends TestBrowser {\n${members.join('\\n\\n')}\n}\n\n/** The app's modules, by the path the manifest records for each. */\nconst modules = {\n${files.map((file, index) => `\\t${JSON.stringify(file)}: __module${index},`).join('\\n')}\n};\n\n/** \\`it\\`, for a test that drives the app the way it runs deployed. */\nexport const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption} });\n`;\n}\n","/**\n * The hand-off between `gkm test` and the Vitest global setup.\n *\n * `gkm test` reconciles and migrates the test stage before Vitest starts; the\n * setup, run inside Vitest, only has to load what that left. Started by plain\n * `vitest` instead, the setup runs `gkm test --setup` itself first — so there is\n * one way the stage gets ready, whichever started the suite.\n */\n\n/** Set on the Vitest process by `gkm test`: the stage is ready, here. */\nexport const TEST_READY_ENV = 'GKM_TEST_READY';\n\n/** Where `gkm test` records what it prepared, from where it ran. */\nexport const TEST_READY_FILE = '.gkm/test-ready.json';\n\nexport interface TestReady {\n\t/** The resolved environment, as a JSON object of strings. */\n\tenv: string;\n\t/** The test manifest the `#test` harness reads. */\n\tmanifest?: string;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as EmailBackend, t as CacheBackend } from "../types-
|
|
1
|
+
import { n as EmailBackend, t as CacheBackend } from "../types-Bj_FpeTc.cjs";
|
|
2
2
|
import { ConstructManifest, Declaration, RouteInfo } from "@geekmidas/manifest";
|
|
3
3
|
//#region src/reconcile/discover.d.ts
|
|
4
4
|
/** The construct face discovery needs: an id, and the ability to declare. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as EmailBackend, t as CacheBackend } from "../types-
|
|
1
|
+
import { n as EmailBackend, t as CacheBackend } from "../types-BTiu5kwo.mjs";
|
|
2
2
|
import { ConstructManifest, Declaration, RouteInfo } from "@geekmidas/manifest";
|
|
3
3
|
//#region src/reconcile/discover.d.ts
|
|
4
4
|
/** The construct face discovery needs: an id, and the ability to declare. */
|