diff --git a/packages/create-docusaurus/templates/classic-typescript/package.json b/packages/create-docusaurus/templates/classic-typescript/package.json
index 56bba3947c42..88f1117814f7 100644
--- a/packages/create-docusaurus/templates/classic-typescript/package.json
+++ b/packages/create-docusaurus/templates/classic-typescript/package.json
@@ -15,6 +15,7 @@
"typecheck": "tsc"
},
"dependencies": {
+ "@docusaurus/babel": "3.5.2",
"@docusaurus/core": "3.5.2",
"@docusaurus/preset-classic": "3.5.2",
"@mdx-js/react": "^3.0.0",
diff --git a/packages/create-docusaurus/templates/classic/package.json b/packages/create-docusaurus/templates/classic/package.json
index 2d2535698e05..c0dad98a022c 100644
--- a/packages/create-docusaurus/templates/classic/package.json
+++ b/packages/create-docusaurus/templates/classic/package.json
@@ -14,6 +14,7 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
+ "@docusaurus/babel": "3.5.2",
"@docusaurus/core": "3.5.2",
"@docusaurus/preset-classic": "3.5.2",
"@mdx-js/react": "^3.0.0",
diff --git a/packages/create-docusaurus/templates/shared/babel.config.js b/packages/create-docusaurus/templates/shared/babel.config.js
index e00595dae7d6..ca4e55cbf11f 100644
--- a/packages/create-docusaurus/templates/shared/babel.config.js
+++ b/packages/create-docusaurus/templates/shared/babel.config.js
@@ -1,3 +1,3 @@
module.exports = {
- presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+ presets: ['@docusaurus/babel/preset'],
};
diff --git a/packages/docusaurus-babel/.npmignore b/packages/docusaurus-babel/.npmignore
new file mode 100644
index 000000000000..03c9ae1e1b54
--- /dev/null
+++ b/packages/docusaurus-babel/.npmignore
@@ -0,0 +1,3 @@
+.tsbuildinfo*
+tsconfig*
+__tests__
diff --git a/packages/docusaurus-babel/README.md b/packages/docusaurus-babel/README.md
new file mode 100644
index 000000000000..1ee7d3f1543e
--- /dev/null
+++ b/packages/docusaurus-babel/README.md
@@ -0,0 +1,3 @@
+# `@docusaurus/babel`
+
+Docusaurus package for Babel-related utils.
diff --git a/packages/docusaurus-babel/package.json b/packages/docusaurus-babel/package.json
new file mode 100644
index 000000000000..38349bb57cdf
--- /dev/null
+++ b/packages/docusaurus-babel/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@docusaurus/babel",
+ "version": "3.5.2",
+ "description": "Docusaurus package for Babel-related utils.",
+ "main": "./lib/index.js",
+ "types": "./lib/index.d.ts",
+ "exports": {
+ "./preset": {
+ "types": "./lib/preset.d.ts",
+ "default": "./lib/preset.js"
+ },
+ ".": {
+ "types": "./lib/index.d.ts",
+ "default": "./lib/index.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc",
+ "watch": "tsc --watch"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/docusaurus.git",
+ "directory": "packages/docusaurus-babel"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.3",
+ "@babel/generator": "^7.23.3",
+ "@babel/traverse": "^7.22.8",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-transform-runtime": "^7.22.9",
+ "@babel/preset-env": "^7.22.9",
+ "@babel/preset-react": "^7.22.5",
+ "@babel/preset-typescript": "^7.22.5",
+ "@babel/runtime": "^7.22.6",
+ "@babel/runtime-corejs3": "^7.22.6",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "babel-plugin-dynamic-import-node": "^2.3.3",
+ "fs-extra": "^11.1.1",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ }
+}
diff --git a/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts b/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts
new file mode 100644
index 000000000000..f221bd235019
--- /dev/null
+++ b/packages/docusaurus-babel/src/__tests__/babelTranslationsExtractor.test.ts
@@ -0,0 +1,537 @@
+/**
+ * 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 {jest} from '@jest/globals';
+import fs from 'fs-extra';
+import tmp from 'tmp-promise';
+import {getBabelOptions} from '../utils';
+import {extractSourceCodeFileTranslations} from '../babelTranslationsExtractor';
+
+const TestBabelOptions = getBabelOptions({
+ isServer: true,
+});
+
+async function createTmpSourceCodeFile({
+ extension,
+ content,
+}: {
+ extension: string;
+ content: string;
+}) {
+ const file = await tmp.file({
+ prefix: 'jest-createTmpSourceCodeFile',
+ postfix: `.${extension}`,
+ });
+
+ await fs.writeFile(file.path, content);
+
+ return {
+ sourceCodeFilePath: file.path,
+ };
+}
+
+describe('extractSourceCodeFileTranslations', () => {
+ it('throws for bad source code', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+const default => {
+
+}
+`,
+ });
+
+ const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ await expect(
+ extractSourceCodeFileTranslations(sourceCodeFilePath, TestBabelOptions),
+ ).rejects.toThrow();
+
+ expect(errorMock).toHaveBeenCalledWith(
+ expect.stringMatching(
+ /Error while attempting to extract Docusaurus translations from source code file at/,
+ ),
+ );
+ });
+
+ it('extracts nothing from untranslated source code', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+const unrelated = 42;
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [],
+ });
+ });
+
+ it('extracts from a translate() functions calls', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import {translate} from '@docusaurus/Translate';
+
+export default function MyComponent() {
+ return (
+
+
+
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ codeId: {message: 'code message', description: 'code description'},
+ codeId1: {message: 'codeId1'},
+ },
+ warnings: [],
+ });
+ });
+
+ it('extracts from a components', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Translate from '@docusaurus/Translate';
+
+export default function MyComponent() {
+ return (
+
+
+ code message
+
+
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ codeId: {message: 'code message', description: 'code description'},
+ codeId1: {message: 'codeId1', description: 'description 2'},
+ },
+ warnings: [],
+ });
+ });
+
+ it('extracts statically evaluable content', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Translate, {translate} from '@docusaurus/Translate';
+
+const prefix = "prefix ";
+
+export default function MyComponent() {
+ return (
+
+
+
+ {prefix + "code message"}
+
+
+
+ {
+
+ prefix + \`Static template literal with unusual formatting!\`
+ }
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ 'prefix codeId comp': {
+ message: 'prefix code message',
+ description: 'prefix code description',
+ },
+ 'prefix codeId fn': {
+ message: 'prefix code message',
+ description: 'prefix code description',
+ },
+ 'prefix Static template literal with unusual formatting!': {
+ message: 'prefix Static template literal with unusual formatting!',
+ },
+ },
+ warnings: [],
+ });
+ });
+
+ it('extracts from TypeScript file', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'tsx',
+ content: `
+import {translate} from '@docusaurus/Translate';
+
+type ComponentProps = {toto: string}
+
+export default function MyComponent(props: ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ codeId: {message: 'code message', description: 'code description'},
+ 'code message 2': {
+ message: 'code message 2',
+ description: 'code description 2',
+ },
+ },
+ warnings: [],
+ });
+ });
+
+ it('does not extract from functions that is not docusaurus provided', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import translate from 'a-lib';
+
+export default function somethingElse() {
+ const a = translate('foo');
+ return bar
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [],
+ });
+ });
+
+ it('does not extract from functions that is internal', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+function translate() {
+ return 'foo'
+}
+
+export default function somethingElse() {
+ const a = translate('foo');
+ return a;
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [],
+ });
+ });
+
+ it('recognizes aliased imports', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Foo, {translate as bar} from '@docusaurus/Translate';
+
+export function MyComponent() {
+ return (
+
+
+ code message
+
+
+
+
+ );
+}
+
+export default function () {
+ return (
+
+
+
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ codeId: {
+ description: 'code description',
+ message: 'code message',
+ },
+ codeId1: {
+ message: 'codeId1',
+ },
+ },
+ warnings: [],
+ });
+ });
+
+ it('recognizes aliased imports as string literal', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import {'translate' as bar} from '@docusaurus/Translate';
+
+export default function () {
+ return (
+
+
+
+
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ codeId1: {
+ message: 'codeId1',
+ },
+ },
+ warnings: [],
+ });
+ });
+
+ it('warns about id if no children', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Translate from '@docusaurus/Translate';
+
+export default function () {
+ return (
+
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [
+ ` without children must have id prop.
+Example:
+File: ${sourceCodeFilePath} at line 6
+Full code: `,
+ ],
+ });
+ });
+
+ it('warns about dynamic id', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Translate from '@docusaurus/Translate';
+
+export default function () {
+ return (
+ foo
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {
+ foo: {
+ message: 'foo',
+ },
+ },
+ warnings: [
+ ` prop=id should be a statically evaluable object.
+Example: Message
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+File: ${sourceCodeFilePath} at line 6
+Full code: foo`,
+ ],
+ });
+ });
+
+ it('warns about dynamic children', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import Translate from '@docusaurus/Translate';
+
+export default function () {
+ return (
+ hhh
+ );
+}
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [
+ `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.
+File: ${sourceCodeFilePath} at line 6
+Full code: hhh`,
+ ],
+ });
+ });
+
+ it('warns about dynamic translate argument', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import {translate} from '@docusaurus/Translate';
+
+translate(foo);
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [
+ `translate() first arg should be a statically evaluable object.
+Example: translate({message: "text",id: "optional.id",description: "optional description"}
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+File: ${sourceCodeFilePath} at line 4
+Full code: translate(foo)`,
+ ],
+ });
+ });
+
+ it('warns about too many arguments', async () => {
+ const {sourceCodeFilePath} = await createTmpSourceCodeFile({
+ extension: 'js',
+ content: `
+import {translate} from '@docusaurus/Translate';
+
+translate({message: 'a'}, {a: 1}, 2);
+`,
+ });
+
+ const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
+ sourceCodeFilePath,
+ TestBabelOptions,
+ );
+
+ expect(sourceCodeFileTranslations).toEqual({
+ sourceCodeFilePath,
+ translations: {},
+ warnings: [
+ `translate() function only takes 1 or 2 args
+File: ${sourceCodeFilePath} at line 4
+Full code: translate({
+ message: 'a'
+}, {
+ a: 1
+}, 2)`,
+ ],
+ });
+ });
+});
diff --git a/packages/docusaurus-babel/src/babelTranslationsExtractor.ts b/packages/docusaurus-babel/src/babelTranslationsExtractor.ts
new file mode 100644
index 000000000000..744f1aaa2161
--- /dev/null
+++ b/packages/docusaurus-babel/src/babelTranslationsExtractor.ts
@@ -0,0 +1,266 @@
+/**
+ * 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 fs from 'fs-extra';
+import logger from '@docusaurus/logger';
+import traverse, {type Node} from '@babel/traverse';
+import generate from '@babel/generator';
+import {
+ parse,
+ type types as t,
+ type NodePath,
+ type TransformOptions,
+} from '@babel/core';
+import type {TranslationFileContent} from '@docusaurus/types';
+
+export type SourceCodeFileTranslations = {
+ sourceCodeFilePath: string;
+ translations: TranslationFileContent;
+ warnings: string[];
+};
+
+export async function extractAllSourceCodeFileTranslations(
+ sourceCodeFilePaths: string[],
+ babelOptions: TransformOptions,
+): Promise {
+ return Promise.all(
+ sourceCodeFilePaths.flatMap((sourceFilePath) =>
+ extractSourceCodeFileTranslations(sourceFilePath, babelOptions),
+ ),
+ );
+}
+
+export async function extractSourceCodeFileTranslations(
+ sourceCodeFilePath: string,
+ babelOptions: TransformOptions,
+): Promise {
+ try {
+ const code = await fs.readFile(sourceCodeFilePath, 'utf8');
+
+ const ast = parse(code, {
+ ...babelOptions,
+ ast: true,
+ // filename is important, because babel does not process the same files
+ // according to their js/ts extensions.
+ // See https://x.com/NicoloRibaudo/status/1321130735605002243
+ filename: sourceCodeFilePath,
+ }) as Node;
+
+ const translations = extractSourceCodeAstTranslations(
+ ast,
+ sourceCodeFilePath,
+ );
+ return translations;
+ } catch (err) {
+ logger.error`Error while attempting to extract Docusaurus translations from source code file at path=${sourceCodeFilePath}.`;
+ throw err;
+ }
+}
+
+/*
+Need help understanding this?
+
+Useful resources:
+https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
+https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-formatjs/index.ts
+https://github.com/pugjs/babel-walk
+ */
+function extractSourceCodeAstTranslations(
+ ast: Node,
+ sourceCodeFilePath: string,
+): SourceCodeFileTranslations {
+ function sourceWarningPart(node: Node) {
+ return `File: ${sourceCodeFilePath} at line ${node.loc?.start.line ?? '?'}
+Full code: ${generate(node).code}`;
+ }
+
+ const translations: TranslationFileContent = {};
+ const warnings: string[] = [];
+ let translateComponentName: string | undefined;
+ let translateFunctionName: string | undefined;
+
+ // First pass: find import declarations of Translate / translate.
+ // If not found, don't process the rest to avoid false positives
+ traverse(ast, {
+ ImportDeclaration(path) {
+ if (
+ path.node.importKind === 'type' ||
+ path.get('source').node.value !== '@docusaurus/Translate'
+ ) {
+ return;
+ }
+ const importSpecifiers = path.get('specifiers');
+ const defaultImport = importSpecifiers.find(
+ (specifier): specifier is NodePath =>
+ specifier.node.type === 'ImportDefaultSpecifier',
+ );
+ const callbackImport = importSpecifiers.find(
+ (specifier): specifier is NodePath =>
+ specifier.node.type === 'ImportSpecifier' &&
+ ((
+ (specifier as NodePath).get('imported')
+ .node as t.Identifier
+ ).name === 'translate' ||
+ (
+ (specifier as NodePath).get('imported')
+ .node as t.StringLiteral
+ ).value === 'translate'),
+ );
+
+ translateComponentName = defaultImport?.get('local').node.name;
+ translateFunctionName = callbackImport?.get('local').node.name;
+ },
+ });
+
+ traverse(ast, {
+ ...(translateComponentName && {
+ JSXElement(path) {
+ if (
+ !path
+ .get('openingElement')
+ .get('name')
+ .isJSXIdentifier({name: translateComponentName})
+ ) {
+ return;
+ }
+ function evaluateJSXProp(propName: string): string | undefined {
+ const attributePath = path
+ .get('openingElement.attributes')
+ .find(
+ (attr) =>
+ attr.isJSXAttribute() &&
+ attr.get('name').isJSXIdentifier({name: propName}),
+ );
+
+ if (attributePath) {
+ const attributeValue = attributePath.get('value') as NodePath;
+
+ const attributeValueEvaluated =
+ attributeValue.isJSXExpressionContainer()
+ ? (attributeValue.get('expression') as NodePath).evaluate()
+ : attributeValue.evaluate();
+
+ if (
+ attributeValueEvaluated.confident &&
+ typeof attributeValueEvaluated.value === 'string'
+ ) {
+ return attributeValueEvaluated.value;
+ }
+ warnings.push(
+ ` prop=${propName} should be a statically evaluable object.
+Example: Message
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+${sourceWarningPart(path.node)}`,
+ );
+ }
+
+ return undefined;
+ }
+
+ const id = evaluateJSXProp('id');
+ const description = evaluateJSXProp('description');
+ let message: string;
+ const childrenPath = path.get('children');
+
+ // Handle empty content
+ if (!childrenPath.length) {
+ if (!id) {
+ warnings.push(` without children must have id prop.
+Example:
+${sourceWarningPart(path.node)}`);
+ } else {
+ translations[id] = {
+ message: id,
+ ...(description && {description}),
+ };
+ }
+
+ return;
+ }
+
+ // Handle single non-empty content
+ const singleChildren = childrenPath
+ // Remove empty/useless text nodes that might be around our
+ // translation! Makes the translation system more reliable to JSX
+ // formatting issues
+ .filter(
+ (children) =>
+ !(
+ children.isJSXText() &&
+ children.node.value.replace('\n', '').trim() === ''
+ ),
+ )
+ .pop();
+ const isJSXText = singleChildren?.isJSXText();
+ const isJSXExpressionContainer =
+ singleChildren?.isJSXExpressionContainer() &&
+ (singleChildren.get('expression') as NodePath).evaluate().confident;
+
+ if (isJSXText || isJSXExpressionContainer) {
+ message = isJSXText
+ ? singleChildren.node.value.trim().replace(/\s+/g, ' ')
+ : String(
+ (singleChildren.get('expression') as NodePath).evaluate().value,
+ );
+
+ translations[id ?? message] = {
+ message,
+ ...(description && {description}),
+ };
+ } else {
+ warnings.push(
+ `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ },
+ }),
+
+ ...(translateFunctionName && {
+ CallExpression(path) {
+ if (!path.get('callee').isIdentifier({name: translateFunctionName})) {
+ return;
+ }
+
+ const args = path.get('arguments');
+ if (args.length === 1 || args.length === 2) {
+ const firstArgPath = args[0]!;
+
+ // translate("x" + "y"); => translate("xy");
+ const firstArgEvaluated = firstArgPath.evaluate();
+
+ if (
+ firstArgEvaluated.confident &&
+ typeof firstArgEvaluated.value === 'object'
+ ) {
+ const {message, id, description} = firstArgEvaluated.value as {
+ [propName: string]: unknown;
+ };
+ translations[String(id ?? message)] = {
+ message: String(message ?? id),
+ ...(Boolean(description) && {description: String(description)}),
+ };
+ } else {
+ warnings.push(
+ `translate() first arg should be a statically evaluable object.
+Example: translate({message: "text",id: "optional.id",description: "optional description"}
+Dynamically constructed values are not allowed, because they prevent translations to be extracted.
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ } else {
+ warnings.push(
+ `translate() function only takes 1 or 2 args
+${sourceWarningPart(path.node)}`,
+ );
+ }
+ },
+ }),
+ });
+
+ return {sourceCodeFilePath, translations, warnings};
+}
diff --git a/packages/docusaurus-babel/src/index.ts b/packages/docusaurus-babel/src/index.ts
new file mode 100644
index 000000000000..643e05734b3d
--- /dev/null
+++ b/packages/docusaurus-babel/src/index.ts
@@ -0,0 +1,10 @@
+/**
+ * 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.
+ */
+
+export {getCustomBabelConfigFilePath, getBabelOptions} from './utils';
+
+export {extractAllSourceCodeFileTranslations} from './babelTranslationsExtractor';
diff --git a/packages/docusaurus-babel/src/preset.ts b/packages/docusaurus-babel/src/preset.ts
new file mode 100644
index 000000000000..cbbeb2207423
--- /dev/null
+++ b/packages/docusaurus-babel/src/preset.ts
@@ -0,0 +1,82 @@
+/**
+ * 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 path from 'path';
+import type {ConfigAPI, TransformOptions} from '@babel/core';
+
+function getTransformOptions(isServer: boolean): TransformOptions {
+ const absoluteRuntimePath = path.dirname(
+ require.resolve(`@babel/runtime/package.json`),
+ );
+ return {
+ // All optional newlines and whitespace will be omitted when generating code
+ // in compact mode
+ compact: true,
+ presets: [
+ isServer
+ ? [
+ require.resolve('@babel/preset-env'),
+ {
+ targets: {
+ node: 'current',
+ },
+ },
+ ]
+ : [
+ require.resolve('@babel/preset-env'),
+ {
+ useBuiltIns: 'entry',
+ loose: true,
+ corejs: '3',
+ // Do not transform modules to CJS
+ modules: false,
+ // Exclude transforms that make all code slower
+ exclude: ['transform-typeof-symbol'],
+ },
+ ],
+ [
+ require.resolve('@babel/preset-react'),
+ {
+ runtime: 'automatic',
+ },
+ ],
+ require.resolve('@babel/preset-typescript'),
+ ],
+ plugins: [
+ // Polyfills the runtime needed for async/await, generators, and friends
+ // https://babeljs.io/docs/en/babel-plugin-transform-runtime
+ [
+ require.resolve('@babel/plugin-transform-runtime'),
+ {
+ corejs: false,
+ helpers: true,
+ // By default, it assumes @babel/runtime@7.0.0. Since we use >7.0.0,
+ // better to explicitly specify the version so that it can reuse the
+ // helper better. See https://github.com/babel/babel/issues/10261
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
+ version: (require('@babel/runtime/package.json') as {version: string})
+ .version,
+ regenerator: true,
+ useESModules: true,
+ // Undocumented option that lets us encapsulate our runtime, ensuring
+ // the correct version is used
+ // https://github.com/babel/babel/blob/090c364a90fe73d36a30707fc612ce037bdbbb24/packages/babel-plugin-transform-runtime/src/index.js#L35-L42
+ absoluteRuntime: absoluteRuntimePath,
+ },
+ ],
+ // Adds syntax support for import()
+ isServer
+ ? require.resolve('babel-plugin-dynamic-import-node')
+ : require.resolve('@babel/plugin-syntax-dynamic-import'),
+ ],
+ };
+}
+
+export default function babelPresets(api: ConfigAPI): TransformOptions {
+ const callerName = api.caller((caller) => caller?.name);
+ return getTransformOptions(callerName === 'server');
+}
diff --git a/packages/docusaurus-babel/src/utils.ts b/packages/docusaurus-babel/src/utils.ts
new file mode 100644
index 000000000000..cb058081c6c1
--- /dev/null
+++ b/packages/docusaurus-babel/src/utils.ts
@@ -0,0 +1,50 @@
+/**
+ * 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 fs from 'fs-extra';
+import path from 'path';
+import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils';
+import type {TransformOptions} from '@babel/core';
+
+export async function getCustomBabelConfigFilePath(
+ siteDir: string,
+): Promise {
+ const customBabelConfigurationPath = path.join(
+ siteDir,
+ BABEL_CONFIG_FILE_NAME,
+ );
+ return (await fs.pathExists(customBabelConfigurationPath))
+ ? customBabelConfigurationPath
+ : undefined;
+}
+
+export function getBabelOptions({
+ isServer,
+ babelOptions,
+}: {
+ isServer?: boolean;
+ // TODO Docusaurus v4 fix this
+ // weird to have getBabelOptions take a babelOptions param
+ babelOptions?: TransformOptions | string;
+} = {}): TransformOptions {
+ const caller = {name: isServer ? 'server' : 'client'};
+ if (typeof babelOptions === 'string') {
+ return {
+ babelrc: false,
+ configFile: babelOptions,
+ caller,
+ };
+ }
+ return {
+ ...(babelOptions ?? {
+ presets: [require.resolve('@docusaurus/babel/preset')],
+ }),
+ babelrc: false,
+ configFile: false,
+ caller,
+ };
+}
diff --git a/packages/docusaurus-babel/tsconfig.json b/packages/docusaurus-babel/tsconfig.json
new file mode 100644
index 000000000000..74731e2257e1
--- /dev/null
+++ b/packages/docusaurus-babel/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "sourceMap": true,
+ "declarationMap": true
+ },
+ "include": ["src"],
+ "exclude": ["**/__tests__/**"]
+}
diff --git a/packages/docusaurus-bundler/.npmignore b/packages/docusaurus-bundler/.npmignore
new file mode 100644
index 000000000000..03c9ae1e1b54
--- /dev/null
+++ b/packages/docusaurus-bundler/.npmignore
@@ -0,0 +1,3 @@
+.tsbuildinfo*
+tsconfig*
+__tests__
diff --git a/packages/docusaurus-bundler/README.md b/packages/docusaurus-bundler/README.md
new file mode 100644
index 000000000000..c63782702b61
--- /dev/null
+++ b/packages/docusaurus-bundler/README.md
@@ -0,0 +1,3 @@
+# `@docusaurus/bundler`
+
+Docusaurus util package to abstract the current bundler.
diff --git a/packages/docusaurus-bundler/package.json b/packages/docusaurus-bundler/package.json
new file mode 100644
index 000000000000..2c6b2235096b
--- /dev/null
+++ b/packages/docusaurus-bundler/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@docusaurus/bundler",
+ "version": "3.5.2",
+ "description": "Docusaurus util package to abstract the current bundler.",
+ "main": "./lib/index.js",
+ "types": "./lib/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "watch": "tsc --watch"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/facebook/docusaurus.git",
+ "directory": "packages/docusaurus-bundler"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.3",
+ "@docusaurus/babel": "3.5.2",
+ "@docusaurus/cssnano-preset": "3.5.2",
+ "@docusaurus/faster": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "autoprefixer": "^10.4.14",
+ "babel-loader": "^9.1.3",
+ "clean-css": "^5.3.2",
+ "copy-webpack-plugin": "^11.0.0",
+ "css-loader": "^6.8.1",
+ "css-minimizer-webpack-plugin": "^5.0.1",
+ "cssnano": "^6.1.2",
+ "postcss": "^8.4.26",
+ "postcss-loader": "^7.3.3",
+ "file-loader": "^6.2.0",
+ "mini-css-extract-plugin": "^2.9.1",
+ "null-loader": "^4.0.1",
+ "react-dev-utils": "^12.0.1",
+ "terser-webpack-plugin": "^5.3.9",
+ "tslib": "^2.6.0",
+ "url-loader": "^4.1.1",
+ "webpack": "^5.88.1",
+ "webpackbar": "^6.0.1"
+ },
+ "devDependencies": {
+ "@total-typescript/shoehorn": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=18.0"
+ }
+}
diff --git a/packages/docusaurus-bundler/src/compiler.ts b/packages/docusaurus-bundler/src/compiler.ts
new file mode 100644
index 000000000000..384d704e48fb
--- /dev/null
+++ b/packages/docusaurus-bundler/src/compiler.ts
@@ -0,0 +1,87 @@
+/**
+ * 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 {type Configuration} from 'webpack';
+import logger from '@docusaurus/logger';
+import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
+import type webpack from 'webpack';
+import type {CurrentBundler} from '@docusaurus/types';
+
+export function formatStatsErrorMessage(
+ statsJson: ReturnType | undefined,
+): string | undefined {
+ if (statsJson?.errors?.length) {
+ // 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);
+ return errors
+ .map((str) => logger.red(str))
+ .join(`\n\n${logger.yellow('--------------------------')}\n\n`);
+ }
+ return undefined;
+}
+
+export function printStatsWarnings(
+ statsJson: ReturnType | undefined,
+): void {
+ if (statsJson?.warnings?.length) {
+ statsJson.warnings?.forEach((warning) => {
+ logger.warn(warning);
+ });
+ }
+}
+
+declare global {
+ interface Error {
+ /** @see https://webpack.js.org/api/node/#error-handling */
+ details?: unknown;
+ }
+}
+
+export function compile({
+ configs,
+ currentBundler,
+}: {
+ configs: Configuration[];
+ currentBundler: CurrentBundler;
+}): Promise {
+ return new Promise((resolve, reject) => {
+ const compiler = currentBundler.instance(configs);
+ compiler.run((err, stats) => {
+ if (err) {
+ logger.error(err.stack ?? err);
+ if (err.details) {
+ logger.error(err.details);
+ }
+ reject(err);
+ }
+ // Let plugins consume all the stats
+ const errorsWarnings = stats?.toJson('errors-warnings');
+ if (stats?.hasErrors()) {
+ const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
+ reject(
+ new Error(
+ `Failed to compile due to Webpack errors.\n${statsErrorMessage}`,
+ ),
+ );
+ }
+ printStatsWarnings(errorsWarnings);
+
+ // Webpack 5 requires calling close() so that persistent caching works
+ // See https://github.com/webpack/webpack.js.org/pull/4775
+ compiler.close((errClose) => {
+ if (errClose) {
+ logger.error(`Error while closing Webpack compiler: ${errClose}`);
+ reject(errClose);
+ } else {
+ resolve(stats!);
+ }
+ });
+ });
+ });
+}
diff --git a/packages/docusaurus/src/webpack/currentBundler.ts b/packages/docusaurus-bundler/src/currentBundler.ts
similarity index 70%
rename from packages/docusaurus/src/webpack/currentBundler.ts
rename to packages/docusaurus-bundler/src/currentBundler.ts
index d0089aeb05a0..2005dea60e67 100644
--- a/packages/docusaurus/src/webpack/currentBundler.ts
+++ b/packages/docusaurus-bundler/src/currentBundler.ts
@@ -6,6 +6,7 @@
*/
import webpack from 'webpack';
+import WebpackBar from 'webpackbar';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import logger from '@docusaurus/logger';
@@ -64,3 +65,25 @@ export async function getCopyPlugin({
// https://github.com/webpack-contrib/copy-webpack-plugin
return CopyWebpackPlugin;
}
+
+export async function getProgressBarPlugin({
+ currentBundler,
+}: {
+ currentBundler: CurrentBundler;
+}): Promise {
+ if (currentBundler.name === 'rspack') {
+ class CustomRspackProgressPlugin extends currentBundler.instance
+ .ProgressPlugin {
+ constructor({name}: {name: string}) {
+ // TODO add support for color
+ // Unfortunately the rspack.ProgressPlugin does not have a name option
+ // See https://rspack.dev/plugins/webpack/progress-plugin
+ // @ts-expect-error: adapt Rspack ProgressPlugin constructor
+ super({prefix: name});
+ }
+ }
+ return CustomRspackProgressPlugin as typeof WebpackBar;
+ }
+
+ return WebpackBar;
+}
diff --git a/packages/docusaurus/src/faster.ts b/packages/docusaurus-bundler/src/importFaster.ts
similarity index 100%
rename from packages/docusaurus/src/faster.ts
rename to packages/docusaurus-bundler/src/importFaster.ts
diff --git a/packages/docusaurus-bundler/src/index.ts b/packages/docusaurus-bundler/src/index.ts
new file mode 100644
index 000000000000..457cdd9d2516
--- /dev/null
+++ b/packages/docusaurus-bundler/src/index.ts
@@ -0,0 +1,19 @@
+/**
+ * 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.
+ */
+
+export {printStatsWarnings, formatStatsErrorMessage, compile} from './compiler';
+
+export {
+ getCurrentBundler,
+ getCSSExtractPlugin,
+ getCopyPlugin,
+ getProgressBarPlugin,
+} from './currentBundler';
+
+export {getMinimizers} from './minification';
+export {createJsLoaderFactory} from './loaders/jsLoader';
+export {createStyleLoadersFactory} from './loaders/styleLoader';
diff --git a/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts b/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts
new file mode 100644
index 000000000000..2ac34d425f40
--- /dev/null
+++ b/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts
@@ -0,0 +1,77 @@
+/**
+ * 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 {fromPartial} from '@total-typescript/shoehorn';
+import {createJsLoaderFactory} from '../jsLoader';
+
+import type {RuleSetRule} from 'webpack';
+
+describe('createJsLoaderFactory', () => {
+ function testJsLoaderFactory(
+ siteConfig?: Parameters[0]['siteConfig'],
+ ) {
+ return createJsLoaderFactory({
+ siteConfig: {
+ ...siteConfig,
+ webpack: {
+ jsLoader: 'babel',
+ ...siteConfig?.webpack,
+ },
+ future: fromPartial(siteConfig?.future),
+ },
+ });
+ }
+
+ it('createJsLoaderFactory defaults to babel loader', async () => {
+ const createJsLoader = await testJsLoaderFactory();
+ expect(createJsLoader({isServer: true}).loader).toBe(
+ require.resolve('babel-loader'),
+ );
+ expect(createJsLoader({isServer: false}).loader).toBe(
+ require.resolve('babel-loader'),
+ );
+ });
+
+ it('createJsLoaderFactory accepts loaders with preset', async () => {
+ const createJsLoader = await testJsLoaderFactory({
+ webpack: {jsLoader: 'babel'},
+ });
+
+ expect(
+ createJsLoader({
+ isServer: true,
+ }).loader,
+ ).toBe(require.resolve('babel-loader'));
+ expect(
+ createJsLoader({
+ isServer: false,
+ }).loader,
+ ).toBe(require.resolve('babel-loader'));
+ });
+
+ it('createJsLoaderFactory allows customization', async () => {
+ const customJSLoader = (isServer: boolean): RuleSetRule => ({
+ loader: 'my-fast-js-loader',
+ options: String(isServer),
+ });
+
+ const createJsLoader = await testJsLoaderFactory({
+ webpack: {jsLoader: customJSLoader},
+ });
+
+ expect(
+ createJsLoader({
+ isServer: true,
+ }),
+ ).toEqual(customJSLoader(true));
+ expect(
+ createJsLoader({
+ isServer: false,
+ }),
+ ).toEqual(customJSLoader(false));
+ });
+});
diff --git a/packages/docusaurus-bundler/src/loaders/jsLoader.ts b/packages/docusaurus-bundler/src/loaders/jsLoader.ts
new file mode 100644
index 000000000000..64c2c9ced976
--- /dev/null
+++ b/packages/docusaurus-bundler/src/loaders/jsLoader.ts
@@ -0,0 +1,54 @@
+/**
+ * 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 {getBabelOptions} from '@docusaurus/babel';
+import {importSwcJsLoaderFactory} from '../importFaster';
+import type {ConfigureWebpackUtils, DocusaurusConfig} from '@docusaurus/types';
+
+const BabelJsLoaderFactory: ConfigureWebpackUtils['getJSLoader'] = ({
+ isServer,
+ babelOptions,
+}) => {
+ return {
+ loader: require.resolve('babel-loader'),
+ options: getBabelOptions({isServer, babelOptions}),
+ };
+};
+
+// Confusing: function that creates a function that creates actual js loaders
+// This is done on purpose because the js loader factory is a public API
+// It is injected in configureWebpack plugin lifecycle for plugin authors
+export async function createJsLoaderFactory({
+ siteConfig,
+}: {
+ siteConfig: {
+ webpack?: DocusaurusConfig['webpack'];
+ future?: {
+ experimental_faster: DocusaurusConfig['future']['experimental_faster'];
+ };
+ };
+}): Promise {
+ const jsLoader = siteConfig.webpack?.jsLoader ?? 'babel';
+ if (
+ jsLoader instanceof Function &&
+ siteConfig.future?.experimental_faster.swcJsLoader
+ ) {
+ throw new Error(
+ "You can't use a custom webpack.jsLoader and experimental_faster.swcJsLoader at the same time",
+ );
+ }
+ if (jsLoader instanceof Function) {
+ return ({isServer}) => jsLoader(isServer);
+ }
+ if (siteConfig.future?.experimental_faster.swcJsLoader) {
+ return importSwcJsLoaderFactory();
+ }
+ if (jsLoader === 'babel') {
+ return BabelJsLoaderFactory;
+ }
+ throw new Error(`Docusaurus bug: unexpected jsLoader value${jsLoader}`);
+}
diff --git a/packages/docusaurus-bundler/src/loaders/styleLoader.ts b/packages/docusaurus-bundler/src/loaders/styleLoader.ts
new file mode 100644
index 000000000000..3aea678d7714
--- /dev/null
+++ b/packages/docusaurus-bundler/src/loaders/styleLoader.ts
@@ -0,0 +1,80 @@
+/**
+ * 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 {getCSSExtractPlugin} from '../currentBundler';
+import type {ConfigureWebpackUtils, CurrentBundler} from '@docusaurus/types';
+
+export async function createStyleLoadersFactory({
+ currentBundler,
+}: {
+ currentBundler: CurrentBundler;
+}): Promise {
+ const CssExtractPlugin = await getCSSExtractPlugin({currentBundler});
+
+ return function getStyleLoaders(
+ isServer: boolean,
+ cssOptionsArg: {
+ [key: string]: unknown;
+ } = {},
+ ) {
+ const cssOptions: {[key: string]: unknown} = {
+ // TODO turn esModule on later, see https://github.com/facebook/docusaurus/pull/6424
+ esModule: false,
+ ...cssOptionsArg,
+ };
+
+ // On the server we don't really need to extract/emit CSS
+ // We only need to transform CSS module imports to a styles object
+ if (isServer) {
+ return cssOptions.modules
+ ? [
+ {
+ loader: require.resolve('css-loader'),
+ options: cssOptions,
+ },
+ ]
+ : // Ignore regular CSS files
+ [{loader: require.resolve('null-loader')}];
+ }
+
+ return [
+ {
+ loader: CssExtractPlugin.loader,
+ options: {
+ esModule: true,
+ },
+ },
+ {
+ loader: require.resolve('css-loader'),
+ options: cssOptions,
+ },
+
+ // TODO apart for configurePostCss(), do we really need this loader?
+ // Note: using postcss here looks inefficient/duplicate
+ // But in practice, it's not a big deal because css-loader also uses postcss
+ // and is able to reuse the parsed AST from postcss-loader
+ // See https://github.com/webpack-contrib/css-loader/blob/master/src/index.js#L159
+ {
+ // Options for PostCSS as we reference these options twice
+ // Adds vendor prefixing based on your specified browser support in
+ // package.json
+ loader: require.resolve('postcss-loader'),
+ options: {
+ postcssOptions: {
+ // Necessary for external CSS imports to work
+ // https://github.com/facebook/create-react-app/issues/2677
+ ident: 'postcss',
+ plugins: [
+ // eslint-disable-next-line global-require
+ require('autoprefixer'),
+ ],
+ },
+ },
+ },
+ ];
+ };
+}
diff --git a/packages/docusaurus/src/webpack/minification.ts b/packages/docusaurus-bundler/src/minification.ts
similarity index 84%
rename from packages/docusaurus/src/webpack/minification.ts
rename to packages/docusaurus-bundler/src/minification.ts
index d3ec2a1951a9..49305b7c6d4d 100644
--- a/packages/docusaurus/src/webpack/minification.ts
+++ b/packages/docusaurus-bundler/src/minification.ts
@@ -7,13 +7,14 @@
import TerserPlugin from 'terser-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
-import {importSwcJsMinifierOptions} from '../faster';
+import {importSwcJsMinifierOptions} from './importFaster';
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
import type {WebpackPluginInstance} from 'webpack';
-import type {FasterConfig} from '@docusaurus/types';
+import type {CurrentBundler, FasterConfig} from '@docusaurus/types';
export type MinimizersConfig = {
faster: Pick;
+ currentBundler: CurrentBundler;
};
// See https://github.com/webpack-contrib/terser-webpack-plugin#parallel
@@ -111,8 +112,23 @@ function getCssMinimizer(): WebpackPluginInstance {
}
}
-export async function getMinimizers(
+async function getWebpackMinimizers(
params: MinimizersConfig,
): Promise {
return Promise.all([getJsMinimizer(params), getCssMinimizer()]);
}
+
+async function getRspackMinimizers({
+ currentBundler,
+}: MinimizersConfig): Promise {
+ console.log('currentBundler', currentBundler.name);
+ throw new Error('TODO Rspack minimizers not implemented yet');
+}
+
+export async function getMinimizers(
+ params: MinimizersConfig,
+): Promise {
+ return params.currentBundler.name === 'rspack'
+ ? getRspackMinimizers(params)
+ : getWebpackMinimizers(params);
+}
diff --git a/packages/docusaurus-bundler/tsconfig.json b/packages/docusaurus-bundler/tsconfig.json
new file mode 100644
index 000000000000..74731e2257e1
--- /dev/null
+++ b/packages/docusaurus-bundler/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "noEmit": false,
+ "sourceMap": true,
+ "declarationMap": true
+ },
+ "include": ["src"],
+ "exclude": ["**/__tests__/**"]
+}
diff --git a/packages/docusaurus-mdx-loader/README.md b/packages/docusaurus-mdx-loader/README.md
index b3a60699a6b5..51895ab92740 100644
--- a/packages/docusaurus-mdx-loader/README.md
+++ b/packages/docusaurus-mdx-loader/README.md
@@ -18,7 +18,6 @@ module: {
{
test: /\.mdx?$/,
use: [
- 'babel-loader',
{
loader: '@docusaurus/mdx-loader',
options: {
diff --git a/packages/docusaurus-plugin-pwa/package.json b/packages/docusaurus-plugin-pwa/package.json
index 39f2c9296ef9..84c8853a8b4a 100644
--- a/packages/docusaurus-plugin-pwa/package.json
+++ b/packages/docusaurus-plugin-pwa/package.json
@@ -22,6 +22,7 @@
"dependencies": {
"@babel/core": "^7.23.3",
"@babel/preset-env": "^7.23.3",
+ "@docusaurus/bundler": "3.5.2",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
@@ -32,11 +33,9 @@
"babel-loader": "^9.1.3",
"clsx": "^2.0.0",
"core-js": "^3.31.1",
- "terser-webpack-plugin": "^5.3.9",
"tslib": "^2.6.0",
"webpack": "^5.88.1",
"webpack-merge": "^5.9.0",
- "webpackbar": "^5.0.2",
"workbox-build": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-window": "^7.0.0"
diff --git a/packages/docusaurus-plugin-pwa/src/deps.d.ts b/packages/docusaurus-plugin-pwa/src/deps.d.ts
deleted file mode 100644
index 104a45712162..000000000000
--- a/packages/docusaurus-plugin-pwa/src/deps.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * 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 incompatible declaration file: https://github.com/unjs/webpackbar/pull/108
-declare module 'webpackbar' {
- import webpack from 'webpack';
-
- export default class WebpackBarPlugin extends webpack.ProgressPlugin {
- constructor(options: {name: string; color?: string});
- }
-}
diff --git a/packages/docusaurus-plugin-pwa/src/index.ts b/packages/docusaurus-plugin-pwa/src/index.ts
index 0fcc59f0a184..85dbebc861d4 100644
--- a/packages/docusaurus-plugin-pwa/src/index.ts
+++ b/packages/docusaurus-plugin-pwa/src/index.ts
@@ -6,13 +6,15 @@
*/
import path from 'path';
-import webpack, {type Configuration} from 'webpack';
-import WebpackBar from 'webpackbar';
-import Terser from 'terser-webpack-plugin';
+import {type Configuration} from 'webpack';
+import {
+ compile,
+ getProgressBarPlugin,
+ getMinimizers,
+} from '@docusaurus/bundler';
import {injectManifest} from 'workbox-build';
import {normalizeUrl} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
-import {compile} from '@docusaurus/core/lib/webpack/utils';
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-pwa';
@@ -89,10 +91,10 @@ export default function pluginPWA(
});
},
- configureWebpack(config) {
+ configureWebpack(config, isServer, {currentBundler}) {
return {
plugins: [
- new webpack.EnvironmentPlugin(
+ new currentBundler.instance.EnvironmentPlugin(
// See https://github.com/facebook/docusaurus/pull/10455#issuecomment-2317593528
// See https://github.com/webpack/webpack/commit/adf2a6b7c6077fd806ea0e378c1450cccecc9ed0#r145989788
// @ts-expect-error: bad Webpack type?
@@ -139,6 +141,10 @@ export default function pluginPWA(
async postBuild(props) {
const swSourceFileTest = /\.m?js$/;
+ const ProgressBarPlugin = await getProgressBarPlugin({
+ currentBundler: props.currentBundler,
+ });
+
const swWebpackConfig: Configuration = {
entry: require.resolve('./sw.js'),
output: {
@@ -155,18 +161,17 @@ export default function pluginPWA(
// See https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
minimizer: debug
? []
- : [
- new Terser({
- test: swSourceFileTest,
- }),
- ],
+ : await getMinimizers({
+ faster: props.siteConfig.future.experimental_faster,
+ currentBundler: props.currentBundler,
+ }),
},
plugins: [
- new webpack.EnvironmentPlugin({
+ new props.currentBundler.instance.EnvironmentPlugin({
// Fallback value required with Webpack 5
PWA_SW_CUSTOM: swCustom ?? '',
}),
- new WebpackBar({
+ new ProgressBarPlugin({
name: 'Service Worker',
color: 'red',
}),
@@ -182,7 +187,10 @@ export default function pluginPWA(
},
};
- await compile([swWebpackConfig]);
+ await compile({
+ configs: [swWebpackConfig],
+ currentBundler: props.currentBundler,
+ });
const swDest = path.resolve(props.outDir, 'sw.js');
diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts
index 9ffb4e3e87db..568fd070c6de 100644
--- a/packages/docusaurus-theme-classic/src/index.ts
+++ b/packages/docusaurus-theme-classic/src/index.ts
@@ -6,7 +6,6 @@
*/
import path from 'path';
-import {createRequire} from 'module';
import rtlcss from 'rtlcss';
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
import {getTranslationFiles, translateThemeConfig} from './translations';
@@ -19,14 +18,6 @@ import type {LoadContext, Plugin} from '@docusaurus/types';
import type {ThemeConfig} from '@docusaurus/theme-common';
import type {Plugin as PostCssPlugin} from 'postcss';
import type {PluginOptions} from '@docusaurus/theme-classic';
-import type webpack from 'webpack';
-
-const requireFromDocusaurusCore = createRequire(
- require.resolve('@docusaurus/core/package.json'),
-);
-const ContextReplacementPlugin = requireFromDocusaurusCore(
- 'webpack/lib/ContextReplacementPlugin',
-) as typeof webpack.ContextReplacementPlugin;
function getInfimaCSSFile(direction: string) {
return `infima/dist/css/default/default${
@@ -89,7 +80,7 @@ export default function themeClassic(
return modules;
},
- configureWebpack() {
+ configureWebpack(__config, __isServer, {currentBundler}) {
const prismLanguages = additionalLanguages
.map((lang) => `prism-${lang}`)
.join('|');
@@ -99,7 +90,7 @@ export default function themeClassic(
// This allows better optimization by only bundling those components
// that the user actually needs, because the modules are dynamically
// required and can't be known during compile time.
- new ContextReplacementPlugin(
+ new currentBundler.instance.ContextReplacementPlugin(
/prismjs[\\/]components$/,
new RegExp(`^./(${prismLanguages})$`),
),
diff --git a/packages/docusaurus-theme-translations/package.json b/packages/docusaurus-theme-translations/package.json
index 9ccec80c4e73..07a82ee4be11 100644
--- a/packages/docusaurus-theme-translations/package.json
+++ b/packages/docusaurus-theme-translations/package.json
@@ -23,8 +23,10 @@
"tslib": "^2.6.0"
},
"devDependencies": {
+ "@docusaurus/babel": "3.5.2",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
"lodash": "^4.17.21"
},
"engines": {
diff --git a/packages/docusaurus-theme-translations/src/utils.ts b/packages/docusaurus-theme-translations/src/utils.ts
index 95cf7f4ec712..481d2116df62 100644
--- a/packages/docusaurus-theme-translations/src/utils.ts
+++ b/packages/docusaurus-theme-translations/src/utils.ts
@@ -13,11 +13,8 @@
import path from 'path';
import fs from 'fs-extra';
-// Unsafe import, should we create a package for the translationsExtractor ?;
-import {
- globSourceCodeFilePaths,
- extractAllSourceCodeFileTranslations,
-} from '@docusaurus/core/lib/server/translations/translationsExtractor';
+import {globTranslatableSourceFiles} from '@docusaurus/utils';
+import {extractAllSourceCodeFileTranslations} from '@docusaurus/babel';
import type {TranslationFileContent} from '@docusaurus/types';
async function getPackageCodePath(packageName: string) {
@@ -62,14 +59,14 @@ export async function extractThemeCodeMessages(
// eslint-disable-next-line no-param-reassign
targetDirs ??= (await getThemes()).flatMap((theme) => theme.src);
- const filePaths = (await globSourceCodeFilePaths(targetDirs)).filter(
+ const filePaths = (await globTranslatableSourceFiles(targetDirs)).filter(
(filePath) => ['.js', '.jsx'].includes(path.extname(filePath)),
);
const filesExtractedTranslations = await extractAllSourceCodeFileTranslations(
filePaths,
{
- presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+ presets: ['@docusaurus/babel/preset'],
},
);
diff --git a/packages/docusaurus-types/src/context.d.ts b/packages/docusaurus-types/src/context.d.ts
index e399c5342191..98c08a4656ae 100644
--- a/packages/docusaurus-types/src/context.d.ts
+++ b/packages/docusaurus-types/src/context.d.ts
@@ -8,6 +8,7 @@ import type {DocusaurusConfig} from './config';
import type {CodeTranslations, I18n} from './i18n';
import type {LoadedPlugin, PluginVersionInformation} from './plugin';
import type {PluginRouteConfig} from './routing';
+import type {CurrentBundler} from './bundler';
export type DocusaurusContext = {
siteConfig: DocusaurusConfig;
@@ -74,6 +75,11 @@ export type LoadContext = {
* Defines the default browser storage behavior for a site
*/
siteStorage: SiteStorage;
+
+ /**
+ * The bundler used to build the site (Webpack or Rspack)
+ */
+ currentBundler: CurrentBundler;
};
export type Props = LoadContext & {
diff --git a/packages/docusaurus-utils/src/globUtils.ts b/packages/docusaurus-utils/src/globUtils.ts
index 72b65d75d7f9..c7e2cab13421 100644
--- a/packages/docusaurus-utils/src/globUtils.ts
+++ b/packages/docusaurus-utils/src/globUtils.ts
@@ -10,8 +10,11 @@
import path from 'path';
import Micromatch from 'micromatch'; // Note: Micromatch is used by Globby
import {addSuffix} from '@docusaurus/utils-common';
+import Globby from 'globby';
+import {posixPath} from './pathUtils';
+
/** A re-export of the globby instance. */
-export {default as Globby} from 'globby';
+export {Globby};
/**
* The default glob patterns we ignore when sourcing content.
@@ -85,3 +88,40 @@ export function createAbsoluteFilePathMatcher(
return (absoluteFilePath: string) =>
matcher(getRelativeFilePath(absoluteFilePath));
}
+
+// Globby that fix Windows path patterns
+// See https://github.com/facebook/docusaurus/pull/4222#issuecomment-795517329
+export async function safeGlobby(
+ patterns: string[],
+ options?: Globby.GlobbyOptions,
+): Promise {
+ // Required for Windows support, as paths using \ should not be used by globby
+ // (also using the windows hard drive prefix like c: is not a good idea)
+ const globPaths = patterns.map((dirPath) =>
+ posixPath(path.relative(process.cwd(), dirPath)),
+ );
+
+ return Globby(globPaths, options);
+}
+
+// A bit weird to put this here, but it's used by core + theme-translations
+export async function globTranslatableSourceFiles(
+ patterns: string[],
+): Promise {
+ // We only support extracting source code translations from these kind of files
+ const extensionsAllowed = new Set([
+ '.js',
+ '.jsx',
+ '.ts',
+ '.tsx',
+ // TODO support md/mdx too? (may be overkill)
+ // need to compile the MDX to JSX first and remove front matter
+ // '.md',
+ // '.mdx',
+ ]);
+
+ const filePaths = await safeGlobby(patterns);
+ return filePaths.filter((filePath) =>
+ extensionsAllowed.has(path.extname(filePath)),
+ );
+}
diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts
index d9a24a408655..5a35bb672df2 100644
--- a/packages/docusaurus-utils/src/index.ts
+++ b/packages/docusaurus-utils/src/index.ts
@@ -97,6 +97,8 @@ export {md5Hash, simpleHash, docuHash} from './hashUtils';
export {
Globby,
GlobExcludeDefault,
+ safeGlobby,
+ globTranslatableSourceFiles,
createMatcher,
createAbsoluteFilePathMatcher,
} from './globUtils';
diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json
index 72bdb1e9376b..45236bcc2c3c 100644
--- a/packages/docusaurus/package.json
+++ b/packages/docusaurus/package.json
@@ -33,54 +33,32 @@
"url": "https://github.com/facebook/docusaurus/issues"
},
"dependencies": {
- "@babel/core": "^7.23.3",
- "@babel/generator": "^7.23.3",
- "@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/plugin-transform-runtime": "^7.22.9",
- "@babel/preset-env": "^7.22.9",
- "@babel/preset-react": "^7.22.5",
- "@babel/preset-typescript": "^7.22.5",
- "@babel/runtime": "^7.22.6",
- "@babel/runtime-corejs3": "^7.22.6",
- "@babel/traverse": "^7.22.8",
- "@docusaurus/cssnano-preset": "3.5.2",
+ "@docusaurus/babel": "3.5.2",
+ "@docusaurus/bundler": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
- "autoprefixer": "^10.4.14",
- "babel-loader": "^9.1.3",
- "babel-plugin-dynamic-import-node": "^2.3.3",
"boxen": "^6.2.1",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
- "clean-css": "^5.3.2",
"cli-table3": "^0.6.3",
"combine-promises": "^1.1.0",
"commander": "^5.1.0",
- "copy-webpack-plugin": "^11.0.0",
"core-js": "^3.31.1",
- "css-loader": "^6.8.1",
- "css-minimizer-webpack-plugin": "^5.0.1",
- "cssnano": "^6.1.2",
"del": "^6.1.1",
"detect-port": "^1.5.1",
"escape-html": "^1.0.3",
"eta": "^2.2.0",
"eval": "^0.1.8",
- "file-loader": "^6.2.0",
"fs-extra": "^11.1.1",
"html-minifier-terser": "^7.2.0",
"html-tags": "^3.3.1",
"html-webpack-plugin": "^5.5.3",
"leven": "^3.1.0",
"lodash": "^4.17.21",
- "mini-css-extract-plugin": "^2.7.6",
- "null-loader": "^4.0.1",
"p-map": "^4.0.0",
- "postcss": "^8.4.26",
- "postcss-loader": "^7.3.3",
"prompts": "^2.4.2",
"react-dev-utils": "^12.0.1",
"react-helmet-async": "^1.3.0",
@@ -93,15 +71,12 @@
"semver": "^7.5.4",
"serve-handler": "npm:@docusaurus/serve-handler@6.2.0",
"shelljs": "^0.8.5",
- "terser-webpack-plugin": "^5.3.10",
"tslib": "^2.6.0",
"update-notifier": "^6.0.2",
- "url-loader": "^4.1.1",
"webpack": "^5.88.1",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-dev-server": "^4.15.1",
- "webpack-merge": "^5.9.0",
- "webpackbar": "^5.0.2"
+ "webpack-merge": "^5.9.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.5.2",
diff --git a/packages/docusaurus/src/babel/preset.ts b/packages/docusaurus/src/babel/preset.ts
index cbbeb2207423..eb74e4085a84 100644
--- a/packages/docusaurus/src/babel/preset.ts
+++ b/packages/docusaurus/src/babel/preset.ts
@@ -5,78 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
-import path from 'path';
-import type {ConfigAPI, TransformOptions} from '@babel/core';
+// TODO Docusaurus v4, do breaking change and expose babel preset cleanly
+/*
+this just ensure retro-compatibility with our former init template .babelrc.js:
-function getTransformOptions(isServer: boolean): TransformOptions {
- const absoluteRuntimePath = path.dirname(
- require.resolve(`@babel/runtime/package.json`),
- );
- return {
- // All optional newlines and whitespace will be omitted when generating code
- // in compact mode
- compact: true,
- presets: [
- isServer
- ? [
- require.resolve('@babel/preset-env'),
- {
- targets: {
- node: 'current',
- },
- },
- ]
- : [
- require.resolve('@babel/preset-env'),
- {
- useBuiltIns: 'entry',
- loose: true,
- corejs: '3',
- // Do not transform modules to CJS
- modules: false,
- // Exclude transforms that make all code slower
- exclude: ['transform-typeof-symbol'],
- },
- ],
- [
- require.resolve('@babel/preset-react'),
- {
- runtime: 'automatic',
- },
- ],
- require.resolve('@babel/preset-typescript'),
- ],
- plugins: [
- // Polyfills the runtime needed for async/await, generators, and friends
- // https://babeljs.io/docs/en/babel-plugin-transform-runtime
- [
- require.resolve('@babel/plugin-transform-runtime'),
- {
- corejs: false,
- helpers: true,
- // By default, it assumes @babel/runtime@7.0.0. Since we use >7.0.0,
- // better to explicitly specify the version so that it can reuse the
- // helper better. See https://github.com/babel/babel/issues/10261
- // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
- version: (require('@babel/runtime/package.json') as {version: string})
- .version,
- regenerator: true,
- useESModules: true,
- // Undocumented option that lets us encapsulate our runtime, ensuring
- // the correct version is used
- // https://github.com/babel/babel/blob/090c364a90fe73d36a30707fc612ce037bdbbb24/packages/babel-plugin-transform-runtime/src/index.js#L35-L42
- absoluteRuntime: absoluteRuntimePath,
- },
- ],
- // Adds syntax support for import()
- isServer
- ? require.resolve('babel-plugin-dynamic-import-node')
- : require.resolve('@babel/plugin-syntax-dynamic-import'),
- ],
- };
-}
+module.exports = {
+ presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+};
+ */
+import BabelPreset from '@docusaurus/babel/preset';
-export default function babelPresets(api: ConfigAPI): TransformOptions {
- const callerName = api.caller((caller) => caller?.name);
- return getTransformOptions(callerName === 'server');
-}
+export default BabelPreset;
diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts
index f06eba8c0c0a..6cc6e73b751f 100644
--- a/packages/docusaurus/src/commands/build.ts
+++ b/packages/docusaurus/src/commands/build.ts
@@ -8,6 +8,7 @@
import fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
+import {compile} from '@docusaurus/bundler';
import logger, {PerfLogger} from '@docusaurus/logger';
import {DOCUSAURUS_VERSION, mapAsyncSequential} from '@docusaurus/utils';
import {loadSite, loadContext, type LoadContextParams} from '../server/site';
@@ -18,7 +19,6 @@ import {
createConfigureWebpackUtils,
executePluginsConfigureWebpack,
} from '../webpack/configure';
-import {compile} from '../webpack/utils';
import {loadI18n} from '../server/i18n';
import {
@@ -174,7 +174,7 @@ async function buildLocale({
// We can build the 2 configs in parallel
const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] =
- await PerfLogger.async('Creating webpack configs', () =>
+ await PerfLogger.async('Creating bundler configs', () =>
Promise.all([
getBuildClientConfig({
props,
@@ -189,13 +189,17 @@ async function buildLocale({
);
// Run webpack to build JS bundle (client) and static html files (server).
- await PerfLogger.async('Bundling with Webpack', () => {
- if (router === 'hash') {
- return compile([clientConfig]);
- } else {
- return compile([clientConfig, serverConfig]);
- }
- });
+ await PerfLogger.async(
+ `Bundling with ${configureWebpackUtils.currentBundler.name}`,
+ () => {
+ return compile({
+ configs:
+ // For hash router we don't do SSG and can skip the server bundle
+ router === 'hash' ? [clientConfig] : [clientConfig, serverConfig],
+ currentBundler: configureWebpackUtils.currentBundler,
+ });
+ },
+ );
const {collectedData} = await PerfLogger.async('SSG', () =>
executeSSG({
diff --git a/packages/docusaurus/src/commands/start/webpack.ts b/packages/docusaurus/src/commands/start/webpack.ts
index b2ddd0853c15..9c462cd7d974 100644
--- a/packages/docusaurus/src/commands/start/webpack.ts
+++ b/packages/docusaurus/src/commands/start/webpack.ts
@@ -8,15 +8,12 @@
import path from 'path';
import merge from 'webpack-merge';
import webpack from 'webpack';
+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 {createPollingOptions} from './watcher';
-import {
- formatStatsErrorMessage,
- getHttpsConfig,
- printStatsWarnings,
-} from '../../webpack/utils';
+import getHttpsConfig from '../../webpack/utils/getHttpsConfig';
import {
createConfigureWebpackUtils,
executePluginsConfigureWebpack,
diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts
index 909e3136b55f..3963270dc582 100644
--- a/packages/docusaurus/src/commands/writeHeadingIds.ts
+++ b/packages/docusaurus/src/commands/writeHeadingIds.ts
@@ -8,12 +8,12 @@
import fs from 'fs-extra';
import logger from '@docusaurus/logger';
import {
+ safeGlobby,
writeMarkdownHeadingId,
type WriteHeadingIDOptions,
} from '@docusaurus/utils';
import {loadContext} from '../server/site';
import {initPlugins} from '../server/plugins/init';
-import {safeGlobby} from '../server/utils';
async function transformMarkdownFile(
filepath: string,
diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts
index 9460622c7bef..ab577806faa3 100644
--- a/packages/docusaurus/src/commands/writeTranslations.ts
+++ b/packages/docusaurus/src/commands/writeTranslations.ts
@@ -7,6 +7,7 @@
import fs from 'fs-extra';
import path from 'path';
+import {globTranslatableSourceFiles} from '@docusaurus/utils';
import {loadContext, type LoadContextParams} from '../server/site';
import {initPlugins} from '../server/plugins/init';
import {
@@ -16,11 +17,7 @@ import {
loadPluginsDefaultCodeTranslationMessages,
applyDefaultCodeTranslations,
} from '../server/translations/translations';
-import {
- extractSiteSourceCodeTranslations,
- globSourceCodeFilePaths,
-} from '../server/translations/translationsExtractor';
-import {getCustomBabelConfigFilePath, getBabelOptions} from '../webpack/utils';
+import {extractSiteSourceCodeTranslations} from '../server/translations/translationsExtractor';
import type {InitializedPlugin} from '@docusaurus/types';
export type WriteTranslationsCLIOptions = Pick<
@@ -49,7 +46,7 @@ async function getExtraSourceCodeFilePaths(): Promise {
if (!themeCommonLibDir) {
return []; // User may not use a Docusaurus official theme? Quite unlikely...
}
- return globSourceCodeFilePaths([themeCommonLibDir]);
+ return globTranslatableSourceFiles([themeCommonLibDir]);
}
async function writePluginTranslationFiles({
@@ -103,16 +100,11 @@ Available locales are: ${context.i18n.locales.join(',')}.`,
);
}
- const babelOptions = getBabelOptions({
- isServer: true,
- babelOptions: await getCustomBabelConfigFilePath(siteDir),
- });
- const extractedCodeTranslations = await extractSiteSourceCodeTranslations(
+ const extractedCodeTranslations = await extractSiteSourceCodeTranslations({
siteDir,
plugins,
- babelOptions,
- await getExtraSourceCodeFilePaths(),
- );
+ extraSourceCodeFilePaths: await getExtraSourceCodeFilePaths(),
+ });
const defaultCodeMessages = await loadPluginsDefaultCodeTranslationMessages(
plugins,
diff --git a/packages/docusaurus/src/deps.d.ts b/packages/docusaurus/src/deps.d.ts
index 4cf9a6df98b9..e14603a3de88 100644
--- a/packages/docusaurus/src/deps.d.ts
+++ b/packages/docusaurus/src/deps.d.ts
@@ -38,15 +38,6 @@ declare module 'webpack/lib/HotModuleReplacementPlugin' {
export default HotModuleReplacementPlugin;
}
-// TODO incompatible declaration file: https://github.com/unjs/webpackbar/pull/108
-declare module 'webpackbar' {
- import webpack from 'webpack';
-
- export default class WebpackBarPlugin extends webpack.ProgressPlugin {
- constructor(options: {name: string; color?: string});
- }
-}
-
// TODO incompatible declaration file
declare module 'eta' {
export const defaultConfig: object;
diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap
index 06fa00daab7d..2c61404bd204 100644
--- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap
+++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap
@@ -4,6 +4,10 @@ exports[`load loads props for site with custom i18n path 1`] = `
{
"baseUrl": "/",
"codeTranslations": {},
+ "currentBundler": {
+ "instance": [Function],
+ "name": "webpack",
+ },
"generatedFilesDir": "/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/.docusaurus",
"headTags": "",
"i18n": {
diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts
index 5e6cb99856d2..fdb5610a6d3b 100644
--- a/packages/docusaurus/src/server/site.ts
+++ b/packages/docusaurus/src/server/site.ts
@@ -13,6 +13,7 @@ import {
} from '@docusaurus/utils';
import {PerfLogger} from '@docusaurus/logger';
import combinePromises from 'combine-promises';
+import {getCurrentBundler} from '@docusaurus/bundler';
import {loadSiteConfig} from './config';
import {getAllClientModules} from './clientModules';
import {loadPlugins, reloadPlugin} from './plugins/plugins';
@@ -88,6 +89,10 @@ export async function loadContext(
}),
});
+ const currentBundler = await getCurrentBundler({
+ siteConfig: initialSiteConfig,
+ });
+
const i18n = await loadI18n(initialSiteConfig, {locale});
const baseUrl = localizePath({
@@ -126,6 +131,7 @@ export async function loadContext(
baseUrl,
i18n,
codeTranslations,
+ currentBundler,
};
}
@@ -145,6 +151,7 @@ function createSiteProps(
i18n,
localizationDir,
codeTranslations: siteCodeTranslations,
+ currentBundler,
} = context;
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags({
@@ -181,6 +188,7 @@ function createSiteProps(
preBodyTags,
postBodyTags,
codeTranslations,
+ currentBundler,
};
}
diff --git a/packages/docusaurus/src/server/translations/__tests__/translationsExtractor.test.ts b/packages/docusaurus/src/server/translations/__tests__/translationsExtractor.test.ts
index 09ea7786323a..69c9ec46831f 100644
--- a/packages/docusaurus/src/server/translations/__tests__/translationsExtractor.test.ts
+++ b/packages/docusaurus/src/server/translations/__tests__/translationsExtractor.test.ts
@@ -10,17 +10,9 @@ import path from 'path';
import fs from 'fs-extra';
import tmp from 'tmp-promise';
import {SRC_DIR_NAME} from '@docusaurus/utils';
-import {
- extractSourceCodeFileTranslations,
- extractSiteSourceCodeTranslations,
-} from '../translationsExtractor';
-import {getBabelOptions} from '../../../webpack/utils';
+import {extractSiteSourceCodeTranslations} from '../translationsExtractor';
import type {InitializedPlugin, LoadedPlugin} from '@docusaurus/types';
-const TestBabelOptions = getBabelOptions({
- isServer: true,
-});
-
async function createTmpDir() {
const {path: siteDirPath} = await tmp.dir({
prefix: 'jest-createTmpSiteDir',
@@ -28,527 +20,6 @@ async function createTmpDir() {
return siteDirPath;
}
-async function createTmpSourceCodeFile({
- extension,
- content,
-}: {
- extension: string;
- content: string;
-}) {
- const file = await tmp.file({
- prefix: 'jest-createTmpSourceCodeFile',
- postfix: `.${extension}`,
- });
-
- await fs.writeFile(file.path, content);
-
- return {
- sourceCodeFilePath: file.path,
- };
-}
-
-describe('extractSourceCodeFileTranslations', () => {
- it('throws for bad source code', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-const default => {
-
-}
-`,
- });
-
- const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(
- extractSourceCodeFileTranslations(sourceCodeFilePath, TestBabelOptions),
- ).rejects.toThrow();
-
- expect(errorMock).toHaveBeenCalledWith(
- expect.stringMatching(
- /Error while attempting to extract Docusaurus translations from source code file at/,
- ),
- );
- });
-
- it('extracts nothing from untranslated source code', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-const unrelated = 42;
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [],
- });
- });
-
- it('extracts from a translate() functions calls', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import {translate} from '@docusaurus/Translate';
-
-export default function MyComponent() {
- return (
-
-
-
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- codeId: {message: 'code message', description: 'code description'},
- codeId1: {message: 'codeId1'},
- },
- warnings: [],
- });
- });
-
- it('extracts from a components', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Translate from '@docusaurus/Translate';
-
-export default function MyComponent() {
- return (
-
-
- code message
-
-
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- codeId: {message: 'code message', description: 'code description'},
- codeId1: {message: 'codeId1', description: 'description 2'},
- },
- warnings: [],
- });
- });
-
- it('extracts statically evaluable content', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Translate, {translate} from '@docusaurus/Translate';
-
-const prefix = "prefix ";
-
-export default function MyComponent() {
- return (
-
-
-
- {prefix + "code message"}
-
-
-
- {
-
- prefix + \`Static template literal with unusual formatting!\`
- }
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- 'prefix codeId comp': {
- message: 'prefix code message',
- description: 'prefix code description',
- },
- 'prefix codeId fn': {
- message: 'prefix code message',
- description: 'prefix code description',
- },
- 'prefix Static template literal with unusual formatting!': {
- message: 'prefix Static template literal with unusual formatting!',
- },
- },
- warnings: [],
- });
- });
-
- it('extracts from TypeScript file', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'tsx',
- content: `
-import {translate} from '@docusaurus/Translate';
-
-type ComponentProps = {toto: string}
-
-export default function MyComponent(props: ComponentProps) {
- return (
-
-
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- codeId: {message: 'code message', description: 'code description'},
- 'code message 2': {
- message: 'code message 2',
- description: 'code description 2',
- },
- },
- warnings: [],
- });
- });
-
- it('does not extract from functions that is not docusaurus provided', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import translate from 'a-lib';
-
-export default function somethingElse() {
- const a = translate('foo');
- return bar
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [],
- });
- });
-
- it('does not extract from functions that is internal', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-function translate() {
- return 'foo'
-}
-
-export default function somethingElse() {
- const a = translate('foo');
- return a;
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [],
- });
- });
-
- it('recognizes aliased imports', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Foo, {translate as bar} from '@docusaurus/Translate';
-
-export function MyComponent() {
- return (
-
-
- code message
-
-
-
-
- );
-}
-
-export default function () {
- return (
-
-
-
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- codeId: {
- description: 'code description',
- message: 'code message',
- },
- codeId1: {
- message: 'codeId1',
- },
- },
- warnings: [],
- });
- });
-
- it('recognizes aliased imports as string literal', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import {'translate' as bar} from '@docusaurus/Translate';
-
-export default function () {
- return (
-
-
-
-
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- codeId1: {
- message: 'codeId1',
- },
- },
- warnings: [],
- });
- });
-
- it('warns about id if no children', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Translate from '@docusaurus/Translate';
-
-export default function () {
- return (
-
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [
- ` without children must have id prop.
-Example:
-File: ${sourceCodeFilePath} at line 6
-Full code: `,
- ],
- });
- });
-
- it('warns about dynamic id', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Translate from '@docusaurus/Translate';
-
-export default function () {
- return (
- foo
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {
- foo: {
- message: 'foo',
- },
- },
- warnings: [
- ` prop=id should be a statically evaluable object.
-Example: Message
-Dynamically constructed values are not allowed, because they prevent translations to be extracted.
-File: ${sourceCodeFilePath} at line 6
-Full code: foo`,
- ],
- });
- });
-
- it('warns about dynamic children', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import Translate from '@docusaurus/Translate';
-
-export default function () {
- return (
- hhh
- );
-}
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [
- `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.
-File: ${sourceCodeFilePath} at line 6
-Full code: hhh`,
- ],
- });
- });
-
- it('warns about dynamic translate argument', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import {translate} from '@docusaurus/Translate';
-
-translate(foo);
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [
- `translate() first arg should be a statically evaluable object.
-Example: translate({message: "text",id: "optional.id",description: "optional description"}
-Dynamically constructed values are not allowed, because they prevent translations to be extracted.
-File: ${sourceCodeFilePath} at line 4
-Full code: translate(foo)`,
- ],
- });
- });
-
- it('warns about too many arguments', async () => {
- const {sourceCodeFilePath} = await createTmpSourceCodeFile({
- extension: 'js',
- content: `
-import {translate} from '@docusaurus/Translate';
-
-translate({message: 'a'}, {a: 1}, 2);
-`,
- });
-
- const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
- sourceCodeFilePath,
- TestBabelOptions,
- );
-
- expect(sourceCodeFileTranslations).toEqual({
- sourceCodeFilePath,
- translations: {},
- warnings: [
- `translate() function only takes 1 or 2 args
-File: ${sourceCodeFilePath} at line 4
-Full code: translate({
- message: 'a'
-}, {
- a: 1
-}, 2)`,
- ],
- });
- });
-});
-
describe('extractSiteSourceCodeTranslations', () => {
it('extracts translation from all plugins source code', async () => {
const siteDir = await createTmpDir();
@@ -694,11 +165,10 @@ export default function MyComponent(props: Props) {
plugin2,
{name: 'dummy', options: {}, version: {type: 'synthetic'}} as const,
] as LoadedPlugin[];
- const translations = await extractSiteSourceCodeTranslations(
+ const translations = await extractSiteSourceCodeTranslations({
siteDir,
plugins,
- TestBabelOptions,
- );
+ });
expect(translations).toEqual({
siteComponentFileId1: {
description: 'site component 1 desc',
diff --git a/packages/docusaurus/src/server/translations/translationsExtractor.ts b/packages/docusaurus/src/server/translations/translationsExtractor.ts
index fc7767b384c4..f3a778286e13 100644
--- a/packages/docusaurus/src/server/translations/translationsExtractor.ts
+++ b/packages/docusaurus/src/server/translations/translationsExtractor.ts
@@ -6,38 +6,18 @@
*/
import nodePath from 'path';
-import fs from 'fs-extra';
import logger from '@docusaurus/logger';
-import traverse, {type Node} from '@babel/traverse';
-import generate from '@babel/generator';
+import {globTranslatableSourceFiles, SRC_DIR_NAME} from '@docusaurus/utils';
import {
- parse,
- type types as t,
- type NodePath,
- type TransformOptions,
-} from '@babel/core';
-import {SRC_DIR_NAME} from '@docusaurus/utils';
-import {safeGlobby} from '../utils';
+ getBabelOptions,
+ getCustomBabelConfigFilePath,
+ extractAllSourceCodeFileTranslations,
+} from '@docusaurus/babel';
import type {
InitializedPlugin,
TranslationFileContent,
} from '@docusaurus/types';
-// We only support extracting source code translations from these kind of files
-const TranslatableSourceCodeExtension = new Set([
- '.js',
- '.jsx',
- '.ts',
- '.tsx',
- // TODO support md/mdx too? (may be overkill)
- // need to compile the MDX to JSX first and remove front matter
- // '.md',
- // '.mdx',
-]);
-function isTranslatableSourceCodePath(filePath: string): boolean {
- return TranslatableSourceCodeExtension.has(nodePath.extname(filePath));
-}
-
function getSiteSourceCodeFilePaths(siteDir: string): string[] {
return [nodePath.join(siteDir, SRC_DIR_NAME)];
}
@@ -58,13 +38,6 @@ function getPluginSourceCodeFilePaths(plugin: InitializedPlugin): string[] {
return codePaths.map((p) => nodePath.resolve(plugin.path, p));
}
-export async function globSourceCodeFilePaths(
- dirPaths: string[],
-): Promise {
- const filePaths = await safeGlobby(dirPaths);
- return filePaths.filter(isTranslatableSourceCodePath);
-}
-
async function getSourceCodeFilePaths(
siteDir: string,
plugins: InitializedPlugin[],
@@ -79,15 +52,23 @@ async function getSourceCodeFilePaths(
const allPaths = [...sitePaths, ...pluginsPaths];
- return globSourceCodeFilePaths(allPaths);
+ return globTranslatableSourceFiles(allPaths);
}
-export async function extractSiteSourceCodeTranslations(
- siteDir: string,
- plugins: InitializedPlugin[],
- babelOptions: TransformOptions,
- extraSourceCodeFilePaths: string[] = [],
-): Promise {
+export async function extractSiteSourceCodeTranslations({
+ siteDir,
+ plugins,
+ extraSourceCodeFilePaths = [],
+}: {
+ siteDir: string;
+ plugins: InitializedPlugin[];
+ extraSourceCodeFilePaths?: string[];
+}): Promise {
+ const babelOptions = getBabelOptions({
+ isServer: true,
+ babelOptions: await getCustomBabelConfigFilePath(siteDir),
+ });
+
// Should we warn here if the same translation "key" is found in multiple
// source code files?
function toTranslationFileContent(
@@ -132,245 +113,3 @@ type SourceCodeFileTranslations = {
translations: TranslationFileContent;
warnings: string[];
};
-
-export async function extractAllSourceCodeFileTranslations(
- sourceCodeFilePaths: string[],
- babelOptions: TransformOptions,
-): Promise {
- return Promise.all(
- sourceCodeFilePaths.flatMap((sourceFilePath) =>
- extractSourceCodeFileTranslations(sourceFilePath, babelOptions),
- ),
- );
-}
-
-export async function extractSourceCodeFileTranslations(
- sourceCodeFilePath: string,
- babelOptions: TransformOptions,
-): Promise {
- try {
- const code = await fs.readFile(sourceCodeFilePath, 'utf8');
-
- const ast = parse(code, {
- ...babelOptions,
- ast: true,
- // filename is important, because babel does not process the same files
- // according to their js/ts extensions.
- // See https://x.com/NicoloRibaudo/status/1321130735605002243
- filename: sourceCodeFilePath,
- }) as Node;
-
- const translations = extractSourceCodeAstTranslations(
- ast,
- sourceCodeFilePath,
- );
- return translations;
- } catch (err) {
- logger.error`Error while attempting to extract Docusaurus translations from source code file at path=${sourceCodeFilePath}.`;
- throw err;
- }
-}
-
-/*
-Need help understanding this?
-
-Useful resources:
-https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md
-https://github.com/formatjs/formatjs/blob/main/packages/babel-plugin-formatjs/index.ts
-https://github.com/pugjs/babel-walk
- */
-function extractSourceCodeAstTranslations(
- ast: Node,
- sourceCodeFilePath: string,
-): SourceCodeFileTranslations {
- function sourceWarningPart(node: Node) {
- return `File: ${sourceCodeFilePath} at line ${node.loc?.start.line ?? '?'}
-Full code: ${generate(node).code}`;
- }
-
- const translations: TranslationFileContent = {};
- const warnings: string[] = [];
- let translateComponentName: string | undefined;
- let translateFunctionName: string | undefined;
-
- // First pass: find import declarations of Translate / translate.
- // If not found, don't process the rest to avoid false positives
- traverse(ast, {
- ImportDeclaration(path) {
- if (
- path.node.importKind === 'type' ||
- path.get('source').node.value !== '@docusaurus/Translate'
- ) {
- return;
- }
- const importSpecifiers = path.get('specifiers');
- const defaultImport = importSpecifiers.find(
- (specifier): specifier is NodePath =>
- specifier.node.type === 'ImportDefaultSpecifier',
- );
- const callbackImport = importSpecifiers.find(
- (specifier): specifier is NodePath =>
- specifier.node.type === 'ImportSpecifier' &&
- ((
- (specifier as NodePath).get('imported')
- .node as t.Identifier
- ).name === 'translate' ||
- (
- (specifier as NodePath).get('imported')
- .node as t.StringLiteral
- ).value === 'translate'),
- );
-
- translateComponentName = defaultImport?.get('local').node.name;
- translateFunctionName = callbackImport?.get('local').node.name;
- },
- });
-
- traverse(ast, {
- ...(translateComponentName && {
- JSXElement(path) {
- if (
- !path
- .get('openingElement')
- .get('name')
- .isJSXIdentifier({name: translateComponentName})
- ) {
- return;
- }
- function evaluateJSXProp(propName: string): string | undefined {
- const attributePath = path
- .get('openingElement.attributes')
- .find(
- (attr) =>
- attr.isJSXAttribute() &&
- attr.get('name').isJSXIdentifier({name: propName}),
- );
-
- if (attributePath) {
- const attributeValue = attributePath.get('value') as NodePath;
-
- const attributeValueEvaluated =
- attributeValue.isJSXExpressionContainer()
- ? (attributeValue.get('expression') as NodePath).evaluate()
- : attributeValue.evaluate();
-
- if (
- attributeValueEvaluated.confident &&
- typeof attributeValueEvaluated.value === 'string'
- ) {
- return attributeValueEvaluated.value;
- }
- warnings.push(
- ` prop=${propName} should be a statically evaluable object.
-Example: Message
-Dynamically constructed values are not allowed, because they prevent translations to be extracted.
-${sourceWarningPart(path.node)}`,
- );
- }
-
- return undefined;
- }
-
- const id = evaluateJSXProp('id');
- const description = evaluateJSXProp('description');
- let message: string;
- const childrenPath = path.get('children');
-
- // Handle empty content
- if (!childrenPath.length) {
- if (!id) {
- warnings.push(` without children must have id prop.
-Example:
-${sourceWarningPart(path.node)}`);
- } else {
- translations[id] = {
- message: id,
- ...(description && {description}),
- };
- }
-
- return;
- }
-
- // Handle single non-empty content
- const singleChildren = childrenPath
- // Remove empty/useless text nodes that might be around our
- // translation! Makes the translation system more reliable to JSX
- // formatting issues
- .filter(
- (children) =>
- !(
- children.isJSXText() &&
- children.node.value.replace('\n', '').trim() === ''
- ),
- )
- .pop();
- const isJSXText = singleChildren?.isJSXText();
- const isJSXExpressionContainer =
- singleChildren?.isJSXExpressionContainer() &&
- (singleChildren.get('expression') as NodePath).evaluate().confident;
-
- if (isJSXText || isJSXExpressionContainer) {
- message = isJSXText
- ? singleChildren.node.value.trim().replace(/\s+/g, ' ')
- : String(
- (singleChildren.get('expression') as NodePath).evaluate().value,
- );
-
- translations[id ?? message] = {
- message,
- ...(description && {description}),
- };
- } else {
- warnings.push(
- `Translate content could not be extracted. It has to be a static string and use optional but static props, like text.
-${sourceWarningPart(path.node)}`,
- );
- }
- },
- }),
-
- ...(translateFunctionName && {
- CallExpression(path) {
- if (!path.get('callee').isIdentifier({name: translateFunctionName})) {
- return;
- }
-
- const args = path.get('arguments');
- if (args.length === 1 || args.length === 2) {
- const firstArgPath = args[0]!;
-
- // translate("x" + "y"); => translate("xy");
- const firstArgEvaluated = firstArgPath.evaluate();
-
- if (
- firstArgEvaluated.confident &&
- typeof firstArgEvaluated.value === 'object'
- ) {
- const {message, id, description} = firstArgEvaluated.value as {
- [propName: string]: unknown;
- };
- translations[String(id ?? message)] = {
- message: String(message ?? id),
- ...(Boolean(description) && {description: String(description)}),
- };
- } else {
- warnings.push(
- `translate() first arg should be a statically evaluable object.
-Example: translate({message: "text",id: "optional.id",description: "optional description"}
-Dynamically constructed values are not allowed, because they prevent translations to be extracted.
-${sourceWarningPart(path.node)}`,
- );
- }
- } else {
- warnings.push(
- `translate() function only takes 1 or 2 args
-${sourceWarningPart(path.node)}`,
- );
- }
- },
- }),
- });
-
- return {sourceCodeFilePath, translations, warnings};
-}
diff --git a/packages/docusaurus/src/server/utils.ts b/packages/docusaurus/src/server/utils.ts
deleted file mode 100644
index d6c09dc468e1..000000000000
--- a/packages/docusaurus/src/server/utils.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * 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 path from 'path';
-import {posixPath, Globby} from '@docusaurus/utils';
-
-// Globby that fix Windows path patterns
-// See https://github.com/facebook/docusaurus/pull/4222#issuecomment-795517329
-export async function safeGlobby(
- patterns: string[],
- options?: Globby.GlobbyOptions,
-): Promise {
- // Required for Windows support, as paths using \ should not be used by globby
- // (also using the windows hard drive prefix like c: is not a good idea)
- const globPaths = patterns.map((dirPath) =>
- posixPath(path.relative(process.cwd(), dirPath)),
- );
-
- return Globby(globPaths, options);
-}
diff --git a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
index 88a08cdf5997..8f321b063446 100644
--- a/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
+++ b/packages/docusaurus/src/webpack/__tests__/__snapshots__/base.test.ts.snap
@@ -52,3 +52,54 @@ exports[`base webpack config creates webpack aliases 1`] = `
"react-dom": "../../../../../../../node_modules/react-dom",
}
`;
+
+exports[`base webpack config uses svg rule 1`] = `
+{
+ "oneOf": [
+ {
+ "issuer": {
+ "and": [
+ /\\\\\\.\\(\\?:tsx\\?\\|jsx\\?\\|mdx\\?\\)\\$/i,
+ ],
+ },
+ "use": [
+ {
+ "loader": "/node_modules/@svgr/webpack/dist/index.js",
+ "options": {
+ "prettier": false,
+ "svgo": true,
+ "svgoConfig": {
+ "plugins": [
+ {
+ "name": "preset-default",
+ "params": {
+ "overrides": {
+ "removeTitle": false,
+ "removeViewBox": false,
+ },
+ },
+ },
+ ],
+ },
+ "titleProp": true,
+ },
+ },
+ ],
+ },
+ {
+ "use": [
+ {
+ "loader": "/node_modules/url-loader/dist/cjs.js",
+ "options": {
+ "emitFile": true,
+ "fallback": "/node_modules/file-loader/dist/cjs.js",
+ "limit": 10000,
+ "name": "assets/images/[name]-[contenthash].[ext]",
+ },
+ },
+ ],
+ },
+ ],
+ "test": /\\\\\\.svg\\$/i,
+}
+`;
diff --git a/packages/docusaurus/src/webpack/__tests__/base.test.ts b/packages/docusaurus/src/webpack/__tests__/base.test.ts
index aa8ec0fb6814..b4c29b514e17 100644
--- a/packages/docusaurus/src/webpack/__tests__/base.test.ts
+++ b/packages/docusaurus/src/webpack/__tests__/base.test.ts
@@ -8,7 +8,7 @@
import {jest} from '@jest/globals';
import path from 'path';
import _ from 'lodash';
-import * as utils from '@docusaurus/utils/lib/webpackUtils';
+import webpack from 'webpack';
import {posixPath} from '@docusaurus/utils';
import {excludeJS, clientDir, createBaseConfig} from '../base';
import {
@@ -87,6 +87,7 @@ describe('base webpack config', () => {
siteMetadata: {
docusaurusVersion: '2.0.0-alpha.70',
},
+ currentBundler: {name: 'webpack', instance: webpack},
plugins: [
{
getThemePath() {
@@ -133,20 +134,18 @@ describe('base webpack config', () => {
});
it('uses svg rule', async () => {
- const isServer = true;
- const fileLoaderUtils = utils.getFileLoaderUtils(isServer);
- const mockSvg = jest.spyOn(fileLoaderUtils.rules, 'svg');
- jest
- .spyOn(utils, 'getFileLoaderUtils')
- .mockImplementation(() => fileLoaderUtils);
-
- await createBaseConfig({
+ const config = await createBaseConfig({
props,
isServer: false,
minify: false,
faster: DEFAULT_FASTER_CONFIG,
configureWebpackUtils: await createTestConfigureWebpackUtils(),
});
- expect(mockSvg).toHaveBeenCalled();
+
+ const svgRule = (config.module?.rules ?? []).find((rule) => {
+ return rule && (rule as any).test.toString().includes('.svg');
+ });
+ expect(svgRule).toBeDefined();
+ expect(svgRule).toMatchSnapshot();
});
});
diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts
deleted file mode 100644
index 3493556f9a78..000000000000
--- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * 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 path from 'path';
-import {createJsLoaderFactory, getHttpsConfig} from '../utils';
-import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation';
-import type {RuleSetRule} from 'webpack';
-
-describe('customize JS loader', () => {
- function testJsLoaderFactory(
- siteConfig?: Parameters[0]['siteConfig'],
- ) {
- return createJsLoaderFactory({
- siteConfig: {
- ...siteConfig,
- webpack: {
- jsLoader: 'babel',
- ...siteConfig?.webpack,
- },
- future: {
- ...DEFAULT_FUTURE_CONFIG,
- ...siteConfig?.future,
- },
- },
- });
- }
-
- it('createJsLoaderFactory defaults to babel loader', async () => {
- const createJsLoader = await testJsLoaderFactory();
- expect(createJsLoader({isServer: true}).loader).toBe(
- require.resolve('babel-loader'),
- );
- expect(createJsLoader({isServer: false}).loader).toBe(
- require.resolve('babel-loader'),
- );
- });
-
- it('createJsLoaderFactory accepts loaders with preset', async () => {
- const createJsLoader = await testJsLoaderFactory({
- webpack: {jsLoader: 'babel'},
- });
-
- expect(
- createJsLoader({
- isServer: true,
- }).loader,
- ).toBe(require.resolve('babel-loader'));
- expect(
- createJsLoader({
- isServer: false,
- }).loader,
- ).toBe(require.resolve('babel-loader'));
- });
-
- it('createJsLoaderFactory allows customization', async () => {
- const customJSLoader = (isServer: boolean): RuleSetRule => ({
- loader: 'my-fast-js-loader',
- options: String(isServer),
- });
-
- const createJsLoader = await testJsLoaderFactory({
- webpack: {jsLoader: customJSLoader},
- });
-
- expect(
- createJsLoader({
- isServer: true,
- }),
- ).toEqual(customJSLoader(true));
- expect(
- createJsLoader({
- isServer: false,
- }),
- ).toEqual(customJSLoader(false));
- });
-});
-
-describe('getHttpsConfig', () => {
- const originalEnv = process.env;
-
- beforeEach(() => {
- jest.resetModules();
- process.env = {...originalEnv};
- });
-
- afterAll(() => {
- process.env = originalEnv;
- });
-
- it('returns true for HTTPS not env', async () => {
- await expect(getHttpsConfig()).resolves.toBe(false);
- });
-
- it('returns true for HTTPS in env', async () => {
- process.env.HTTPS = 'true';
- await expect(getHttpsConfig()).resolves.toBe(true);
- });
-
- it('returns custom certs if they are in env', async () => {
- process.env.HTTPS = 'true';
- process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt');
- process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
- await expect(getHttpsConfig()).resolves.toEqual({
- key: expect.any(Buffer),
- cert: expect.any(Buffer),
- });
- });
-
- it("throws if file doesn't exist", async () => {
- process.env.HTTPS = 'true';
- process.env.SSL_CRT_FILE = path.join(
- __dirname,
- '__fixtures__/nonexistent.crt',
- );
- process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
- await expect(getHttpsConfig()).rejects.toThrowErrorMatchingInlineSnapshot(
- `"You specified SSL_CRT_FILE in your env, but the file "/packages/docusaurus/src/webpack/__tests__/__fixtures__/nonexistent.crt" can't be found."`,
- );
- });
-
- it('throws for invalid key', async () => {
- process.env.HTTPS = 'true';
- process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt');
- process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/invalid.key');
- await expect(getHttpsConfig()).rejects.toThrow();
- });
-
- it('throws for invalid cert', async () => {
- process.env.HTTPS = 'true';
- process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/invalid.crt');
- process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key');
- await expect(getHttpsConfig()).rejects.toThrow();
- });
-});
diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts
index 14f053cdf086..a9d4adc05c96 100644
--- a/packages/docusaurus/src/webpack/base.ts
+++ b/packages/docusaurus/src/webpack/base.ts
@@ -7,11 +7,15 @@
import fs from 'fs-extra';
import path from 'path';
+import {getCustomBabelConfigFilePath} from '@docusaurus/babel';
+import {
+ getCSSExtractPlugin,
+ getMinimizers,
+ createJsLoaderFactory,
+} from '@docusaurus/bundler';
+
import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils';
-import {createJsLoaderFactory, getCustomBabelConfigFilePath} from './utils';
-import {getMinimizers} from './minification';
import {loadThemeAliases, loadDocusaurusAliases} from './aliases';
-import {getCSSExtractPlugin} from './currentBundler';
import type {Configuration} from 'webpack';
import type {
ConfigureWebpackUtils,
@@ -91,7 +95,7 @@ export async function createBaseConfig({
const createJsLoader = await createJsLoaderFactory({siteConfig});
const CSSExtractPlugin = await getCSSExtractPlugin({
- currentBundler: configureWebpackUtils.currentBundler,
+ currentBundler: props.currentBundler,
});
return {
@@ -180,7 +184,9 @@ export async function createBaseConfig({
// Only minimize client bundle in production because server bundle is only
// used for static site generation
minimize: minimizeEnabled,
- minimizer: minimizeEnabled ? await getMinimizers({faster}) : undefined,
+ minimizer: minimizeEnabled
+ ? await getMinimizers({faster, currentBundler: props.currentBundler})
+ : undefined,
splitChunks: isServer
? false
: {
diff --git a/packages/docusaurus/src/webpack/client.ts b/packages/docusaurus/src/webpack/client.ts
index dca201849d0a..6ac4c5de2cb3 100644
--- a/packages/docusaurus/src/webpack/client.ts
+++ b/packages/docusaurus/src/webpack/client.ts
@@ -7,11 +7,10 @@
import path from 'path';
import merge from 'webpack-merge';
-import WebpackBar from 'webpackbar';
-import webpack from 'webpack';
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
import HtmlWebpackPlugin from 'html-webpack-plugin';
+import {getProgressBarPlugin} from '@docusaurus/bundler';
import {createBaseConfig} from './base';
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
import CleanWebpackPlugin from './plugins/CleanWebpackPlugin';
@@ -45,6 +44,10 @@ async function createBaseClientConfig({
configureWebpackUtils,
});
+ const ProgressBarPlugin = await getProgressBarPlugin({
+ currentBundler: configureWebpackUtils.currentBundler,
+ });
+
return merge(baseConfig, {
// Useless, disabled on purpose (errors on existing sites with no
// browserslist config)
@@ -56,17 +59,15 @@ async function createBaseClientConfig({
runtimeChunk: true,
},
plugins: [
- new webpack.DefinePlugin({
+ new props.currentBundler.instance.DefinePlugin({
'process.env.HYDRATE_CLIENT_ENTRY': JSON.stringify(hydrate),
}),
new ChunkAssetPlugin(),
- // Show compilation progress bar and build time.
- new WebpackBar({
+ new ProgressBarPlugin({
name: 'Client',
}),
await createStaticDirectoriesCopyPlugin({
props,
- currentBundler: configureWebpackUtils.currentBundler,
}),
].filter(Boolean),
});
@@ -88,7 +89,7 @@ export async function createStartClientConfig({
}): Promise<{clientConfig: Configuration}> {
const {siteConfig, headTags, preBodyTags, postBodyTags} = props;
- const clientConfig: webpack.Configuration = merge(
+ const clientConfig = merge(
await createBaseClientConfig({
props,
minify,
diff --git a/packages/docusaurus/src/webpack/configure.ts b/packages/docusaurus/src/webpack/configure.ts
index 2f50bfbe05ce..86803108b497 100644
--- a/packages/docusaurus/src/webpack/configure.ts
+++ b/packages/docusaurus/src/webpack/configure.ts
@@ -10,8 +10,11 @@ import {
customizeArray,
customizeObject,
} from 'webpack-merge';
-import {createJsLoaderFactory, createStyleLoadersFactory} from './utils';
-import {getCurrentBundler} from './currentBundler';
+import {
+ getCurrentBundler,
+ createJsLoaderFactory,
+ createStyleLoadersFactory,
+} from '@docusaurus/bundler';
import type {Configuration, RuleSetRule} from 'webpack';
import type {
Plugin,
diff --git a/packages/docusaurus/src/webpack/plugins/ForceTerminatePlugin.ts b/packages/docusaurus/src/webpack/plugins/ForceTerminatePlugin.ts
index 15a41a2127f1..18a1c4104eb0 100644
--- a/packages/docusaurus/src/webpack/plugins/ForceTerminatePlugin.ts
+++ b/packages/docusaurus/src/webpack/plugins/ForceTerminatePlugin.ts
@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
+import {formatStatsErrorMessage} from '@docusaurus/bundler';
import logger from '@docusaurus/logger';
-import {formatStatsErrorMessage} from '../utils';
import type webpack from 'webpack';
// When building, include the plugin to force terminate building if errors
diff --git a/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts b/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts
index 297d1bc99190..3e5ec8ddb78a 100644
--- a/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts
+++ b/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts
@@ -7,19 +7,17 @@
import path from 'path';
import fs from 'fs-extra';
-import {getCopyPlugin} from '../currentBundler';
-import type {CurrentBundler, Props} from '@docusaurus/types';
+import {getCopyPlugin} from '@docusaurus/bundler';
+import type {Props} from '@docusaurus/types';
import type {WebpackPluginInstance} from 'webpack';
export async function createStaticDirectoriesCopyPlugin({
props,
- currentBundler,
}: {
props: Props;
- currentBundler: CurrentBundler;
}): Promise {
const CopyPlugin = await getCopyPlugin({
- currentBundler,
+ currentBundler: props.currentBundler,
});
const {
diff --git a/packages/docusaurus/src/webpack/server.ts b/packages/docusaurus/src/webpack/server.ts
index e8df2b485c23..205d157a1323 100644
--- a/packages/docusaurus/src/webpack/server.ts
+++ b/packages/docusaurus/src/webpack/server.ts
@@ -8,7 +8,7 @@
import path from 'path';
import merge from 'webpack-merge';
import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils';
-import WebpackBar from 'webpackbar';
+import {getProgressBarPlugin} from '@docusaurus/bundler';
import {createBaseConfig} from './base';
import type {ConfigureWebpackUtils, Props} from '@docusaurus/types';
import type {Configuration} from 'webpack';
@@ -28,6 +28,10 @@ export default async function createServerConfig({
configureWebpackUtils,
});
+ const ProgressBarPlugin = await getProgressBarPlugin({
+ currentBundler: props.currentBundler,
+ });
+
const outputFilename = 'server.bundle.js';
const outputDir = path.join(props.outDir, '__server');
const serverBundlePath = path.join(outputDir, outputFilename);
@@ -43,8 +47,7 @@ export default async function createServerConfig({
libraryTarget: 'commonjs2',
},
plugins: [
- // Show compilation progress bar.
- new WebpackBar({
+ new ProgressBarPlugin({
name: 'Server',
color: 'yellow',
}),
diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts
deleted file mode 100644
index 8711be52c544..000000000000
--- a/packages/docusaurus/src/webpack/utils.ts
+++ /dev/null
@@ -1,304 +0,0 @@
-/**
- * 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 fs from 'fs-extra';
-import path from 'path';
-import crypto from 'crypto';
-import logger from '@docusaurus/logger';
-import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils';
-import webpack, {type Configuration} from 'webpack';
-import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
-import {importSwcJsLoaderFactory} from '../faster';
-import {getCSSExtractPlugin} from './currentBundler';
-import type {
- ConfigureWebpackUtils,
- CurrentBundler,
- DocusaurusConfig,
-} from '@docusaurus/types';
-import type {TransformOptions} from '@babel/core';
-
-export function formatStatsErrorMessage(
- statsJson: ReturnType | undefined,
-): string | undefined {
- if (statsJson?.errors?.length) {
- // 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);
- return errors
- .map((str) => logger.red(str))
- .join(`\n\n${logger.yellow('--------------------------')}\n\n`);
- }
- return undefined;
-}
-
-export function printStatsWarnings(
- statsJson: ReturnType | undefined,
-): void {
- if (statsJson?.warnings?.length) {
- statsJson.warnings?.forEach((warning) => {
- logger.warn(warning);
- });
- }
-}
-
-export async function createStyleLoadersFactory({
- currentBundler,
-}: {
- currentBundler: CurrentBundler;
-}): Promise {
- const CssExtractPlugin = await getCSSExtractPlugin({currentBundler});
-
- return function getStyleLoaders(
- isServer: boolean,
- cssOptionsArg: {
- [key: string]: unknown;
- } = {},
- ) {
- const cssOptions: {[key: string]: unknown} = {
- // TODO turn esModule on later, see https://github.com/facebook/docusaurus/pull/6424
- esModule: false,
- ...cssOptionsArg,
- };
-
- // On the server we don't really need to extract/emit CSS
- // We only need to transform CSS module imports to a styles object
- if (isServer) {
- return cssOptions.modules
- ? [
- {
- loader: require.resolve('css-loader'),
- options: cssOptions,
- },
- ]
- : // Ignore regular CSS files
- [{loader: require.resolve('null-loader')}];
- }
-
- return [
- {
- loader: CssExtractPlugin.loader,
- options: {
- esModule: true,
- },
- },
- {
- loader: require.resolve('css-loader'),
- options: cssOptions,
- },
-
- // TODO apart for configurePostCss(), do we really need this loader?
- // Note: using postcss here looks inefficient/duplicate
- // But in practice, it's not a big deal because css-loader also uses postcss
- // and is able to reuse the parsed AST from postcss-loader
- // See https://github.com/webpack-contrib/css-loader/blob/master/src/index.js#L159
- {
- // Options for PostCSS as we reference these options twice
- // Adds vendor prefixing based on your specified browser support in
- // package.json
- loader: require.resolve('postcss-loader'),
- options: {
- postcssOptions: {
- // Necessary for external CSS imports to work
- // https://github.com/facebook/create-react-app/issues/2677
- ident: 'postcss',
- plugins: [
- // eslint-disable-next-line global-require
- require('autoprefixer'),
- ],
- },
- },
- },
- ];
- };
-}
-
-export async function getCustomBabelConfigFilePath(
- siteDir: string,
-): Promise {
- const customBabelConfigurationPath = path.join(
- siteDir,
- BABEL_CONFIG_FILE_NAME,
- );
- return (await fs.pathExists(customBabelConfigurationPath))
- ? customBabelConfigurationPath
- : undefined;
-}
-
-export function getBabelOptions({
- isServer,
- babelOptions,
-}: {
- isServer?: boolean;
- babelOptions?: TransformOptions | string;
-} = {}): TransformOptions {
- if (typeof babelOptions === 'string') {
- return {
- babelrc: false,
- configFile: babelOptions,
- caller: {name: isServer ? 'server' : 'client'},
- };
- }
- return {
- ...(babelOptions ?? {presets: [require.resolve('../babel/preset')]}),
- babelrc: false,
- configFile: false,
- caller: {name: isServer ? 'server' : 'client'},
- };
-}
-
-const BabelJsLoaderFactory: ConfigureWebpackUtils['getJSLoader'] = ({
- isServer,
- babelOptions,
-}) => {
- return {
- loader: require.resolve('babel-loader'),
- options: getBabelOptions({isServer, babelOptions}),
- };
-};
-
-// Confusing: function that creates a function that creates actual js loaders
-// This is done on purpose because the js loader factory is a public API
-// It is injected in configureWebpack plugin lifecycle for plugin authors
-export async function createJsLoaderFactory({
- siteConfig,
-}: {
- siteConfig: {
- webpack?: DocusaurusConfig['webpack'];
- future?: {
- experimental_faster: DocusaurusConfig['future']['experimental_faster'];
- };
- };
-}): Promise {
- const jsLoader = siteConfig.webpack?.jsLoader ?? 'babel';
- if (
- jsLoader instanceof Function &&
- siteConfig.future?.experimental_faster.swcJsLoader
- ) {
- throw new Error(
- "You can't use a custom webpack.jsLoader and experimental_faster.swcJsLoader at the same time",
- );
- }
- if (jsLoader instanceof Function) {
- return ({isServer}) => jsLoader(isServer);
- }
- if (siteConfig.future?.experimental_faster.swcJsLoader) {
- return importSwcJsLoaderFactory();
- }
- if (jsLoader === 'babel') {
- return BabelJsLoaderFactory;
- }
- throw new Error(`Docusaurus bug: unexpected jsLoader value${jsLoader}`);
-}
-
-declare global {
- interface Error {
- /** @see https://webpack.js.org/api/node/#error-handling */
- details: unknown;
- }
-}
-
-export function compile(config: Configuration[]): Promise {
- return new Promise((resolve, reject) => {
- const compiler = webpack(config);
- compiler.run((err, stats) => {
- if (err) {
- logger.error(err.stack ?? err);
- if (err.details) {
- logger.error(err.details);
- }
- reject(err);
- }
- // Let plugins consume all the stats
- const errorsWarnings = stats?.toJson('errors-warnings');
- if (stats?.hasErrors()) {
- const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
- reject(
- new Error(
- `Failed to compile due to Webpack errors.\n${statsErrorMessage}`,
- ),
- );
- }
- printStatsWarnings(errorsWarnings);
-
- // Webpack 5 requires calling close() so that persistent caching works
- // See https://github.com/webpack/webpack.js.org/pull/4775
- compiler.close((errClose) => {
- if (errClose) {
- logger.error(`Error while closing Webpack compiler: ${errClose}`);
- reject(errClose);
- } else {
- resolve(stats!);
- }
- });
- });
- });
-}
-
-// Ensure the certificate and key provided are valid and if not
-// throw an easy to debug error
-function validateKeyAndCerts({
- cert,
- key,
- keyFile,
- crtFile,
-}: {
- cert: Buffer;
- key: Buffer;
- keyFile: string;
- crtFile: string;
-}) {
- let encrypted: Buffer;
- try {
- // publicEncrypt will throw an error with an invalid cert
- encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
- } catch (err) {
- logger.error`The certificate path=${crtFile} is invalid.`;
- throw err;
- }
-
- try {
- // privateDecrypt will throw an error with an invalid key
- crypto.privateDecrypt(key, encrypted);
- } catch (err) {
- logger.error`The certificate key path=${keyFile} is invalid.`;
- throw err;
- }
-}
-
-// Read file and throw an error if it doesn't exist
-async function readEnvFile(file: string, type: string) {
- if (!(await fs.pathExists(file))) {
- throw new Error(
- `You specified ${type} in your env, but the file "${file}" can't be found.`,
- );
- }
- return fs.readFile(file);
-}
-
-// Get the https config
-// Return cert files if provided in env, otherwise just true or false
-export async function getHttpsConfig(): Promise<
- boolean | {cert: Buffer; key: Buffer}
-> {
- const appDirectory = await fs.realpath(process.cwd());
- const {SSL_CRT_FILE, SSL_KEY_FILE, HTTPS} = process.env;
- const isHttps = HTTPS === 'true';
-
- if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
- const crtFile = path.resolve(appDirectory, SSL_CRT_FILE);
- const keyFile = path.resolve(appDirectory, SSL_KEY_FILE);
- const config = {
- cert: await readEnvFile(crtFile, 'SSL_CRT_FILE'),
- key: await readEnvFile(keyFile, 'SSL_KEY_FILE'),
- };
-
- validateKeyAndCerts({...config, keyFile, crtFile});
- return config;
- }
- return isHttps;
-}
diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.crt b/packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/host.crt
similarity index 100%
rename from packages/docusaurus/src/webpack/__tests__/__fixtures__/host.crt
rename to packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/host.crt
diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.key b/packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/host.key
similarity index 100%
rename from packages/docusaurus/src/webpack/__tests__/__fixtures__/host.key
rename to packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/host.key
diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.crt b/packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/invalid.crt
similarity index 100%
rename from packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.crt
rename to packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/invalid.crt
diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.key b/packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/invalid.key
similarity index 100%
rename from packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.key
rename to packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/invalid.key
diff --git a/packages/docusaurus/src/webpack/utils/__tests__/getHttpsConfig.test.ts b/packages/docusaurus/src/webpack/utils/__tests__/getHttpsConfig.test.ts
new file mode 100644
index 000000000000..2f395cd4bcd6
--- /dev/null
+++ b/packages/docusaurus/src/webpack/utils/__tests__/getHttpsConfig.test.ts
@@ -0,0 +1,68 @@
+/**
+ * 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 path from 'path';
+import getHttpsConfig from '../getHttpsConfig';
+
+describe('getHttpsConfig', () => {
+ const originalEnv = process.env;
+
+ function getFixture(name: string) {
+ return path.join(__dirname, '__fixtures__/getHttpsConfig', name);
+ }
+
+ beforeEach(() => {
+ jest.resetModules();
+ process.env = {...originalEnv};
+ });
+
+ afterAll(() => {
+ process.env = originalEnv;
+ });
+
+ it('returns true for HTTPS not env', async () => {
+ await expect(getHttpsConfig()).resolves.toBe(false);
+ });
+
+ it('returns true for HTTPS in env', async () => {
+ process.env.HTTPS = 'true';
+ await expect(getHttpsConfig()).resolves.toBe(true);
+ });
+
+ it('returns custom certs if they are in env', async () => {
+ process.env.HTTPS = 'true';
+ process.env.SSL_CRT_FILE = getFixture('host.crt');
+ process.env.SSL_KEY_FILE = getFixture('host.key');
+ await expect(getHttpsConfig()).resolves.toEqual({
+ key: expect.any(Buffer),
+ cert: expect.any(Buffer),
+ });
+ });
+
+ it("throws if file doesn't exist", async () => {
+ process.env.HTTPS = 'true';
+ process.env.SSL_CRT_FILE = getFixture('nonexistent.crt');
+ process.env.SSL_KEY_FILE = getFixture('host.key');
+ await expect(getHttpsConfig()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"You specified SSL_CRT_FILE in your env, but the file "/packages/docusaurus/src/webpack/utils/__tests__/__fixtures__/getHttpsConfig/nonexistent.crt" can't be found."`,
+ );
+ });
+
+ it('throws for invalid key', async () => {
+ process.env.HTTPS = 'true';
+ process.env.SSL_CRT_FILE = getFixture('host.crt');
+ process.env.SSL_KEY_FILE = getFixture('invalid.key');
+ await expect(getHttpsConfig()).rejects.toThrow();
+ });
+
+ it('throws for invalid cert', async () => {
+ process.env.HTTPS = 'true';
+ process.env.SSL_CRT_FILE = getFixture('invalid.crt');
+ process.env.SSL_KEY_FILE = getFixture('host.key');
+ await expect(getHttpsConfig()).rejects.toThrow();
+ });
+});
diff --git a/packages/docusaurus/src/webpack/utils/getHttpsConfig.ts b/packages/docusaurus/src/webpack/utils/getHttpsConfig.ts
new file mode 100644
index 000000000000..083614ceb889
--- /dev/null
+++ b/packages/docusaurus/src/webpack/utils/getHttpsConfig.ts
@@ -0,0 +1,75 @@
+/**
+ * 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 fs from 'fs-extra';
+import path from 'path';
+import crypto from 'crypto';
+import logger from '@docusaurus/logger';
+
+// Ensure the certificate and key provided are valid and if not
+// throw an easy to debug error
+function validateKeyAndCerts({
+ cert,
+ key,
+ keyFile,
+ crtFile,
+}: {
+ cert: Buffer;
+ key: Buffer;
+ keyFile: string;
+ crtFile: string;
+}) {
+ let encrypted: Buffer;
+ try {
+ // publicEncrypt will throw an error with an invalid cert
+ encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
+ } catch (err) {
+ logger.error`The certificate path=${crtFile} is invalid.`;
+ throw err;
+ }
+
+ try {
+ // privateDecrypt will throw an error with an invalid key
+ crypto.privateDecrypt(key, encrypted);
+ } catch (err) {
+ logger.error`The certificate key path=${keyFile} is invalid.`;
+ throw err;
+ }
+}
+
+// Read file and throw an error if it doesn't exist
+async function readEnvFile(file: string, type: string) {
+ if (!(await fs.pathExists(file))) {
+ throw new Error(
+ `You specified ${type} in your env, but the file "${file}" can't be found.`,
+ );
+ }
+ return fs.readFile(file);
+}
+
+// Get the https config
+// Return cert files if provided in env, otherwise just true or false
+export default async function getHttpsConfig(): Promise<
+ boolean | {cert: Buffer; key: Buffer}
+> {
+ const appDirectory = await fs.realpath(process.cwd());
+ const {SSL_CRT_FILE, SSL_KEY_FILE, HTTPS} = process.env;
+ const isHttps = HTTPS === 'true';
+
+ if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
+ const crtFile = path.resolve(appDirectory, SSL_CRT_FILE);
+ const keyFile = path.resolve(appDirectory, SSL_KEY_FILE);
+ const config = {
+ cert: await readEnvFile(crtFile, 'SSL_CRT_FILE'),
+ key: await readEnvFile(keyFile, 'SSL_KEY_FILE'),
+ };
+
+ validateKeyAndCerts({...config, keyFile, crtFile});
+ return config;
+ }
+ return isHttps;
+}
diff --git a/website/babel.config.js b/website/babel.config.js
index cd005dd9cccc..25875e982f12 100644
--- a/website/babel.config.js
+++ b/website/babel.config.js
@@ -6,5 +6,5 @@
*/
module.exports = {
- presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
+ presets: ['@docusaurus/babel/preset'],
};
diff --git a/website/docs/configuration.mdx b/website/docs/configuration.mdx
index 239ced56edee..8278b5cc3fe9 100644
--- a/website/docs/configuration.mdx
+++ b/website/docs/configuration.mdx
@@ -279,7 +279,7 @@ For new Docusaurus projects, we automatically generated a `babel.config.js` in t
```js title="babel.config.js"
export default {
- presets: ['@docusaurus/core/lib/babel/preset'],
+ presets: ['@docusaurus/babel/preset'],
};
```
diff --git a/yarn.lock b/yarn.lock
index 15359c6b7d4c..08b0a1ef68d3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4383,7 +4383,7 @@ ansi-colors@^4.1.1:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
-ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
+ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
@@ -4620,15 +4620,15 @@ at-least-node@^1.0.0:
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
autoprefixer@^10.4.14, autoprefixer@^10.4.19:
- version "10.4.19"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f"
- integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==
+ version "10.4.20"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
+ integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==
dependencies:
- browserslist "^4.23.0"
- caniuse-lite "^1.0.30001599"
+ browserslist "^4.23.3"
+ caniuse-lite "^1.0.30001646"
fraction.js "^4.3.7"
normalize-range "^0.1.2"
- picocolors "^1.0.0"
+ picocolors "^1.0.1"
postcss-value-parser "^4.2.0"
available-typed-arrays@^1.0.5:
@@ -4959,7 +4959,7 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
-browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1:
+browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3:
version "4.23.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800"
integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==
@@ -5186,7 +5186,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001646:
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646:
version "1.0.30001651"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
@@ -5765,10 +5765,10 @@ connect@3.7.0:
parseurl "~1.3.3"
utils-merge "1.0.1"
-consola@^2.15.3:
- version "2.15.3"
- resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
- integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
+consola@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f"
+ integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==
console-control-strings@^1.1.0:
version "1.1.0"
@@ -6006,7 +6006,7 @@ cosmiconfig@^7.1.0:
path-type "^4.0.0"
yaml "^1.10.0"
-cosmiconfig@^8.1.3, cosmiconfig@^8.2.0:
+cosmiconfig@^8.1.3, cosmiconfig@^8.3.5:
version "8.3.6"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==
@@ -6199,18 +6199,18 @@ css-functions-list@^3.1.0:
integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==
css-loader@^6.8.1:
- version "6.8.1"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
- integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba"
+ integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==
dependencies:
icss-utils "^5.1.0"
- postcss "^8.4.21"
- postcss-modules-extract-imports "^3.0.0"
- postcss-modules-local-by-default "^4.0.3"
- postcss-modules-scope "^3.0.0"
+ postcss "^8.4.33"
+ postcss-modules-extract-imports "^3.1.0"
+ postcss-modules-local-by-default "^4.0.5"
+ postcss-modules-scope "^3.2.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.2.0"
- semver "^7.3.8"
+ semver "^7.5.4"
css-minimizer-webpack-plugin@^5.0.1:
version "5.0.1"
@@ -7962,7 +7962,7 @@ feed@^4.2.2:
dependencies:
xml-js "^1.6.11"
-figures@3.2.0, figures@^3.0.0:
+figures@3.2.0, figures@^3.0.0, figures@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
@@ -10313,7 +10313,7 @@ jest@^29.7.0:
import-local "^3.0.2"
jest-cli "^29.7.0"
-jiti@^1.18.2, jiti@^1.20.0:
+jiti@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
@@ -11124,6 +11124,13 @@ markdown-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4"
integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==
+markdown-table@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
+ integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==
+ dependencies:
+ repeat-string "^1.0.0"
+
markdown-table@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
@@ -11966,12 +11973,13 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-mini-css-extract-plugin@^2.7.6:
- version "2.7.6"
- resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d"
- integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==
+mini-css-extract-plugin@^2.9.1:
+ version "2.9.1"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758"
+ integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==
dependencies:
schema-utils "^4.0.0"
+ tapable "^2.2.1"
minimalistic-assert@^1.0.0:
version "1.0.1"
@@ -13297,10 +13305,10 @@ periscopic@^3.0.0:
estree-walker "^3.0.0"
is-reference "^3.0.0"
-picocolors@^1.0.0, picocolors@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
- integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
+ integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
@@ -13453,13 +13461,13 @@ postcss-discard-unused@^6.0.5:
postcss-selector-parser "^6.0.16"
postcss-loader@^7.3.3:
- version "7.3.3"
- resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd"
- integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209"
+ integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==
dependencies:
- cosmiconfig "^8.2.0"
- jiti "^1.18.2"
- semver "^7.3.8"
+ cosmiconfig "^8.3.5"
+ jiti "^1.20.0"
+ semver "^7.5.4"
postcss-media-query-parser@^0.2.3:
version "0.2.3"
@@ -13524,24 +13532,24 @@ postcss-minify-selectors@^6.0.4:
dependencies:
postcss-selector-parser "^6.0.16"
-postcss-modules-extract-imports@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
- integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+postcss-modules-extract-imports@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002"
+ integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==
-postcss-modules-local-by-default@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
- integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==
+postcss-modules-local-by-default@^4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f"
+ integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser "^6.0.2"
postcss-value-parser "^4.1.0"
-postcss-modules-scope@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
- integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+postcss-modules-scope@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5"
+ integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==
dependencies:
postcss-selector-parser "^6.0.4"
@@ -13655,9 +13663,9 @@ postcss-safe-parser@^6.0.0:
integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
- version "6.0.16"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
- integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de"
+ integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
@@ -13694,14 +13702,14 @@ postcss-zindex@^6.0.2:
resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-6.0.2.tgz#e498304b83a8b165755f53db40e2ea65a99b56e1"
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
-postcss@^8.2.x, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38:
- version "8.4.38"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
- integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+postcss@^8.2.x, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.38:
+ version "8.4.47"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
+ integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.7"
- picocolors "^1.0.0"
- source-map-js "^1.2.0"
+ picocolors "^1.1.0"
+ source-map-js "^1.2.1"
prebuild-install@^7.1.1:
version "7.1.1"
@@ -14658,7 +14666,7 @@ renderkid@^3.0.0:
lodash "^4.17.21"
strip-ansi "^6.0.1"
-repeat-string@^1.6.1:
+repeat-string@^1.0.0, repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
@@ -15075,9 +15083,9 @@ serialize-javascript@^4.0.0:
randombytes "^2.1.0"
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
- integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+ integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
dependencies:
randombytes "^2.1.0"
@@ -15434,10 +15442,10 @@ sort-keys@^2.0.0:
dependencies:
is-plain-obj "^1.0.0"
-source-map-js@^1.0.1, source-map-js@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
- integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+source-map-js@^1.0.1, source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@0.5.13:
version "0.5.13"
@@ -15593,10 +15601,10 @@ statuses@2.0.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
-std-env@^3.0.1:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
- integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==
+std-env@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
+ integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
streamx@^2.15.0:
version "2.15.0"
@@ -15988,7 +15996,7 @@ table@^6.8.1:
string-width "^4.2.3"
strip-ansi "^6.0.1"
-tapable@2.2.1, tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
+tapable@2.2.1, tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
@@ -17143,15 +17151,19 @@ webpack@^5, webpack@^5.88.1:
watchpack "^2.4.1"
webpack-sources "^3.2.3"
-webpackbar@^5.0.2:
- version "5.0.2"
- resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570"
- integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==
+webpackbar@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b"
+ integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==
dependencies:
- chalk "^4.1.0"
- consola "^2.15.3"
+ ansi-escapes "^4.3.2"
+ chalk "^4.1.2"
+ consola "^3.2.3"
+ figures "^3.2.0"
+ markdown-table "^2.0.0"
pretty-time "^1.1.0"
- std-env "^3.0.1"
+ std-env "^3.7.0"
+ wrap-ansi "^7.0.0"
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
version "0.7.4"