@codemation/cli 0.0.5 → 0.0.11
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 +20 -26
- package/dist/{CliBin-C3ar49fj.js → CliBin-BAnFX1wL.js} +1105 -366
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +655 -207
- package/dist/index.js +1 -1
- package/package.json +14 -6
- package/src/CliProgramFactory.ts +23 -8
- package/src/Program.ts +7 -3
- package/src/bootstrap/CodemationCliApplicationSession.ts +17 -19
- package/src/commands/DevCommand.ts +203 -171
- package/src/commands/ServeWebCommand.ts +26 -1
- package/src/commands/ServeWorkerCommand.ts +46 -30
- package/src/commands/devCommandLifecycle.types.ts +7 -11
- package/src/database/ConsumerDatabaseConnectionResolver.ts +55 -9
- package/src/database/DatabaseMigrationsApplyService.ts +2 -2
- package/src/dev/Builder.ts +1 -14
- package/src/dev/CliDevProxyServer.ts +457 -0
- package/src/dev/CliDevProxyServerFactory.ts +10 -0
- package/src/dev/DevApiRuntimeFactory.ts +44 -0
- package/src/dev/DevApiRuntimeHost.ts +130 -0
- package/src/dev/DevApiRuntimeServer.ts +107 -0
- package/src/dev/DevApiRuntimeTypes.ts +24 -0
- package/src/dev/DevAuthSettingsLoader.ts +9 -3
- package/src/dev/DevBootstrapSummaryFetcher.ts +1 -1
- package/src/dev/DevHttpProbe.ts +2 -2
- package/src/dev/DevNextHostEnvironmentBuilder.ts +35 -5
- package/src/dev/DevRebuildQueue.ts +54 -0
- package/src/dev/DevRebuildQueueFactory.ts +7 -0
- package/src/dev/DevSessionPortsResolver.ts +2 -2
- package/src/dev/DevSessionServices.ts +0 -4
- package/src/dev/DevSourceChangeClassifier.ts +33 -13
- package/src/dev/ListenPortConflictDescriber.ts +83 -0
- package/src/dev/WatchRootsResolver.ts +6 -4
- package/src/runtime/NextHostConsumerServerCommandFactory.ts +11 -2
- package/src/user/CliDatabaseUrlDescriptor.ts +2 -2
- package/src/user/UserAdminCliBootstrap.ts +9 -21
- package/codemation-cli-0.0.3.tgz +0 -0
- package/src/dev/DevSourceRestartCoordinator.ts +0 -48
- package/src/dev/DevelopmentGatewayNotifier.ts +0 -35
- package/src/dev/RuntimeToolEntrypointResolver.ts +0 -47
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import type { Logger } from "@codemation/host/next/server";
|
|
2
|
-
import { CodemationPluginDiscovery } from "@codemation/host/server";
|
|
3
2
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
4
3
|
import { createRequire } from "node:module";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import process from "node:process";
|
|
7
6
|
|
|
8
7
|
import type { DatabaseMigrationsApplyService } from "../database/DatabaseMigrationsApplyService";
|
|
8
|
+
import type { DevApiRuntimeFactory, DevApiRuntimeServerHandle } from "../dev/DevApiRuntimeFactory";
|
|
9
9
|
import type { DevBootstrapSummaryFetcher } from "../dev/DevBootstrapSummaryFetcher";
|
|
10
|
+
import type { CliDevProxyServer } from "../dev/CliDevProxyServer";
|
|
11
|
+
import type { CliDevProxyServerFactory } from "../dev/CliDevProxyServerFactory";
|
|
10
12
|
import type { DevCliBannerRenderer } from "../dev/DevCliBannerRenderer";
|
|
11
13
|
import type { DevConsumerPublishBootstrap } from "../dev/DevConsumerPublishBootstrap";
|
|
12
14
|
import { ConsumerEnvDotenvFilePredicate } from "../dev/ConsumerEnvDotenvFilePredicate";
|
|
15
|
+
import type { DevRebuildQueueFactory } from "../dev/DevRebuildQueueFactory";
|
|
13
16
|
import type { DevSourceWatcher } from "../dev/DevSourceWatcher";
|
|
14
17
|
import { DevSessionServices } from "../dev/DevSessionServices";
|
|
15
18
|
import { DevLockFactory } from "../dev/Factory";
|
|
16
19
|
import { DevTrackedProcessTreeKiller } from "../dev/DevTrackedProcessTreeKiller";
|
|
17
20
|
import { DevSourceWatcherFactory } from "../dev/Runner";
|
|
21
|
+
import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
|
|
18
22
|
import { CliPathResolver, type CliPaths } from "../path/CliPathResolver";
|
|
19
|
-
import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
|
|
20
23
|
import { NextHostConsumerServerCommandFactory } from "../runtime/NextHostConsumerServerCommandFactory";
|
|
21
|
-
|
|
22
|
-
import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
|
|
24
|
+
import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
|
|
23
25
|
|
|
24
26
|
import type { DevMode, DevMutableProcessState, DevPreparedRuntime } from "./devCommandLifecycle.types";
|
|
25
27
|
|
|
@@ -28,7 +30,6 @@ export class DevCommand {
|
|
|
28
30
|
|
|
29
31
|
constructor(
|
|
30
32
|
private readonly pathResolver: CliPathResolver,
|
|
31
|
-
private readonly pluginDiscovery: CodemationPluginDiscovery,
|
|
32
33
|
private readonly tsRuntime: TypeScriptRuntimeConfigurator,
|
|
33
34
|
private readonly devLockFactory: DevLockFactory,
|
|
34
35
|
private readonly devSourceWatcherFactory: DevSourceWatcherFactory,
|
|
@@ -41,15 +42,18 @@ export class DevCommand {
|
|
|
41
42
|
private readonly consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate,
|
|
42
43
|
private readonly devTrackedProcessTreeKiller: DevTrackedProcessTreeKiller,
|
|
43
44
|
private readonly nextHostConsumerServerCommandFactory: NextHostConsumerServerCommandFactory,
|
|
45
|
+
private readonly devApiRuntimeFactory: DevApiRuntimeFactory,
|
|
46
|
+
private readonly cliDevProxyServerFactory: CliDevProxyServerFactory,
|
|
47
|
+
private readonly devRebuildQueueFactory: DevRebuildQueueFactory,
|
|
44
48
|
) {}
|
|
45
49
|
|
|
46
|
-
async execute(consumerRoot: string): Promise<void> {
|
|
47
|
-
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
50
|
+
async execute(args: Readonly<{ consumerRoot: string; watchFramework?: boolean }>): Promise<void> {
|
|
51
|
+
const paths = await this.pathResolver.resolve(args.consumerRoot);
|
|
48
52
|
this.devCliBannerRenderer.renderBrandHeader();
|
|
49
53
|
this.tsRuntime.configure(paths.repoRoot);
|
|
50
54
|
await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot);
|
|
51
55
|
await this.devConsumerPublishBootstrap.ensurePublished(paths);
|
|
52
|
-
const devMode = this.
|
|
56
|
+
const devMode = this.resolveDevMode(args);
|
|
53
57
|
const { nextPort, gatewayPort } = await this.session.sessionPorts.resolve({
|
|
54
58
|
devMode,
|
|
55
59
|
portEnv: process.env.PORT,
|
|
@@ -58,37 +62,42 @@ export class DevCommand {
|
|
|
58
62
|
const devLock = this.devLockFactory.create();
|
|
59
63
|
await devLock.acquire({
|
|
60
64
|
consumerRoot: paths.consumerRoot,
|
|
61
|
-
nextPort: devMode === "framework" ? nextPort : gatewayPort,
|
|
65
|
+
nextPort: devMode === "watch-framework" ? nextPort : gatewayPort,
|
|
62
66
|
});
|
|
63
67
|
const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
|
|
64
68
|
const watcher = this.devSourceWatcherFactory.create();
|
|
65
69
|
const processState = this.createInitialProcessState();
|
|
70
|
+
let proxyServer: CliDevProxyServer | null = null;
|
|
66
71
|
try {
|
|
67
72
|
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
68
73
|
const stopPromise = this.wireStopPromise(processState);
|
|
69
|
-
const uiProxyBase = await this.
|
|
74
|
+
const uiProxyBase = await this.startPackagedUiWhenNeeded(prepared, processState);
|
|
75
|
+
proxyServer = await this.startProxyServer(prepared.gatewayPort, uiProxyBase);
|
|
70
76
|
const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
|
|
71
|
-
await this.
|
|
77
|
+
await this.bootInitialRuntime(prepared, processState, proxyServer);
|
|
72
78
|
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
73
79
|
const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
74
80
|
if (initialSummary) {
|
|
75
81
|
this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
|
|
76
82
|
}
|
|
77
|
-
this.bindShutdownSignalsToChildProcesses(processState);
|
|
78
|
-
await this.
|
|
79
|
-
await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
|
|
80
|
-
this.
|
|
83
|
+
this.bindShutdownSignalsToChildProcesses(processState, proxyServer);
|
|
84
|
+
await this.spawnDevUiWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
85
|
+
await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl, proxyServer);
|
|
86
|
+
this.logPackagedUiDevHintWhenNeeded(devMode, gatewayPort);
|
|
81
87
|
await stopPromise;
|
|
82
88
|
} finally {
|
|
83
89
|
processState.stopRequested = true;
|
|
84
|
-
await this.
|
|
90
|
+
await this.stopLiveProcesses(processState, proxyServer);
|
|
85
91
|
await watcher.stop();
|
|
86
92
|
await devLock.release();
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
private
|
|
91
|
-
|
|
96
|
+
private resolveDevMode(args: Readonly<{ watchFramework?: boolean }>): DevMode {
|
|
97
|
+
if (args.watchFramework === true || process.env.CODEMATION_DEV_MODE === "framework") {
|
|
98
|
+
return "watch-framework";
|
|
99
|
+
}
|
|
100
|
+
return "packaged-ui";
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
private async prepareDevRuntime(
|
|
@@ -101,19 +110,7 @@ export class DevCommand {
|
|
|
101
110
|
const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(
|
|
102
111
|
process.env.CODEMATION_DEV_SERVER_TOKEN,
|
|
103
112
|
);
|
|
104
|
-
const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
105
|
-
packageName: "@codemation/dev-gateway",
|
|
106
|
-
repoRoot: paths.repoRoot,
|
|
107
|
-
sourceEntrypoint: "packages/dev-gateway/src/bin.ts",
|
|
108
|
-
});
|
|
109
|
-
const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
110
|
-
packageName: "@codemation/runtime-dev",
|
|
111
|
-
repoRoot: paths.repoRoot,
|
|
112
|
-
sourceEntrypoint: "packages/runtime-dev/src/bin.ts",
|
|
113
|
-
});
|
|
114
|
-
const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
|
|
115
113
|
const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
|
|
116
|
-
const discoveredPluginPackagesJson = JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot));
|
|
117
114
|
return {
|
|
118
115
|
paths,
|
|
119
116
|
devMode,
|
|
@@ -121,21 +118,17 @@ export class DevCommand {
|
|
|
121
118
|
gatewayPort,
|
|
122
119
|
authSettings,
|
|
123
120
|
developmentServerToken,
|
|
124
|
-
gatewayEntrypoint,
|
|
125
|
-
runtimeEntrypoint,
|
|
126
|
-
runtimeWorkingDirectory,
|
|
127
|
-
discoveredPluginPackagesJson,
|
|
128
121
|
consumerEnv,
|
|
129
122
|
};
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
private createInitialProcessState(): DevMutableProcessState {
|
|
133
126
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
currentDevUi: null,
|
|
128
|
+
currentPackagedUi: null,
|
|
129
|
+
currentPackagedUiBaseUrl: null,
|
|
130
|
+
currentRuntime: null,
|
|
131
|
+
isRestartingUi: false,
|
|
139
132
|
stopRequested: false,
|
|
140
133
|
stopResolve: null,
|
|
141
134
|
stopReject: null,
|
|
@@ -153,25 +146,22 @@ export class DevCommand {
|
|
|
153
146
|
return `http://127.0.0.1:${gatewayPort}`;
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
* Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
|
|
158
|
-
* Framework mode: no separate UI child (Next runs in dev later).
|
|
159
|
-
*/
|
|
160
|
-
private async startConsumerUiProxyWhenNeeded(
|
|
149
|
+
private async startPackagedUiWhenNeeded(
|
|
161
150
|
prepared: DevPreparedRuntime,
|
|
162
151
|
state: DevMutableProcessState,
|
|
163
152
|
): Promise<string> {
|
|
164
|
-
if (prepared.devMode !== "
|
|
153
|
+
if (prepared.devMode !== "packaged-ui") {
|
|
165
154
|
return "";
|
|
166
155
|
}
|
|
167
156
|
const websocketPort = prepared.gatewayPort;
|
|
168
|
-
const uiProxyBase =
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
const uiProxyBase =
|
|
158
|
+
state.currentPackagedUiBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
|
|
159
|
+
state.currentPackagedUiBaseUrl = uiProxyBase;
|
|
160
|
+
await this.spawnPackagedUi(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
|
|
171
161
|
return uiProxyBase;
|
|
172
162
|
}
|
|
173
163
|
|
|
174
|
-
private async
|
|
164
|
+
private async spawnPackagedUi(
|
|
175
165
|
prepared: DevPreparedRuntime,
|
|
176
166
|
state: DevMutableProcessState,
|
|
177
167
|
authSettings: DevResolvedAuthSettings,
|
|
@@ -200,77 +190,48 @@ export class DevCommand {
|
|
|
200
190
|
skipUiAuth: authSettings.skipUiAuth,
|
|
201
191
|
websocketPort,
|
|
202
192
|
});
|
|
203
|
-
state.
|
|
193
|
+
state.currentPackagedUi = spawn(nextHostCommand.command, nextHostCommand.args, {
|
|
204
194
|
cwd: nextHostCommand.cwd,
|
|
205
195
|
...this.devDetachedChildSpawnOptions(),
|
|
206
196
|
env: nextHostEnvironment,
|
|
207
197
|
});
|
|
208
|
-
state.
|
|
209
|
-
if (state.stopRequested || state.
|
|
198
|
+
state.currentPackagedUi.on("error", (error) => {
|
|
199
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
210
200
|
return;
|
|
211
201
|
}
|
|
212
202
|
state.stopRequested = true;
|
|
213
203
|
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
214
204
|
});
|
|
215
|
-
state.
|
|
216
|
-
if (state.stopRequested || state.
|
|
205
|
+
state.currentPackagedUi.on("exit", (code) => {
|
|
206
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
217
207
|
return;
|
|
218
208
|
}
|
|
219
209
|
state.stopRequested = true;
|
|
220
|
-
|
|
221
|
-
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
222
|
-
}
|
|
223
|
-
state.stopReject?.(new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
210
|
+
state.stopReject?.(new Error(`next start (packaged UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
224
211
|
});
|
|
225
212
|
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
226
213
|
}
|
|
227
214
|
|
|
228
|
-
private async
|
|
215
|
+
private async startProxyServer(gatewayPort: number, uiProxyBase: string): Promise<CliDevProxyServer> {
|
|
216
|
+
const proxyServer = this.cliDevProxyServerFactory.create(gatewayPort);
|
|
217
|
+
proxyServer.setUiProxyTarget(uiProxyBase.length > 0 ? uiProxyBase : null);
|
|
218
|
+
await proxyServer.start();
|
|
219
|
+
await this.session.devHttpProbe.waitUntilGatewayHealthy(this.gatewayBaseHttpUrl(gatewayPort));
|
|
220
|
+
return proxyServer;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async bootInitialRuntime(
|
|
229
224
|
prepared: DevPreparedRuntime,
|
|
230
225
|
state: DevMutableProcessState,
|
|
231
|
-
|
|
232
|
-
uiProxyBase: string,
|
|
226
|
+
proxyServer: CliDevProxyServer,
|
|
233
227
|
): Promise<void> {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
cwd: prepared.runtimeWorkingDirectory,
|
|
240
|
-
...this.devDetachedChildSpawnOptions(),
|
|
241
|
-
env: {
|
|
242
|
-
...gatewayProcessEnv,
|
|
243
|
-
...prepared.gatewayEntrypoint.env,
|
|
244
|
-
CODEMATION_DEV_GATEWAY_HTTP_PORT: String(prepared.gatewayPort),
|
|
245
|
-
CODEMATION_RUNTIME_CHILD_BIN: prepared.runtimeEntrypoint.command,
|
|
246
|
-
CODEMATION_RUNTIME_CHILD_ARGS_JSON: JSON.stringify(prepared.runtimeEntrypoint.args),
|
|
247
|
-
CODEMATION_RUNTIME_CHILD_ENV_JSON: JSON.stringify(prepared.runtimeEntrypoint.env),
|
|
248
|
-
CODEMATION_RUNTIME_CHILD_CWD: prepared.runtimeWorkingDirectory,
|
|
249
|
-
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
250
|
-
CODEMATION_DISCOVERED_PLUGIN_PACKAGES_JSON: prepared.discoveredPluginPackagesJson,
|
|
251
|
-
CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY: "true",
|
|
252
|
-
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
253
|
-
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
254
|
-
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
255
|
-
WS_NO_BUFFER_UTIL: "1",
|
|
256
|
-
WS_NO_UTF_8_VALIDATE: "1",
|
|
257
|
-
...(uiProxyBase.length > 0 ? { CODEMATION_DEV_UI_PROXY_TARGET: uiProxyBase } : {}),
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
state.currentGateway.on("error", (error) => {
|
|
261
|
-
if (!state.stopRequested) {
|
|
262
|
-
state.stopRequested = true;
|
|
263
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
state.currentGateway.on("exit", (code) => {
|
|
267
|
-
if (state.stopRequested) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
state.stopRequested = true;
|
|
271
|
-
state.stopReject?.(new Error(`codemation dev-gateway exited unexpectedly with code ${code ?? 0}.`));
|
|
228
|
+
const runtime = await this.createRuntime(prepared);
|
|
229
|
+
state.currentRuntime = runtime;
|
|
230
|
+
await proxyServer.activateRuntime({
|
|
231
|
+
httpPort: runtime.httpPort,
|
|
232
|
+
workflowWebSocketPort: runtime.workflowWebSocketPort,
|
|
272
233
|
});
|
|
273
|
-
|
|
234
|
+
proxyServer.setBuildStatus("idle");
|
|
274
235
|
}
|
|
275
236
|
|
|
276
237
|
private devDetachedChildSpawnOptions(): Readonly<{
|
|
@@ -283,7 +244,10 @@ export class DevCommand {
|
|
|
283
244
|
: { stdio: "inherit", detached: true };
|
|
284
245
|
}
|
|
285
246
|
|
|
286
|
-
private bindShutdownSignalsToChildProcesses(
|
|
247
|
+
private bindShutdownSignalsToChildProcesses(
|
|
248
|
+
state: DevMutableProcessState,
|
|
249
|
+
proxyServer: CliDevProxyServer | null,
|
|
250
|
+
): void {
|
|
287
251
|
let shutdownInProgress = false;
|
|
288
252
|
const runShutdown = async (): Promise<void> => {
|
|
289
253
|
if (shutdownInProgress) {
|
|
@@ -292,13 +256,7 @@ export class DevCommand {
|
|
|
292
256
|
shutdownInProgress = true;
|
|
293
257
|
state.stopRequested = true;
|
|
294
258
|
process.stdout.write("\n[codemation] Stopping..\n");
|
|
295
|
-
|
|
296
|
-
for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
|
|
297
|
-
if (child && child.exitCode === null && child.signalCode === null) {
|
|
298
|
-
children.push(child);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
259
|
+
await this.stopLiveProcesses(state, proxyServer);
|
|
302
260
|
process.stdout.write("[codemation] Stopped.\n");
|
|
303
261
|
state.stopResolve?.();
|
|
304
262
|
};
|
|
@@ -309,21 +267,18 @@ export class DevCommand {
|
|
|
309
267
|
}
|
|
310
268
|
}
|
|
311
269
|
|
|
312
|
-
|
|
313
|
-
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
314
|
-
*/
|
|
315
|
-
private async spawnFrameworkNextHostWhenNeeded(
|
|
270
|
+
private async spawnDevUiWhenNeeded(
|
|
316
271
|
prepared: DevPreparedRuntime,
|
|
317
272
|
state: DevMutableProcessState,
|
|
318
273
|
gatewayBaseUrl: string,
|
|
319
274
|
): Promise<void> {
|
|
320
|
-
if (prepared.devMode !== "framework") {
|
|
275
|
+
if (prepared.devMode !== "watch-framework") {
|
|
321
276
|
return;
|
|
322
277
|
}
|
|
323
|
-
await this.
|
|
278
|
+
await this.spawnDevUi(prepared, state, gatewayBaseUrl, prepared.authSettings);
|
|
324
279
|
}
|
|
325
280
|
|
|
326
|
-
private async
|
|
281
|
+
private async spawnDevUi(
|
|
327
282
|
prepared: DevPreparedRuntime,
|
|
328
283
|
state: DevMutableProcessState,
|
|
329
284
|
gatewayBaseUrl: string,
|
|
@@ -341,29 +296,26 @@ export class DevCommand {
|
|
|
341
296
|
websocketPort,
|
|
342
297
|
runtimeDevUrl: gatewayBaseUrl,
|
|
343
298
|
});
|
|
344
|
-
state.
|
|
299
|
+
state.currentDevUi = spawn("pnpm", ["exec", "next", "dev"], {
|
|
345
300
|
cwd: nextHostRoot,
|
|
346
301
|
...this.devDetachedChildSpawnOptions(),
|
|
347
302
|
env: nextHostEnvironment,
|
|
348
303
|
});
|
|
349
|
-
state.
|
|
304
|
+
state.currentDevUi.on("exit", (code) => {
|
|
350
305
|
const normalizedCode = code ?? 0;
|
|
351
|
-
if (state.stopRequested || state.
|
|
306
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
352
307
|
return;
|
|
353
308
|
}
|
|
354
309
|
if (normalizedCode === 0) {
|
|
355
310
|
state.stopRequested = true;
|
|
356
|
-
if (state.currentGateway?.exitCode === null) {
|
|
357
|
-
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
358
|
-
}
|
|
359
311
|
state.stopResolve?.();
|
|
360
312
|
return;
|
|
361
313
|
}
|
|
362
314
|
state.stopRequested = true;
|
|
363
315
|
state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
|
|
364
316
|
});
|
|
365
|
-
state.
|
|
366
|
-
if (state.stopRequested || state.
|
|
317
|
+
state.currentDevUi.on("error", (error) => {
|
|
318
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
367
319
|
return;
|
|
368
320
|
}
|
|
369
321
|
state.stopRequested = true;
|
|
@@ -378,7 +330,13 @@ export class DevCommand {
|
|
|
378
330
|
watcher: DevSourceWatcher,
|
|
379
331
|
devMode: DevMode,
|
|
380
332
|
gatewayBaseUrl: string,
|
|
333
|
+
proxyServer: CliDevProxyServer,
|
|
381
334
|
): Promise<void> {
|
|
335
|
+
const rebuildQueue = this.devRebuildQueueFactory.create({
|
|
336
|
+
run: async (request) => {
|
|
337
|
+
await this.runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
382
340
|
await watcher.start({
|
|
383
341
|
roots: this.session.watchRootsResolver.resolve({
|
|
384
342
|
consumerRoot: prepared.paths.consumerRoot,
|
|
@@ -388,7 +346,7 @@ export class DevCommand {
|
|
|
388
346
|
onChange: async ({ changedPaths }) => {
|
|
389
347
|
if (changedPaths.length > 0 && changedPaths.every((p) => this.consumerEnvDotenvFilePredicate.matches(p))) {
|
|
390
348
|
process.stdout.write(
|
|
391
|
-
"\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the
|
|
349
|
+
"\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the runtime picks up updated variables (host `process.env` does not hot-reload).\n",
|
|
392
350
|
);
|
|
393
351
|
return;
|
|
394
352
|
}
|
|
@@ -397,113 +355,187 @@ export class DevCommand {
|
|
|
397
355
|
changedPaths,
|
|
398
356
|
consumerRoot: prepared.paths.consumerRoot,
|
|
399
357
|
});
|
|
400
|
-
const
|
|
358
|
+
const shouldRestartUi = this.session.sourceChangeClassifier.requiresUiRestart({
|
|
401
359
|
changedPaths,
|
|
402
360
|
consumerRoot: prepared.paths.consumerRoot,
|
|
403
361
|
});
|
|
404
362
|
process.stdout.write(
|
|
405
|
-
|
|
406
|
-
? "\n[codemation]
|
|
407
|
-
: "\n[codemation] Source change detected — rebuilding consumer and restarting runtime…\n",
|
|
363
|
+
shouldRestartUi
|
|
364
|
+
? "\n[codemation] Source change detected — rebuilding consumer, restarting the runtime, and restarting the UI…\n"
|
|
365
|
+
: "\n[codemation] Source change detected — rebuilding consumer and restarting the runtime…\n",
|
|
408
366
|
);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
prepared.developmentServerToken,
|
|
415
|
-
);
|
|
416
|
-
process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
417
|
-
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
418
|
-
if (shouldRestartNextHost) {
|
|
419
|
-
await this.restartNextHostForConfigChange(prepared, state, gatewayBaseUrl);
|
|
420
|
-
}
|
|
421
|
-
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
422
|
-
if (json) {
|
|
423
|
-
this.devCliBannerRenderer.renderCompact(json);
|
|
424
|
-
}
|
|
425
|
-
process.stdout.write("[codemation] Runtime ready.\n");
|
|
367
|
+
await rebuildQueue.enqueue({
|
|
368
|
+
changedPaths,
|
|
369
|
+
shouldRepublishConsumerOutput,
|
|
370
|
+
shouldRestartUi,
|
|
371
|
+
});
|
|
426
372
|
} catch (error) {
|
|
427
|
-
await this.failDevSessionAfterIrrecoverableSourceError(state, error);
|
|
373
|
+
await this.failDevSessionAfterIrrecoverableSourceError(state, proxyServer, error);
|
|
428
374
|
}
|
|
429
375
|
},
|
|
430
376
|
});
|
|
431
377
|
}
|
|
432
378
|
|
|
433
|
-
private async
|
|
379
|
+
private async runQueuedRebuild(
|
|
380
|
+
prepared: DevPreparedRuntime,
|
|
381
|
+
state: DevMutableProcessState,
|
|
382
|
+
gatewayBaseUrl: string,
|
|
383
|
+
proxyServer: CliDevProxyServer,
|
|
384
|
+
request: Readonly<{
|
|
385
|
+
changedPaths: ReadonlyArray<string>;
|
|
386
|
+
shouldRepublishConsumerOutput: boolean;
|
|
387
|
+
shouldRestartUi: boolean;
|
|
388
|
+
}>,
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
void request.changedPaths;
|
|
391
|
+
proxyServer.setBuildStatus("building");
|
|
392
|
+
proxyServer.broadcastBuildStarted();
|
|
393
|
+
try {
|
|
394
|
+
if (request.shouldRepublishConsumerOutput) {
|
|
395
|
+
await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
|
|
396
|
+
}
|
|
397
|
+
await this.stopCurrentRuntime(state, proxyServer);
|
|
398
|
+
process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
399
|
+
const runtime = await this.createRuntime(prepared);
|
|
400
|
+
state.currentRuntime = runtime;
|
|
401
|
+
await proxyServer.activateRuntime({
|
|
402
|
+
httpPort: runtime.httpPort,
|
|
403
|
+
workflowWebSocketPort: runtime.workflowWebSocketPort,
|
|
404
|
+
});
|
|
405
|
+
if (request.shouldRestartUi) {
|
|
406
|
+
await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
|
|
407
|
+
}
|
|
408
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
409
|
+
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
410
|
+
if (json) {
|
|
411
|
+
this.devCliBannerRenderer.renderCompact(json);
|
|
412
|
+
}
|
|
413
|
+
proxyServer.setBuildStatus("idle");
|
|
414
|
+
proxyServer.broadcastBuildCompleted(runtime.buildVersion);
|
|
415
|
+
process.stdout.write("[codemation] Runtime ready.\n");
|
|
416
|
+
} catch (error) {
|
|
417
|
+
proxyServer.setBuildStatus("idle");
|
|
418
|
+
proxyServer.broadcastBuildFailed(error instanceof Error ? error.message : String(error));
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async restartUiAfterSourceChange(
|
|
434
424
|
prepared: DevPreparedRuntime,
|
|
435
425
|
state: DevMutableProcessState,
|
|
436
426
|
gatewayBaseUrl: string,
|
|
437
427
|
): Promise<void> {
|
|
438
428
|
const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
|
|
439
|
-
process.stdout.write("[codemation] Restarting
|
|
440
|
-
state.
|
|
429
|
+
process.stdout.write("[codemation] Restarting the UI process to apply source changes…\n");
|
|
430
|
+
state.isRestartingUi = true;
|
|
441
431
|
try {
|
|
442
|
-
if (prepared.devMode === "
|
|
443
|
-
await this.
|
|
432
|
+
if (prepared.devMode === "packaged-ui") {
|
|
433
|
+
await this.restartPackagedUi(prepared, state, refreshedAuthSettings);
|
|
444
434
|
return;
|
|
445
435
|
}
|
|
446
|
-
await this.
|
|
436
|
+
await this.restartDevUi(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
|
|
447
437
|
} finally {
|
|
448
|
-
state.
|
|
438
|
+
state.isRestartingUi = false;
|
|
449
439
|
}
|
|
450
440
|
}
|
|
451
441
|
|
|
452
|
-
private async
|
|
442
|
+
private async restartPackagedUi(
|
|
453
443
|
prepared: DevPreparedRuntime,
|
|
454
444
|
state: DevMutableProcessState,
|
|
455
445
|
authSettings: DevResolvedAuthSettings,
|
|
456
446
|
): Promise<void> {
|
|
457
|
-
if (
|
|
458
|
-
|
|
447
|
+
if (
|
|
448
|
+
state.currentPackagedUi &&
|
|
449
|
+
state.currentPackagedUi.exitCode === null &&
|
|
450
|
+
state.currentPackagedUi.signalCode === null
|
|
451
|
+
) {
|
|
452
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentPackagedUi);
|
|
459
453
|
}
|
|
460
|
-
state.
|
|
461
|
-
const uiProxyBaseUrl = state.
|
|
454
|
+
state.currentPackagedUi = null;
|
|
455
|
+
const uiProxyBaseUrl = state.currentPackagedUiBaseUrl;
|
|
462
456
|
if (!uiProxyBaseUrl) {
|
|
463
|
-
throw new Error("
|
|
457
|
+
throw new Error("Packaged UI proxy base URL is missing during UI restart.");
|
|
464
458
|
}
|
|
465
|
-
await this.
|
|
459
|
+
await this.spawnPackagedUi(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
|
|
466
460
|
}
|
|
467
461
|
|
|
468
|
-
private async
|
|
462
|
+
private async restartDevUi(
|
|
469
463
|
prepared: DevPreparedRuntime,
|
|
470
464
|
state: DevMutableProcessState,
|
|
471
465
|
gatewayBaseUrl: string,
|
|
472
466
|
authSettings: DevResolvedAuthSettings,
|
|
473
467
|
): Promise<void> {
|
|
474
|
-
if (state.
|
|
475
|
-
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.
|
|
468
|
+
if (state.currentDevUi && state.currentDevUi.exitCode === null && state.currentDevUi.signalCode === null) {
|
|
469
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentDevUi);
|
|
476
470
|
}
|
|
477
|
-
state.
|
|
478
|
-
await this.
|
|
471
|
+
state.currentDevUi = null;
|
|
472
|
+
await this.spawnDevUi(prepared, state, gatewayBaseUrl, authSettings);
|
|
479
473
|
}
|
|
480
474
|
|
|
481
475
|
private async failDevSessionAfterIrrecoverableSourceError(
|
|
482
476
|
state: DevMutableProcessState,
|
|
477
|
+
proxyServer: CliDevProxyServer | null,
|
|
483
478
|
error: unknown,
|
|
484
479
|
): Promise<void> {
|
|
485
480
|
const exception = error instanceof Error ? error : new Error(String(error));
|
|
486
481
|
state.stopRequested = true;
|
|
487
|
-
await this.
|
|
482
|
+
await this.stopLiveProcesses(state, proxyServer);
|
|
488
483
|
state.stopReject?.(exception);
|
|
489
484
|
}
|
|
490
485
|
|
|
491
|
-
private async
|
|
486
|
+
private async stopLiveProcesses(state: DevMutableProcessState, proxyServer: CliDevProxyServer | null): Promise<void> {
|
|
487
|
+
await this.stopCurrentRuntime(state, proxyServer);
|
|
492
488
|
const children: ChildProcess[] = [];
|
|
493
|
-
for (const child of [state.
|
|
489
|
+
for (const child of [state.currentPackagedUi, state.currentDevUi]) {
|
|
494
490
|
if (child && child.exitCode === null && child.signalCode === null) {
|
|
495
491
|
children.push(child);
|
|
496
492
|
}
|
|
497
493
|
}
|
|
498
494
|
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
495
|
+
if (proxyServer) {
|
|
496
|
+
await proxyServer.stop();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private async stopCurrentRuntime(
|
|
501
|
+
state: DevMutableProcessState,
|
|
502
|
+
proxyServer: CliDevProxyServer | null,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
const runtime = state.currentRuntime;
|
|
505
|
+
state.currentRuntime = null;
|
|
506
|
+
if (proxyServer) {
|
|
507
|
+
await proxyServer.activateRuntime(null);
|
|
508
|
+
}
|
|
509
|
+
if (runtime) {
|
|
510
|
+
await runtime.stop();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async createRuntime(prepared: DevPreparedRuntime): Promise<DevApiRuntimeServerHandle> {
|
|
515
|
+
const runtimeEnvironment = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(
|
|
516
|
+
process.env,
|
|
517
|
+
prepared.consumerEnv,
|
|
518
|
+
);
|
|
519
|
+
return await this.devApiRuntimeFactory.create({
|
|
520
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
521
|
+
runtimeWorkingDirectory: process.cwd(),
|
|
522
|
+
env: {
|
|
523
|
+
...runtimeEnvironment,
|
|
524
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
525
|
+
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
526
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
527
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
528
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
529
|
+
},
|
|
530
|
+
});
|
|
499
531
|
}
|
|
500
532
|
|
|
501
|
-
private
|
|
502
|
-
if (devMode !== "
|
|
533
|
+
private logPackagedUiDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
|
|
534
|
+
if (devMode !== "packaged-ui") {
|
|
503
535
|
return;
|
|
504
536
|
}
|
|
505
537
|
this.cliLogger.info(
|
|
506
|
-
`codemation dev
|
|
538
|
+
`codemation dev: open http://127.0.0.1:${gatewayPort} — this uses the packaged @codemation/next-host UI. Use \`codemation dev --watch-framework\` only when working on the framework UI itself.`,
|
|
507
539
|
);
|
|
508
540
|
}
|
|
509
541
|
}
|