@geekmidas/cli 10.0.0-alpha.29 → 10.0.0-alpha.30
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/{HostingerProvider-Btxpto71.mjs → HostingerProvider-GF6NAZAK.mjs} +2 -2
- package/dist/{HostingerProvider-Btxpto71.mjs.map → HostingerProvider-GF6NAZAK.mjs.map} +1 -1
- package/dist/{openapi-K4H5tHzA.cjs → OpenApiTsGenerator-D1nhN98m.cjs} +2 -224
- package/dist/OpenApiTsGenerator-D1nhN98m.cjs.map +1 -0
- package/dist/{openapi-D-Lk5Hr5.mjs → OpenApiTsGenerator-hDINScnz.mjs} +4 -190
- package/dist/OpenApiTsGenerator-hDINScnz.mjs.map +1 -0
- package/dist/{bundler-7iaO93G8.mjs → bundler-D9IPNvDt.mjs} +2 -2
- package/dist/{bundler-7iaO93G8.mjs.map → bundler-D9IPNvDt.mjs.map} +1 -1
- package/dist/{config-Dqxa-_Ge.mjs → config-BH3Ki69F.mjs} +2 -2
- package/dist/{config-Dqxa-_Ge.mjs.map → config-BH3Ki69F.mjs.map} +1 -1
- package/dist/{config-BTFLHfRb.cjs → config-URVD4bTj.cjs} +2 -2
- package/dist/{config-BTFLHfRb.cjs.map → config-URVD4bTj.cjs.map} +1 -1
- package/dist/config.cjs +1 -1
- package/dist/config.mjs +1 -1
- package/dist/{discover-CXGlQHm1.mjs → discover-CMxesskG.mjs} +8 -2
- package/dist/{discover-CXGlQHm1.mjs.map → discover-CMxesskG.mjs.map} +1 -1
- package/dist/{discover-BTh87DOO.cjs → discover-CroTIm0K.cjs} +12 -1
- package/dist/{discover-BTh87DOO.cjs.map → discover-CroTIm0K.cjs.map} +1 -1
- package/dist/{github-ZL3RcUVD.cjs → github-B2fis34P.cjs} +2 -2
- package/dist/{github-ZL3RcUVD.cjs.map → github-B2fis34P.cjs.map} +1 -1
- package/dist/{github-DPpNfzu4.mjs → github-BrDmlm1x.mjs} +2 -2
- package/dist/{github-DPpNfzu4.mjs.map → github-BrDmlm1x.mjs.map} +1 -1
- package/dist/index.cjs +187 -455
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +181 -451
- package/dist/index.mjs.map +1 -1
- package/dist/migrate-BuCx7tIv.cjs +498 -0
- package/dist/migrate-BuCx7tIv.cjs.map +1 -0
- package/dist/migrate-CBICVMAi.mjs +493 -0
- package/dist/migrate-CBICVMAi.mjs.map +1 -0
- package/dist/openapi.cjs +197 -7
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.mjs +191 -1
- package/dist/openapi.mjs.map +1 -0
- package/dist/ready-CvP3JVEw.mjs +355 -0
- package/dist/ready-CvP3JVEw.mjs.map +1 -0
- package/dist/ready-DXtNfk7v.cjs +390 -0
- package/dist/ready-DXtNfk7v.cjs.map +1 -0
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.mjs +1 -1
- package/dist/vitest.cjs +61 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +28 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +28 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +61 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/{workspace-B3ApLW6p.cjs → workspace-BhiMkRuA.cjs} +3 -3
- package/dist/{workspace-B3ApLW6p.cjs.map → workspace-BhiMkRuA.cjs.map} +1 -1
- package/dist/{workspace-DnHW8ooI.mjs → workspace-C97QUCIf.mjs} +3 -3
- package/dist/{workspace-DnHW8ooI.mjs.map → workspace-C97QUCIf.mjs.map} +1 -1
- package/package.json +24 -14
- package/dist/openapi-D-Lk5Hr5.mjs.map +0 -1
- package/dist/openapi-K4H5tHzA.cjs.map +0 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { a as DEFAULT_CACHE, t as discover } from "./discover-CMxesskG.mjs";
|
|
2
|
+
import { n as EndpointGenerator, t as OpenApiTsGenerator } from "./OpenApiTsGenerator-hDINScnz.mjs";
|
|
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";
|
|
7
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
8
|
+
import { serviceContext } from "@geekmidas/services";
|
|
9
|
+
//#region src/generators/drivers.ts
|
|
10
|
+
/**
|
|
11
|
+
* Driver registration for generated entry points.
|
|
12
|
+
*
|
|
13
|
+
* A construct declares that it needs object storage and is handed one URL; the
|
|
14
|
+
* scheme in that URL picks the driver that builds the client. Which drivers
|
|
15
|
+
* exist is therefore the *entry point's* decision, not the construct's and not
|
|
16
|
+
* the application's — an app that never talks to S3 should never resolve the AWS
|
|
17
|
+
* SDK, and application code that registers a driver has named a provider in the
|
|
18
|
+
* one layer this design keeps provider-free.
|
|
19
|
+
*
|
|
20
|
+
* So the generated entries register: `gkm dev`'s server for local development,
|
|
21
|
+
* and each Lambda handler for its own target. Registration is an explicit call
|
|
22
|
+
* rather than an import side effect, because a side-effecting module is exactly
|
|
23
|
+
* what a bundler is entitled to drop.
|
|
24
|
+
*/
|
|
25
|
+
/** The `s3://` driver, which serves MinIO locally and S3 deployed. */
|
|
26
|
+
const S3 = {
|
|
27
|
+
imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
|
|
28
|
+
setup: "registerStorageDriver(s3Driver);"
|
|
29
|
+
};
|
|
30
|
+
const NONE = {
|
|
31
|
+
imports: "",
|
|
32
|
+
setup: ""
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The cache driver for a backend — exactly one, never all three.
|
|
36
|
+
*
|
|
37
|
+
* Keyed off the *backend* rather than off a dependency, because unlike storage
|
|
38
|
+
* the backend already says which driver is right: a project caching in Postgres
|
|
39
|
+
* resolves `pg` and never `ioredis`, and one caching in Upstash resolves
|
|
40
|
+
* neither. That is the whole reason the drivers live behind separate subpaths.
|
|
41
|
+
*
|
|
42
|
+
* Both HTTP schemes are registered for Upstash, because the local proxy speaks
|
|
43
|
+
* `http://` and Upstash speaks `https://`, and an entry that registered one
|
|
44
|
+
* would work in exactly one of the two places.
|
|
45
|
+
*/
|
|
46
|
+
const CACHE_DRIVERS = {
|
|
47
|
+
upstash: {
|
|
48
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
|
|
49
|
+
setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
|
|
50
|
+
},
|
|
51
|
+
elasticache: {
|
|
52
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
|
|
53
|
+
setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
|
|
54
|
+
},
|
|
55
|
+
db: {
|
|
56
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
|
|
57
|
+
setup: "registerCacheDriver(postgresCacheDriver);"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Everything a generated entry should register: object storage, and the cache.
|
|
62
|
+
*
|
|
63
|
+
* Merged here rather than threaded separately, because a generated file has one
|
|
64
|
+
* import block and one setup block whatever fills them.
|
|
65
|
+
*/
|
|
66
|
+
function driversFor(options) {
|
|
67
|
+
const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
|
|
68
|
+
const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
|
|
69
|
+
if (parts.length === 0) return NONE;
|
|
70
|
+
return {
|
|
71
|
+
imports: parts.map((part) => part.imports).join("\n"),
|
|
72
|
+
setup: parts.map((part) => part.setup).join("\n")
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Which cache backends an app's entry actually has to speak.
|
|
77
|
+
*
|
|
78
|
+
* The config answers this for a cache that named nowhere, and the *declaration*
|
|
79
|
+
* answers it for one that named a database — `orders.cache('Sessions')` is a
|
|
80
|
+
* statement about the application, so a deployment cannot move that cache and
|
|
81
|
+
* the entry cannot register a driver for somewhere else.
|
|
82
|
+
*
|
|
83
|
+
* Reading only the config is how an entry ends up registering the Upstash
|
|
84
|
+
* driver for a URL the target composed as `postgres://`, which fails at the
|
|
85
|
+
* first request with `UnregisteredCacheScheme` and a stack that points at the
|
|
86
|
+
* cache rather than at the config that disagreed.
|
|
87
|
+
*/
|
|
88
|
+
function cacheBackendsIn(manifest, configured) {
|
|
89
|
+
const caches = Object.values(manifest).filter((d) => d.kind === "cache");
|
|
90
|
+
return [...new Set(caches.map((c) => c.of ? "db" : configured))];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The drivers an app's entry points should register.
|
|
94
|
+
*
|
|
95
|
+
* Keyed off the dependency rather than off a declared bucket, because the entry
|
|
96
|
+
* is generated before discovery has run on the watcher's rebuild path — and an
|
|
97
|
+
* app that installed `@geekmidas/storage` has already paid for the SDK it would
|
|
98
|
+
* otherwise resolve lazily.
|
|
99
|
+
*/
|
|
100
|
+
function storageDriversFor(appRoot) {
|
|
101
|
+
return dependsOnStorage(appRoot) ? S3 : NONE;
|
|
102
|
+
}
|
|
103
|
+
function dependsOnStorage(appRoot) {
|
|
104
|
+
const manifest = join(appRoot, "package.json");
|
|
105
|
+
if (!existsSync(manifest)) return false;
|
|
106
|
+
try {
|
|
107
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
108
|
+
return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/test/harness.ts
|
|
115
|
+
/**
|
|
116
|
+
* What `gkm test` writes for a feature test to be built from.
|
|
117
|
+
*
|
|
118
|
+
* `gkm test` already discovers an app's constructs and resolves the test
|
|
119
|
+
* stage's environment; this keeps both, under `.gkm/test/`:
|
|
120
|
+
*
|
|
121
|
+
* - `manifest.json` — which construct and endpoint is exported where, and the
|
|
122
|
+
* environment the test stage resolved, keyed as the constructs derive keys.
|
|
123
|
+
* - `clients/<surface>.ts` — each surface's typed client, from the same
|
|
124
|
+
* generator `gkm build` uses for the app's own.
|
|
125
|
+
* - `index.ts` — the configured `it`, and a `Browser` holding one client per
|
|
126
|
+
* surface and one per auth server, each on the browser's `fetch`.
|
|
127
|
+
*
|
|
128
|
+
* A test imports `it` from there and declares nothing — not a construct, not an
|
|
129
|
+
* environment key, not a client.
|
|
130
|
+
*/
|
|
131
|
+
/**
|
|
132
|
+
* The variable the manifest's path travels in — what
|
|
133
|
+
* `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
|
|
134
|
+
* that entry carries the whole harness (MSW, Hono, Vitest), and importing it
|
|
135
|
+
* here put all of it on the start-up path of every `gkm` command.
|
|
136
|
+
*/
|
|
137
|
+
const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
|
|
138
|
+
/** Where the harness lives, relative to the workspace root. */
|
|
139
|
+
const TEST_HARNESS_DIR = ".gkm/test";
|
|
140
|
+
/**
|
|
141
|
+
* Better-auth's client plugin for each server plugin id.
|
|
142
|
+
*
|
|
143
|
+
* Written down rather than derived: the ids and the client names do not follow
|
|
144
|
+
* one rule (`admin` pairs with `adminClient`, `email-otp` with
|
|
145
|
+
* `emailOTPClient`), and a guess that missed would hand a test a client
|
|
146
|
+
* silently lacking every method of that plugin. A server plugin not listed has
|
|
147
|
+
* no client counterpart — `bearer`, `open-api` — or needs one from another
|
|
148
|
+
* package, and is left out.
|
|
149
|
+
*/
|
|
150
|
+
const CLIENT_PLUGINS = {
|
|
151
|
+
admin: "adminClient",
|
|
152
|
+
anonymous: "anonymousClient",
|
|
153
|
+
"device-authorization": "deviceAuthorizationClient",
|
|
154
|
+
"email-otp": "emailOTPClient",
|
|
155
|
+
jwt: "jwtClient",
|
|
156
|
+
"last-login-method": "lastLoginMethodClient",
|
|
157
|
+
"magic-link": "magicLinkClient",
|
|
158
|
+
"multi-session": "multiSessionClient",
|
|
159
|
+
"oauth-popup": "oauthPopupClient",
|
|
160
|
+
"one-tap": "oneTapClient",
|
|
161
|
+
"one-time-token": "oneTimeTokenClient",
|
|
162
|
+
organization: "organizationClient",
|
|
163
|
+
"phone-number": "phoneNumberClient",
|
|
164
|
+
siwe: "siweClient",
|
|
165
|
+
"two-factor": "twoFactorClient",
|
|
166
|
+
username: "usernameClient"
|
|
167
|
+
};
|
|
168
|
+
/** Write the manifest, the clients and the harness; return the manifest path. */
|
|
169
|
+
async function writeTestHarness(options) {
|
|
170
|
+
const sources = {};
|
|
171
|
+
const declared = await discover({
|
|
172
|
+
patterns: options.patterns,
|
|
173
|
+
cwd: options.root,
|
|
174
|
+
sources
|
|
175
|
+
});
|
|
176
|
+
const endpoints = (await new EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
|
|
177
|
+
surface: construct.owner ?? construct.surface?.id,
|
|
178
|
+
construct,
|
|
179
|
+
source: {
|
|
180
|
+
file: path.absolute,
|
|
181
|
+
export: key
|
|
182
|
+
}
|
|
183
|
+
})).filter((endpoint) => endpoint.surface !== void 0);
|
|
184
|
+
const manifest = {
|
|
185
|
+
stage: options.stage,
|
|
186
|
+
constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
|
|
187
|
+
const source = sources[id];
|
|
188
|
+
return source ? [[id, {
|
|
189
|
+
kind: declaration.kind,
|
|
190
|
+
source: {
|
|
191
|
+
file: source.file,
|
|
192
|
+
export: source.exportName
|
|
193
|
+
}
|
|
194
|
+
}]] : [];
|
|
195
|
+
})),
|
|
196
|
+
endpoints: endpoints.map(({ surface, source }) => ({
|
|
197
|
+
surface,
|
|
198
|
+
source
|
|
199
|
+
})),
|
|
200
|
+
env: options.env
|
|
201
|
+
};
|
|
202
|
+
const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
|
|
203
|
+
const generator = new OpenApiTsGenerator();
|
|
204
|
+
const clients = await Promise.all(surfaces.map(async (surface) => ({
|
|
205
|
+
file: `${kebabCase(surface)}.ts`,
|
|
206
|
+
content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
|
|
207
|
+
title: surface,
|
|
208
|
+
version: "1.0.0"
|
|
209
|
+
})
|
|
210
|
+
})));
|
|
211
|
+
const auths = await authClients(declared, sources, options.env);
|
|
212
|
+
const drivers = driversFor({
|
|
213
|
+
appRoot: options.root,
|
|
214
|
+
cache: cacheBackendsIn(declared, options.cacheBackend)
|
|
215
|
+
});
|
|
216
|
+
const database = await databaseSource(declared, sources, endpoints.map(({ construct }) => construct));
|
|
217
|
+
const files = [.../* @__PURE__ */ new Set([...Object.values(manifest.constructs).map(({ source }) => source.file), ...manifest.endpoints.map(({ source }) => source.file)])];
|
|
218
|
+
const harnessFor = (dir) => harnessModule({
|
|
219
|
+
dir,
|
|
220
|
+
files,
|
|
221
|
+
database,
|
|
222
|
+
surfaces: surfaces.map((id, index) => ({
|
|
223
|
+
id,
|
|
224
|
+
secured: clients[index].content.includes("authStrategies:")
|
|
225
|
+
})),
|
|
226
|
+
auths,
|
|
227
|
+
drivers
|
|
228
|
+
});
|
|
229
|
+
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
230
|
+
return (await Promise.all([...new Set(options.targets)].map(async (target) => {
|
|
231
|
+
const dir = join(target, TEST_HARNESS_DIR);
|
|
232
|
+
await rm(dir, {
|
|
233
|
+
recursive: true,
|
|
234
|
+
force: true
|
|
235
|
+
});
|
|
236
|
+
await mkdir(join(dir, "clients"), { recursive: true });
|
|
237
|
+
await writeFile(join(dir, "manifest.json"), json);
|
|
238
|
+
await writeFile(join(dir, "index.ts"), harnessFor(dir));
|
|
239
|
+
for (const client of clients) await writeFile(join(dir, "clients", client.file), client.content);
|
|
240
|
+
return join(dir, "manifest.json");
|
|
241
|
+
})))[0];
|
|
242
|
+
}
|
|
243
|
+
/** Each auth server, with the client plugins its server plugins pair with. */
|
|
244
|
+
async function authClients(declared, sources, env) {
|
|
245
|
+
const clients = [];
|
|
246
|
+
const envParser = new EnvironmentParser({ ...env });
|
|
247
|
+
for (const [id, declaration] of Object.entries(declared)) {
|
|
248
|
+
const source = sources[id];
|
|
249
|
+
if (declaration.kind !== "rest-api" || !source) continue;
|
|
250
|
+
const exported = (await import(source.file))[source.exportName];
|
|
251
|
+
if (!isBetterAuth(exported) || exported.id !== id) continue;
|
|
252
|
+
const ids = await exported.pluginIds({
|
|
253
|
+
envParser,
|
|
254
|
+
context: serviceContext
|
|
255
|
+
});
|
|
256
|
+
clients.push({
|
|
257
|
+
id,
|
|
258
|
+
basePath: exported.basePath,
|
|
259
|
+
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? [])
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return clients;
|
|
263
|
+
}
|
|
264
|
+
function isBetterAuth(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
|
|
266
|
+
}
|
|
267
|
+
/** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
|
|
268
|
+
function propertyOf(id) {
|
|
269
|
+
return id.charAt(0).toLowerCase() + id.slice(1);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The database a test's `db` is — the one the endpoints were given with
|
|
273
|
+
* `.database()`, found by the service they hold, which is the construct's own.
|
|
274
|
+
* The same rule `featureTest` applies at runtime, applied here so `db` can be
|
|
275
|
+
* typed by the construct's schema.
|
|
276
|
+
*/
|
|
277
|
+
async function databaseSource(declared, sources, endpoints) {
|
|
278
|
+
const services = new Set(endpoints.map((endpoint) => endpoint.databaseService).filter(Boolean));
|
|
279
|
+
if (services.size === 0) return void 0;
|
|
280
|
+
for (const [id, declaration] of Object.entries(declared)) {
|
|
281
|
+
const source = sources[id];
|
|
282
|
+
if (!source || !declaration.kind.startsWith("database")) continue;
|
|
283
|
+
const exported = (await import(source.file))[source.exportName];
|
|
284
|
+
if (services.has(exported?.service)) return source;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** An import specifier for `file`, from the harness written into `dir`. */
|
|
288
|
+
function specifierFrom(dir, file) {
|
|
289
|
+
const path = relative(dir, file).replace(/\.tsx?$/, ".js");
|
|
290
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
291
|
+
}
|
|
292
|
+
function harnessModule(options) {
|
|
293
|
+
const { surfaces, auths, drivers, database, dir, files } = options;
|
|
294
|
+
const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
|
|
295
|
+
const imports = [
|
|
296
|
+
`import {${database ? " type DatabaseOf," : ""} featureTest, loadTestManifest } from '@geekmidas/constructs/testing';`,
|
|
297
|
+
...database ? [`import type { ${database.exportName} as __database } from '${specifierFrom(dir, database.file)}';`] : [],
|
|
298
|
+
`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
|
|
299
|
+
...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`),
|
|
300
|
+
...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
|
|
301
|
+
...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
|
|
302
|
+
...drivers.imports ? [drivers.imports] : [],
|
|
303
|
+
...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
|
|
304
|
+
];
|
|
305
|
+
const url = (id) => `manifest.env[${JSON.stringify(provideKey(id, "url"))}]!`;
|
|
306
|
+
const members = [...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${provideKey(id, "url")}. */
|
|
307
|
+
readonly ${propertyOf(id)} = create${id}({
|
|
308
|
+
baseURL: ${url(id)},
|
|
309
|
+
fetch: this.fetch,${secured ? `
|
|
310
|
+
// Served in-process: no authorizer runs, so nothing is signed. A scheme
|
|
311
|
+
// with no strategy sends its request without credentials.
|
|
312
|
+
authStrategies: {} as never,` : ""}
|
|
313
|
+
});`), ...auths.map((auth) => ` /** \`${auth.id}\`, at ${provideKey(auth.id, "url")}. */
|
|
314
|
+
readonly ${propertyOf(auth.id)} = createAuthClient({
|
|
315
|
+
baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
|
|
316
|
+
plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
|
|
317
|
+
fetchOptions: { customFetchImpl: this.fetch },
|
|
318
|
+
});`)];
|
|
319
|
+
return `// Generated by \`gkm test\` from the app's constructs — do not edit.
|
|
320
|
+
${imports.join("\n")}
|
|
321
|
+
|
|
322
|
+
${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));
|
|
323
|
+
|
|
324
|
+
/** A browser with a client for every surface the app declares. */
|
|
325
|
+
export class Browser extends TestBrowser {
|
|
326
|
+
${members.join("\n\n")}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The app's modules, by the path the manifest records for each. */
|
|
330
|
+
const modules = {
|
|
331
|
+
${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/** \`it\`, for a test that drives the app the way it runs deployed. */
|
|
335
|
+
export const it = featureTest${database ? "<Browser, DatabaseOf<typeof __database>>" : ""}({ manifest, modules, browser: Browser });
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/test/ready.ts
|
|
340
|
+
/**
|
|
341
|
+
* The hand-off between `gkm test` and the Vitest global setup.
|
|
342
|
+
*
|
|
343
|
+
* `gkm test` reconciles and migrates the test stage before Vitest starts; the
|
|
344
|
+
* setup, run inside Vitest, only has to load what that left. Started by plain
|
|
345
|
+
* `vitest` instead, the setup runs `gkm test --setup` itself first — so there is
|
|
346
|
+
* one way the stage gets ready, whichever started the suite.
|
|
347
|
+
*/
|
|
348
|
+
/** Set on the Vitest process by `gkm test`: the stage is ready, here. */
|
|
349
|
+
const TEST_READY_ENV = "GKM_TEST_READY";
|
|
350
|
+
/** Where `gkm test` records what it prepared, from where it ran. */
|
|
351
|
+
const TEST_READY_FILE = ".gkm/test-ready.json";
|
|
352
|
+
//#endregion
|
|
353
|
+
export { cacheBackendsIn as a, writeTestHarness as i, TEST_READY_FILE as n, driversFor as o, TEST_MANIFEST_ENV as r, TEST_READY_ENV as t };
|
|
354
|
+
|
|
355
|
+
//# sourceMappingURL=ready-CvP3JVEw.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ready-CvP3JVEw.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, rm, writeFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\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 { kebabCase, provideKey } 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}\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\tconst database = await databaseSource(\n\t\tdeclared,\n\t\tsources,\n\t\tendpoints.map(({ construct }) => construct as Endpoint<any, any, any, any>),\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\tdatabase,\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});\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}\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});\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 */\nasync function databaseSource(\n\tdeclared: Awaited<ReturnType<typeof discover>>,\n\tsources: Record<string, ConstructSource>,\n\tendpoints: Endpoint<any, any, any, any>[],\n): Promise<ConstructSource | undefined> {\n\tconst services = new Set<unknown>(\n\t\tendpoints.map((endpoint) => endpoint.databaseService).filter(Boolean),\n\t);\n\tif (services.size === 0) return undefined;\n\n\tfor (const [id, declaration] of Object.entries(declared)) {\n\t\tconst source = sources[id];\n\t\tif (!source || !declaration.kind.startsWith('database')) continue;\n\n\t\tconst exported = (await import(source.file))[source.exportName] as {\n\t\t\tservice?: unknown;\n\t\t};\n\t\tif (services.has(exported?.service)) return source;\n\t}\n\treturn undefined;\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\tdatabase: ConstructSource | undefined;\n\tsurfaces: { id: string; secured: boolean }[];\n\tauths: AuthClient[];\n\tdrivers: RuntimeDrivers;\n}): string {\n\tconst { surfaces, auths, drivers, database, dir, files } = options;\n\tconst plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();\n\n\tconst imports = [\n\t\t`import {${database ? ' type DatabaseOf,' : ''} featureTest, loadTestManifest } from '@geekmidas/constructs/testing';`,\n\t\t...(database\n\t\t\t? [\n\t\t\t\t\t`import type { ${database.exportName} as __database } from '${specifierFrom(dir, database.file)}';`,\n\t\t\t\t]\n\t\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// 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];\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${database ? '<Browser, DatabaseOf<typeof __database>>' : ''}({ manifest, modules, browser: Browser });\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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"}
|