@codemation/cli 0.0.5 → 0.0.7
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-Bx1lFBi5.js} +1041 -365
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +644 -207
- package/dist/index.js +1 -1
- package/package.json +9 -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 +202 -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 +447 -0
- package/src/dev/CliDevProxyServerFactory.ts +7 -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/WatchRootsResolver.ts +6 -4
- package/src/runtime/NextHostConsumerServerCommandFactory.ts +11 -2
- package/src/runtime/TypeScriptRuntimeConfigurator.ts +7 -0
- 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,186 @@ 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
|
+
proxyServer.setBuildStatus("building");
|
|
391
|
+
proxyServer.broadcastBuildStarted();
|
|
392
|
+
try {
|
|
393
|
+
if (request.shouldRepublishConsumerOutput) {
|
|
394
|
+
await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
|
|
395
|
+
}
|
|
396
|
+
await this.stopCurrentRuntime(state, proxyServer);
|
|
397
|
+
process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
398
|
+
const runtime = await this.createRuntime(prepared);
|
|
399
|
+
state.currentRuntime = runtime;
|
|
400
|
+
await proxyServer.activateRuntime({
|
|
401
|
+
httpPort: runtime.httpPort,
|
|
402
|
+
workflowWebSocketPort: runtime.workflowWebSocketPort,
|
|
403
|
+
});
|
|
404
|
+
if (request.shouldRestartUi) {
|
|
405
|
+
await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
|
|
406
|
+
}
|
|
407
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
408
|
+
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
409
|
+
if (json) {
|
|
410
|
+
this.devCliBannerRenderer.renderCompact(json);
|
|
411
|
+
}
|
|
412
|
+
proxyServer.setBuildStatus("idle");
|
|
413
|
+
proxyServer.broadcastBuildCompleted(runtime.buildVersion);
|
|
414
|
+
process.stdout.write("[codemation] Runtime ready.\n");
|
|
415
|
+
} catch (error) {
|
|
416
|
+
proxyServer.setBuildStatus("idle");
|
|
417
|
+
proxyServer.broadcastBuildFailed(error instanceof Error ? error.message : String(error));
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async restartUiAfterSourceChange(
|
|
434
423
|
prepared: DevPreparedRuntime,
|
|
435
424
|
state: DevMutableProcessState,
|
|
436
425
|
gatewayBaseUrl: string,
|
|
437
426
|
): Promise<void> {
|
|
438
427
|
const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
|
|
439
|
-
process.stdout.write("[codemation] Restarting
|
|
440
|
-
state.
|
|
428
|
+
process.stdout.write("[codemation] Restarting the UI process to apply source changes…\n");
|
|
429
|
+
state.isRestartingUi = true;
|
|
441
430
|
try {
|
|
442
|
-
if (prepared.devMode === "
|
|
443
|
-
await this.
|
|
431
|
+
if (prepared.devMode === "packaged-ui") {
|
|
432
|
+
await this.restartPackagedUi(prepared, state, refreshedAuthSettings);
|
|
444
433
|
return;
|
|
445
434
|
}
|
|
446
|
-
await this.
|
|
435
|
+
await this.restartDevUi(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
|
|
447
436
|
} finally {
|
|
448
|
-
state.
|
|
437
|
+
state.isRestartingUi = false;
|
|
449
438
|
}
|
|
450
439
|
}
|
|
451
440
|
|
|
452
|
-
private async
|
|
441
|
+
private async restartPackagedUi(
|
|
453
442
|
prepared: DevPreparedRuntime,
|
|
454
443
|
state: DevMutableProcessState,
|
|
455
444
|
authSettings: DevResolvedAuthSettings,
|
|
456
445
|
): Promise<void> {
|
|
457
|
-
if (
|
|
458
|
-
|
|
446
|
+
if (
|
|
447
|
+
state.currentPackagedUi &&
|
|
448
|
+
state.currentPackagedUi.exitCode === null &&
|
|
449
|
+
state.currentPackagedUi.signalCode === null
|
|
450
|
+
) {
|
|
451
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentPackagedUi);
|
|
459
452
|
}
|
|
460
|
-
state.
|
|
461
|
-
const uiProxyBaseUrl = state.
|
|
453
|
+
state.currentPackagedUi = null;
|
|
454
|
+
const uiProxyBaseUrl = state.currentPackagedUiBaseUrl;
|
|
462
455
|
if (!uiProxyBaseUrl) {
|
|
463
|
-
throw new Error("
|
|
456
|
+
throw new Error("Packaged UI proxy base URL is missing during UI restart.");
|
|
464
457
|
}
|
|
465
|
-
await this.
|
|
458
|
+
await this.spawnPackagedUi(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
|
|
466
459
|
}
|
|
467
460
|
|
|
468
|
-
private async
|
|
461
|
+
private async restartDevUi(
|
|
469
462
|
prepared: DevPreparedRuntime,
|
|
470
463
|
state: DevMutableProcessState,
|
|
471
464
|
gatewayBaseUrl: string,
|
|
472
465
|
authSettings: DevResolvedAuthSettings,
|
|
473
466
|
): Promise<void> {
|
|
474
|
-
if (state.
|
|
475
|
-
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.
|
|
467
|
+
if (state.currentDevUi && state.currentDevUi.exitCode === null && state.currentDevUi.signalCode === null) {
|
|
468
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentDevUi);
|
|
476
469
|
}
|
|
477
|
-
state.
|
|
478
|
-
await this.
|
|
470
|
+
state.currentDevUi = null;
|
|
471
|
+
await this.spawnDevUi(prepared, state, gatewayBaseUrl, authSettings);
|
|
479
472
|
}
|
|
480
473
|
|
|
481
474
|
private async failDevSessionAfterIrrecoverableSourceError(
|
|
482
475
|
state: DevMutableProcessState,
|
|
476
|
+
proxyServer: CliDevProxyServer | null,
|
|
483
477
|
error: unknown,
|
|
484
478
|
): Promise<void> {
|
|
485
479
|
const exception = error instanceof Error ? error : new Error(String(error));
|
|
486
480
|
state.stopRequested = true;
|
|
487
|
-
await this.
|
|
481
|
+
await this.stopLiveProcesses(state, proxyServer);
|
|
488
482
|
state.stopReject?.(exception);
|
|
489
483
|
}
|
|
490
484
|
|
|
491
|
-
private async
|
|
485
|
+
private async stopLiveProcesses(state: DevMutableProcessState, proxyServer: CliDevProxyServer | null): Promise<void> {
|
|
486
|
+
await this.stopCurrentRuntime(state, proxyServer);
|
|
492
487
|
const children: ChildProcess[] = [];
|
|
493
|
-
for (const child of [state.
|
|
488
|
+
for (const child of [state.currentPackagedUi, state.currentDevUi]) {
|
|
494
489
|
if (child && child.exitCode === null && child.signalCode === null) {
|
|
495
490
|
children.push(child);
|
|
496
491
|
}
|
|
497
492
|
}
|
|
498
493
|
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
494
|
+
if (proxyServer) {
|
|
495
|
+
await proxyServer.stop();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private async stopCurrentRuntime(
|
|
500
|
+
state: DevMutableProcessState,
|
|
501
|
+
proxyServer: CliDevProxyServer | null,
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const runtime = state.currentRuntime;
|
|
504
|
+
state.currentRuntime = null;
|
|
505
|
+
if (proxyServer) {
|
|
506
|
+
await proxyServer.activateRuntime(null);
|
|
507
|
+
}
|
|
508
|
+
if (runtime) {
|
|
509
|
+
await runtime.stop();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async createRuntime(prepared: DevPreparedRuntime): Promise<DevApiRuntimeServerHandle> {
|
|
514
|
+
const runtimeEnvironment = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(
|
|
515
|
+
process.env,
|
|
516
|
+
prepared.consumerEnv,
|
|
517
|
+
);
|
|
518
|
+
return await this.devApiRuntimeFactory.create({
|
|
519
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
520
|
+
runtimeWorkingDirectory: process.cwd(),
|
|
521
|
+
env: {
|
|
522
|
+
...runtimeEnvironment,
|
|
523
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
524
|
+
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
525
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
526
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
527
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
528
|
+
},
|
|
529
|
+
});
|
|
499
530
|
}
|
|
500
531
|
|
|
501
|
-
private
|
|
502
|
-
if (devMode !== "
|
|
532
|
+
private logPackagedUiDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
|
|
533
|
+
if (devMode !== "packaged-ui") {
|
|
503
534
|
return;
|
|
504
535
|
}
|
|
505
536
|
this.cliLogger.info(
|
|
506
|
-
`codemation dev
|
|
537
|
+
`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
538
|
);
|
|
508
539
|
}
|
|
509
540
|
}
|