@codemation/cli 0.0.3 → 0.0.5
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/codemation-cli-0.0.3.tgz +0 -0
- package/dist/{CliBin-900C8Din.js → CliBin-C3ar49fj.js} +158 -49
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +1 -1
- package/package.json +6 -6
- package/src/commands/DevCommand.ts +160 -47
- package/src/commands/devCommandLifecycle.types.ts +2 -0
- package/src/dev/Builder.ts +2 -0
- package/src/dev/DevHttpProbe.ts +6 -2
- package/src/dev/DevNextHostEnvironmentBuilder.ts +32 -0
- package/src/dev/DevSessionServices.ts +2 -0
- package/src/dev/DevSourceChangeClassifier.ts +39 -0
|
Binary file
|
|
@@ -155,9 +155,9 @@ var DevCommand = class {
|
|
|
155
155
|
});
|
|
156
156
|
const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
|
|
157
157
|
const watcher = this.devSourceWatcherFactory.create();
|
|
158
|
+
const processState = this.createInitialProcessState();
|
|
158
159
|
try {
|
|
159
160
|
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
160
|
-
const processState = this.createInitialProcessState();
|
|
161
161
|
const stopPromise = this.wireStopPromise(processState);
|
|
162
162
|
const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
|
|
163
163
|
const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
|
|
@@ -166,11 +166,13 @@ var DevCommand = class {
|
|
|
166
166
|
const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
167
167
|
if (initialSummary) this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
|
|
168
168
|
this.bindShutdownSignalsToChildProcesses(processState);
|
|
169
|
-
this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
170
|
-
await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
|
|
169
|
+
await this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
170
|
+
await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
|
|
171
171
|
this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
|
|
172
172
|
await stopPromise;
|
|
173
173
|
} finally {
|
|
174
|
+
processState.stopRequested = true;
|
|
175
|
+
await this.stopLiveChildProcesses(processState);
|
|
174
176
|
await watcher.stop();
|
|
175
177
|
await devLock.release();
|
|
176
178
|
}
|
|
@@ -211,6 +213,8 @@ var DevCommand = class {
|
|
|
211
213
|
currentGateway: null,
|
|
212
214
|
currentNextHost: null,
|
|
213
215
|
currentUiNext: null,
|
|
216
|
+
currentUiProxyBaseUrl: null,
|
|
217
|
+
isRestartingNextHost: false,
|
|
214
218
|
stopRequested: false,
|
|
215
219
|
stopResolve: null,
|
|
216
220
|
stopReject: null
|
|
@@ -232,48 +236,46 @@ var DevCommand = class {
|
|
|
232
236
|
async startConsumerUiProxyWhenNeeded(prepared, state) {
|
|
233
237
|
if (prepared.devMode !== "consumer") return "";
|
|
234
238
|
const websocketPort = prepared.gatewayPort;
|
|
235
|
-
const
|
|
236
|
-
|
|
239
|
+
const uiProxyBase = state.currentUiProxyBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
|
|
240
|
+
state.currentUiProxyBaseUrl = uiProxyBase;
|
|
241
|
+
await this.spawnConsumerUiProxy(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
|
|
242
|
+
return uiProxyBase;
|
|
243
|
+
}
|
|
244
|
+
async spawnConsumerUiProxy(prepared, state, authSettings, websocketPort, uiProxyBase) {
|
|
237
245
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
238
246
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
239
247
|
const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
|
|
240
248
|
const consumerOutputManifestPath = path.resolve(prepared.paths.consumerRoot, ".codemation", "output", "current.json");
|
|
249
|
+
const uiPort = Number(new URL(uiProxyBase).port);
|
|
250
|
+
const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
|
|
251
|
+
authConfigJson: authSettings.authConfigJson,
|
|
252
|
+
authSecret: authSettings.authSecret,
|
|
253
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
254
|
+
consumerOutputManifestPath,
|
|
255
|
+
developmentServerToken: prepared.developmentServerToken,
|
|
256
|
+
nextPort: uiPort,
|
|
257
|
+
publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
258
|
+
runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
259
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
260
|
+
websocketPort
|
|
261
|
+
});
|
|
241
262
|
state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
|
|
242
263
|
cwd: nextHostCommand.cwd,
|
|
243
264
|
...this.devDetachedChildSpawnOptions(),
|
|
244
|
-
env:
|
|
245
|
-
...process$1.env,
|
|
246
|
-
...prepared.consumerEnv,
|
|
247
|
-
PORT: String(uiPort),
|
|
248
|
-
CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
|
|
249
|
-
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
250
|
-
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
|
|
251
|
-
CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
252
|
-
AUTH_SECRET: prepared.authSettings.authSecret,
|
|
253
|
-
NEXTAUTH_SECRET: prepared.authSettings.authSecret,
|
|
254
|
-
NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
255
|
-
CODEMATION_WS_PORT: String(websocketPort),
|
|
256
|
-
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
257
|
-
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
258
|
-
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
|
|
259
|
-
WS_NO_BUFFER_UTIL: "1",
|
|
260
|
-
WS_NO_UTF_8_VALIDATE: "1"
|
|
261
|
-
}
|
|
265
|
+
env: nextHostEnvironment
|
|
262
266
|
});
|
|
263
267
|
state.currentUiNext.on("error", (error) => {
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
+
if (state.stopRequested || state.isRestartingNextHost) return;
|
|
269
|
+
state.stopRequested = true;
|
|
270
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
268
271
|
});
|
|
269
272
|
state.currentUiNext.on("exit", (code) => {
|
|
270
|
-
if (state.stopRequested) return;
|
|
273
|
+
if (state.stopRequested || state.isRestartingNextHost) return;
|
|
271
274
|
state.stopRequested = true;
|
|
272
275
|
if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
273
276
|
state.stopReject?.(/* @__PURE__ */ new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
274
277
|
});
|
|
275
278
|
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
276
|
-
return uiProxyBase;
|
|
277
279
|
}
|
|
278
280
|
async spawnGatewayChildAndWaitForHealth(prepared, state, gatewayBaseUrl, uiProxyBase) {
|
|
279
281
|
const gatewayProcessEnv = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(process$1.env, prepared.consumerEnv);
|
|
@@ -350,17 +352,20 @@ var DevCommand = class {
|
|
|
350
352
|
/**
|
|
351
353
|
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
352
354
|
*/
|
|
353
|
-
spawnFrameworkNextHostWhenNeeded(prepared, state, gatewayBaseUrl) {
|
|
355
|
+
async spawnFrameworkNextHostWhenNeeded(prepared, state, gatewayBaseUrl) {
|
|
354
356
|
if (prepared.devMode !== "framework") return;
|
|
357
|
+
await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, prepared.authSettings);
|
|
358
|
+
}
|
|
359
|
+
async spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings) {
|
|
355
360
|
const websocketPort = prepared.gatewayPort;
|
|
356
361
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
357
362
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
358
363
|
const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
|
|
359
|
-
authConfigJson:
|
|
364
|
+
authConfigJson: authSettings.authConfigJson,
|
|
360
365
|
consumerRoot: prepared.paths.consumerRoot,
|
|
361
366
|
developmentServerToken: prepared.developmentServerToken,
|
|
362
367
|
nextPort: prepared.nextPort,
|
|
363
|
-
skipUiAuth:
|
|
368
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
364
369
|
websocketPort,
|
|
365
370
|
runtimeDevUrl: gatewayBaseUrl
|
|
366
371
|
});
|
|
@@ -375,7 +380,7 @@ var DevCommand = class {
|
|
|
375
380
|
});
|
|
376
381
|
state.currentNextHost.on("exit", (code) => {
|
|
377
382
|
const normalizedCode = code ?? 0;
|
|
378
|
-
if (state.stopRequested) return;
|
|
383
|
+
if (state.stopRequested || state.isRestartingNextHost) return;
|
|
379
384
|
if (normalizedCode === 0) {
|
|
380
385
|
state.stopRequested = true;
|
|
381
386
|
if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
@@ -386,13 +391,13 @@ var DevCommand = class {
|
|
|
386
391
|
state.stopReject?.(/* @__PURE__ */ new Error(`next host exited with code ${normalizedCode}.`));
|
|
387
392
|
});
|
|
388
393
|
state.currentNextHost.on("error", (error) => {
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
394
|
+
if (state.stopRequested || state.isRestartingNextHost) return;
|
|
395
|
+
state.stopRequested = true;
|
|
396
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
393
397
|
});
|
|
398
|
+
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
|
|
394
399
|
}
|
|
395
|
-
async startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl) {
|
|
400
|
+
async startWatcherForSourceRestart(prepared, state, watcher, devMode, gatewayBaseUrl) {
|
|
396
401
|
await watcher.start({
|
|
397
402
|
roots: this.session.watchRootsResolver.resolve({
|
|
398
403
|
consumerRoot: prepared.paths.consumerRoot,
|
|
@@ -404,16 +409,71 @@ var DevCommand = class {
|
|
|
404
409
|
process$1.stdout.write("\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the gateway and runtime pick up updated variables (host `process.env` does not hot-reload).\n");
|
|
405
410
|
return;
|
|
406
411
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
try {
|
|
413
|
+
const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
|
|
414
|
+
changedPaths,
|
|
415
|
+
consumerRoot: prepared.paths.consumerRoot
|
|
416
|
+
});
|
|
417
|
+
const shouldRestartNextHost = this.session.sourceChangeClassifier.requiresNextHostRestart({
|
|
418
|
+
changedPaths,
|
|
419
|
+
consumerRoot: prepared.paths.consumerRoot
|
|
420
|
+
});
|
|
421
|
+
process$1.stdout.write(shouldRestartNextHost ? "\n[codemation] Consumer config change detected — rebuilding consumer, restarting runtime, and restarting Next host…\n" : "\n[codemation] Source change detected — rebuilding consumer and restarting runtime…\n");
|
|
422
|
+
if (shouldRepublishConsumerOutput) await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
|
|
423
|
+
await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(gatewayBaseUrl, prepared.developmentServerToken);
|
|
424
|
+
process$1.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
425
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
426
|
+
if (shouldRestartNextHost) await this.restartNextHostForConfigChange(prepared, state, gatewayBaseUrl);
|
|
427
|
+
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
428
|
+
if (json) this.devCliBannerRenderer.renderCompact(json);
|
|
429
|
+
process$1.stdout.write("[codemation] Runtime ready.\n");
|
|
430
|
+
} catch (error) {
|
|
431
|
+
await this.failDevSessionAfterIrrecoverableSourceError(state, error);
|
|
432
|
+
}
|
|
414
433
|
}
|
|
415
434
|
});
|
|
416
435
|
}
|
|
436
|
+
async restartNextHostForConfigChange(prepared, state, gatewayBaseUrl) {
|
|
437
|
+
const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
|
|
438
|
+
process$1.stdout.write("[codemation] Restarting Next host to apply auth/database/scheduler/branding changes…\n");
|
|
439
|
+
state.isRestartingNextHost = true;
|
|
440
|
+
try {
|
|
441
|
+
if (prepared.devMode === "consumer") {
|
|
442
|
+
await this.restartConsumerUiProxy(prepared, state, refreshedAuthSettings);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await this.restartFrameworkNextHost(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
|
|
446
|
+
} finally {
|
|
447
|
+
state.isRestartingNextHost = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async restartConsumerUiProxy(prepared, state, authSettings) {
|
|
451
|
+
if (state.currentUiNext && state.currentUiNext.exitCode === null && state.currentUiNext.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentUiNext);
|
|
452
|
+
state.currentUiNext = null;
|
|
453
|
+
const uiProxyBaseUrl = state.currentUiProxyBaseUrl;
|
|
454
|
+
if (!uiProxyBaseUrl) throw new Error("Consumer UI proxy base URL is missing during Next host restart.");
|
|
455
|
+
await this.spawnConsumerUiProxy(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
|
|
456
|
+
}
|
|
457
|
+
async restartFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings) {
|
|
458
|
+
if (state.currentNextHost && state.currentNextHost.exitCode === null && state.currentNextHost.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentNextHost);
|
|
459
|
+
state.currentNextHost = null;
|
|
460
|
+
await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings);
|
|
461
|
+
}
|
|
462
|
+
async failDevSessionAfterIrrecoverableSourceError(state, error) {
|
|
463
|
+
const exception = error instanceof Error ? error : new Error(String(error));
|
|
464
|
+
state.stopRequested = true;
|
|
465
|
+
await this.stopLiveChildProcesses(state);
|
|
466
|
+
state.stopReject?.(exception);
|
|
467
|
+
}
|
|
468
|
+
async stopLiveChildProcesses(state) {
|
|
469
|
+
const children = [];
|
|
470
|
+
for (const child of [
|
|
471
|
+
state.currentUiNext,
|
|
472
|
+
state.currentNextHost,
|
|
473
|
+
state.currentGateway
|
|
474
|
+
]) if (child && child.exitCode === null && child.signalCode === null) children.push(child);
|
|
475
|
+
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
476
|
+
}
|
|
417
477
|
logConsumerDevHintWhenNeeded(devMode, gatewayPort) {
|
|
418
478
|
if (devMode !== "consumer") return;
|
|
419
479
|
this.cliLogger.info(`codemation dev (consumer): open http://127.0.0.1:${gatewayPort} — uses the packaged @codemation/next-host UI. For framework UI HMR, set CODEMATION_DEV_MODE=framework.`);
|
|
@@ -1532,8 +1592,8 @@ var DevHttpProbe = class {
|
|
|
1532
1592
|
async waitUntilUrlRespondsOk(url) {
|
|
1533
1593
|
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
1534
1594
|
try {
|
|
1535
|
-
const response = await fetch(url);
|
|
1536
|
-
if (response.ok || response.status === 404) return;
|
|
1595
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
1596
|
+
if (response.ok || response.status === 404 || this.isRedirectStatus(response.status)) return;
|
|
1537
1597
|
} catch {}
|
|
1538
1598
|
await setTimeout$1(50);
|
|
1539
1599
|
}
|
|
@@ -1562,6 +1622,9 @@ var DevHttpProbe = class {
|
|
|
1562
1622
|
}
|
|
1563
1623
|
throw new Error("Timed out waiting for dev runtime bootstrap summary.");
|
|
1564
1624
|
}
|
|
1625
|
+
isRedirectStatus(status) {
|
|
1626
|
+
return status >= 300 && status < 400;
|
|
1627
|
+
}
|
|
1565
1628
|
};
|
|
1566
1629
|
|
|
1567
1630
|
//#endregion
|
|
@@ -1571,6 +1634,24 @@ var DevNextHostEnvironmentBuilder = class {
|
|
|
1571
1634
|
this.consumerEnvLoader = consumerEnvLoader;
|
|
1572
1635
|
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
1573
1636
|
}
|
|
1637
|
+
buildConsumerUiProxy(args) {
|
|
1638
|
+
return {
|
|
1639
|
+
...this.build({
|
|
1640
|
+
authConfigJson: args.authConfigJson,
|
|
1641
|
+
consumerRoot: args.consumerRoot,
|
|
1642
|
+
developmentServerToken: args.developmentServerToken,
|
|
1643
|
+
nextPort: args.nextPort,
|
|
1644
|
+
runtimeDevUrl: args.runtimeDevUrl,
|
|
1645
|
+
skipUiAuth: args.skipUiAuth,
|
|
1646
|
+
websocketPort: args.websocketPort,
|
|
1647
|
+
consumerOutputManifestPath: args.consumerOutputManifestPath
|
|
1648
|
+
}),
|
|
1649
|
+
AUTH_SECRET: args.authSecret,
|
|
1650
|
+
AUTH_URL: args.publicBaseUrl,
|
|
1651
|
+
NEXTAUTH_SECRET: args.authSecret,
|
|
1652
|
+
NEXTAUTH_URL: args.publicBaseUrl
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1574
1655
|
build(args) {
|
|
1575
1656
|
const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env);
|
|
1576
1657
|
const manifestPath = args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
|
|
@@ -1616,7 +1697,7 @@ var DevSessionPortsResolver = class {
|
|
|
1616
1697
|
* Bundles dependencies for {@link DevCommand} so the command stays a thin orchestrator.
|
|
1617
1698
|
*/
|
|
1618
1699
|
var DevSessionServices = class {
|
|
1619
|
-
constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceRestartCoordinator) {
|
|
1700
|
+
constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceChangeClassifier, sourceRestartCoordinator) {
|
|
1620
1701
|
this.consumerEnvLoader = consumerEnvLoader;
|
|
1621
1702
|
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
1622
1703
|
this.sessionPorts = sessionPorts;
|
|
@@ -1626,10 +1707,38 @@ var DevSessionServices = class {
|
|
|
1626
1707
|
this.devAuthLoader = devAuthLoader;
|
|
1627
1708
|
this.nextHostEnvBuilder = nextHostEnvBuilder;
|
|
1628
1709
|
this.watchRootsResolver = watchRootsResolver;
|
|
1710
|
+
this.sourceChangeClassifier = sourceChangeClassifier;
|
|
1629
1711
|
this.sourceRestartCoordinator = sourceRestartCoordinator;
|
|
1630
1712
|
}
|
|
1631
1713
|
};
|
|
1632
1714
|
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/dev/DevSourceChangeClassifier.ts
|
|
1717
|
+
var DevSourceChangeClassifier = class {
|
|
1718
|
+
shouldRepublishConsumerOutput(args) {
|
|
1719
|
+
const resolvedConsumerRoot = path.resolve(args.consumerRoot);
|
|
1720
|
+
return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
|
|
1721
|
+
}
|
|
1722
|
+
requiresNextHostRestart(args) {
|
|
1723
|
+
const configPaths = new Set(this.resolveConfigPaths(args.consumerRoot));
|
|
1724
|
+
return args.changedPaths.some((changedPath) => configPaths.has(path.resolve(changedPath)));
|
|
1725
|
+
}
|
|
1726
|
+
resolveConfigPaths(consumerRoot) {
|
|
1727
|
+
const resolvedConsumerRoot = path.resolve(consumerRoot);
|
|
1728
|
+
return [
|
|
1729
|
+
path.resolve(resolvedConsumerRoot, "codemation.config.ts"),
|
|
1730
|
+
path.resolve(resolvedConsumerRoot, "codemation.config.js"),
|
|
1731
|
+
path.resolve(resolvedConsumerRoot, "src", "codemation.config.ts"),
|
|
1732
|
+
path.resolve(resolvedConsumerRoot, "src", "codemation.config.js")
|
|
1733
|
+
];
|
|
1734
|
+
}
|
|
1735
|
+
isPathInsideDirectory(filePath, directoryPath) {
|
|
1736
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
1737
|
+
const relativePath = path.relative(directoryPath, resolvedFilePath);
|
|
1738
|
+
return relativePath.length === 0 || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1633
1742
|
//#endregion
|
|
1634
1743
|
//#region src/dev/DevSourceRestartCoordinator.ts
|
|
1635
1744
|
var DevSourceRestartCoordinator = class {
|
|
@@ -1754,7 +1863,7 @@ var DevSessionServicesBuilder = class {
|
|
|
1754
1863
|
const listenPortResolver = new ListenPortResolver();
|
|
1755
1864
|
const loopbackPortAllocator = new LoopbackPortAllocator();
|
|
1756
1865
|
const cliLogger = this.loggerFactory.create("codemation-cli");
|
|
1757
|
-
return new DevSessionServices(consumerEnvLoader, sourceMapNodeOptions, new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator), loopbackPortAllocator, new DevHttpProbe(), new RuntimeToolEntrypointResolver(), new DevAuthSettingsLoader(new CodemationConsumerConfigLoader()), new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions), new WatchRootsResolver(), new DevSourceRestartCoordinator(new DevelopmentGatewayNotifier(cliLogger), this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"), cliLogger));
|
|
1866
|
+
return new DevSessionServices(consumerEnvLoader, sourceMapNodeOptions, new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator), loopbackPortAllocator, new DevHttpProbe(), new RuntimeToolEntrypointResolver(), new DevAuthSettingsLoader(new CodemationConsumerConfigLoader()), new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions), new WatchRootsResolver(), new DevSourceChangeClassifier(), new DevSourceRestartCoordinator(new DevelopmentGatewayNotifier(cliLogger), this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"), cliLogger));
|
|
1758
1867
|
}
|
|
1759
1868
|
};
|
|
1760
1869
|
|
package/dist/bin.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -23118,6 +23118,7 @@ declare class DevHttpProbe {
|
|
|
23118
23118
|
* Polls until the runtime child serves bootstrap summary (after gateway is up, the disposable runtime may still be wiring).
|
|
23119
23119
|
*/
|
|
23120
23120
|
waitUntilBootstrapSummaryReady(gatewayBaseUrl: string): Promise<void>;
|
|
23121
|
+
private isRedirectStatus;
|
|
23121
23122
|
}
|
|
23122
23123
|
//#endregion
|
|
23123
23124
|
//#region src/dev/DevNextHostEnvironmentBuilder.d.ts
|
|
@@ -23125,6 +23126,18 @@ declare class DevNextHostEnvironmentBuilder {
|
|
|
23125
23126
|
private readonly consumerEnvLoader;
|
|
23126
23127
|
private readonly sourceMapNodeOptions;
|
|
23127
23128
|
constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions);
|
|
23129
|
+
buildConsumerUiProxy(args: Readonly<{
|
|
23130
|
+
authConfigJson: string;
|
|
23131
|
+
authSecret: string;
|
|
23132
|
+
consumerRoot: string;
|
|
23133
|
+
developmentServerToken: string;
|
|
23134
|
+
nextPort: number;
|
|
23135
|
+
publicBaseUrl: string;
|
|
23136
|
+
runtimeDevUrl: string;
|
|
23137
|
+
skipUiAuth: boolean;
|
|
23138
|
+
websocketPort: number;
|
|
23139
|
+
consumerOutputManifestPath?: string;
|
|
23140
|
+
}>): NodeJS.ProcessEnv;
|
|
23128
23141
|
build(args: Readonly<{
|
|
23129
23142
|
authConfigJson: string;
|
|
23130
23143
|
consumerRoot: string;
|
|
@@ -23172,6 +23185,20 @@ declare class DevSessionPortsResolver {
|
|
|
23172
23185
|
}>>;
|
|
23173
23186
|
}
|
|
23174
23187
|
//#endregion
|
|
23188
|
+
//#region src/dev/DevSourceChangeClassifier.d.ts
|
|
23189
|
+
declare class DevSourceChangeClassifier {
|
|
23190
|
+
shouldRepublishConsumerOutput(args: Readonly<{
|
|
23191
|
+
changedPaths: ReadonlyArray<string>;
|
|
23192
|
+
consumerRoot: string;
|
|
23193
|
+
}>): boolean;
|
|
23194
|
+
requiresNextHostRestart(args: Readonly<{
|
|
23195
|
+
changedPaths: ReadonlyArray<string>;
|
|
23196
|
+
consumerRoot: string;
|
|
23197
|
+
}>): boolean;
|
|
23198
|
+
private resolveConfigPaths;
|
|
23199
|
+
private isPathInsideDirectory;
|
|
23200
|
+
}
|
|
23201
|
+
//#endregion
|
|
23175
23202
|
//#region src/dev/DevelopmentGatewayNotifier.d.ts
|
|
23176
23203
|
declare class DevelopmentGatewayNotifier {
|
|
23177
23204
|
private readonly cliLogger;
|
|
@@ -23235,8 +23262,9 @@ declare class DevSessionServices {
|
|
|
23235
23262
|
readonly devAuthLoader: DevAuthSettingsLoader;
|
|
23236
23263
|
readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder;
|
|
23237
23264
|
readonly watchRootsResolver: WatchRootsResolver;
|
|
23265
|
+
readonly sourceChangeClassifier: DevSourceChangeClassifier;
|
|
23238
23266
|
readonly sourceRestartCoordinator: DevSourceRestartCoordinator;
|
|
23239
|
-
constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions, sessionPorts: DevSessionPortsResolver, loopbackPortAllocator: LoopbackPortAllocator, devHttpProbe: DevHttpProbe, runtimeEntrypointResolver: RuntimeToolEntrypointResolver, devAuthLoader: DevAuthSettingsLoader, nextHostEnvBuilder: DevNextHostEnvironmentBuilder, watchRootsResolver: WatchRootsResolver, sourceRestartCoordinator: DevSourceRestartCoordinator);
|
|
23267
|
+
constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions, sessionPorts: DevSessionPortsResolver, loopbackPortAllocator: LoopbackPortAllocator, devHttpProbe: DevHttpProbe, runtimeEntrypointResolver: RuntimeToolEntrypointResolver, devAuthLoader: DevAuthSettingsLoader, nextHostEnvBuilder: DevNextHostEnvironmentBuilder, watchRootsResolver: WatchRootsResolver, sourceChangeClassifier: DevSourceChangeClassifier, sourceRestartCoordinator: DevSourceRestartCoordinator);
|
|
23240
23268
|
}
|
|
23241
23269
|
//#endregion
|
|
23242
23270
|
//#region src/dev/DevLock.d.ts
|
|
@@ -23345,6 +23373,7 @@ declare class DevCommand {
|
|
|
23345
23373
|
* Framework mode: no separate UI child (Next runs in dev later).
|
|
23346
23374
|
*/
|
|
23347
23375
|
private startConsumerUiProxyWhenNeeded;
|
|
23376
|
+
private spawnConsumerUiProxy;
|
|
23348
23377
|
private spawnGatewayChildAndWaitForHealth;
|
|
23349
23378
|
private devDetachedChildSpawnOptions;
|
|
23350
23379
|
private bindShutdownSignalsToChildProcesses;
|
|
@@ -23352,7 +23381,13 @@ declare class DevCommand {
|
|
|
23352
23381
|
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
23353
23382
|
*/
|
|
23354
23383
|
private spawnFrameworkNextHostWhenNeeded;
|
|
23384
|
+
private spawnFrameworkNextHost;
|
|
23355
23385
|
private startWatcherForSourceRestart;
|
|
23386
|
+
private restartNextHostForConfigChange;
|
|
23387
|
+
private restartConsumerUiProxy;
|
|
23388
|
+
private restartFrameworkNextHost;
|
|
23389
|
+
private failDevSessionAfterIrrecoverableSourceError;
|
|
23390
|
+
private stopLiveChildProcesses;
|
|
23356
23391
|
private logConsumerDevHintWhenNeeded;
|
|
23357
23392
|
}
|
|
23358
23393
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as CliProgram, i as CliPathResolver, n as CliProgramFactory, o as ConsumerOutputBuilder, r as CodemationCliApplicationSession, s as ConsumerBuildOptionsParser, t as CliBin } from "./CliBin-
|
|
1
|
+
import { a as CliProgram, i as CliPathResolver, n as CliProgramFactory, o as ConsumerOutputBuilder, r as CodemationCliApplicationSession, s as ConsumerBuildOptionsParser, t as CliBin } from "./CliBin-C3ar49fj.js";
|
|
2
2
|
import { CodemationPluginDiscovery } from "@codemation/host/server";
|
|
3
3
|
|
|
4
4
|
export { CliBin, CliPathResolver, CliProgram, CliProgramFactory, CodemationCliApplicationSession, CodemationPluginDiscovery, ConsumerBuildOptionsParser, ConsumerOutputBuilder };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"figlet": "^1.11.0",
|
|
29
29
|
"reflect-metadata": "^0.2.2",
|
|
30
30
|
"typescript": "^5.9.3",
|
|
31
|
-
"@codemation/dev-gateway": "0.0.
|
|
32
|
-
"@codemation/host": "0.0.
|
|
33
|
-
"@codemation/
|
|
34
|
-
"@codemation/
|
|
35
|
-
"@codemation/worker-cli": "0.0.
|
|
31
|
+
"@codemation/dev-gateway": "0.0.5",
|
|
32
|
+
"@codemation/host": "0.0.5",
|
|
33
|
+
"@codemation/next-host": "0.0.5",
|
|
34
|
+
"@codemation/runtime-dev": "0.0.5",
|
|
35
|
+
"@codemation/worker-cli": "0.0.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^25.3.5",
|
|
@@ -62,9 +62,9 @@ export class DevCommand {
|
|
|
62
62
|
});
|
|
63
63
|
const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
|
|
64
64
|
const watcher = this.devSourceWatcherFactory.create();
|
|
65
|
+
const processState = this.createInitialProcessState();
|
|
65
66
|
try {
|
|
66
67
|
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
67
|
-
const processState = this.createInitialProcessState();
|
|
68
68
|
const stopPromise = this.wireStopPromise(processState);
|
|
69
69
|
const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
|
|
70
70
|
const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
|
|
@@ -75,11 +75,13 @@ export class DevCommand {
|
|
|
75
75
|
this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
|
|
76
76
|
}
|
|
77
77
|
this.bindShutdownSignalsToChildProcesses(processState);
|
|
78
|
-
this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
79
|
-
await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
|
|
78
|
+
await this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
79
|
+
await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
|
|
80
80
|
this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
|
|
81
81
|
await stopPromise;
|
|
82
82
|
} finally {
|
|
83
|
+
processState.stopRequested = true;
|
|
84
|
+
await this.stopLiveChildProcesses(processState);
|
|
83
85
|
await watcher.stop();
|
|
84
86
|
await devLock.release();
|
|
85
87
|
}
|
|
@@ -132,6 +134,8 @@ export class DevCommand {
|
|
|
132
134
|
currentGateway: null,
|
|
133
135
|
currentNextHost: null,
|
|
134
136
|
currentUiNext: null,
|
|
137
|
+
currentUiProxyBaseUrl: null,
|
|
138
|
+
isRestartingNextHost: false,
|
|
135
139
|
stopRequested: false,
|
|
136
140
|
stopResolve: null,
|
|
137
141
|
stopReject: null,
|
|
@@ -161,8 +165,19 @@ export class DevCommand {
|
|
|
161
165
|
return "";
|
|
162
166
|
}
|
|
163
167
|
const websocketPort = prepared.gatewayPort;
|
|
164
|
-
const
|
|
165
|
-
|
|
168
|
+
const uiProxyBase = state.currentUiProxyBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
|
|
169
|
+
state.currentUiProxyBaseUrl = uiProxyBase;
|
|
170
|
+
await this.spawnConsumerUiProxy(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
|
|
171
|
+
return uiProxyBase;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async spawnConsumerUiProxy(
|
|
175
|
+
prepared: DevPreparedRuntime,
|
|
176
|
+
state: DevMutableProcessState,
|
|
177
|
+
authSettings: DevResolvedAuthSettings,
|
|
178
|
+
websocketPort: number,
|
|
179
|
+
uiProxyBase: string,
|
|
180
|
+
): Promise<void> {
|
|
166
181
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
167
182
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
168
183
|
const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
|
|
@@ -172,36 +187,33 @@ export class DevCommand {
|
|
|
172
187
|
"output",
|
|
173
188
|
"current.json",
|
|
174
189
|
);
|
|
190
|
+
const uiPort = Number(new URL(uiProxyBase).port);
|
|
191
|
+
const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
|
|
192
|
+
authConfigJson: authSettings.authConfigJson,
|
|
193
|
+
authSecret: authSettings.authSecret,
|
|
194
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
195
|
+
consumerOutputManifestPath,
|
|
196
|
+
developmentServerToken: prepared.developmentServerToken,
|
|
197
|
+
nextPort: uiPort,
|
|
198
|
+
publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
199
|
+
runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
|
|
200
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
201
|
+
websocketPort,
|
|
202
|
+
});
|
|
175
203
|
state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
|
|
176
204
|
cwd: nextHostCommand.cwd,
|
|
177
205
|
...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
|
-
},
|
|
206
|
+
env: nextHostEnvironment,
|
|
196
207
|
});
|
|
197
208
|
state.currentUiNext.on("error", (error) => {
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
209
|
+
if (state.stopRequested || state.isRestartingNextHost) {
|
|
210
|
+
return;
|
|
201
211
|
}
|
|
212
|
+
state.stopRequested = true;
|
|
213
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
202
214
|
});
|
|
203
215
|
state.currentUiNext.on("exit", (code) => {
|
|
204
|
-
if (state.stopRequested) {
|
|
216
|
+
if (state.stopRequested || state.isRestartingNextHost) {
|
|
205
217
|
return;
|
|
206
218
|
}
|
|
207
219
|
state.stopRequested = true;
|
|
@@ -211,7 +223,6 @@ export class DevCommand {
|
|
|
211
223
|
state.stopReject?.(new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
212
224
|
});
|
|
213
225
|
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
214
|
-
return uiProxyBase;
|
|
215
226
|
}
|
|
216
227
|
|
|
217
228
|
private async spawnGatewayChildAndWaitForHealth(
|
|
@@ -301,23 +312,32 @@ export class DevCommand {
|
|
|
301
312
|
/**
|
|
302
313
|
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
303
314
|
*/
|
|
304
|
-
private spawnFrameworkNextHostWhenNeeded(
|
|
315
|
+
private async spawnFrameworkNextHostWhenNeeded(
|
|
305
316
|
prepared: DevPreparedRuntime,
|
|
306
317
|
state: DevMutableProcessState,
|
|
307
318
|
gatewayBaseUrl: string,
|
|
308
|
-
): void {
|
|
319
|
+
): Promise<void> {
|
|
309
320
|
if (prepared.devMode !== "framework") {
|
|
310
321
|
return;
|
|
311
322
|
}
|
|
323
|
+
await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, prepared.authSettings);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async spawnFrameworkNextHost(
|
|
327
|
+
prepared: DevPreparedRuntime,
|
|
328
|
+
state: DevMutableProcessState,
|
|
329
|
+
gatewayBaseUrl: string,
|
|
330
|
+
authSettings: DevResolvedAuthSettings,
|
|
331
|
+
): Promise<void> {
|
|
312
332
|
const websocketPort = prepared.gatewayPort;
|
|
313
333
|
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
314
334
|
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
315
335
|
const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
|
|
316
|
-
authConfigJson:
|
|
336
|
+
authConfigJson: authSettings.authConfigJson,
|
|
317
337
|
consumerRoot: prepared.paths.consumerRoot,
|
|
318
338
|
developmentServerToken: prepared.developmentServerToken,
|
|
319
339
|
nextPort: prepared.nextPort,
|
|
320
|
-
skipUiAuth:
|
|
340
|
+
skipUiAuth: authSettings.skipUiAuth,
|
|
321
341
|
websocketPort,
|
|
322
342
|
runtimeDevUrl: gatewayBaseUrl,
|
|
323
343
|
});
|
|
@@ -328,7 +348,7 @@ export class DevCommand {
|
|
|
328
348
|
});
|
|
329
349
|
state.currentNextHost.on("exit", (code) => {
|
|
330
350
|
const normalizedCode = code ?? 0;
|
|
331
|
-
if (state.stopRequested) {
|
|
351
|
+
if (state.stopRequested || state.isRestartingNextHost) {
|
|
332
352
|
return;
|
|
333
353
|
}
|
|
334
354
|
if (normalizedCode === 0) {
|
|
@@ -343,15 +363,18 @@ export class DevCommand {
|
|
|
343
363
|
state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
|
|
344
364
|
});
|
|
345
365
|
state.currentNextHost.on("error", (error) => {
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
366
|
+
if (state.stopRequested || state.isRestartingNextHost) {
|
|
367
|
+
return;
|
|
349
368
|
}
|
|
369
|
+
state.stopRequested = true;
|
|
370
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
350
371
|
});
|
|
372
|
+
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
|
|
351
373
|
}
|
|
352
374
|
|
|
353
375
|
private async startWatcherForSourceRestart(
|
|
354
376
|
prepared: DevPreparedRuntime,
|
|
377
|
+
state: DevMutableProcessState,
|
|
355
378
|
watcher: DevSourceWatcher,
|
|
356
379
|
devMode: DevMode,
|
|
357
380
|
gatewayBaseUrl: string,
|
|
@@ -369,22 +392,112 @@ export class DevCommand {
|
|
|
369
392
|
);
|
|
370
393
|
return;
|
|
371
394
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
395
|
+
try {
|
|
396
|
+
const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
|
|
397
|
+
changedPaths,
|
|
398
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
399
|
+
});
|
|
400
|
+
const shouldRestartNextHost = this.session.sourceChangeClassifier.requiresNextHostRestart({
|
|
401
|
+
changedPaths,
|
|
402
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
403
|
+
});
|
|
404
|
+
process.stdout.write(
|
|
405
|
+
shouldRestartNextHost
|
|
406
|
+
? "\n[codemation] Consumer config change detected — rebuilding consumer, restarting runtime, and restarting Next host…\n"
|
|
407
|
+
: "\n[codemation] Source change detected — rebuilding consumer and restarting runtime…\n",
|
|
408
|
+
);
|
|
409
|
+
if (shouldRepublishConsumerOutput) {
|
|
410
|
+
await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
|
|
411
|
+
}
|
|
412
|
+
await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(
|
|
413
|
+
gatewayBaseUrl,
|
|
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");
|
|
426
|
+
} catch (error) {
|
|
427
|
+
await this.failDevSessionAfterIrrecoverableSourceError(state, error);
|
|
382
428
|
}
|
|
383
|
-
process.stdout.write("[codemation] Runtime ready.\n");
|
|
384
429
|
},
|
|
385
430
|
});
|
|
386
431
|
}
|
|
387
432
|
|
|
433
|
+
private async restartNextHostForConfigChange(
|
|
434
|
+
prepared: DevPreparedRuntime,
|
|
435
|
+
state: DevMutableProcessState,
|
|
436
|
+
gatewayBaseUrl: string,
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
|
|
439
|
+
process.stdout.write("[codemation] Restarting Next host to apply auth/database/scheduler/branding changes…\n");
|
|
440
|
+
state.isRestartingNextHost = true;
|
|
441
|
+
try {
|
|
442
|
+
if (prepared.devMode === "consumer") {
|
|
443
|
+
await this.restartConsumerUiProxy(prepared, state, refreshedAuthSettings);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
await this.restartFrameworkNextHost(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
|
|
447
|
+
} finally {
|
|
448
|
+
state.isRestartingNextHost = false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async restartConsumerUiProxy(
|
|
453
|
+
prepared: DevPreparedRuntime,
|
|
454
|
+
state: DevMutableProcessState,
|
|
455
|
+
authSettings: DevResolvedAuthSettings,
|
|
456
|
+
): Promise<void> {
|
|
457
|
+
if (state.currentUiNext && state.currentUiNext.exitCode === null && state.currentUiNext.signalCode === null) {
|
|
458
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentUiNext);
|
|
459
|
+
}
|
|
460
|
+
state.currentUiNext = null;
|
|
461
|
+
const uiProxyBaseUrl = state.currentUiProxyBaseUrl;
|
|
462
|
+
if (!uiProxyBaseUrl) {
|
|
463
|
+
throw new Error("Consumer UI proxy base URL is missing during Next host restart.");
|
|
464
|
+
}
|
|
465
|
+
await this.spawnConsumerUiProxy(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private async restartFrameworkNextHost(
|
|
469
|
+
prepared: DevPreparedRuntime,
|
|
470
|
+
state: DevMutableProcessState,
|
|
471
|
+
gatewayBaseUrl: string,
|
|
472
|
+
authSettings: DevResolvedAuthSettings,
|
|
473
|
+
): Promise<void> {
|
|
474
|
+
if (state.currentNextHost && state.currentNextHost.exitCode === null && state.currentNextHost.signalCode === null) {
|
|
475
|
+
await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentNextHost);
|
|
476
|
+
}
|
|
477
|
+
state.currentNextHost = null;
|
|
478
|
+
await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private async failDevSessionAfterIrrecoverableSourceError(
|
|
482
|
+
state: DevMutableProcessState,
|
|
483
|
+
error: unknown,
|
|
484
|
+
): Promise<void> {
|
|
485
|
+
const exception = error instanceof Error ? error : new Error(String(error));
|
|
486
|
+
state.stopRequested = true;
|
|
487
|
+
await this.stopLiveChildProcesses(state);
|
|
488
|
+
state.stopReject?.(exception);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private async stopLiveChildProcesses(state: DevMutableProcessState): Promise<void> {
|
|
492
|
+
const children: ChildProcess[] = [];
|
|
493
|
+
for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
|
|
494
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
495
|
+
children.push(child);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
499
|
+
}
|
|
500
|
+
|
|
388
501
|
private logConsumerDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
|
|
389
502
|
if (devMode !== "consumer") {
|
|
390
503
|
return;
|
|
@@ -11,6 +11,8 @@ export type DevMutableProcessState = {
|
|
|
11
11
|
currentGateway: ChildProcess | null;
|
|
12
12
|
currentNextHost: ChildProcess | null;
|
|
13
13
|
currentUiNext: ChildProcess | null;
|
|
14
|
+
currentUiProxyBaseUrl: string | null;
|
|
15
|
+
isRestartingNextHost: boolean;
|
|
14
16
|
stopRequested: boolean;
|
|
15
17
|
stopResolve: (() => void) | null;
|
|
16
18
|
stopReject: ((error: Error) => void) | null;
|
package/src/dev/Builder.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { DevHttpProbe } from "./DevHttpProbe";
|
|
|
11
11
|
import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
|
|
12
12
|
import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
|
|
13
13
|
import { DevSessionServices } from "./DevSessionServices";
|
|
14
|
+
import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
|
|
14
15
|
import { DevSourceRestartCoordinator } from "./DevSourceRestartCoordinator";
|
|
15
16
|
import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
|
|
16
17
|
import { RuntimeToolEntrypointResolver } from "./RuntimeToolEntrypointResolver";
|
|
@@ -35,6 +36,7 @@ export class DevSessionServicesBuilder {
|
|
|
35
36
|
new DevAuthSettingsLoader(new CodemationConsumerConfigLoader()),
|
|
36
37
|
new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions),
|
|
37
38
|
new WatchRootsResolver(),
|
|
39
|
+
new DevSourceChangeClassifier(),
|
|
38
40
|
new DevSourceRestartCoordinator(
|
|
39
41
|
new DevelopmentGatewayNotifier(cliLogger),
|
|
40
42
|
this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"),
|
package/src/dev/DevHttpProbe.ts
CHANGED
|
@@ -4,8 +4,8 @@ export class DevHttpProbe {
|
|
|
4
4
|
async waitUntilUrlRespondsOk(url: string): Promise<void> {
|
|
5
5
|
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
6
6
|
try {
|
|
7
|
-
const response = await fetch(url);
|
|
8
|
-
if (response.ok || response.status === 404) {
|
|
7
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
8
|
+
if (response.ok || response.status === 404 || this.isRedirectStatus(response.status)) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
} catch {
|
|
@@ -51,4 +51,8 @@ export class DevHttpProbe {
|
|
|
51
51
|
}
|
|
52
52
|
throw new Error("Timed out waiting for dev runtime bootstrap summary.");
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
private isRedirectStatus(status: number): boolean {
|
|
56
|
+
return status >= 300 && status < 400;
|
|
57
|
+
}
|
|
54
58
|
}
|
|
@@ -10,6 +10,38 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
10
10
|
private readonly sourceMapNodeOptions: SourceMapNodeOptions,
|
|
11
11
|
) {}
|
|
12
12
|
|
|
13
|
+
buildConsumerUiProxy(
|
|
14
|
+
args: Readonly<{
|
|
15
|
+
authConfigJson: string;
|
|
16
|
+
authSecret: string;
|
|
17
|
+
consumerRoot: string;
|
|
18
|
+
developmentServerToken: string;
|
|
19
|
+
nextPort: number;
|
|
20
|
+
publicBaseUrl: string;
|
|
21
|
+
runtimeDevUrl: string;
|
|
22
|
+
skipUiAuth: boolean;
|
|
23
|
+
websocketPort: number;
|
|
24
|
+
consumerOutputManifestPath?: string;
|
|
25
|
+
}>,
|
|
26
|
+
): NodeJS.ProcessEnv {
|
|
27
|
+
return {
|
|
28
|
+
...this.build({
|
|
29
|
+
authConfigJson: args.authConfigJson,
|
|
30
|
+
consumerRoot: args.consumerRoot,
|
|
31
|
+
developmentServerToken: args.developmentServerToken,
|
|
32
|
+
nextPort: args.nextPort,
|
|
33
|
+
runtimeDevUrl: args.runtimeDevUrl,
|
|
34
|
+
skipUiAuth: args.skipUiAuth,
|
|
35
|
+
websocketPort: args.websocketPort,
|
|
36
|
+
consumerOutputManifestPath: args.consumerOutputManifestPath,
|
|
37
|
+
}),
|
|
38
|
+
AUTH_SECRET: args.authSecret,
|
|
39
|
+
AUTH_URL: args.publicBaseUrl,
|
|
40
|
+
NEXTAUTH_SECRET: args.authSecret,
|
|
41
|
+
NEXTAUTH_URL: args.publicBaseUrl,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
build(
|
|
14
46
|
args: Readonly<{
|
|
15
47
|
authConfigJson: string;
|
|
@@ -5,6 +5,7 @@ import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
|
|
|
5
5
|
import { DevHttpProbe } from "./DevHttpProbe";
|
|
6
6
|
import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
|
|
7
7
|
import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
|
|
8
|
+
import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
|
|
8
9
|
import { DevSourceRestartCoordinator } from "./DevSourceRestartCoordinator";
|
|
9
10
|
import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
|
|
10
11
|
import { RuntimeToolEntrypointResolver } from "./RuntimeToolEntrypointResolver";
|
|
@@ -24,6 +25,7 @@ export class DevSessionServices {
|
|
|
24
25
|
readonly devAuthLoader: DevAuthSettingsLoader,
|
|
25
26
|
readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder,
|
|
26
27
|
readonly watchRootsResolver: WatchRootsResolver,
|
|
28
|
+
readonly sourceChangeClassifier: DevSourceChangeClassifier,
|
|
27
29
|
readonly sourceRestartCoordinator: DevSourceRestartCoordinator,
|
|
28
30
|
) {}
|
|
29
31
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export class DevSourceChangeClassifier {
|
|
4
|
+
shouldRepublishConsumerOutput(
|
|
5
|
+
args: Readonly<{
|
|
6
|
+
changedPaths: ReadonlyArray<string>;
|
|
7
|
+
consumerRoot: string;
|
|
8
|
+
}>,
|
|
9
|
+
): boolean {
|
|
10
|
+
const resolvedConsumerRoot = path.resolve(args.consumerRoot);
|
|
11
|
+
return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
requiresNextHostRestart(
|
|
15
|
+
args: Readonly<{
|
|
16
|
+
changedPaths: ReadonlyArray<string>;
|
|
17
|
+
consumerRoot: string;
|
|
18
|
+
}>,
|
|
19
|
+
): boolean {
|
|
20
|
+
const configPaths = new Set(this.resolveConfigPaths(args.consumerRoot));
|
|
21
|
+
return args.changedPaths.some((changedPath) => configPaths.has(path.resolve(changedPath)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private resolveConfigPaths(consumerRoot: string): ReadonlyArray<string> {
|
|
25
|
+
const resolvedConsumerRoot = path.resolve(consumerRoot);
|
|
26
|
+
return [
|
|
27
|
+
path.resolve(resolvedConsumerRoot, "codemation.config.ts"),
|
|
28
|
+
path.resolve(resolvedConsumerRoot, "codemation.config.js"),
|
|
29
|
+
path.resolve(resolvedConsumerRoot, "src", "codemation.config.ts"),
|
|
30
|
+
path.resolve(resolvedConsumerRoot, "src", "codemation.config.js"),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private isPathInsideDirectory(filePath: string, directoryPath: string): boolean {
|
|
35
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
36
|
+
const relativePath = path.relative(directoryPath, resolvedFilePath);
|
|
37
|
+
return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
38
|
+
}
|
|
39
|
+
}
|