@lark-apaas/fullstack-cli 1.1.59-alpha.2 → 1.1.59-alpha.20260717075919

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.59-alpha.2",
3
+ "version": "1.1.59-alpha.20260717075919",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,5 +21,9 @@
21
21
  }
22
22
  }
23
23
  ]
24
+ },
25
+ "x-miaoda-preview": {
26
+ "compiler": "swc",
27
+ "typeCheck": "deferred"
24
28
  }
25
- }
29
+ }
@@ -3,10 +3,15 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { spawn, execSync } = require('child_process');
6
+ const { spawn, execFileSync, execSync } = require('child_process');
7
7
  const readline = require('readline');
8
8
  const {
9
+ buildListeningPortLookupArgs,
10
+ createDeferredTypecheckGate,
9
11
  createPreviewPhaseReporter,
12
+ normalizeTcpPort,
13
+ parseTscWatchSummary,
14
+ resolvePreviewServerCommand,
10
15
  waitForTcpReady,
11
16
  } = require('./preview-startup-timing.cjs');
12
17
 
@@ -40,8 +45,12 @@ const MAX_RESTART_COUNT = process.env.MAX_RESTART_COUNT != null && process.env.M
40
45
  : Infinity;
41
46
  const RESTART_DELAY = parseInt(process.env.RESTART_DELAY, 10) || 2;
42
47
  const MAX_DELAY = 8;
43
- const SERVER_PORT = process.env.SERVER_PORT || '3000';
44
- const CLIENT_DEV_PORT = process.env.CLIENT_DEV_PORT || '8080';
48
+ const SERVER_PORT = normalizeTcpPort(process.env.SERVER_PORT, 3000, 'SERVER_PORT');
49
+ const CLIENT_DEV_PORT = normalizeTcpPort(
50
+ process.env.CLIENT_DEV_PORT,
51
+ 8080,
52
+ 'CLIENT_DEV_PORT'
53
+ );
45
54
 
46
55
  fs.mkdirSync(LOG_DIR, { recursive: true });
47
56
 
@@ -102,16 +111,38 @@ function killProcessGroup(pid, signal) {
102
111
 
103
112
  function killOrphansByPort(port) {
104
113
  try {
105
- const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 }).trim();
106
- if (pids) {
107
- const pidList = pids.split('\n').filter(Boolean);
108
- for (const p of pidList) {
109
- try { process.kill(parseInt(p, 10), 'SIGKILL'); } catch {}
114
+ const output = execFileSync('lsof', buildListeningPortLookupArgs(port), {
115
+ encoding: 'utf8',
116
+ timeout: 5000,
117
+ }).trim();
118
+ const killedPids = [];
119
+ const failures = [];
120
+ if (output) {
121
+ for (const pidText of output.split('\n').filter(Boolean)) {
122
+ const pid = Number(pidText);
123
+ try {
124
+ process.kill(pid, 'SIGKILL');
125
+ killedPids.push(pidText);
126
+ } catch (error) {
127
+ if (error?.code !== 'ESRCH') {
128
+ failures.push(
129
+ `${pidText}: ${error instanceof Error ? error.message : String(error)}`
130
+ );
131
+ }
132
+ }
110
133
  }
111
- return pidList;
112
134
  }
113
- } catch {}
114
- return [];
135
+ return {
136
+ pids: killedPids,
137
+ error: failures.length > 0 ? failures.join('; ') : null,
138
+ };
139
+ } catch (error) {
140
+ if (error?.status === 1) return { pids: [], error: null };
141
+ return {
142
+ pids: [],
143
+ error: error instanceof Error ? error.message : String(error),
144
+ };
145
+ }
115
146
  }
116
147
 
117
148
  // ── Process supervision ───────────────────────────────────────────────────────
@@ -126,7 +157,7 @@ function sleep(ms) {
126
157
  * Start and supervise a process with auto-restart and log piping.
127
158
  * Returns a promise that resolves when the process loop ends.
128
159
  */
129
- function startProcess({ name, command, args, cleanupPort }) {
160
+ function startProcess({ name, command, args, cleanupPort, phaseDetail = {}, onReady, onOutputLine }) {
130
161
  const logFilePath = path.join(LOG_DIR, `${name}.std.log`);
131
162
  const logFd = fs.openSync(logFilePath, 'a');
132
163
 
@@ -136,6 +167,30 @@ function startProcess({ name, command, args, cleanupPort }) {
136
167
  const run = async () => {
137
168
  let restartCount = 0;
138
169
 
170
+ if (cleanupPort) {
171
+ const portCleanup = killOrphansByPort(cleanupPort);
172
+ const stalePids = portCleanup.pids;
173
+ logEvent(
174
+ portCleanup.error ? 'ERROR' : stalePids.length > 0 ? 'WARN' : 'INFO',
175
+ name,
176
+ portCleanup.error
177
+ ? `Port ${cleanupPort} preflight failed: ${portCleanup.error}`
178
+ : stalePids.length > 0
179
+ ? `Killed stale processes on port ${cleanupPort} before first spawn: ${stalePids.join(' ')}`
180
+ : `Port ${cleanupPort} is clear before first spawn`
181
+ );
182
+ previewPhaseReporter.emit(
183
+ name === 'server' ? 'backend_port_preflight' : 'client_port_preflight',
184
+ portCleanup.error ? 'error' : 'success',
185
+ {
186
+ exact: true,
187
+ cleanup_count: stalePids.length,
188
+ ...(portCleanup.error ? { error: portCleanup.error } : {}),
189
+ }
190
+ );
191
+ if (stalePids.length > 0) await sleep(500);
192
+ }
193
+
139
194
  while (!stopping) {
140
195
  const child = spawn(command, args, {
141
196
  detached: true,
@@ -151,11 +206,17 @@ function startProcess({ name, command, args, cleanupPort }) {
151
206
  const startTime = Date.now();
152
207
  const attempt = restartCount + 1;
153
208
  logEvent('INFO', name, `Process started (PGID: ${child.pid}): ${command} ${args.join(' ')}`);
154
- const processPhase = name === 'server' ? 'backend_process_spawn' : 'client_process_spawn';
209
+ const processPhase =
210
+ name === 'server'
211
+ ? 'backend_process_spawn'
212
+ : name === 'server-typecheck'
213
+ ? 'backend_typecheck_process_spawn'
214
+ : 'client_process_spawn';
155
215
  previewPhaseReporter.emit(processPhase, 'success', {
156
216
  at_ms: startTime,
157
217
  attempt,
158
218
  exact: true,
219
+ ...phaseDetail,
159
220
  });
160
221
  if (name === 'server') {
161
222
  void waitForTcpReady({
@@ -164,29 +225,60 @@ function startProcess({ name, command, args, cleanupPort }) {
164
225
  interval_ms: 50,
165
226
  timeout_ms: 120000,
166
227
  should_continue: () => entry.child === child && !stopping,
167
- }).then((result) => {
168
- if (result.cancelled) return;
169
- previewPhaseReporter.emit(
170
- 'backend_tcp_ready',
171
- result.ready ? 'success' : 'error',
172
- { ...result, attempt, exact: false }
173
- );
174
- }).catch((error) => {
175
- if (entry.child !== child || stopping) return;
176
- previewPhaseReporter.emit('backend_tcp_ready', 'error', {
177
- duration_ms: Date.now() - startTime,
178
- attempt,
179
- exact: false,
180
- precision_ms: 50,
181
- error: error instanceof Error ? error.message : String(error),
228
+ })
229
+ .then(result => {
230
+ if (result.cancelled) return;
231
+ previewPhaseReporter.emit(
232
+ 'backend_tcp_ready',
233
+ result.ready ? 'success' : 'error',
234
+ { ...result, attempt, exact: false }
235
+ );
236
+ if (result.ready && onReady) {
237
+ try {
238
+ onReady(result);
239
+ } catch (error) {
240
+ logEvent(
241
+ 'ERROR',
242
+ name,
243
+ `Backend ready callback failed: ${error instanceof Error ? error.message : String(error)}`
244
+ );
245
+ }
246
+ }
247
+ })
248
+ .catch(error => {
249
+ if (entry.child !== child || stopping) return;
250
+ previewPhaseReporter.emit('backend_tcp_ready', 'error', {
251
+ duration_ms: Date.now() - startTime,
252
+ attempt,
253
+ exact: false,
254
+ precision_ms: 50,
255
+ error: error instanceof Error ? error.message : String(error),
256
+ });
182
257
  });
183
- });
184
258
  }
185
259
 
186
260
  // Pipe stdout and stderr through readline for timestamped logging
261
+ let typecheckSummaryReported = false;
187
262
  const pipeLines = (stream) => {
188
263
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
189
264
  rl.on('line', (line) => {
265
+ if (onOutputLine) onOutputLine(line);
266
+ if (name === 'server-typecheck' && !typecheckSummaryReported) {
267
+ const summary = parseTscWatchSummary(line);
268
+ if (summary) {
269
+ typecheckSummaryReported = true;
270
+ previewPhaseReporter.emit(
271
+ 'backend_typecheck_ready',
272
+ summary.passed ? 'success' : 'error',
273
+ {
274
+ duration_ms: Date.now() - startTime,
275
+ attempt,
276
+ exact: true,
277
+ ...summary,
278
+ }
279
+ );
280
+ }
281
+ }
190
282
  const msg = `[${timestamp()}] [${name}] ${line}\n`;
191
283
  try { fs.writeSync(logFd, msg); } catch {}
192
284
  writeOutput(msg);
@@ -215,7 +307,15 @@ function startProcess({ name, command, args, cleanupPort }) {
215
307
 
216
308
  // Port cleanup fallback
217
309
  if (cleanupPort) {
218
- const orphans = killOrphansByPort(cleanupPort);
310
+ const portCleanup = killOrphansByPort(cleanupPort);
311
+ const orphans = portCleanup.pids;
312
+ if (portCleanup.error) {
313
+ logEvent(
314
+ 'ERROR',
315
+ name,
316
+ `Port ${cleanupPort} cleanup failed: ${portCleanup.error}`
317
+ );
318
+ }
219
319
  if (orphans.length > 0) {
220
320
  logEvent('WARN', name, `Killed orphan processes on port ${cleanupPort}: ${orphans.join(' ')}`);
221
321
  await sleep(500);
@@ -277,8 +377,12 @@ async function cleanup() {
277
377
  }
278
378
 
279
379
  // Port cleanup fallback
280
- killOrphansByPort(SERVER_PORT);
281
- killOrphansByPort(CLIENT_DEV_PORT);
380
+ for (const port of [SERVER_PORT, CLIENT_DEV_PORT]) {
381
+ const portCleanup = killOrphansByPort(port);
382
+ if (portCleanup.error) {
383
+ logEvent('ERROR', 'main', `Port ${port} cleanup failed: ${portCleanup.error}`);
384
+ }
385
+ }
282
386
 
283
387
  logEvent('INFO', 'main', 'All processes stopped');
284
388
 
@@ -332,11 +436,101 @@ async function main() {
332
436
  }
333
437
 
334
438
  // Start server and client
439
+ const serverCommand = resolvePreviewServerCommand({
440
+ project_root: PROJECT_ROOT,
441
+ });
442
+ logEvent(
443
+ 'INFO',
444
+ 'server',
445
+ `Selected ${serverCommand.compiler} compiler (${serverCommand.reason})`
446
+ );
447
+ previewPhaseReporter.emit('backend_compiler_selected', 'success', {
448
+ exact: true,
449
+ compiler: serverCommand.compiler,
450
+ type_check: serverCommand.type_check,
451
+ type_check_mode: serverCommand.type_check_mode,
452
+ reason: serverCommand.reason,
453
+ missing_modules: serverCommand.missing_modules,
454
+ });
455
+ const typecheckGate = createDeferredTypecheckGate();
456
+ let serverTypecheckStarted = false;
457
+ const startServerTypecheck = trigger => {
458
+ if (serverTypecheckStarted || !serverCommand.type_check_command) return;
459
+ try {
460
+ logEvent(
461
+ 'INFO',
462
+ 'server-typecheck',
463
+ `Starting deferred server type checker after ${trigger}`
464
+ );
465
+ previewPhaseReporter.emit('backend_typecheck_deferred_start', 'success', {
466
+ exact: true,
467
+ type_check_mode: serverCommand.type_check_mode,
468
+ trigger,
469
+ });
470
+ const typecheckPromise = startProcess({
471
+ name: 'server-typecheck',
472
+ command: serverCommand.type_check_command.command,
473
+ args: serverCommand.type_check_command.args,
474
+ phaseDetail: { type_check_mode: serverCommand.type_check_mode },
475
+ });
476
+ serverTypecheckStarted = true;
477
+ void typecheckPromise.catch(error => {
478
+ logEvent(
479
+ 'ERROR',
480
+ 'server-typecheck',
481
+ `Deferred server type checker failed: ${error instanceof Error ? error.message : String(error)}`
482
+ );
483
+ });
484
+ } catch (error) {
485
+ previewPhaseReporter.emit('backend_typecheck_deferred_start', 'error', {
486
+ exact: true,
487
+ type_check_mode: serverCommand.type_check_mode,
488
+ error: error instanceof Error ? error.message : String(error),
489
+ });
490
+ logEvent(
491
+ 'ERROR',
492
+ 'server-typecheck',
493
+ `Failed to start deferred server type checker: ${error instanceof Error ? error.message : String(error)}`
494
+ );
495
+ }
496
+ };
497
+ const handleBackendReady = () => {
498
+ if (!serverCommand.type_check_command) return;
499
+ const decision = typecheckGate.onBackendReady();
500
+ if (decision.should_start) {
501
+ startServerTypecheck(decision.reason);
502
+ return;
503
+ }
504
+ logEvent(
505
+ 'INFO',
506
+ 'server-typecheck',
507
+ 'Skipped type checking during initial preview startup'
508
+ );
509
+ previewPhaseReporter.emit('backend_typecheck_skipped', 'success', {
510
+ exact: true,
511
+ type_check_mode: serverCommand.type_check_mode,
512
+ reason: decision.reason,
513
+ next_trigger: 'source_change_or_backend_restart',
514
+ });
515
+ };
516
+ const handleServerOutput = line => {
517
+ if (!serverCommand.type_check_command || serverTypecheckStarted) return;
518
+ const decision = typecheckGate.onCompilerOutput(line);
519
+ if (decision?.should_start) startServerTypecheck(decision.reason);
520
+ };
335
521
  const serverPromise = startProcess({
336
522
  name: 'server',
337
- command: 'npm',
338
- args: ['run', 'dev:server'],
523
+ command: serverCommand.command,
524
+ args: serverCommand.args,
339
525
  cleanupPort: SERVER_PORT,
526
+ phaseDetail: {
527
+ compiler: serverCommand.compiler,
528
+ type_check: serverCommand.type_check,
529
+ type_check_mode: serverCommand.type_check_mode,
530
+ compiler_selection_reason: serverCommand.reason,
531
+ },
532
+ onReady: handleBackendReady,
533
+ onOutputLine: handleServerOutput,
340
534
  });
341
535
 
342
536
  const clientPromise = startProcess({
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
3
4
  const net = require('net');
5
+ const path = require('path');
4
6
 
5
7
  const PREFIX = '[MiaodaPreviewPhase] ';
6
8
  const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,128}$/;
@@ -37,6 +39,277 @@ function createPreviewPhaseReporter(options = {}) {
37
39
  return { emit };
38
40
  }
39
41
 
42
+ function createTscServerCommand(reason, missingModules = []) {
43
+ return {
44
+ command: 'npm',
45
+ args: ['run', 'dev:server', '--', '--builder', 'tsc'],
46
+ compiler: 'tsc',
47
+ type_check: true,
48
+ type_check_mode: 'compiler_integrated',
49
+ type_check_command: null,
50
+ reason,
51
+ missing_modules: missingModules,
52
+ };
53
+ }
54
+
55
+ function normalizeTcpPort(value, fallback, name = 'TCP port') {
56
+ const candidate = value == null || value === '' ? fallback : value;
57
+ const serialized = String(candidate);
58
+ if (!/^\d+$/.test(serialized)) {
59
+ throw new Error(`${name} must be a valid TCP port`);
60
+ }
61
+ const port = Number(serialized);
62
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
63
+ throw new Error(`${name} must be a valid TCP port`);
64
+ }
65
+ return port;
66
+ }
67
+
68
+ function buildListeningPortLookupArgs(port) {
69
+ const validatedPort = normalizeTcpPort(port, 0);
70
+ return [`-tiTCP:${validatedPort}`, '-sTCP:LISTEN'];
71
+ }
72
+
73
+ function hasRuntimeCompilerPluginConsumer(projectRoot, sourceRoot) {
74
+ const root = path.resolve(projectRoot, sourceRoot || 'src');
75
+ const stack = [root];
76
+ let scannedFiles = 0;
77
+ let scannedBytes = 0;
78
+ const maxFiles = 2000;
79
+ const maxBytes = 16 * 1024 * 1024;
80
+ const consumerPatterns = [
81
+ /\bSwaggerModule\s*\.\s*(?:createDocument|loadPluginMetadata)\b/,
82
+ /\bcreateDocument\s*\(/,
83
+ /\bloadPluginMetadata\s*\(/,
84
+ /\bDevTools(?:V2)?Module\s*\.\s*mount\b/,
85
+ ];
86
+
87
+ if (!fs.existsSync(root)) return true;
88
+
89
+ try {
90
+ while (stack.length > 0) {
91
+ const current = stack.pop();
92
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
93
+ if (entry.isDirectory()) {
94
+ if (!['node_modules', 'dist', '.git'].includes(entry.name)) {
95
+ stack.push(path.join(current, entry.name));
96
+ }
97
+ continue;
98
+ }
99
+ if (!entry.isFile() || !/\.(?:[cm]?js|tsx?)$/.test(entry.name)) {
100
+ continue;
101
+ }
102
+ scannedFiles += 1;
103
+ const filePath = path.join(current, entry.name);
104
+ const stat = fs.statSync(filePath);
105
+ scannedBytes += stat.size;
106
+ if (scannedFiles > maxFiles || scannedBytes > maxBytes) return true;
107
+ const source = fs.readFileSync(filePath, 'utf8');
108
+ if (consumerPatterns.some(pattern => pattern.test(source))) return true;
109
+ }
110
+ }
111
+ } catch {
112
+ // If we cannot prove the compiler plugin is unobservable at runtime, keep TSC.
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+
118
+ function resolvePreviewServerCommand(options = {}) {
119
+ const projectRoot = options.project_root || path.resolve(__dirname, '..');
120
+ let packageJson = options.package_json;
121
+ if (!packageJson) {
122
+ try {
123
+ packageJson = JSON.parse(
124
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
125
+ );
126
+ } catch {
127
+ packageJson = {};
128
+ }
129
+ }
130
+
131
+ let nestCliConfig = options.nest_cli_config;
132
+ if (!nestCliConfig) {
133
+ try {
134
+ nestCliConfig = JSON.parse(
135
+ fs.readFileSync(path.join(projectRoot, 'nest-cli.json'), 'utf8')
136
+ );
137
+ } catch {
138
+ nestCliConfig = {};
139
+ }
140
+ }
141
+
142
+ const serverScript = packageJson?.scripts?.['dev:server'] || '';
143
+ const normalizedServerScript = serverScript
144
+ .trim()
145
+ .replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '')
146
+ .replace(/\s+/g, ' ');
147
+ const isNestWatchScript = normalizedServerScript === 'nest start --watch';
148
+ if (!isNestWatchScript) {
149
+ return {
150
+ command: 'npm',
151
+ args: ['run', 'dev:server'],
152
+ compiler: 'configured',
153
+ type_check: false,
154
+ type_check_mode: 'configured',
155
+ type_check_command: null,
156
+ reason: 'custom_server_script',
157
+ missing_modules: [],
158
+ };
159
+ }
160
+
161
+ const configuredBuilder = nestCliConfig?.compilerOptions?.builder;
162
+ if (configuredBuilder && configuredBuilder !== 'tsc') {
163
+ return {
164
+ command: 'npm',
165
+ args: ['run', 'dev:server'],
166
+ compiler: 'configured',
167
+ type_check: false,
168
+ type_check_mode: 'configured',
169
+ type_check_command: null,
170
+ reason: 'custom_nest_builder',
171
+ missing_modules: [],
172
+ };
173
+ }
174
+
175
+ const serverTypecheckScript =
176
+ packageJson?.scripts?.['type:check:server'] || '';
177
+ const normalizedTypecheckScript = serverTypecheckScript
178
+ .trim()
179
+ .replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '')
180
+ .replace(/\s+/g, ' ');
181
+ const isNonEmittingTscScript =
182
+ /^(?:npx )?tsc --noEmit --project [A-Za-z0-9_./-]+$/.test(
183
+ normalizedTypecheckScript
184
+ );
185
+ if (!isNonEmittingTscScript) {
186
+ return createTscServerCommand('server_typecheck_script_unavailable');
187
+ }
188
+
189
+ const previewCapability = nestCliConfig?.['x-miaoda-preview'];
190
+ if (
191
+ previewCapability?.compiler !== 'swc' ||
192
+ previewCapability?.typeCheck !== 'deferred'
193
+ ) {
194
+ return createTscServerCommand('preview_swc_capability_not_declared');
195
+ }
196
+
197
+ const compilerPlugins = Array.isArray(nestCliConfig?.compilerOptions?.plugins)
198
+ ? nestCliConfig.compilerOptions.plugins
199
+ : [];
200
+ const compilerPluginNames = compilerPlugins.map(plugin =>
201
+ typeof plugin === 'string' ? plugin : plugin?.name || ''
202
+ );
203
+ if (
204
+ compilerPluginNames.some(pluginName => pluginName !== '@nestjs/swagger')
205
+ ) {
206
+ return createTscServerCommand('unsupported_nest_compiler_plugins');
207
+ }
208
+ if (compilerPluginNames.includes('@nestjs/swagger')) {
209
+ const runtimeConsumer =
210
+ typeof options.has_runtime_compiler_plugin_consumer === 'boolean'
211
+ ? options.has_runtime_compiler_plugin_consumer
212
+ : hasRuntimeCompilerPluginConsumer(
213
+ projectRoot,
214
+ nestCliConfig?.sourceRoot || 'src'
215
+ );
216
+ if (runtimeConsumer) {
217
+ return createTscServerCommand(
218
+ 'runtime_compiler_plugin_consumer_detected'
219
+ );
220
+ }
221
+ }
222
+
223
+ const resolveModule =
224
+ options.resolve_module ||
225
+ (moduleName => {
226
+ const resolved = require.resolve(moduleName, { paths: [projectRoot] });
227
+ if (moduleName === '@swc/core') {
228
+ const swc = require(resolved);
229
+ if (typeof swc.transformSync !== 'function') {
230
+ throw new Error('@swc/core native binding is unavailable');
231
+ }
232
+ }
233
+ return resolved;
234
+ });
235
+ const requiredModules = ['@swc/cli', '@swc/core'];
236
+ const missingModules = requiredModules.filter(moduleName => {
237
+ try {
238
+ resolveModule(moduleName);
239
+ return false;
240
+ } catch {
241
+ return true;
242
+ }
243
+ });
244
+
245
+ if (missingModules.length > 0) {
246
+ return createTscServerCommand('swc_dependencies_missing', missingModules);
247
+ }
248
+
249
+ return {
250
+ command: 'npm',
251
+ args: ['run', 'dev:server', '--', '--builder', 'swc'],
252
+ compiler: 'swc',
253
+ type_check: true,
254
+ type_check_mode: 'deferred_tsc_watch',
255
+ type_check_command: {
256
+ command: 'npm',
257
+ args: [
258
+ 'run',
259
+ 'type:check:server',
260
+ '--',
261
+ '--watch',
262
+ '--preserveWatchOutput',
263
+ '--locale',
264
+ 'en',
265
+ ],
266
+ },
267
+ reason: 'swc_dependencies_ready',
268
+ missing_modules: [],
269
+ };
270
+ }
271
+
272
+ function createDeferredTypecheckGate() {
273
+ let initialBackendReadySeen = false;
274
+
275
+ return {
276
+ onBackendReady() {
277
+ if (!initialBackendReadySeen) {
278
+ initialBackendReadySeen = true;
279
+ return {
280
+ should_start: false,
281
+ reason: 'initial_preview_startup',
282
+ };
283
+ }
284
+ return {
285
+ should_start: true,
286
+ reason: 'backend_restart',
287
+ };
288
+ },
289
+ onCompilerOutput(line) {
290
+ if (
291
+ !initialBackendReadySeen ||
292
+ !/Successfully compiled:\s+\d+\s+files?\s+with\s+swc\b/i.test(line)
293
+ ) {
294
+ return null;
295
+ }
296
+ return {
297
+ should_start: true,
298
+ reason: 'source_change',
299
+ };
300
+ },
301
+ };
302
+ }
303
+
304
+ function parseTscWatchSummary(line) {
305
+ const match = /Found\s+(\d+)\s+errors?\.\s+Watching for file changes\./i.exec(
306
+ line
307
+ );
308
+ if (!match) return null;
309
+ const errorCount = Number(match[1]);
310
+ return { error_count: errorCount, passed: errorCount === 0 };
311
+ }
312
+
40
313
  function connectTcpOnce({ host, port, timeout_ms }) {
41
314
  return new Promise(resolve => {
42
315
  const socket = net.createConnection({ host, port });
@@ -127,6 +400,11 @@ async function waitForTcpReady(options) {
127
400
  }
128
401
 
129
402
  module.exports = {
403
+ buildListeningPortLookupArgs,
404
+ createDeferredTypecheckGate,
130
405
  createPreviewPhaseReporter,
406
+ normalizeTcpPort,
407
+ parseTscWatchSummary,
408
+ resolvePreviewServerCommand,
131
409
  waitForTcpReady,
132
410
  };