mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
refactor: create @docusaurus/bundler
and @docusaurus/babel
packages (#10511)
This commit is contained in:
parent
fd14d6af55
commit
9ecff801ff
65 changed files with 1921 additions and 1588 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
presets: ['@docusaurus/babel/preset'],
|
||||
};
|
||||
|
|
3
packages/docusaurus-babel/.npmignore
Normal file
3
packages/docusaurus-babel/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
.tsbuildinfo*
|
||||
tsconfig*
|
||||
__tests__
|
3
packages/docusaurus-babel/README.md
Normal file
3
packages/docusaurus-babel/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `@docusaurus/babel`
|
||||
|
||||
Docusaurus package for Babel-related utils.
|
50
packages/docusaurus-babel/package.json
Normal file
50
packages/docusaurus-babel/package.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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)`,
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
266
packages/docusaurus-babel/src/babelTranslationsExtractor.ts
Normal file
266
packages/docusaurus-babel/src/babelTranslationsExtractor.ts
Normal 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};
|
||||
}
|
10
packages/docusaurus-babel/src/index.ts
Normal file
10
packages/docusaurus-babel/src/index.ts
Normal 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';
|
82
packages/docusaurus-babel/src/preset.ts
Normal file
82
packages/docusaurus-babel/src/preset.ts
Normal 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');
|
||||
}
|
50
packages/docusaurus-babel/src/utils.ts
Normal file
50
packages/docusaurus-babel/src/utils.ts
Normal 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,
|
||||
};
|
||||
}
|
10
packages/docusaurus-babel/tsconfig.json
Normal file
10
packages/docusaurus-babel/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"sourceMap": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__/**"]
|
||||
}
|
3
packages/docusaurus-bundler/.npmignore
Normal file
3
packages/docusaurus-bundler/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
.tsbuildinfo*
|
||||
tsconfig*
|
||||
__tests__
|
3
packages/docusaurus-bundler/README.md
Normal file
3
packages/docusaurus-bundler/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# `@docusaurus/bundler`
|
||||
|
||||
Docusaurus util package to abstract the current bundler.
|
53
packages/docusaurus-bundler/package.json
Normal file
53
packages/docusaurus-bundler/package.json
Normal 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"
|
||||
}
|
||||
}
|
87
packages/docusaurus-bundler/src/compiler.ts
Normal file
87
packages/docusaurus-bundler/src/compiler.ts
Normal 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!);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
19
packages/docusaurus-bundler/src/index.ts
Normal file
19
packages/docusaurus-bundler/src/index.ts
Normal 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';
|
|
@ -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));
|
||||
});
|
||||
});
|
54
packages/docusaurus-bundler/src/loaders/jsLoader.ts
Normal file
54
packages/docusaurus-bundler/src/loaders/jsLoader.ts
Normal 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}`);
|
||||
}
|
80
packages/docusaurus-bundler/src/loaders/styleLoader.ts
Normal file
80
packages/docusaurus-bundler/src/loaders/styleLoader.ts
Normal 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'),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
10
packages/docusaurus-bundler/tsconfig.json
Normal file
10
packages/docusaurus-bundler/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"sourceMap": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__/**"]
|
||||
}
|
|
@ -18,7 +18,6 @@ module: {
|
|||
{
|
||||
test: /\.mdx?$/,
|
||||
use: [
|
||||
'babel-loader',
|
||||
{
|
||||
loader: '@docusaurus/mdx-loader',
|
||||
options: {
|
||||
|
|
|
@ -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"
|
||||
|
|
15
packages/docusaurus-plugin-pwa/src/deps.d.ts
vendored
15
packages/docusaurus-plugin-pwa/src/deps.d.ts
vendored
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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})$`),
|
||||
),
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
);
|
||||
|
||||
|
|
6
packages/docusaurus-types/src/context.d.ts
vendored
6
packages/docusaurus-types/src/context.d.ts
vendored
|
@ -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 & {
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -97,6 +97,8 @@ export {md5Hash, simpleHash, docuHash} from './hashUtils';
|
|||
export {
|
||||
Globby,
|
||||
GlobExcludeDefault,
|
||||
safeGlobby,
|
||||
globTranslatableSourceFiles,
|
||||
createMatcher,
|
||||
createAbsoluteFilePathMatcher,
|
||||
} from './globUtils';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
9
packages/docusaurus/src/deps.d.ts
vendored
9
packages/docusaurus/src/deps.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
75
packages/docusaurus/src/webpack/utils/getHttpsConfig.ts
Normal file
75
packages/docusaurus/src/webpack/utils/getHttpsConfig.ts
Normal 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;
|
||||
}
|
|
@ -6,5 +6,5 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
presets: ['@docusaurus/babel/preset'],
|
||||
};
|
||||
|
|
|
@ -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
170
yarn.lock
|
@ -4383,7 +4383,7 @@ ansi-colors@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
|
||||
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
|
||||
|
||||
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
|
||||
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
|
||||
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
|
||||
|
@ -4620,15 +4620,15 @@ at-least-node@^1.0.0:
|
|||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
autoprefixer@^10.4.14, autoprefixer@^10.4.19:
|
||||
version "10.4.19"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f"
|
||||
integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==
|
||||
version "10.4.20"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
|
||||
integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==
|
||||
dependencies:
|
||||
browserslist "^4.23.0"
|
||||
caniuse-lite "^1.0.30001599"
|
||||
browserslist "^4.23.3"
|
||||
caniuse-lite "^1.0.30001646"
|
||||
fraction.js "^4.3.7"
|
||||
normalize-range "^0.1.2"
|
||||
picocolors "^1.0.0"
|
||||
picocolors "^1.0.1"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
available-typed-arrays@^1.0.5:
|
||||
|
@ -4959,7 +4959,7 @@ braces@^3.0.3, braces@~3.0.2:
|
|||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1:
|
||||
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3:
|
||||
version "4.23.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800"
|
||||
integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==
|
||||
|
@ -5186,7 +5186,7 @@ caniuse-api@^3.0.0:
|
|||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001646:
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646:
|
||||
version "1.0.30001651"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138"
|
||||
integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==
|
||||
|
@ -5765,10 +5765,10 @@ connect@3.7.0:
|
|||
parseurl "~1.3.3"
|
||||
utils-merge "1.0.1"
|
||||
|
||||
consola@^2.15.3:
|
||||
version "2.15.3"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
|
||||
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
|
||||
consola@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f"
|
||||
integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==
|
||||
|
||||
console-control-strings@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -6006,7 +6006,7 @@ cosmiconfig@^7.1.0:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
cosmiconfig@^8.1.3, cosmiconfig@^8.2.0:
|
||||
cosmiconfig@^8.1.3, cosmiconfig@^8.3.5:
|
||||
version "8.3.6"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
|
||||
integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==
|
||||
|
@ -6199,18 +6199,18 @@ css-functions-list@^3.1.0:
|
|||
integrity sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==
|
||||
|
||||
css-loader@^6.8.1:
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88"
|
||||
integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba"
|
||||
integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==
|
||||
dependencies:
|
||||
icss-utils "^5.1.0"
|
||||
postcss "^8.4.21"
|
||||
postcss-modules-extract-imports "^3.0.0"
|
||||
postcss-modules-local-by-default "^4.0.3"
|
||||
postcss-modules-scope "^3.0.0"
|
||||
postcss "^8.4.33"
|
||||
postcss-modules-extract-imports "^3.1.0"
|
||||
postcss-modules-local-by-default "^4.0.5"
|
||||
postcss-modules-scope "^3.2.0"
|
||||
postcss-modules-values "^4.0.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
semver "^7.3.8"
|
||||
semver "^7.5.4"
|
||||
|
||||
css-minimizer-webpack-plugin@^5.0.1:
|
||||
version "5.0.1"
|
||||
|
@ -7962,7 +7962,7 @@ feed@^4.2.2:
|
|||
dependencies:
|
||||
xml-js "^1.6.11"
|
||||
|
||||
figures@3.2.0, figures@^3.0.0:
|
||||
figures@3.2.0, figures@^3.0.0, figures@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
|
||||
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
|
||||
|
@ -10313,7 +10313,7 @@ jest@^29.7.0:
|
|||
import-local "^3.0.2"
|
||||
jest-cli "^29.7.0"
|
||||
|
||||
jiti@^1.18.2, jiti@^1.20.0:
|
||||
jiti@^1.20.0:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
|
||||
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
|
||||
|
@ -11124,6 +11124,13 @@ markdown-extensions@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4"
|
||||
integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==
|
||||
|
||||
markdown-table@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
|
||||
integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==
|
||||
dependencies:
|
||||
repeat-string "^1.0.0"
|
||||
|
||||
markdown-table@^3.0.0:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
|
||||
|
@ -11966,12 +11973,13 @@ min-indent@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||
|
||||
mini-css-extract-plugin@^2.7.6:
|
||||
version "2.7.6"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d"
|
||||
integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==
|
||||
mini-css-extract-plugin@^2.9.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz#4d184f12ce90582e983ccef0f6f9db637b4be758"
|
||||
integrity sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==
|
||||
dependencies:
|
||||
schema-utils "^4.0.0"
|
||||
tapable "^2.2.1"
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.1"
|
||||
|
@ -13297,10 +13305,10 @@ periscopic@^3.0.0:
|
|||
estree-walker "^3.0.0"
|
||||
is-reference "^3.0.0"
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
|
||||
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
|
||||
picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
||||
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
|
@ -13453,13 +13461,13 @@ postcss-discard-unused@^6.0.5:
|
|||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
postcss-loader@^7.3.3:
|
||||
version "7.3.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd"
|
||||
integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==
|
||||
version "7.3.4"
|
||||
resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209"
|
||||
integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==
|
||||
dependencies:
|
||||
cosmiconfig "^8.2.0"
|
||||
jiti "^1.18.2"
|
||||
semver "^7.3.8"
|
||||
cosmiconfig "^8.3.5"
|
||||
jiti "^1.20.0"
|
||||
semver "^7.5.4"
|
||||
|
||||
postcss-media-query-parser@^0.2.3:
|
||||
version "0.2.3"
|
||||
|
@ -13524,24 +13532,24 @@ postcss-minify-selectors@^6.0.4:
|
|||
dependencies:
|
||||
postcss-selector-parser "^6.0.16"
|
||||
|
||||
postcss-modules-extract-imports@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
|
||||
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
|
||||
postcss-modules-extract-imports@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002"
|
||||
integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==
|
||||
|
||||
postcss-modules-local-by-default@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
|
||||
integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==
|
||||
postcss-modules-local-by-default@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f"
|
||||
integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==
|
||||
dependencies:
|
||||
icss-utils "^5.0.0"
|
||||
postcss-selector-parser "^6.0.2"
|
||||
postcss-value-parser "^4.1.0"
|
||||
|
||||
postcss-modules-scope@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
|
||||
integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
|
||||
postcss-modules-scope@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5"
|
||||
integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==
|
||||
dependencies:
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
|
@ -13655,9 +13663,9 @@ postcss-safe-parser@^6.0.0:
|
|||
integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==
|
||||
|
||||
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
|
||||
version "6.0.16"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
|
||||
integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de"
|
||||
integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
@ -13694,14 +13702,14 @@ postcss-zindex@^6.0.2:
|
|||
resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-6.0.2.tgz#e498304b83a8b165755f53db40e2ea65a99b56e1"
|
||||
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
|
||||
|
||||
postcss@^8.2.x, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38:
|
||||
version "8.4.38"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||
postcss@^8.2.x, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.38:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.2.0"
|
||||
picocolors "^1.1.0"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
prebuild-install@^7.1.1:
|
||||
version "7.1.1"
|
||||
|
@ -14658,7 +14666,7 @@ renderkid@^3.0.0:
|
|||
lodash "^4.17.21"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
repeat-string@^1.6.1:
|
||||
repeat-string@^1.0.0, repeat-string@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
|
||||
|
@ -15075,9 +15083,9 @@ serialize-javascript@^4.0.0:
|
|||
randombytes "^2.1.0"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"
|
||||
integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
|
@ -15434,10 +15442,10 @@ sort-keys@^2.0.0:
|
|||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
source-map-js@^1.0.1, source-map-js@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||
source-map-js@^1.0.1, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-support@0.5.13:
|
||||
version "0.5.13"
|
||||
|
@ -15593,10 +15601,10 @@ statuses@2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
|
||||
|
||||
std-env@^3.0.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
|
||||
integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==
|
||||
std-env@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||
|
||||
streamx@^2.15.0:
|
||||
version "2.15.0"
|
||||
|
@ -15988,7 +15996,7 @@ table@^6.8.1:
|
|||
string-width "^4.2.3"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
tapable@2.2.1, tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
|
||||
tapable@2.2.1, tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
@ -17143,15 +17151,19 @@ webpack@^5, webpack@^5.88.1:
|
|||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
webpackbar@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570"
|
||||
integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-6.0.1.tgz#5ef57d3bf7ced8b19025477bc7496ea9d502076b"
|
||||
integrity sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
consola "^2.15.3"
|
||||
ansi-escapes "^4.3.2"
|
||||
chalk "^4.1.2"
|
||||
consola "^3.2.3"
|
||||
figures "^3.2.0"
|
||||
markdown-table "^2.0.0"
|
||||
pretty-time "^1.1.0"
|
||||
std-env "^3.0.1"
|
||||
std-env "^3.7.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
|
||||
version "0.7.4"
|
||||
|
|
Loading…
Add table
Reference in a new issue