@codemation/cli 0.0.4 → 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-900C8Din.js → CliBin-Bx1lFBi5.js} +1125 -340
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +669 -197
- 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 +302 -158
- package/src/commands/ServeWebCommand.ts +26 -1
- package/src/commands/ServeWorkerCommand.ts +46 -30
- package/src/commands/devCommandLifecycle.types.ts +7 -9
- package/src/database/ConsumerDatabaseConnectionResolver.ts +55 -9
- package/src/database/DatabaseMigrationsApplyService.ts +2 -2
- package/src/dev/Builder.ts +3 -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 +8 -4
- package/src/dev/DevNextHostEnvironmentBuilder.ts +65 -3
- 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 +2 -4
- package/src/dev/DevSourceChangeClassifier.ts +59 -0
- 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,35 +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();
|
|
69
|
+
const processState = this.createInitialProcessState();
|
|
70
|
+
let proxyServer: CliDevProxyServer | null = null;
|
|
65
71
|
try {
|
|
66
72
|
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
67
|
-
const processState = this.createInitialProcessState();
|
|
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
|
-
this.
|
|
79
|
-
await this.startWatcherForSourceRestart(prepared, 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 {
|
|
89
|
+
processState.stopRequested = true;
|
|
90
|
+
await this.stopLiveProcesses(processState, proxyServer);
|
|
83
91
|
await watcher.stop();
|
|
84
92
|
await devLock.release();
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
private
|
|
89
|
-
|
|
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";
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
private async prepareDevRuntime(
|
|
@@ -99,19 +110,7 @@ export class DevCommand {
|
|
|
99
110
|
const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(
|
|
100
111
|
process.env.CODEMATION_DEV_SERVER_TOKEN,
|
|
101
112
|
);
|
|
102
|
-
const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
103
|
-
packageName: "@codemation/dev-gateway",
|
|
104
|
-
repoRoot: paths.repoRoot,
|
|
105
|
-
sourceEntrypoint: "packages/dev-gateway/src/bin.ts",
|
|
106
|
-
});
|
|
107
|
-
const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
108
|
-
packageName: "@codemation/runtime-dev",
|
|
109
|
-
repoRoot: paths.repoRoot,
|
|
110
|
-
sourceEntrypoint: "packages/runtime-dev/src/bin.ts",
|
|
111
|
-
});
|
|
112
|
-
const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
|
|
113
113
|
const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
|
|
114
|
-
const discoveredPluginPackagesJson = JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot));
|
|
115
114
|
return {
|
|
116
115
|
paths,
|
|
117
116
|
devMode,
|
|
@@ -119,19 +118,17 @@ export class DevCommand {
|
|
|
119
118
|
gatewayPort,
|
|
120
119
|
authSettings,
|
|
121
120
|
developmentServerToken,
|
|
122
|
-
gatewayEntrypoint,
|
|
123
|
-
runtimeEntrypoint,
|
|
124
|
-
runtimeWorkingDirectory,
|
|
125
|
-
discoveredPluginPackagesJson,
|
|
126
121
|
consumerEnv,
|
|
127
122
|
};
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
private createInitialProcessState(): DevMutableProcessState {
|
|
131
126
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
currentDevUi: null,
|
|
128
|
+
currentPackagedUi: null,
|
|
129
|
+
currentPackagedUiBaseUrl: null,
|
|
130
|
+
currentRuntime: null,
|
|
131
|
+
isRestartingUi: false,
|
|
135
132
|
stopRequested: false,
|
|
136
133
|
stopResolve: null,
|
|
137
134
|
stopReject: null,
|
|
@@ -149,20 +146,28 @@ export class DevCommand {
|
|
|
149
146
|
return `http://127.0.0.1:${gatewayPort}`;
|
|
150
147
|
}
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
* Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
|
|
154
|
-
* Framework mode: no separate UI child (Next runs in dev later).
|
|
155
|
-
*/
|
|
156
|
-
private async startConsumerUiProxyWhenNeeded(
|
|
149
|
+
private async startPackagedUiWhenNeeded(
|
|
157
150
|
prepared: DevPreparedRuntime,
|
|
158
151
|
state: DevMutableProcessState,
|
|
159
152
|
): Promise<string> {
|
|
160
|
-
if (prepared.devMode !== "
|
|
153
|
+
if (prepared.devMode !== "packaged-ui") {
|
|
161
154
|
return "";
|
|
162
155
|
}
|
|
163
156
|
const websocketPort = prepared.gatewayPort;
|
|
164
|
-
const
|
|
165
|
-
|
|
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);
|
|
161
|
+
return uiProxyBase;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async spawnPackagedUi(
|
|
165
|
+
prepared: DevPreparedRuntime,
|
|
166
|
+
state: DevMutableProcessState,
|
|
167
|
+
authSettings: DevResolvedAuthSettings,
|
|
168
|
+
websocketPort: number,
|
|
169
|
+
uiProxyBase: string,
|
|
170
|
+
): Promise<void> {
|
|
166
171
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
167
172
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
168
173
|
const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
|
|
@@ -172,94 +177,61 @@ export class DevCommand {
|
|
|
172
177
|
"output",
|
|
173
178
|
"current.json",
|
|
174
179
|
);
|
|
175
|
-
|
|
180
|
+
const uiPort = Number(new URL(uiProxyBase).port);
|
|
181
|
+
const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
|
|
182
|
+
authConfigJson: authSettings.authConfigJson,
|
|
183
|
+
authSecret: authSettings.authSecret,
|
|
184
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
185
|
+
consumerOutputManifestPath,
|
|
186
|
+
developmentServerToken: prepared.developmentServerToken,
|
|
187
|
+
nextPort: uiPort,
|
|
188
|
+
publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
189
|
+
runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
190
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
191
|
+
websocketPort,
|
|
192
|
+
});
|
|
193
|
+
state.currentPackagedUi = spawn(nextHostCommand.command, nextHostCommand.args, {
|
|
176
194
|
cwd: nextHostCommand.cwd,
|
|
177
195
|
...this.devDetachedChildSpawnOptions(),
|
|
178
|
-
env:
|
|
179
|
-
...process.env,
|
|
180
|
-
...prepared.consumerEnv,
|
|
181
|
-
PORT: String(uiPort),
|
|
182
|
-
CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
|
|
183
|
-
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
184
|
-
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
|
|
185
|
-
CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
186
|
-
AUTH_SECRET: prepared.authSettings.authSecret,
|
|
187
|
-
NEXTAUTH_SECRET: prepared.authSettings.authSecret,
|
|
188
|
-
NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
189
|
-
CODEMATION_WS_PORT: String(websocketPort),
|
|
190
|
-
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
191
|
-
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
192
|
-
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
193
|
-
WS_NO_BUFFER_UTIL: "1",
|
|
194
|
-
WS_NO_UTF_8_VALIDATE: "1",
|
|
195
|
-
},
|
|
196
|
+
env: nextHostEnvironment,
|
|
196
197
|
});
|
|
197
|
-
state.
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
198
|
+
state.currentPackagedUi.on("error", (error) => {
|
|
199
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
200
|
+
return;
|
|
201
201
|
}
|
|
202
|
+
state.stopRequested = true;
|
|
203
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
202
204
|
});
|
|
203
|
-
state.
|
|
204
|
-
if (state.stopRequested) {
|
|
205
|
+
state.currentPackagedUi.on("exit", (code) => {
|
|
206
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
205
207
|
return;
|
|
206
208
|
}
|
|
207
209
|
state.stopRequested = true;
|
|
208
|
-
|
|
209
|
-
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
210
|
-
}
|
|
211
|
-
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}.`));
|
|
212
211
|
});
|
|
213
212
|
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
214
|
-
return uiProxyBase;
|
|
215
213
|
}
|
|
216
214
|
|
|
217
|
-
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(
|
|
218
224
|
prepared: DevPreparedRuntime,
|
|
219
225
|
state: DevMutableProcessState,
|
|
220
|
-
|
|
221
|
-
uiProxyBase: string,
|
|
226
|
+
proxyServer: CliDevProxyServer,
|
|
222
227
|
): Promise<void> {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
cwd: prepared.runtimeWorkingDirectory,
|
|
229
|
-
...this.devDetachedChildSpawnOptions(),
|
|
230
|
-
env: {
|
|
231
|
-
...gatewayProcessEnv,
|
|
232
|
-
...prepared.gatewayEntrypoint.env,
|
|
233
|
-
CODEMATION_DEV_GATEWAY_HTTP_PORT: String(prepared.gatewayPort),
|
|
234
|
-
CODEMATION_RUNTIME_CHILD_BIN: prepared.runtimeEntrypoint.command,
|
|
235
|
-
CODEMATION_RUNTIME_CHILD_ARGS_JSON: JSON.stringify(prepared.runtimeEntrypoint.args),
|
|
236
|
-
CODEMATION_RUNTIME_CHILD_ENV_JSON: JSON.stringify(prepared.runtimeEntrypoint.env),
|
|
237
|
-
CODEMATION_RUNTIME_CHILD_CWD: prepared.runtimeWorkingDirectory,
|
|
238
|
-
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
239
|
-
CODEMATION_DISCOVERED_PLUGIN_PACKAGES_JSON: prepared.discoveredPluginPackagesJson,
|
|
240
|
-
CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY: "true",
|
|
241
|
-
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
242
|
-
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
243
|
-
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
244
|
-
WS_NO_BUFFER_UTIL: "1",
|
|
245
|
-
WS_NO_UTF_8_VALIDATE: "1",
|
|
246
|
-
...(uiProxyBase.length > 0 ? { CODEMATION_DEV_UI_PROXY_TARGET: uiProxyBase } : {}),
|
|
247
|
-
},
|
|
228
|
+
const runtime = await this.createRuntime(prepared);
|
|
229
|
+
state.currentRuntime = runtime;
|
|
230
|
+
await proxyServer.activateRuntime({
|
|
231
|
+
httpPort: runtime.httpPort,
|
|
232
|
+
workflowWebSocketPort: runtime.workflowWebSocketPort,
|
|
248
233
|
});
|
|
249
|
-
|
|
250
|
-
if (!state.stopRequested) {
|
|
251
|
-
state.stopRequested = true;
|
|
252
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
state.currentGateway.on("exit", (code) => {
|
|
256
|
-
if (state.stopRequested) {
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
state.stopRequested = true;
|
|
260
|
-
state.stopReject?.(new Error(`codemation dev-gateway exited unexpectedly with code ${code ?? 0}.`));
|
|
261
|
-
});
|
|
262
|
-
await this.session.devHttpProbe.waitUntilGatewayHealthy(gatewayBaseUrl);
|
|
234
|
+
proxyServer.setBuildStatus("idle");
|
|
263
235
|
}
|
|
264
236
|
|
|
265
237
|
private devDetachedChildSpawnOptions(): Readonly<{
|
|
@@ -272,7 +244,10 @@ export class DevCommand {
|
|
|
272
244
|
: { stdio: "inherit", detached: true };
|
|
273
245
|
}
|
|
274
246
|
|
|
275
|
-
private bindShutdownSignalsToChildProcesses(
|
|
247
|
+
private bindShutdownSignalsToChildProcesses(
|
|
248
|
+
state: DevMutableProcessState,
|
|
249
|
+
proxyServer: CliDevProxyServer | null,
|
|
250
|
+
): void {
|
|
276
251
|
let shutdownInProgress = false;
|
|
277
252
|
const runShutdown = async (): Promise<void> => {
|
|
278
253
|
if (shutdownInProgress) {
|
|
@@ -281,13 +256,7 @@ export class DevCommand {
|
|
|
281
256
|
shutdownInProgress = true;
|
|
282
257
|
state.stopRequested = true;
|
|
283
258
|
process.stdout.write("\n[codemation] Stopping..\n");
|
|
284
|
-
|
|
285
|
-
for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
|
|
286
|
-
if (child && child.exitCode === null && child.signalCode === null) {
|
|
287
|
-
children.push(child);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
259
|
+
await this.stopLiveProcesses(state, proxyServer);
|
|
291
260
|
process.stdout.write("[codemation] Stopped.\n");
|
|
292
261
|
state.stopResolve?.();
|
|
293
262
|
};
|
|
@@ -298,64 +267,76 @@ export class DevCommand {
|
|
|
298
267
|
}
|
|
299
268
|
}
|
|
300
269
|
|
|
301
|
-
|
|
302
|
-
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
303
|
-
*/
|
|
304
|
-
private spawnFrameworkNextHostWhenNeeded(
|
|
270
|
+
private async spawnDevUiWhenNeeded(
|
|
305
271
|
prepared: DevPreparedRuntime,
|
|
306
272
|
state: DevMutableProcessState,
|
|
307
273
|
gatewayBaseUrl: string,
|
|
308
|
-
): void {
|
|
309
|
-
if (prepared.devMode !== "framework") {
|
|
274
|
+
): Promise<void> {
|
|
275
|
+
if (prepared.devMode !== "watch-framework") {
|
|
310
276
|
return;
|
|
311
277
|
}
|
|
278
|
+
await this.spawnDevUi(prepared, state, gatewayBaseUrl, prepared.authSettings);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async spawnDevUi(
|
|
282
|
+
prepared: DevPreparedRuntime,
|
|
283
|
+
state: DevMutableProcessState,
|
|
284
|
+
gatewayBaseUrl: string,
|
|
285
|
+
authSettings: DevResolvedAuthSettings,
|
|
286
|
+
): Promise<void> {
|
|
312
287
|
const websocketPort = prepared.gatewayPort;
|
|
313
288
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
314
289
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
315
290
|
const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
|
|
316
|
-
authConfigJson:
|
|
291
|
+
authConfigJson: authSettings.authConfigJson,
|
|
317
292
|
consumerRoot: prepared.paths.consumerRoot,
|
|
318
293
|
developmentServerToken: prepared.developmentServerToken,
|
|
319
294
|
nextPort: prepared.nextPort,
|
|
320
|
-
skipUiAuth:
|
|
295
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
321
296
|
websocketPort,
|
|
322
297
|
runtimeDevUrl: gatewayBaseUrl,
|
|
323
298
|
});
|
|
324
|
-
state.
|
|
299
|
+
state.currentDevUi = spawn("pnpm", ["exec", "next", "dev"], {
|
|
325
300
|
cwd: nextHostRoot,
|
|
326
301
|
...this.devDetachedChildSpawnOptions(),
|
|
327
302
|
env: nextHostEnvironment,
|
|
328
303
|
});
|
|
329
|
-
state.
|
|
304
|
+
state.currentDevUi.on("exit", (code) => {
|
|
330
305
|
const normalizedCode = code ?? 0;
|
|
331
|
-
if (state.stopRequested) {
|
|
306
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
332
307
|
return;
|
|
333
308
|
}
|
|
334
309
|
if (normalizedCode === 0) {
|
|
335
310
|
state.stopRequested = true;
|
|
336
|
-
if (state.currentGateway?.exitCode === null) {
|
|
337
|
-
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
338
|
-
}
|
|
339
311
|
state.stopResolve?.();
|
|
340
312
|
return;
|
|
341
313
|
}
|
|
342
314
|
state.stopRequested = true;
|
|
343
315
|
state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
|
|
344
316
|
});
|
|
345
|
-
state.
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
317
|
+
state.currentDevUi.on("error", (error) => {
|
|
318
|
+
if (state.stopRequested || state.isRestartingUi) {
|
|
319
|
+
return;
|
|
349
320
|
}
|
|
321
|
+
state.stopRequested = true;
|
|
322
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
350
323
|
});
|
|
324
|
+
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
|
|
351
325
|
}
|
|
352
326
|
|
|
353
327
|
private async startWatcherForSourceRestart(
|
|
354
328
|
prepared: DevPreparedRuntime,
|
|
329
|
+
state: DevMutableProcessState,
|
|
355
330
|
watcher: DevSourceWatcher,
|
|
356
331
|
devMode: DevMode,
|
|
357
332
|
gatewayBaseUrl: string,
|
|
333
|
+
proxyServer: CliDevProxyServer,
|
|
358
334
|
): Promise<void> {
|
|
335
|
+
const rebuildQueue = this.devRebuildQueueFactory.create({
|
|
336
|
+
run: async (request) => {
|
|
337
|
+
await this.runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
359
340
|
await watcher.start({
|
|
360
341
|
roots: this.session.watchRootsResolver.resolve({
|
|
361
342
|
consumerRoot: prepared.paths.consumerRoot,
|
|
@@ -365,32 +346,195 @@ export class DevCommand {
|
|
|
365
346
|
onChange: async ({ changedPaths }) => {
|
|
366
347
|
if (changedPaths.length > 0 && changedPaths.every((p) => this.consumerEnvDotenvFilePredicate.matches(p))) {
|
|
367
348
|
process.stdout.write(
|
|
368
|
-
"\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",
|
|
369
350
|
);
|
|
370
351
|
return;
|
|
371
352
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
353
|
+
try {
|
|
354
|
+
const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
|
|
355
|
+
changedPaths,
|
|
356
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
357
|
+
});
|
|
358
|
+
const shouldRestartUi = this.session.sourceChangeClassifier.requiresUiRestart({
|
|
359
|
+
changedPaths,
|
|
360
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
361
|
+
});
|
|
362
|
+
process.stdout.write(
|
|
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",
|
|
366
|
+
);
|
|
367
|
+
await rebuildQueue.enqueue({
|
|
368
|
+
changedPaths,
|
|
369
|
+
shouldRepublishConsumerOutput,
|
|
370
|
+
shouldRestartUi,
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
await this.failDevSessionAfterIrrecoverableSourceError(state, proxyServer, error);
|
|
382
374
|
}
|
|
383
|
-
process.stdout.write("[codemation] Runtime ready.\n");
|
|
384
375
|
},
|
|
385
376
|
});
|
|
386
377
|
}
|
|
387
378
|
|
|
388
|
-
private
|
|
389
|
-
|
|
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(
|
|
423
|
+
prepared: DevPreparedRuntime,
|
|
424
|
+
state: DevMutableProcessState,
|
|
425
|
+
gatewayBaseUrl: string,
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
|
|
428
|
+
process.stdout.write("[codemation] Restarting the UI process to apply source changes…\n");
|
|
429
|
+
state.isRestartingUi = true;
|
|
430
|
+
try {
|
|
431
|
+
if (prepared.devMode === "packaged-ui") {
|
|
432
|
+
await this.restartPackagedUi(prepared, state, refreshedAuthSettings);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await this.restartDevUi(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
|
|
436
|
+
} finally {
|
|
437
|
+
state.isRestartingUi = false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private async restartPackagedUi(
|
|
442
|
+
prepared: DevPreparedRuntime,
|
|
443
|
+
state: DevMutableProcessState,
|
|
444
|
+
authSettings: DevResolvedAuthSettings,
|
|
445
|
+
): Promise<void> {
|
|
446
|
+
if (
|
|
447
|
+
state.currentPackagedUi &&
|
|
448
|
+
state.currentPackagedUi.exitCode === null &&
|
|
449
|
+
state.currentPackagedUi.signalCode === null
|
|
450
|
+
) {
|
|
451
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentPackagedUi);
|
|
452
|
+
}
|
|
453
|
+
state.currentPackagedUi = null;
|
|
454
|
+
const uiProxyBaseUrl = state.currentPackagedUiBaseUrl;
|
|
455
|
+
if (!uiProxyBaseUrl) {
|
|
456
|
+
throw new Error("Packaged UI proxy base URL is missing during UI restart.");
|
|
457
|
+
}
|
|
458
|
+
await this.spawnPackagedUi(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private async restartDevUi(
|
|
462
|
+
prepared: DevPreparedRuntime,
|
|
463
|
+
state: DevMutableProcessState,
|
|
464
|
+
gatewayBaseUrl: string,
|
|
465
|
+
authSettings: DevResolvedAuthSettings,
|
|
466
|
+
): Promise<void> {
|
|
467
|
+
if (state.currentDevUi && state.currentDevUi.exitCode === null && state.currentDevUi.signalCode === null) {
|
|
468
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentDevUi);
|
|
469
|
+
}
|
|
470
|
+
state.currentDevUi = null;
|
|
471
|
+
await this.spawnDevUi(prepared, state, gatewayBaseUrl, authSettings);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async failDevSessionAfterIrrecoverableSourceError(
|
|
475
|
+
state: DevMutableProcessState,
|
|
476
|
+
proxyServer: CliDevProxyServer | null,
|
|
477
|
+
error: unknown,
|
|
478
|
+
): Promise<void> {
|
|
479
|
+
const exception = error instanceof Error ? error : new Error(String(error));
|
|
480
|
+
state.stopRequested = true;
|
|
481
|
+
await this.stopLiveProcesses(state, proxyServer);
|
|
482
|
+
state.stopReject?.(exception);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private async stopLiveProcesses(state: DevMutableProcessState, proxyServer: CliDevProxyServer | null): Promise<void> {
|
|
486
|
+
await this.stopCurrentRuntime(state, proxyServer);
|
|
487
|
+
const children: ChildProcess[] = [];
|
|
488
|
+
for (const child of [state.currentPackagedUi, state.currentDevUi]) {
|
|
489
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
490
|
+
children.push(child);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
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
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private logPackagedUiDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
|
|
533
|
+
if (devMode !== "packaged-ui") {
|
|
390
534
|
return;
|
|
391
535
|
}
|
|
392
536
|
this.cliLogger.info(
|
|
393
|
-
`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.`,
|
|
394
538
|
);
|
|
395
539
|
}
|
|
396
540
|
}
|