@geekmidas/cli 10.0.0-alpha.6 → 10.0.0-alpha.61
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/README.md +225 -268
- package/bin/adjacent-tsconfig.mjs +97 -0
- package/bin/gkm.mjs +16 -2
- package/bin/owning-tsconfig-jsx-register.mjs +18 -0
- package/bin/owning-tsconfig-jsx.mjs +108 -0
- package/dist/{HostingerProvider-h3EbEp-Z.cjs → HostingerProvider-CgDaD8VR.cjs} +21 -22
- package/dist/HostingerProvider-CgDaD8VR.cjs.map +1 -0
- package/dist/{HostingerProvider-Cft7lEYr.mjs → HostingerProvider-DI_Pr2VE.mjs} +20 -21
- package/dist/HostingerProvider-DI_Pr2VE.mjs.map +1 -0
- package/dist/LegacyStateStore-C7vFAxbu.mjs +89 -0
- package/dist/LegacyStateStore-C7vFAxbu.mjs.map +1 -0
- package/dist/LegacyStateStore-JbBVFp_2.cjs +89 -0
- package/dist/LegacyStateStore-JbBVFp_2.cjs.map +1 -0
- package/dist/LocalStateStore-DHj_-CQB.cjs +176 -0
- package/dist/LocalStateStore-DHj_-CQB.cjs.map +1 -0
- package/dist/LocalStateStore-DfUwQsL8.mjs +165 -0
- package/dist/LocalStateStore-DfUwQsL8.mjs.map +1 -0
- package/dist/{openapi-D-dx9Akj.cjs → OpenApiTsGenerator-BSrgw7B0.cjs} +360 -470
- package/dist/OpenApiTsGenerator-BSrgw7B0.cjs.map +1 -0
- package/dist/{openapi-Bw8DWFWz.mjs → OpenApiTsGenerator-q5B0z8Xg.mjs} +325 -404
- package/dist/OpenApiTsGenerator-q5B0z8Xg.mjs.map +1 -0
- package/dist/{Route53Provider-RdpUDJQk.mjs → Route53Provider-Cvj40A8I.mjs} +12 -12
- package/dist/Route53Provider-Cvj40A8I.mjs.map +1 -0
- package/dist/{Route53Provider-di2fMM5N.cjs → Route53Provider-DVILBsvV.cjs} +20 -21
- package/dist/Route53Provider-DVILBsvV.cjs.map +1 -0
- package/dist/S3StateStore-Dq6hHtzY.mjs +161 -0
- package/dist/S3StateStore-Dq6hHtzY.mjs.map +1 -0
- package/dist/S3StateStore-xqnpUrzy.cjs +160 -0
- package/dist/S3StateStore-xqnpUrzy.cjs.map +1 -0
- package/dist/SSMStateStore-5dBRwVlJ.cjs +153 -0
- package/dist/SSMStateStore-5dBRwVlJ.cjs.map +1 -0
- package/dist/SSMStateStore-SUmvAbLi.mjs +154 -0
- package/dist/SSMStateStore-SUmvAbLi.mjs.map +1 -0
- package/dist/appPorts-C9z9Pzn9.mjs +223 -0
- package/dist/appPorts-C9z9Pzn9.mjs.map +1 -0
- package/dist/appPorts-Cu79hJF6.cjs +264 -0
- package/dist/appPorts-Cu79hJF6.cjs.map +1 -0
- package/dist/aws-DkHdTV-p.cjs +61 -0
- package/dist/aws-DkHdTV-p.cjs.map +1 -0
- package/dist/aws-DojzQnLd.mjs +61 -0
- package/dist/aws-DojzQnLd.mjs.map +1 -0
- package/dist/{backup-provisioner-BrsHwYG0.cjs → backup-provisioner-BmhIO_Yr.cjs} +21 -19
- package/dist/backup-provisioner-BmhIO_Yr.cjs.map +1 -0
- package/dist/{backup-provisioner-Bgj5Vi_L.mjs → backup-provisioner-DIVCbfee.mjs} +10 -7
- package/dist/backup-provisioner-DIVCbfee.mjs.map +1 -0
- package/dist/{bundler-Be8Zg4Wd.mjs → bundler-B5VsKW-_.mjs} +18 -29
- package/dist/bundler-B5VsKW-_.mjs.map +1 -0
- package/dist/{bundler-BY1OmV1e.cjs → bundler-DXATpO1T.cjs} +20 -32
- package/dist/bundler-DXATpO1T.cjs.map +1 -0
- package/dist/cli-DEA_J3Sa.mjs +69 -0
- package/dist/cli-DEA_J3Sa.mjs.map +1 -0
- package/dist/cli-DQdrVkj2.cjs +69 -0
- package/dist/cli-DQdrVkj2.cjs.map +1 -0
- package/dist/{config-CxP7P-qG.mjs → config-C5-EEdmr.mjs} +144 -127
- package/dist/config-C5-EEdmr.mjs.map +1 -0
- package/dist/{config-CNz5KaSR.cjs → config-DXnkCEq0.cjs} +212 -184
- package/dist/config-DXnkCEq0.cjs.map +1 -0
- package/dist/config.cjs +9 -6
- package/dist/config.d.cts +124 -33
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +124 -33
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -5
- package/dist/deploy/public.cjs +22 -0
- package/dist/deploy/public.d.cts +459 -0
- package/dist/deploy/public.d.cts.map +1 -0
- package/dist/deploy/public.d.mts +459 -0
- package/dist/deploy/public.d.mts.map +1 -0
- package/dist/deploy/public.mjs +5 -0
- package/dist/deploy/sniffer-envkit-patch.cjs +18 -10
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.cts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.d.mts +0 -2
- package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs +10 -2
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-hooks.cjs +13 -7
- package/dist/deploy/sniffer-hooks.cjs.map +1 -1
- package/dist/deploy/sniffer-hooks.d.cts +1 -2
- package/dist/deploy/sniffer-hooks.d.cts.map +1 -1
- package/dist/deploy/sniffer-hooks.d.mts +1 -2
- package/dist/deploy/sniffer-hooks.d.mts.map +1 -1
- package/dist/deploy/sniffer-hooks.mjs +10 -3
- package/dist/deploy/sniffer-hooks.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +16 -7
- package/dist/deploy/sniffer-loader.cjs.map +1 -1
- package/dist/deploy/sniffer-loader.d.cts +1 -1
- package/dist/deploy/sniffer-loader.d.mts +1 -1
- package/dist/deploy/sniffer-loader.mjs +14 -3
- package/dist/deploy/sniffer-loader.mjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.cjs +11 -11
- package/dist/deploy/sniffer-routes-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-routes-worker.d.cts +1 -1
- package/dist/deploy/sniffer-routes-worker.d.mts +1 -1
- package/dist/deploy/sniffer-routes-worker.mjs +3 -3
- package/dist/deploy/sniffer-routes-worker.mjs.map +1 -1
- package/dist/deploy/sniffer-worker.cjs +15 -6
- package/dist/deploy/sniffer-worker.cjs.map +1 -1
- package/dist/deploy/sniffer-worker.d.cts +1 -3
- package/dist/deploy/sniffer-worker.d.cts.map +1 -1
- package/dist/deploy/sniffer-worker.d.mts +1 -3
- package/dist/deploy/sniffer-worker.d.mts.map +1 -1
- package/dist/deploy/sniffer-worker.mjs +15 -4
- package/dist/deploy/sniffer-worker.mjs.map +1 -1
- package/dist/deploy-BJcLmVTc.mjs +3282 -0
- package/dist/deploy-BJcLmVTc.mjs.map +1 -0
- package/dist/deploy-BbJdgQ2b.cjs +3365 -0
- package/dist/deploy-BbJdgQ2b.cjs.map +1 -0
- package/dist/{discover-DTmFeloL.cjs → discover-Bc-4eA_p.cjs} +131 -103
- package/dist/discover-Bc-4eA_p.cjs.map +1 -0
- package/dist/{discover-CAlFgYec.mjs → discover-FrYvzkeC.mjs} +91 -58
- package/dist/discover-FrYvzkeC.mjs.map +1 -0
- package/dist/{emit-CiZS_5Ub.mjs → emit-C1e8qD08.mjs} +13 -26
- package/dist/emit-C1e8qD08.mjs.map +1 -0
- package/dist/{emit-CbcRolsa.cjs → emit-Dmc1nvdS.cjs} +31 -57
- package/dist/emit-Dmc1nvdS.cjs.map +1 -0
- package/dist/encryption-DaA9gRXr.mjs +90 -0
- package/dist/encryption-DaA9gRXr.mjs.map +1 -0
- package/dist/encryption-ojzUo7-z.cjs +91 -0
- package/dist/encryption-ojzUo7-z.cjs.map +1 -0
- package/dist/fakes-C9RiT6XG.mjs +125 -0
- package/dist/fakes-C9RiT6XG.mjs.map +1 -0
- package/dist/fakes-zW-jNAbQ.cjs +148 -0
- package/dist/fakes-zW-jNAbQ.cjs.map +1 -0
- package/dist/github-Cq57poPj.cjs +213 -0
- package/dist/github-Cq57poPj.cjs.map +1 -0
- package/dist/github-DmCj2u95.mjs +213 -0
- package/dist/github-DmCj2u95.mjs.map +1 -0
- package/dist/home-DOzBaWaL.cjs +33 -0
- package/dist/home-DOzBaWaL.cjs.map +1 -0
- package/dist/home-GI7pJl1M.mjs +22 -0
- package/dist/home-GI7pJl1M.mjs.map +1 -0
- package/dist/{index-DEbaleny.d.mts → index-Da1-oa9W.d.mts} +4 -23
- package/dist/index-Da1-oa9W.d.mts.map +1 -0
- package/dist/{index-BGGdjueF.d.cts → index-UiWCaGzU.d.cts} +4 -23
- package/dist/index-UiWCaGzU.d.cts.map +1 -0
- package/dist/index.cjs +10712 -12248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +10480 -12040
- package/dist/index.mjs.map +1 -1
- package/dist/migrate-5_J5JySj.mjs +265 -0
- package/dist/migrate-5_J5JySj.mjs.map +1 -0
- package/dist/migrate-BScRpCEx.cjs +270 -0
- package/dist/migrate-BScRpCEx.cjs.map +1 -0
- package/dist/openapi.cjs +198 -10
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +21 -19
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +21 -19
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +193 -5
- package/dist/openapi.mjs.map +1 -0
- package/dist/ready-DQIy21W5.mjs +760 -0
- package/dist/ready-DQIy21W5.mjs.map +1 -0
- package/dist/ready-XlNx-Gbo.cjs +807 -0
- package/dist/ready-XlNx-Gbo.cjs.map +1 -0
- package/dist/reconcile/public.cjs +3 -5
- package/dist/reconcile/public.d.cts +30 -33
- package/dist/reconcile/public.d.cts.map +1 -1
- package/dist/reconcile/public.d.mts +30 -33
- package/dist/reconcile/public.d.mts.map +1 -1
- package/dist/reconcile/public.mjs +3 -4
- package/dist/{reconcile-CFs8C_1a.mjs → reconcile--I7V4Klj.mjs} +4 -7
- package/dist/reconcile--I7V4Klj.mjs.map +1 -0
- package/dist/{reconcile-D0pimNiy.cjs → reconcile-CuCQBa5-.cjs} +5 -8
- package/dist/reconcile-CuCQBa5-.cjs.map +1 -0
- package/dist/rolldown-runtime-BhDjJH2R.mjs +15 -0
- package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-C0BPl7ul.cjs} +23 -10
- package/dist/sniffer-CQmJnNJM.cjs +7115 -0
- package/dist/sniffer-CQmJnNJM.cjs.map +1 -0
- package/dist/sniffer-bdleks5l.mjs +6561 -0
- package/dist/sniffer-bdleks5l.mjs.map +1 -0
- package/dist/storage-BzaUxmqe.mjs +124 -0
- package/dist/storage-BzaUxmqe.mjs.map +1 -0
- package/dist/storage-CSGoC3bK.cjs +152 -0
- package/dist/storage-CSGoC3bK.cjs.map +1 -0
- package/dist/store-BS8A7NcA.mjs +415 -0
- package/dist/store-BS8A7NcA.mjs.map +1 -0
- package/dist/store-KvNQwYaB.cjs +510 -0
- package/dist/store-KvNQwYaB.cjs.map +1 -0
- package/dist/types-Bp1mVxG0.d.mts +6410 -0
- package/dist/types-Bp1mVxG0.d.mts.map +1 -0
- package/dist/types-efNLu9xu.d.cts +6411 -0
- package/dist/types-efNLu9xu.d.cts.map +1 -0
- package/dist/vitest.cjs +65 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +29 -0
- package/dist/vitest.d.cts.map +1 -0
- package/dist/vitest.d.mts +29 -0
- package/dist/vitest.d.mts.map +1 -0
- package/dist/vitest.mjs +65 -0
- package/dist/vitest.mjs.map +1 -0
- package/dist/workspace/index.cjs +3 -4
- package/dist/workspace/index.d.cts +3 -3
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +2 -3
- package/dist/{workspace-LKiNyaDy.cjs → workspace-B64Xk1Mb.cjs} +1070 -1118
- package/dist/workspace-B64Xk1Mb.cjs.map +1 -0
- package/dist/{workspace-DIuZx-cc.mjs → workspace-Bp1xVaOz.mjs} +901 -972
- package/dist/workspace-Bp1xVaOz.mjs.map +1 -0
- package/package.json +38 -20
- package/dist/CachedStateProvider-BLo7zv7S.cjs +0 -3
- package/dist/CachedStateProvider-BxoAm-B2.mjs +0 -3
- package/dist/CachedStateProvider-CqPdW7CB.cjs +0 -60
- package/dist/CachedStateProvider-CqPdW7CB.cjs.map +0 -1
- package/dist/CachedStateProvider-DTniehMP.mjs +0 -54
- package/dist/CachedStateProvider-DTniehMP.mjs.map +0 -1
- package/dist/HostingerProvider-Cft7lEYr.mjs.map +0 -1
- package/dist/HostingerProvider-h3EbEp-Z.cjs.map +0 -1
- package/dist/LocalStateProvider-Bn6tS-oL.mjs +0 -42
- package/dist/LocalStateProvider-Bn6tS-oL.mjs.map +0 -1
- package/dist/LocalStateProvider-Db9SEjyH.cjs +0 -43
- package/dist/LocalStateProvider-Db9SEjyH.cjs.map +0 -1
- package/dist/Route53Provider-RdpUDJQk.mjs.map +0 -1
- package/dist/Route53Provider-di2fMM5N.cjs.map +0 -1
- package/dist/SSMStateProvider-BRt2py-A.mjs +0 -66
- package/dist/SSMStateProvider-BRt2py-A.mjs.map +0 -1
- package/dist/SSMStateProvider-DyX8s9HI.cjs +0 -66
- package/dist/SSMStateProvider-DyX8s9HI.cjs.map +0 -1
- package/dist/backup-provisioner-Bgj5Vi_L.mjs.map +0 -1
- package/dist/backup-provisioner-BrsHwYG0.cjs.map +0 -1
- package/dist/bundler-BY1OmV1e.cjs.map +0 -1
- package/dist/bundler-Be8Zg4Wd.mjs.map +0 -1
- package/dist/chunk-Duj1WY3L.mjs +0 -7
- package/dist/config-CNz5KaSR.cjs.map +0 -1
- package/dist/config-CxP7P-qG.mjs.map +0 -1
- package/dist/credentials-B5zRHdAl.mjs +0 -141
- package/dist/credentials-B5zRHdAl.mjs.map +0 -1
- package/dist/credentials-BK3Z93MX.mjs +0 -2548
- package/dist/credentials-BK3Z93MX.mjs.map +0 -1
- package/dist/credentials-BapzjyV5.cjs +0 -195
- package/dist/credentials-BapzjyV5.cjs.map +0 -1
- package/dist/credentials-DzsXZzkx.cjs +0 -2680
- package/dist/credentials-DzsXZzkx.cjs.map +0 -1
- package/dist/discover-CAlFgYec.mjs.map +0 -1
- package/dist/discover-DTmFeloL.cjs.map +0 -1
- package/dist/dokploy-api-BS2xnJLZ.mjs +0 -3
- package/dist/dokploy-api-BsVH0y_s.cjs +0 -718
- package/dist/dokploy-api-BsVH0y_s.cjs.map +0 -1
- package/dist/dokploy-api-BuxDRo3l.cjs +0 -3
- package/dist/dokploy-api-aTFPz7_4.mjs +0 -706
- package/dist/dokploy-api-aTFPz7_4.mjs.map +0 -1
- package/dist/emit-CbcRolsa.cjs.map +0 -1
- package/dist/emit-CiZS_5Ub.mjs.map +0 -1
- package/dist/encryption-C6jaU0hE.cjs +0 -54
- package/dist/encryption-C6jaU0hE.cjs.map +0 -1
- package/dist/encryption-CHzzeR8a.cjs +0 -4
- package/dist/encryption-CQUncu7v.mjs +0 -3
- package/dist/encryption-D-abyhZx.mjs +0 -42
- package/dist/encryption-D-abyhZx.mjs.map +0 -1
- package/dist/fullstack-secrets-C-GGohke.cjs +0 -111
- package/dist/fullstack-secrets-C-GGohke.cjs.map +0 -1
- package/dist/fullstack-secrets-D1o9-G5l.mjs +0 -87
- package/dist/fullstack-secrets-D1o9-G5l.mjs.map +0 -1
- package/dist/generator-CNzBw7g5.cjs +0 -289
- package/dist/generator-CNzBw7g5.cjs.map +0 -1
- package/dist/generator-VE2JrFlY.mjs +0 -253
- package/dist/generator-VE2JrFlY.mjs.map +0 -1
- package/dist/index-BGGdjueF.d.cts.map +0 -1
- package/dist/index-DEbaleny.d.mts.map +0 -1
- package/dist/openapi-Bw8DWFWz.mjs.map +0 -1
- package/dist/openapi-D-dx9Akj.cjs.map +0 -1
- package/dist/openapi-react-query-Byltvm_7.mjs +0 -167
- package/dist/openapi-react-query-Byltvm_7.mjs.map +0 -1
- package/dist/openapi-react-query-CmJ1xDbn.cjs +0 -173
- package/dist/openapi-react-query-CmJ1xDbn.cjs.map +0 -1
- package/dist/openapi-react-query.cjs +0 -4
- package/dist/openapi-react-query.d.cts +0 -11
- package/dist/openapi-react-query.d.cts.map +0 -1
- package/dist/openapi-react-query.d.mts +0 -11
- package/dist/openapi-react-query.d.mts.map +0 -1
- package/dist/openapi-react-query.mjs +0 -4
- package/dist/reconcile-CFs8C_1a.mjs.map +0 -1
- package/dist/reconcile-D0pimNiy.cjs.map +0 -1
- package/dist/setup-8IWPsRIN.cjs +0 -11
- package/dist/setup-BWMhbW-p.mjs +0 -443
- package/dist/setup-BWMhbW-p.mjs.map +0 -1
- package/dist/setup-D7L4BgBT.cjs +0 -473
- package/dist/setup-D7L4BgBT.cjs.map +0 -1
- package/dist/setup-DGIPd2fh.mjs +0 -11
- package/dist/storage-BJmzYxt8.cjs +0 -363
- package/dist/storage-BJmzYxt8.cjs.map +0 -1
- package/dist/storage-BM9hGpgv.cjs +0 -7
- package/dist/storage-Bi24WfoW.mjs +0 -297
- package/dist/storage-Bi24WfoW.mjs.map +0 -1
- package/dist/storage-CYlGwWTj.mjs +0 -3
- package/dist/sync-CmEuNZsB.mjs +0 -76
- package/dist/sync-CmEuNZsB.mjs.map +0 -1
- package/dist/sync-D306yOWu.cjs +0 -4
- package/dist/sync-SizaChmC.cjs +0 -93
- package/dist/sync-SizaChmC.cjs.map +0 -1
- package/dist/sync-hOqAWHR6.mjs +0 -3
- package/dist/types-BLSNdrk0.d.cts +0 -2176
- package/dist/types-BLSNdrk0.d.cts.map +0 -1
- package/dist/types-C3POXhw3.d.mts +0 -2176
- package/dist/types-C3POXhw3.d.mts.map +0 -1
- package/dist/workspace-CGaUAw5A.cjs +0 -9
- package/dist/workspace-CnTqIlB3.mjs +0 -8
- package/dist/workspace-DIuZx-cc.mjs.map +0 -1
- package/dist/workspace-LKiNyaDy.cjs.map +0 -1
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import { i as ConstructGenerator, t as discover } from "./discover-FrYvzkeC.mjs";
|
|
2
|
+
import { n as EndpointGenerator, r as runtimeFor, t as OpenApiTsGenerator } from "./OpenApiTsGenerator-q5B0z8Xg.mjs";
|
|
3
|
+
import { n as DEFAULT_CACHE, t as readFakes } from "./fakes-C9RiT6XG.mjs";
|
|
4
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { canonicalId, kebabCase, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
10
|
+
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
11
|
+
import { serviceContext } from "@geekmidas/services";
|
|
12
|
+
//#region src/generators/drivers.ts
|
|
13
|
+
/**
|
|
14
|
+
* Driver registration for generated entry points.
|
|
15
|
+
*
|
|
16
|
+
* A construct declares that it needs object storage and is handed one URL; the
|
|
17
|
+
* scheme in that URL picks the driver that builds the client. Which drivers
|
|
18
|
+
* exist is therefore the *entry point's* decision, not the construct's and not
|
|
19
|
+
* the application's — an app that never talks to S3 should never resolve the AWS
|
|
20
|
+
* SDK, and application code that registers a driver has named a provider in the
|
|
21
|
+
* one layer this design keeps provider-free.
|
|
22
|
+
*
|
|
23
|
+
* So the generated entries register: `gkm dev`'s server for local development,
|
|
24
|
+
* and each Lambda handler for its own target. Registration is an explicit call
|
|
25
|
+
* rather than an import side effect, because a side-effecting module is exactly
|
|
26
|
+
* what a bundler is entitled to drop.
|
|
27
|
+
*/
|
|
28
|
+
/** The `s3://` driver, which serves MinIO locally and S3 deployed. */
|
|
29
|
+
const S3 = {
|
|
30
|
+
imports: `import { registerStorageDriver } from '@geekmidas/storage';\nimport { s3Driver } from '@geekmidas/storage/aws';`,
|
|
31
|
+
setup: "registerStorageDriver(s3Driver);"
|
|
32
|
+
};
|
|
33
|
+
const NONE = {
|
|
34
|
+
imports: "",
|
|
35
|
+
setup: ""
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The cache driver for a backend — exactly one, never all three.
|
|
39
|
+
*
|
|
40
|
+
* Keyed off the *backend* rather than off a dependency, because unlike storage
|
|
41
|
+
* the backend already says which driver is right: a project caching in Postgres
|
|
42
|
+
* resolves `pg` and never `ioredis`, and one caching in Upstash resolves
|
|
43
|
+
* neither. That is the whole reason the drivers live behind separate subpaths.
|
|
44
|
+
*
|
|
45
|
+
* Both HTTP schemes are registered for Upstash, because the local proxy speaks
|
|
46
|
+
* `http://` and Upstash speaks `https://`, and an entry that registered one
|
|
47
|
+
* would work in exactly one of the two places.
|
|
48
|
+
*/
|
|
49
|
+
const CACHE_DRIVERS = {
|
|
50
|
+
upstash: {
|
|
51
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { upstashCacheDriver, upstashInsecureCacheDriver } from '@geekmidas/cache/upstash';`,
|
|
52
|
+
setup: "registerCacheDriver(upstashCacheDriver);\nregisterCacheDriver(upstashInsecureCacheDriver);"
|
|
53
|
+
},
|
|
54
|
+
elasticache: {
|
|
55
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { redisCacheDriver, redissCacheDriver } from '@geekmidas/cache/redis';`,
|
|
56
|
+
setup: "registerCacheDriver(redisCacheDriver);\nregisterCacheDriver(redissCacheDriver);"
|
|
57
|
+
},
|
|
58
|
+
db: {
|
|
59
|
+
imports: `import { registerCacheDriver } from '@geekmidas/cache';\nimport { postgresCacheDriver } from '@geekmidas/cache/postgres';`,
|
|
60
|
+
setup: "registerCacheDriver(postgresCacheDriver);"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Everything a generated entry should register: object storage, and the cache.
|
|
65
|
+
*
|
|
66
|
+
* Merged here rather than threaded separately, because a generated file has one
|
|
67
|
+
* import block and one setup block whatever fills them.
|
|
68
|
+
*/
|
|
69
|
+
function driversFor(options) {
|
|
70
|
+
const backends = options.cache === false || options.cache === void 0 ? [] : typeof options.cache === "string" ? [options.cache] : options.cache;
|
|
71
|
+
const parts = [storageDriversFor(options.appRoot), ...[...new Set(backends)].map((backend) => CACHE_DRIVERS[backend] ?? CACHE_DRIVERS[DEFAULT_CACHE.aws])].filter((part) => part.imports || part.setup);
|
|
72
|
+
if (parts.length === 0) return NONE;
|
|
73
|
+
return {
|
|
74
|
+
imports: parts.map((part) => part.imports).join("\n"),
|
|
75
|
+
setup: parts.map((part) => part.setup).join("\n")
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Which cache backends an app's entry actually has to speak.
|
|
80
|
+
*
|
|
81
|
+
* The config answers this for a cache that named nowhere, and the *declaration*
|
|
82
|
+
* answers it for one that named a database — `orders.cache('Sessions')` is a
|
|
83
|
+
* statement about the application, so a deployment cannot move that cache and
|
|
84
|
+
* the entry cannot register a driver for somewhere else.
|
|
85
|
+
*
|
|
86
|
+
* Reading only the config is how an entry ends up registering the Upstash
|
|
87
|
+
* driver for a URL the target composed as `postgres://`, which fails at the
|
|
88
|
+
* first request with `UnregisteredCacheScheme` and a stack that points at the
|
|
89
|
+
* cache rather than at the config that disagreed.
|
|
90
|
+
*/
|
|
91
|
+
function cacheBackendsIn(manifest, configured) {
|
|
92
|
+
const caches = Object.values(manifest).filter((d) => d.kind === "cache");
|
|
93
|
+
return [...new Set(caches.map((c) => c.of ? "db" : configured))];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The drivers an app's entry points should register.
|
|
97
|
+
*
|
|
98
|
+
* Keyed off the dependency rather than off a declared bucket, because the entry
|
|
99
|
+
* is generated before discovery has run on the watcher's rebuild path — and an
|
|
100
|
+
* app that installed `@geekmidas/storage` has already paid for the SDK it would
|
|
101
|
+
* otherwise resolve lazily.
|
|
102
|
+
*
|
|
103
|
+
* "Installed" the way Node resolves it: the app's own `package.json`, or any
|
|
104
|
+
* directory above it. A workspace that lists `@geekmidas/storage` once, at its
|
|
105
|
+
* root, is the common case — and reading only the app's file registered no S3
|
|
106
|
+
* driver there, so the first service to inject a bucket threw
|
|
107
|
+
* `UnregisteredStorageScheme`. A queue whose consumer depended on one logged
|
|
108
|
+
* that once and was never polled.
|
|
109
|
+
*/
|
|
110
|
+
function storageDriversFor(appRoot) {
|
|
111
|
+
return dependsOnStorage(appRoot) ? S3 : NONE;
|
|
112
|
+
}
|
|
113
|
+
function dependsOnStorage(appRoot) {
|
|
114
|
+
for (let dir = resolve(appRoot);; dir = dirname(dir)) {
|
|
115
|
+
if (listsStorage(join(dir, "package.json"))) return true;
|
|
116
|
+
if (dirname(dir) === dir) return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function listsStorage(manifest) {
|
|
120
|
+
if (!existsSync(manifest)) return false;
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
123
|
+
return Boolean(pkg.dependencies?.["@geekmidas/storage"] ?? pkg.devDependencies?.["@geekmidas/storage"]);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/generators/SubscriberGenerator.ts
|
|
130
|
+
var SubscriberGenerator = class extends ConstructGenerator {
|
|
131
|
+
isConstruct(value) {
|
|
132
|
+
return Subscriber.isSubscriber(value);
|
|
133
|
+
}
|
|
134
|
+
async build(context, constructs, outputDir, options) {
|
|
135
|
+
const target = options?.target ?? "aws";
|
|
136
|
+
const logger = console;
|
|
137
|
+
const subscriberInfos = [];
|
|
138
|
+
if (target === "server") {
|
|
139
|
+
await this.generateServerSubscribersFile(outputDir, constructs);
|
|
140
|
+
logger.log(`Generated server subscribers file with ${constructs.length} subscribers (polling mode)`);
|
|
141
|
+
return subscriberInfos;
|
|
142
|
+
}
|
|
143
|
+
if (constructs.length === 0) return subscriberInfos;
|
|
144
|
+
const subscribersDir = join(outputDir, "subscribers");
|
|
145
|
+
await mkdir(subscribersDir, { recursive: true });
|
|
146
|
+
for (const { key, construct, path } of constructs) {
|
|
147
|
+
const handlerFile = await this.generateSubscriberHandler(subscribersDir, path.relative, key, construct, context, construct.owner);
|
|
148
|
+
subscriberInfos.push({
|
|
149
|
+
name: key,
|
|
150
|
+
handler: relative(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
151
|
+
subscribedEvents: construct.subscribedEvents || [],
|
|
152
|
+
...construct.topicName ? {
|
|
153
|
+
transport: "topic",
|
|
154
|
+
topic: construct.topicName
|
|
155
|
+
} : {},
|
|
156
|
+
timeout: construct.timeout,
|
|
157
|
+
memorySize: construct.memorySize,
|
|
158
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
159
|
+
dependencies: construct.constructs
|
|
160
|
+
});
|
|
161
|
+
logger.log(`Generated subscriber handler: ${key}`);
|
|
162
|
+
}
|
|
163
|
+
return subscriberInfos;
|
|
164
|
+
}
|
|
165
|
+
async generateSubscriberHandler(outputDir, sourceFile, exportName, _subscriber, context, owner) {
|
|
166
|
+
const handlerFileName = `${exportName}.ts`;
|
|
167
|
+
const handlerPath = join(outputDir, handlerFileName);
|
|
168
|
+
const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
169
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
170
|
+
const content = `import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws';
|
|
171
|
+
import { ${exportName} } from '${importPath}';
|
|
172
|
+
${runtime.imports}
|
|
173
|
+
${runtime.bindings}
|
|
174
|
+
|
|
175
|
+
const adapter = new AWSLambdaSubscriber(envParser, ${exportName});
|
|
176
|
+
|
|
177
|
+
export const handler = adapter.handler;
|
|
178
|
+
`;
|
|
179
|
+
await writeFile(handlerPath, content);
|
|
180
|
+
return handlerPath;
|
|
181
|
+
}
|
|
182
|
+
async generateServerSubscribersFile(outputDir, subscribers) {
|
|
183
|
+
await mkdir(outputDir, { recursive: true });
|
|
184
|
+
const subscribersPath = join(outputDir, "subscribers.ts");
|
|
185
|
+
if (subscribers.length === 0) {
|
|
186
|
+
await writeFile(subscribersPath, `/**
|
|
187
|
+
* Generated subscribers setup — this app declares none.
|
|
188
|
+
*/
|
|
189
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
190
|
+
import type { Logger } from '@geekmidas/logger';
|
|
191
|
+
|
|
192
|
+
export async function setupSubscribers(
|
|
193
|
+
_app: unknown,
|
|
194
|
+
_envParser: EnvironmentParser<any>,
|
|
195
|
+
_logger: Logger,
|
|
196
|
+
): Promise<(port: number) => Promise<void>> {
|
|
197
|
+
return async () => {};
|
|
198
|
+
}
|
|
199
|
+
`);
|
|
200
|
+
return subscribersPath;
|
|
201
|
+
}
|
|
202
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const { path, key } of subscribers) {
|
|
204
|
+
const importPath = relative(dirname(subscribersPath), path.relative).replace(/\.ts$/, ".js");
|
|
205
|
+
importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
|
|
206
|
+
}
|
|
207
|
+
const content = `/**
|
|
208
|
+
* Generated subscribers setup.
|
|
209
|
+
*
|
|
210
|
+
* Each subscriber is run against the topic it is bound to, through that
|
|
211
|
+
* topic's own connection string:
|
|
212
|
+
*
|
|
213
|
+
* - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
|
|
214
|
+
* server, and the route is subscribed to the topic with a filter policy of
|
|
215
|
+
* the events it named, so SNS fans out to each subscriber. The route runs the
|
|
216
|
+
* subscriber through the same adaptor a Lambda subscription does.
|
|
217
|
+
* - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
|
|
218
|
+
* each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
|
|
219
|
+
* every subscriber sees every event; replicas of one subscriber share it.
|
|
220
|
+
*
|
|
221
|
+
* GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
|
|
222
|
+
* GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
|
|
223
|
+
* emulator it defaults to this server through host.docker.internal.
|
|
224
|
+
*/
|
|
225
|
+
import { connect } from 'node:net';
|
|
226
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
227
|
+
import type { Logger } from '@geekmidas/logger';
|
|
228
|
+
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
229
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
230
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
231
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
232
|
+
|
|
233
|
+
const subscribers = [
|
|
234
|
+
${subscribers.map(({ key, construct }) => {
|
|
235
|
+
const connectionKey = construct.topicName ? provideKey(canonicalId(construct.topicName), "publisherConnectionString") : void 0;
|
|
236
|
+
return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
|
|
237
|
+
}).join("\n")}
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
/** The route a subscriber's pushes arrive on. */
|
|
241
|
+
export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
|
|
242
|
+
|
|
243
|
+
/** Resolves once something is accepting connections on the port. */
|
|
244
|
+
async function listening(port: number): Promise<void> {
|
|
245
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
246
|
+
const open = await new Promise<boolean>((resolve) => {
|
|
247
|
+
const socket = connect(port, '127.0.0.1');
|
|
248
|
+
socket.once('connect', () => { socket.end(); resolve(true); });
|
|
249
|
+
socket.once('error', () => resolve(false));
|
|
250
|
+
});
|
|
251
|
+
if (open) return;
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Mount each pushed subscriber's route and start each polled one. Returns what
|
|
258
|
+
* to run once the server is listening: subscribing a route makes SNS send its
|
|
259
|
+
* confirmation at once, so the route has to be reachable first.
|
|
260
|
+
*/
|
|
261
|
+
export async function setupSubscribers(
|
|
262
|
+
app: { post: (path: string, handler: (c: any) => unknown) => unknown },
|
|
263
|
+
envParser: EnvironmentParser<any>,
|
|
264
|
+
logger: Logger,
|
|
265
|
+
): Promise<(port: number) => Promise<void>> {
|
|
266
|
+
if (process.env.GKM_SUBSCRIBERS === 'off') {
|
|
267
|
+
logger.info('Subscribers are off (--no-subscribers)');
|
|
268
|
+
return async () => {};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
272
|
+
const connections = new Map<string, EventConnection>();
|
|
273
|
+
const afterListening: ((port: number) => Promise<void>)[] = [];
|
|
274
|
+
|
|
275
|
+
for (const { id, subscriber, topic, connectionKey } of subscribers) {
|
|
276
|
+
const events = subscriber.subscribedEvents || [];
|
|
277
|
+
if (events.length === 0) {
|
|
278
|
+
logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const connectionString = connectionKey ? process.env[connectionKey] : undefined;
|
|
282
|
+
if (!connectionString) {
|
|
283
|
+
logger.error(
|
|
284
|
+
{ subscriber: id, connectionKey },
|
|
285
|
+
'No connection string for the topic this subscriber is bound to',
|
|
286
|
+
);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
if (connectionString.startsWith('sns:')) {
|
|
292
|
+
const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
|
|
293
|
+
const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/subscribers');
|
|
294
|
+
const address = snsUrl.parse(connectionString);
|
|
295
|
+
const route = SUBSCRIBER_ROUTE(id);
|
|
296
|
+
const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
|
|
297
|
+
topicArn: address.topicArn,
|
|
298
|
+
// An emulator signs nothing. Decided by the address this server was
|
|
299
|
+
// configured with, never by anything a request says about itself.
|
|
300
|
+
verify: !address.endpoint,
|
|
301
|
+
...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.post(route, async (c: any) => {
|
|
305
|
+
const body = JSON.parse(await c.req.text());
|
|
306
|
+
const response = await adaptor.handle(body);
|
|
307
|
+
return c.json(response.body, response.status);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
afterListening.push(async (port) => {
|
|
311
|
+
const base =
|
|
312
|
+
process.env.GKM_SUBSCRIBER_PUSH_URL ??
|
|
313
|
+
(address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
|
|
314
|
+
if (!base) {
|
|
315
|
+
logger.error(
|
|
316
|
+
{ subscriber: id },
|
|
317
|
+
'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
322
|
+
await subscribeHttpEndpoint(connection, {
|
|
323
|
+
endpoint: new URL(route, base).toString(),
|
|
324
|
+
events,
|
|
325
|
+
});
|
|
326
|
+
logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
|
|
327
|
+
});
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let connection = connections.get(connectionString);
|
|
332
|
+
if (!connection) {
|
|
333
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
334
|
+
connections.set(connectionString, connection);
|
|
335
|
+
}
|
|
336
|
+
// Named, so a broker with one address for everything (pg-boss) gives
|
|
337
|
+
// this subscriber a queue of its own: fan-out, not competition.
|
|
338
|
+
const eventSubscriber = await Subscriber.fromConnection(connection, {
|
|
339
|
+
topic,
|
|
340
|
+
subscription: id,
|
|
341
|
+
});
|
|
342
|
+
const services = subscriber.services.length > 0
|
|
343
|
+
? await serviceDiscovery.register(subscriber.services)
|
|
344
|
+
: {};
|
|
345
|
+
// The subscriber's database — the worker's, or its own — as \`db\`. A
|
|
346
|
+
// pushed subscriber gets it from the adaptor above instead.
|
|
347
|
+
const db = subscriber.databaseService
|
|
348
|
+
? (await serviceDiscovery.register([subscriber.databaseService]))[
|
|
349
|
+
subscriber.databaseService.serviceName
|
|
350
|
+
]
|
|
351
|
+
: undefined;
|
|
352
|
+
|
|
353
|
+
await eventSubscriber.subscribe(events, async (event) => {
|
|
354
|
+
try {
|
|
355
|
+
await subscriber.handler({
|
|
356
|
+
events: [event],
|
|
357
|
+
services: services as any,
|
|
358
|
+
logger: subscriber.logger,
|
|
359
|
+
db,
|
|
360
|
+
} as any);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.error({ error, event, subscriber: id }, 'Failed to process event');
|
|
363
|
+
// Rethrown so the transport keeps the event for a retry — as a queue
|
|
364
|
+
// consumer's failure does, and as a pushed subscriber's 500 does.
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
logger.info({ subscriber: id, events }, 'Subscriber started polling');
|
|
369
|
+
} catch (error) {
|
|
370
|
+
// The message too: a missing module's code alone does not say which.
|
|
371
|
+
logger.error(
|
|
372
|
+
{ error, message: (error as Error)?.message, subscriber: id },
|
|
373
|
+
'Failed to set up subscriber',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const shutdown = () => {
|
|
379
|
+
for (const connection of connections.values()) void connection.close();
|
|
380
|
+
};
|
|
381
|
+
process.once('SIGTERM', shutdown);
|
|
382
|
+
process.once('SIGINT', shutdown);
|
|
383
|
+
|
|
384
|
+
return async (port) => {
|
|
385
|
+
if (afterListening.length === 0) return;
|
|
386
|
+
await listening(port);
|
|
387
|
+
for (const subscribe of afterListening) {
|
|
388
|
+
await subscribe(port).catch((error) => {
|
|
389
|
+
logger.error({ error }, 'Failed to subscribe for push');
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
`;
|
|
395
|
+
await writeFile(subscribersPath, content);
|
|
396
|
+
return subscribersPath;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/test/harness.ts
|
|
401
|
+
/**
|
|
402
|
+
* What `gkm test` writes for a feature test to be built from.
|
|
403
|
+
*
|
|
404
|
+
* `gkm test` already discovers an app's constructs and resolves the test
|
|
405
|
+
* stage's environment; this keeps both, under `.gkm/test/`:
|
|
406
|
+
*
|
|
407
|
+
* - `manifest.json` — which construct and endpoint is exported where, and the
|
|
408
|
+
* environment the test stage resolved, keyed as the constructs derive keys.
|
|
409
|
+
* - `clients/<surface>.ts` — each surface's typed client, from the same
|
|
410
|
+
* generator `gkm build` uses for the app's own.
|
|
411
|
+
* - `index.ts` — the configured `it`, and a `Browser` holding one client per
|
|
412
|
+
* surface and one per auth server, each on the browser's `fetch`.
|
|
413
|
+
*
|
|
414
|
+
* A test imports `it` from there and declares nothing — not a construct, not an
|
|
415
|
+
* environment key, not a client.
|
|
416
|
+
*/
|
|
417
|
+
/**
|
|
418
|
+
* The variable the manifest's path travels in — what
|
|
419
|
+
* `@geekmidas/constructs/testing` reads. Its own constant rather than an import:
|
|
420
|
+
* that entry carries the whole harness (MSW, Hono, Vitest), and importing it
|
|
421
|
+
* here put all of it on the start-up path of every `gkm` command.
|
|
422
|
+
*/
|
|
423
|
+
const TEST_MANIFEST_ENV = "GKM_TEST_MANIFEST";
|
|
424
|
+
/** Where the harness lives, relative to the workspace root. */
|
|
425
|
+
const TEST_HARNESS_DIR = ".gkm/test";
|
|
426
|
+
/**
|
|
427
|
+
* Better-auth's client plugin for each server plugin id.
|
|
428
|
+
*
|
|
429
|
+
* Written down rather than derived: the ids and the client names do not follow
|
|
430
|
+
* one rule (`admin` pairs with `adminClient`, `email-otp` with
|
|
431
|
+
* `emailOTPClient`), and a guess that missed would hand a test a client
|
|
432
|
+
* silently lacking every method of that plugin. A server plugin not listed has
|
|
433
|
+
* no client counterpart — `bearer`, `open-api` — or needs one from another
|
|
434
|
+
* package, and is left out.
|
|
435
|
+
*/
|
|
436
|
+
const CLIENT_PLUGINS = {
|
|
437
|
+
admin: "adminClient",
|
|
438
|
+
anonymous: "anonymousClient",
|
|
439
|
+
"device-authorization": "deviceAuthorizationClient",
|
|
440
|
+
"email-otp": "emailOTPClient",
|
|
441
|
+
jwt: "jwtClient",
|
|
442
|
+
"last-login-method": "lastLoginMethodClient",
|
|
443
|
+
"magic-link": "magicLinkClient",
|
|
444
|
+
"multi-session": "multiSessionClient",
|
|
445
|
+
"oauth-popup": "oauthPopupClient",
|
|
446
|
+
"one-tap": "oneTapClient",
|
|
447
|
+
"one-time-token": "oneTimeTokenClient",
|
|
448
|
+
organization: "organizationClient",
|
|
449
|
+
"phone-number": "phoneNumberClient",
|
|
450
|
+
siwe: "siweClient",
|
|
451
|
+
"two-factor": "twoFactorClient",
|
|
452
|
+
username: "usernameClient"
|
|
453
|
+
};
|
|
454
|
+
/** Where a project keeps its test factories, relative to its root. */
|
|
455
|
+
const DEFAULT_FACTORIES_DIR = "test/factories";
|
|
456
|
+
/** A file in the factories folder that names no database construct. */
|
|
457
|
+
var UnknownFactoryFile = class extends Error {
|
|
458
|
+
file;
|
|
459
|
+
known;
|
|
460
|
+
constructor(file, known) {
|
|
461
|
+
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.`);
|
|
462
|
+
this.file = file;
|
|
463
|
+
this.known = known;
|
|
464
|
+
this.name = "UnknownFactoryFile";
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
/** A factory file that exports no `createFactory`. */
|
|
468
|
+
var FactoryHasNoCreate = class extends Error {
|
|
469
|
+
file;
|
|
470
|
+
constructor(file) {
|
|
471
|
+
super(`${file} is a test factory but exports no \`createFactory\`. Export \`function createFactory(db: Kysely<Database>)\` returning the factory.`);
|
|
472
|
+
this.file = file;
|
|
473
|
+
this.name = "FactoryHasNoCreate";
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* The factories in a folder, each matched to the database it is named after.
|
|
478
|
+
*
|
|
479
|
+
* A file that names no database, or exports no `createFactory`, is refused
|
|
480
|
+
* rather than skipped: either is a factory no test would ever be handed.
|
|
481
|
+
*/
|
|
482
|
+
async function factorySources(folder, databases) {
|
|
483
|
+
if (!folder) return [];
|
|
484
|
+
const entries = await readdir(folder, { withFileTypes: true }).catch((error) => {
|
|
485
|
+
if (error.code === "ENOENT") return [];
|
|
486
|
+
throw error;
|
|
487
|
+
});
|
|
488
|
+
const found = [];
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
if (!entry.isFile() || !isFactoryFile(entry.name)) continue;
|
|
491
|
+
const file = join(folder, entry.name);
|
|
492
|
+
const id = canonicalId(entry.name.slice(0, -extname(entry.name).length));
|
|
493
|
+
if (!databases.includes(id)) throw new UnknownFactoryFile(file, databases);
|
|
494
|
+
if (typeof (await import(pathToFileURL(file).href)).createFactory !== "function") throw new FactoryHasNoCreate(file);
|
|
495
|
+
found.push({
|
|
496
|
+
file,
|
|
497
|
+
service: serviceKey(id)
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return found.sort((a, b) => a.service < b.service ? -1 : 1);
|
|
501
|
+
}
|
|
502
|
+
function isFactoryFile(file) {
|
|
503
|
+
if (file.endsWith(".d.ts")) return false;
|
|
504
|
+
if (/\.(spec|test)\.[cm]?[jt]s$/.test(file)) return false;
|
|
505
|
+
return [
|
|
506
|
+
".ts",
|
|
507
|
+
".mts",
|
|
508
|
+
".js",
|
|
509
|
+
".mjs"
|
|
510
|
+
].includes(extname(file));
|
|
511
|
+
}
|
|
512
|
+
/** Write the manifest, the clients and the harness; return the manifest path. */
|
|
513
|
+
async function writeTestHarness(options) {
|
|
514
|
+
const sources = {};
|
|
515
|
+
const declared = await discover({
|
|
516
|
+
patterns: options.patterns,
|
|
517
|
+
cwd: options.root,
|
|
518
|
+
sources
|
|
519
|
+
});
|
|
520
|
+
const endpoints = (await new EndpointGenerator().load(options.patterns, options.root)).map(({ key, construct, path }) => ({
|
|
521
|
+
surface: construct.owner ?? construct.surface?.id,
|
|
522
|
+
construct,
|
|
523
|
+
source: {
|
|
524
|
+
file: path.absolute,
|
|
525
|
+
export: key
|
|
526
|
+
}
|
|
527
|
+
})).filter((endpoint) => endpoint.surface !== void 0);
|
|
528
|
+
const subscribers = (await new SubscriberGenerator().load(options.patterns, options.root)).map(({ key, path }) => ({ source: {
|
|
529
|
+
file: path.absolute,
|
|
530
|
+
export: key
|
|
531
|
+
} }));
|
|
532
|
+
const manifest = {
|
|
533
|
+
stage: options.stage,
|
|
534
|
+
constructs: Object.fromEntries(Object.entries(declared).flatMap(([id, declaration]) => {
|
|
535
|
+
const source = sources[id];
|
|
536
|
+
return source ? [[id, {
|
|
537
|
+
kind: declaration.kind,
|
|
538
|
+
source: {
|
|
539
|
+
file: source.file,
|
|
540
|
+
export: source.exportName
|
|
541
|
+
}
|
|
542
|
+
}]] : [];
|
|
543
|
+
})),
|
|
544
|
+
endpoints: endpoints.map(({ surface, source }) => ({
|
|
545
|
+
surface,
|
|
546
|
+
source
|
|
547
|
+
})),
|
|
548
|
+
subscribers,
|
|
549
|
+
env: options.env
|
|
550
|
+
};
|
|
551
|
+
const surfaces = [...new Set(endpoints.map(({ surface }) => surface))];
|
|
552
|
+
const generator = new OpenApiTsGenerator();
|
|
553
|
+
const clients = await Promise.all(surfaces.map(async (surface) => ({
|
|
554
|
+
file: `${kebabCase(surface)}.ts`,
|
|
555
|
+
content: await generator.generate(endpoints.filter((endpoint) => endpoint.surface === surface).map(({ construct }) => construct), {
|
|
556
|
+
title: surface,
|
|
557
|
+
version: "1.0.0"
|
|
558
|
+
})
|
|
559
|
+
})));
|
|
560
|
+
const auths = await authClients(declared, sources, options.env);
|
|
561
|
+
const drivers = driversFor({
|
|
562
|
+
appRoot: options.root,
|
|
563
|
+
cache: cacheBackendsIn(declared, options.cacheBackend)
|
|
564
|
+
});
|
|
565
|
+
const owners = new Set(auths.map((auth) => auth.databaseId));
|
|
566
|
+
const databases = databaseSources(declared, sources).filter(({ id, kind }) => !owners.has(id) && kind !== "database-reader");
|
|
567
|
+
const factories = await factorySources(options.factories, databases.map(({ id }) => id));
|
|
568
|
+
const fakes = Object.entries(await readFakes(options.root, declared)).map(([id, { file }]) => ({
|
|
569
|
+
id,
|
|
570
|
+
file
|
|
571
|
+
}));
|
|
572
|
+
const services = serviceSources(declared, sources).filter(({ id }) => !owners.has(id));
|
|
573
|
+
const files = [.../* @__PURE__ */ new Set([
|
|
574
|
+
...Object.values(manifest.constructs).map(({ source }) => source.file),
|
|
575
|
+
...manifest.endpoints.map(({ source }) => source.file),
|
|
576
|
+
...(manifest.subscribers ?? []).map(({ source }) => source.file)
|
|
577
|
+
])];
|
|
578
|
+
const harnessFor = (dir) => harnessModule({
|
|
579
|
+
dir,
|
|
580
|
+
files,
|
|
581
|
+
databases,
|
|
582
|
+
surfaces: surfaces.map((id, index) => ({
|
|
583
|
+
id,
|
|
584
|
+
secured: clients[index].content.includes("authStrategies:")
|
|
585
|
+
})),
|
|
586
|
+
auths,
|
|
587
|
+
drivers,
|
|
588
|
+
factories,
|
|
589
|
+
fakes,
|
|
590
|
+
services,
|
|
591
|
+
mail: Object.values(declared).some(({ kind }) => kind === "email")
|
|
592
|
+
});
|
|
593
|
+
const json = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
594
|
+
return (await Promise.all([...new Set(options.targets)].map(async (target) => {
|
|
595
|
+
const dir = join(target, TEST_HARNESS_DIR);
|
|
596
|
+
await rm(dir, {
|
|
597
|
+
recursive: true,
|
|
598
|
+
force: true
|
|
599
|
+
});
|
|
600
|
+
await mkdir(join(dir, "clients"), { recursive: true });
|
|
601
|
+
await writeFile(join(dir, "manifest.json"), json);
|
|
602
|
+
await writeFile(join(dir, "index.ts"), harnessFor(dir));
|
|
603
|
+
for (const client of clients) await writeFile(join(dir, "clients", client.file), client.content);
|
|
604
|
+
return join(dir, "manifest.json");
|
|
605
|
+
})))[0];
|
|
606
|
+
}
|
|
607
|
+
/** Each auth server, with the client plugins its server plugins pair with. */
|
|
608
|
+
async function authClients(declared, sources, env) {
|
|
609
|
+
const clients = [];
|
|
610
|
+
const envParser = new EnvironmentParser({ ...env });
|
|
611
|
+
for (const [id, declaration] of Object.entries(declared)) {
|
|
612
|
+
const source = sources[id];
|
|
613
|
+
if (declaration.kind !== "rest-api" || !source) continue;
|
|
614
|
+
const exported = (await import(source.file))[source.exportName];
|
|
615
|
+
if (!isBetterAuth(exported) || exported.id !== id) continue;
|
|
616
|
+
const ids = await exported.pluginIds({
|
|
617
|
+
envParser,
|
|
618
|
+
context: serviceContext
|
|
619
|
+
});
|
|
620
|
+
clients.push({
|
|
621
|
+
id,
|
|
622
|
+
basePath: exported.basePath,
|
|
623
|
+
plugins: ids.flatMap((plugin) => CLIENT_PLUGINS[plugin] ?? []),
|
|
624
|
+
databaseId: exported.databaseId
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return clients;
|
|
628
|
+
}
|
|
629
|
+
function isBetterAuth(value) {
|
|
630
|
+
return typeof value === "object" && value !== null && "pluginIds" in value && "basePath" in value;
|
|
631
|
+
}
|
|
632
|
+
/** `Api` → `api`, `AdminApi` → `adminApi`: a construct id as a property name. */
|
|
633
|
+
function propertyOf(id) {
|
|
634
|
+
return id.charAt(0).toLowerCase() + id.slice(1);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Every database construct the app declares — a database, a schema tenant, a
|
|
638
|
+
* reader — keyed as a test is handed its transaction.
|
|
639
|
+
*/
|
|
640
|
+
function databaseSources(declared, sources) {
|
|
641
|
+
return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && declaration.kind.startsWith("database"))).map(([id, declaration]) => ({
|
|
642
|
+
id,
|
|
643
|
+
kind: declaration.kind,
|
|
644
|
+
service: serviceKey(id),
|
|
645
|
+
source: sources[id]
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
/** The kinds whose construct hands a handler a client — what `services` reaches. */
|
|
649
|
+
const SERVICE_KINDS = /* @__PURE__ */ new Set([
|
|
650
|
+
"cache",
|
|
651
|
+
"credential",
|
|
652
|
+
"database",
|
|
653
|
+
"database-schema",
|
|
654
|
+
"database-reader",
|
|
655
|
+
"email",
|
|
656
|
+
"encryption",
|
|
657
|
+
"external-api",
|
|
658
|
+
"file-server",
|
|
659
|
+
"objects",
|
|
660
|
+
"queue",
|
|
661
|
+
"topic"
|
|
662
|
+
]);
|
|
663
|
+
function serviceSources(declared, sources) {
|
|
664
|
+
return Object.entries(declared).filter(([id, declaration]) => Boolean(sources[id] && SERVICE_KINDS.has(declaration.kind))).map(([id, declaration]) => ({
|
|
665
|
+
id,
|
|
666
|
+
kind: declaration.kind,
|
|
667
|
+
service: serviceKey(id),
|
|
668
|
+
source: sources[id]
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
/** An import specifier for `file`, from the harness written into `dir`. */
|
|
672
|
+
function specifierFrom(dir, file) {
|
|
673
|
+
const path = relative(dir, file).replace(/\.tsx?$/, ".js");
|
|
674
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
675
|
+
}
|
|
676
|
+
function harnessModule(options) {
|
|
677
|
+
const { surfaces, auths, drivers, databases, services, dir, files, factories, fakes, mail } = options;
|
|
678
|
+
const magicLinks = mail ? auths.filter(({ plugins }) => plugins.includes("magicLinkClient")) : [];
|
|
679
|
+
const signIn = magicLinks.length === 1 ? magicLinks[0] : void 0;
|
|
680
|
+
const plugins = [...new Set(auths.flatMap(({ plugins }) => plugins))].sort();
|
|
681
|
+
const imports = [
|
|
682
|
+
`import {${services.length ? " type ClientOf," : ""}${databases.length ? " type DatabaseOf," : ""} featureTest, loadTestManifest${signIn ? ", signInWithMagicLink" : ""} } from '@geekmidas/constructs/testing';`,
|
|
683
|
+
...[...new Map([...databases, ...services].map((construct) => [construct.id, construct])).values()].map(({ id, source }) => `import type { ${source.exportName} as __${id} } from '${specifierFrom(dir, source.file)}';`),
|
|
684
|
+
`import { Browser as TestBrowser } from '@geekmidas/testkit/browser';`,
|
|
685
|
+
...surfaces.map(({ id }) => `import { createApi as create${id} } from './clients/${kebabCase(id)}.js';`),
|
|
686
|
+
...auths.length ? [`import { createAuthClient } from 'better-auth/client';`] : [],
|
|
687
|
+
...plugins.length ? [`import { ${plugins.join(", ")} } from 'better-auth/client/plugins';`] : [],
|
|
688
|
+
...drivers.imports ? [drivers.imports] : [],
|
|
689
|
+
...factories.map(({ file, service }) => `import { createFactory as __${service}Factory } from '${specifierFrom(dir, file)}';`),
|
|
690
|
+
...fakes.map(({ id, file }) => `import __${id}Fake from '${specifierFrom(dir, file)}';`),
|
|
691
|
+
...files.map((file, index) => `import * as __module${index} from '${specifierFrom(dir, file)}';`)
|
|
692
|
+
];
|
|
693
|
+
const url = (id) => `manifest.env[${JSON.stringify(provideKey(id, "url"))}]!`;
|
|
694
|
+
const members = [
|
|
695
|
+
...surfaces.map(({ id, secured }) => ` /** \`${id}\`, at ${provideKey(id, "url")}. */
|
|
696
|
+
readonly ${propertyOf(id)} = create${id}({
|
|
697
|
+
baseURL: ${url(id)},
|
|
698
|
+
fetch: this.fetch,${secured ? `
|
|
699
|
+
// Served in-process: no authorizer runs, so nothing is signed. A scheme
|
|
700
|
+
// with no strategy sends its request without credentials.
|
|
701
|
+
authStrategies: {} as never,` : ""}
|
|
702
|
+
});`),
|
|
703
|
+
...auths.map((auth) => ` /** \`${auth.id}\`, at ${provideKey(auth.id, "url")}. */
|
|
704
|
+
readonly ${propertyOf(auth.id)} = createAuthClient({
|
|
705
|
+
baseURL: \`\${${url(auth.id)}}${auth.basePath}\`,
|
|
706
|
+
plugins: [${auth.plugins.map((plugin) => `${plugin}()`).join(", ")}],
|
|
707
|
+
fetchOptions: { customFetchImpl: this.fetch },
|
|
708
|
+
});`),
|
|
709
|
+
...signIn ? [` /**
|
|
710
|
+
* Sign in as \`email\` through \`${signIn.id}\`'s magic link, the way a person
|
|
711
|
+
* does — the email opened, the link followed. The session it then reports.
|
|
712
|
+
* Without an address, as somebody new: \`user.email\` says who.
|
|
713
|
+
*/
|
|
714
|
+
signIn(email?: string) {
|
|
715
|
+
return signInWithMagicLink(this, this.${propertyOf(signIn.id)}, email);
|
|
716
|
+
}`] : []
|
|
717
|
+
];
|
|
718
|
+
const factoriesType = factories.length ? `{ ${factories.map(({ service }) => `${service}: typeof __${service}Factory`).join("; ")} }` : "";
|
|
719
|
+
const factoriesOption = factories.length ? `, factories: { ${factories.map(({ service }) => `${service}: __${service}Factory`).join(", ")} }` : "";
|
|
720
|
+
const fakesOption = fakes.length ? `, fakes: { ${fakes.map(({ id }) => `${id}: __${id}Fake`).join(", ")} }` : "";
|
|
721
|
+
const databasesType = `{ ${databases.map(({ id, service }) => `${service}: DatabaseOf<typeof __${id}>`).join("; ")} }`;
|
|
722
|
+
const servicesType = services.length ? `{ ${services.map(({ id, service }) => `${service}: ClientOf<typeof __${id}>`).join("; ")} }` : "";
|
|
723
|
+
const generics = databases.length || factoriesType || servicesType ? `<Browser, ${databases.length ? databasesType : "{}"}${factoriesType || servicesType ? `, ${factoriesType || "{}"}` : ""}${servicesType ? `, ${servicesType}` : ""}>` : "";
|
|
724
|
+
return `// Generated by \`gkm test\` from the app's constructs — do not edit.
|
|
725
|
+
${imports.join("\n")}
|
|
726
|
+
|
|
727
|
+
${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));
|
|
728
|
+
|
|
729
|
+
/** A browser with a client for every surface the app declares. */
|
|
730
|
+
export class Browser extends TestBrowser {
|
|
731
|
+
${members.join("\n\n")}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** The app's modules, by the path the manifest records for each. */
|
|
735
|
+
const modules = {
|
|
736
|
+
${files.map((file, index) => `\t${JSON.stringify(file)}: __module${index},`).join("\n")}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
/** \`it\`, for a test that drives the app the way it runs deployed. */
|
|
740
|
+
export const it = featureTest${generics}({ manifest, modules, browser: Browser${factoriesOption}${fakesOption} });
|
|
741
|
+
`;
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/test/ready.ts
|
|
745
|
+
/**
|
|
746
|
+
* The hand-off between `gkm test` and the Vitest global setup.
|
|
747
|
+
*
|
|
748
|
+
* `gkm test` reconciles and migrates the test stage before Vitest starts; the
|
|
749
|
+
* setup, run inside Vitest, only has to load what that left. Started by plain
|
|
750
|
+
* `vitest` instead, the setup runs `gkm test --setup` itself first — so there is
|
|
751
|
+
* one way the stage gets ready, whichever started the suite.
|
|
752
|
+
*/
|
|
753
|
+
/** Set on the Vitest process by `gkm test`: the stage is ready, here. */
|
|
754
|
+
const TEST_READY_ENV = "GKM_TEST_READY";
|
|
755
|
+
/** Where `gkm test` records what it prepared, from where it ran. */
|
|
756
|
+
const TEST_READY_FILE = ".gkm/test-ready.json";
|
|
757
|
+
//#endregion
|
|
758
|
+
export { writeTestHarness as a, driversFor as c, TEST_MANIFEST_ENV as i, TEST_READY_FILE as n, SubscriberGenerator as o, DEFAULT_FACTORIES_DIR as r, cacheBackendsIn as s, TEST_READY_ENV as t };
|
|
759
|
+
|
|
760
|
+
//# sourceMappingURL=ready-DQIy21W5.mjs.map
|