diff --git a/.changeset/ssg-retries-option.md b/.changeset/ssg-retries-option.md
new file mode 100644
index 00000000000..28802038dc9
--- /dev/null
+++ b/.changeset/ssg-retries-option.md
@@ -0,0 +1,5 @@
+---
+'@qwik.dev/router': minor
+---
+
+feat: add the ssg `retries` option to re-render a failed route before the build fails
diff --git a/packages/docs/src/routes/api/qwik-router-ssg/api.json b/packages/docs/src/routes/api/qwik-router-ssg/api.json
index 6917f09ae91..3172ece372e 100644
--- a/packages/docs/src/routes/api/qwik-router-ssg/api.json
+++ b/packages/docs/src/routes/api/qwik-router-ssg/api.json
@@ -68,7 +68,7 @@
}
],
"kind": "Interface",
- "content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n
|
+
sitemapOutFile?
|
diff --git a/packages/qwik-router/src/ssg/orchestrator.ts b/packages/qwik-router/src/ssg/orchestrator.ts
index 439a984ad71..d27983b1349 100644
--- a/packages/qwik-router/src/ssg/orchestrator.ts
+++ b/packages/qwik-router/src/ssg/orchestrator.ts
@@ -16,6 +16,8 @@ export async function mainThread(sys: System) {
const qwikRouterConfig = opts.qwikRouterConfig;
const renderTimeout = 30_000;
+ const maxRetries = Math.max(0, opts.retries ?? 0);
+ const failedAttempts = new Map();
const queue: SsgRoute[] = [];
const active = new Set();
@@ -117,6 +119,17 @@ export async function mainThread(sys: System) {
log.debug(`render done: ${staticRoute.pathname}`);
if (result.error) {
+ const attempt = (failedAttempts.get(staticRoute.pathname) ?? 0) + 1;
+ if (attempt <= maxRetries) {
+ failedAttempts.set(staticRoute.pathname, attempt);
+ log.info(
+ `Retrying ${magenta(staticRoute.pathname)} after a failed render (${attempt} of ${maxRetries})`
+ );
+ queue.push(staticRoute);
+ flushQueue();
+ return;
+ }
+
const err = new Error(result.error.message);
err.stack = result.error.stack;
log.error(`\n${bold(red(`!!! ${result.pathname}: Error during SSG`))}`);
diff --git a/packages/qwik-router/src/ssg/orchestrator.unit.ts b/packages/qwik-router/src/ssg/orchestrator.unit.ts
index 54667a0000d..0be3791b339 100644
--- a/packages/qwik-router/src/ssg/orchestrator.unit.ts
+++ b/packages/qwik-router/src/ssg/orchestrator.unit.ts
@@ -108,14 +108,70 @@ test('should prerender /404.html for each _4 / _E boundary', async () => {
assert.include(result.staticPaths, '/blog/404.html');
});
+test('retries a failed route until it succeeds', async () => {
+ let renderCount = 0;
+ const sys = createSystem({
+ routes: { _I: async () => ({ default: () => null as any }) },
+ render: async ({ pathname }) => {
+ renderCount++;
+ return renderCount === 1 ? createFailedRenderResult(pathname) : createRenderResult(pathname);
+ },
+ retries: 2,
+ });
+
+ const result = await mainThread(sys);
+
+ assert.equal(renderCount, 2);
+ assert.equal(result.errors, 0);
+ assert.equal(result.rendered, 1);
+ assert.deepEqual(result.staticPaths, ['/']);
+});
+
+test('records the error once all retries fail', async () => {
+ let renderCount = 0;
+ const sys = createSystem({
+ routes: { _I: async () => ({ default: () => null as any }) },
+ render: async ({ pathname }) => {
+ renderCount++;
+ return createFailedRenderResult(pathname);
+ },
+ retries: 2,
+ });
+
+ const result = await mainThread(sys);
+
+ assert.equal(renderCount, 3);
+ assert.equal(result.errors, 1);
+ assert.equal(result.rendered, 0);
+ assert.deepEqual(result.staticPaths, []);
+});
+
+test('does not retry a failed route by default', async () => {
+ let renderCount = 0;
+ const sys = createSystem({
+ routes: { _I: async () => ({ default: () => null as any }) },
+ render: async ({ pathname }) => {
+ renderCount++;
+ return createFailedRenderResult(pathname);
+ },
+ });
+
+ const result = await mainThread(sys);
+
+ assert.equal(renderCount, 1);
+ assert.equal(result.errors, 1);
+});
+
function createSystem({
routes,
render,
basePathname = '/',
+ retries,
}: {
routes: RouteData;
render: MainContext['render'];
basePathname?: string;
+ retries?: number;
}): System {
return {
createMainProcess: async () => ({
@@ -129,6 +185,7 @@ function createSystem({
outDir: 'C:/tmp/out',
origin: 'https://qwik.dev',
basePathname,
+ retries,
include: ['/*'],
render: (() => null) as any,
qwikRouterConfig: {
@@ -150,6 +207,15 @@ function createSystem({
};
}
+function createFailedRenderResult(pathname: string) {
+ return {
+ ...createRenderResult(pathname),
+ ok: false,
+ error: { message: 'render blip', stack: undefined },
+ filePath: null,
+ };
+}
+
function createRenderResult(pathname: string) {
return {
type: 'render' as const,
diff --git a/packages/qwik-router/src/ssg/qwik-router.ssg.api.md b/packages/qwik-router/src/ssg/qwik-router.ssg.api.md
index 4b25c7db2ce..b701c602877 100644
--- a/packages/qwik-router/src/ssg/qwik-router.ssg.api.md
+++ b/packages/qwik-router/src/ssg/qwik-router.ssg.api.md
@@ -41,6 +41,7 @@ export interface SsgRenderOptions extends RenderOptions {
maxWorkers?: number;
origin: string;
outDir: string;
+ retries?: number;
sitemapOutFile?: string | null;
}
diff --git a/packages/qwik-router/src/ssg/types.ts b/packages/qwik-router/src/ssg/types.ts
index 11782506992..929508f6ca1 100644
--- a/packages/qwik-router/src/ssg/types.ts
+++ b/packages/qwik-router/src/ssg/types.ts
@@ -59,6 +59,11 @@ export interface SsgRenderOptions extends RenderOptions {
maxWorkers?: number;
/** Maximum number of tasks to be running at one time per worker. Defaults to `20`. */
maxTasksPerWorker?: number;
+ /**
+ * Number of times to re-render a route whose render failed before the route is recorded as an
+ * error. Defaults to `0` (a failed render fails the route right away).
+ */
+ retries?: number;
/**
* File system path to write the `sitemap.xml` to. Defaults to `sitemap.xml` and written to the
* root of the `outDir`. Setting to `null` will prevent the sitemap from being created.
|