@geekmidas/cli 0.18.0 → 0.20.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/dist/{bundler-C74EKlNa.cjs → bundler-CyHg1v_T.cjs} +3 -3
- package/dist/{bundler-C74EKlNa.cjs.map → bundler-CyHg1v_T.cjs.map} +1 -1
- package/dist/{bundler-B6z6HEeh.mjs → bundler-DQIuE3Kn.mjs} +3 -3
- package/dist/{bundler-B6z6HEeh.mjs.map → bundler-DQIuE3Kn.mjs.map} +1 -1
- package/dist/{config-DYULeEv8.mjs → config-BaYqrF3n.mjs} +48 -10
- package/dist/config-BaYqrF3n.mjs.map +1 -0
- package/dist/{config-AmInkU7k.cjs → config-CxrLu8ia.cjs} +53 -9
- package/dist/config-CxrLu8ia.cjs.map +1 -0
- package/dist/config.cjs +4 -1
- package/dist/config.d.cts +27 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +27 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -2
- package/dist/dokploy-api-B0w17y4_.mjs +3 -0
- package/dist/{dokploy-api-CaETb2L6.mjs → dokploy-api-B9qR2Yn1.mjs} +1 -1
- package/dist/{dokploy-api-CaETb2L6.mjs.map → dokploy-api-B9qR2Yn1.mjs.map} +1 -1
- package/dist/dokploy-api-BnGeUqN4.cjs +3 -0
- package/dist/{dokploy-api-C7F9VykY.cjs → dokploy-api-C5czOZoc.cjs} +1 -1
- package/dist/{dokploy-api-C7F9VykY.cjs.map → dokploy-api-C5czOZoc.cjs.map} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs → encryption-BAz0xQ1Q.cjs} +1 -1
- package/dist/{encryption-D7Efcdi9.cjs.map → encryption-BAz0xQ1Q.cjs.map} +1 -1
- package/dist/{encryption-h4Nb6W-M.mjs → encryption-JtMsiGNp.mjs} +2 -2
- package/dist/{encryption-h4Nb6W-M.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/index-CWN-bgrO.d.mts +495 -0
- package/dist/index-CWN-bgrO.d.mts.map +1 -0
- package/dist/index-DEWYvYvg.d.cts +495 -0
- package/dist/index-DEWYvYvg.d.cts.map +1 -0
- package/dist/index.cjs +2640 -564
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2635 -564
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZVcfxk-.mjs → openapi-CgqR6Jkw.mjs} +3 -3
- package/dist/{openapi-CZVcfxk-.mjs.map → openapi-CgqR6Jkw.mjs.map} +1 -1
- package/dist/{openapi-C89hhkZC.cjs → openapi-DfpxS0xv.cjs} +8 -2
- package/dist/{openapi-C89hhkZC.cjs.map → openapi-DfpxS0xv.cjs.map} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs → openapi-react-query-5rSortLH.mjs} +1 -1
- package/dist/{openapi-react-query-CM2_qlW9.mjs.map → openapi-react-query-5rSortLH.mjs.map} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs → openapi-react-query-DvNpdDpM.cjs} +1 -1
- package/dist/{openapi-react-query-iKjfLzff.cjs.map → openapi-react-query-DvNpdDpM.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -2
- package/dist/{storage-Bn3K9Ccu.cjs → storage-BPRgh3DU.cjs} +136 -5
- package/dist/storage-BPRgh3DU.cjs.map +1 -0
- package/dist/{storage-nkGIjeXt.mjs → storage-DNj_I11J.mjs} +1 -1
- package/dist/storage-Dhst7BhI.mjs +272 -0
- package/dist/storage-Dhst7BhI.mjs.map +1 -0
- package/dist/{storage-UfyTn7Zm.cjs → storage-fOR8dMu5.cjs} +1 -1
- package/dist/{types-iFk5ms7y.d.mts → types-K2uQJ-FO.d.mts} +2 -2
- package/dist/{types-BgaMXsUa.d.cts.map → types-K2uQJ-FO.d.mts.map} +1 -1
- package/dist/{types-BgaMXsUa.d.cts → types-l53qUmGt.d.cts} +2 -2
- package/dist/{types-iFk5ms7y.d.mts.map → types-l53qUmGt.d.cts.map} +1 -1
- package/dist/workspace/index.cjs +19 -0
- package/dist/workspace/index.d.cts +3 -0
- package/dist/workspace/index.d.mts +3 -0
- package/dist/workspace/index.mjs +3 -0
- package/dist/workspace-CPLEZDZf.mjs +3788 -0
- package/dist/workspace-CPLEZDZf.mjs.map +1 -0
- package/dist/workspace-iWgBlX6h.cjs +3885 -0
- package/dist/workspace-iWgBlX6h.cjs.map +1 -0
- package/package.json +9 -4
- package/src/build/__tests__/workspace-build.spec.ts +215 -0
- package/src/build/index.ts +189 -1
- package/src/config.ts +71 -14
- package/src/deploy/__tests__/docker.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +305 -1
- package/src/deploy/index.ts +426 -4
- package/src/deploy/types.ts +32 -0
- package/src/dev/__tests__/index.spec.ts +572 -1
- package/src/dev/index.ts +582 -2
- package/src/docker/__tests__/compose.spec.ts +425 -0
- package/src/docker/__tests__/templates.spec.ts +145 -0
- package/src/docker/compose.ts +248 -0
- package/src/docker/index.ts +159 -3
- package/src/docker/templates.ts +219 -4
- package/src/index.ts +24 -0
- package/src/init/__tests__/generators.spec.ts +17 -24
- package/src/init/__tests__/init.spec.ts +157 -5
- package/src/init/generators/auth.ts +220 -0
- package/src/init/generators/config.ts +61 -4
- package/src/init/generators/docker.ts +115 -8
- package/src/init/generators/env.ts +7 -127
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -1
- package/src/init/generators/monorepo.ts +154 -10
- package/src/init/generators/package.ts +5 -3
- package/src/init/generators/web.ts +213 -0
- package/src/init/index.ts +290 -58
- package/src/init/templates/api.ts +38 -29
- package/src/init/templates/index.ts +132 -4
- package/src/init/templates/minimal.ts +33 -35
- package/src/init/templates/serverless.ts +16 -19
- package/src/init/templates/worker.ts +50 -25
- package/src/init/versions.ts +47 -0
- package/src/secrets/keystore.ts +144 -0
- package/src/secrets/storage.ts +109 -6
- package/src/test/index.ts +97 -0
- package/src/workspace/__tests__/client-generator.spec.ts +357 -0
- package/src/workspace/__tests__/index.spec.ts +543 -0
- package/src/workspace/__tests__/schema.spec.ts +519 -0
- package/src/workspace/__tests__/type-inference.spec.ts +251 -0
- package/src/workspace/client-generator.ts +307 -0
- package/src/workspace/index.ts +372 -0
- package/src/workspace/schema.ts +368 -0
- package/src/workspace/types.ts +336 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsdown.config.ts +1 -0
- package/dist/config-AmInkU7k.cjs.map +0 -1
- package/dist/config-DYULeEv8.mjs.map +0 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +0 -3
- package/dist/dokploy-api-DHvfmWbi.mjs +0 -3
- package/dist/storage-BaOP55oq.mjs +0 -147
- package/dist/storage-BaOP55oq.mjs.map +0 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"import": "./dist/config.mjs",
|
|
16
16
|
"require": "./dist/config.cjs"
|
|
17
17
|
},
|
|
18
|
+
"./workspace": {
|
|
19
|
+
"types": "./dist/workspace/index.d.ts",
|
|
20
|
+
"import": "./dist/workspace/index.mjs",
|
|
21
|
+
"require": "./dist/workspace/index.cjs"
|
|
22
|
+
},
|
|
18
23
|
"./openapi": {
|
|
19
24
|
"types": "./dist/openapi.d.ts",
|
|
20
25
|
"import": "./dist/openapi.mjs",
|
|
@@ -53,11 +58,11 @@
|
|
|
53
58
|
"@geekmidas/testkit": "0.6.0"
|
|
54
59
|
},
|
|
55
60
|
"peerDependencies": {
|
|
56
|
-
"@geekmidas/constructs": "~0.6.0",
|
|
57
61
|
"@geekmidas/envkit": "~0.4.0",
|
|
62
|
+
"@geekmidas/logger": "~0.4.0",
|
|
63
|
+
"@geekmidas/constructs": "~0.6.0",
|
|
58
64
|
"@geekmidas/schema": "~0.1.0",
|
|
59
|
-
"@geekmidas/telescope": "~0.4.0"
|
|
60
|
-
"@geekmidas/logger": "~0.4.0"
|
|
65
|
+
"@geekmidas/telescope": "~0.4.0"
|
|
61
66
|
},
|
|
62
67
|
"peerDependenciesMeta": {
|
|
63
68
|
"@geekmidas/telescope": {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import type { NormalizedWorkspace } from '../../workspace/types.js';
|
|
6
|
+
import {
|
|
7
|
+
type AppBuildResult,
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
getTurboCommand,
|
|
10
|
+
} from '../index.js';
|
|
11
|
+
|
|
12
|
+
describe('Workspace Build Command', () => {
|
|
13
|
+
describe('detectPackageManager', () => {
|
|
14
|
+
let testDir: string;
|
|
15
|
+
let originalCwd: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
testDir = join(
|
|
19
|
+
tmpdir(),
|
|
20
|
+
`gkm-build-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
21
|
+
);
|
|
22
|
+
mkdirSync(testDir, { recursive: true });
|
|
23
|
+
originalCwd = process.cwd();
|
|
24
|
+
process.chdir(testDir);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
process.chdir(originalCwd);
|
|
29
|
+
if (testDir) {
|
|
30
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should detect pnpm from lock file', () => {
|
|
35
|
+
writeFileSync(join(testDir, 'pnpm-lock.yaml'), '');
|
|
36
|
+
expect(detectPackageManager()).toBe('pnpm');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should detect yarn from lock file', () => {
|
|
40
|
+
writeFileSync(join(testDir, 'yarn.lock'), '');
|
|
41
|
+
expect(detectPackageManager()).toBe('yarn');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should default to npm when no lock file exists', () => {
|
|
45
|
+
expect(detectPackageManager()).toBe('npm');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should prefer pnpm over yarn when both exist', () => {
|
|
49
|
+
writeFileSync(join(testDir, 'pnpm-lock.yaml'), '');
|
|
50
|
+
writeFileSync(join(testDir, 'yarn.lock'), '');
|
|
51
|
+
expect(detectPackageManager()).toBe('pnpm');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getTurboCommand', () => {
|
|
56
|
+
it('should generate pnpm turbo command', () => {
|
|
57
|
+
expect(getTurboCommand('pnpm')).toBe('pnpm exec turbo run build');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should generate yarn turbo command', () => {
|
|
61
|
+
expect(getTurboCommand('yarn')).toBe('yarn turbo run build');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should generate npm turbo command', () => {
|
|
65
|
+
expect(getTurboCommand('npm')).toBe('npx turbo run build');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should add filter argument when provided', () => {
|
|
69
|
+
expect(getTurboCommand('pnpm', 'api')).toBe(
|
|
70
|
+
'pnpm exec turbo run build --filter=api',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should add filter for yarn', () => {
|
|
75
|
+
expect(getTurboCommand('yarn', 'web')).toBe(
|
|
76
|
+
'yarn turbo run build --filter=web',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should add filter for npm', () => {
|
|
81
|
+
expect(getTurboCommand('npm', '@myapp/api')).toBe(
|
|
82
|
+
'npx turbo run build --filter=@myapp/api',
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('AppBuildResult type', () => {
|
|
88
|
+
it('should have correct structure for successful build', () => {
|
|
89
|
+
const result: AppBuildResult = {
|
|
90
|
+
appName: 'api',
|
|
91
|
+
type: 'backend',
|
|
92
|
+
success: true,
|
|
93
|
+
outputPath: '/path/to/.gkm',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
expect(result.appName).toBe('api');
|
|
97
|
+
expect(result.type).toBe('backend');
|
|
98
|
+
expect(result.success).toBe(true);
|
|
99
|
+
expect(result.outputPath).toBe('/path/to/.gkm');
|
|
100
|
+
expect(result.error).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should have correct structure for failed build', () => {
|
|
104
|
+
const result: AppBuildResult = {
|
|
105
|
+
appName: 'web',
|
|
106
|
+
type: 'frontend',
|
|
107
|
+
success: false,
|
|
108
|
+
error: 'Build failed',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
expect(result.appName).toBe('web');
|
|
112
|
+
expect(result.type).toBe('frontend');
|
|
113
|
+
expect(result.success).toBe(false);
|
|
114
|
+
expect(result.error).toBe('Build failed');
|
|
115
|
+
expect(result.outputPath).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('workspace build integration', () => {
|
|
120
|
+
it('should correctly categorize apps by type', () => {
|
|
121
|
+
const workspace: NormalizedWorkspace = {
|
|
122
|
+
name: 'test-workspace',
|
|
123
|
+
root: '/test',
|
|
124
|
+
apps: {
|
|
125
|
+
api: {
|
|
126
|
+
type: 'backend',
|
|
127
|
+
path: 'apps/api',
|
|
128
|
+
port: 3000,
|
|
129
|
+
dependencies: [],
|
|
130
|
+
routes: './src/**/*.ts',
|
|
131
|
+
},
|
|
132
|
+
auth: {
|
|
133
|
+
type: 'backend',
|
|
134
|
+
path: 'apps/auth',
|
|
135
|
+
port: 3001,
|
|
136
|
+
dependencies: [],
|
|
137
|
+
routes: './src/**/*.ts',
|
|
138
|
+
},
|
|
139
|
+
web: {
|
|
140
|
+
type: 'frontend',
|
|
141
|
+
path: 'apps/web',
|
|
142
|
+
port: 3002,
|
|
143
|
+
dependencies: ['api', 'auth'],
|
|
144
|
+
framework: 'nextjs',
|
|
145
|
+
},
|
|
146
|
+
admin: {
|
|
147
|
+
type: 'frontend',
|
|
148
|
+
path: 'apps/admin',
|
|
149
|
+
port: 3003,
|
|
150
|
+
dependencies: ['api'],
|
|
151
|
+
framework: 'nextjs',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
services: {},
|
|
155
|
+
deploy: { default: 'dokploy' },
|
|
156
|
+
shared: { packages: [] },
|
|
157
|
+
secrets: {},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const apps = Object.entries(workspace.apps);
|
|
161
|
+
const backendApps = apps.filter(([, app]) => app.type === 'backend');
|
|
162
|
+
const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
|
|
163
|
+
|
|
164
|
+
expect(backendApps).toHaveLength(2);
|
|
165
|
+
expect(frontendApps).toHaveLength(2);
|
|
166
|
+
expect(backendApps.map(([name]) => name)).toContain('api');
|
|
167
|
+
expect(backendApps.map(([name]) => name)).toContain('auth');
|
|
168
|
+
expect(frontendApps.map(([name]) => name)).toContain('web');
|
|
169
|
+
expect(frontendApps.map(([name]) => name)).toContain('admin');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should generate correct output paths for different app types', () => {
|
|
173
|
+
const workspace: NormalizedWorkspace = {
|
|
174
|
+
name: 'test',
|
|
175
|
+
root: '/workspace',
|
|
176
|
+
apps: {
|
|
177
|
+
api: {
|
|
178
|
+
type: 'backend',
|
|
179
|
+
path: 'apps/api',
|
|
180
|
+
port: 3000,
|
|
181
|
+
dependencies: [],
|
|
182
|
+
},
|
|
183
|
+
web: {
|
|
184
|
+
type: 'frontend',
|
|
185
|
+
path: 'apps/web',
|
|
186
|
+
port: 3001,
|
|
187
|
+
dependencies: [],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
services: {},
|
|
191
|
+
deploy: { default: 'dokploy' },
|
|
192
|
+
shared: { packages: [] },
|
|
193
|
+
secrets: {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Test that backend apps output to .gkm
|
|
197
|
+
const backendApp = workspace.apps.api!;
|
|
198
|
+
const backendOutputPath = join(
|
|
199
|
+
workspace.root,
|
|
200
|
+
backendApp.path,
|
|
201
|
+
backendApp.type === 'backend' ? '.gkm' : '.next',
|
|
202
|
+
);
|
|
203
|
+
expect(backendOutputPath).toBe('/workspace/apps/api/.gkm');
|
|
204
|
+
|
|
205
|
+
// Test that frontend apps output to .next
|
|
206
|
+
const frontendApp = workspace.apps.web!;
|
|
207
|
+
const frontendOutputPath = join(
|
|
208
|
+
workspace.root,
|
|
209
|
+
frontendApp.path,
|
|
210
|
+
frontendApp.type === 'frontend' ? '.next' : '.gkm',
|
|
211
|
+
);
|
|
212
|
+
expect(frontendOutputPath).toBe('/workspace/apps/web/.next');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
package/src/build/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
1
3
|
import { mkdir } from 'node:fs/promises';
|
|
2
4
|
import { join, relative } from 'node:path';
|
|
3
5
|
import type { Cron } from '@geekmidas/constructs/crons';
|
|
4
6
|
import type { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
5
7
|
import type { Function } from '@geekmidas/constructs/functions';
|
|
6
8
|
import type { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
7
|
-
import { loadConfig, parseModuleConfig } from '../config';
|
|
9
|
+
import { loadConfig, loadWorkspaceConfig, parseModuleConfig } from '../config';
|
|
8
10
|
import {
|
|
9
11
|
getProductionConfigFromGkm,
|
|
10
12
|
normalizeHooksConfig,
|
|
@@ -25,6 +27,11 @@ import type {
|
|
|
25
27
|
LegacyProvider,
|
|
26
28
|
RouteInfo,
|
|
27
29
|
} from '../types';
|
|
30
|
+
import {
|
|
31
|
+
getAppBuildOrder,
|
|
32
|
+
type NormalizedAppConfig,
|
|
33
|
+
type NormalizedWorkspace,
|
|
34
|
+
} from '../workspace/index.js';
|
|
28
35
|
import {
|
|
29
36
|
generateAwsManifest,
|
|
30
37
|
generateServerManifest,
|
|
@@ -38,6 +45,16 @@ const logger = console;
|
|
|
38
45
|
export async function buildCommand(
|
|
39
46
|
options: BuildOptions,
|
|
40
47
|
): Promise<BuildResult> {
|
|
48
|
+
// Load config with workspace detection
|
|
49
|
+
const loadedConfig = await loadWorkspaceConfig();
|
|
50
|
+
|
|
51
|
+
// Route to workspace build mode for multi-app workspaces
|
|
52
|
+
if (loadedConfig.type === 'workspace') {
|
|
53
|
+
logger.log('📦 Detected workspace configuration');
|
|
54
|
+
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Single-app build - use existing logic
|
|
41
58
|
const config = await loadConfig();
|
|
42
59
|
|
|
43
60
|
// Resolve providers from new config format
|
|
@@ -299,3 +316,174 @@ async function buildForProvider(
|
|
|
299
316
|
|
|
300
317
|
return {};
|
|
301
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Result of building a single app in a workspace.
|
|
322
|
+
*/
|
|
323
|
+
export interface AppBuildResult {
|
|
324
|
+
appName: string;
|
|
325
|
+
type: 'backend' | 'frontend';
|
|
326
|
+
success: boolean;
|
|
327
|
+
outputPath?: string;
|
|
328
|
+
error?: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Result of workspace build command.
|
|
333
|
+
*/
|
|
334
|
+
export interface WorkspaceBuildResult extends BuildResult {
|
|
335
|
+
apps: AppBuildResult[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Detect available package manager.
|
|
340
|
+
* @internal Exported for testing
|
|
341
|
+
*/
|
|
342
|
+
export function detectPackageManager(): 'pnpm' | 'npm' | 'yarn' {
|
|
343
|
+
if (existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
344
|
+
if (existsSync('yarn.lock')) return 'yarn';
|
|
345
|
+
return 'npm';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get the turbo command for running builds.
|
|
350
|
+
* @internal Exported for testing
|
|
351
|
+
*/
|
|
352
|
+
export function getTurboCommand(
|
|
353
|
+
pm: 'pnpm' | 'npm' | 'yarn',
|
|
354
|
+
filter?: string,
|
|
355
|
+
): string {
|
|
356
|
+
const filterArg = filter ? ` --filter=${filter}` : '';
|
|
357
|
+
switch (pm) {
|
|
358
|
+
case 'pnpm':
|
|
359
|
+
return `pnpm exec turbo run build${filterArg}`;
|
|
360
|
+
case 'yarn':
|
|
361
|
+
return `yarn turbo run build${filterArg}`;
|
|
362
|
+
case 'npm':
|
|
363
|
+
return `npx turbo run build${filterArg}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Build all apps in a workspace using Turbo for dependency-ordered parallel builds.
|
|
369
|
+
* @internal Exported for testing
|
|
370
|
+
*/
|
|
371
|
+
export async function workspaceBuildCommand(
|
|
372
|
+
workspace: NormalizedWorkspace,
|
|
373
|
+
options: BuildOptions,
|
|
374
|
+
): Promise<WorkspaceBuildResult> {
|
|
375
|
+
const results: AppBuildResult[] = [];
|
|
376
|
+
const apps = Object.entries(workspace.apps);
|
|
377
|
+
const backendApps = apps.filter(([, app]) => app.type === 'backend');
|
|
378
|
+
const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
|
|
379
|
+
|
|
380
|
+
logger.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
381
|
+
logger.log(
|
|
382
|
+
` Backend apps: ${backendApps.map(([name]) => name).join(', ') || 'none'}`,
|
|
383
|
+
);
|
|
384
|
+
logger.log(
|
|
385
|
+
` Frontend apps: ${frontendApps.map(([name]) => name).join(', ') || 'none'}`,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (options.production) {
|
|
389
|
+
logger.log(` 🏭 Production mode enabled`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Get build order (topologically sorted by dependencies)
|
|
393
|
+
const buildOrder = getAppBuildOrder(workspace);
|
|
394
|
+
logger.log(` Build order: ${buildOrder.join(' → ')}`);
|
|
395
|
+
|
|
396
|
+
// Use Turbo for parallel builds with dependency awareness
|
|
397
|
+
const pm = detectPackageManager();
|
|
398
|
+
logger.log(`\n📦 Using ${pm} with Turbo for parallel builds...\n`);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// Run turbo build which handles dependency ordering and parallelization
|
|
402
|
+
const turboCommand = getTurboCommand(pm);
|
|
403
|
+
logger.log(`Running: ${turboCommand}`);
|
|
404
|
+
|
|
405
|
+
await new Promise<void>((resolve, reject) => {
|
|
406
|
+
const child = spawn(turboCommand, {
|
|
407
|
+
shell: true,
|
|
408
|
+
cwd: workspace.root,
|
|
409
|
+
stdio: 'inherit',
|
|
410
|
+
env: {
|
|
411
|
+
...process.env,
|
|
412
|
+
// Pass production flag to builds
|
|
413
|
+
NODE_ENV: options.production ? 'production' : 'development',
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
child.on('close', (code) => {
|
|
418
|
+
if (code === 0) {
|
|
419
|
+
resolve();
|
|
420
|
+
} else {
|
|
421
|
+
reject(new Error(`Turbo build failed with exit code ${code}`));
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
child.on('error', (err) => {
|
|
426
|
+
reject(err);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Mark all apps as successful
|
|
431
|
+
for (const [appName, app] of apps) {
|
|
432
|
+
const outputPath = getAppOutputPath(workspace, appName, app);
|
|
433
|
+
results.push({
|
|
434
|
+
appName,
|
|
435
|
+
type: app.type,
|
|
436
|
+
success: true,
|
|
437
|
+
outputPath,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
logger.log(`\n✅ Workspace build complete!`);
|
|
442
|
+
|
|
443
|
+
// Summary
|
|
444
|
+
logger.log(`\n📋 Build Summary:`);
|
|
445
|
+
for (const result of results) {
|
|
446
|
+
const icon = result.type === 'backend' ? '⚙️' : '🌐';
|
|
447
|
+
logger.log(
|
|
448
|
+
` ${icon} ${result.appName}: ${result.outputPath || 'built'}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const errorMessage =
|
|
453
|
+
error instanceof Error ? error.message : 'Build failed';
|
|
454
|
+
logger.log(`\n❌ Build failed: ${errorMessage}`);
|
|
455
|
+
|
|
456
|
+
// Mark all apps as failed
|
|
457
|
+
for (const [appName, app] of apps) {
|
|
458
|
+
results.push({
|
|
459
|
+
appName,
|
|
460
|
+
type: app.type,
|
|
461
|
+
success: false,
|
|
462
|
+
error: errorMessage,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { apps: results };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get the output path for a built app.
|
|
474
|
+
*/
|
|
475
|
+
function getAppOutputPath(
|
|
476
|
+
workspace: NormalizedWorkspace,
|
|
477
|
+
_appName: string,
|
|
478
|
+
app: NormalizedAppConfig,
|
|
479
|
+
): string {
|
|
480
|
+
const appPath = join(workspace.root, app.path);
|
|
481
|
+
|
|
482
|
+
if (app.type === 'frontend') {
|
|
483
|
+
// Next.js standalone output
|
|
484
|
+
return join(appPath, '.next');
|
|
485
|
+
} else {
|
|
486
|
+
// Backend .gkm output
|
|
487
|
+
return join(appPath, '.gkm');
|
|
488
|
+
}
|
|
489
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type { GkmConfig } from './types.
|
|
3
|
+
import type { GkmConfig } from './types.js';
|
|
4
|
+
import {
|
|
5
|
+
isWorkspaceConfig,
|
|
6
|
+
type LoadedConfig,
|
|
7
|
+
processConfig,
|
|
8
|
+
type WorkspaceConfig,
|
|
9
|
+
} from './workspace/index.js';
|
|
4
10
|
|
|
5
|
-
export type { GkmConfig } from './types.
|
|
11
|
+
export type { GkmConfig } from './types.js';
|
|
12
|
+
export type { LoadedConfig, WorkspaceConfig } from './workspace/index.js';
|
|
13
|
+
export { defineWorkspace } from './workspace/index.js';
|
|
6
14
|
/**
|
|
7
15
|
* Define GKM configuration with full TypeScript support.
|
|
8
16
|
* This is an identity function that provides type safety and autocomplete.
|
|
@@ -62,32 +70,81 @@ export function parseModuleConfig(
|
|
|
62
70
|
return { path, importPattern };
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Find and return the path to the config file.
|
|
75
|
+
*/
|
|
76
|
+
function findConfigPath(cwd: string): string {
|
|
68
77
|
const files = ['gkm.config.json', 'gkm.config.ts', 'gkm.config.js'];
|
|
69
|
-
let configPath = '';
|
|
70
78
|
|
|
71
79
|
for (const file of files) {
|
|
72
80
|
const path = join(cwd, file);
|
|
73
81
|
if (existsSync(path)) {
|
|
74
|
-
|
|
75
|
-
break;
|
|
82
|
+
return path;
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
throw new Error(
|
|
87
|
+
'Configuration file not found. Please create gkm.config.json, gkm.config.ts, or gkm.config.js in the project root.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load raw configuration from file.
|
|
93
|
+
*/
|
|
94
|
+
async function loadRawConfig(
|
|
95
|
+
cwd: string,
|
|
96
|
+
): Promise<GkmConfig | WorkspaceConfig> {
|
|
97
|
+
const configPath = findConfigPath(cwd);
|
|
84
98
|
|
|
85
99
|
try {
|
|
86
100
|
const config = await import(configPath);
|
|
87
101
|
return config.default;
|
|
88
102
|
} catch (error) {
|
|
103
|
+
throw new Error(`Failed to load config: ${(error as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load configuration file (single-app format).
|
|
109
|
+
* For backwards compatibility with existing code.
|
|
110
|
+
*
|
|
111
|
+
* @deprecated Use loadWorkspaceConfig for new code
|
|
112
|
+
*/
|
|
113
|
+
export async function loadConfig(
|
|
114
|
+
cwd: string = process.cwd(),
|
|
115
|
+
): Promise<GkmConfig> {
|
|
116
|
+
const config = await loadRawConfig(cwd);
|
|
117
|
+
|
|
118
|
+
// If it's a workspace config, throw an error
|
|
119
|
+
if (isWorkspaceConfig(config)) {
|
|
89
120
|
throw new Error(
|
|
90
|
-
|
|
121
|
+
'Workspace configuration detected. Use loadWorkspaceConfig() instead.',
|
|
91
122
|
);
|
|
92
123
|
}
|
|
124
|
+
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Load configuration file and process it as a workspace.
|
|
130
|
+
* Works with both single-app and workspace configurations.
|
|
131
|
+
*
|
|
132
|
+
* Single-app configs are automatically wrapped as a workspace with one app.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const { type, workspace } = await loadWorkspaceConfig();
|
|
137
|
+
*
|
|
138
|
+
* if (type === 'workspace') {
|
|
139
|
+
* console.log('Multi-app workspace:', workspace.apps);
|
|
140
|
+
* } else {
|
|
141
|
+
* console.log('Single app wrapped as workspace');
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export async function loadWorkspaceConfig(
|
|
146
|
+
cwd: string = process.cwd(),
|
|
147
|
+
): Promise<LoadedConfig> {
|
|
148
|
+
const config = await loadRawConfig(cwd);
|
|
149
|
+
return processConfig(config, cwd);
|
|
93
150
|
}
|