Skip to content

Commit b100ef5

Browse files
authored
Merge pull request #8071 from QwikDev/v2-fix-ssg-windows
fix: ssg on windows
2 parents dbd78f6 + 1e5c740 commit b100ef5

File tree

12 files changed

+126
-124
lines changed

12 files changed

+126
-124
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,8 @@ jobs:
663663
# too slow and flaky. Perhaps better in v2?
664664
# - host: ubuntu-latest
665665
# browser: firefox
666+
- host: windows-latest
667+
browser: chromium
666668
- host: macos-latest
667669
browser: webkit
668670

@@ -760,8 +762,6 @@ jobs:
760762
run: pnpm run test.e2e.integrations.${{ matrix.settings.browser }} --timeout 60000 --retries 7 --workers 1
761763

762764
- name: Playwright E2E Qwik React Tests
763-
# This just fails constantly on Windows due to weird segfaults
764-
if: matrix.settings.host != 'windows-latest'
765765
run: pnpm run test.e2e.qwik-react.${{ matrix.settings.browser }} --timeout 60000 --retries 7 --workers 1
766766

767767
- name: CLI E2E Tests

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-router/src/adapters/shared/vite/index.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -232,34 +232,6 @@ export function viteAdapter(opts: ViteAdapterPluginOptions) {
232232
process.exit(0);
233233
}, 5000).unref();
234234
}
235-
if (opts.ssg !== null) {
236-
/**
237-
* HACK: for some reason the build hangs after SSG. `why-is-node-running` shows 4
238-
* culprits:
239-
*
240-
* ```
241-
* There are 4 handle(s) keeping the process running.
242-
*
243-
* # CustomGC
244-
* ./node_modules/.pnpm/[email protected]/node_modules/lightningcss/node/index.js:20 - module.exports = require(`lightningcss-${parts.join('-')}`);
245-
*
246-
* # CustomGC
247-
* ./node_modules/.pnpm/@[email protected]/node_modules/@tailwindcss/oxide/index.js:229 - return require('@tailwindcss/oxide-linux-x64-gnu')
248-
*
249-
* # Timeout
250-
* node_modules/.vite-temp/vite.config.timestamp-1755270314169-a2a97ad5233f9.mjs:357
251-
* ./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;
252-
*
253-
* # CustomGC
254-
* ./packages/qwik/dist/optimizer.mjs:1328 - const mod2 = module.default.createRequire(import.meta.url)(`../bindings/${triple.platformArchABI}`);
255-
* ```
256-
*
257-
* For now, we'll force exit the process after SSG with some delay.
258-
*/
259-
setTimeout(() => {
260-
process.exit(0);
261-
}, 5000).unref();
262-
}
263235
}
264236
},
265237
},

packages/qwik-router/src/ssg/deno/index.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/qwik-router/src/ssg/index.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { SsgOptions, SsgRenderOptions, SsgResult } from './types';
1010
*/
1111
export async function generate(opts: SsgOptions) {
1212
const ssgPlatform = await getEntryModule();
13-
const result: SsgResult = (await ssgPlatform.generate(opts)) as any;
13+
const result: SsgResult = await ssgPlatform.generate(opts);
1414
return result;
1515
}
1616

@@ -20,27 +20,10 @@ export type {
2020
SsgResult as StaticGenerateResult,
2121
};
2222

23-
function getEntryModule() {
24-
if (isDeno()) {
25-
return import('./deno');
23+
async function getEntryModule() {
24+
try {
25+
return await import('./node');
26+
} catch (e) {
27+
throw new Error(`Unsupported platform`, { cause: e });
2628
}
27-
if (isBun() || isNode()) {
28-
return import('./node');
29-
}
30-
throw new Error(`Unsupported platform`);
31-
}
32-
33-
function isDeno() {
34-
return typeof Deno !== 'undefined';
35-
}
36-
37-
function isBun() {
38-
return typeof Bun !== 'undefined';
39-
}
40-
41-
function isNode() {
42-
return !isBun() && !isDeno() && typeof process !== 'undefined' && !!process.versions?.node;
4329
}
44-
45-
declare const Deno: any;
46-
declare const Bun: any;

packages/qwik-router/src/ssg/main-thread.ts

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

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

@@ -135,6 +139,7 @@ export async function mainThread(sys: System) {
135139

136140
flushQueue();
137141
} catch (e) {
142+
console.error(`render failed for ${staticRoute.pathname}`, e);
138143
isCompleted = true;
139144
reject(e);
140145
}
@@ -217,8 +222,12 @@ export async function mainThread(sys: System) {
217222
flushQueue();
218223
};
219224

220-
loadStaticRoutes();
225+
loadStaticRoutes().catch((e) => {
226+
console.error('SSG route loading failed', e);
227+
reject(e);
228+
});
221229
} catch (e) {
230+
console.error('SSG main thread failed', e);
222231
reject(e);
223232
}
224233
});
@@ -251,6 +260,6 @@ function validateOptions(opts: SsgOptions) {
251260
try {
252261
new URL(siteOrigin);
253262
} catch (e) {
254-
throw new Error(`Invalid "origin": ${e}`);
263+
throw new Error(`Invalid "origin"`, { cause: e as Error });
255264
}
256265
}

packages/qwik-router/src/ssg/node/index.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SsgOptions } 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: SsgOptions) {
1515
}
1616

1717
if (!isMainThread && workerData) {
18+
const opts = workerData as SsgOptions;
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-router/src/ssg/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: SsgOptions) {
1918
const ssgWorkers: SsgWorker[] = [];
@@ -51,45 +50,34 @@ export async function createNodeMainProcess(sys: System, opts: SsgOptions) {
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: SsgWorker = {
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: SsgWorker = {
9482
activeTasks: 0,
9583
totalTasks: 0,
@@ -116,7 +104,9 @@ export async function createNodeMainProcess(sys: System, opts: SsgOptions) {
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: SsgOptions) {
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: SsgOptions) {
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: SsgOptions) {
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-router/src/ssg/node/node-system.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createNodeWorkerProcess } from './node-worker';
77
import { normalizePath } from '../../utils/fs';
88

99
/** @public */
10-
export async function createSystem(opts: SsgOptions) {
10+
export async function createSystem(opts: SsgOptions, threadId?: number): Promise<System> {
1111
const createWriteStream = (filePath: string) => {
1212
return fs.createWriteStream(filePath, {
1313
flags: 'w',
@@ -26,6 +26,13 @@ export async function createSystem(opts: SsgOptions) {
2626
};
2727

2828
const createLogger = async () => {
29+
if (threadId !== undefined) {
30+
return {
31+
debug: opts.log === 'debug' ? console.debug.bind(console, `[${threadId}]`) : () => {},
32+
error: console.error.bind(console, `[${threadId}]`),
33+
info: console.info.bind(console, `[${threadId}]`),
34+
};
35+
}
2936
return {
3037
debug: opts.log === 'debug' ? console.debug.bind(console) : () => {},
3138
error: console.error.bind(console),

packages/qwik-router/src/ssg/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
}

0 commit comments

Comments
 (0)