refactor: create @docusaurus/bundler and @docusaurus/babel packages (#10511)

This commit is contained in:
Sébastien Lorber 2024-09-21 16:35:49 +02:00 committed by GitHub
parent fd14d6af55
commit 9ecff801ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 1921 additions and 1588 deletions

View file

@ -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",

View file

@ -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",

View file

@ -1,3 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
presets: ['@docusaurus/babel/preset'],
};

View file

@ -0,0 +1,3 @@
.tsbuildinfo*
tsconfig*
__tests__

View file

@ -0,0 +1,3 @@
# `@docusaurus/babel`
Docusaurus package for Babel-related utils.

View file

@ -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"
}
}

View file

@ -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 (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={translate({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 <Translate> components', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function MyComponent() {
return (
<div>
<Translate id="codeId" description={"code description"}>
code message
</Translate>
<Translate id="codeId1" description="description 2" />
</div>
);
}
`,
});
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 (
<div>
<input
text={translate({
id: prefix + 'codeId fn',
message: prefix + 'code message',
description: prefix + 'code description'}
)}
/>
<Translate
id={prefix + "codeId comp"}
description={prefix + "code description"}
>
{prefix + "code message"}
</Translate>
<Translate>
{
prefix + \`Static template literal with unusual formatting!\`
}
</Translate>
</div>
);
}
`,
});
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<T> = {toto: string}
export default function MyComponent<T>(props: ComponentProps<T>) {
return (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'}) as string}/>
<input text={translate({message: 'code message 2',description: 'code description 2'}) as string}/>
</div>
);
}
`,
});
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 <Translate>bar</Translate>
}
`,
});
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 (
<div>
<Foo id="codeId" description={"code description"}>
code message
</Foo>
<Translate id="codeId1" />
</div>
);
}
export default function () {
return (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={bar({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={bar({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 (
<Translate description="foo" />
);
}
`,
});
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
sourceCodeFilePath,
TestBabelOptions,
);
expect(sourceCodeFileTranslations).toEqual({
sourceCodeFilePath,
translations: {},
warnings: [
`<Translate> without children must have id prop.
Example: <Translate id="my-id" />
File: ${sourceCodeFilePath} at line 6
Full code: <Translate description="foo" />`,
],
});
});
it('warns about dynamic id', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function () {
return (
<Translate id={index}>foo</Translate>
);
}
`,
});
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
sourceCodeFilePath,
TestBabelOptions,
);
expect(sourceCodeFileTranslations).toEqual({
sourceCodeFilePath,
translations: {
foo: {
message: 'foo',
},
},
warnings: [
`<Translate> prop=id should be a statically evaluable object.
Example: <Translate id="optional id" description="optional description">Message</Translate>
Dynamically constructed values are not allowed, because they prevent translations to be extracted.
File: ${sourceCodeFilePath} at line 6
Full code: <Translate id={index}>foo</Translate>`,
],
});
});
it('warns about dynamic children', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function () {
return (
<Translate id='foo'><a>hhh</a></Translate>
);
}
`,
});
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 <Translate id="my-id" description="my-description">text</Translate>.
File: ${sourceCodeFilePath} at line 6
Full code: <Translate id='foo'><a>hhh</a></Translate>`,
],
});
});
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)`,
],
});
});
});

View file

@ -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<SourceCodeFileTranslations[]> {
return Promise.all(
sourceCodeFilePaths.flatMap((sourceFilePath) =>
extractSourceCodeFileTranslations(sourceFilePath, babelOptions),
),
);
}
export async function extractSourceCodeFileTranslations(
sourceCodeFilePath: string,
babelOptions: TransformOptions,
): Promise<SourceCodeFileTranslations> {
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<t.ImportDefaultSpecifier> =>
specifier.node.type === 'ImportDefaultSpecifier',
);
const callbackImport = importSpecifiers.find(
(specifier): specifier is NodePath<t.ImportSpecifier> =>
specifier.node.type === 'ImportSpecifier' &&
((
(specifier as NodePath<t.ImportSpecifier>).get('imported')
.node as t.Identifier
).name === 'translate' ||
(
(specifier as NodePath<t.ImportSpecifier>).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(
`<Translate> prop=${propName} should be a statically evaluable object.
Example: <Translate id="optional id" description="optional description">Message</Translate>
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(`<Translate> without children must have id prop.
Example: <Translate id="my-id" />
${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 <Translate id="my-id" description="my-description">text</Translate>.
${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};
}

View file

@ -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';

View file

@ -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');
}

View file

@ -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<string | undefined> {
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,
};
}

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": false,
"sourceMap": true,
"declarationMap": true
},
"include": ["src"],
"exclude": ["**/__tests__/**"]
}

View file

@ -0,0 +1,3 @@
.tsbuildinfo*
tsconfig*
__tests__

View file

@ -0,0 +1,3 @@
# `@docusaurus/bundler`
Docusaurus util package to abstract the current bundler.

View file

@ -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"
}
}

View file

@ -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<webpack.Stats['toJson']> | 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<webpack.Stats['toJson']> | 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<webpack.MultiStats> {
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!);
}
});
});
});
}

View file

@ -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<typeof WebpackBar> {
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;
}

View file

@ -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';

View file

@ -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<typeof createJsLoaderFactory>[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));
});
});

View file

@ -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<ConfigureWebpackUtils['getJSLoader']> {
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}`);
}

View file

@ -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<ConfigureWebpackUtils['getStyleLoaders']> {
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'),
],
},
},
},
];
};
}

View file

@ -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<FasterConfig, 'swcJsMinimizer'>;
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<WebpackPluginInstance[]> {
return Promise.all([getJsMinimizer(params), getCssMinimizer()]);
}
async function getRspackMinimizers({
currentBundler,
}: MinimizersConfig): Promise<WebpackPluginInstance[]> {
console.log('currentBundler', currentBundler.name);
throw new Error('TODO Rspack minimizers not implemented yet');
}
export async function getMinimizers(
params: MinimizersConfig,
): Promise<WebpackPluginInstance[]> {
return params.currentBundler.name === 'rspack'
? getRspackMinimizers(params)
: getWebpackMinimizers(params);
}

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"noEmit": false,
"sourceMap": true,
"declarationMap": true
},
"include": ["src"],
"exclude": ["**/__tests__/**"]
}

View file

@ -18,7 +18,6 @@ module: {
{
test: /\.mdx?$/,
use: [
'babel-loader',
{
loader: '@docusaurus/mdx-loader',
options: {

View file

@ -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"

View file

@ -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});
}
}

View file

@ -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');

View file

@ -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})$`),
),

View file

@ -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": {

View file

@ -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'],
},
);

View file

@ -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 & {

View file

@ -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<string[]> {
// 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<string[]> {
// 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)),
);
}

View file

@ -97,6 +97,8 @@ export {md5Hash, simpleHash, docuHash} from './hashUtils';
export {
Globby,
GlobExcludeDefault,
safeGlobby,
globTranslatableSourceFiles,
createMatcher,
createAbsoluteFilePathMatcher,
} from './globUtils';

View file

@ -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",

View file

@ -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;

View file

@ -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({

View file

@ -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,

View file

@ -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,

View file

@ -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<string[]> {
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,

View file

@ -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;

View file

@ -4,6 +4,10 @@ exports[`load loads props for site with custom i18n path 1`] = `
{
"baseUrl": "/",
"codeTranslations": {},
"currentBundler": {
"instance": [Function],
"name": "webpack",
},
"generatedFilesDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/.docusaurus",
"headTags": "",
"i18n": {

View file

@ -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,
};
}

View file

@ -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 (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={translate({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 <Translate> components', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function MyComponent() {
return (
<div>
<Translate id="codeId" description={"code description"}>
code message
</Translate>
<Translate id="codeId1" description="description 2" />
</div>
);
}
`,
});
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 (
<div>
<input
text={translate({
id: prefix + 'codeId fn',
message: prefix + 'code message',
description: prefix + 'code description'}
)}
/>
<Translate
id={prefix + "codeId comp"}
description={prefix + "code description"}
>
{prefix + "code message"}
</Translate>
<Translate>
{
prefix + \`Static template literal with unusual formatting!\`
}
</Translate>
</div>
);
}
`,
});
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<T> = {toto: string}
export default function MyComponent<T>(props: ComponentProps<T>) {
return (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'}) as string}/>
<input text={translate({message: 'code message 2',description: 'code description 2'}) as string}/>
</div>
);
}
`,
});
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 <Translate>bar</Translate>
}
`,
});
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 (
<div>
<Foo id="codeId" description={"code description"}>
code message
</Foo>
<Translate id="codeId1" />
</div>
);
}
export default function () {
return (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={bar({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 (
<div>
<input text={translate({id: 'codeId',message: 'code message',description: 'code description'})}/>
<input text={bar({id: 'codeId1'})}/>
</div>
);
}
`,
});
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 (
<Translate description="foo" />
);
}
`,
});
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
sourceCodeFilePath,
TestBabelOptions,
);
expect(sourceCodeFileTranslations).toEqual({
sourceCodeFilePath,
translations: {},
warnings: [
`<Translate> without children must have id prop.
Example: <Translate id="my-id" />
File: ${sourceCodeFilePath} at line 6
Full code: <Translate description="foo" />`,
],
});
});
it('warns about dynamic id', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function () {
return (
<Translate id={index}>foo</Translate>
);
}
`,
});
const sourceCodeFileTranslations = await extractSourceCodeFileTranslations(
sourceCodeFilePath,
TestBabelOptions,
);
expect(sourceCodeFileTranslations).toEqual({
sourceCodeFilePath,
translations: {
foo: {
message: 'foo',
},
},
warnings: [
`<Translate> prop=id should be a statically evaluable object.
Example: <Translate id="optional id" description="optional description">Message</Translate>
Dynamically constructed values are not allowed, because they prevent translations to be extracted.
File: ${sourceCodeFilePath} at line 6
Full code: <Translate id={index}>foo</Translate>`,
],
});
});
it('warns about dynamic children', async () => {
const {sourceCodeFilePath} = await createTmpSourceCodeFile({
extension: 'js',
content: `
import Translate from '@docusaurus/Translate';
export default function () {
return (
<Translate id='foo'><a>hhh</a></Translate>
);
}
`,
});
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 <Translate id="my-id" description="my-description">text</Translate>.
File: ${sourceCodeFilePath} at line 6
Full code: <Translate id='foo'><a>hhh</a></Translate>`,
],
});
});
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',

View file

@ -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<string[]> {
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<TranslationFileContent> {
export async function extractSiteSourceCodeTranslations({
siteDir,
plugins,
extraSourceCodeFilePaths = [],
}: {
siteDir: string;
plugins: InitializedPlugin[];
extraSourceCodeFilePaths?: string[];
}): Promise<TranslationFileContent> {
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<SourceCodeFileTranslations[]> {
return Promise.all(
sourceCodeFilePaths.flatMap((sourceFilePath) =>
extractSourceCodeFileTranslations(sourceFilePath, babelOptions),
),
);
}
export async function extractSourceCodeFileTranslations(
sourceCodeFilePath: string,
babelOptions: TransformOptions,
): Promise<SourceCodeFileTranslations> {
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<t.ImportDefaultSpecifier> =>
specifier.node.type === 'ImportDefaultSpecifier',
);
const callbackImport = importSpecifiers.find(
(specifier): specifier is NodePath<t.ImportSpecifier> =>
specifier.node.type === 'ImportSpecifier' &&
((
(specifier as NodePath<t.ImportSpecifier>).get('imported')
.node as t.Identifier
).name === 'translate' ||
(
(specifier as NodePath<t.ImportSpecifier>).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(
`<Translate> prop=${propName} should be a statically evaluable object.
Example: <Translate id="optional id" description="optional description">Message</Translate>
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(`<Translate> without children must have id prop.
Example: <Translate id="my-id" />
${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 <Translate id="my-id" description="my-description">text</Translate>.
${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};
}

View file

@ -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<string[]> {
// 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);
}

View file

@ -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": "<PROJECT_ROOT>/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": "<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js",
"options": {
"emitFile": true,
"fallback": "<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js",
"limit": 10000,
"name": "assets/images/[name]-[contenthash].[ext]",
},
},
],
},
],
"test": /\\\\\\.svg\\$/i,
}
`;

View file

@ -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();
});
});

View file

@ -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<typeof createJsLoaderFactory>[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 "<PROJECT_ROOT>/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();
});
});

View file

@ -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
: {

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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<WebpackPluginInstance | undefined> {
const CopyPlugin = await getCopyPlugin({
currentBundler,
currentBundler: props.currentBundler,
});
const {

View file

@ -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',
}),

View file

@ -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<webpack.Stats['toJson']> | 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<webpack.Stats['toJson']> | undefined,
): void {
if (statsJson?.warnings?.length) {
statsJson.warnings?.forEach((warning) => {
logger.warn(warning);
});
}
}
export async function createStyleLoadersFactory({
currentBundler,
}: {
currentBundler: CurrentBundler;
}): Promise<ConfigureWebpackUtils['getStyleLoaders']> {
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<string | undefined> {
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<ConfigureWebpackUtils['getJSLoader']> {
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<webpack.MultiStats> {
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;
}

View file

@ -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 "<PROJECT_ROOT>/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();
});
});

View file

@ -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;
}

View file

@ -6,5 +6,5 @@
*/
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
presets: ['@docusaurus/babel/preset'],
};

View file

@ -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'],
};
```

170
yarn.lock
View file

@ -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"