Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ssg-retries-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': minor
---

feat: add the ssg `retries` option to re-render a failed route before the build fails
2 changes: 1 addition & 1 deletion packages/docs/src/routes/api/qwik-router-ssg/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nemitData?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nemitHtml?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nexclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\ninclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nlog?\n\n\n</td><td>\n\n\n</td><td>\n\n'debug' \\| 'quiet'\n\n\n</td><td>\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n</td></tr>\n<tr><td>\n\nmaxTasksPerWorker?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nmaxWorkers?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n</td></tr>\n<tr><td>\n\norigin\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nThe URL `origin`<!-- -->, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`<!-- -->. However, the `origin` does not include a `pathname`<!-- -->.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`<!-- -->. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`<!-- -->, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`<!-- -->, please use the `basePathname` option in the Qwik Router config options.\n\n\n</td></tr>\n<tr><td>\n\noutDir\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nFile system directory where the static files should be written.\n\n\n</td></tr>\n<tr><td>\n\nsitemapOutFile?\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| null\n\n\n</td><td>\n\n_(Optional)_ 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.\n\n\n</td></tr>\n</tbody></table>",
"content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nemitData?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nemitHtml?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nexclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\ninclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nlog?\n\n\n</td><td>\n\n\n</td><td>\n\n'debug' \\| 'quiet'\n\n\n</td><td>\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n</td></tr>\n<tr><td>\n\nmaxTasksPerWorker?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nmaxWorkers?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n</td></tr>\n<tr><td>\n\norigin\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nThe URL `origin`<!-- -->, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`<!-- -->. However, the `origin` does not include a `pathname`<!-- -->.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`<!-- -->. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`<!-- -->, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`<!-- -->, please use the `basePathname` option in the Qwik Router config options.\n\n\n</td></tr>\n<tr><td>\n\noutDir\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nFile system directory where the static files should be written.\n\n\n</td></tr>\n<tr><td>\n\nretries?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ 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).\n\n\n</td></tr>\n<tr><td>\n\nsitemapOutFile?\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| null\n\n\n</td><td>\n\n_(Optional)_ 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.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/ssg/types.ts",
"mdFile": "router.ssgrenderoptions.md"
},
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/routes/api/qwik-router-ssg/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,21 @@ File system directory where the static files should be written.
</td></tr>
<tr><td>

retries?

</td><td>

</td><td>

number

</td><td>

_(Optional)_ 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).

</td></tr>
<tr><td>

sitemapOutFile?

</td><td>
Expand Down
13 changes: 13 additions & 0 deletions packages/qwik-router/src/ssg/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();

const queue: SsgRoute[] = [];
const active = new Set<string>();
Expand Down Expand Up @@ -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`))}`);
Expand Down
66 changes: 66 additions & 0 deletions packages/qwik-router/src/ssg/orchestrator.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,70 @@ test('should prerender <dir>/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 () => ({
Expand All @@ -129,6 +185,7 @@ function createSystem({
outDir: 'C:/tmp/out',
origin: 'https://qwik.dev',
basePathname,
retries,
include: ['/*'],
render: (() => null) as any,
qwikRouterConfig: {
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-router/src/ssg/qwik-router.ssg.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface SsgRenderOptions extends RenderOptions {
maxWorkers?: number;
origin: string;
outDir: string;
retries?: number;
sitemapOutFile?: string | null;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/qwik-router/src/ssg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading