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

refactor: remove react-dev-utils (CRA) dependency, internalize code #10956

Merged
merged 22 commits into from
Feb 28, 2025
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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
"@types/node": "^18.16.19",
"@types/prompts": "^2.4.4",
"@types/react": "^18.2.15",
"@types/react-dev-utils": "^9.0.11",
"@types/react-test-renderer": "^18.0.0",
"@types/semver": "^7.5.0",
"@types/shelljs": "^0.8.12",
Expand Down
1 change: 0 additions & 1 deletion packages/docusaurus-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"postcss": "^8.4.26",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^10.1.0",
"react-dev-utils": "^12.0.1",
"terser-webpack-plugin": "^5.3.9",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-bundler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import {type Configuration} from 'webpack';
import logger from '@docusaurus/logger';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import formatWebpackMessages from './legacy/formatWebpackMessages';
import type webpack from 'webpack';
import type {CurrentBundler} from '@docusaurus/types';

Expand Down
138 changes: 138 additions & 0 deletions packages/docusaurus-bundler/src/legacy/formatWebpackMessages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* 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.
*/

// TODO Legacy CRA react-dev-utils package code
// This code was in CRA/react-dev-utils (deprecated in 2025)
// We just copied the code as-is to remove a fat/useless dependency subtree
// See https://github.com/facebook/docusaurus/pull/10956
// See https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/formatWebpackMessages.js

/* eslint-disable */

const friendlySyntaxErrorLabel = 'Syntax error:';

function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}

// Cleans up webpack error messages.
function formatMessage(message) {
let lines = [];

if (typeof message === 'string') {
lines = message.split('\n');
} else if ('message' in message) {
lines = message['message'].split('\n');
} else if (Array.isArray(message)) {
message.forEach((message) => {
if ('message' in message) {
lines = message['message'].split('\n');
}
});
}

// Strip webpack-added headers off errors/warnings
// https://github.com/webpack/webpack/blob/master/lib/ModuleError.js
lines = lines.filter((line) => !/Module [A-z ]+\(from/.test(line));

// Transform parsing error into syntax error
// TODO: move this to our ESLint formatter?
lines = lines.map((line) => {
const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(
line,
);
if (!parsingError) {
return line;
}
const [, errorLine, errorColumn, errorMessage] = parsingError;
return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`;
});

message = lines.join('\n');
// Smoosh syntax errors (commonly found in CSS)
message = message.replace(
/SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g,
`${friendlySyntaxErrorLabel} $3 ($1:$2)\n`,
);
// Clean up export errors
message = message.replace(
/^.*export '(.+?)' was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$2'.`,
);
message = message.replace(
/^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$2' does not contain a default export (imported as '$1').`,
);
message = message.replace(
/^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm,
`Attempted import error: '$1' is not exported from '$3' (imported as '$2').`,
);
lines = message.split('\n');

// Remove leading newline
if (lines.length > 2 && lines[1].trim() === '') {
lines.splice(1, 1);
}
// Clean up file name
lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1');

// Cleans up verbose "module not found" messages for files and packages.
if (lines[1] && lines[1].indexOf('Module not found: ') === 0) {
lines = [
lines[0],
lines[1]
.replace('Error: ', '')
.replace('Module not found: Cannot find file:', 'Cannot find file:'),
];
}

// Add helpful message for users trying to use Sass for the first time
if (lines[1] && lines[1].match(/Cannot find module.+sass/)) {
lines[1] = 'To import Sass files, you first need to install sass.\n';
lines[1] +=
'Run `npm install sass` or `yarn add sass` inside your workspace.';
}

message = lines.join('\n');
// Internal stacks are generally useless so we strip them... with the
// exception of stacks containing `webpack:` because they're normally
// from user code generated by webpack. For more information see
// https://github.com/facebook/create-react-app/pull/1050
message = message.replace(
/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,
'',
); // at ... ...:x:y
message = message.replace(/^\s*at\s<anonymous>(\n|$)/gm, ''); // at <anonymous>
lines = message.split('\n');

// Remove duplicated newlines
lines = lines.filter(
(line, index, arr) =>
index === 0 ||
line.trim() !== '' ||
line.trim() !== arr[index - 1].trim(),
);

// Reassemble the message
message = lines.join('\n');
return message.trim();
}

/**
* @param {import("webpack").Stats.ToJsonOutput} json.
* @returns {{ errors: string[], warnings: string[] }}
*/
module.exports = function formatWebpackMessages(json) {
const formattedErrors = json.errors.map(formatMessage);
const formattedWarnings = json.warnings.map(formatMessage);
const result = {errors: formattedErrors, warnings: formattedWarnings};
if (result.errors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
result.errors = result.errors.filter(isLikelyASyntaxError);
}
return result;
};
2 changes: 1 addition & 1 deletion packages/docusaurus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
"html-webpack-plugin": "^5.6.0",
"leven": "^3.1.0",
"lodash": "^4.17.21",
"open": "^8.4.0",
"p-map": "^4.0.0",
"prompts": "^2.4.2",
"react-dev-utils": "^12.0.1",
"react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0",
"react-loadable": "npm:@docusaurus/react-loadable@6.0.0",
"react-loadable-ssr-addon-v5-slorber": "^1.0.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/docusaurus/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import path from 'path';
import logger from '@docusaurus/logger';
import {DEFAULT_BUILD_DIR_NAME} from '@docusaurus/utils';
import serveHandler from 'serve-handler';
import openBrowser from 'react-dev-utils/openBrowser';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import openBrowser from './utils/openBrowser/openBrowser';
import {loadSiteConfig} from '../server/config';
import {build} from './build/build';
import {getHostPort, type HostPortOptions} from '../server/getHostPort';
Expand Down Expand Up @@ -110,6 +110,6 @@ export async function serve(
server.listen(port);

if (cliOptions.open && !process.env.CI) {
openBrowser(url);
await openBrowser(url);
}
}
4 changes: 2 additions & 2 deletions packages/docusaurus/src/commands/start/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import logger from '@docusaurus/logger';
import openBrowser from 'react-dev-utils/openBrowser';
import openBrowser from '../utils/openBrowser/openBrowser';
import {setupSiteFileWatchers} from './watcher';
import {createWebpackDevServer} from './webpack';
import {createReloadableSite} from './utils';
Expand Down Expand Up @@ -59,6 +59,6 @@ export async function start(

await devServer.start();
if (cliOptions.open) {
openBrowser(reloadableSite.getOpenUrl());
await openBrowser(reloadableSite.getOpenUrl());
}
}
25 changes: 22 additions & 3 deletions packages/docusaurus/src/commands/start/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

import fs from 'fs-extra';
import url from 'url';
import _ from 'lodash';
import {prepareUrls} from 'react-dev-utils/WebpackDevServerUtils';
import {normalizeUrl} from '@docusaurus/utils';
import logger, {PerfLogger} from '@docusaurus/logger';
import {getHostPort} from '../../server/getHostPort';
Expand All @@ -21,6 +21,26 @@ import {formatPluginName} from '../../server/plugins/pluginsUtils';
import type {StartCLIOptions} from './start';
import type {LoadedPlugin, RouterType} from '@docusaurus/types';

// This code was historically in CRA/react-dev-utils (deprecated in 2025)
// We internalized it, refactored and removed useless code paths
// See https://github.com/facebook/docusaurus/pull/10956
// See https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/WebpackDevServerUtils.js
function getOpenUrlOrigin(
protocol: string,
host: string,
port: number,
): string {
const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
const prettyHost = isUnspecifiedHost ? 'localhost' : host;
const localUrlForBrowser = url.format({
protocol,
hostname: prettyHost,
port,
pathname: '/',
});
return localUrlForBrowser;
}

export type OpenUrlContext = {
host: string;
port: number;
Expand All @@ -46,9 +66,8 @@ export async function createOpenUrlContext({
}

const getOpenUrl: OpenUrlContext['getOpenUrl'] = ({baseUrl, router}) => {
const urls = prepareUrls(protocol, host, port);
return normalizeUrl([
urls.localUrlForBrowser,
getOpenUrlOrigin(protocol, host, port),
router === 'hash' ? '/#/' : '',
baseUrl,
]);
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus/src/commands/start/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import merge from 'webpack-merge';
import {formatStatsErrorMessage, printStatsWarnings} from '@docusaurus/bundler';
import logger from '@docusaurus/logger';
import WebpackDevServer from 'webpack-dev-server';
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
import evalSourceMapMiddleware from '../utils/legacy/evalSourceMapMiddleware';
import {createPollingOptions} from './watcher';
import getHttpsConfig from '../../webpack/utils/getHttpsConfig';
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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.
*/

// TODO Legacy CRA react-dev-utils package code
// This code was in CRA/react-dev-utils (deprecated in 2025)
// We just copied the code as-is to remove a fat/useless dependency subtree
// See https://github.com/facebook/docusaurus/pull/10956
// See https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/evalSourceMapMiddleware.js

/* eslint-disable */

function base64SourceMap(source) {
const base64 = Buffer.from(JSON.stringify(source.map()), 'utf8').toString(
'base64',
);
return `data:application/json;charset=utf-8;base64,${base64}`;
}

function getSourceById(server, id) {
const module = Array.from(server._stats.compilation.modules).find(
(m) => server._stats.compilation.chunkGraph.getModuleId(m) == id,
);
return module.originalSource();
}

/**
* Middleware responsible for retrieving a generated source
* Receives a webpack internal url: "webpack-internal:///<module-id>"
* Returns a generated source: "<source-text><sourceMappingURL><sourceURL>"
*
* Based on EvalSourceMapDevToolModuleTemplatePlugin.js
*
* @param {import("webpack-dev-server").default} server
* @returns {import("express").Handler}
*/
module.exports = function createEvalSourceMapMiddleware(server) {
return function handleWebpackInternalMiddleware(req, res, next) {
if (req.url.startsWith('/__get-internal-source')) {
const fileName = req.query.fileName;
const id = fileName.match(/webpack-internal:\/\/\/(.+)/)[1];
if (!id || !server._stats) {
next();
}

const source = getSourceById(server, id);
const sourceMapURL = `//# sourceMappingURL=${base64SourceMap(source)}`;
const sourceURL = `//# sourceURL=webpack-internal:///${module.id}`;
res.end(`${source.source()}\n${sourceMapURL}\n${sourceURL}`);
} else {
next();
}
};
};
Loading