feat(core): faster transpiler option - siteConfig.future.experimental_faster.swcJsLoader (#10435)

This commit is contained in:
Sébastien Lorber 2024-08-23 13:48:52 +02:00 committed by GitHub
parent 349a58453a
commit 418247ec87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1298 additions and 847 deletions

View file

@ -28,6 +28,7 @@
"build:website:deployPreview:build": "cross-env NETLIFY=true CONTEXT='deploy-preview' yarn workspace website build", "build:website:deployPreview:build": "cross-env NETLIFY=true CONTEXT='deploy-preview' yarn workspace website build",
"build:website:deployPreview": "yarn build:website:deployPreview:testWrap && yarn build:website:deployPreview:build", "build:website:deployPreview": "yarn build:website:deployPreview:testWrap && yarn build:website:deployPreview:build",
"build:website:fast": "yarn workspace website build:fast", "build:website:fast": "yarn workspace website build:fast",
"build:website:fast:rsdoctor": "yarn workspace website build:fast:rsdoctor",
"build:website:fast:profile": "yarn workspace website build:fast:profile", "build:website:fast:profile": "yarn workspace website build:fast:profile",
"build:website:en": "yarn workspace website build --locale en", "build:website:en": "yarn workspace website build --locale en",
"clear:website": "yarn workspace website clear", "clear:website": "yarn workspace website clear",
@ -70,11 +71,11 @@
"devDependencies": { "devDependencies": {
"@crowdin/cli": "^3.13.0", "@crowdin/cli": "^3.13.0",
"@prettier/plugin-xml": "^2.2.0", "@prettier/plugin-xml": "^2.2.0",
"@swc/core": "1.2.197", "@swc/core": "^1.7.14",
"@swc/jest": "^0.2.26", "@swc/jest": "^0.2.36",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.12",
"@types/lodash": "^4.14.197", "@types/lodash": "^4.14.197",
"@types/node": "^18.16.19", "@types/node": "^18.16.19",
"@types/prompts": "^2.4.4", "@types/prompts": "^2.4.4",
@ -99,9 +100,9 @@
"eslint-plugin-regexp": "^1.15.0", "eslint-plugin-regexp": "^1.15.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"image-size": "^1.0.2", "image-size": "^1.0.2",
"jest": "^29.6.1", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.6.1", "jest-environment-jsdom": "^29.7.0",
"jest-serializer-ansi-escapes": "^2.0.1", "jest-serializer-ansi-escapes": "^3.0.0",
"jest-serializer-react-helmet-async": "^1.0.21", "jest-serializer-react-helmet-async": "^1.0.21",
"lerna": "^6.6.2", "lerna": "^6.6.2",
"lerna-changelog": "^2.2.0", "lerna-changelog": "^2.2.0",

View file

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

View file

@ -0,0 +1,3 @@
# `@docusaurus/faster`
Docusaurus experimental package exposing new modern dependencies to make the build faster.

View file

@ -0,0 +1,31 @@
{
"name": "@docusaurus/faster",
"version": "3.5.2",
"description": "Docusaurus experimental package exposing new modern dependencies to make the build faster.",
"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-faster"
},
"license": "MIT",
"dependencies": {
"webpack": "^5.88.1",
"@swc/core": "^1.7.14",
"swc-loader": "^0.2.6"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"@docusaurus/types": "*"
}
}

View file

@ -0,0 +1,35 @@
/**
* 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 {RuleSetRule} from 'webpack';
export function getSwcJsLoaderFactory({
isServer,
}: {
isServer: boolean;
}): RuleSetRule {
return {
loader: require.resolve('swc-loader'),
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
target: 'es2017',
},
module: {
type: isServer ? 'commonjs' : 'es6',
},
},
};
}

View file

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

View file

@ -13,7 +13,10 @@ import {isMatch} from 'picomatch';
import commander from 'commander'; import commander from 'commander';
import webpack from 'webpack'; import webpack from 'webpack';
import {loadContext} from '@docusaurus/core/src/server/site'; import {loadContext} from '@docusaurus/core/src/server/site';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/configure'; import {
applyConfigureWebpack,
createConfigureWebpackUtils,
} from '@docusaurus/core/src/webpack/configure';
import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig';
import {posixPath} from '@docusaurus/utils'; import {posixPath} from '@docusaurus/utils';
import {normalizePluginOptions} from '@docusaurus/utils-validation'; import {normalizePluginOptions} from '@docusaurus/utils-validation';
@ -22,7 +25,7 @@ import pluginContentDocs from '../index';
import {toSidebarsProp} from '../props'; import {toSidebarsProp} from '../props';
import {DefaultSidebarItemsGenerator} from '../sidebars/generator'; import {DefaultSidebarItemsGenerator} from '../sidebars/generator';
import {DisabledSidebars} from '../sidebars'; import {DisabledSidebars} from '../sidebars';
import * as cliDocs from '../cli'; import cliDocs from '../cli';
import {validateOptions} from '../options'; import {validateOptions} from '../options';
import type {RouteConfig, Validate, Plugin} from '@docusaurus/types'; import type {RouteConfig, Validate, Plugin} from '@docusaurus/types';
@ -273,19 +276,23 @@ describe('simple website', () => {
const content = await plugin.loadContent?.(); const content = await plugin.loadContent?.();
const config = applyConfigureWebpack( const config = applyConfigureWebpack({
plugin.configureWebpack as NonNullable<Plugin['configureWebpack']>, configureWebpack: plugin.configureWebpack as NonNullable<
{ Plugin['configureWebpack']
>,
config: {
entry: './src/index.js', entry: './src/index.js',
output: { output: {
filename: 'main.js', filename: 'main.js',
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
}, },
}, },
false, isServer: false,
undefined, utils: createConfigureWebpackUtils({
siteConfig: {webpack: {jsLoader: 'babel'}},
}),
content, content,
); });
const errors = webpack.validate(config); const errors = webpack.validate(config);
expect(errors).toBeUndefined(); expect(errors).toBeUndefined();
}); });

View file

@ -53,7 +53,7 @@ async function createVersionedSidebarFile({
} }
// Tests depend on non-default export for mocking. // Tests depend on non-default export for mocking.
export async function cliDocsVersionCommand( async function cliDocsVersionCommand(
version: unknown, version: unknown,
{id: pluginId, path: docsPath, sidebarPath}: PluginOptions, {id: pluginId, path: docsPath, sidebarPath}: PluginOptions,
{siteDir, i18n}: LoadContext, {siteDir, i18n}: LoadContext,
@ -142,3 +142,17 @@ export async function cliDocsVersionCommand(
logger.success`name=${pluginIdLogPrefix}: version name=${version} created!`; logger.success`name=${pluginIdLogPrefix}: version name=${version} created!`;
} }
// TODO try to remove this workaround
// Why use a default export instead of named exports here?
// This is only to make Jest mocking happy
// After upgrading Jest/SWC we got this weird mocking error in extendCli tests
// "spyOn: Cannot redefine property cliDocsVersionCommand"
// I tried various workarounds, and it's the only one that worked :/
// See also:
// - https://pyk.sh/fixing-typeerror-cannot-redefine-property-x-error-in-jest-tests#heading-solution-2-using-barrel-imports
// - https://github.com/aelbore/esbuild-jest/issues/26
// - https://stackoverflow.com/questions/67872622/jest-spyon-not-working-on-index-file-cannot-redefine-property/69951703#69951703
export default {
cliDocsVersionCommand,
};

View file

@ -40,7 +40,7 @@ import {
readVersionsMetadata, readVersionsMetadata,
toFullVersion, toFullVersion,
} from './versions'; } from './versions';
import {cliDocsVersionCommand} from './cli'; import cliDocs from './cli';
import {VERSIONS_JSON_FILE} from './constants'; import {VERSIONS_JSON_FILE} from './constants';
import {toGlobalDataVersion} from './globalData'; import {toGlobalDataVersion} from './globalData';
import { import {
@ -134,7 +134,7 @@ export default async function pluginContentDocs(
.arguments('<version>') .arguments('<version>')
.description(commandDescription) .description(commandDescription)
.action((version: unknown) => .action((version: unknown) =>
cliDocsVersionCommand(version, options, context), cliDocs.cliDocsVersionCommand(version, options, context),
); );
}, },

View file

@ -7,7 +7,7 @@
import type {SiteStorage} from './context'; import type {SiteStorage} from './context';
import type {RuleSetRule} from 'webpack'; import type {RuleSetRule} from 'webpack';
import type {Required as RequireKeys, DeepPartial} from 'utility-types'; import type {DeepPartial, Overwrite} from 'utility-types';
import type {I18nConfig} from './i18n'; import type {I18nConfig} from './i18n';
import type {PluginConfig, PresetConfig, HtmlTagObject} from './plugin'; import type {PluginConfig, PresetConfig, HtmlTagObject} from './plugin';
@ -123,7 +123,13 @@ export type StorageConfig = {
namespace: boolean | string; namespace: boolean | string;
}; };
export type FasterConfig = {
swcJsLoader: boolean;
};
export type FutureConfig = { export type FutureConfig = {
experimental_faster: FasterConfig;
experimental_storage: StorageConfig; experimental_storage: StorageConfig;
/** /**
@ -416,6 +422,9 @@ export type DocusaurusConfig = {
* Babel loader and preset; otherwise, you can provide your custom Webpack * Babel loader and preset; otherwise, you can provide your custom Webpack
* rule set. * rule set.
*/ */
// TODO Docusaurus v4
// Use an object type ({isServer}) so that it conforms to jsLoaderFactory
// Eventually deprecate this if swc loader becomes stable?
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule); jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule);
}; };
/** Markdown-related options. */ /** Markdown-related options. */
@ -423,11 +432,21 @@ export type DocusaurusConfig = {
}; };
/** /**
* Docusaurus config, as provided by the user (partial/unnormalized). This type * Docusaurus config, as provided by the user (partial/un-normalized). This type
* is used to provide type-safety / IDE auto-complete on the config file. * is used to provide type-safety / IDE auto-complete on the config file.
* @see https://docusaurus.io/docs/typescript-support * @see https://docusaurus.io/docs/typescript-support
*/ */
export type Config = RequireKeys< export type Config = Overwrite<
DeepPartial<DocusaurusConfig>, DeepPartial<DocusaurusConfig>,
'title' | 'url' | 'baseUrl' {
title: DocusaurusConfig['title'];
url: DocusaurusConfig['url'];
baseUrl: DocusaurusConfig['baseUrl'];
future?: Overwrite<
DeepPartial<FutureConfig>,
{
experimental_faster?: boolean | FasterConfig;
}
>;
}
>; >;

View file

@ -60,7 +60,9 @@ export type ConfigureWebpackUtils = {
) => RuleSetRule[]; ) => RuleSetRule[];
getJSLoader: (options: { getJSLoader: (options: {
isServer: boolean; isServer: boolean;
babelOptions?: {[key: string]: unknown}; // TODO Docusaurus v4 remove?
// not ideal because JS Loader might not use Babel...
babelOptions?: string | {[key: string]: unknown};
}) => RuleSetRule; }) => RuleSetRule;
}; };
@ -122,8 +124,9 @@ export type Plugin<Content = unknown> = {
head: {[location: string]: HelmetServerState}; head: {[location: string]: HelmetServerState};
}, },
) => Promise<void> | void; ) => Promise<void> | void;
// TODO refactor the configureWebpack API surface: use an object instead of // TODO Docusaurus v4 ?
// multiple params (requires breaking change) // refactor the configureWebpack API surface: use an object instead of
// multiple params (requires breaking change)
configureWebpack?: ( configureWebpack?: (
config: WebpackConfiguration, config: WebpackConfiguration,
isServer: boolean, isServer: boolean,

View file

@ -118,10 +118,16 @@
"tree-node-cli": "^1.6.0" "tree-node-cli": "^1.6.0"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/faster": "3.5.2",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0" "react-dom": "^18.0.0"
}, },
"peerDependenciesMeta": {
"@docusaurus/faster": {
"optional": true
}
},
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"
} }

View file

@ -15,7 +15,10 @@ import {handleBrokenLinks} from '../server/brokenLinks';
import {createBuildClientConfig} from '../webpack/client'; import {createBuildClientConfig} from '../webpack/client';
import createServerConfig from '../webpack/server'; import createServerConfig from '../webpack/server';
import {executePluginsConfigureWebpack} from '../webpack/configure'; import {
createConfigureWebpackUtils,
executePluginsConfigureWebpack,
} from '../webpack/configure';
import {compile} from '../webpack/utils'; import {compile} from '../webpack/utils';
import {PerfLogger} from '../utils'; import {PerfLogger} from '../utils';
@ -338,7 +341,9 @@ async function getBuildClientConfig({
plugins, plugins,
config, config,
isServer: false, isServer: false,
jsLoader: props.siteConfig.webpack?.jsLoader, utils: await createConfigureWebpackUtils({
siteConfig: props.siteConfig,
}),
}); });
return {clientConfig: config, clientManifestPath: result.clientManifestPath}; return {clientConfig: config, clientManifestPath: result.clientManifestPath};
} }
@ -353,7 +358,9 @@ async function getBuildServerConfig({props}: {props: Props}) {
plugins, plugins,
config, config,
isServer: true, isServer: true,
jsLoader: props.siteConfig.webpack?.jsLoader, utils: await createConfigureWebpackUtils({
siteConfig: props.siteConfig,
}),
}); });
return {serverConfig: config, serverBundlePath: result.serverBundlePath}; return {serverConfig: config, serverBundlePath: result.serverBundlePath};
} }

View file

@ -17,7 +17,10 @@ import {
getHttpsConfig, getHttpsConfig,
printStatsWarnings, printStatsWarnings,
} from '../../webpack/utils'; } from '../../webpack/utils';
import {executePluginsConfigureWebpack} from '../../webpack/configure'; import {
createConfigureWebpackUtils,
executePluginsConfigureWebpack,
} from '../../webpack/configure';
import {createStartClientConfig} from '../../webpack/client'; import {createStartClientConfig} from '../../webpack/client';
import type {StartCLIOptions} from './start'; import type {StartCLIOptions} from './start';
import type {Props} from '@docusaurus/types'; import type {Props} from '@docusaurus/types';
@ -139,7 +142,7 @@ async function getStartClientConfig({
plugins, plugins,
config, config,
isServer: false, isServer: false,
jsLoader: siteConfig.webpack?.jsLoader, utils: await createConfigureWebpackUtils({siteConfig}),
}); });
return config; return config;
} }

View file

@ -0,0 +1,30 @@
/**
* 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 {ConfigureWebpackUtils} from '@docusaurus/types';
async function importFaster() {
return import('@docusaurus/faster');
}
async function ensureFaster() {
try {
return await importFaster();
} catch (error) {
throw new Error(
'Your Docusaurus site need to add the @docusaurus/faster package as a dependency.',
{cause: error},
);
}
}
export async function getSwcJsLoaderFactory(): Promise<
ConfigureWebpackUtils['getJSLoader']
> {
const faster = await ensureFaster();
return faster.getSwcJsLoaderFactory;
}

View file

@ -8,6 +8,9 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -69,6 +72,9 @@ exports[`loadSiteConfig website with ts + js config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -130,6 +136,9 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -191,6 +200,9 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -252,6 +264,9 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -313,6 +328,9 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -374,6 +392,9 @@ exports[`loadSiteConfig website with valid async config 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -437,6 +458,9 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -500,6 +524,9 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,
@ -566,6 +593,9 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
"customFields": {}, "customFields": {},
"favicon": "img/docusaurus.ico", "favicon": "img/docusaurus.ico",
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,

View file

@ -78,6 +78,9 @@ exports[`load loads props for site with custom i18n path 1`] = `
"clientModules": [], "clientModules": [],
"customFields": {}, "customFields": {},
"future": { "future": {
"experimental_faster": {
"swcJsLoader": false,
},
"experimental_router": "browser", "experimental_router": "browser",
"experimental_storage": { "experimental_storage": {
"namespace": false, "namespace": false,

View file

@ -8,10 +8,17 @@
import { import {
ConfigSchema, ConfigSchema,
DEFAULT_CONFIG, DEFAULT_CONFIG,
DEFAULT_FASTER_CONFIG,
DEFAULT_FASTER_CONFIG_TRUE,
DEFAULT_FUTURE_CONFIG,
DEFAULT_STORAGE_CONFIG, DEFAULT_STORAGE_CONFIG,
validateConfig, validateConfig,
} from '../configValidation'; } from '../configValidation';
import type {StorageConfig} from '@docusaurus/types/src/config'; import type {
FasterConfig,
FutureConfig,
StorageConfig,
} from '@docusaurus/types/src/config';
import type {Config, DocusaurusConfig, PluginConfig} from '@docusaurus/types'; import type {Config, DocusaurusConfig, PluginConfig} from '@docusaurus/types';
import type {DeepPartial} from 'utility-types'; import type {DeepPartial} from 'utility-types';
@ -38,6 +45,9 @@ describe('normalizeConfig', () => {
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,
...baseConfig, ...baseConfig,
future: { future: {
experimental_faster: {
swcJsLoader: true,
},
experimental_storage: { experimental_storage: {
type: 'sessionStorage', type: 'sessionStorage',
namespace: true, namespace: true,
@ -350,7 +360,7 @@ describe('markdown', () => {
}); });
it('accepts valid markdown object', () => { it('accepts valid markdown object', () => {
const markdown: DocusaurusConfig['markdown'] = { const markdown: Config['markdown'] = {
format: 'md', format: 'md',
mermaid: true, mermaid: true,
parseFrontMatter: async (params) => parseFrontMatter: async (params) =>
@ -378,7 +388,7 @@ describe('markdown', () => {
}); });
it('accepts partial markdown object', () => { it('accepts partial markdown object', () => {
const markdown: DeepPartial<DocusaurusConfig['markdown']> = { const markdown: DeepPartial<Config['markdown']> = {
mdx1Compat: { mdx1Compat: {
admonitions: true, admonitions: true,
headingIds: false, headingIds: false,
@ -705,12 +715,18 @@ describe('presets', () => {
}); });
describe('future', () => { describe('future', () => {
function futureContaining(future: Partial<FutureConfig>) {
return expect.objectContaining({
future: expect.objectContaining(future),
});
}
it('accepts future - undefined', () => { it('accepts future - undefined', () => {
expect( expect(
normalizeConfig({ normalizeConfig({
future: undefined, future: undefined,
}), }),
).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
}); });
it('accepts future - empty', () => { it('accepts future - empty', () => {
@ -718,11 +734,14 @@ describe('future', () => {
normalizeConfig({ normalizeConfig({
future: {}, future: {},
}), }),
).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
}); });
it('accepts future', () => { it('accepts future - full', () => {
const future: DocusaurusConfig['future'] = { const future: DocusaurusConfig['future'] = {
experimental_faster: {
swcJsLoader: true,
},
experimental_storage: { experimental_storage: {
type: 'sessionStorage', type: 'sessionStorage',
namespace: 'myNamespace', namespace: 'myNamespace',
@ -733,11 +752,11 @@ describe('future', () => {
normalizeConfig({ normalizeConfig({
future, future,
}), }),
).toEqual(expect.objectContaining({future})); ).toEqual(futureContaining(future));
}); });
it('rejects future - unknown key', () => { it('rejects future - unknown key', () => {
const future: DocusaurusConfig['future'] = { const future: Config['future'] = {
// @ts-expect-error: invalid // @ts-expect-error: invalid
doesNotExistKey: { doesNotExistKey: {
type: 'sessionStorage', type: 'sessionStorage',
@ -763,11 +782,7 @@ describe('future', () => {
experimental_router: undefined, experimental_router: undefined,
}, },
}), }),
).toEqual( ).toEqual(futureContaining({experimental_router: 'browser'}));
expect.objectContaining({
future: expect.objectContaining({experimental_router: 'browser'}),
}),
);
}); });
it('accepts router - hash', () => { it('accepts router - hash', () => {
@ -777,11 +792,7 @@ describe('future', () => {
experimental_router: 'hash', experimental_router: 'hash',
}, },
}), }),
).toEqual( ).toEqual(futureContaining({experimental_router: 'hash'}));
expect.objectContaining({
future: expect.objectContaining({experimental_router: 'hash'}),
}),
);
}); });
it('accepts router - browser', () => { it('accepts router - browser', () => {
@ -791,17 +802,12 @@ describe('future', () => {
experimental_router: 'browser', experimental_router: 'browser',
}, },
}), }),
).toEqual( ).toEqual(futureContaining({experimental_router: 'browser'}));
expect.objectContaining({
future: expect.objectContaining({experimental_router: 'browser'}),
}),
);
}); });
it('rejects router - invalid enum value', () => { it('rejects router - invalid enum value', () => {
// @ts-expect-error: invalid // @ts-expect-error: invalid
const router: DocusaurusConfig['future']['experimental_router'] = const router: Config['future']['experimental_router'] = 'badRouter';
'badRouter';
expect(() => expect(() =>
normalizeConfig({ normalizeConfig({
future: { future: {
@ -816,7 +822,7 @@ describe('future', () => {
it('rejects router - null', () => { it('rejects router - null', () => {
// @ts-expect-error: bad value // @ts-expect-error: bad value
const router: DocusaurusConfig['future']['experimental_router'] = null; const router: Config['future']['experimental_router'] = null;
expect(() => expect(() =>
normalizeConfig({ normalizeConfig({
future: { future: {
@ -832,7 +838,7 @@ describe('future', () => {
it('rejects router - number', () => { it('rejects router - number', () => {
// @ts-expect-error: invalid // @ts-expect-error: invalid
const router: DocusaurusConfig['future']['experimental_router'] = 42; const router: Config['future']['experimental_router'] = 42;
expect(() => expect(() =>
normalizeConfig({ normalizeConfig({
future: { future: {
@ -848,6 +854,12 @@ describe('future', () => {
}); });
describe('storage', () => { describe('storage', () => {
function storageContaining(storage: Partial<StorageConfig>) {
return futureContaining({
experimental_storage: expect.objectContaining(storage),
});
}
it('accepts storage - undefined', () => { it('accepts storage - undefined', () => {
expect( expect(
normalizeConfig({ normalizeConfig({
@ -855,7 +867,7 @@ describe('future', () => {
experimental_storage: undefined, experimental_storage: undefined,
}, },
}), }),
).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
}); });
it('accepts storage - empty', () => { it('accepts storage - empty', () => {
@ -863,7 +875,7 @@ describe('future', () => {
normalizeConfig({ normalizeConfig({
future: {experimental_storage: {}}, future: {experimental_storage: {}},
}), }),
).toEqual(expect.objectContaining({future: DEFAULT_CONFIG.future})); ).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
}); });
it('accepts storage - full', () => { it('accepts storage - full', () => {
@ -877,13 +889,7 @@ describe('future', () => {
experimental_storage: storage, experimental_storage: storage,
}, },
}), }),
).toEqual( ).toEqual(storageContaining(storage));
expect.objectContaining({
future: expect.objectContaining({
experimental_storage: storage,
}),
}),
);
}); });
it('rejects storage - boolean', () => { it('rejects storage - boolean', () => {
@ -928,13 +934,9 @@ describe('future', () => {
}, },
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ storageContaining({
future: expect.objectContaining({ ...DEFAULT_STORAGE_CONFIG,
experimental_storage: { ...storage,
...DEFAULT_STORAGE_CONFIG,
...storage,
},
}),
}), }),
); );
}); });
@ -949,16 +951,7 @@ describe('future', () => {
experimental_storage: storage, experimental_storage: storage,
}, },
}), }),
).toEqual( ).toEqual(storageContaining({type: 'localStorage'}));
expect.objectContaining({
future: expect.objectContaining({
experimental_storage: {
...DEFAULT_STORAGE_CONFIG,
type: 'localStorage',
},
}),
}),
);
}); });
it('rejects type - null', () => { it('rejects type - null', () => {
@ -971,10 +964,10 @@ describe('future', () => {
}, },
}), }),
).toThrowErrorMatchingInlineSnapshot(` ).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_storage.type" must be one of [localStorage, sessionStorage] ""future.experimental_storage.type" must be one of [localStorage, sessionStorage]
"future.experimental_storage.type" must be a string "future.experimental_storage.type" must be a string
" "
`); `);
}); });
it('rejects type - number', () => { it('rejects type - number', () => {
@ -987,10 +980,10 @@ describe('future', () => {
}, },
}), }),
).toThrowErrorMatchingInlineSnapshot(` ).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_storage.type" must be one of [localStorage, sessionStorage] ""future.experimental_storage.type" must be one of [localStorage, sessionStorage]
"future.experimental_storage.type" must be a string "future.experimental_storage.type" must be a string
" "
`); `);
}); });
it('rejects type - invalid enum value', () => { it('rejects type - invalid enum value', () => {
@ -1003,9 +996,9 @@ describe('future', () => {
}, },
}), }),
).toThrowErrorMatchingInlineSnapshot(` ).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_storage.type" must be one of [localStorage, sessionStorage] ""future.experimental_storage.type" must be one of [localStorage, sessionStorage]
" "
`); `);
}); });
}); });
@ -1020,16 +1013,7 @@ describe('future', () => {
experimental_storage: storage, experimental_storage: storage,
}, },
}), }),
).toEqual( ).toEqual(storageContaining(storage));
expect.objectContaining({
future: expect.objectContaining({
experimental_storage: {
...DEFAULT_STORAGE_CONFIG,
...storage,
},
}),
}),
);
}); });
it('accepts namespace - string', () => { it('accepts namespace - string', () => {
@ -1042,16 +1026,7 @@ describe('future', () => {
experimental_storage: storage, experimental_storage: storage,
}, },
}), }),
).toEqual( ).toEqual(storageContaining(storage));
expect.objectContaining({
future: expect.objectContaining({
experimental_storage: {
...DEFAULT_STORAGE_CONFIG,
...storage,
},
}),
}),
);
}); });
it('rejects namespace - null', () => { it('rejects namespace - null', () => {
@ -1064,9 +1039,9 @@ describe('future', () => {
}, },
}), }),
).toThrowErrorMatchingInlineSnapshot(` ).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_storage.namespace" must be one of [string, boolean] ""future.experimental_storage.namespace" must be one of [string, boolean]
" "
`); `);
}); });
it('rejects namespace - number', () => { it('rejects namespace - number', () => {
@ -1079,9 +1054,150 @@ describe('future', () => {
}, },
}), }),
).toThrowErrorMatchingInlineSnapshot(` ).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_storage.namespace" must be one of [string, boolean] ""future.experimental_storage.namespace" must be one of [string, boolean]
"
`);
});
});
});
describe('faster', () => {
function fasterContaining(faster: Partial<FasterConfig>) {
return futureContaining({
experimental_faster: expect.objectContaining(faster),
});
}
it('accepts faster - undefined', () => {
expect(
normalizeConfig({
future: {
experimental_faster: undefined,
},
}),
).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
});
it('accepts faster - empty', () => {
expect(
normalizeConfig({
future: {experimental_faster: {}},
}),
).toEqual(futureContaining(DEFAULT_FUTURE_CONFIG));
});
it('accepts faster - full', () => {
const faster: FasterConfig = {
swcJsLoader: true,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining(faster));
});
it('accepts faster - false', () => {
expect(
normalizeConfig({
future: {experimental_faster: false},
}),
).toEqual(fasterContaining(DEFAULT_FASTER_CONFIG));
});
it('accepts faster - true', () => {
expect(
normalizeConfig({
future: {experimental_faster: true},
}),
).toEqual(fasterContaining(DEFAULT_FASTER_CONFIG_TRUE));
});
it('rejects faster - number', () => {
// @ts-expect-error: invalid
const faster: Partial<FasterConfig> = 42;
expect(() =>
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_faster" must be one of [object, boolean]
" "
`); `);
});
describe('swcJsLoader', () => {
it('accepts - undefined', () => {
const faster: Partial<FasterConfig> = {
swcJsLoader: undefined,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsLoader: false}));
});
it('accepts - true', () => {
const faster: Partial<FasterConfig> = {
swcJsLoader: true,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsLoader: true}));
});
it('accepts - false', () => {
const faster: Partial<FasterConfig> = {
swcJsLoader: false,
};
expect(
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toEqual(fasterContaining({swcJsLoader: false}));
});
it('rejects - null', () => {
// @ts-expect-error: invalid
const faster: Partial<FasterConfig> = {swcJsLoader: 42};
expect(() =>
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_faster.swcJsLoader" must be a boolean
"
`);
});
it('rejects - number', () => {
// @ts-expect-error: invalid
const faster: Partial<FasterConfig> = {swcJsLoader: 42};
expect(() =>
normalizeConfig({
future: {
experimental_faster: faster,
},
}),
).toThrowErrorMatchingInlineSnapshot(`
""future.experimental_faster.swcJsLoader" must be a boolean
"
`);
}); });
}); });
}); });

View file

@ -16,7 +16,11 @@ import {
addLeadingSlash, addLeadingSlash,
removeTrailingSlash, removeTrailingSlash,
} from '@docusaurus/utils-common'; } from '@docusaurus/utils-common';
import type {FutureConfig, StorageConfig} from '@docusaurus/types/src/config'; import type {
FasterConfig,
FutureConfig,
StorageConfig,
} from '@docusaurus/types/src/config';
import type { import type {
DocusaurusConfig, DocusaurusConfig,
I18nConfig, I18nConfig,
@ -37,7 +41,17 @@ export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
namespace: false, namespace: false,
}; };
export const DEFAULT_FASTER_CONFIG: FasterConfig = {
swcJsLoader: false,
};
// When using the "faster: true" shortcut
export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = {
swcJsLoader: true,
};
export const DEFAULT_FUTURE_CONFIG: FutureConfig = { export const DEFAULT_FUTURE_CONFIG: FutureConfig = {
experimental_faster: DEFAULT_FASTER_CONFIG,
experimental_storage: DEFAULT_STORAGE_CONFIG, experimental_storage: DEFAULT_STORAGE_CONFIG,
experimental_router: 'browser', experimental_router: 'browser',
}; };
@ -194,6 +208,20 @@ const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
.optional() .optional()
.default(DEFAULT_I18N_CONFIG); .default(DEFAULT_I18N_CONFIG);
const FASTER_CONFIG_SCHEMA = Joi.alternatives()
.try(
Joi.object<FasterConfig>({
swcJsLoader: Joi.boolean().default(DEFAULT_FASTER_CONFIG.swcJsLoader),
}),
Joi.boolean()
.required()
.custom((bool) =>
bool ? DEFAULT_FASTER_CONFIG_TRUE : DEFAULT_FASTER_CONFIG,
),
)
.optional()
.default(DEFAULT_FASTER_CONFIG);
const STORAGE_CONFIG_SCHEMA = Joi.object({ const STORAGE_CONFIG_SCHEMA = Joi.object({
type: Joi.string() type: Joi.string()
.equal('localStorage', 'sessionStorage') .equal('localStorage', 'sessionStorage')
@ -206,6 +234,7 @@ const STORAGE_CONFIG_SCHEMA = Joi.object({
.default(DEFAULT_STORAGE_CONFIG); .default(DEFAULT_STORAGE_CONFIG);
const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({ const FUTURE_CONFIG_SCHEMA = Joi.object<FutureConfig>({
experimental_faster: FASTER_CONFIG_SCHEMA,
experimental_storage: STORAGE_CONFIG_SCHEMA, experimental_storage: STORAGE_CONFIG_SCHEMA,
experimental_router: Joi.string() experimental_router: Joi.string()
.equal('browser', 'hash') .equal('browser', 'hash')

View file

@ -11,6 +11,7 @@ import _ from 'lodash';
import * as utils from '@docusaurus/utils/lib/webpackUtils'; import * as utils from '@docusaurus/utils/lib/webpackUtils';
import {posixPath} from '@docusaurus/utils'; import {posixPath} from '@docusaurus/utils';
import {excludeJS, clientDir, createBaseConfig} from '../base'; import {excludeJS, clientDir, createBaseConfig} from '../base';
import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation';
import type {Props} from '@docusaurus/types'; import type {Props} from '@docusaurus/types';
describe('babel transpilation exclude logic', () => { describe('babel transpilation exclude logic', () => {
@ -66,7 +67,7 @@ describe('base webpack config', () => {
const props = { const props = {
outDir: '', outDir: '',
siteDir: path.resolve(__dirname, '__fixtures__', 'base_test_site'), siteDir: path.resolve(__dirname, '__fixtures__', 'base_test_site'),
siteConfig: {staticDirectories: ['static'], future: {}}, siteConfig: {staticDirectories: ['static'], future: DEFAULT_FUTURE_CONFIG},
baseUrl: '', baseUrl: '',
generatedFilesDir: '', generatedFilesDir: '',
routesPaths: [''], routesPaths: [''],

View file

@ -12,10 +12,17 @@ import {
applyConfigureWebpack, applyConfigureWebpack,
applyConfigurePostCss, applyConfigurePostCss,
executePluginsConfigureWebpack, executePluginsConfigureWebpack,
createConfigureWebpackUtils,
} from '../configure'; } from '../configure';
import type {Configuration} from 'webpack'; import type {Configuration} from 'webpack';
import type {LoadedPlugin, Plugin} from '@docusaurus/types'; import type {LoadedPlugin, Plugin} from '@docusaurus/types';
const utils = createConfigureWebpackUtils({
siteConfig: {webpack: {jsLoader: 'babel'}},
});
const isServer = false;
describe('extending generated webpack config', () => { describe('extending generated webpack config', () => {
it('direct mutation on generated webpack config object', async () => { it('direct mutation on generated webpack config object', async () => {
// Fake generated webpack config // Fake generated webpack config
@ -29,9 +36,9 @@ describe('extending generated webpack config', () => {
// @ts-expect-error: Testing an edge-case that we did not write types for // @ts-expect-error: Testing an edge-case that we did not write types for
const configureWebpack: NonNullable<Plugin['configureWebpack']> = ( const configureWebpack: NonNullable<Plugin['configureWebpack']> = (
generatedConfig, generatedConfig,
isServer, isServerParam,
) => { ) => {
if (!isServer) { if (!isServerParam) {
generatedConfig.entry = 'entry.js'; generatedConfig.entry = 'entry.js';
generatedConfig.output = { generatedConfig.output = {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
@ -41,8 +48,14 @@ describe('extending generated webpack config', () => {
// Implicitly returning undefined to test null-safety // Implicitly returning undefined to test null-safety
}; };
config = applyConfigureWebpack(configureWebpack, config, false, undefined, { config = applyConfigureWebpack({
content: 42, configureWebpack,
config,
isServer,
utils,
content: {
content: 42,
},
}); });
expect(config).toEqual({ expect(config).toEqual({
entry: 'entry.js', entry: 'entry.js',
@ -71,8 +84,14 @@ describe('extending generated webpack config', () => {
}, },
}); });
config = applyConfigureWebpack(configureWebpack, config, false, undefined, { config = applyConfigureWebpack({
content: 42, configureWebpack,
config,
isServer,
utils,
content: {
content: 42,
},
}); });
expect(config).toEqual({ expect(config).toEqual({
entry: 'entry.js', entry: 'entry.js',
@ -103,39 +122,41 @@ describe('extending generated webpack config', () => {
mergeStrategy, mergeStrategy,
}); });
const defaultStrategyMergeConfig = applyConfigureWebpack( const defaultStrategyMergeConfig = applyConfigureWebpack({
createConfigureWebpack(), configureWebpack: createConfigureWebpack(),
config, config,
false, isServer,
undefined, utils,
{content: 42}, content: {content: 42},
); });
expect(defaultStrategyMergeConfig).toEqual({ expect(defaultStrategyMergeConfig).toEqual({
module: { module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
}, },
}); });
const prependRulesStrategyConfig = applyConfigureWebpack( const prependRulesStrategyConfig = applyConfigureWebpack({
createConfigureWebpack({'module.rules': 'prepend'}), configureWebpack: createConfigureWebpack({'module.rules': 'prepend'}),
config, config,
false, isServer,
undefined, utils,
{content: 42}, content: {content: 42},
); });
expect(prependRulesStrategyConfig).toEqual({ expect(prependRulesStrategyConfig).toEqual({
module: { module: {
rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}], rules: [{use: 'zzz'}, {use: 'xxx'}, {use: 'yyy'}],
}, },
}); });
const uselessMergeStrategyConfig = applyConfigureWebpack( const uselessMergeStrategyConfig = applyConfigureWebpack({
createConfigureWebpack({uselessAttributeName: 'append'}), configureWebpack: createConfigureWebpack({
uselessAttributeName: 'append',
}),
config, config,
false, isServer,
undefined, utils,
{content: 42}, content: {content: 42},
); });
expect(uselessMergeStrategyConfig).toEqual({ expect(uselessMergeStrategyConfig).toEqual({
module: { module: {
rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}], rules: [{use: 'xxx'}, {use: 'yyy'}, {use: 'zzz'}],
@ -275,8 +296,8 @@ describe('executePluginsConfigureWebpack', () => {
it('can merge Webpack aliases of 2 plugins into base config', () => { it('can merge Webpack aliases of 2 plugins into base config', () => {
const config = executePluginsConfigureWebpack({ const config = executePluginsConfigureWebpack({
config: {resolve: {alias: {'initial-alias': 'initial-alias-value'}}}, config: {resolve: {alias: {'initial-alias': 'initial-alias-value'}}},
isServer: false, isServer,
jsLoader: 'babel', utils,
plugins: [ plugins: [
fakePlugin({ fakePlugin({
configureWebpack: () => { configureWebpack: () => {
@ -310,8 +331,8 @@ describe('executePluginsConfigureWebpack', () => {
it('can configurePostCSS() for all loaders added through configureWebpack()', () => { it('can configurePostCSS() for all loaders added through configureWebpack()', () => {
const config = executePluginsConfigureWebpack({ const config = executePluginsConfigureWebpack({
config: {}, config: {},
isServer: false, isServer,
jsLoader: 'babel', utils,
plugins: [ plugins: [
fakePlugin({ fakePlugin({
configurePostCss: (postCssOptions) => { configurePostCss: (postCssOptions) => {

View file

@ -6,40 +6,76 @@
*/ */
import path from 'path'; import path from 'path';
import {getCustomizableJSLoader, getHttpsConfig} from '../utils'; import {createJsLoaderFactory, getHttpsConfig} from '../utils';
import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation';
import type {RuleSetRule} from 'webpack'; import type {RuleSetRule} from 'webpack';
describe('customize JS loader', () => { describe('customize JS loader', () => {
it('getCustomizableJSLoader defaults to babel loader', () => { function testJsLoaderFactory(
expect(getCustomizableJSLoader()({isServer: true}).loader).toBe( 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'), require.resolve('babel-loader'),
); );
expect(getCustomizableJSLoader()({isServer: false}).loader).toBe( expect(createJsLoader({isServer: false}).loader).toBe(
require.resolve('babel-loader'), require.resolve('babel-loader'),
); );
}); });
it('getCustomizableJSLoader accepts loaders with preset', () => { it('createJsLoaderFactory accepts loaders with preset', async () => {
expect(getCustomizableJSLoader('babel')({isServer: true}).loader).toBe( const createJsLoader = await testJsLoaderFactory({
require.resolve('babel-loader'), webpack: {jsLoader: 'babel'},
); });
expect(getCustomizableJSLoader('babel')({isServer: false}).loader).toBe(
require.resolve('babel-loader'), expect(
); createJsLoader({
isServer: true,
}).loader,
).toBe(require.resolve('babel-loader'));
expect(
createJsLoader({
isServer: false,
}).loader,
).toBe(require.resolve('babel-loader'));
}); });
it('getCustomizableJSLoader allows customization', () => { it('createJsLoaderFactory allows customization', async () => {
const customJSLoader = (isServer: boolean): RuleSetRule => ({ const customJSLoader = (isServer: boolean): RuleSetRule => ({
loader: 'my-fast-js-loader', loader: 'my-fast-js-loader',
options: String(isServer), options: String(isServer),
}); });
expect(getCustomizableJSLoader(customJSLoader)({isServer: true})).toEqual( const createJsLoader = await testJsLoaderFactory({
customJSLoader(true), webpack: {jsLoader: customJSLoader},
); });
expect(getCustomizableJSLoader(customJSLoader)({isServer: false})).toEqual(
customJSLoader(false), expect(
); createJsLoader({
isServer: true,
}),
).toEqual(customJSLoader(true));
expect(
createJsLoader({
isServer: false,
}),
).toEqual(customJSLoader(false));
}); });
}); });

View file

@ -10,7 +10,7 @@ import path from 'path';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils'; import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils';
import { import {
getCustomizableJSLoader, createJsLoaderFactory,
getStyleLoaders, getStyleLoaders,
getCustomBabelConfigFilePath, getCustomBabelConfigFilePath,
} from './utils'; } from './utils';
@ -84,6 +84,8 @@ export async function createBaseConfig({
const themeAliases = await loadThemeAliases({siteDir, plugins}); const themeAliases = await loadThemeAliases({siteDir, plugins});
const createJsLoader = await createJsLoaderFactory({siteConfig});
return { return {
mode, mode,
name, name,
@ -211,7 +213,7 @@ export async function createBaseConfig({
test: /\.[jt]sx?$/i, test: /\.[jt]sx?$/i,
exclude: excludeJS, exclude: excludeJS,
use: [ use: [
getCustomizableJSLoader(siteConfig.webpack?.jsLoader)({ createJsLoader({
isServer, isServer,
babelOptions: await getCustomBabelConfigFilePath(siteDir), babelOptions: await getCustomBabelConfigFilePath(siteDir),
}), }),

View file

@ -10,7 +10,7 @@ import {
customizeArray, customizeArray,
customizeObject, customizeObject,
} from 'webpack-merge'; } from 'webpack-merge';
import {getCustomizableJSLoader, getStyleLoaders} from './utils'; import {createJsLoaderFactory, getStyleLoaders} from './utils';
import type {Configuration, RuleSetRule} from 'webpack'; import type {Configuration, RuleSetRule} from 'webpack';
import type { import type {
@ -20,27 +20,43 @@ import type {
LoadedPlugin, LoadedPlugin,
} from '@docusaurus/types'; } from '@docusaurus/types';
/**
* Creates convenient utils to inject into the configureWebpack() lifecycle
* @param config the Docusaurus config
*/
export async function createConfigureWebpackUtils({
siteConfig,
}: {
siteConfig: Parameters<typeof createJsLoaderFactory>[0]['siteConfig'];
}): Promise<ConfigureWebpackUtils> {
return {
getStyleLoaders,
getJSLoader: await createJsLoaderFactory({siteConfig}),
};
}
/** /**
* Helper function to modify webpack config * Helper function to modify webpack config
* @param configureWebpack a webpack config or a function to modify config * @param configureWebpack a webpack config or a function to modify config
* @param config initial webpack config * @param config initial webpack config
* @param isServer indicates if this is a server webpack configuration * @param isServer indicates if this is a server webpack configuration
* @param jsLoader custom js loader config * @param utils the <code>ConfigureWebpackUtils</code> utils to inject into the configureWebpack() lifecycle
* @param content content loaded by the plugin * @param content content loaded by the plugin
* @returns final/ modified webpack config * @returns final/ modified webpack config
*/ */
export function applyConfigureWebpack( export function applyConfigureWebpack({
configureWebpack: NonNullable<Plugin['configureWebpack']>, configureWebpack,
config: Configuration, config,
isServer: boolean, isServer,
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined, utils,
content: unknown, content,
): Configuration { }: {
// Export some utility functions configureWebpack: NonNullable<Plugin['configureWebpack']>;
const utils: ConfigureWebpackUtils = { config: Configuration;
getStyleLoaders, isServer: boolean;
getJSLoader: getCustomizableJSLoader(jsLoader), utils: ConfigureWebpackUtils;
}; content: unknown;
}): Configuration {
if (typeof configureWebpack === 'function') { if (typeof configureWebpack === 'function') {
const {mergeStrategy, ...res} = const {mergeStrategy, ...res} =
configureWebpack(config, isServer, utils, content) ?? {}; configureWebpack(config, isServer, utils, content) ?? {};
@ -116,27 +132,28 @@ function executePluginsConfigurePostCss({
// Plugin Lifecycle - configureWebpack() // Plugin Lifecycle - configureWebpack()
export function executePluginsConfigureWebpack({ export function executePluginsConfigureWebpack({
plugins, plugins,
config, config: configInput,
isServer, isServer,
jsLoader, utils,
}: { }: {
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
config: Configuration; config: Configuration;
isServer: boolean; isServer: boolean;
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined; utils: ConfigureWebpackUtils;
}): Configuration { }): Configuration {
let config = configInput;
// Step1 - Configure Webpack // Step1 - Configure Webpack
let resultConfig = config;
plugins.forEach((plugin) => { plugins.forEach((plugin) => {
const {configureWebpack} = plugin; const {configureWebpack} = plugin;
if (configureWebpack) { if (configureWebpack) {
resultConfig = applyConfigureWebpack( config = applyConfigureWebpack({
configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. configureWebpack: configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`.
resultConfig, config,
isServer, isServer,
jsLoader, utils,
plugin.content, content: plugin.content,
); });
} }
}); });
@ -146,11 +163,11 @@ export function executePluginsConfigureWebpack({
// See https://github.com/facebook/docusaurus/issues/10106 // See https://github.com/facebook/docusaurus/issues/10106
// Note: it's useless to configure postCSS for the server // Note: it's useless to configure postCSS for the server
if (!isServer) { if (!isServer) {
resultConfig = executePluginsConfigurePostCss({ config = executePluginsConfigurePostCss({
plugins, plugins,
config: resultConfig, config,
}); });
} }
return resultConfig; return config;
} }

View file

@ -13,6 +13,8 @@ import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import webpack, {type Configuration, type RuleSetRule} from 'webpack'; import webpack, {type Configuration, type RuleSetRule} from 'webpack';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import {getSwcJsLoaderFactory} from '../faster';
import type {ConfigureWebpackUtils, DocusaurusConfig} from '@docusaurus/types';
import type {TransformOptions} from '@babel/core'; import type {TransformOptions} from '@babel/core';
export function formatStatsErrorMessage( export function formatStatsErrorMessage(
@ -142,33 +144,49 @@ export function getBabelOptions({
}; };
} }
// Name is generic on purpose const BabelJsLoaderFactory: ConfigureWebpackUtils['getJSLoader'] = ({
// we want to support multiple js loader implementations (babel + esbuild)
function getDefaultBabelLoader({
isServer, isServer,
babelOptions, babelOptions,
}: { }) => {
isServer: boolean;
babelOptions?: TransformOptions | string;
}): RuleSetRule {
return { return {
loader: require.resolve('babel-loader'), loader: require.resolve('babel-loader'),
options: getBabelOptions({isServer, babelOptions}), options: getBabelOptions({isServer, babelOptions}),
}; };
} };
export const getCustomizableJSLoader = // Confusing: function that creates a function that creates actual js loaders
(jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) = 'babel') => // This is done on purpose because the js loader factory is a public API
({ // It is injected in configureWebpack plugin lifecycle for plugin authors
isServer, export async function createJsLoaderFactory({
babelOptions, siteConfig,
}: { }: {
isServer: boolean; siteConfig: {
babelOptions?: TransformOptions | string; webpack?: DocusaurusConfig['webpack'];
}): RuleSetRule => future?: {
jsLoader === 'babel' experimental_faster: DocusaurusConfig['future']['experimental_faster'];
? getDefaultBabelLoader({isServer, babelOptions}) };
: jsLoader(isServer); };
}): 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 getSwcJsLoaderFactory();
}
if (jsLoader === 'babel') {
return BabelJsLoaderFactory;
}
throw new Error(`Docusaurus bug: unexpected jsLoader value${jsLoader}`);
}
declare global { declare global {
interface Error { interface Error {

View file

@ -197,6 +197,9 @@ Example:
```js title="docusaurus.config.js" ```js title="docusaurus.config.js"
export default { export default {
future: { future: {
experimental_faster: {
swcJsLoader: true,
},
experimental_storage: { experimental_storage: {
type: 'localStorage', type: 'localStorage',
namespace: true, namespace: true,
@ -206,6 +209,8 @@ export default {
}; };
``` ```
- `experimental_faster`: An object containing feature flags to make the Docusaurus build faster. This requires adding the `@docusaurus/faster` package to your site's dependencies. Use `true` as a shorthand to enable all flags.
- `swcJsLoader`: Use `true` to replace the default [Babel](https://babeljs.io/) JS loader by [SWC](https://swc.rs/) loader to speed up the bundling phase.
- `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect. - `experimental_storage`: Site-wide browser storage options that theme authors should strive to respect.
- `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`. - `type`: The browser storage theme authors should use. Possible values are `localStorage` and `sessionStorage`. Defaults to `localStorage`.
- `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior). - `namespace`: Whether to namespace the browser storage keys to avoid storage key conflicts when Docusaurus sites are hosted under the same domain, or on localhost. Possible values are `string | boolean`. The namespace is appended at the end of the storage keys `key-namespace`. Use `true` to automatically generate a random namespace from your site `url + baseUrl`. Defaults to `false` (no namespace, historical behavior).

View file

@ -96,6 +96,14 @@ function getNextVersionName() {
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast // Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true'; const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
// By default, we use Docusaurus Faster
// DOCUSAURUS_SLOWER=true is useful for benchmarking faster against slower
// hyperfine --prepare 'yarn clear:website' --runs 3 'DOCUSAURUS_SLOWER=true yarn build:website:fast' 'yarn build:website:fast'
const isSlower = process.env.DOCUSAURUS_SLOWER === 'true';
if (isSlower) {
console.log('🐢 Using slower Docusaurus build');
}
const router = process.env const router = process.env
.DOCUSAURUS_ROUTER as DocusaurusConfig['future']['experimental_router']; .DOCUSAURUS_ROUTER as DocusaurusConfig['future']['experimental_router'];
@ -152,6 +160,7 @@ export default async function createConfigAsync() {
baseUrlIssueBanner: true, baseUrlIssueBanner: true,
url: 'https://docusaurus.io', url: 'https://docusaurus.io',
future: { future: {
experimental_faster: !isSlower,
experimental_storage: { experimental_storage: {
namespace: true, namespace: true,
}, },
@ -180,28 +189,6 @@ export default async function createConfigAsync() {
: // Production locales : // Production locales
[defaultLocale, 'fr', 'pt-BR', 'ko', 'zh-CN'], [defaultLocale, 'fr', 'pt-BR', 'ko', 'zh-CN'],
}, },
webpack: {
jsLoader: (isServer) => ({
loader: require.resolve('swc-loader'),
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
target: 'es2017',
},
module: {
type: isServer ? 'commonjs' : 'es6',
},
},
}),
},
markdown: { markdown: {
format: 'detect', format: 'detect',
mermaid: true, mermaid: true,

View file

@ -22,6 +22,7 @@
"start:blogOnly": "cross-env yarn start --config=docusaurus.config-blog-only.js", "start:blogOnly": "cross-env yarn start --config=docusaurus.config-blog-only.js",
"build:blogOnly": "cross-env yarn build --config=docusaurus.config-blog-only.js", "build:blogOnly": "cross-env yarn build --config=docusaurus.config-blog-only.js",
"build:fast": "cross-env BUILD_FAST=true yarn build --locale en", "build:fast": "cross-env BUILD_FAST=true yarn build --locale en",
"build:fast:rsdoctor": "cross-env BUILD_FAST=true RSDOCTOR=true yarn build --locale en",
"build:fast:profile": "cross-env BUILD_FAST=true node --cpu-prof --cpu-prof-dir .cpu-prof ./node_modules/.bin/docusaurus build --locale en", "build:fast:profile": "cross-env BUILD_FAST=true node --cpu-prof --cpu-prof-dir .cpu-prof ./node_modules/.bin/docusaurus build --locale en",
"netlify:build:production": "yarn docusaurus write-translations && yarn netlify:crowdin:delay && yarn netlify:crowdin:uploadSources && yarn netlify:crowdin:downloadTranslations && yarn build && yarn test:css-order", "netlify:build:production": "yarn docusaurus write-translations && yarn netlify:crowdin:delay && yarn netlify:crowdin:uploadSources && yarn netlify:crowdin:downloadTranslations && yarn build && yarn test:css-order",
"netlify:build:branchDeploy": "yarn build && yarn test:css-order", "netlify:build:branchDeploy": "yarn build && yarn test:css-order",
@ -50,7 +51,6 @@
"@docusaurus/theme-mermaid": "3.5.2", "@docusaurus/theme-mermaid": "3.5.2",
"@docusaurus/utils": "3.5.2", "@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2", "@docusaurus/utils-common": "3.5.2",
"@swc/core": "1.2.197",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"color": "^4.2.3", "color": "^4.2.3",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
@ -64,7 +64,6 @@
"recma-mdx-displayname": "^0.4.1", "recma-mdx-displayname": "^0.4.1",
"rehype-katex": "^7.0.0", "rehype-katex": "^7.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"swc-loader": "^0.2.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"webpack": "^5.88.1", "webpack": "^5.88.1",
"workbox-routing": "^7.0.0", "workbox-routing": "^7.0.0",

1233
yarn.lock

File diff suppressed because it is too large Load diff