Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): fix handling of Swc html minifier warnings #10581

Merged
merged 2 commits into from
Oct 13, 2024
Merged
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
66 changes: 19 additions & 47 deletions packages/docusaurus-bundler/src/minifyHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@
* 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';

// 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<string>;
minify: (html: string) => Promise<HtmlMinifierResult>;
};

const NoopMinifier: HtmlMinifier = {
minify: async (html: string) => html,
minify: async (html: string) => ({code: html, warnings: []}),
};

type SiteConfigSlice = {
Expand Down Expand Up @@ -50,7 +54,7 @@ async function getTerserMinifier(): Promise<HtmlMinifier> {
return {
minify: async function minifyHtmlWithTerser(html) {
try {
return await terserHtmlMinifier(html, {
const code = await terserHtmlMinifier(html, {
removeComments: false,
removeRedundantAttributes: true,
removeEmptyAttributes: true,
Expand All @@ -59,6 +63,7 @@ async function getTerserMinifier(): Promise<HtmlMinifier> {
useShortDoctype: true,
minifyJS: true,
});
return {code, warnings: []};
} catch (err) {
throw new Error(`HTML minification failed (Terser)`, {
cause: err as Error,
Expand Down Expand Up @@ -95,49 +100,16 @@ async function getSwcMinifier(): Promise<HtmlMinifier> {
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,
Expand Down
95 changes: 86 additions & 9 deletions packages/docusaurus/src/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -179,7 +252,7 @@ async function generateStaticFile({
renderer: AppRenderer;
params: SSGParams;
htmlMinifier: HtmlMinifier;
}) {
}): Promise<AppRenderResult & {warnings: string[]}> {
try {
// This only renders the app HTML
const result = await renderer({
Expand All @@ -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);
Expand Down