@geekmidas/cli 1.10.36 → 1.10.37
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/CHANGELOG.md +10 -0
- package/dist/{config-U-gojtxn.mjs → config-Bbu1JKvf.mjs} +2 -2
- package/dist/{config-U-gojtxn.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
- package/dist/{config-QnuOcYXp.cjs → config-DujV8DbR.cjs} +2 -2
- package/dist/{config-QnuOcYXp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
- package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
- package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
- package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
- package/dist/{index-D7iT4dnv.d.mts → index-BBvGMG_A.d.mts} +124 -67
- package/dist/index-BBvGMG_A.d.mts.map +1 -0
- package/dist/{index-DRQq26DF.d.cts → index-Dz2a7xQU.d.cts} +124 -67
- package/dist/index-Dz2a7xQU.d.cts.map +1 -0
- package/dist/index.cjs +244 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +244 -91
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Bb4UEyZN.cjs → openapi-0JUzU5Tv.cjs} +9 -2
- package/dist/{openapi-Bb4UEyZN.cjs.map → openapi-0JUzU5Tv.cjs.map} +1 -1
- package/dist/{openapi-BA0e3I_s.mjs → openapi-C5AycKTR.mjs} +10 -3
- package/dist/{openapi-BA0e3I_s.mjs.map → openapi-C5AycKTR.mjs.map} +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.mjs +3 -3
- package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
- package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
- package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
- package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-BobDOIG9.cjs → workspace-CQTT7HJQ.cjs} +34 -25
- package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
- package/dist/{workspace-Cgmvgwh8.mjs → workspace-D-37Ylqo.mjs} +34 -25
- package/dist/workspace-D-37Ylqo.mjs.map +1 -0
- package/docs/dev-server-resilience-design.md +189 -0
- package/package.json +5 -5
- package/src/build/__tests__/workspace-build.spec.ts +7 -7
- package/src/build/index.ts +25 -9
- package/src/credentials/__tests__/helpers.ts +1 -1
- package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
- package/src/deploy/__tests__/domain.spec.ts +12 -12
- package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
- package/src/deploy/__tests__/index.spec.ts +14 -14
- package/src/deploy/__tests__/sniffer.spec.ts +9 -9
- package/src/deploy/domain.ts +19 -11
- package/src/deploy/index.ts +12 -1
- package/src/deploy/sniffer.ts +1 -1
- package/src/deploy/types.ts +1 -1
- package/src/dev/__tests__/index.spec.ts +11 -11
- package/src/dev/index.ts +14 -8
- package/src/docker/__tests__/compose.spec.ts +4 -4
- package/src/docker/compose.ts +5 -3
- package/src/docker/index.ts +34 -6
- package/src/docker/templates.ts +138 -0
- package/src/generators/OpenApiTsGenerator.ts +22 -0
- package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
- package/src/init/__tests__/init.spec.ts +1 -1
- package/src/init/generators/monorepo.ts +3 -3
- package/src/secrets/__tests__/reconcile.spec.ts +1 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
- package/src/setup/fullstack-secrets.ts +7 -1
- package/src/setup/index.ts +2 -1
- package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
- package/src/test/__tests__/web.spec.ts +2 -2
- package/src/workspace/__tests__/client-generator.spec.ts +6 -6
- package/src/workspace/__tests__/index.spec.ts +4 -4
- package/src/workspace/__tests__/schema.spec.ts +27 -30
- package/src/workspace/__tests__/type-inference.spec.ts +5 -5
- package/src/workspace/client-generator.ts +10 -7
- package/src/workspace/index.ts +1 -1
- package/src/workspace/publicEnv.ts +2 -4
- package/src/workspace/schema.ts +126 -27
- package/src/workspace/types.ts +40 -69
- package/dist/index-D7iT4dnv.d.mts.map +0 -1
- package/dist/index-DRQq26DF.d.cts.map +0 -1
- package/dist/workspace-BobDOIG9.cjs.map +0 -1
- package/dist/workspace-Cgmvgwh8.mjs.map +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Dev Server Resilience Design
|
|
2
|
+
|
|
3
|
+
Status: parked / not yet started
|
|
4
|
+
Owner: TBD
|
|
5
|
+
Scope: `packages/cli/src/dev/index.ts`, `packages/cli/src/generators/Generator.ts`
|
|
6
|
+
|
|
7
|
+
## Background
|
|
8
|
+
|
|
9
|
+
Running `gkm dev` against a real workspace (e.g. `rezgo`: api, auth, web, app/Expo)
|
|
10
|
+
exposes three classes of failure. None are caused by user code; they are
|
|
11
|
+
limitations of the current dev orchestrator and hot-reload implementation.
|
|
12
|
+
|
|
13
|
+
## Issue 1 — Hot reload misses new files and serves stale transitive modules
|
|
14
|
+
|
|
15
|
+
### Symptoms
|
|
16
|
+
|
|
17
|
+
- Adding a new route file does not trigger a rebuild.
|
|
18
|
+
- After adding a new export to a non-route file, route files that import it
|
|
19
|
+
fail with:
|
|
20
|
+
`SyntaxError: The requested module '~/.../foo.ts' does not provide an export named 'bar'`
|
|
21
|
+
- Some changes "just don't get picked up" until the dev server is restarted.
|
|
22
|
+
|
|
23
|
+
### Root causes
|
|
24
|
+
|
|
25
|
+
1. **`add`/`unlink` events are ignored.** The chokidar watcher in `dev/index.ts`
|
|
26
|
+
only listens for `change`:
|
|
27
|
+
```ts
|
|
28
|
+
watcher.on('change', async (path) => { ... });
|
|
29
|
+
```
|
|
30
|
+
New or deleted files never trigger a rebuild.
|
|
31
|
+
|
|
32
|
+
2. **Cache-bust is shallow.** `Generator.ts:load()` appends `?t=<now>` only to
|
|
33
|
+
the top-level globbed route file URL. Node's ESM cache is keyed by URL, so
|
|
34
|
+
any *transitive* import (services, domain modules, env, etc.) is served from
|
|
35
|
+
cache forever within the CLI process. When a transitive module is edited,
|
|
36
|
+
the route file is re-evaluated against the stale cached version → the
|
|
37
|
+
"does not provide an export named" error above.
|
|
38
|
+
|
|
39
|
+
3. **Watch patterns are too narrow.** Only `routes`, `functions`, `crons`,
|
|
40
|
+
`subscribers` (plus envParser / logger / hooks files) are watched. Edits to
|
|
41
|
+
`src/domain/**`, `src/services/**`, etc. don't fire any rebuild even though
|
|
42
|
+
those files are pulled in transitively at openapi/build time.
|
|
43
|
+
|
|
44
|
+
### Proposed fix
|
|
45
|
+
|
|
46
|
+
- Subscribe to `add` and `unlink` on the watcher and re-resolve the glob set
|
|
47
|
+
before rebuilding.
|
|
48
|
+
- Broaden the watch pattern to cover the app's `src/**/*.{ts,tsx}` (configurable),
|
|
49
|
+
not just construct entry directories.
|
|
50
|
+
- Bust transitive caches. Two viable approaches:
|
|
51
|
+
- **Worker thread per rebuild** (preferred): move `EndpointGenerator.load` /
|
|
52
|
+
`FunctionGenerator.load` / etc. into a short-lived `worker_threads` worker
|
|
53
|
+
that gets recreated on each rebuild. Each worker has a fresh module graph,
|
|
54
|
+
so transitive imports are guaranteed fresh. Cost is one worker spawn per
|
|
55
|
+
rebuild (~50-100ms), but avoids the unbounded-memory leak of accumulating
|
|
56
|
+
`?t=N` URLs in the main process.
|
|
57
|
+
- **Custom loader hook**: install a Node loader that rewrites every resolved
|
|
58
|
+
URL to include the current rebuild generation. Cheaper per rebuild but
|
|
59
|
+
fragile (loader compatibility with tsx, source maps, etc.).
|
|
60
|
+
|
|
61
|
+
Worker thread is the recommended path.
|
|
62
|
+
|
|
63
|
+
## Issue 2 — Ctrl+C orphans `tsx`/`node` processes
|
|
64
|
+
|
|
65
|
+
### Symptoms
|
|
66
|
+
|
|
67
|
+
After pressing Ctrl+C in a workspace `gkm dev`, the shell returns but
|
|
68
|
+
`tsx` and `node` processes remain alive. Workaround is `killall node`.
|
|
69
|
+
|
|
70
|
+
### Process chain in workspace mode
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
shell
|
|
74
|
+
└── gkm dev (top-level, workspace)
|
|
75
|
+
└── pnpm turbo run dev [spawned with detached: true]
|
|
76
|
+
├── @rezgo/api: gkm dev (per-app)
|
|
77
|
+
│ └── tsx [spawned with detached: true]
|
|
78
|
+
│ └── node (Hono server)
|
|
79
|
+
├── @rezgo/auth: ...
|
|
80
|
+
└── @rezgo/app: ...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Root cause
|
|
84
|
+
|
|
85
|
+
The per-app `gkm dev` spawns `tsx` with `detached: true`
|
|
86
|
+
(`dev/index.ts:1521`). That moves tsx into a *new process group* outside the
|
|
87
|
+
turbo subtree. The workspace shutdown handler only sends SIGTERM/SIGKILL to
|
|
88
|
+
turbo's pgid; tsx's pgid is never targeted. After SIGKILL hits turbo's group,
|
|
89
|
+
the per-app gkm processes die *before* their async shutdown finishes calling
|
|
90
|
+
`process.kill(-tsxPgid, 'SIGKILL')`. tsx and its node child are orphaned.
|
|
91
|
+
|
|
92
|
+
The `detached: true` flag exists because the user's running Hono server may
|
|
93
|
+
ignore SIGTERM, so we need to kill its whole group. The design conflict is:
|
|
94
|
+
detaching tsx is necessary to forcibly kill it, but it also escapes the
|
|
95
|
+
top-level reachability tree.
|
|
96
|
+
|
|
97
|
+
### Proposed fix
|
|
98
|
+
|
|
99
|
+
Track every tsx PID centrally:
|
|
100
|
+
|
|
101
|
+
- Each `DevServer` registers its `tsx` pid into a shared `Set<number>` at the
|
|
102
|
+
workspace level (or writes to a state file under `.gkm/`).
|
|
103
|
+
- Workspace shutdown iterates that set and issues `process.kill(-pid, 'SIGKILL')`
|
|
104
|
+
for each known tsx pgid *before* falling back to the turbo-pgid SIGKILL.
|
|
105
|
+
- As belt-and-suspenders, drain the registry on per-app `gkm dev` exit so dead
|
|
106
|
+
pids are not retargeted.
|
|
107
|
+
|
|
108
|
+
This makes the kill reachable regardless of process-group escape.
|
|
109
|
+
|
|
110
|
+
## Issue 3 — One app's crash brings down the whole workspace
|
|
111
|
+
|
|
112
|
+
### Symptoms
|
|
113
|
+
|
|
114
|
+
`@rezgo/app` (Expo) crashed with a Metro `DependencyGraph._onHasteChange`
|
|
115
|
+
TypeError. Within seconds, `@rezgo/api`, `@rezgo/auth`, `@rezgo/web` all
|
|
116
|
+
received SIGTERM and shut down. The user lost 21 minutes of warmed dev state.
|
|
117
|
+
|
|
118
|
+
### Root causes
|
|
119
|
+
|
|
120
|
+
1. **Turbo's persistent-task semantics.** When a persistent task in
|
|
121
|
+
`turbo run dev` exits non-zero, turbo cancels sibling persistent tasks. There
|
|
122
|
+
is no flag (as of turbo 2.x) that keeps survivors running through a sibling
|
|
123
|
+
crash. `--continue` only affects scheduling of *queued* tasks, not running
|
|
124
|
+
persistent ones.
|
|
125
|
+
2. **gkm's orchestrator treats turbo's non-zero exit as fatal.** In
|
|
126
|
+
`workspaceDevCommand`:
|
|
127
|
+
```ts
|
|
128
|
+
turboProcess.on('exit', (code) => {
|
|
129
|
+
if (code !== null && code !== 0) {
|
|
130
|
+
reject(new Error(`Turbo exited with code ${code}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
The reject fires the shutdown handler, which kills any survivors that
|
|
135
|
+
turbo hadn't already torn down.
|
|
136
|
+
|
|
137
|
+
### Proposed fix
|
|
138
|
+
|
|
139
|
+
Stop using `turbo run dev` for the persistent dev loop. Build an in-process
|
|
140
|
+
supervisor inside `workspaceDevCommand`:
|
|
141
|
+
|
|
142
|
+
- Resolve dependency order via the existing `getAppBuildOrder` (we still use
|
|
143
|
+
turbo's task graph indirectly via workspace config).
|
|
144
|
+
- For each app, `spawn` its dev command directly (`pnpm dev` in that app's
|
|
145
|
+
cwd, or for backend apps invoke the per-app gkm dev codepath directly without
|
|
146
|
+
going through pnpm/turbo).
|
|
147
|
+
- Supervise each child:
|
|
148
|
+
- `on('exit', code)`: log the crash, do **not** propagate, schedule a restart
|
|
149
|
+
with exponential backoff (start at 1s, cap at 30s, reset after a 60s clean
|
|
150
|
+
run).
|
|
151
|
+
- Stream stdout/stderr to the parent with an app-name prefix so output stays
|
|
152
|
+
readable.
|
|
153
|
+
- Centralize the PID registry from Issue 2 here.
|
|
154
|
+
- Ctrl+C handler: iterate registry, SIGKILL each child group, then exit.
|
|
155
|
+
|
|
156
|
+
Turbo remains the build-time orchestrator (`gkm build`, dependency-ordered
|
|
157
|
+
package builds). Only the dev persistent loop is moved off it.
|
|
158
|
+
|
|
159
|
+
### Why not fix turbo's behavior instead
|
|
160
|
+
|
|
161
|
+
Tried first. `--continue=dependencies-successful` and friends don't apply to
|
|
162
|
+
sibling persistent tasks. Filing it upstream is fine, but the dev experience
|
|
163
|
+
needs to be fixed now and the supervisor pattern is also what makes Issues 1
|
|
164
|
+
and 2 solvable cleanly.
|
|
165
|
+
|
|
166
|
+
## Implementation order (when unparked)
|
|
167
|
+
|
|
168
|
+
1. Land the in-process supervisor (Issue 3 fix). This gives us a single owner
|
|
169
|
+
of the dev process tree.
|
|
170
|
+
2. Move tsx PID registration into the supervisor (Issue 2 fix).
|
|
171
|
+
3. Move hot-reload (worker thread + broadened watcher + `add`/`unlink`) into
|
|
172
|
+
the per-app branch of the supervisor (Issue 1 fix).
|
|
173
|
+
|
|
174
|
+
Each step is shippable independently. Order matters: 1 unblocks the cleanest
|
|
175
|
+
form of 2 and 3.
|
|
176
|
+
|
|
177
|
+
## Out of scope
|
|
178
|
+
|
|
179
|
+
- Replacing `tsx` (e.g. with `node --experimental-strip-types`).
|
|
180
|
+
- Native Bun runtime for dev — separate decision.
|
|
181
|
+
- Replacing chokidar.
|
|
182
|
+
|
|
183
|
+
## Open questions
|
|
184
|
+
|
|
185
|
+
- Should the supervisor expose a `--no-restart` flag for CI-like local runs?
|
|
186
|
+
- Restart backoff thresholds — should they be configurable per app in
|
|
187
|
+
workspace config, or fixed?
|
|
188
|
+
- Should we surface a small status line ("3 of 4 apps running") when one app
|
|
189
|
+
is dead but the rest are healthy?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.37",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"prompts": "~2.4.2",
|
|
57
57
|
"tsx": "~4.20.3",
|
|
58
58
|
"yaml": "~2.8.2",
|
|
59
|
-
"@geekmidas/constructs": "~3.0.
|
|
60
|
-
"@geekmidas/errors": "~1.0.0",
|
|
61
|
-
"@geekmidas/schema": "~1.0.0",
|
|
59
|
+
"@geekmidas/constructs": "~3.0.10",
|
|
62
60
|
"@geekmidas/envkit": "~1.0.6",
|
|
63
|
-
"@geekmidas/
|
|
61
|
+
"@geekmidas/errors": "~1.0.0",
|
|
62
|
+
"@geekmidas/logger": "~1.0.1",
|
|
63
|
+
"@geekmidas/schema": "~1.0.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -103,13 +103,13 @@ describe('Workspace Build Command', () => {
|
|
|
103
103
|
it('should have correct structure for failed build', () => {
|
|
104
104
|
const result: AppBuildResult = {
|
|
105
105
|
appName: 'web',
|
|
106
|
-
type: '
|
|
106
|
+
type: 'web',
|
|
107
107
|
success: false,
|
|
108
108
|
error: 'Build failed',
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
expect(result.appName).toBe('web');
|
|
112
|
-
expect(result.type).toBe('
|
|
112
|
+
expect(result.type).toBe('web');
|
|
113
113
|
expect(result.success).toBe(false);
|
|
114
114
|
expect(result.error).toBe('Build failed');
|
|
115
115
|
expect(result.outputPath).toBeUndefined();
|
|
@@ -137,14 +137,14 @@ describe('Workspace Build Command', () => {
|
|
|
137
137
|
routes: './src/**/*.ts',
|
|
138
138
|
},
|
|
139
139
|
web: {
|
|
140
|
-
type: '
|
|
140
|
+
type: 'web',
|
|
141
141
|
path: 'apps/web',
|
|
142
142
|
port: 3002,
|
|
143
143
|
dependencies: ['api', 'auth'],
|
|
144
144
|
framework: 'nextjs',
|
|
145
145
|
},
|
|
146
146
|
admin: {
|
|
147
|
-
type: '
|
|
147
|
+
type: 'web',
|
|
148
148
|
path: 'apps/admin',
|
|
149
149
|
port: 3003,
|
|
150
150
|
dependencies: ['api'],
|
|
@@ -159,7 +159,7 @@ describe('Workspace Build Command', () => {
|
|
|
159
159
|
|
|
160
160
|
const apps = Object.entries(workspace.apps);
|
|
161
161
|
const backendApps = apps.filter(([, app]) => app.type === 'backend');
|
|
162
|
-
const frontendApps = apps.filter(([, app]) => app.type === '
|
|
162
|
+
const frontendApps = apps.filter(([, app]) => app.type === 'web');
|
|
163
163
|
|
|
164
164
|
expect(backendApps).toHaveLength(2);
|
|
165
165
|
expect(frontendApps).toHaveLength(2);
|
|
@@ -181,7 +181,7 @@ describe('Workspace Build Command', () => {
|
|
|
181
181
|
dependencies: [],
|
|
182
182
|
},
|
|
183
183
|
web: {
|
|
184
|
-
type: '
|
|
184
|
+
type: 'web',
|
|
185
185
|
path: 'apps/web',
|
|
186
186
|
port: 3001,
|
|
187
187
|
dependencies: [],
|
|
@@ -207,7 +207,7 @@ describe('Workspace Build Command', () => {
|
|
|
207
207
|
const frontendOutputPath = join(
|
|
208
208
|
workspace.root,
|
|
209
209
|
frontendApp.path,
|
|
210
|
-
frontendApp.type === '
|
|
210
|
+
frontendApp.type === 'web' ? '.next' : '.gkm',
|
|
211
211
|
);
|
|
212
212
|
expect(frontendOutputPath).toBe('/workspace/apps/web/.next');
|
|
213
213
|
});
|
package/src/build/index.ts
CHANGED
|
@@ -362,7 +362,7 @@ async function buildForProvider(
|
|
|
362
362
|
*/
|
|
363
363
|
export interface AppBuildResult {
|
|
364
364
|
appName: string;
|
|
365
|
-
type: 'backend' | '
|
|
365
|
+
type: 'backend' | 'web' | 'mobile';
|
|
366
366
|
success: boolean;
|
|
367
367
|
outputPath?: string;
|
|
368
368
|
error?: string;
|
|
@@ -415,7 +415,7 @@ export async function workspaceBuildCommand(
|
|
|
415
415
|
const results: AppBuildResult[] = [];
|
|
416
416
|
const apps = Object.entries(workspace.apps);
|
|
417
417
|
const backendApps = apps.filter(([, app]) => app.type === 'backend');
|
|
418
|
-
const frontendApps = apps.filter(([, app]) => app.type === '
|
|
418
|
+
const frontendApps = apps.filter(([, app]) => app.type === 'web');
|
|
419
419
|
|
|
420
420
|
logger.log(`\n🏗️ Building workspace: ${workspace.name}`);
|
|
421
421
|
logger.log(
|
|
@@ -486,7 +486,12 @@ export async function workspaceBuildCommand(
|
|
|
486
486
|
// Summary
|
|
487
487
|
logger.log(`\n📋 Build Summary:`);
|
|
488
488
|
for (const result of results) {
|
|
489
|
-
const icon =
|
|
489
|
+
const icon =
|
|
490
|
+
result.type === 'backend'
|
|
491
|
+
? '⚙️'
|
|
492
|
+
: result.type === 'mobile'
|
|
493
|
+
? '📱'
|
|
494
|
+
: '🌐';
|
|
490
495
|
logger.log(
|
|
491
496
|
` ${icon} ${result.appName}: ${result.outputPath || 'built'}`,
|
|
492
497
|
);
|
|
@@ -522,13 +527,24 @@ function getAppOutputPath(
|
|
|
522
527
|
): string {
|
|
523
528
|
const appPath = join(workspace.root, app.path);
|
|
524
529
|
|
|
525
|
-
if (app.type === '
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
530
|
+
if (app.type === 'mobile') {
|
|
531
|
+
// Mobile builds are produced by the framework's own toolchain
|
|
532
|
+
// (e.g. EAS Build) — no local output path.
|
|
533
|
+
return '';
|
|
534
|
+
}
|
|
535
|
+
if (app.type === 'web') {
|
|
536
|
+
switch (app.framework) {
|
|
537
|
+
case 'vite':
|
|
538
|
+
case 'tanstack-start':
|
|
539
|
+
return join(appPath, 'dist');
|
|
540
|
+
case 'remix':
|
|
541
|
+
return join(appPath, 'build');
|
|
542
|
+
default:
|
|
543
|
+
return join(appPath, '.next');
|
|
544
|
+
}
|
|
531
545
|
}
|
|
546
|
+
// Backend .gkm output
|
|
547
|
+
return join(appPath, '.gkm');
|
|
532
548
|
}
|
|
533
549
|
|
|
534
550
|
/**
|
|
@@ -76,7 +76,7 @@ export function createWorkspaceConfig(
|
|
|
76
76
|
const deps = app.dependencies
|
|
77
77
|
? `dependencies: [${app.dependencies.map((d) => `'${d}'`).join(', ')}],`
|
|
78
78
|
: '';
|
|
79
|
-
if (app.type === '
|
|
79
|
+
if (app.type === 'web') {
|
|
80
80
|
const framework = app.framework ?? 'nextjs';
|
|
81
81
|
return ` ${name}: {
|
|
82
82
|
type: '${app.type}',
|
|
@@ -55,7 +55,7 @@ describe('resolveHost', () => {
|
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('should return base domain for main frontend app', () => {
|
|
58
|
-
const app = createApp({ type: '
|
|
58
|
+
const app = createApp({ type: 'web' });
|
|
59
59
|
const host = resolveHost('web', app, 'production', dokployConfig, true);
|
|
60
60
|
expect(host).toBe('myapp.com');
|
|
61
61
|
});
|
|
@@ -100,7 +100,7 @@ describe('resolveHost', () => {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
describe('isMainFrontendApp', () => {
|
|
103
|
-
const createApp = (type: 'backend' | '
|
|
103
|
+
const createApp = (type: 'backend' | 'web'): NormalizedAppConfig => ({
|
|
104
104
|
type,
|
|
105
105
|
path: 'apps/test',
|
|
106
106
|
port: 3000,
|
|
@@ -111,7 +111,7 @@ describe('isMainFrontendApp', () => {
|
|
|
111
111
|
it('should return false for backend apps', () => {
|
|
112
112
|
const apps = {
|
|
113
113
|
api: createApp('backend'),
|
|
114
|
-
web: createApp('
|
|
114
|
+
web: createApp('web'),
|
|
115
115
|
};
|
|
116
116
|
expect(isMainFrontendApp('api', apps.api, apps)).toBe(false);
|
|
117
117
|
});
|
|
@@ -119,8 +119,8 @@ describe('isMainFrontendApp', () => {
|
|
|
119
119
|
it('should return true for app named "web" if it is frontend', () => {
|
|
120
120
|
const apps = {
|
|
121
121
|
api: createApp('backend'),
|
|
122
|
-
web: createApp('
|
|
123
|
-
admin: createApp('
|
|
122
|
+
web: createApp('web'),
|
|
123
|
+
admin: createApp('web'),
|
|
124
124
|
};
|
|
125
125
|
expect(isMainFrontendApp('web', apps.web, apps)).toBe(true);
|
|
126
126
|
});
|
|
@@ -128,8 +128,8 @@ describe('isMainFrontendApp', () => {
|
|
|
128
128
|
it('should return true for first frontend app when no "web" app', () => {
|
|
129
129
|
const apps = {
|
|
130
130
|
api: createApp('backend'),
|
|
131
|
-
dashboard: createApp('
|
|
132
|
-
admin: createApp('
|
|
131
|
+
dashboard: createApp('web'),
|
|
132
|
+
admin: createApp('web'),
|
|
133
133
|
};
|
|
134
134
|
expect(isMainFrontendApp('dashboard', apps.dashboard, apps)).toBe(true);
|
|
135
135
|
expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
|
|
@@ -138,22 +138,22 @@ describe('isMainFrontendApp', () => {
|
|
|
138
138
|
it('should return false for non-first frontend when no "web" app', () => {
|
|
139
139
|
const apps = {
|
|
140
140
|
api: createApp('backend'),
|
|
141
|
-
dashboard: createApp('
|
|
142
|
-
admin: createApp('
|
|
141
|
+
dashboard: createApp('web'),
|
|
142
|
+
admin: createApp('web'),
|
|
143
143
|
};
|
|
144
144
|
expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
it('should return false when no frontend apps in allApps (edge case)', () => {
|
|
148
148
|
// Edge case: checking a frontend app against an empty allApps
|
|
149
|
-
const frontendApp = createApp('
|
|
149
|
+
const frontendApp = createApp('web');
|
|
150
150
|
expect(isMainFrontendApp('myapp', frontendApp, {})).toBe(false);
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
describe('generatePublicUrlBuildArgs', () => {
|
|
155
155
|
const createApp = (dependencies: string[]): NormalizedAppConfig => ({
|
|
156
|
-
type: '
|
|
156
|
+
type: 'web',
|
|
157
157
|
path: 'apps/web',
|
|
158
158
|
port: 3001,
|
|
159
159
|
dependencies,
|
|
@@ -214,7 +214,7 @@ describe('generatePublicUrlBuildArgs', () => {
|
|
|
214
214
|
|
|
215
215
|
describe('getPublicUrlArgNames', () => {
|
|
216
216
|
const createApp = (dependencies: string[]): NormalizedAppConfig => ({
|
|
217
|
-
type: '
|
|
217
|
+
type: 'web',
|
|
218
218
|
path: 'apps/web',
|
|
219
219
|
port: 3001,
|
|
220
220
|
dependencies,
|
|
@@ -430,7 +430,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
430
430
|
resolvedDeployTarget: 'dokploy',
|
|
431
431
|
},
|
|
432
432
|
web: {
|
|
433
|
-
type: '
|
|
433
|
+
type: 'web',
|
|
434
434
|
path: 'apps/web',
|
|
435
435
|
port: 3001,
|
|
436
436
|
dependencies: ['api'],
|
|
@@ -527,7 +527,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
527
527
|
resolvedDeployTarget: 'dokploy',
|
|
528
528
|
},
|
|
529
529
|
web: {
|
|
530
|
-
type: '
|
|
530
|
+
type: 'web',
|
|
531
531
|
path: 'apps/web',
|
|
532
532
|
port: 3002,
|
|
533
533
|
dependencies: ['api', 'auth'],
|
|
@@ -566,7 +566,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
566
566
|
resolvedDeployTarget: 'dokploy',
|
|
567
567
|
},
|
|
568
568
|
web: {
|
|
569
|
-
type: '
|
|
569
|
+
type: 'web',
|
|
570
570
|
path: 'apps/web',
|
|
571
571
|
port: 3002,
|
|
572
572
|
dependencies: ['api'],
|
|
@@ -614,7 +614,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
614
614
|
};
|
|
615
615
|
|
|
616
616
|
const app = {
|
|
617
|
-
type: '
|
|
617
|
+
type: 'web' as const,
|
|
618
618
|
path: 'apps/web',
|
|
619
619
|
port: 3000,
|
|
620
620
|
dependencies: ['api', 'auth'],
|
|
@@ -645,7 +645,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
645
645
|
};
|
|
646
646
|
|
|
647
647
|
const app = {
|
|
648
|
-
type: '
|
|
648
|
+
type: 'web' as const,
|
|
649
649
|
path: 'apps/web',
|
|
650
650
|
port: 3000,
|
|
651
651
|
dependencies: ['api', 'auth'], // wants both api and auth
|
|
@@ -719,7 +719,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
719
719
|
it('should inject DATABASE_URL for backend apps', () => {
|
|
720
720
|
const workspaceName = 'test-workspace';
|
|
721
721
|
const hasPostgres = true;
|
|
722
|
-
const appType: 'backend' | '
|
|
722
|
+
const appType: 'backend' | 'web' = 'backend';
|
|
723
723
|
|
|
724
724
|
const envVars: string[] = [];
|
|
725
725
|
|
|
@@ -735,7 +735,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
735
735
|
|
|
736
736
|
it('should not inject DATABASE_URL for frontend apps', () => {
|
|
737
737
|
const hasPostgres = true;
|
|
738
|
-
const appType = '
|
|
738
|
+
const appType = 'web' as 'backend' | 'web';
|
|
739
739
|
|
|
740
740
|
const envVars: string[] = [];
|
|
741
741
|
|
|
@@ -794,7 +794,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
794
794
|
it('should have correct structure for failed AppDeployResult', () => {
|
|
795
795
|
const failedResult = {
|
|
796
796
|
appName: 'web',
|
|
797
|
-
type: '
|
|
797
|
+
type: 'web' as const,
|
|
798
798
|
success: false,
|
|
799
799
|
error: 'Build failed',
|
|
800
800
|
};
|
|
@@ -809,7 +809,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
809
809
|
{ appName: 'api', type: 'backend' as const, success: true },
|
|
810
810
|
{
|
|
811
811
|
appName: 'web',
|
|
812
|
-
type: '
|
|
812
|
+
type: 'web' as const,
|
|
813
813
|
success: false,
|
|
814
814
|
error: 'Failed',
|
|
815
815
|
},
|
|
@@ -834,7 +834,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
834
834
|
};
|
|
835
835
|
|
|
836
836
|
const webApp = {
|
|
837
|
-
type: '
|
|
837
|
+
type: 'web' as const,
|
|
838
838
|
path: 'apps/web',
|
|
839
839
|
port: 3000,
|
|
840
840
|
dependencies: ['api', 'auth'],
|
|
@@ -873,7 +873,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
873
873
|
};
|
|
874
874
|
|
|
875
875
|
const webApp = {
|
|
876
|
-
type: '
|
|
876
|
+
type: 'web' as const,
|
|
877
877
|
path: 'apps/web',
|
|
878
878
|
port: 3001,
|
|
879
879
|
dependencies: ['api', 'payments'],
|
|
@@ -922,7 +922,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
922
922
|
};
|
|
923
923
|
|
|
924
924
|
const webApp = {
|
|
925
|
-
type: '
|
|
925
|
+
type: 'web' as const,
|
|
926
926
|
path: 'apps/web',
|
|
927
927
|
port: 3001,
|
|
928
928
|
dependencies: ['api', 'auth'],
|
|
@@ -997,7 +997,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
997
997
|
|
|
998
998
|
// Step 2: Deploy web (depends on api)
|
|
999
999
|
const webApp = {
|
|
1000
|
-
type: '
|
|
1000
|
+
type: 'web' as const,
|
|
1001
1001
|
path: 'apps/web',
|
|
1002
1002
|
port: 3001,
|
|
1003
1003
|
dependencies: ['api'],
|
|
@@ -1048,7 +1048,7 @@ describe('workspaceDeployCommand', () => {
|
|
|
1048
1048
|
};
|
|
1049
1049
|
|
|
1050
1050
|
const webApp = {
|
|
1051
|
-
type: '
|
|
1051
|
+
type: 'web' as const,
|
|
1052
1052
|
path: 'apps/web',
|
|
1053
1053
|
port: 3000,
|
|
1054
1054
|
dependencies: ['api', 'auth', 'notifications'],
|
|
@@ -32,7 +32,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
32
32
|
|
|
33
33
|
describe('frontend apps', () => {
|
|
34
34
|
it('should return empty env vars for frontend apps with no dependencies', async () => {
|
|
35
|
-
const app = createApp({ type: '
|
|
35
|
+
const app = createApp({ type: 'web', dependencies: [] });
|
|
36
36
|
|
|
37
37
|
const result = await sniffAppEnvironment(app, 'web', workspacePath);
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
42
42
|
|
|
43
43
|
it('should return NEXT_PUBLIC_{DEP}_URL for frontend dependencies', async () => {
|
|
44
44
|
const app = createApp({
|
|
45
|
-
type: '
|
|
45
|
+
type: 'web',
|
|
46
46
|
dependencies: ['api', 'auth'],
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
56
56
|
|
|
57
57
|
it('should generate uppercase dep names in NEXT_PUBLIC_{DEP}_URL', async () => {
|
|
58
58
|
const app = createApp({
|
|
59
|
-
type: '
|
|
59
|
+
type: 'web',
|
|
60
60
|
dependencies: ['payments-service', 'notification_api'],
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -73,7 +73,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
73
73
|
describe('config sniffing', () => {
|
|
74
74
|
it('should sniff env vars from config.client path', async () => {
|
|
75
75
|
const app = createApp({
|
|
76
|
-
type: '
|
|
76
|
+
type: 'web',
|
|
77
77
|
path: fixturesPath,
|
|
78
78
|
dependencies: [],
|
|
79
79
|
config: {
|
|
@@ -90,7 +90,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
90
90
|
|
|
91
91
|
it('should sniff env vars from config.server path', async () => {
|
|
92
92
|
const app = createApp({
|
|
93
|
-
type: '
|
|
93
|
+
type: 'web',
|
|
94
94
|
path: fixturesPath,
|
|
95
95
|
dependencies: [],
|
|
96
96
|
config: {
|
|
@@ -107,7 +107,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
107
107
|
|
|
108
108
|
it('should combine vars from both config.client and config.server', async () => {
|
|
109
109
|
const app = createApp({
|
|
110
|
-
type: '
|
|
110
|
+
type: 'web',
|
|
111
111
|
path: fixturesPath,
|
|
112
112
|
dependencies: ['api'],
|
|
113
113
|
config: {
|
|
@@ -129,7 +129,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
129
129
|
|
|
130
130
|
it('should deduplicate vars from both config files', async () => {
|
|
131
131
|
const app = createApp({
|
|
132
|
-
type: '
|
|
132
|
+
type: 'web',
|
|
133
133
|
path: fixturesPath,
|
|
134
134
|
dependencies: [],
|
|
135
135
|
config: {
|
|
@@ -306,7 +306,7 @@ describe('sniffAppEnvironment', () => {
|
|
|
306
306
|
|
|
307
307
|
it('returns empty optionalEnvVars for frontend apps', async () => {
|
|
308
308
|
const app = createApp({
|
|
309
|
-
type: '
|
|
309
|
+
type: 'web',
|
|
310
310
|
dependencies: ['api'],
|
|
311
311
|
});
|
|
312
312
|
|
|
@@ -339,7 +339,7 @@ describe('sniffAllApps', () => {
|
|
|
339
339
|
// No entry, routes, or envParser - will return empty
|
|
340
340
|
},
|
|
341
341
|
web: {
|
|
342
|
-
type: '
|
|
342
|
+
type: 'web',
|
|
343
343
|
path: 'apps/web',
|
|
344
344
|
port: 3001,
|
|
345
345
|
dependencies: ['api', 'auth'],
|