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\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nemitData?\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\nemitHtml?\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\nexclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\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
\n\ninclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\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
\n\nlog?\n\n\n\n\n\n\n\n'debug' \\| 'quiet'\n\n\n\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n
\n\nmaxTasksPerWorker?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`.\n\n\n
\n\nmaxWorkers?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n
\n\norigin\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\noutDir\n\n\n\n\n\n\n\nstring\n\n\n\n\nFile system directory where the static files should be written.\n\n\n
\n\nsitemapOutFile?\n\n\n\n\n\n\n\nstring \\| null\n\n\n\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
", + "content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nemitData?\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\nemitHtml?\n\n\n\n\n\n\n\nboolean\n\n\n\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
\n\nexclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\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
\n\ninclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\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
\n\nlog?\n\n\n\n\n\n\n\n'debug' \\| 'quiet'\n\n\n\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n
\n\nmaxTasksPerWorker?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`.\n\n\n
\n\nmaxWorkers?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n
\n\norigin\n\n\n\n\n\n\n\nstring\n\n\n\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
\n\noutDir\n\n\n\n\n\n\n\nstring\n\n\n\n\nFile system directory where the static files should be written.\n\n\n
\n\nretries?\n\n\n\n\n\n\n\nnumber\n\n\n\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
\n\nsitemapOutFile?\n\n\n\n\n\n\n\nstring \\| null\n\n\n\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
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/ssg/types.ts", "mdFile": "router.ssgrenderoptions.md" }, diff --git a/packages/docs/src/routes/api/qwik-router-ssg/index.mdx b/packages/docs/src/routes/api/qwik-router-ssg/index.mdx index 3286397c329..8528d8564a2 100644 --- a/packages/docs/src/routes/api/qwik-router-ssg/index.mdx +++ b/packages/docs/src/routes/api/qwik-router-ssg/index.mdx @@ -387,6 +387,21 @@ File system directory where the static files should be written. +retries? + + + + + +number + + + +_(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). + + + + 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.