Skip to content

Commit b809c8f

Browse files
committed
chore(router): better error handling for SSG
1 parent 354d1cf commit b809c8f

File tree

10 files changed

+87
-113
lines changed

10 files changed

+87
-113
lines changed

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: 15 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,19 @@ 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+
console.log(`Worker thread starting (ID: ${threadId})`);
23+
}
24+
// self initializing worker thread with workerData
25+
const sys = await createSystem(opts, threadId);
26+
await workerThread(sys);
27+
} catch (error) {
28+
console.error(`Error occurred in worker thread (ID: ${threadId}): ${error}`);
29+
}
30+
})().catch((e) => {
31+
console.error(e);
32+
});
2333
}

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

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Worker } from 'node:worker_threads';
1313
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,28 +50,7 @@ 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

@@ -98,7 +76,7 @@ export async function createNodeMainProcess(sys: System, opts: SsgOptions) {
9876
}
9977

10078
const nodeWorker = new Worker(workerFilePath, { workerData: opts });
101-
79+
nodeWorker.unref();
10280
const ssgWorker: SsgWorker = {
10381
activeTasks: 0,
10482
totalTasks: 0,
@@ -223,7 +201,7 @@ export async function createNodeMainProcess(sys: System, opts: SsgOptions) {
223201
}
224202

225203
for (let i = 0; i < maxWorkers; i++) {
226-
ssgWorkers.push(createWorker(i));
204+
ssgWorkers.push(createWorker());
227205
}
228206

229207
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
}

packages/qwik-router/src/ssg/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: () => SsgOptions;
1212
ensureDir: (filePath: string) => Promise<void>;

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

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,46 @@ export async function workerThread(sys: System) {
2222
delete (globalThis as any).__qwik;
2323
const ssgOpts = sys.getOptions();
2424
const pendingPromises = new Set<Promise<any>>();
25+
const log = await sys.createLogger();
2526

2627
const opts: SsgHandlerOptions = {
2728
...ssgOpts,
29+
// TODO export this from server
2830
render: (await import(pathToFileURL(ssgOpts.renderModulePath).href)).default,
31+
// TODO this should be built-in
2932
qwikRouterConfig: (await import(pathToFileURL(ssgOpts.qwikRouterConfigModulePath).href))
3033
.default,
3134
};
3235

33-
sys.createWorkerProcess(async (msg) => {
34-
switch (msg.type) {
35-
case 'render': {
36-
return new Promise<SsgWorkerRenderResult>((resolve) => {
37-
workerRender(sys, opts, msg, pendingPromises, resolve);
38-
});
39-
}
40-
case 'close': {
41-
const promises = Array.from(pendingPromises);
42-
pendingPromises.clear();
43-
await Promise.all(promises);
44-
return { type: 'close' };
36+
sys
37+
.createWorkerProcess(async (msg) => {
38+
switch (msg.type) {
39+
case 'render': {
40+
log.debug(`Worker thread rendering: ${msg.pathname}`);
41+
return new Promise<SsgWorkerRenderResult>((resolve) => {
42+
workerRender(sys, opts, msg, pendingPromises, resolve).catch((e) => {
43+
console.error('Error during render', msg.pathname, e);
44+
});
45+
});
46+
}
47+
case 'close': {
48+
if (pendingPromises.size) {
49+
log.debug(`Worker thread closing, waiting for ${pendingPromises.size} pending renders`);
50+
const promises = Array.from(pendingPromises);
51+
pendingPromises.clear();
52+
await Promise.all(promises);
53+
}
54+
log.debug(`Worker thread closed`);
55+
return { type: 'close' };
56+
}
4557
}
46-
}
47-
});
58+
})
59+
?.catch((e) => {
60+
console.error('Worker process creation failed', e);
61+
});
4862
}
4963

5064
export async function createSingleThreadWorker(sys: System) {
51-
// Special case: we allow importing qwik again in the same process, it's ok because we just needed the serializer
52-
// TODO: remove this once we have vite environment API and no longer need the serializer separately
53-
delete (globalThis as any).__qwik;
5465
const ssgOpts = sys.getOptions();
5566
const pendingPromises = new Set<Promise<any>>();
5667

@@ -63,7 +74,9 @@ export async function createSingleThreadWorker(sys: System) {
6374

6475
return (staticRoute: SsgRoute) => {
6576
return new Promise<SsgWorkerRenderResult>((resolve) => {
66-
workerRender(sys, opts, staticRoute, pendingPromises, resolve);
77+
workerRender(sys, opts, staticRoute, pendingPromises, resolve).catch((e) => {
78+
console.error('Error during render', staticRoute.pathname, e);
79+
});
6780
});
6881
};
6982
}
@@ -270,6 +283,13 @@ async function workerRender(
270283
console.error('Error during request handling', staticRoute.pathname, e);
271284
}
272285
})
286+
.catch((e) => {
287+
console.error('Unhandled error during request handling', staticRoute.pathname, e);
288+
result.error = {
289+
message: String(e),
290+
stack: e.stack || '',
291+
};
292+
})
273293
.finally(() => {
274294
pendingPromises.delete(promise);
275295
callback(result);

0 commit comments

Comments
 (0)