create-svc 0.1.10 → 0.1.12
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 +51 -47
- package/index.ts +2 -2
- package/package.json +10 -9
- package/src/cli.test.ts +28 -10
- package/src/cli.ts +196 -33
- package/src/git-bootstrap.test.ts +40 -0
- package/src/git-bootstrap.ts +110 -0
- package/src/naming.test.ts +1 -0
- package/src/naming.ts +23 -0
- package/src/post-scaffold.test.ts +19 -0
- package/src/post-scaffold.ts +17 -4
- package/src/profiles.ts +2 -5
- package/src/scaffold.test.ts +232 -41
- package/src/scaffold.ts +81 -36
- package/src/service.test.ts +30 -0
- package/src/service.ts +65 -0
- package/src/vault.test.ts +61 -1
- package/src/vault.ts +77 -15
- package/templates/shared/.github/workflows/ci.yml +2 -1
- package/templates/shared/.github/workflows/deploy.yml +2 -0
- package/templates/shared/README.md +124 -47
- package/templates/shared/grafana/alerts.yaml +54 -0
- package/templates/shared/grafana/waitlist-dashboard.json +63 -0
- package/templates/shared/scripts/authctl.ts +231 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
- package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
- package/templates/shared/scripts/cloudrun/cli.ts +329 -7
- package/templates/shared/scripts/cloudrun/config.ts +11 -4
- package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
- package/templates/shared/scripts/cloudrun/lib.ts +174 -41
- package/templates/shared/scripts/cloudrun/neon.ts +45 -0
- package/templates/shared/scripts/dev.ts +22 -0
- package/templates/shared/scripts/ensure-local-db.ts +3 -0
- package/templates/shared/scripts/local-docker.ts +63 -0
- package/templates/shared/scripts/local-env.ts +27 -0
- package/templates/shared/scripts/seed.ts +73 -0
- package/templates/shared/scripts/wait-for-db.ts +32 -0
- package/templates/shared/service.config.ts +59 -0
- package/templates/shared/service.yaml +24 -44
- package/templates/targets/workers/.github/workflows/ci.yml +19 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
- package/templates/targets/workers/Makefile +33 -0
- package/templates/targets/workers/README.md +75 -0
- package/templates/targets/workers/package.json +35 -0
- package/templates/targets/workers/scripts/workers/cli.ts +402 -0
- package/templates/targets/workers/src/auth.ts +178 -0
- package/templates/targets/workers/src/index.ts +198 -0
- package/templates/targets/workers/src/storage.ts +370 -0
- package/templates/targets/workers/test/app.test.ts +108 -0
- package/templates/targets/workers/tsconfig.json +11 -0
- package/templates/targets/workers/wrangler.toml +24 -0
- package/templates/variants/bun-connectrpc/Makefile +14 -8
- package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-connectrpc/package.json +12 -5
- package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
- package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
- package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
- package/templates/variants/bun-connectrpc/src/index.ts +76 -176
- package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
- package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
- package/templates/variants/bun-hono/Makefile +14 -8
- package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-hono/package.json +12 -5
- package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
- package/templates/variants/bun-hono/src/auth.ts +181 -0
- package/templates/variants/bun-hono/src/db/repository.ts +68 -421
- package/templates/variants/bun-hono/src/db/schema.ts +15 -64
- package/templates/variants/bun-hono/src/index.ts +65 -180
- package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
- package/templates/variants/bun-hono/test/app.test.ts +72 -41
- package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
- package/templates/variants/go-chi/Makefile +27 -11
- package/templates/variants/go-chi/atlas.hcl +8 -0
- package/templates/variants/go-chi/cmd/server/main.go +21 -10
- package/templates/variants/go-chi/go.mod +1 -3
- package/templates/variants/go-chi/internal/app/service.go +202 -685
- package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
- package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-chi/internal/config/config.go +27 -11
- package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
- package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
- package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
- package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
- package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
- package/templates/variants/go-chi/migrations/atlas.sum +2 -0
- package/templates/variants/go-chi/package.json +7 -1
- package/templates/variants/go-connectrpc/Makefile +26 -9
- package/templates/variants/go-connectrpc/atlas.hcl +8 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
- package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
- package/templates/variants/go-connectrpc/go.mod +1 -1
- package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
- package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
- package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
- package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
- package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
- package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
- package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
- package/templates/variants/go-connectrpc/package.json +7 -1
- package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
- package/templates/root/.github/workflows/buf-publish.yml +0 -19
- package/templates/root/.github/workflows/ci.yml +0 -26
- package/templates/root/.github/workflows/deploy.yml +0 -22
- package/templates/root/Dockerfile +0 -23
- package/templates/root/README.md +0 -69
- package/templates/root/buf.gen.yaml +0 -10
- package/templates/root/buf.yaml +0 -9
- package/templates/root/cmd/server/main.go +0 -44
- package/templates/root/gen/dns/v1/dns.pb.go +0 -623
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/root/go.mod +0 -10
- package/templates/root/internal/app/service.go +0 -152
- package/templates/root/internal/app/token_source.go +0 -50
- package/templates/root/internal/cloudflare/client.go +0 -160
- package/templates/root/internal/config/config.go +0 -55
- package/templates/root/internal/connectapi/handler.go +0 -79
- package/templates/root/internal/httpapi/routes.go +0 -93
- package/templates/root/internal/vault/client.go +0 -148
- package/templates/root/package.json +0 -12
- package/templates/root/protos/dns/v1/dns.proto +0 -58
- package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
- package/templates/root/scripts/cloudrun/config.ts +0 -50
- package/templates/root/scripts/cloudrun/deploy.ts +0 -41
- package/templates/root/scripts/cloudrun/lib.ts +0 -244
- package/templates/root/service.yaml +0 -50
- package/templates/root/test/go.test.ts +0 -19
- package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
- package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
- package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
- package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
- package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
- package/templates/variants/bun-hono/src/chat/service.ts +0 -384
- package/templates/variants/bun-hono/src/chat/types.ts +0 -142
- package/templates/variants/bun-hono/src/storage.ts +0 -72
- package/templates/variants/bun-hono/src/webhooks.ts +0 -35
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
- package/templates/variants/go-chi/buf.gen.yaml +0 -12
- package/templates/variants/go-chi/buf.yaml +0 -9
- package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
- /package/bin/{create-svc.mjs → service.mjs} +0 -0
package/src/scaffold.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ function baseConfig(overrides: Partial<ScaffoldConfig> = {}): ScaffoldConfig {
|
|
|
10
10
|
directory: "svc",
|
|
11
11
|
serviceName: "dns-api",
|
|
12
12
|
modulePath: "example.com/dns-api",
|
|
13
|
+
target: "cloudrun",
|
|
13
14
|
runtime: "bun",
|
|
14
15
|
framework: "hono",
|
|
15
16
|
region: "us-west1",
|
|
@@ -19,6 +20,11 @@ function baseConfig(overrides: Partial<ScaffoldConfig> = {}): ScaffoldConfig {
|
|
|
19
20
|
billingAccount: "billingAccounts/01BD2E-3A6949-8F4C84",
|
|
20
21
|
quotaProjectId: "anmho-infra-prod",
|
|
21
22
|
profile: "microservice",
|
|
23
|
+
git: {
|
|
24
|
+
enabled: false,
|
|
25
|
+
owner: "anmho",
|
|
26
|
+
repository: "dns-api",
|
|
27
|
+
},
|
|
22
28
|
neonDatabaseName: "dns_api",
|
|
23
29
|
apiHostname: "api.dns-api.anmho.com",
|
|
24
30
|
generatorRoot: join(import.meta.dir, ".."),
|
|
@@ -49,6 +55,14 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
49
55
|
);
|
|
50
56
|
|
|
51
57
|
const configScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "config.ts")).text();
|
|
58
|
+
const serviceConfig = await Bun.file(join(generatedRoot, "service.config.ts")).text();
|
|
59
|
+
expect(serviceConfig).toContain('service_id: "dns-api"');
|
|
60
|
+
expect(serviceConfig).toContain('target: "cloudrun"');
|
|
61
|
+
expect(serviceConfig).toContain('module: "buf.build/anmho/dns-api"');
|
|
62
|
+
expect(serviceConfig).toContain('issuer: "https://auth.anmho.com/api/auth"');
|
|
63
|
+
expect(serviceConfig).toContain('audience: "api://dns-api"');
|
|
64
|
+
expect(serviceConfig).toContain('vault_path_prefix: "prod/apps/dns-api/server/oauth-clients"');
|
|
65
|
+
expect(serviceConfig).toContain('api_key_secret_name: "dns-api-temporal-api-key"');
|
|
52
66
|
expect(configScript).toContain('profile: "microservice"');
|
|
53
67
|
expect(configScript).toContain('domain: "waitlist"');
|
|
54
68
|
expect(configScript).toContain('kind: "microservice"');
|
|
@@ -56,30 +70,57 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
56
70
|
expect(configScript).toContain(`framework: "${variant.framework}"`);
|
|
57
71
|
expect(configScript).toContain('mode: "create_new"');
|
|
58
72
|
expect(configScript).toContain('quotaProjectId: "anmho-infra-prod"');
|
|
73
|
+
expect(configScript).toContain('issuer: "https://auth.anmho.com/api/auth"');
|
|
74
|
+
expect(configScript).toContain('audience: "api://dns-api"');
|
|
75
|
+
expect(configScript).toContain('jwksUrl: "https://auth.anmho.com/api/auth/jwks"');
|
|
76
|
+
expect(configScript).toContain('apiKeySecretName: "dns-api-temporal-api-key"');
|
|
59
77
|
expect(configScript).toContain('projectId: ""');
|
|
60
78
|
expect(configScript).toContain('baseBranchId: ""');
|
|
61
79
|
expect(configScript).toContain('baseBranchName: "main"');
|
|
62
80
|
expect(configScript).toContain('previewBranchPrefix: "dns-api-pr"');
|
|
63
81
|
expect(configScript).toContain('hostname: "api.dns-api.anmho.com"');
|
|
64
|
-
expect(configScript).toContain('attachmentBucket: "anmho-dns-api-dns-api-attachments"');
|
|
65
|
-
expect(configScript).toContain('attachmentPublicBaseUrl: "https://storage.googleapis.com/anmho-dns-api-dns-api-attachments"');
|
|
66
82
|
expect(configScript).not.toContain("github:");
|
|
83
|
+
expect(configScript).not.toContain("attachmentBucket");
|
|
67
84
|
|
|
68
85
|
const deployScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
|
|
69
86
|
expect(deployScript).toContain('--billing-project", config.project.quotaProjectId');
|
|
87
|
+
expect(deployScript).toContain('config.project.mode === "use_existing"');
|
|
70
88
|
expect(deployScript).toContain("serviceDomain");
|
|
71
89
|
expect(deployScript).toContain("ensureProductionDomainMapping");
|
|
72
|
-
expect(deployScript).toContain("
|
|
90
|
+
expect(deployScript).toContain('"domain-mappings",');
|
|
91
|
+
expect(deployScript).toContain('"--region",');
|
|
92
|
+
expect(deployScript).toContain("assertProductionDomainAvailable");
|
|
93
|
+
expect(deployScript).toContain("assertServiceNameAvailable");
|
|
94
|
+
expect(deployScript).not.toContain("ensureStorageBucket");
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "integrations.ts")).exists()).toBeFalse();
|
|
97
|
+
const destroyScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cleanup.ts")).text();
|
|
98
|
+
expect(destroyScript).toContain("assertOwnedResource");
|
|
99
|
+
expect(destroyScript).toContain("assertProductionDomainMappingOwned");
|
|
100
|
+
expect(destroyScript).toContain("deleteGrafanaResources");
|
|
101
|
+
expect(destroyScript).toContain('gcx", ["resources", "delete"');
|
|
102
|
+
expect(destroyScript).toContain("config.temporal.apiKeySecretName");
|
|
103
|
+
const neonScript = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "neon.ts")).text();
|
|
104
|
+
expect(neonScript).toContain("assertDatabaseOwned");
|
|
105
|
+
expect(neonScript).toContain("assertDisposableBranchName");
|
|
106
|
+
const seedScript = await Bun.file(join(generatedRoot, "scripts", "seed.ts")).text();
|
|
107
|
+
expect(seedScript).toContain("SEED_PROD=true");
|
|
108
|
+
expect(seedScript).toContain("waitlist_entries");
|
|
76
109
|
|
|
77
110
|
const manifest = await Bun.file(join(generatedRoot, "service.yaml")).text();
|
|
78
|
-
expect(manifest).toContain("
|
|
79
|
-
expect(manifest).toContain("
|
|
80
|
-
expect(manifest).toContain("
|
|
81
|
-
expect(manifest).toContain("
|
|
82
|
-
expect(manifest).toContain("
|
|
111
|
+
expect(manifest).toContain("DATABASE_URL");
|
|
112
|
+
expect(manifest).toContain("TEMPORAL_ENABLED");
|
|
113
|
+
expect(manifest).toContain("TEMPORAL_TASK_QUEUE");
|
|
114
|
+
expect(manifest).toContain("TEMPORAL_API_KEY_ENV");
|
|
115
|
+
expect(manifest).toContain("AUTH_ENABLED");
|
|
116
|
+
expect(manifest).toContain("${AUTH_AUDIENCE}");
|
|
117
|
+
expect(manifest).toContain("managed_by: create-service");
|
|
118
|
+
expect(manifest).toContain("service_id: ${SERVICE_ID}");
|
|
119
|
+
expect(manifest).not.toContain("CLERK_SECRET_KEY");
|
|
120
|
+
expect(manifest).not.toContain("STRIPE_SECRET_KEY");
|
|
121
|
+
expect(manifest).not.toContain("REVENUECAT_API_KEY");
|
|
122
|
+
expect(manifest).not.toContain("RESEND_API_KEY");
|
|
123
|
+
expect(manifest).not.toContain("POSTHOG_API_KEY");
|
|
83
124
|
|
|
84
125
|
const gitignore = await Bun.file(join(generatedRoot, ".gitignore")).text();
|
|
85
126
|
expect(gitignore).toContain("node_modules");
|
|
@@ -90,59 +131,138 @@ test("scaffolds all runtime/framework variants with shared cloudrun config", asy
|
|
|
90
131
|
expect(dockerCompose).toContain(`127.0.0.1:${localPort}:5432`);
|
|
91
132
|
|
|
92
133
|
const envExample = await Bun.file(join(generatedRoot, ".env.example")).text();
|
|
93
|
-
expect(envExample).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api`);
|
|
94
|
-
expect(envExample).toContain("
|
|
95
|
-
expect(envExample).toContain("
|
|
96
|
-
expect(envExample).toContain("
|
|
97
|
-
expect(envExample).toContain("
|
|
98
|
-
expect(envExample).toContain("
|
|
99
|
-
expect(envExample).toContain("
|
|
134
|
+
expect(envExample).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api?sslmode=disable`);
|
|
135
|
+
expect(envExample).toContain("AUTH_ENABLED=false");
|
|
136
|
+
expect(envExample).toContain("AUTH_AUDIENCE=api://dns-api");
|
|
137
|
+
expect(envExample).toContain("CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID=");
|
|
138
|
+
expect(envExample).toContain("TEMPORAL_API_KEY=");
|
|
139
|
+
expect(envExample).toContain("The base waitlist service does not require");
|
|
140
|
+
expect(envExample).not.toContain("ATTACHMENT_BUCKET=");
|
|
141
|
+
expect(envExample).not.toContain("CLERK_SECRET_KEY=");
|
|
142
|
+
expect(envExample).not.toContain("STRIPE_SECRET_KEY=");
|
|
143
|
+
expect(envExample).not.toContain("REVENUECAT_API_KEY=");
|
|
144
|
+
expect(envExample).not.toContain("RESEND_API_KEY=");
|
|
145
|
+
expect(envExample).not.toContain("POSTHOG_API_KEY=");
|
|
100
146
|
|
|
101
147
|
const localEnv = await Bun.file(join(generatedRoot, ".env.local")).text();
|
|
102
|
-
expect(localEnv).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api`);
|
|
103
|
-
expect(localEnv).toContain("ATTACHMENT_PUBLIC_BASE_URL=
|
|
148
|
+
expect(localEnv).toContain(`DATABASE_URL=postgres://postgres:postgres@127.0.0.1:${localPort}/dns_api?sslmode=disable`);
|
|
149
|
+
expect(localEnv).not.toContain("ATTACHMENT_PUBLIC_BASE_URL=");
|
|
104
150
|
|
|
105
|
-
|
|
151
|
+
const ciWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).text();
|
|
152
|
+
expect(ciWorkflow).toContain("bun run dashboards");
|
|
153
|
+
expect(ciWorkflow).toContain("GCX_ENABLED");
|
|
154
|
+
expect(await Bun.file(join(generatedRoot, "grafana", "waitlist-dashboard.json")).exists()).toBeTrue();
|
|
155
|
+
expect(await Bun.file(join(generatedRoot, "grafana", "alerts.yaml")).exists()).toBeTrue();
|
|
106
156
|
|
|
107
157
|
if (variant.runtime === "go") {
|
|
108
158
|
const goMod = await Bun.file(join(generatedRoot, "go.mod")).text();
|
|
109
|
-
expect(goMod).toContain("connectrpc.com/connect");
|
|
110
159
|
expect(goMod).toContain("module example.com/dns-api");
|
|
111
160
|
expect(goMod).not.toContain("module github.com/anmho/dns-api");
|
|
112
161
|
|
|
113
162
|
const mainGo = await Bun.file(join(generatedRoot, "cmd", "server", "main.go")).text();
|
|
114
|
-
expect(mainGo).toContain("NewChatService");
|
|
115
163
|
expect(mainGo).toContain("example.com/dns-api");
|
|
164
|
+
if (variant.framework === "connectrpc") {
|
|
165
|
+
expect(goMod).toContain("connectrpc.com/connect");
|
|
166
|
+
expect(mainGo).toContain("NewWaitlistService");
|
|
167
|
+
expect(mainGo).toContain("WaitlistServiceName");
|
|
168
|
+
} else {
|
|
169
|
+
expect(goMod).not.toContain("connectrpc.com/connect");
|
|
170
|
+
expect(mainGo).toContain("NewWaitlistService");
|
|
171
|
+
const routes = await Bun.file(join(generatedRoot, "internal", "httpapi", "routes.go")).text();
|
|
172
|
+
expect(routes).toContain("/v1/waitlist");
|
|
173
|
+
expect(routes).toContain("/v1/admin/waitlist");
|
|
174
|
+
expect(routes).toContain("/v1/triggers/waitlist");
|
|
175
|
+
expect(await Bun.file(join(generatedRoot, "buf.yaml")).exists()).toBeFalse();
|
|
176
|
+
}
|
|
177
|
+
expect(mainGo).toContain("internal/auth");
|
|
178
|
+
expect(mainGo).toContain("cfg.AuthAudience");
|
|
179
|
+
expect(mainGo).toContain("cfg.TemporalAPIKey");
|
|
180
|
+
expect(await Bun.file(join(generatedRoot, "internal", "auth", "middleware.go")).exists()).toBeTrue();
|
|
181
|
+
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
182
|
+
expect(makefile).toContain("$(ATLAS) migrate apply --env local");
|
|
183
|
+
expect(makefile).toContain("$(ATLAS) migrate lint --env local --latest 1");
|
|
184
|
+
expect(makefile).toContain("bun run ./scripts/ensure-local-db.ts");
|
|
185
|
+
expect(makefile).toContain("bun run ./scripts/wait-for-db.ts");
|
|
186
|
+
expect(makefile).toContain("bun run ./scripts/dev.ts go run ./cmd/server");
|
|
187
|
+
expect(await Bun.file(join(generatedRoot, "atlas.hcl")).exists()).toBeTrue();
|
|
188
|
+
expect(await Bun.file(join(generatedRoot, "migrations", "atlas.sum")).exists()).toBeTrue();
|
|
189
|
+
expect(await Bun.file(join(generatedRoot, "cmd", "migrate", "main.go")).exists()).toBeFalse();
|
|
190
|
+
expect(await Bun.file(join(generatedRoot, "internal", "temporal", "worker.go")).exists()).toBeTrue();
|
|
191
|
+
expect(goMod).toContain("go.temporal.io/sdk");
|
|
116
192
|
} else {
|
|
117
193
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
118
|
-
expect(packageJson).toContain('"
|
|
119
|
-
expect(packageJson).toContain(
|
|
194
|
+
expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
|
|
195
|
+
expect(packageJson).toContain("@temporalio/worker");
|
|
196
|
+
expect(packageJson).toContain('"service": "./scripts/cloudrun/cli.ts"');
|
|
197
|
+
expect(packageJson).toContain('"dev": "bun run ./scripts/dev.ts bun run ./src/index.ts"');
|
|
120
198
|
expect(packageJson).toContain('"gen": "bun run ./scripts/codegen.ts"');
|
|
121
|
-
expect(packageJson).toContain('"
|
|
199
|
+
expect(packageJson).toContain('"create": "bun run ./scripts/cloudrun/cli.ts create"');
|
|
122
200
|
expect(packageJson).toContain('"deploy": "bun run ./scripts/cloudrun/cli.ts deploy"');
|
|
123
|
-
expect(packageJson).toContain('"
|
|
201
|
+
expect(packageJson).toContain('"dashboards": "bun run ./scripts/cloudrun/cli.ts dashboards"');
|
|
202
|
+
expect(packageJson).toContain('"auth": "bun run ./scripts/cloudrun/cli.ts auth"');
|
|
203
|
+
expect(packageJson).toContain('"destroy": "bun run ./scripts/cloudrun/cli.ts destroy"');
|
|
204
|
+
const serviceCli = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).text();
|
|
205
|
+
expect(serviceCli).toContain("service <create|deploy|migrate|seed|dashboards|dns|doctor|destroy|auth|sdk>");
|
|
206
|
+
expect(serviceCli).toContain("assertServiceNameAvailable(config.serviceName)");
|
|
207
|
+
expect(serviceCli).toContain("ensureAuthResourceServer");
|
|
208
|
+
expect(serviceCli).toContain('["resources", "push", "--path", "./grafana"]');
|
|
209
|
+
const cloudrunLib = await Bun.file(join(generatedRoot, "scripts", "cloudrun", "lib.ts")).text();
|
|
210
|
+
expect(cloudrunLib).toContain("resolveTemporalRuntimeConfig");
|
|
211
|
+
expect(cloudrunLib).toContain("TEMPORAL_API_KEY_ENV");
|
|
212
|
+
expect(cloudrunLib).toContain("value === undefined");
|
|
213
|
+
|
|
214
|
+
const authctlScript = await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).text();
|
|
215
|
+
expect(authctlScript).toContain("authctl");
|
|
216
|
+
expect(authctlScript).toContain("resource-servers");
|
|
217
|
+
expect(authctlScript).toContain("clients");
|
|
218
|
+
expect(authctlScript).toContain("defaultClientTargetArgs");
|
|
219
|
+
expect(authctlScript).toContain('existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl")');
|
|
220
|
+
expect(authctlScript).not.toContain('defaultAuthResourceServerArgs(), "--yes", "--json"');
|
|
221
|
+
const authScript = await Bun.file(join(generatedRoot, "src", "auth.ts")).text();
|
|
222
|
+
expect(authScript).toContain('"Ed25519"');
|
|
124
223
|
|
|
125
224
|
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
126
|
-
expect(makefile).toContain("npx --no-install
|
|
225
|
+
expect(makefile).toContain("npx --no-install service");
|
|
226
|
+
expect(makefile).toContain("dashboards:");
|
|
227
|
+
expect(makefile).toContain("auth:");
|
|
228
|
+
expect(makefile).toContain("bun run dev");
|
|
229
|
+
const devScript = await Bun.file(join(generatedRoot, "scripts", "dev.ts")).text();
|
|
230
|
+
expect(devScript).toContain("ensureLocalPostgres");
|
|
231
|
+
const localDocker = await Bun.file(join(generatedRoot, "scripts", "local-docker.ts")).text();
|
|
232
|
+
expect(localDocker).toContain('["docker", "info"]');
|
|
233
|
+
expect(localDocker).toContain('["open", "-a", "Docker"]');
|
|
234
|
+
expect(localDocker).toContain('["docker", "compose", "up", "-d"]');
|
|
127
235
|
|
|
128
236
|
const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
|
|
237
|
+
expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
|
|
238
|
+
expect(entrypoint).toContain(variant.framework === "hono" ? 'app.use("/v1/*", authMiddleware())' : "withServiceAuth");
|
|
239
|
+
expect(await Bun.file(join(generatedRoot, "src", "temporal", "worker.ts")).exists()).toBeTrue();
|
|
129
240
|
const readme = await Bun.file(join(generatedRoot, "README.md")).text();
|
|
130
241
|
if (variant.framework === "connectrpc") {
|
|
131
|
-
expect(entrypoint).toContain("
|
|
132
|
-
expect(gitignore).toContain("gen/");
|
|
242
|
+
expect(entrypoint).toContain("WaitlistService");
|
|
243
|
+
expect(gitignore).not.toContain("gen/");
|
|
133
244
|
expect(readme).toContain("Local introspection");
|
|
134
245
|
} else {
|
|
135
|
-
expect(entrypoint).toContain("/v1/
|
|
246
|
+
expect(entrypoint).toContain("/v1/waitlist");
|
|
247
|
+
expect(entrypoint).toContain("/v1/admin/waitlist");
|
|
248
|
+
expect(entrypoint).toContain("/v1/triggers/waitlist");
|
|
136
249
|
expect(gitignore).not.toContain("gen/");
|
|
137
250
|
expect(readme).not.toContain("Local introspection");
|
|
138
251
|
}
|
|
139
252
|
expect(entrypoint).toContain(variant.framework === "hono" ? "Hono" : "connectNodeAdapter");
|
|
140
|
-
expect(readme).toContain("ATTACHMENT_BUCKET");
|
|
141
253
|
expect(readme).toContain("/webhooks/:provider");
|
|
142
254
|
expect(readme).toContain("microservice profile");
|
|
143
255
|
expect(readme).toContain("waitlist/launch service");
|
|
256
|
+
expect(readme).toContain("resource=api://<resource_server_id>");
|
|
144
257
|
expect(readme).toContain("Terraform is optional");
|
|
258
|
+
expect(readme).toContain("AUTH_ENABLED=true");
|
|
259
|
+
expect(readme).toContain("verifies JWT bearer tokens");
|
|
260
|
+
expect(readme).toContain("prod/apps/auth/authctl/cloudflare-access");
|
|
261
|
+
expect(readme).toContain("service auth resource-server");
|
|
145
262
|
}
|
|
263
|
+
|
|
264
|
+
const deployWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "deploy.yml")).text();
|
|
265
|
+
expect(deployWorkflow).toContain("bun run dashboards");
|
|
146
266
|
}
|
|
147
267
|
}, 30000);
|
|
148
268
|
|
|
@@ -161,30 +281,101 @@ test("scaffolds a backend package cleanly into a nested monorepo-style directory
|
|
|
161
281
|
const readme = await Bun.file(join(generatedRoot, "README.md")).text();
|
|
162
282
|
expect(readme).toContain("`microservice` profile");
|
|
163
283
|
expect(readme).toContain("api.dns-api.anmho.com");
|
|
164
|
-
expect(readme).toContain("
|
|
284
|
+
expect(readme).toContain("open Docker Desktop if needed");
|
|
165
285
|
expect(readme).toContain("local Postgres service in `docker-compose.yml`");
|
|
166
286
|
expect(readme).toContain("gcloud auth login");
|
|
167
287
|
expect(readme).toContain("known-good CLIs");
|
|
168
|
-
expect(readme).toContain("
|
|
169
|
-
expect(readme).toContain("
|
|
170
|
-
expect(readme).toContain("
|
|
171
|
-
expect(readme).toContain("one-command production bootstrap");
|
|
288
|
+
expect(readme).toContain("service create");
|
|
289
|
+
expect(readme).toContain("service deploy");
|
|
290
|
+
expect(readme).toContain("one-command production create");
|
|
172
291
|
expect(readme).toContain("waitlist/launch service");
|
|
173
292
|
expect(readme).toContain("Terraform is optional");
|
|
174
|
-
expect(readme).toContain("
|
|
293
|
+
expect(readme).toContain("waitlist/launch service");
|
|
175
294
|
expect(readme).not.toContain("Neon main, preview, and personal branch provisioning");
|
|
176
|
-
|
|
295
|
+
const ciWorkflow = await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).text();
|
|
296
|
+
expect(ciWorkflow).toContain("bun run dashboards");
|
|
177
297
|
expect(readme).not.toContain("repository");
|
|
178
298
|
|
|
179
299
|
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
180
300
|
expect(packageJson).toContain('"hono"');
|
|
181
301
|
|
|
182
302
|
const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
|
|
183
|
-
expect(entrypoint).toContain("/v1/
|
|
184
|
-
|
|
185
|
-
expect(await Bun.file(join(generatedRoot, ".github", "workflows", "ci.yml")).exists()).toBeFalse();
|
|
303
|
+
expect(entrypoint).toContain("/v1/waitlist");
|
|
304
|
+
expect(entrypoint).toContain("/v1/admin/waitlist");
|
|
186
305
|
}, 15000);
|
|
187
306
|
|
|
307
|
+
test("scaffolds the workers target with wrangler lifecycle commands", async () => {
|
|
308
|
+
const root = await mkdtemp(join(tmpdir(), "create-svc-workers-"));
|
|
309
|
+
const generatedRoot = join(root, "edge-api");
|
|
310
|
+
|
|
311
|
+
await scaffoldProject(
|
|
312
|
+
baseConfig({
|
|
313
|
+
directory: generatedRoot,
|
|
314
|
+
target: "workers",
|
|
315
|
+
runtime: "bun",
|
|
316
|
+
framework: "hono",
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const packageJson = await Bun.file(join(generatedRoot, "package.json")).text();
|
|
321
|
+
expect(packageJson).toContain('"@anmho/authctl": "0.1.1"');
|
|
322
|
+
expect(packageJson).toContain('"service": "./scripts/workers/cli.ts"');
|
|
323
|
+
expect(packageJson).toContain('"dev": "wrangler dev"');
|
|
324
|
+
expect(packageJson).toContain('"auth": "bun run ./scripts/workers/cli.ts auth"');
|
|
325
|
+
expect(packageJson).toContain('"wrangler"');
|
|
326
|
+
expect(packageJson).toContain('"pg"');
|
|
327
|
+
|
|
328
|
+
const wranglerConfig = await Bun.file(join(generatedRoot, "wrangler.toml")).text();
|
|
329
|
+
expect(wranglerConfig).toContain('name = "dns-api"');
|
|
330
|
+
expect(wranglerConfig).toContain('compatibility_flags = ["nodejs_compat"]');
|
|
331
|
+
expect(wranglerConfig).toContain('pattern = "api.dns-api.anmho.com/*"');
|
|
332
|
+
expect(wranglerConfig).toContain('binding = "HYPERDRIVE"');
|
|
333
|
+
expect(wranglerConfig).toContain('AUTH_ENABLED = "true"');
|
|
334
|
+
expect(wranglerConfig).toContain('AUTH_AUDIENCE = "api://dns-api"');
|
|
335
|
+
|
|
336
|
+
const entrypoint = await Bun.file(join(generatedRoot, "src", "index.ts")).text();
|
|
337
|
+
expect(entrypoint).toContain("/v1/waitlist");
|
|
338
|
+
expect(entrypoint).toContain("/v1/admin/waitlist");
|
|
339
|
+
expect(entrypoint).toContain('app.use("/v1/*", authMiddleware())');
|
|
340
|
+
expect(entrypoint).toContain("createStorage(context.env)");
|
|
341
|
+
expect(entrypoint).toContain("scheduled");
|
|
342
|
+
const readme = await Bun.file(join(generatedRoot, "README.md")).text();
|
|
343
|
+
expect(readme).toContain("Cloudflare Workers");
|
|
344
|
+
expect(readme).toContain("Hyperdrive binding for Neon-backed Postgres persistence");
|
|
345
|
+
expect(readme).not.toContain("Cloud Run");
|
|
346
|
+
const workerCli = await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).text();
|
|
347
|
+
expect(workerCli).toContain("hyperdrive");
|
|
348
|
+
expect(workerCli).toContain('["resources", "push", "--path", "./grafana"]');
|
|
349
|
+
expect(workerCli).toContain("ensureAuthResourceServer");
|
|
350
|
+
expect(workerCli).toContain("Workers database schema applied");
|
|
351
|
+
expect(workerCli).toContain("create table if not exists waitlist_entries");
|
|
352
|
+
expect(workerCli).toContain("DATABASE_URL or NEON_API_KEY is required to provision the Hyperdrive binding");
|
|
353
|
+
expect(workerCli).toContain("createProjectBranchDatabase");
|
|
354
|
+
expect(workerCli).toContain("deleteNeonDatabase");
|
|
355
|
+
expect(workerCli).toContain("deleteGrafanaResources");
|
|
356
|
+
expect(workerCli).toContain("hyperdrive\", \"delete");
|
|
357
|
+
const makefile = await Bun.file(join(generatedRoot, "Makefile")).text();
|
|
358
|
+
expect(makefile).toContain('no generated code for workers');
|
|
359
|
+
expect(makefile).toContain("auth:");
|
|
360
|
+
expect(makefile).not.toContain("scripts/codegen.ts");
|
|
361
|
+
|
|
362
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "authctl.ts")).exists()).toBeTrue();
|
|
363
|
+
expect(await Bun.file(join(generatedRoot, "src", "auth.ts")).exists()).toBeTrue();
|
|
364
|
+
expect(await Bun.file(join(generatedRoot, "src", "storage.ts")).exists()).toBeTrue();
|
|
365
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "workers", "cli.ts")).exists()).toBeTrue();
|
|
366
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "cloudrun", "cli.ts")).exists()).toBeFalse();
|
|
367
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "dev.ts")).exists()).toBeFalse();
|
|
368
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "ensure-local-db.ts")).exists()).toBeFalse();
|
|
369
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "local-docker.ts")).exists()).toBeFalse();
|
|
370
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "wait-for-db.ts")).exists()).toBeFalse();
|
|
371
|
+
expect(await Bun.file(join(generatedRoot, "service.yaml")).exists()).toBeFalse();
|
|
372
|
+
expect(await Bun.file(join(generatedRoot, "Dockerfile")).exists()).toBeFalse();
|
|
373
|
+
expect(await Bun.file(join(generatedRoot, "docker-compose.yml")).exists()).toBeFalse();
|
|
374
|
+
expect(await Bun.file(join(generatedRoot, "src", "db", "repository.ts")).exists()).toBeFalse();
|
|
375
|
+
expect(await Bun.file(join(generatedRoot, "src", "temporal", "worker.ts")).exists()).toBeFalse();
|
|
376
|
+
expect(await Bun.file(join(generatedRoot, "scripts", "codegen.ts")).exists()).toBeFalse();
|
|
377
|
+
});
|
|
378
|
+
|
|
188
379
|
test("microservice profile does not generate a website package", async () => {
|
|
189
380
|
const root = await mkdtemp(join(tmpdir(), "create-svc-microservice-profile-"));
|
|
190
381
|
const generatedRoot = join(root, "service");
|
package/src/scaffold.ts
CHANGED
|
@@ -4,16 +4,19 @@ import {
|
|
|
4
4
|
compactIdentifier,
|
|
5
5
|
compactDatabaseName,
|
|
6
6
|
deriveLocalPostgresPort,
|
|
7
|
+
type DeployTarget,
|
|
7
8
|
type Framework,
|
|
8
9
|
type GcpProjectMode,
|
|
9
10
|
type Runtime,
|
|
10
11
|
} from "./naming";
|
|
11
12
|
import { exampleForProfile, type Profile } from "./profiles";
|
|
13
|
+
import type { GitBootstrapConfig } from "./git-bootstrap";
|
|
12
14
|
|
|
13
15
|
export type ScaffoldConfig = {
|
|
14
16
|
directory: string;
|
|
15
17
|
serviceName: string;
|
|
16
18
|
modulePath: string;
|
|
19
|
+
target: DeployTarget;
|
|
17
20
|
runtime: Runtime;
|
|
18
21
|
framework: Framework;
|
|
19
22
|
profile: Profile;
|
|
@@ -24,6 +27,7 @@ export type ScaffoldConfig = {
|
|
|
24
27
|
billingAccount: string;
|
|
25
28
|
quotaProjectId: string;
|
|
26
29
|
autoDeploy: boolean;
|
|
30
|
+
git: GitBootstrapConfig;
|
|
27
31
|
neonDatabaseName: string;
|
|
28
32
|
apiHostname: string;
|
|
29
33
|
generatorRoot: string;
|
|
@@ -46,15 +50,20 @@ export async function scaffoldProject(config: ScaffoldConfig) {
|
|
|
46
50
|
await ensureTargetDirectory(targetDir);
|
|
47
51
|
|
|
48
52
|
const replacements = buildReplacements(config);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const templateRoots = [
|
|
54
|
+
{ kind: "shared" as const, root: resolve(config.generatorRoot, "templates", "shared") },
|
|
55
|
+
{ kind: "variant" as const, root: resolve(config.generatorRoot, "templates", "variants", `${config.runtime}-${config.framework}`) },
|
|
56
|
+
{ kind: "target" as const, root: resolve(config.generatorRoot, "templates", "targets", config.target) },
|
|
57
|
+
];
|
|
52
58
|
|
|
53
|
-
for (const
|
|
54
|
-
const files = await collectTemplateFiles(
|
|
59
|
+
for (const template of templateRoots) {
|
|
60
|
+
const files = await collectTemplateFiles(template.root);
|
|
55
61
|
|
|
56
62
|
for (const relativePath of files) {
|
|
57
|
-
|
|
63
|
+
if (shouldSkipForTarget(config.target, template.kind, relativePath)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const sourcePath = join(template.root, relativePath);
|
|
58
67
|
const destinationPath = join(targetDir, relativePath);
|
|
59
68
|
const raw = await Bun.file(sourcePath).text();
|
|
60
69
|
const rendered = renderTemplate(raw, replacements);
|
|
@@ -67,6 +76,43 @@ export async function scaffoldProject(config: ScaffoldConfig) {
|
|
|
67
76
|
await writeLocalEnvFile(targetDir, replacements);
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
function shouldSkipForTarget(target: DeployTarget, templateKind: "shared" | "variant" | "target", relativePath: string) {
|
|
80
|
+
if (target === "workers") {
|
|
81
|
+
if (templateKind === "target") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (relativePath === "Dockerfile" || relativePath === "docker-compose.yml") {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (templateKind === "shared") {
|
|
90
|
+
return (
|
|
91
|
+
relativePath === "service.yaml" ||
|
|
92
|
+
relativePath === "scripts/dev.ts" ||
|
|
93
|
+
relativePath === "scripts/ensure-local-db.ts" ||
|
|
94
|
+
relativePath === "scripts/local-docker.ts" ||
|
|
95
|
+
relativePath === "scripts/local-env.ts" ||
|
|
96
|
+
relativePath === "scripts/seed.ts" ||
|
|
97
|
+
relativePath === "scripts/wait-for-db.ts" ||
|
|
98
|
+
relativePath.startsWith("scripts/cloudrun/")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
relativePath.startsWith("src/db/") ||
|
|
104
|
+
relativePath.startsWith("src/temporal/") ||
|
|
105
|
+
relativePath.startsWith("src/waitlist/") ||
|
|
106
|
+
relativePath.startsWith("test/") ||
|
|
107
|
+
relativePath.startsWith("migrations/") ||
|
|
108
|
+
relativePath === "scripts/codegen.ts" ||
|
|
109
|
+
relativePath === "scripts/migrate.ts"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return relativePath.startsWith("scripts/workers/") || relativePath === "wrangler.toml";
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
async function ensureTargetDirectory(targetDir: string) {
|
|
71
117
|
await assertTargetDirectoryIsEmpty(targetDir);
|
|
72
118
|
await mkdir(targetDir, { recursive: true });
|
|
@@ -88,21 +134,26 @@ export async function assertTargetDirectoryIsEmpty(targetDir: string) {
|
|
|
88
134
|
|
|
89
135
|
async function collectTemplateFiles(root: string, relative = ""): Promise<string[]> {
|
|
90
136
|
const cwd = join(root, relative);
|
|
91
|
-
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = await readdir(cwd, { withFileTypes: true });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
92
146
|
const files: string[] = [];
|
|
93
147
|
|
|
94
148
|
for (const entry of entries) {
|
|
95
149
|
const nextRelative = relative ? join(relative, entry.name) : entry.name;
|
|
96
150
|
if (entry.isDirectory()) {
|
|
97
|
-
if (
|
|
151
|
+
if (entry.name === "node_modules" || entry.name === ".git") {
|
|
98
152
|
continue;
|
|
99
153
|
}
|
|
100
154
|
files.push(...(await collectTemplateFiles(root, nextRelative)));
|
|
101
155
|
continue;
|
|
102
156
|
}
|
|
103
|
-
if (nextRelative === ".github" || nextRelative.startsWith(".github/")) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
157
|
files.push(nextRelative);
|
|
107
158
|
}
|
|
108
159
|
|
|
@@ -115,16 +166,17 @@ function buildReplacements(config: ScaffoldConfig) {
|
|
|
115
166
|
const runtimeServiceAccount = `${serviceAccountBase}-runtime@${config.gcpProject}.iam.gserviceaccount.com`;
|
|
116
167
|
const previewBranchPrefix = `${config.serviceName}-pr`;
|
|
117
168
|
const personalBranchPrefix = `${config.serviceName}-dev`;
|
|
118
|
-
const remoteAttachmentBucket = `${config.gcpProject}-${config.serviceName}-attachments`;
|
|
119
|
-
const remoteAttachmentPublicBaseUrl = `https://storage.googleapis.com/${remoteAttachmentBucket}`;
|
|
120
169
|
const localDatabaseName = compactDatabaseName(config.serviceName);
|
|
121
170
|
const localDatabasePort = deriveLocalPostgresPort(config.serviceName);
|
|
122
|
-
const
|
|
123
|
-
const
|
|
171
|
+
const authIssuer = "https://auth.anmho.com/api/auth";
|
|
172
|
+
const authAudience = `api://${config.serviceName}`;
|
|
173
|
+
const authJwksUrl = `${authIssuer}/jwks`;
|
|
124
174
|
|
|
125
175
|
return {
|
|
126
176
|
SERVICE_NAME: config.serviceName,
|
|
177
|
+
SERVICE_ID: config.serviceName,
|
|
127
178
|
MODULE_PATH: config.modulePath,
|
|
179
|
+
TARGET: config.target,
|
|
128
180
|
PROJECT_ID: config.gcpProject,
|
|
129
181
|
PROJECT_NAME: config.gcpProjectName,
|
|
130
182
|
REGION: config.region,
|
|
@@ -150,32 +202,27 @@ function buildReplacements(config: ScaffoldConfig) {
|
|
|
150
202
|
RUNTIME_SERVICE_ACCOUNT: runtimeServiceAccount,
|
|
151
203
|
API_HOSTNAME: config.apiHostname,
|
|
152
204
|
API_BASE_DOMAIN: "anmho.com",
|
|
153
|
-
|
|
154
|
-
|
|
205
|
+
AUTH_ISSUER: authIssuer,
|
|
206
|
+
AUTH_AUDIENCE: authAudience,
|
|
207
|
+
AUTH_JWKS_URL: authJwksUrl,
|
|
155
208
|
LOCAL_DATABASE_NAME: localDatabaseName,
|
|
156
209
|
LOCAL_DATABASE_PORT: localDatabasePort,
|
|
157
210
|
LOCAL_DATABASE_USER: "postgres",
|
|
158
211
|
LOCAL_DATABASE_PASSWORD: "postgres",
|
|
159
|
-
LOCAL_ATTACHMENT_BUCKET: localAttachmentBucket,
|
|
160
|
-
LOCAL_ATTACHMENT_PUBLIC_BASE_URL: localAttachmentPublicBaseUrl,
|
|
161
212
|
COMMAND_DEV: config.runtime === "bun" ? "bun run dev" : "make dev",
|
|
162
213
|
COMMAND_MIGRATE: config.runtime === "bun" ? "bun run migrate" : "make migrate",
|
|
163
214
|
COMMAND_GEN: config.runtime === "bun" ? "bun run gen" : "make gen",
|
|
164
215
|
COMMAND_LINT: config.runtime === "bun" ? "bun run lint" : "make lint",
|
|
165
216
|
COMMAND_TEST: config.runtime === "bun" ? "bun run test" : "make test",
|
|
166
|
-
COMMAND_BOOTSTRAP:
|
|
167
|
-
COMMAND_DEPLOY:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
: 'make deploy ARGS="--destroy --environment personal --name <slug>"',
|
|
176
|
-
COMMAND_CLEANUP: config.runtime === "bun" ? "bun run cleanup" : "make cleanup",
|
|
177
|
-
COMMAND_CLEANUP_PROJECT: config.runtime === "bun" ? "bun run cleanup -- --project" : 'make cleanup ARGS="--project"',
|
|
178
|
-
GITIGNORE_EXTRA: config.framework === "connectrpc" ? "gen/" : "",
|
|
217
|
+
COMMAND_BOOTSTRAP: "service create",
|
|
218
|
+
COMMAND_DEPLOY: "service deploy",
|
|
219
|
+
COMMAND_AUTH_RESOURCE: "service auth resource-server",
|
|
220
|
+
COMMAND_AUTH_CLIENT: "service auth client create",
|
|
221
|
+
COMMAND_DEPLOY_PERSONAL: "service deploy --environment personal --name <name>",
|
|
222
|
+
COMMAND_DEPLOY_DESTROY: "service destroy --environment personal --name <name>",
|
|
223
|
+
COMMAND_CLEANUP: "service destroy",
|
|
224
|
+
COMMAND_CLEANUP_PROJECT: "service destroy --project",
|
|
225
|
+
GITIGNORE_EXTRA: "",
|
|
179
226
|
LOCAL_INTROSPECTION_NOTE:
|
|
180
227
|
config.framework === "connectrpc"
|
|
181
228
|
? [
|
|
@@ -200,12 +247,10 @@ async function writeLocalEnvFile(targetDir: string, replacements: Record<string,
|
|
|
200
247
|
|
|
201
248
|
const rendered = renderTemplate(
|
|
202
249
|
[
|
|
203
|
-
"# Generated local development defaults for create-
|
|
250
|
+
"# Generated local development defaults for create-service.",
|
|
204
251
|
"# This file is user-owned after scaffold and is gitignored.",
|
|
205
252
|
"",
|
|
206
|
-
"DATABASE_URL=postgres://{{LOCAL_DATABASE_USER}}:{{LOCAL_DATABASE_PASSWORD}}@127.0.0.1:{{LOCAL_DATABASE_PORT}}/{{LOCAL_DATABASE_NAME}}",
|
|
207
|
-
"ATTACHMENT_BUCKET={{LOCAL_ATTACHMENT_BUCKET}}",
|
|
208
|
-
"ATTACHMENT_PUBLIC_BASE_URL={{LOCAL_ATTACHMENT_PUBLIC_BASE_URL}}",
|
|
253
|
+
"DATABASE_URL=postgres://{{LOCAL_DATABASE_USER}}:{{LOCAL_DATABASE_PASSWORD}}@127.0.0.1:{{LOCAL_DATABASE_PORT}}/{{LOCAL_DATABASE_NAME}}?sslmode=disable",
|
|
209
254
|
"",
|
|
210
255
|
].join("\n"),
|
|
211
256
|
replacements
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { findGeneratedServiceRoot, normalizeScaffoldArgs } from "./service";
|
|
6
|
+
|
|
7
|
+
test("normalizeScaffoldArgs treats service create as the scaffold command outside a service repo", () => {
|
|
8
|
+
expect(normalizeScaffoldArgs(["create", "launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
9
|
+
expect(normalizeScaffoldArgs(["new", "launch-api"])).toEqual(["launch-api"]);
|
|
10
|
+
expect(normalizeScaffoldArgs(["init", "launch-api"])).toEqual(["launch-api"]);
|
|
11
|
+
expect(normalizeScaffoldArgs(["launch-api", "--yes"])).toEqual(["launch-api", "--yes"]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("normalizeScaffoldArgs maps service help to generator help outside a service repo", () => {
|
|
15
|
+
expect(normalizeScaffoldArgs(["help"])).toEqual(["--help"]);
|
|
16
|
+
expect(normalizeScaffoldArgs(["help", "--verbose"])).toEqual(["--help", "--verbose"]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("findGeneratedServiceRoot detects generated service context from nested directories", async () => {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), "create-svc-service-root-"));
|
|
21
|
+
const serviceRoot = join(root, "generated-api");
|
|
22
|
+
const nested = join(serviceRoot, "src", "waitlist");
|
|
23
|
+
await mkdir(join(serviceRoot, "scripts", "cloudrun"), { recursive: true });
|
|
24
|
+
await mkdir(nested, { recursive: true });
|
|
25
|
+
await writeFile(join(serviceRoot, "service.config.ts"), "export default {}");
|
|
26
|
+
await writeFile(join(serviceRoot, "scripts", "cloudrun", "cli.ts"), "");
|
|
27
|
+
|
|
28
|
+
expect(findGeneratedServiceRoot(nested)).toBe(serviceRoot);
|
|
29
|
+
expect(findGeneratedServiceRoot(root)).toBeUndefined();
|
|
30
|
+
});
|