From 3e6bc934d6fa9d7d45269bb8e49c0bf5cacf918f Mon Sep 17 00:00:00 2001 From: haranishi <223786623+haranishi@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:27:33 +0900 Subject: [PATCH 1/2] fix(bundler): retain source locations in build errors (AI-assisted) --- .../src/__tests__/compiler.test.ts | 119 ++++++++++++++++++ packages/docusaurus-bundler/src/compiler.ts | 14 ++- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 packages/docusaurus-bundler/src/__tests__/compiler.test.ts diff --git a/packages/docusaurus-bundler/src/__tests__/compiler.test.ts b/packages/docusaurus-bundler/src/__tests__/compiler.test.ts new file mode 100644 index 000000000000..7ed80cb05236 --- /dev/null +++ b/packages/docusaurus-bundler/src/__tests__/compiler.test.ts @@ -0,0 +1,119 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {describe, expect, it} from 'vitest'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import {tmpdir} from 'node:os'; +import {compile, formatStatsErrorMessage} from '../compiler'; +import {getCurrentBundler} from '../currentBundler'; + +describe('formatStatsErrorMessage', () => { + it('includes the source module and location', () => { + const result = formatStatsErrorMessage({ + errors: [ + { + moduleIdentifier: '/project/src/pages/thing.xyz', + file: 'main.js', + moduleName: './src/pages/thing.xyz', + loc: '1:0', + message: 'Module parse failed: Unexpected character', + }, + ], + warnings: [], + }); + expect(result).toContain('./src/pages/thing.xyz:1:0'); + expect(result).toContain('Module parse failed: Unexpected character'); + expect(result).not.toContain('/project/'); + expect(result).not.toContain('main.js'); + }); + + it('uses the file when no source module is provided', () => { + expect( + formatStatsErrorMessage({ + errors: [{file: 'styles.css', message: 'Invalid CSS'}], + warnings: [], + }), + ).toContain('styles.css'); + }); + + it('keeps errors without source metadata', () => { + expect( + formatStatsErrorMessage({ + errors: [{message: 'A plain error'}], + warnings: [], + }), + ).toContain('A plain error'); + }); + + it('keeps the source of each formatted error', () => { + const result = formatStatsErrorMessage({ + errors: [ + {moduleName: './first.js', message: 'First error'}, + {moduleName: './second.js', message: 'Second error'}, + ], + warnings: [], + }); + expect(result).toContain('./first.js'); + expect(result).toContain('First error'); + expect(result).toContain('./second.js'); + expect(result).toContain('Second error'); + expect(result).toContain('--------------------------'); + }); + + it('keeps the matching source when syntax errors filter other errors', () => { + const result = formatStatsErrorMessage({ + errors: [ + {moduleName: './other.js', message: 'Another error'}, + {moduleName: './broken.js', message: 'Syntax error: Invalid token'}, + ], + warnings: [], + }); + expect(result).toContain('./broken.js'); + expect(result).toContain('Syntax error: Invalid token'); + expect(result).not.toContain('./other.js'); + expect(result).not.toContain('Another error'); + }); + + it('returns undefined when there are no errors', () => { + expect(formatStatsErrorMessage(undefined)).toBeUndefined(); + expect(formatStatsErrorMessage({errors: [], warnings: []})).toBeUndefined(); + }); +}); + +describe.each(['webpack', 'rspack'] as const)( + '%s error diagnostics', + (name) => { + it('identifies the source of a real module parse failure', async () => { + await using dir = await fs.mkdtempDisposable( + path.join(await fs.realpath(tmpdir()), 'docusaurus-compiler-'), + ); + await fs.mkdir(path.join(dir.path, 'src')); + await fs.writeFile( + path.join(dir.path, 'src/index.js'), + "import './thing.xyz';", + ); + await fs.writeFile(path.join(dir.path, 'src/thing.xyz'), '🔥 invalid'); + const currentBundler = await getCurrentBundler({ + siteConfig: {webpack: name === 'webpack' ? {} : undefined}, + }); + await expect( + compile({ + currentBundler, + configs: [ + { + mode: 'development', + context: dir.path, + entry: './src/index.js', + output: {path: path.join(dir.path, 'build')}, + }, + ], + }), + ).rejects.toThrow('./src/thing.xyz'); + }); + }, +); diff --git a/packages/docusaurus-bundler/src/compiler.ts b/packages/docusaurus-bundler/src/compiler.ts index 7faaea10ffa1..97f29775500c 100644 --- a/packages/docusaurus-bundler/src/compiler.ts +++ b/packages/docusaurus-bundler/src/compiler.ts @@ -18,7 +18,10 @@ export function formatStatsErrorMessage( // TODO formatWebpackMessages does not print stack-traces // Also the error causal chain is lost here // We log the stacktrace inside serverEntry.tsx for now (not ideal) - const {errors} = formatWebpackMessages(statsJson); + const {errors} = formatWebpackMessages({ + ...statsJson, + errors: statsJson.errors.map(withErrorSource), + }); return errors .map((str) => logger.red(str)) .join(`\n\n${logger.yellow('--------------------------')}\n\n`); @@ -26,6 +29,15 @@ export function formatStatsErrorMessage( return undefined; } +function withErrorSource(error: webpack.StatsError): webpack.StatsError { + const source = error.moduleName ?? error.file; + if (!source) { + return error; + } + const sourceLocation = error.loc ? `${source}:${error.loc}` : source; + return {...error, message: `${sourceLocation}\n${error.message}`}; +} + export function printStatsWarnings( statsJson: ReturnType | undefined, ): void { From 1778308ff6bf82097974448acc21536380fa444d Mon Sep 17 00:00:00 2001 From: haranishi <223786623+haranishi@users.noreply.github.com> Date: Wed, 11 Nov 2026 21:45:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(bundler):=20=E4=BE=9D=E5=AD=98=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AE=E8=A8=BA=E6=96=AD?= =?UTF-8?q?=E8=A9=B3=E7=B4=B0=E3=82=92=E4=BF=9D=E6=8C=81=20(AI-assisted)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/__tests__/compiler.test.ts | 68 +++++++++++++++++++ packages/docusaurus-bundler/src/compiler.ts | 14 +--- .../src/legacy/formatWebpackMessages.js | 16 ++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/packages/docusaurus-bundler/src/__tests__/compiler.test.ts b/packages/docusaurus-bundler/src/__tests__/compiler.test.ts index 7ed80cb05236..72936ed26f6a 100644 --- a/packages/docusaurus-bundler/src/__tests__/compiler.test.ts +++ b/packages/docusaurus-bundler/src/__tests__/compiler.test.ts @@ -79,6 +79,45 @@ describe('formatStatsErrorMessage', () => { expect(result).not.toContain('Another error'); }); + it('preserves module-resolution details when adding the source', () => { + const result = formatStatsErrorMessage({ + errors: [ + { + moduleName: './src/index.js', + loc: '1:0-24', + message: [ + "Module not found: Error: Can't resolve 'buffer'", + 'BREAKING CHANGE: webpack < 5 included polyfills by default.', + 'Configure resolve.fallback to provide a polyfill.', + ].join('\n'), + }, + ], + warnings: [], + }); + expect(result).toContain('./src/index.js:1:0-24'); + expect(result).toContain("Can't resolve 'buffer'"); + expect(result).toContain('BREAKING CHANGE:'); + expect(result).toContain('Configure resolve.fallback'); + }); + + it('preserves Sass installation guidance when adding the source', () => { + const result = formatStatsErrorMessage({ + errors: [ + { + moduleName: './src/styles.scss', + message: + "./src/styles.scss\nCannot find module 'sass'\nRequire stack:", + }, + ], + warnings: [], + }); + expect(result).toContain('./src/styles.scss'); + expect(result).toContain( + 'To import Sass files, you first need to install sass.', + ); + expect(result).toContain('npm install sass'); + }); + it('returns undefined when there are no errors', () => { expect(formatStatsErrorMessage(undefined)).toBeUndefined(); expect(formatStatsErrorMessage({errors: [], warnings: []})).toBeUndefined(); @@ -88,6 +127,35 @@ describe('formatStatsErrorMessage', () => { describe.each(['webpack', 'rspack'] as const)( '%s error diagnostics', (name) => { + it('preserves real module-resolution diagnostics', async () => { + await using dir = await fs.mkdtempDisposable( + path.join(await fs.realpath(tmpdir()), 'docusaurus-compiler-'), + ); + await fs.mkdir(path.join(dir.path, 'src')); + await fs.writeFile( + path.join(dir.path, 'src/index.js'), + "import 'buffer';", + ); + const currentBundler = await getCurrentBundler({ + siteConfig: {webpack: name === 'webpack' ? {} : undefined}, + }); + const compilation = compile({ + currentBundler, + configs: [ + { + mode: 'development', + context: dir.path, + entry: './src/index.js', + output: {path: path.join(dir.path, 'build')}, + }, + ], + }); + await expect(compilation).rejects.toThrow('./src/index.js'); + await expect(compilation).rejects.toThrow( + name === 'webpack' ? 'resolve.fallback' : "Can't resolve 'buffer'", + ); + }); + it('identifies the source of a real module parse failure', async () => { await using dir = await fs.mkdtempDisposable( path.join(await fs.realpath(tmpdir()), 'docusaurus-compiler-'), diff --git a/packages/docusaurus-bundler/src/compiler.ts b/packages/docusaurus-bundler/src/compiler.ts index 97f29775500c..7faaea10ffa1 100644 --- a/packages/docusaurus-bundler/src/compiler.ts +++ b/packages/docusaurus-bundler/src/compiler.ts @@ -18,10 +18,7 @@ export function formatStatsErrorMessage( // TODO formatWebpackMessages does not print stack-traces // Also the error causal chain is lost here // We log the stacktrace inside serverEntry.tsx for now (not ideal) - const {errors} = formatWebpackMessages({ - ...statsJson, - errors: statsJson.errors.map(withErrorSource), - }); + const {errors} = formatWebpackMessages(statsJson); return errors .map((str) => logger.red(str)) .join(`\n\n${logger.yellow('--------------------------')}\n\n`); @@ -29,15 +26,6 @@ export function formatStatsErrorMessage( return undefined; } -function withErrorSource(error: webpack.StatsError): webpack.StatsError { - const source = error.moduleName ?? error.file; - if (!source) { - return error; - } - const sourceLocation = error.loc ? `${source}:${error.loc}` : source; - return {...error, message: `${sourceLocation}\n${error.message}`}; -} - export function printStatsWarnings( statsJson: ReturnType | undefined, ): void { diff --git a/packages/docusaurus-bundler/src/legacy/formatWebpackMessages.js b/packages/docusaurus-bundler/src/legacy/formatWebpackMessages.js index 8e66cb8aab24..ec87f8f9a58a 100644 --- a/packages/docusaurus-bundler/src/legacy/formatWebpackMessages.js +++ b/packages/docusaurus-bundler/src/legacy/formatWebpackMessages.js @@ -122,12 +122,26 @@ function formatMessage(message) { return message.trim(); } +/** + * Adds source metadata after cleanup, which relies on diagnostic line positions. + * @param {import("webpack").StatsError} error + */ +function formatError(error) { + const message = formatMessage(error); + const source = error.moduleName ?? error.file; + if (!source) { + return message; + } + const sourceLocation = error.loc ? `${source}:${error.loc}` : source; + return `${sourceLocation}\n${message}`; +} + /** * @param {import("webpack").Stats.ToJsonOutput} json. * @returns {{ errors: string[], warnings: string[] }} */ module.exports = function formatWebpackMessages(json) { - const formattedErrors = json.errors.map(formatMessage); + const formattedErrors = json.errors.map(formatError); const formattedWarnings = json.warnings.map(formatMessage); const result = {errors: formattedErrors, warnings: formattedWarnings}; if (result.errors.some(isLikelyASyntaxError)) {