From 904b53b9635699248aff22a897f637c4458c1595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Sun, 13 Oct 2024 09:57:59 +0200 Subject: [PATCH] fix(core): fix handling of Swc html minifier warnings (#10581) --- packages/docusaurus-bundler/src/minifyHtml.ts | 66 ++++--------- packages/docusaurus/src/ssg.ts | 95 +++++++++++++++++-- 2 files changed, 105 insertions(+), 56 deletions(-) diff --git a/packages/docusaurus-bundler/src/minifyHtml.ts b/packages/docusaurus-bundler/src/minifyHtml.ts index e84b03bede8d..f2c3eeec2a0c 100644 --- a/packages/docusaurus-bundler/src/minifyHtml.ts +++ b/packages/docusaurus-bundler/src/minifyHtml.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import logger from '@docusaurus/logger'; import {minify as terserHtmlMinifier} from 'html-minifier-terser'; import {importSwcHtmlMinifier} from './importFaster'; import type {DocusaurusConfig} from '@docusaurus/types'; @@ -13,12 +12,17 @@ import type {DocusaurusConfig} from '@docusaurus/types'; // Historical env variable const SkipHtmlMinification = process.env.SKIP_HTML_MINIFICATION === 'true'; +export type HtmlMinifierResult = { + code: string; + warnings: string[]; +}; + export type HtmlMinifier = { - minify: (html: string) => Promise; + minify: (html: string) => Promise; }; const NoopMinifier: HtmlMinifier = { - minify: async (html: string) => html, + minify: async (html: string) => ({code: html, warnings: []}), }; type SiteConfigSlice = { @@ -50,7 +54,7 @@ async function getTerserMinifier(): Promise { return { minify: async function minifyHtmlWithTerser(html) { try { - return await terserHtmlMinifier(html, { + const code = await terserHtmlMinifier(html, { removeComments: false, removeRedundantAttributes: true, removeEmptyAttributes: true, @@ -59,6 +63,7 @@ async function getTerserMinifier(): Promise { useShortDoctype: true, minifyJS: true, }); + return {code, warnings: []}; } catch (err) { throw new Error(`HTML minification failed (Terser)`, { cause: err as Error, @@ -95,49 +100,16 @@ async function getSwcMinifier(): Promise { minifyCss: true, }); - // Escape hatch because SWC is quite aggressive to report errors - // TODO figure out what to do with these errors: throw or swallow? - // See https://github.com/facebook/docusaurus/pull/10554 - // See https://github.com/swc-project/swc/discussions/9616#discussioncomment-10846201 - const ignoreSwcMinifierErrors = - process.env.DOCUSAURUS_IGNORE_SWC_HTML_MINIFIER_ERRORS === 'true'; - if (!ignoreSwcMinifierErrors && result.errors) { - const ignoredErrors: string[] = [ - // TODO Docusaurus seems to emit NULL chars, and minifier detects it - // see https://github.com/facebook/docusaurus/issues/9985 - 'Unexpected null character', - ]; - result.errors = result.errors.filter( - (diagnostic) => !ignoredErrors.includes(diagnostic.message), - ); - if (result.errors.length) { - throw new Error( - `HTML minification diagnostic errors: -- ${result.errors - .map( - (diagnostic) => - `[${diagnostic.level}] ${ - diagnostic.message - } - ${JSON.stringify(diagnostic.span)}`, - ) - .join('\n- ')} -Note: please report the problem to the Docusaurus team -In the meantime, you can skip this error with ${logger.code( - 'DOCUSAURUS_IGNORE_SWC_HTML_MINIFIER_ERRORS=true', - )}`, - ); - } - /* - if (result.errors.length) { - throw new AggregateError( - result.errors.map( - (diagnostic) => new Error(JSON.stringify(diagnostic, null, 2)), - ), - ); - } - */ - } - return result.code; + const warnings = (result.errors ?? []).map((diagnostic) => { + return `[HTML minifier diagnostic - ${diagnostic.level}] ${ + diagnostic.message + } - ${JSON.stringify(diagnostic.span)}`; + }); + + return { + code: result.code, + warnings, + }; } catch (err) { throw new Error(`HTML minification failed (SWC)`, { cause: err as Error, diff --git a/packages/docusaurus/src/ssg.ts b/packages/docusaurus/src/ssg.ts index 77f6a1cf5d7d..78895e4b1c6e 100644 --- a/packages/docusaurus/src/ssg.ts +++ b/packages/docusaurus/src/ssg.ts @@ -110,6 +110,57 @@ function pathnameToFilename({ return `${outputFileName}.html`; } +export function printSSGWarnings( + results: { + pathname: string; + warnings: string[]; + }[], +): void { + // Escape hatch because SWC is quite aggressive to report errors + // See https://github.com/facebook/docusaurus/pull/10554 + // See https://github.com/swc-project/swc/discussions/9616#discussioncomment-10846201 + if (process.env.DOCUSAURUS_IGNORE_SSG_WARNINGS === 'true') { + return; + } + + const ignoredWarnings: string[] = [ + // TODO React/Docusaurus emit NULL chars, and minifier detects it + // see https://github.com/facebook/docusaurus/issues/9985 + 'Unexpected null character', + ]; + + const keepWarning = (warning: string) => { + return !ignoredWarnings.some((iw) => warning.includes(iw)); + }; + + const resultsWithWarnings = results + .map((result) => { + return { + ...result, + warnings: result.warnings.filter(keepWarning), + }; + }) + .filter((result) => result.warnings.length > 0); + + if (resultsWithWarnings.length) { + const message = `Docusaurus static site generation process emitted warnings for ${ + resultsWithWarnings.length + } path${resultsWithWarnings.length ? 's' : ''} +This is non-critical and can be disabled with DOCUSAURUS_IGNORE_SSG_WARNINGS=true +Troubleshooting guide: https://github.com/facebook/docusaurus/discussions/10580 + +- ${resultsWithWarnings + .map( + (result) => `${logger.path(result.pathname)}: + - ${result.warnings.join('\n - ')} +`, + ) + .join('\n- ')}`; + + logger.warn(message); + } +} + export async function generateStaticFiles({ pathnames, renderer, @@ -121,8 +172,18 @@ export async function generateStaticFiles({ params: SSGParams; htmlMinifier: HtmlMinifier; }): Promise<{collectedData: SiteCollectedData}> { - type SSGSuccess = {pathname: string; error: null; result: AppRenderResult}; - type SSGError = {pathname: string; error: Error; result: null}; + type SSGSuccess = { + pathname: string; + error: null; + result: AppRenderResult; + warnings: string[]; + }; + type SSGError = { + pathname: string; + error: Error; + result: null; + warnings: string[]; + }; type SSGResult = SSGSuccess | SSGError; // Note that we catch all async errors on purpose @@ -136,15 +197,27 @@ export async function generateStaticFiles({ params, htmlMinifier, }).then( - (result) => ({pathname, result, error: null}), - (error) => ({pathname, result: null, error: error as Error}), + (result) => ({ + pathname, + result, + error: null, + warnings: result.warnings, + }), + (error) => ({ + pathname, + result: null, + error: error as Error, + warnings: [], + }), ), {concurrency: Concurrency}, ); + printSSGWarnings(results); + const [allSSGErrors, allSSGSuccesses] = _.partition( results, - (r): r is SSGError => !!r.error, + (result): result is SSGError => !!result.error, ); if (allSSGErrors.length > 0) { @@ -179,7 +252,7 @@ async function generateStaticFile({ renderer: AppRenderer; params: SSGParams; htmlMinifier: HtmlMinifier; -}) { +}): Promise { try { // This only renders the app HTML const result = await renderer({ @@ -190,13 +263,17 @@ async function generateStaticFile({ params, result, }); - const content = await htmlMinifier.minify(fullPageHtml); + const minifierResult = await htmlMinifier.minify(fullPageHtml); await writeStaticFile({ pathname, - content, + content: minifierResult.code, params, }); - return result; + return { + ...result, + // As of today, only the html minifier can emit SSG warnings + warnings: minifierResult.warnings, + }; } catch (errorUnknown) { const error = errorUnknown as Error; const tips = getSSGErrorTips(error);