@geekmidas/cli 0.10.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +525 -0
- package/dist/bundler-B1qy9b-j.cjs +112 -0
- package/dist/bundler-B1qy9b-j.cjs.map +1 -0
- package/dist/bundler-DskIqW2t.mjs +111 -0
- package/dist/bundler-DskIqW2t.mjs.map +1 -0
- package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/encryption-C8H-38Yy.mjs +42 -0
- package/dist/encryption-C8H-38Yy.mjs.map +1 -0
- package/dist/encryption-Dyf_r1h-.cjs +44 -0
- package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
- package/dist/index.cjs +2123 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2141 -192
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
- package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
- package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
- package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
- package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
- package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.d.cts.map +1 -1
- package/dist/openapi-react-query.d.mts.map +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BOOpAF8N.cjs +5 -0
- package/dist/storage-Bj1E26lU.cjs +187 -0
- package/dist/storage-Bj1E26lU.cjs.map +1 -0
- package/dist/storage-kSxTjkNb.mjs +133 -0
- package/dist/storage-kSxTjkNb.mjs.map +1 -0
- package/dist/storage-tgZSUnKl.mjs +3 -0
- package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
- package/dist/types-BR0M2v_c.d.mts.map +1 -0
- package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
- package/dist/types-BhkZc-vm.d.cts.map +1 -0
- package/examples/cron-example.ts +27 -27
- package/examples/env.ts +27 -27
- package/examples/function-example.ts +31 -31
- package/examples/gkm.config.json +20 -20
- package/examples/gkm.config.ts +8 -8
- package/examples/gkm.minimal.config.json +5 -5
- package/examples/gkm.production.config.json +25 -25
- package/examples/logger.ts +2 -2
- package/package.json +6 -6
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
- package/src/__tests__/config.spec.ts +55 -55
- package/src/__tests__/loadEnvFiles.spec.ts +93 -93
- package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
- package/src/__tests__/openapi-react-query.spec.ts +497 -497
- package/src/__tests__/openapi.spec.ts +428 -428
- package/src/__tests__/test-helpers.ts +76 -76
- package/src/auth/__tests__/credentials.spec.ts +204 -0
- package/src/auth/__tests__/index.spec.ts +168 -0
- package/src/auth/credentials.ts +187 -0
- package/src/auth/index.ts +226 -0
- package/src/build/__tests__/bundler.spec.ts +444 -0
- package/src/build/__tests__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +210 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +260 -179
- package/src/build/manifests.ts +52 -52
- package/src/build/providerResolver.ts +145 -145
- package/src/build/types.ts +64 -43
- package/src/config.ts +39 -39
- package/src/deploy/__tests__/docker.spec.ts +111 -0
- package/src/deploy/__tests__/dokploy.spec.ts +245 -0
- package/src/deploy/__tests__/init.spec.ts +662 -0
- package/src/deploy/docker.ts +128 -0
- package/src/deploy/dokploy.ts +204 -0
- package/src/deploy/index.ts +136 -0
- package/src/deploy/init.ts +484 -0
- package/src/deploy/types.ts +48 -0
- package/src/dev/__tests__/index.spec.ts +266 -266
- package/src/dev/index.ts +647 -601
- package/src/docker/__tests__/compose.spec.ts +531 -0
- package/src/docker/__tests__/templates.spec.ts +280 -0
- package/src/docker/compose.ts +273 -0
- package/src/docker/index.ts +230 -0
- package/src/docker/templates.ts +446 -0
- package/src/generators/CronGenerator.ts +72 -72
- package/src/generators/EndpointGenerator.ts +699 -398
- package/src/generators/FunctionGenerator.ts +84 -84
- package/src/generators/Generator.ts +72 -72
- package/src/generators/OpenApiTsGenerator.ts +577 -577
- package/src/generators/SubscriberGenerator.ts +124 -124
- package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
- package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
- package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
- package/src/generators/index.ts +4 -4
- package/src/index.ts +623 -201
- package/src/init/__tests__/generators.spec.ts +334 -334
- package/src/init/__tests__/init.spec.ts +332 -332
- package/src/init/__tests__/utils.spec.ts +89 -89
- package/src/init/generators/config.ts +175 -175
- package/src/init/generators/docker.ts +41 -41
- package/src/init/generators/env.ts +72 -72
- package/src/init/generators/index.ts +1 -1
- package/src/init/generators/models.ts +64 -64
- package/src/init/generators/monorepo.ts +161 -161
- package/src/init/generators/package.ts +71 -71
- package/src/init/generators/source.ts +6 -6
- package/src/init/index.ts +203 -208
- package/src/init/templates/api.ts +115 -115
- package/src/init/templates/index.ts +75 -75
- package/src/init/templates/minimal.ts +98 -98
- package/src/init/templates/serverless.ts +89 -89
- package/src/init/templates/worker.ts +98 -98
- package/src/init/utils.ts +54 -56
- package/src/openapi-react-query.ts +194 -194
- package/src/openapi.ts +63 -63
- package/src/secrets/__tests__/encryption.spec.ts +226 -0
- package/src/secrets/__tests__/generator.spec.ts +319 -0
- package/src/secrets/__tests__/index.spec.ts +91 -0
- package/src/secrets/__tests__/storage.spec.ts +611 -0
- package/src/secrets/encryption.ts +91 -0
- package/src/secrets/generator.ts +164 -0
- package/src/secrets/index.ts +383 -0
- package/src/secrets/storage.ts +192 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsdown.config.ts +11 -8
- package/dist/config-BrkUalUh.mjs.map +0 -1
- package/dist/config-C9aXOHBe.cjs.map +0 -1
- package/dist/openapi-BeHLKcwP.cjs.map +0 -1
- package/dist/openapi-CZLI4QTr.mjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
- package/dist/types-DXgiA1sF.d.mts.map +0 -1
- package/dist/types-b-vwGpqc.d.cts.map +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { loadConfig } from '../config';
|
|
6
|
+
import { generateDockerCompose, generateMinimalDockerCompose } from './compose';
|
|
7
|
+
import {
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
generateDockerEntrypoint,
|
|
10
|
+
generateDockerignore,
|
|
11
|
+
generateMultiStageDockerfile,
|
|
12
|
+
generateSlimDockerfile,
|
|
13
|
+
resolveDockerConfig,
|
|
14
|
+
} from './templates';
|
|
15
|
+
|
|
16
|
+
const logger = console;
|
|
17
|
+
|
|
18
|
+
export interface DockerOptions {
|
|
19
|
+
/** Build Docker image after generating files */
|
|
20
|
+
build?: boolean;
|
|
21
|
+
/** Push image to registry after building */
|
|
22
|
+
push?: boolean;
|
|
23
|
+
/** Image tag (default: 'latest') */
|
|
24
|
+
tag?: string;
|
|
25
|
+
/** Container registry URL */
|
|
26
|
+
registry?: string;
|
|
27
|
+
/** Use slim Dockerfile (requires pre-built bundle from `gkm build --production`) */
|
|
28
|
+
slim?: boolean;
|
|
29
|
+
/** Enable turbo prune for monorepo optimization */
|
|
30
|
+
turbo?: boolean;
|
|
31
|
+
/** Package name for turbo prune (defaults to package.json name) */
|
|
32
|
+
turboPackage?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DockerGeneratedFiles {
|
|
36
|
+
dockerfile: string;
|
|
37
|
+
dockerCompose: string;
|
|
38
|
+
dockerignore: string;
|
|
39
|
+
entrypoint: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Docker command implementation
|
|
44
|
+
* Generates Dockerfile, docker-compose.yml, and related files
|
|
45
|
+
*
|
|
46
|
+
* Default: Multi-stage Dockerfile that builds from source inside Docker
|
|
47
|
+
* --slim: Slim Dockerfile that copies pre-built bundle (requires prior build)
|
|
48
|
+
*/
|
|
49
|
+
export async function dockerCommand(
|
|
50
|
+
options: DockerOptions,
|
|
51
|
+
): Promise<DockerGeneratedFiles> {
|
|
52
|
+
const config = await loadConfig();
|
|
53
|
+
const dockerConfig = resolveDockerConfig(config);
|
|
54
|
+
|
|
55
|
+
// Get health check path from production config
|
|
56
|
+
const serverConfig =
|
|
57
|
+
typeof config.providers?.server === 'object'
|
|
58
|
+
? config.providers.server
|
|
59
|
+
: undefined;
|
|
60
|
+
const healthCheckPath = serverConfig?.production?.healthCheck ?? '/health';
|
|
61
|
+
|
|
62
|
+
// Determine Dockerfile type
|
|
63
|
+
// Default: Multi-stage (builds inside Docker for reproducibility)
|
|
64
|
+
// --slim: Requires pre-built bundle
|
|
65
|
+
const useSlim = options.slim === true;
|
|
66
|
+
|
|
67
|
+
if (useSlim) {
|
|
68
|
+
// Verify pre-built bundle exists for slim mode
|
|
69
|
+
const distDir = join(process.cwd(), '.gkm', 'server', 'dist');
|
|
70
|
+
const hasBuild = existsSync(join(distDir, 'server.mjs'));
|
|
71
|
+
|
|
72
|
+
if (!hasBuild) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Slim Dockerfile requires a pre-built bundle. Run `gkm build --provider server --production` first, or omit --slim to use multi-stage build.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate Docker files
|
|
80
|
+
const dockerDir = join(process.cwd(), '.gkm', 'docker');
|
|
81
|
+
await mkdir(dockerDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
// Detect package manager from lockfiles
|
|
84
|
+
const packageManager = detectPackageManager();
|
|
85
|
+
|
|
86
|
+
const templateOptions = {
|
|
87
|
+
imageName: dockerConfig.imageName,
|
|
88
|
+
baseImage: dockerConfig.baseImage,
|
|
89
|
+
port: dockerConfig.port,
|
|
90
|
+
healthCheckPath,
|
|
91
|
+
prebuilt: useSlim,
|
|
92
|
+
turbo: options.turbo,
|
|
93
|
+
turboPackage: options.turboPackage ?? dockerConfig.imageName,
|
|
94
|
+
packageManager,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Generate Dockerfile
|
|
98
|
+
const dockerfile = useSlim
|
|
99
|
+
? generateSlimDockerfile(templateOptions)
|
|
100
|
+
: generateMultiStageDockerfile(templateOptions);
|
|
101
|
+
|
|
102
|
+
const dockerMode = useSlim ? 'slim' : options.turbo ? 'turbo' : 'multi-stage';
|
|
103
|
+
|
|
104
|
+
const dockerfilePath = join(dockerDir, 'Dockerfile');
|
|
105
|
+
await writeFile(dockerfilePath, dockerfile);
|
|
106
|
+
logger.log(
|
|
107
|
+
`Generated: .gkm/docker/Dockerfile (${dockerMode}, ${packageManager})`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Generate docker-compose.yml
|
|
111
|
+
const composeOptions = {
|
|
112
|
+
imageName: dockerConfig.imageName,
|
|
113
|
+
registry: options.registry ?? dockerConfig.registry,
|
|
114
|
+
port: dockerConfig.port,
|
|
115
|
+
healthCheckPath,
|
|
116
|
+
services: dockerConfig.compose?.services ?? {},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Check if there are any services configured
|
|
120
|
+
const hasServices = Array.isArray(composeOptions.services)
|
|
121
|
+
? composeOptions.services.length > 0
|
|
122
|
+
: Object.keys(composeOptions.services).length > 0;
|
|
123
|
+
|
|
124
|
+
const dockerCompose = hasServices
|
|
125
|
+
? generateDockerCompose(composeOptions)
|
|
126
|
+
: generateMinimalDockerCompose(composeOptions);
|
|
127
|
+
|
|
128
|
+
const composePath = join(dockerDir, 'docker-compose.yml');
|
|
129
|
+
await writeFile(composePath, dockerCompose);
|
|
130
|
+
logger.log('Generated: .gkm/docker/docker-compose.yml');
|
|
131
|
+
|
|
132
|
+
// Generate .dockerignore in project root (Docker looks for it there)
|
|
133
|
+
const dockerignore = generateDockerignore();
|
|
134
|
+
const dockerignorePath = join(process.cwd(), '.dockerignore');
|
|
135
|
+
await writeFile(dockerignorePath, dockerignore);
|
|
136
|
+
logger.log('Generated: .dockerignore (project root)');
|
|
137
|
+
|
|
138
|
+
// Generate docker-entrypoint.sh
|
|
139
|
+
const entrypoint = generateDockerEntrypoint();
|
|
140
|
+
const entrypointPath = join(dockerDir, 'docker-entrypoint.sh');
|
|
141
|
+
await writeFile(entrypointPath, entrypoint);
|
|
142
|
+
logger.log('Generated: .gkm/docker/docker-entrypoint.sh');
|
|
143
|
+
|
|
144
|
+
const result: DockerGeneratedFiles = {
|
|
145
|
+
dockerfile: dockerfilePath,
|
|
146
|
+
dockerCompose: composePath,
|
|
147
|
+
dockerignore: dockerignorePath,
|
|
148
|
+
entrypoint: entrypointPath,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Build Docker image if requested
|
|
152
|
+
if (options.build) {
|
|
153
|
+
await buildDockerImage(dockerConfig.imageName, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Push Docker image if requested
|
|
157
|
+
if (options.push) {
|
|
158
|
+
await pushDockerImage(dockerConfig.imageName, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Build Docker image
|
|
166
|
+
* Uses BuildKit for cache mount support
|
|
167
|
+
*/
|
|
168
|
+
async function buildDockerImage(
|
|
169
|
+
imageName: string,
|
|
170
|
+
options: DockerOptions,
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
const tag = options.tag ?? 'latest';
|
|
173
|
+
const registry = options.registry;
|
|
174
|
+
|
|
175
|
+
const fullImageName = registry
|
|
176
|
+
? `${registry}/${imageName}:${tag}`
|
|
177
|
+
: `${imageName}:${tag}`;
|
|
178
|
+
|
|
179
|
+
logger.log(`\n🐳 Building Docker image: ${fullImageName}`);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Use BuildKit for cache mount support (required for --mount=type=cache)
|
|
183
|
+
execSync(
|
|
184
|
+
`DOCKER_BUILDKIT=1 docker build -f .gkm/docker/Dockerfile -t ${fullImageName} .`,
|
|
185
|
+
{
|
|
186
|
+
cwd: process.cwd(),
|
|
187
|
+
stdio: 'inherit',
|
|
188
|
+
env: { ...process.env, DOCKER_BUILDKIT: '1' },
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
logger.log(`✅ Docker image built: ${fullImageName}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Failed to build Docker image: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Push Docker image to registry
|
|
201
|
+
*/
|
|
202
|
+
async function pushDockerImage(
|
|
203
|
+
imageName: string,
|
|
204
|
+
options: DockerOptions,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const tag = options.tag ?? 'latest';
|
|
207
|
+
const registry = options.registry;
|
|
208
|
+
|
|
209
|
+
if (!registry) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'Registry is required to push Docker image. Use --registry or configure docker.registry in gkm.config.ts',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const fullImageName = `${registry}/${imageName}:${tag}`;
|
|
216
|
+
|
|
217
|
+
logger.log(`\n🚀 Pushing Docker image: ${fullImageName}`);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
execSync(`docker push ${fullImageName}`, {
|
|
221
|
+
cwd: process.cwd(),
|
|
222
|
+
stdio: 'inherit',
|
|
223
|
+
});
|
|
224
|
+
logger.log(`✅ Docker image pushed: ${fullImageName}`);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Failed to push Docker image: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, parse } from 'node:path';
|
|
3
|
+
import type { DockerConfig, GkmConfig } from '../types';
|
|
4
|
+
|
|
5
|
+
export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
|
|
6
|
+
|
|
7
|
+
export interface DockerTemplateOptions {
|
|
8
|
+
imageName: string;
|
|
9
|
+
baseImage: string;
|
|
10
|
+
port: number;
|
|
11
|
+
healthCheckPath: string;
|
|
12
|
+
/** Whether the build is pre-built (slim Dockerfile) or needs building */
|
|
13
|
+
prebuilt: boolean;
|
|
14
|
+
/** Detected package manager */
|
|
15
|
+
packageManager: PackageManager;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MultiStageDockerfileOptions extends DockerTemplateOptions {
|
|
19
|
+
/** Enable turbo prune for monorepo optimization */
|
|
20
|
+
turbo?: boolean;
|
|
21
|
+
/** Package name for turbo prune (defaults to current directory name) */
|
|
22
|
+
turboPackage?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detect package manager from lockfiles
|
|
27
|
+
* Walks up the directory tree to find lockfile (for monorepos)
|
|
28
|
+
*/
|
|
29
|
+
export function detectPackageManager(
|
|
30
|
+
cwd: string = process.cwd(),
|
|
31
|
+
): PackageManager {
|
|
32
|
+
const lockfiles: [string, PackageManager][] = [
|
|
33
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
34
|
+
['bun.lockb', 'bun'],
|
|
35
|
+
['yarn.lock', 'yarn'],
|
|
36
|
+
['package-lock.json', 'npm'],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
let dir = cwd;
|
|
40
|
+
const root = parse(dir).root;
|
|
41
|
+
|
|
42
|
+
// Walk up the directory tree
|
|
43
|
+
while (dir !== root) {
|
|
44
|
+
for (const [lockfile, pm] of lockfiles) {
|
|
45
|
+
if (existsSync(join(dir, lockfile))) {
|
|
46
|
+
return pm;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
dir = dirname(dir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check root directory
|
|
53
|
+
for (const [lockfile, pm] of lockfiles) {
|
|
54
|
+
if (existsSync(join(root, lockfile))) {
|
|
55
|
+
return pm;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'pnpm'; // default
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get package manager specific commands and paths
|
|
64
|
+
*/
|
|
65
|
+
function getPmConfig(pm: PackageManager) {
|
|
66
|
+
const configs = {
|
|
67
|
+
pnpm: {
|
|
68
|
+
install: 'corepack enable && corepack prepare pnpm@latest --activate',
|
|
69
|
+
lockfile: 'pnpm-lock.yaml',
|
|
70
|
+
fetch: 'pnpm fetch',
|
|
71
|
+
installCmd: 'pnpm install --frozen-lockfile --offline',
|
|
72
|
+
cacheTarget: '/root/.local/share/pnpm/store',
|
|
73
|
+
cacheId: 'pnpm',
|
|
74
|
+
run: 'pnpm',
|
|
75
|
+
addGlobal: 'pnpm add -g',
|
|
76
|
+
},
|
|
77
|
+
npm: {
|
|
78
|
+
install: '', // npm comes with node
|
|
79
|
+
lockfile: 'package-lock.json',
|
|
80
|
+
fetch: '', // npm doesn't have fetch
|
|
81
|
+
installCmd: 'npm ci',
|
|
82
|
+
cacheTarget: '/root/.npm',
|
|
83
|
+
cacheId: 'npm',
|
|
84
|
+
run: 'npm run',
|
|
85
|
+
addGlobal: 'npm install -g',
|
|
86
|
+
},
|
|
87
|
+
yarn: {
|
|
88
|
+
install: 'corepack enable && corepack prepare yarn@stable --activate',
|
|
89
|
+
lockfile: 'yarn.lock',
|
|
90
|
+
fetch: '', // yarn doesn't have fetch
|
|
91
|
+
installCmd: 'yarn install --frozen-lockfile',
|
|
92
|
+
cacheTarget: '/root/.yarn/cache',
|
|
93
|
+
cacheId: 'yarn',
|
|
94
|
+
run: 'yarn',
|
|
95
|
+
addGlobal: 'yarn global add',
|
|
96
|
+
},
|
|
97
|
+
bun: {
|
|
98
|
+
install: 'npm install -g bun',
|
|
99
|
+
lockfile: 'bun.lockb',
|
|
100
|
+
fetch: '', // bun doesn't have fetch
|
|
101
|
+
installCmd: 'bun install --frozen-lockfile',
|
|
102
|
+
cacheTarget: '/root/.bun/install/cache',
|
|
103
|
+
cacheId: 'bun',
|
|
104
|
+
run: 'bun run',
|
|
105
|
+
addGlobal: 'bun add -g',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
return configs[pm];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate a multi-stage Dockerfile for building from source
|
|
113
|
+
* Optimized for build speed with:
|
|
114
|
+
* - BuildKit cache mounts for package manager store
|
|
115
|
+
* - pnpm fetch for better layer caching (when using pnpm)
|
|
116
|
+
* - Optional turbo prune for monorepos
|
|
117
|
+
*/
|
|
118
|
+
export function generateMultiStageDockerfile(
|
|
119
|
+
options: MultiStageDockerfileOptions,
|
|
120
|
+
): string {
|
|
121
|
+
const {
|
|
122
|
+
baseImage,
|
|
123
|
+
port,
|
|
124
|
+
healthCheckPath,
|
|
125
|
+
turbo,
|
|
126
|
+
turboPackage,
|
|
127
|
+
packageManager,
|
|
128
|
+
} = options;
|
|
129
|
+
|
|
130
|
+
if (turbo) {
|
|
131
|
+
return generateTurboDockerfile({
|
|
132
|
+
...options,
|
|
133
|
+
turboPackage: turboPackage ?? 'api',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pm = getPmConfig(packageManager);
|
|
138
|
+
const installPm = pm.install
|
|
139
|
+
? `\n# Install ${packageManager}\nRUN ${pm.install}\n`
|
|
140
|
+
: '';
|
|
141
|
+
const hasFetch = packageManager === 'pnpm';
|
|
142
|
+
|
|
143
|
+
// pnpm has fetch which allows better caching
|
|
144
|
+
const depsStage = hasFetch
|
|
145
|
+
? `# Copy lockfile first for better caching
|
|
146
|
+
COPY ${pm.lockfile} ./
|
|
147
|
+
|
|
148
|
+
# Fetch dependencies (downloads to virtual store, cached separately)
|
|
149
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
150
|
+
${pm.fetch}
|
|
151
|
+
|
|
152
|
+
# Copy package.json after fetch
|
|
153
|
+
COPY package.json ./
|
|
154
|
+
|
|
155
|
+
# Install from cache (fast - no network needed)
|
|
156
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
157
|
+
${pm.installCmd}`
|
|
158
|
+
: `# Copy package files
|
|
159
|
+
COPY package.json ${pm.lockfile} ./
|
|
160
|
+
|
|
161
|
+
# Install dependencies with cache
|
|
162
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
163
|
+
${pm.installCmd}`;
|
|
164
|
+
|
|
165
|
+
return `# syntax=docker/dockerfile:1
|
|
166
|
+
# Stage 1: Dependencies
|
|
167
|
+
FROM ${baseImage} AS deps
|
|
168
|
+
|
|
169
|
+
WORKDIR /app
|
|
170
|
+
${installPm}
|
|
171
|
+
${depsStage}
|
|
172
|
+
|
|
173
|
+
# Stage 2: Build
|
|
174
|
+
FROM deps AS builder
|
|
175
|
+
|
|
176
|
+
WORKDIR /app
|
|
177
|
+
|
|
178
|
+
# Copy source (deps already installed)
|
|
179
|
+
COPY . .
|
|
180
|
+
|
|
181
|
+
# Build production server
|
|
182
|
+
RUN ${pm.run} gkm build --provider server --production
|
|
183
|
+
|
|
184
|
+
# Stage 3: Production
|
|
185
|
+
FROM ${baseImage} AS runner
|
|
186
|
+
|
|
187
|
+
WORKDIR /app
|
|
188
|
+
|
|
189
|
+
# Install tini for proper signal handling as PID 1
|
|
190
|
+
RUN apk add --no-cache tini
|
|
191
|
+
|
|
192
|
+
# Create non-root user
|
|
193
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
194
|
+
adduser --system --uid 1001 hono
|
|
195
|
+
|
|
196
|
+
# Copy bundled server
|
|
197
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
198
|
+
|
|
199
|
+
# Environment
|
|
200
|
+
ENV NODE_ENV=production
|
|
201
|
+
ENV PORT=${port}
|
|
202
|
+
|
|
203
|
+
# Health check
|
|
204
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
205
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
206
|
+
|
|
207
|
+
# Switch to non-root user
|
|
208
|
+
USER hono
|
|
209
|
+
|
|
210
|
+
EXPOSE ${port}
|
|
211
|
+
|
|
212
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
213
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
214
|
+
CMD ["node", "server.mjs"]
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generate a Dockerfile optimized for Turbo monorepos
|
|
220
|
+
* Uses turbo prune to create minimal Docker context
|
|
221
|
+
*/
|
|
222
|
+
function generateTurboDockerfile(options: MultiStageDockerfileOptions): string {
|
|
223
|
+
const { baseImage, port, healthCheckPath, turboPackage, packageManager } =
|
|
224
|
+
options;
|
|
225
|
+
|
|
226
|
+
const pm = getPmConfig(packageManager);
|
|
227
|
+
const installPm = pm.install ? `RUN ${pm.install}` : '';
|
|
228
|
+
const hasFetch = packageManager === 'pnpm';
|
|
229
|
+
|
|
230
|
+
// pnpm has fetch which allows better caching
|
|
231
|
+
const depsInstall = hasFetch
|
|
232
|
+
? `# Fetch and install from cache
|
|
233
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
234
|
+
${pm.fetch}
|
|
235
|
+
|
|
236
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
237
|
+
${pm.installCmd}`
|
|
238
|
+
: `# Install dependencies with cache
|
|
239
|
+
RUN --mount=type=cache,id=${pm.cacheId},target=${pm.cacheTarget} \\
|
|
240
|
+
${pm.installCmd}`;
|
|
241
|
+
|
|
242
|
+
return `# syntax=docker/dockerfile:1
|
|
243
|
+
# Stage 1: Prune monorepo
|
|
244
|
+
FROM ${baseImage} AS pruner
|
|
245
|
+
|
|
246
|
+
WORKDIR /app
|
|
247
|
+
|
|
248
|
+
${installPm}
|
|
249
|
+
RUN ${pm.addGlobal} turbo
|
|
250
|
+
|
|
251
|
+
COPY . .
|
|
252
|
+
|
|
253
|
+
# Prune to only include necessary packages
|
|
254
|
+
RUN turbo prune ${turboPackage} --docker
|
|
255
|
+
|
|
256
|
+
# Stage 2: Install dependencies
|
|
257
|
+
FROM ${baseImage} AS deps
|
|
258
|
+
|
|
259
|
+
WORKDIR /app
|
|
260
|
+
|
|
261
|
+
${installPm}
|
|
262
|
+
|
|
263
|
+
# Copy pruned lockfile and package.jsons
|
|
264
|
+
COPY --from=pruner /app/out/${pm.lockfile} ./
|
|
265
|
+
COPY --from=pruner /app/out/json/ ./
|
|
266
|
+
|
|
267
|
+
${depsInstall}
|
|
268
|
+
|
|
269
|
+
# Stage 3: Build
|
|
270
|
+
FROM deps AS builder
|
|
271
|
+
|
|
272
|
+
WORKDIR /app
|
|
273
|
+
|
|
274
|
+
# Copy pruned source
|
|
275
|
+
COPY --from=pruner /app/out/full/ ./
|
|
276
|
+
|
|
277
|
+
# Build production server
|
|
278
|
+
RUN ${pm.run} gkm build --provider server --production
|
|
279
|
+
|
|
280
|
+
# Stage 4: Production
|
|
281
|
+
FROM ${baseImage} AS runner
|
|
282
|
+
|
|
283
|
+
WORKDIR /app
|
|
284
|
+
|
|
285
|
+
RUN apk add --no-cache tini
|
|
286
|
+
|
|
287
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
288
|
+
adduser --system --uid 1001 hono
|
|
289
|
+
|
|
290
|
+
COPY --from=builder --chown=hono:nodejs /app/.gkm/server/dist/server.mjs ./
|
|
291
|
+
|
|
292
|
+
ENV NODE_ENV=production
|
|
293
|
+
ENV PORT=${port}
|
|
294
|
+
|
|
295
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
296
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
297
|
+
|
|
298
|
+
USER hono
|
|
299
|
+
|
|
300
|
+
EXPOSE ${port}
|
|
301
|
+
|
|
302
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
303
|
+
CMD ["node", "server.mjs"]
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Generate a slim Dockerfile for pre-built bundles
|
|
309
|
+
*/
|
|
310
|
+
export function generateSlimDockerfile(options: DockerTemplateOptions): string {
|
|
311
|
+
const { baseImage, port, healthCheckPath } = options;
|
|
312
|
+
|
|
313
|
+
return `# Slim Dockerfile for pre-built production bundle
|
|
314
|
+
FROM ${baseImage}
|
|
315
|
+
|
|
316
|
+
WORKDIR /app
|
|
317
|
+
|
|
318
|
+
# Install tini for proper signal handling as PID 1
|
|
319
|
+
# Handles SIGTERM propagation and zombie process reaping
|
|
320
|
+
RUN apk add --no-cache tini
|
|
321
|
+
|
|
322
|
+
# Create non-root user
|
|
323
|
+
RUN addgroup --system --gid 1001 nodejs && \\
|
|
324
|
+
adduser --system --uid 1001 hono
|
|
325
|
+
|
|
326
|
+
# Copy pre-built bundle
|
|
327
|
+
COPY .gkm/server/dist/server.mjs ./
|
|
328
|
+
|
|
329
|
+
# Environment
|
|
330
|
+
ENV NODE_ENV=production
|
|
331
|
+
ENV PORT=${port}
|
|
332
|
+
|
|
333
|
+
# Health check
|
|
334
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
335
|
+
CMD wget -q --spider http://localhost:${port}${healthCheckPath} || exit 1
|
|
336
|
+
|
|
337
|
+
# Switch to non-root user
|
|
338
|
+
USER hono
|
|
339
|
+
|
|
340
|
+
EXPOSE ${port}
|
|
341
|
+
|
|
342
|
+
# Use tini as entrypoint to handle PID 1 responsibilities
|
|
343
|
+
ENTRYPOINT ["/sbin/tini", "--"]
|
|
344
|
+
CMD ["node", "server.mjs"]
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate .dockerignore file
|
|
350
|
+
*/
|
|
351
|
+
export function generateDockerignore(): string {
|
|
352
|
+
return `# Dependencies
|
|
353
|
+
node_modules
|
|
354
|
+
.pnpm-store
|
|
355
|
+
|
|
356
|
+
# Build output (except what we need)
|
|
357
|
+
.gkm/aws*
|
|
358
|
+
.gkm/server/*.ts
|
|
359
|
+
!.gkm/server/dist
|
|
360
|
+
|
|
361
|
+
# IDE and editor
|
|
362
|
+
.idea
|
|
363
|
+
.vscode
|
|
364
|
+
*.swp
|
|
365
|
+
*.swo
|
|
366
|
+
|
|
367
|
+
# Git
|
|
368
|
+
.git
|
|
369
|
+
.gitignore
|
|
370
|
+
|
|
371
|
+
# Logs
|
|
372
|
+
*.log
|
|
373
|
+
npm-debug.log*
|
|
374
|
+
pnpm-debug.log*
|
|
375
|
+
|
|
376
|
+
# Test files
|
|
377
|
+
**/*.test.ts
|
|
378
|
+
**/*.spec.ts
|
|
379
|
+
**/__tests__
|
|
380
|
+
coverage
|
|
381
|
+
|
|
382
|
+
# Documentation
|
|
383
|
+
docs
|
|
384
|
+
*.md
|
|
385
|
+
!README.md
|
|
386
|
+
|
|
387
|
+
# Environment files (handle secrets separately)
|
|
388
|
+
.env
|
|
389
|
+
.env.*
|
|
390
|
+
!.env.example
|
|
391
|
+
|
|
392
|
+
# Docker files (don't copy recursively)
|
|
393
|
+
Dockerfile*
|
|
394
|
+
docker-compose*
|
|
395
|
+
.dockerignore
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Generate docker-entrypoint.sh for custom startup logic
|
|
401
|
+
*/
|
|
402
|
+
export function generateDockerEntrypoint(): string {
|
|
403
|
+
return `#!/bin/sh
|
|
404
|
+
set -e
|
|
405
|
+
|
|
406
|
+
# Run any custom startup scripts here
|
|
407
|
+
# Example: wait for database
|
|
408
|
+
# until nc -z $DB_HOST $DB_PORT; do
|
|
409
|
+
# echo "Waiting for database..."
|
|
410
|
+
# sleep 1
|
|
411
|
+
# done
|
|
412
|
+
|
|
413
|
+
# Execute the main command
|
|
414
|
+
exec "$@"
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Resolve Docker configuration from GkmConfig with defaults
|
|
420
|
+
*/
|
|
421
|
+
export function resolveDockerConfig(
|
|
422
|
+
config: GkmConfig,
|
|
423
|
+
): Required<Omit<DockerConfig, 'compose'>> & Pick<DockerConfig, 'compose'> {
|
|
424
|
+
const docker = config.docker ?? {};
|
|
425
|
+
|
|
426
|
+
// Try to get image name from package.json name
|
|
427
|
+
let defaultImageName = 'api';
|
|
428
|
+
try {
|
|
429
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
430
|
+
const pkg = require(`${process.cwd()}/package.json`);
|
|
431
|
+
if (pkg.name) {
|
|
432
|
+
// Remove scope and use just the package name
|
|
433
|
+
defaultImageName = pkg.name.replace(/^@[^/]+\//, '');
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
// Ignore if package.json doesn't exist
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
registry: docker.registry ?? '',
|
|
441
|
+
imageName: docker.imageName ?? defaultImageName,
|
|
442
|
+
baseImage: docker.baseImage ?? 'node:22-alpine',
|
|
443
|
+
port: docker.port ?? 3000,
|
|
444
|
+
compose: docker.compose,
|
|
445
|
+
};
|
|
446
|
+
}
|