@lark-apaas/fullstack-cli 1.1.59 → 1.1.61-alpha.20260818172555
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/dist/client-dependency-graph.d.ts +37 -0
- package/dist/client-dependency-graph.js +466 -0
- package/dist/index.js +3482 -143
- package/package.json +4 -1
- package/templates/scripts/cache-generation-preflight.mjs +1346 -0
- package/templates/scripts/cache-generation-prune.mjs +174 -0
- package/templates/scripts/cache-runtime-coordinator.mjs +487 -0
- package/templates/scripts/dev.js +433 -59
- package/templates/scripts/dev.sh +2 -0
- package/templates/scripts/lint.js +2 -25
- package/templates/scripts/patch-vite-dependency-graph-hash.mjs +141 -0
- package/templates/scripts/server-cache-module-resolver.cjs +63 -0
- package/templates/scripts/server-cache-runtime.mjs +681 -0
- package/templates/scripts/server-startup-runtime.mjs +496 -0
- package/templates/scripts/server-transition-runtime.mjs +655 -0
- package/templates/scripts/vite-cache-runtime.mjs +3100 -0
- package/templates/scripts/workspace-client-runtime.mjs +176 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
validateCacheGeneration,
|
|
12
|
+
validateCacheGenerationReceipt,
|
|
13
|
+
writeCacheGenerationValidationReceipt,
|
|
14
|
+
} from './cache-generation-preflight.mjs';
|
|
15
|
+
|
|
16
|
+
const projectRoot = path.resolve(
|
|
17
|
+
process.env.MIAODA_WORKSPACE_ROOT || process.cwd()
|
|
18
|
+
);
|
|
19
|
+
const defaultCachePointer = path.join(projectRoot, '.miaoda-cache', 'current');
|
|
20
|
+
const requestedCacheRoot = path.resolve(
|
|
21
|
+
process.env.MIAODA_CACHE_ROOT || defaultCachePointer
|
|
22
|
+
);
|
|
23
|
+
const cacheRoot = fs.realpathSync(requestedCacheRoot);
|
|
24
|
+
if (!process.env.MIAODA_CACHE_ROOT) {
|
|
25
|
+
const generationStore = fs.realpathSync(
|
|
26
|
+
path.join(projectRoot, '.miaoda-cache', 'generations')
|
|
27
|
+
);
|
|
28
|
+
const generationRelative = path.relative(generationStore, cacheRoot);
|
|
29
|
+
if (!/^[a-f0-9]{64}$/.test(generationRelative)) {
|
|
30
|
+
throw new Error('MIAODA_CACHE_GENERATION_POINTER_REJECTED');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const runtimeManifestFile = path.resolve(
|
|
34
|
+
process.env.MIAODA_PLATFORM_RUNTIME_MANIFEST ||
|
|
35
|
+
'/opt/miaoda/preview-runtime/runtime-manifest.json'
|
|
36
|
+
);
|
|
37
|
+
const validateOnly = process.argv.includes('--validate-only');
|
|
38
|
+
const artifactScopeOptionIndex = process.argv.indexOf('--artifact-scope');
|
|
39
|
+
const validationArtifactScope =
|
|
40
|
+
artifactScopeOptionIndex >= 0
|
|
41
|
+
? process.argv[artifactScopeOptionIndex + 1]
|
|
42
|
+
: 'all';
|
|
43
|
+
const skipWorkspaceInputs = process.argv.includes('--skip-workspace-inputs');
|
|
44
|
+
const servingReadyMarker = '/tmp/event/MIAODA_NEST_CACHE_SERVING_READY';
|
|
45
|
+
const skipConfigEnvironment = process.argv.includes(
|
|
46
|
+
'--skip-config-environment'
|
|
47
|
+
);
|
|
48
|
+
const runtimeStartedAt = process.hrtime.bigint();
|
|
49
|
+
const durationMs = startedAt =>
|
|
50
|
+
Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
51
|
+
function emitRuntimePhase(phase, startedAt, details = {}) {
|
|
52
|
+
if (validateOnly) return;
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
`${JSON.stringify({
|
|
55
|
+
event: 'miaoda_cache_runtime_phase',
|
|
56
|
+
runtime: 'nest',
|
|
57
|
+
phase,
|
|
58
|
+
durationMs: Number(durationMs(startedAt).toFixed(3)),
|
|
59
|
+
...details,
|
|
60
|
+
})}\n`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function publishServingReadyMarker(file) {
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
const temporaryFile = `${file}.${process.pid}.tmp`;
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
temporaryFile,
|
|
68
|
+
`${JSON.stringify({ schemaVersion: 1, readyAt: new Date().toISOString() })}\n`,
|
|
69
|
+
{ mode: 0o600 }
|
|
70
|
+
);
|
|
71
|
+
fs.renameSync(temporaryFile, file);
|
|
72
|
+
}
|
|
73
|
+
if (!validateOnly) {
|
|
74
|
+
fs.rmSync(servingReadyMarker, { force: true });
|
|
75
|
+
process.once('exit', () => {
|
|
76
|
+
try {
|
|
77
|
+
fs.rmSync(servingReadyMarker, { force: true });
|
|
78
|
+
} catch {}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (skipConfigEnvironment && !validateOnly) {
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
'MIAODA_CACHE_CONFIG_ENVIRONMENT_SKIP_REQUIRES_VALIDATE_ONLY\n'
|
|
84
|
+
);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
(!validateOnly && artifactScopeOptionIndex >= 0) ||
|
|
89
|
+
(!validateOnly && skipWorkspaceInputs) ||
|
|
90
|
+
!['all', 'server', 'vite'].includes(validationArtifactScope) ||
|
|
91
|
+
(skipWorkspaceInputs && validationArtifactScope !== 'vite')
|
|
92
|
+
) {
|
|
93
|
+
process.stderr.write('MIAODA_CACHE_VALIDATION_OPTIONS_INVALID\n');
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let validated;
|
|
98
|
+
let validatedBy = 'full-sha';
|
|
99
|
+
let validationReceiptMiss;
|
|
100
|
+
const validationStartedAt = process.hrtime.bigint();
|
|
101
|
+
try {
|
|
102
|
+
const validationReceiptFile =
|
|
103
|
+
process.env.MIAODA_CACHE_VALIDATION_RECEIPT_FILE;
|
|
104
|
+
if (validationReceiptFile && !skipWorkspaceInputs) {
|
|
105
|
+
try {
|
|
106
|
+
validated = validateCacheGenerationReceipt({
|
|
107
|
+
projectRoot,
|
|
108
|
+
cacheRoot,
|
|
109
|
+
runtimeManifestFile,
|
|
110
|
+
receiptFile: validationReceiptFile,
|
|
111
|
+
validateConfigEnvironment: !skipConfigEnvironment,
|
|
112
|
+
artifactScope: validateOnly ? validationArtifactScope : 'server',
|
|
113
|
+
});
|
|
114
|
+
validatedBy = 'platform-receipt';
|
|
115
|
+
} catch (error) {
|
|
116
|
+
validationReceiptMiss = String(
|
|
117
|
+
error instanceof Error ? error.message : error
|
|
118
|
+
).split(':')[0];
|
|
119
|
+
// A missing/stale receipt is only a fast-path miss. Preserve the legacy
|
|
120
|
+
// full preflight and dependency-backed fallback semantics below.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!validated) {
|
|
124
|
+
validated = validateCacheGeneration({
|
|
125
|
+
projectRoot,
|
|
126
|
+
cacheRoot,
|
|
127
|
+
runtimeManifestFile,
|
|
128
|
+
validateConfigEnvironment: !skipConfigEnvironment,
|
|
129
|
+
artifactScope: validateOnly ? validationArtifactScope : 'server',
|
|
130
|
+
validateWorkspaceInputs: !skipWorkspaceInputs,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
!process.env.MIAODA_CACHE_ROOT &&
|
|
135
|
+
path.basename(cacheRoot) !== validated.generation.generationId
|
|
136
|
+
) {
|
|
137
|
+
throw new Error('MIAODA_CACHE_GENERATION_POINTER_ID_MISMATCH');
|
|
138
|
+
}
|
|
139
|
+
if (validateOnly) {
|
|
140
|
+
if (
|
|
141
|
+
validationReceiptFile &&
|
|
142
|
+
validatedBy === 'full-sha' &&
|
|
143
|
+
!skipWorkspaceInputs
|
|
144
|
+
) {
|
|
145
|
+
writeCacheGenerationValidationReceipt({
|
|
146
|
+
validated,
|
|
147
|
+
receiptFile: validationReceiptFile,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write(
|
|
151
|
+
`${JSON.stringify({
|
|
152
|
+
valid: true,
|
|
153
|
+
generationId: validated.generation.generationId,
|
|
154
|
+
artifactScope: validationArtifactScope,
|
|
155
|
+
workspaceInputsValidated: !skipWorkspaceInputs,
|
|
156
|
+
validatedBy,
|
|
157
|
+
receiptWritten: Boolean(
|
|
158
|
+
validationReceiptFile &&
|
|
159
|
+
validatedBy === 'full-sha' &&
|
|
160
|
+
!skipWorkspaceInputs
|
|
161
|
+
),
|
|
162
|
+
})}\n`
|
|
163
|
+
);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
emitRuntimePhase('generation_validation', validationStartedAt, {
|
|
171
|
+
artifactScope: 'server',
|
|
172
|
+
validatedBy,
|
|
173
|
+
...(validationReceiptMiss ? { validationReceiptMiss } : {}),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const publicPort = Number(process.env.SERVER_PORT || 3000);
|
|
177
|
+
const publicHost = process.env.SERVER_HOST || '0.0.0.0';
|
|
178
|
+
const dependencyReadyFile = path.resolve(
|
|
179
|
+
process.env.MIAODA_DEPENDENCY_READY_FILE ||
|
|
180
|
+
path.join(projectRoot, '.miaoda-runtime', 'dependencies-ready.json')
|
|
181
|
+
);
|
|
182
|
+
function positiveDuration(name, fallback) {
|
|
183
|
+
const value = Number(process.env[name] || fallback);
|
|
184
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
185
|
+
}
|
|
186
|
+
const sourceReadyTimeoutMs = positiveDuration(
|
|
187
|
+
'MIAODA_NEST_SOURCE_READY_TIMEOUT_MS',
|
|
188
|
+
60_000
|
|
189
|
+
);
|
|
190
|
+
const cacheDrainTimeoutMs = positiveDuration(
|
|
191
|
+
'MIAODA_NEST_CACHE_DRAIN_TIMEOUT_MS',
|
|
192
|
+
5_000
|
|
193
|
+
);
|
|
194
|
+
const serviceId = randomUUID();
|
|
195
|
+
let stopping = false;
|
|
196
|
+
|
|
197
|
+
function resolveProbeHost(host) {
|
|
198
|
+
const normalized = String(host || '').trim();
|
|
199
|
+
const lower = normalized.toLowerCase();
|
|
200
|
+
if (!normalized || lower === '0.0.0.0') return '127.0.0.1';
|
|
201
|
+
if (lower === '::' || lower === '[::]') return '::1';
|
|
202
|
+
if (normalized.startsWith('[') && normalized.endsWith(']')) {
|
|
203
|
+
return normalized.slice(1, -1);
|
|
204
|
+
}
|
|
205
|
+
return normalized;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function waitForPort(host, port, timeoutMs = 60_000) {
|
|
209
|
+
const deadline = Date.now() + timeoutMs;
|
|
210
|
+
const probeHost = resolveProbeHost(host);
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const attempt = () => {
|
|
213
|
+
if (stopping) return reject(new Error('MIAODA_SERVER_RUNTIME_STOPPING'));
|
|
214
|
+
const socket = net.createConnection({ host: probeHost, port });
|
|
215
|
+
const failed = () => {
|
|
216
|
+
socket.destroy();
|
|
217
|
+
if (Date.now() >= deadline) {
|
|
218
|
+
reject(new Error(`MIAODA_SERVER_UPSTREAM_TIMEOUT: ${port}`));
|
|
219
|
+
} else {
|
|
220
|
+
setTimeout(attempt, 100);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
socket.once('connect', () => {
|
|
224
|
+
socket.destroy();
|
|
225
|
+
resolve();
|
|
226
|
+
});
|
|
227
|
+
socket.once('error', failed);
|
|
228
|
+
};
|
|
229
|
+
attempt();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function reservePort(host) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const server = net.createServer();
|
|
236
|
+
server.once('error', reject);
|
|
237
|
+
server.listen(0, host, () => {
|
|
238
|
+
const address = server.address();
|
|
239
|
+
if (!address || typeof address === 'string') {
|
|
240
|
+
server.close(() =>
|
|
241
|
+
reject(new Error('MIAODA_SERVER_UPSTREAM_PORT_RESERVATION_FAILED'))
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
server.close(error => (error ? reject(error) : resolve(address.port)));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function delay(ms) {
|
|
251
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function childIsRunning(child) {
|
|
255
|
+
return Boolean(
|
|
256
|
+
child?.pid && child.exitCode === null && child.signalCode === null
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function terminate(child) {
|
|
261
|
+
if (!child?.pid) return;
|
|
262
|
+
try {
|
|
263
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
264
|
+
} catch {
|
|
265
|
+
try {
|
|
266
|
+
child.kill('SIGTERM');
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function waitForChildExit(child, timeoutMs) {
|
|
272
|
+
if (!childIsRunning(child)) return Promise.resolve();
|
|
273
|
+
return Promise.race([
|
|
274
|
+
new Promise(resolve => child.once('exit', resolve)),
|
|
275
|
+
delay(timeoutMs),
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function pruneSupersededGenerations() {
|
|
280
|
+
if (process.env.MIAODA_CACHE_ROOT) return;
|
|
281
|
+
const pruneScript = fileURLToPath(
|
|
282
|
+
new URL('./cache-generation-prune.mjs', import.meta.url)
|
|
283
|
+
);
|
|
284
|
+
if (!fs.existsSync(pruneScript)) {
|
|
285
|
+
process.stderr.write(
|
|
286
|
+
`${JSON.stringify({ event: 'miaoda_cache_generation_prune_failed', error: 'MIAODA_CACHE_GENERATION_PRUNE_SCRIPT_MISSING' })}\n`
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const pruneChild = spawn(process.execPath, [pruneScript, '--wait-ms=60000'], {
|
|
291
|
+
cwd: projectRoot,
|
|
292
|
+
stdio: 'inherit',
|
|
293
|
+
env: { ...process.env, MIAODA_WORKSPACE_ROOT: projectRoot },
|
|
294
|
+
});
|
|
295
|
+
pruneChild.once('error', error => {
|
|
296
|
+
process.stderr.write(
|
|
297
|
+
`${JSON.stringify({ event: 'miaoda_cache_generation_prune_failed', error: error.message })}\n`
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
pruneChild.once('exit', (code, signal) => {
|
|
301
|
+
if (code === 0) return;
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
`${JSON.stringify({
|
|
304
|
+
event: 'miaoda_cache_generation_prune_failed',
|
|
305
|
+
error: signal ? `signal-${signal}` : `exit-${code}`,
|
|
306
|
+
})}\n`
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const internalHost = resolveProbeHost(publicHost);
|
|
312
|
+
const cacheUpstreamPort = await reservePort(internalHost);
|
|
313
|
+
let sourceUpstreamPort = await reservePort(internalHost);
|
|
314
|
+
while (sourceUpstreamPort === cacheUpstreamPort) {
|
|
315
|
+
sourceUpstreamPort = await reservePort(internalHost);
|
|
316
|
+
}
|
|
317
|
+
const cacheUpstream = {
|
|
318
|
+
name: 'cache',
|
|
319
|
+
host: internalHost,
|
|
320
|
+
port: cacheUpstreamPort,
|
|
321
|
+
inflight: 0,
|
|
322
|
+
};
|
|
323
|
+
const sourceUpstream = {
|
|
324
|
+
name: 'source',
|
|
325
|
+
host: internalHost,
|
|
326
|
+
port: sourceUpstreamPort,
|
|
327
|
+
inflight: 0,
|
|
328
|
+
};
|
|
329
|
+
let activeUpstream = cacheUpstream;
|
|
330
|
+
let cacheRetired = false;
|
|
331
|
+
let sourceChild;
|
|
332
|
+
let actionPluginInitChild;
|
|
333
|
+
let actionPluginsInitialized = false;
|
|
334
|
+
|
|
335
|
+
function retain(upstream) {
|
|
336
|
+
upstream.inflight += 1;
|
|
337
|
+
let released = false;
|
|
338
|
+
return () => {
|
|
339
|
+
if (released) return;
|
|
340
|
+
released = true;
|
|
341
|
+
upstream.inflight = Math.max(0, upstream.inflight - 1);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const proxy = http.createServer((request, response) => {
|
|
346
|
+
const upstream = activeUpstream;
|
|
347
|
+
const release = retain(upstream);
|
|
348
|
+
const upstreamRequest = http.request(
|
|
349
|
+
{
|
|
350
|
+
hostname: upstream.host,
|
|
351
|
+
port: upstream.port,
|
|
352
|
+
method: request.method,
|
|
353
|
+
path: request.url,
|
|
354
|
+
headers: request.headers,
|
|
355
|
+
},
|
|
356
|
+
upstreamResponse => {
|
|
357
|
+
if (upstreamResponse.statusMessage) {
|
|
358
|
+
response.writeHead(
|
|
359
|
+
upstreamResponse.statusCode || 502,
|
|
360
|
+
upstreamResponse.statusMessage,
|
|
361
|
+
upstreamResponse.headers
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
response.writeHead(
|
|
365
|
+
upstreamResponse.statusCode || 502,
|
|
366
|
+
upstreamResponse.headers
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
upstreamResponse.pipe(response);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
upstreamRequest.once('error', error => {
|
|
373
|
+
if (!response.headersSent) {
|
|
374
|
+
response.writeHead(502, { 'content-type': 'application/json' });
|
|
375
|
+
response.end(
|
|
376
|
+
`${JSON.stringify({
|
|
377
|
+
error: 'MIAODA_NEST_UPSTREAM_UNAVAILABLE',
|
|
378
|
+
upstream: upstream.name,
|
|
379
|
+
})}\n`
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
response.destroy(error);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
request.once('aborted', () => upstreamRequest.destroy());
|
|
386
|
+
response.once('finish', release);
|
|
387
|
+
response.once('close', release);
|
|
388
|
+
request.pipe(upstreamRequest);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
proxy.on('upgrade', (request, clientSocket, head) => {
|
|
392
|
+
const upstream = activeUpstream;
|
|
393
|
+
const release = retain(upstream);
|
|
394
|
+
const upstreamSocket = net.createConnection({
|
|
395
|
+
host: upstream.host,
|
|
396
|
+
port: upstream.port,
|
|
397
|
+
});
|
|
398
|
+
const close = () => {
|
|
399
|
+
release();
|
|
400
|
+
clientSocket.destroy();
|
|
401
|
+
upstreamSocket.destroy();
|
|
402
|
+
};
|
|
403
|
+
upstreamSocket.once('connect', () => {
|
|
404
|
+
const requestLine = `${request.method} ${request.url} HTTP/${request.httpVersion}\r\n`;
|
|
405
|
+
const headers = [];
|
|
406
|
+
for (let index = 0; index < request.rawHeaders.length; index += 2) {
|
|
407
|
+
headers.push(
|
|
408
|
+
`${request.rawHeaders[index]}: ${request.rawHeaders[index + 1]}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
upstreamSocket.write(`${requestLine}${headers.join('\r\n')}\r\n\r\n`);
|
|
412
|
+
if (head.length > 0) upstreamSocket.write(head);
|
|
413
|
+
clientSocket.pipe(upstreamSocket).pipe(clientSocket);
|
|
414
|
+
});
|
|
415
|
+
upstreamSocket.once('error', close);
|
|
416
|
+
clientSocket.once('error', close);
|
|
417
|
+
upstreamSocket.once('close', release);
|
|
418
|
+
clientSocket.once('close', release);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
proxy.on('clientError', (_error, socket) => {
|
|
422
|
+
if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
function listen(server, host, port) {
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
server.once('error', reject);
|
|
428
|
+
server.listen(port, host, resolve);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const bundleSpawnStartedAt = process.hrtime.bigint();
|
|
433
|
+
const cacheChild = spawn(process.execPath, [validated.artifacts.serverBundle], {
|
|
434
|
+
cwd: projectRoot,
|
|
435
|
+
detached: true,
|
|
436
|
+
stdio: 'inherit',
|
|
437
|
+
env: {
|
|
438
|
+
...process.env,
|
|
439
|
+
NODE_PATH: '',
|
|
440
|
+
SERVER_HOST: cacheUpstream.host,
|
|
441
|
+
SERVER_PORT: String(cacheUpstream.port),
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
emitRuntimePhase('bundle_spawn', bundleSpawnStartedAt, {
|
|
445
|
+
childPid: cacheChild.pid,
|
|
446
|
+
upstreamPort: cacheUpstream.port,
|
|
447
|
+
});
|
|
448
|
+
cacheChild.once('exit', (code, signal) => {
|
|
449
|
+
if (stopping || cacheRetired) return;
|
|
450
|
+
process.stderr.write(
|
|
451
|
+
`${JSON.stringify({ event: 'miaoda_nest_cache_runtime_exited', code, signal })}\n`
|
|
452
|
+
);
|
|
453
|
+
if (activeUpstream === cacheUpstream) process.exit(code || 1);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
function dependenciesAreReady() {
|
|
457
|
+
try {
|
|
458
|
+
const marker = JSON.parse(fs.readFileSync(dependencyReadyFile, 'utf8'));
|
|
459
|
+
return (
|
|
460
|
+
marker?.schemaVersion === 1 &&
|
|
461
|
+
marker.generationId === validated.generation.generationId &&
|
|
462
|
+
fs.statSync(path.join(projectRoot, 'node_modules')).isDirectory()
|
|
463
|
+
);
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function waitForDependencies() {
|
|
470
|
+
while (!stopping) {
|
|
471
|
+
if (dependenciesAreReady()) return;
|
|
472
|
+
await delay(100);
|
|
473
|
+
}
|
|
474
|
+
throw new Error('MIAODA_SERVER_RUNTIME_STOPPING');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function initializeActionPlugins() {
|
|
478
|
+
if (actionPluginsInitialized) return;
|
|
479
|
+
const cli = [
|
|
480
|
+
process.env.MIAODA_FULLSTACK_CLI_BIN,
|
|
481
|
+
path.join(
|
|
482
|
+
projectRoot,
|
|
483
|
+
'node_modules',
|
|
484
|
+
'@lark-apaas',
|
|
485
|
+
'fullstack-cli',
|
|
486
|
+
'bin',
|
|
487
|
+
'cli.js'
|
|
488
|
+
),
|
|
489
|
+
'/usr/lib/node_modules/@lark-apaas/fullstack-cli/bin/cli.js',
|
|
490
|
+
].find(candidate => candidate && fs.existsSync(candidate));
|
|
491
|
+
if (!cli) {
|
|
492
|
+
process.stdout.write(
|
|
493
|
+
`${JSON.stringify({ event: 'miaoda_action_plugin_init_skipped', reason: 'project-cli-missing' })}\n`
|
|
494
|
+
);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const startedAt = process.hrtime.bigint();
|
|
498
|
+
actionPluginInitChild = spawn(
|
|
499
|
+
process.execPath,
|
|
500
|
+
[cli, 'action-plugin', 'init'],
|
|
501
|
+
{
|
|
502
|
+
cwd: projectRoot,
|
|
503
|
+
detached: true,
|
|
504
|
+
stdio: 'inherit',
|
|
505
|
+
env: { ...process.env },
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
const code = await new Promise(resolve => {
|
|
509
|
+
actionPluginInitChild.once('exit', exitCode => resolve(exitCode ?? 1));
|
|
510
|
+
actionPluginInitChild.once('error', () => resolve(1));
|
|
511
|
+
});
|
|
512
|
+
actionPluginInitChild = undefined;
|
|
513
|
+
process.stdout.write(
|
|
514
|
+
`${JSON.stringify({
|
|
515
|
+
event: 'miaoda_action_plugin_init_finished',
|
|
516
|
+
code,
|
|
517
|
+
durationMs: Number(durationMs(startedAt).toFixed(3)),
|
|
518
|
+
})}\n`
|
|
519
|
+
);
|
|
520
|
+
if (code !== 0) {
|
|
521
|
+
process.stderr.write(
|
|
522
|
+
`${JSON.stringify({
|
|
523
|
+
event: 'miaoda_action_plugin_init_failed',
|
|
524
|
+
code,
|
|
525
|
+
continueStartup: true,
|
|
526
|
+
})}\n`
|
|
527
|
+
);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
actionPluginsInitialized = true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function drainCacheRuntime() {
|
|
534
|
+
const deadline = Date.now() + cacheDrainTimeoutMs;
|
|
535
|
+
while (!stopping && cacheUpstream.inflight > 0 && Date.now() < deadline) {
|
|
536
|
+
await delay(25);
|
|
537
|
+
}
|
|
538
|
+
cacheRetired = true;
|
|
539
|
+
terminate(cacheChild);
|
|
540
|
+
await waitForChildExit(cacheChild, 1_000);
|
|
541
|
+
process.stdout.write(
|
|
542
|
+
`${JSON.stringify({
|
|
543
|
+
event: 'miaoda_nest_cache_runtime_retired',
|
|
544
|
+
remainingInflight: cacheUpstream.inflight,
|
|
545
|
+
})}\n`
|
|
546
|
+
);
|
|
547
|
+
pruneSupersededGenerations();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function promoteSourceRuntime() {
|
|
551
|
+
await waitForDependencies();
|
|
552
|
+
if (stopping) return;
|
|
553
|
+
const startedAt = process.hrtime.bigint();
|
|
554
|
+
process.stdout.write(
|
|
555
|
+
`${JSON.stringify({
|
|
556
|
+
event: 'miaoda_nest_source_runtime_starting',
|
|
557
|
+
dependencyReadyFile,
|
|
558
|
+
})}\n`
|
|
559
|
+
);
|
|
560
|
+
await initializeActionPlugins();
|
|
561
|
+
if (stopping) return;
|
|
562
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
563
|
+
sourceChild = spawn(npmCommand, ['run', 'dev:server'], {
|
|
564
|
+
cwd: projectRoot,
|
|
565
|
+
detached: true,
|
|
566
|
+
stdio: 'inherit',
|
|
567
|
+
env: {
|
|
568
|
+
...process.env,
|
|
569
|
+
MIAODA_CACHE_BOOTSTRAP_ENABLED: 'false',
|
|
570
|
+
SERVER_HOST: sourceUpstream.host,
|
|
571
|
+
SERVER_PORT: String(sourceUpstream.port),
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
let sourceExit;
|
|
575
|
+
let sourcePromoted = false;
|
|
576
|
+
const sourceExitPromise = new Promise((_, reject) => {
|
|
577
|
+
sourceChild.once('exit', (code, signal) => {
|
|
578
|
+
sourceExit = { code, signal };
|
|
579
|
+
if (stopping) return;
|
|
580
|
+
process.stderr.write(
|
|
581
|
+
`${JSON.stringify({ event: 'miaoda_nest_source_runtime_exited', code, signal })}\n`
|
|
582
|
+
);
|
|
583
|
+
if (sourcePromoted) {
|
|
584
|
+
// The outer dev supervisor restarts this bridge. It first restores the
|
|
585
|
+
// already-sealed cache worker, then promotes the dependency-backed worker.
|
|
586
|
+
process.exit(code || 1);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
reject(
|
|
590
|
+
new Error(
|
|
591
|
+
`MIAODA_NEST_SOURCE_RUNTIME_EXITED_BEFORE_READY: code=${code} signal=${signal}`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
sourceChild.once('error', reject);
|
|
596
|
+
});
|
|
597
|
+
await Promise.race([
|
|
598
|
+
waitForPort(sourceUpstream.host, sourceUpstream.port, sourceReadyTimeoutMs),
|
|
599
|
+
sourceExitPromise,
|
|
600
|
+
]);
|
|
601
|
+
sourcePromoted = true;
|
|
602
|
+
if (sourceExit || !childIsRunning(sourceChild)) {
|
|
603
|
+
throw new Error('MIAODA_NEST_SOURCE_RUNTIME_EXITED_DURING_PROMOTION');
|
|
604
|
+
}
|
|
605
|
+
if (stopping) return;
|
|
606
|
+
activeUpstream = sourceUpstream;
|
|
607
|
+
process.stdout.write(
|
|
608
|
+
`${JSON.stringify({
|
|
609
|
+
event: 'miaoda_nest_source_runtime_ready',
|
|
610
|
+
pid: sourceChild.pid,
|
|
611
|
+
supervisorPid: process.pid,
|
|
612
|
+
serviceId,
|
|
613
|
+
port: publicPort,
|
|
614
|
+
upstreamPort: sourceUpstream.port,
|
|
615
|
+
cutoverDurationMs: Number(durationMs(startedAt).toFixed(3)),
|
|
616
|
+
})}\n`
|
|
617
|
+
);
|
|
618
|
+
void drainCacheRuntime();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function close(signal) {
|
|
622
|
+
if (stopping) return;
|
|
623
|
+
stopping = true;
|
|
624
|
+
process.stdout.write(
|
|
625
|
+
`${JSON.stringify({ event: 'miaoda_nest_runtime_stopping', signal })}\n`
|
|
626
|
+
);
|
|
627
|
+
proxy.close();
|
|
628
|
+
terminate(actionPluginInitChild);
|
|
629
|
+
terminate(sourceChild);
|
|
630
|
+
terminate(cacheChild);
|
|
631
|
+
setTimeout(() => process.exit(0), 250).unref();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
635
|
+
process.once(signal, () => void close(signal));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const listenWaitStartedAt = process.hrtime.bigint();
|
|
639
|
+
await waitForPort(cacheUpstream.host, cacheUpstream.port);
|
|
640
|
+
await listen(proxy, publicHost, publicPort);
|
|
641
|
+
emitRuntimePhase('listen_wait', listenWaitStartedAt, {
|
|
642
|
+
port: publicPort,
|
|
643
|
+
cacheUpstreamPort: cacheUpstream.port,
|
|
644
|
+
});
|
|
645
|
+
publishServingReadyMarker(servingReadyMarker);
|
|
646
|
+
process.stdout.write(
|
|
647
|
+
`${JSON.stringify({
|
|
648
|
+
event: 'miaoda_nest_cache_runtime_ready',
|
|
649
|
+
pid: cacheChild.pid,
|
|
650
|
+
supervisorPid: process.pid,
|
|
651
|
+
serviceId,
|
|
652
|
+
generationId: validated.generation.generationId,
|
|
653
|
+
port: publicPort,
|
|
654
|
+
startupDurationMs: Number(durationMs(runtimeStartedAt).toFixed(3)),
|
|
655
|
+
})}\n`
|
|
656
|
+
);
|
|
657
|
+
async function promoteSourceRuntimeWithRetry() {
|
|
658
|
+
try {
|
|
659
|
+
await promoteSourceRuntime();
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (stopping) return;
|
|
662
|
+
terminate(sourceChild);
|
|
663
|
+
await waitForChildExit(sourceChild, 1_000);
|
|
664
|
+
cacheRetired = true;
|
|
665
|
+
proxy.close();
|
|
666
|
+
terminate(cacheChild);
|
|
667
|
+
process.stderr.write(
|
|
668
|
+
`${JSON.stringify({
|
|
669
|
+
event: 'miaoda_nest_source_runtime_promotion_failed',
|
|
670
|
+
cacheServicePreserved: false,
|
|
671
|
+
fallback: 'original-preview-retry',
|
|
672
|
+
error: error instanceof Error ? error.message : String(error),
|
|
673
|
+
})}\n`
|
|
674
|
+
);
|
|
675
|
+
process.exitCode = 1;
|
|
676
|
+
setTimeout(() => process.exit(1), 250).unref();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
void promoteSourceRuntimeWithRetry();
|
|
681
|
+
await new Promise(() => {});
|