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"