@se-studio/site-check 2.1.0 → 2.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix `smoke-test-one` appearing to hang: add fetch timeouts, startup/progress logging, fail fast when port is busy but not responding, reuse an already-healthy dev server (`SMOKE_TEST_USE_RUNNING_SERVER`, default true), and inherit stdio so dev server output is visible.
8
+
3
9
  ## 2.1.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/site-check",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Validate SE marketing sites (sitemap, llms.txt) and download markdown files preserving structure",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5,13 +5,15 @@
5
5
  * Usage: smoke-test-one <port> (from app directory)
6
6
  *
7
7
  * Env:
8
- * SMOKE_TEST_IGNORE=true — skip entirely
9
- * SMOKE_TEST_SERVER_SCRIPT=dev:dev — pnpm script to start server (default dev:dev; use "start" for prod)
10
- * SMOKE_TEST_VERIFY_CACHE=true — run smoke twice and assert cache headers / timing
11
- * SMOKE_TEST_READY_PATH=/ — path polled until server responds
12
- * LOG_CMS_FETCH=1 set automatically when SMOKE_TEST_VERIFY_CACHE=true
8
+ * SMOKE_TEST_IGNORE=true — skip entirely
9
+ * SMOKE_TEST_SERVER_SCRIPT=dev:dev — pnpm script to start server (default dev:dev; use "start" for prod)
10
+ * SMOKE_TEST_VERIFY_CACHE=true — run smoke twice and assert cache headers / timing
11
+ * SMOKE_TEST_READY_PATH=/ — path polled until server responds
12
+ * SMOKE_TEST_USE_RUNNING_SERVER=true reuse server already on port (default true)
13
+ * LOG_CMS_FETCH=1 — set automatically when SMOKE_TEST_VERIFY_CACHE=true
13
14
  */
14
15
 
16
+ import net from 'node:net';
15
17
  import fs from 'node:fs';
16
18
  import path from 'node:path';
17
19
  import { spawn, spawnSync } from 'node:child_process';
@@ -25,6 +27,15 @@ if (!port || !/^\d+$/.test(port)) {
25
27
  const appDir = process.cwd();
26
28
  const baseUrl = `http://localhost:${port}`;
27
29
  const pollIntervalMs = 1_000;
30
+ const progressIntervalMs = 10_000;
31
+ const fetchTimeoutMs = 10_000;
32
+
33
+ function fetchWithTimeout(url, options = {}) {
34
+ return fetch(url, {
35
+ ...options,
36
+ signal: AbortSignal.timeout(fetchTimeoutMs),
37
+ });
38
+ }
28
39
 
29
40
  function parseEnvFile(filePath) {
30
41
  const env = {};
@@ -49,20 +60,70 @@ function loadEnv() {
49
60
  return { ...process.env, ...envLocal, PORT: port };
50
61
  }
51
62
 
52
- async function waitForServer(url, timeoutMs) {
53
- const deadline = Date.now() + timeoutMs;
54
- while (Date.now() < deadline) {
55
- try {
56
- const res = await fetch(url, { method: 'GET', redirect: 'follow' });
57
- if (res.status >= 200 && res.status < 500) {
58
- return;
59
- }
60
- } catch {
61
- // Server not ready yet
63
+ function sleep(ms) {
64
+ return new Promise((resolve) => setTimeout(resolve, ms));
65
+ }
66
+
67
+ async function isServerUp(url) {
68
+ try {
69
+ const res = await fetchWithTimeout(url, { method: 'GET', redirect: 'follow' });
70
+ return res.status >= 200 && res.status < 500;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function isPortListening(listenPort) {
77
+ return new Promise((resolve) => {
78
+ const socket = net.connect(Number(listenPort), '127.0.0.1');
79
+ socket.setTimeout(2_000);
80
+ socket.once('connect', () => {
81
+ socket.destroy();
82
+ resolve(true);
83
+ });
84
+ socket.once('timeout', () => {
85
+ socket.destroy();
86
+ resolve(false);
87
+ });
88
+ socket.once('error', () => resolve(false));
89
+ });
90
+ }
91
+
92
+ async function waitForServer(url, timeoutMs, options = {}) {
93
+ const { child, label = 'Server' } = options;
94
+ const started = Date.now();
95
+ let lastProgressAt = 0;
96
+ let childExited = false;
97
+ let childExitCode = null;
98
+
99
+ if (child) {
100
+ child.on('exit', (code) => {
101
+ childExited = true;
102
+ childExitCode = code;
103
+ });
104
+ }
105
+
106
+ while (Date.now() - started < timeoutMs) {
107
+ if (childExited) {
108
+ throw new Error(
109
+ `${label} process exited with code ${childExitCode ?? 'unknown'} before ${url} was ready`,
110
+ );
111
+ }
112
+
113
+ if (await isServerUp(url)) {
114
+ return Date.now() - started;
115
+ }
116
+
117
+ const elapsed = Date.now() - started;
118
+ if (elapsed - lastProgressAt >= progressIntervalMs) {
119
+ console.log(`Still waiting for ${url} (${Math.round(elapsed / 1000)}s)...`);
120
+ lastProgressAt = elapsed;
62
121
  }
63
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
122
+
123
+ await sleep(pollIntervalMs);
64
124
  }
65
- throw new Error(`Server did not respond at ${url} within ${timeoutMs}ms`);
125
+
126
+ throw new Error(`${label} did not respond at ${url} within ${timeoutMs}ms`);
66
127
  }
67
128
 
68
129
  function killProcessTree(child) {
@@ -78,21 +139,6 @@ function killProcessTree(child) {
78
139
  }
79
140
  }
80
141
 
81
- function createLogForwarder(onLine) {
82
- let buffer = '';
83
- return (chunk) => {
84
- process.stdout.write(chunk);
85
- buffer += chunk.toString();
86
- let newlineIndex = buffer.indexOf('\n');
87
- while (newlineIndex !== -1) {
88
- const line = buffer.slice(0, newlineIndex);
89
- buffer = buffer.slice(newlineIndex + 1);
90
- onLine(line);
91
- newlineIndex = buffer.indexOf('\n');
92
- }
93
- };
94
- }
95
-
96
142
  const env = loadEnv();
97
143
 
98
144
  if ((env.SMOKE_TEST_IGNORE ?? '').toLowerCase() === 'true') {
@@ -102,6 +148,7 @@ if ((env.SMOKE_TEST_IGNORE ?? '').toLowerCase() === 'true') {
102
148
 
103
149
  const serverScript = env.SMOKE_TEST_SERVER_SCRIPT ?? 'dev:dev';
104
150
  const verifyCache = (env.SMOKE_TEST_VERIFY_CACHE ?? '').toLowerCase() === 'true';
151
+ const useRunningServer = (env.SMOKE_TEST_USE_RUNNING_SERVER ?? 'true').toLowerCase() !== 'false';
105
152
  const readyPath = env.SMOKE_TEST_READY_PATH ?? '/';
106
153
  const readyUrl = `${baseUrl}${readyPath.startsWith('/') ? readyPath : `/${readyPath}`}`;
107
154
  const isProductionStart = serverScript === 'start';
@@ -119,36 +166,52 @@ if (isProductionStart) {
119
166
  }
120
167
  }
121
168
 
122
- let cmsFetchCount = 0;
123
-
124
- const logForwarder = createLogForwarder((line) => {
125
- if (!line.includes('[CMS fetch]')) return;
126
- cmsFetchCount++;
127
- });
128
-
129
169
  const smokeEnv = {
130
170
  ...env,
131
171
  SMOKE_TEST_PORT: port,
132
172
  ...(verifyCache ? { SMOKE_TEST_VERIFY_CACHE: 'true', LOG_CMS_FETCH: '1' } : {}),
133
173
  };
134
174
 
135
- console.log(`Starting server (${serverScript}) on ${baseUrl}...`);
136
- const server = spawn('pnpm', [serverScript], {
137
- cwd: appDir,
138
- env: smokeEnv,
139
- stdio: ['inherit', 'pipe', 'pipe'],
140
- detached: true,
141
- });
142
-
143
- server.stdout?.on('data', logForwarder);
144
- server.stderr?.on('data', logForwarder);
175
+ console.log(`Smoke test: checking ${readyUrl} (port ${port})...`);
145
176
 
177
+ let server = null;
178
+ let spawnedServer = false;
146
179
  let exitCode = 1;
147
180
 
148
181
  try {
149
- await waitForServer(readyUrl, startTimeoutMs);
150
- console.log(`Server ready at ${readyUrl}. Running smoke tests...`);
182
+ const serverUp = await isServerUp(readyUrl);
183
+ const portBusy = await isPortListening(port);
184
+
185
+ if (serverUp && useRunningServer) {
186
+ console.log(`Using existing server at ${readyUrl}.`);
187
+ } else if (serverUp && !useRunningServer) {
188
+ console.error(
189
+ `Server already running at ${readyUrl}. Stop it first, or set SMOKE_TEST_USE_RUNNING_SERVER=true.`,
190
+ );
191
+ process.exit(1);
192
+ } else if (portBusy) {
193
+ console.error(
194
+ `Port ${port} is in use but ${readyUrl} is not responding. Stop the process on :${port} and retry.`,
195
+ );
196
+ process.exit(1);
197
+ } else {
198
+ console.log(`Starting server (${serverScript}) on ${baseUrl}...`);
199
+ server = spawn('pnpm', [serverScript], {
200
+ cwd: appDir,
201
+ env: smokeEnv,
202
+ stdio: 'inherit',
203
+ detached: true,
204
+ });
205
+ spawnedServer = true;
206
+
207
+ const readyMs = await waitForServer(readyUrl, startTimeoutMs, {
208
+ child: server,
209
+ label: 'Dev server',
210
+ });
211
+ console.log(`Server ready at ${readyUrl} (${Math.round(readyMs / 1000)}s).`);
212
+ }
151
213
 
214
+ console.log('Running smoke tests...');
152
215
  const runResult = spawnSync('pnpm', ['smoke-test:run'], {
153
216
  cwd: appDir,
154
217
  stdio: 'inherit',
@@ -158,15 +221,19 @@ try {
158
221
 
159
222
  if (verifyCache) {
160
223
  console.log('');
161
- console.log('## CMS fetch log count (LOG_CMS_FETCH=1 on dev server)');
162
- console.log(` total [CMS fetch] lines during smoke run: ${cmsFetchCount}`);
163
- console.log(' (pass-level split is in x-nextjs-cache headers in the smoke report)');
224
+ console.log('## CMS fetch logging');
225
+ console.log(
226
+ ' LOG_CMS_FETCH=1 is set on the dev server check its terminal for [CMS fetch] lines.',
227
+ );
228
+ console.log(' Pass-level cache stats are in the smoke report (x-nextjs-cache).');
164
229
  }
165
230
  } catch (error) {
166
231
  console.error(error instanceof Error ? error.message : error);
167
232
  exitCode = 1;
168
233
  } finally {
169
- killProcessTree(server);
234
+ if (spawnedServer) {
235
+ killProcessTree(server);
236
+ }
170
237
  }
171
238
 
172
239
  process.exit(exitCode);