Skip to content

Commit 6e866a0

Browse files
authored
Merge pull request #8072 from QwikDev/fix-ssg-windows
fix: ssg on windows
2 parents 4922047 + 47a3b9f commit 6e866a0

File tree

10 files changed

+123
-93
lines changed

10 files changed

+123
-93
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,10 +682,10 @@ jobs:
682682
# too slow and flaky. Perhaps better in v2?
683683
# - host: ubuntu-latest
684684
# browser: firefox
685+
- host: windows-latest
686+
browser: chromium
685687
- host: macos-latest
686688
browser: webkit
687-
# - host: windows-latest
688-
# browser: chromium
689689

690690
runs-on: ${{ matrix.settings.host }}
691691

e2e/adapters-e2e/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@ export default defineConfig({
4444
port: 3000,
4545
stdout: 'pipe',
4646
reuseExistingServer: !process.env.CI,
47+
timeout: 120000,
4748
},
4849
});

packages/qwik-city/src/adapters/shared/vite/index.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -202,34 +202,6 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) {
202202
`\n==============================================`
203203
);
204204
}
205-
if (opts.ssg !== null) {
206-
/**
207-
* HACK: for some reason the build hangs after SSG. `why-is-node-running` shows 4
208-
* culprits:
209-
*
210-
* ```
211-
* There are 4 handle(s) keeping the process running.
212-
*
213-
* # CustomGC
214-
* ./node_modules/.pnpm/[email protected]/node_modules/lightningcss/node/index.js:20 - module.exports = require(`lightningcss-${parts.join('-')}`);
215-
*
216-
* # CustomGC
217-
* ./node_modules/.pnpm/@[email protected]/node_modules/@tailwindcss/oxide/index.js:229 - return require('@tailwindcss/oxide-linux-x64-gnu')
218-
*
219-
* # Timeout
220-
* node_modules/.vite-temp/vite.config.timestamp-1755270314169-a2a97ad5233f9.mjs:357
221-
* ./node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected][email protected][email protected]/node_modules/vite/dist/node/chunks/dep-CMEinpL-.js:36657 - return (await import(pathToFileURL(tempFileName).href)).default;
222-
*
223-
* # CustomGC
224-
* ./packages/qwik/dist/optimizer.mjs:1328 - const mod2 = module.default.createRequire(import.meta.url)(`../bindings/${triple.platformArchABI}`);
225-
* ```
226-
*
227-
* For now, we'll force exit the process after SSG with some delay.
228-
*/
229-
setTimeout(() => {
230-
process.exit(0);
231-
}, 5000).unref();
232-
}
233205
}
234206
},
235207
},

packages/qwik-city/src/static/main-thread.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,17 @@ export async function mainThread(sys: System) {
8080
while (!isCompleted && main.hasAvailableWorker() && queue.length > 0) {
8181
const staticRoute = queue.shift();
8282
if (staticRoute) {
83-
render(staticRoute);
83+
render(staticRoute).catch((e) => {
84+
console.error(`render failed for ${staticRoute.pathname}`, e);
85+
});
8486
}
8587
}
8688

8789
if (!isCompleted && isRoutesLoaded && queue.length === 0 && active.size === 0) {
8890
isCompleted = true;
89-
completed();
91+
completed().catch((e) => {
92+
console.error('SSG completion failed', e);
93+
});
9094
}
9195
};
9296

@@ -134,6 +138,7 @@ export async function mainThread(sys: System) {
134138

135139
flushQueue();
136140
} catch (e) {
141+
console.error(`render failed for ${staticRoute.pathname}`, e);
137142
isCompleted = true;
138143
reject(e);
139144
}
@@ -216,8 +221,12 @@ export async function mainThread(sys: System) {
216221
flushQueue();
217222
};
218223

219-
loadStaticRoutes();
224+
loadStaticRoutes().catch((e) => {
225+
console.error('SSG route loading failed', e);
226+
reject(e);
227+
});
220228
} catch (e) {
229+
console.error('SSG main thread failed', e);
221230
reject(e);
222231
}
223232
});
@@ -244,6 +253,6 @@ function validateOptions(opts: StaticGenerateOptions) {
244253
try {
245254
new URL(siteOrigin);
246255
} catch (e) {
247-
throw new Error(`Invalid "origin": ${e}`);
256+
throw new Error(`Invalid "origin"`, { cause: e as Error });
248257
}
249258
}

packages/qwik-city/src/static/node/index.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { StaticGenerateOptions } from '../types';
22
import { createSystem } from './node-system';
3-
import { isMainThread, workerData } from 'node:worker_threads';
3+
import { isMainThread, workerData, threadId } from 'node:worker_threads';
44
import { mainThread } from '../main-thread';
55
import { workerThread } from '../worker-thread';
66

@@ -15,9 +15,20 @@ export async function generate(opts: StaticGenerateOptions) {
1515
}
1616

1717
if (!isMainThread && workerData) {
18+
const opts = workerData as StaticGenerateOptions;
1819
(async () => {
19-
// self initializing worker thread with workerData
20-
const sys = await createSystem(workerData);
21-
await workerThread(sys);
22-
})();
20+
try {
21+
if (opts.log === 'debug') {
22+
// eslint-disable-next-line no-console
23+
console.debug(`Worker thread starting (ID: ${threadId})`);
24+
}
25+
// self initializing worker thread with workerData
26+
const sys = await createSystem(opts, threadId);
27+
await workerThread(sys);
28+
} catch (error) {
29+
console.error(`Error occurred in worker thread (ID: ${threadId}): ${error}`);
30+
}
31+
})().catch((e) => {
32+
console.error(e);
33+
});
2334
}

packages/qwik-city/src/static/node/node-main.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import type {
1010
import fs from 'node:fs';
1111
import { cpus as nodeCpus } from 'node:os';
1212
import { Worker } from 'node:worker_threads';
13-
import { isAbsolute, resolve } from 'node:path';
13+
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
1414
import { ensureDir } from './node-system';
1515
import { normalizePath } from '../../utils/fs';
16-
import { createSingleThreadWorker } from '../worker-thread';
1716

1817
export async function createNodeMainProcess(sys: System, opts: StaticGenerateOptions) {
1918
const ssgWorkers: StaticGeneratorWorker[] = [];
@@ -51,45 +50,34 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
5150
}
5251
}
5352

54-
const singleThreadWorker = await createSingleThreadWorker(sys);
55-
56-
const createWorker = (workerIndex: number) => {
57-
if (workerIndex === 0) {
58-
// same thread worker, don't start a new process
59-
const ssgSameThreadWorker: StaticGeneratorWorker = {
60-
activeTasks: 0,
61-
totalTasks: 0,
62-
63-
render: async (staticRoute) => {
64-
ssgSameThreadWorker.activeTasks++;
65-
ssgSameThreadWorker.totalTasks++;
66-
const result = await singleThreadWorker(staticRoute);
67-
ssgSameThreadWorker.activeTasks--;
68-
return result;
69-
},
70-
71-
terminate: async () => {},
72-
};
73-
return ssgSameThreadWorker;
74-
}
75-
53+
const createWorker = () => {
7654
let terminateResolve: (() => void) | null = null;
7755
const mainTasks = new Map<string, WorkerMainTask>();
7856

7957
let workerFilePath: string | URL;
58+
let terminateTimeout: number | null = null;
8059

60+
// Launch the worker using the package's index module, which bootstraps the worker thread.
8161
if (typeof __filename === 'string') {
82-
workerFilePath = __filename;
62+
// CommonJS path
63+
const ext = extname(__filename) || '.js';
64+
workerFilePath = join(dirname(__filename), `index${ext}`);
8365
} else {
84-
workerFilePath = import.meta.url;
85-
}
66+
// ESM path (import.meta.url)
67+
const thisUrl = new URL(import.meta.url);
68+
const pathname = thisUrl.pathname || '';
69+
let ext = '.js';
70+
if (pathname.endsWith('.ts')) {
71+
ext = '.ts';
72+
} else if (pathname.endsWith('.mjs')) {
73+
ext = '.mjs';
74+
}
8675

87-
if (typeof workerFilePath === 'string' && workerFilePath.startsWith('file://')) {
88-
workerFilePath = new URL(workerFilePath);
76+
workerFilePath = new URL(`./index${ext}`, thisUrl);
8977
}
9078

9179
const nodeWorker = new Worker(workerFilePath, { workerData: opts });
92-
80+
nodeWorker.unref();
9381
const ssgWorker: StaticGeneratorWorker = {
9482
activeTasks: 0,
9583
totalTasks: 0,
@@ -116,7 +104,9 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
116104
terminateResolve = resolve;
117105
nodeWorker.postMessage(msg);
118106
});
119-
await nodeWorker.terminate();
107+
terminateTimeout = setTimeout(async () => {
108+
await nodeWorker.terminate();
109+
}, 1000) as unknown as number;
120110
},
121111
};
122112

@@ -146,7 +136,11 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
146136
});
147137

148138
nodeWorker.on('exit', (code) => {
149-
if (code !== 1) {
139+
if (terminateTimeout) {
140+
clearTimeout(terminateTimeout);
141+
terminateTimeout = null;
142+
}
143+
if (code !== 0) {
150144
console.error(`worker exit ${code}`);
151145
}
152146
});
@@ -200,9 +194,15 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
200194
console.error(e);
201195
}
202196
}
203-
ssgWorkers.length = 0;
204197

205198
await Promise.all(promises);
199+
ssgWorkers.length = 0;
200+
201+
// On Windows, give extra time for all workers to fully exit
202+
// This prevents resource conflicts in back-to-back builds
203+
if (process.platform === 'win32') {
204+
await new Promise((resolve) => setTimeout(resolve, 300));
205+
}
206206
};
207207

208208
if (sitemapOutFile) {
@@ -214,7 +214,11 @@ export async function createNodeMainProcess(sys: System, opts: StaticGenerateOpt
214214
}
215215

216216
for (let i = 0; i < maxWorkers; i++) {
217-
ssgWorkers.push(createWorker(i));
217+
ssgWorkers.push(createWorker());
218+
// On Windows, add delay between worker creation to avoid resource contention
219+
if (process.platform === 'win32' && i < maxWorkers - 1) {
220+
await new Promise((resolve) => setTimeout(resolve, 100));
221+
}
218222
}
219223

220224
const mainCtx: MainContext = {

packages/qwik-city/src/static/node/node-system.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
import type { StaticGenerateOptions, System } from '../types';
33
import fs from 'node:fs';
44
import { dirname, join } from 'node:path';
5-
import { patchGlobalThis } from '../../middleware/node/node-fetch';
65
import { createNodeMainProcess } from './node-main';
76
import { createNodeWorkerProcess } from './node-worker';
87
import { normalizePath } from '../../utils/fs';
98

109
/** @public */
11-
export async function createSystem(opts: StaticGenerateOptions) {
12-
patchGlobalThis();
13-
10+
export async function createSystem(
11+
opts: StaticGenerateOptions,
12+
threadId?: number
13+
): Promise<System> {
1414
const createWriteStream = (filePath: string) => {
1515
return fs.createWriteStream(filePath, {
1616
flags: 'w',
@@ -29,6 +29,13 @@ export async function createSystem(opts: StaticGenerateOptions) {
2929
};
3030

3131
const createLogger = async () => {
32+
if (threadId !== undefined) {
33+
return {
34+
debug: opts.log === 'debug' ? console.debug.bind(console, `[${threadId}]`) : () => {},
35+
error: console.error.bind(console, `[${threadId}]`),
36+
info: console.info.bind(console, `[${threadId}]`),
37+
};
38+
}
3239
return {
3340
debug: opts.log === 'debug' ? console.debug.bind(console) : () => {},
3441
error: console.error.bind(console),

packages/qwik-city/src/static/node/node-worker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ export async function createNodeWorkerProcess(
66
) {
77
parentPort?.on('message', async (msg: WorkerInputMessage) => {
88
parentPort?.postMessage(await onMessage(msg));
9+
if (msg.type === 'close') {
10+
parentPort?.close();
11+
}
912
});
1013
}

packages/qwik-city/src/static/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface System {
66
createMainProcess: (() => Promise<MainContext>) | null;
77
createWorkerProcess: (
88
onMessage: (msg: WorkerInputMessage) => Promise<WorkerOutputMessage>
9-
) => void;
9+
) => void | Promise<void>;
1010
createLogger: () => Promise<Logger>;
1111
getOptions: () => StaticGenerateOptions;
1212
ensureDir: (filePath: string) => Promise<void>;

0 commit comments

Comments
 (0)