diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index d399ac0ae4d0..b0b7272a5c07 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -21,6 +21,7 @@ import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig'; import {posixPath} from '@docusaurus/utils'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; +import {fromPartial} from '@total-typescript/shoehorn'; import pluginContentDocs from '../index'; import {toSidebarsProp} from '../props'; import {DefaultSidebarItemsGenerator} from '../sidebars/generator'; @@ -288,8 +289,11 @@ describe('simple website', () => { }, }, isServer: false, - utils: createConfigureWebpackUtils({ - siteConfig: {webpack: {jsLoader: 'babel'}}, + configureWebpackUtils: await createConfigureWebpackUtils({ + siteConfig: { + webpack: {jsLoader: 'babel'}, + future: {experimental_faster: fromPartial({})}, + }, }), content, }); diff --git a/packages/docusaurus-types/src/bundler.d.ts b/packages/docusaurus-types/src/bundler.d.ts new file mode 100644 index 000000000000..3204afd00ca5 --- /dev/null +++ b/packages/docusaurus-types/src/bundler.d.ts @@ -0,0 +1,17 @@ +/** + * 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 webpack from 'webpack'; + +// We use Webpack and Rspack interchangeably because most Rspack APIs are +// compatible with Webpack. So it's ok to use Webpack types for Rspack too. +// When compatibility doesn't work, use "CurrentBundler.name" +// See https://github.com/facebook/docusaurus/pull/10402 +export type CurrentBundler = { + name: 'webpack' | 'rspack'; + instance: typeof webpack; +}; diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index ceb154c01729..460333a483d9 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -127,6 +127,7 @@ export type FasterConfig = { swcJsLoader: boolean; swcJsMinimizer: boolean; mdxCrossCompilerCache: boolean; + rspackBundler: boolean; }; export type FutureConfig = { diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 91a0c0243987..9592bc00d49e 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -73,6 +73,8 @@ export { HtmlTags, } from './plugin'; +export {CurrentBundler} from './bundler'; + export { RouteConfig, PluginRouteConfig, diff --git a/packages/docusaurus-types/src/plugin.d.ts b/packages/docusaurus-types/src/plugin.d.ts index 949e2d5e6a38..fae721dcd0cb 100644 --- a/packages/docusaurus-types/src/plugin.d.ts +++ b/packages/docusaurus-types/src/plugin.d.ts @@ -15,6 +15,7 @@ import type {ThemeConfig} from './config'; import type {LoadContext, Props} from './context'; import type {SwizzleConfig} from './swizzle'; import type {RouteConfig} from './routing'; +import type {CurrentBundler} from './bundler'; export type PluginOptions = {id?: string} & {[key: string]: unknown}; @@ -54,6 +55,7 @@ export type PluginContentLoadedActions = { }; export type ConfigureWebpackUtils = { + currentBundler: CurrentBundler; getStyleLoaders: ( isServer: boolean, cssOptions: {[key: string]: unknown}, @@ -130,7 +132,7 @@ export type Plugin = { configureWebpack?: ( config: WebpackConfiguration, isServer: boolean, - utils: ConfigureWebpackUtils, + configureWebpackUtils: ConfigureWebpackUtils, content: Content, ) => WebpackConfiguration & { mergeStrategy?: { diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index cf8699a7042f..f06eba8c0c0a 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -34,7 +34,12 @@ import defaultSSRTemplate from '../templates/ssr.html.template'; import type {SSGParams} from '../ssg'; import type {Manifest} from 'react-loadable-ssr-addon-v5-slorber'; -import type {LoadedPlugin, Props, RouterType} from '@docusaurus/types'; +import type { + ConfigureWebpackUtils, + LoadedPlugin, + Props, + RouterType, +} from '@docusaurus/types'; import type {SiteCollectedData} from '../common'; export type BuildCLIOptions = Pick< @@ -165,6 +170,8 @@ async function buildLocale({ const router = siteConfig.future.experimental_router; + const configureWebpackUtils = await createConfigureWebpackUtils({siteConfig}); + // We can build the 2 configs in parallel const [{clientConfig, clientManifestPath}, {serverConfig, serverBundlePath}] = await PerfLogger.async('Creating webpack configs', () => @@ -172,9 +179,11 @@ async function buildLocale({ getBuildClientConfig({ props, cliOptions, + configureWebpackUtils, }), getBuildServerConfig({ props, + configureWebpackUtils, }), ]), ); @@ -324,15 +333,18 @@ async function executeBrokenLinksCheck({ async function getBuildClientConfig({ props, cliOptions, + configureWebpackUtils, }: { props: Props; cliOptions: BuildCLIOptions; + configureWebpackUtils: ConfigureWebpackUtils; }) { const {plugins} = props; const result = await createBuildClientConfig({ props, minify: cliOptions.minify ?? true, faster: props.siteConfig.future.experimental_faster, + configureWebpackUtils, bundleAnalyzer: cliOptions.bundleAnalyzer ?? false, }); let {config} = result; @@ -340,26 +352,29 @@ async function getBuildClientConfig({ plugins, config, isServer: false, - utils: await createConfigureWebpackUtils({ - siteConfig: props.siteConfig, - }), + configureWebpackUtils, }); return {clientConfig: config, clientManifestPath: result.clientManifestPath}; } -async function getBuildServerConfig({props}: {props: Props}) { +async function getBuildServerConfig({ + props, + configureWebpackUtils, +}: { + props: Props; + configureWebpackUtils: ConfigureWebpackUtils; +}) { const {plugins} = props; const result = await createServerConfig({ props, + configureWebpackUtils, }); let {config} = result; config = executePluginsConfigureWebpack({ plugins, config, isServer: true, - utils: await createConfigureWebpackUtils({ - siteConfig: props.siteConfig, - }), + configureWebpackUtils, }); return {serverConfig: config, serverBundlePath: result.serverBundlePath}; } diff --git a/packages/docusaurus/src/commands/start/webpack.ts b/packages/docusaurus/src/commands/start/webpack.ts index a148c77e9bb5..b2ddd0853c15 100644 --- a/packages/docusaurus/src/commands/start/webpack.ts +++ b/packages/docusaurus/src/commands/start/webpack.ts @@ -23,7 +23,7 @@ import { } from '../../webpack/configure'; import {createStartClientConfig} from '../../webpack/client'; import type {StartCLIOptions} from './start'; -import type {Props} from '@docusaurus/types'; +import type {ConfigureWebpackUtils, Props} from '@docusaurus/types'; import type {Compiler} from 'webpack'; import type {OpenUrlContext} from './utils'; @@ -127,23 +127,26 @@ async function getStartClientConfig({ props, minify, poll, + configureWebpackUtils, }: { props: Props; minify: boolean; poll: number | boolean | undefined; + configureWebpackUtils: ConfigureWebpackUtils; }) { - const {plugins, siteConfig} = props; + const {plugins} = props; let {clientConfig: config} = await createStartClientConfig({ props, minify, faster: props.siteConfig.future.experimental_faster, poll, + configureWebpackUtils, }); config = executePluginsConfigureWebpack({ plugins, config, isServer: false, - utils: await createConfigureWebpackUtils({siteConfig}), + configureWebpackUtils, }); return config; } @@ -157,10 +160,15 @@ export async function createWebpackDevServer({ cliOptions: StartCLIOptions; openUrlContext: OpenUrlContext; }): Promise { + const configureWebpackUtils = await createConfigureWebpackUtils({ + siteConfig: props.siteConfig, + }); + const config = await getStartClientConfig({ props, minify: cliOptions.minify ?? true, poll: cliOptions.poll, + configureWebpackUtils, }); const compiler = webpack(config); diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 00135ff98e3a..af9b1a67560e 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -10,6 +10,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -76,6 +77,7 @@ exports[`loadSiteConfig website with ts + js config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -142,6 +144,7 @@ exports[`loadSiteConfig website with valid JS CJS config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -208,6 +211,7 @@ exports[`loadSiteConfig website with valid JS ESM config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -274,6 +278,7 @@ exports[`loadSiteConfig website with valid TypeScript CJS config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -340,6 +345,7 @@ exports[`loadSiteConfig website with valid TypeScript ESM config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -406,6 +412,7 @@ exports[`loadSiteConfig website with valid async config 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -474,6 +481,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -542,6 +550,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, @@ -613,6 +622,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap index 73fd5dc8d58f..06fa00daab7d 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap @@ -80,6 +80,7 @@ exports[`load loads props for site with custom i18n path 1`] = ` "future": { "experimental_faster": { "mdxCrossCompilerCache": false, + "rspackBundler": false, "swcJsLoader": false, "swcJsMinimizer": false, }, diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 49d9860bcbe1..0521af1e3e97 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -49,6 +49,7 @@ describe('normalizeConfig', () => { swcJsLoader: true, swcJsMinimizer: true, mdxCrossCompilerCache: true, + rspackBundler: true, }, experimental_storage: { type: 'sessionStorage', @@ -745,6 +746,7 @@ describe('future', () => { swcJsLoader: true, swcJsMinimizer: true, mdxCrossCompilerCache: true, + rspackBundler: true, }, experimental_storage: { type: 'sessionStorage', @@ -1095,6 +1097,7 @@ describe('future', () => { swcJsLoader: true, swcJsMinimizer: true, mdxCrossCompilerCache: true, + rspackBundler: true, }; expect( normalizeConfig({ @@ -1348,5 +1351,76 @@ describe('future', () => { `); }); }); + + describe('rspackBundler', () => { + it('accepts - undefined', () => { + const faster: Partial = { + rspackBundler: undefined, + }; + expect( + normalizeConfig({ + future: { + experimental_faster: faster, + }, + }), + ).toEqual(fasterContaining({rspackBundler: false})); + }); + + it('accepts - true', () => { + const faster: Partial = { + rspackBundler: true, + }; + expect( + normalizeConfig({ + future: { + experimental_faster: faster, + }, + }), + ).toEqual(fasterContaining({rspackBundler: true})); + }); + + it('accepts - false', () => { + const faster: Partial = { + rspackBundler: false, + }; + expect( + normalizeConfig({ + future: { + experimental_faster: faster, + }, + }), + ).toEqual(fasterContaining({rspackBundler: false})); + }); + + it('rejects - null', () => { + // @ts-expect-error: invalid + const faster: Partial = {rspackBundler: 42}; + expect(() => + normalizeConfig({ + future: { + experimental_faster: faster, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_faster.rspackBundler" must be a boolean + " + `); + }); + + it('rejects - number', () => { + // @ts-expect-error: invalid + const faster: Partial = {rspackBundler: 42}; + expect(() => + normalizeConfig({ + future: { + experimental_faster: faster, + }, + }), + ).toThrowErrorMatchingInlineSnapshot(` + ""future.experimental_faster.rspackBundler" must be a boolean + " + `); + }); + }); }); }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index c066d5ce955a..ca5e6c7a1da2 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -45,6 +45,7 @@ export const DEFAULT_FASTER_CONFIG: FasterConfig = { swcJsLoader: false, swcJsMinimizer: false, mdxCrossCompilerCache: false, + rspackBundler: false, }; // When using the "faster: true" shortcut @@ -52,6 +53,7 @@ export const DEFAULT_FASTER_CONFIG_TRUE: FasterConfig = { swcJsLoader: true, swcJsMinimizer: true, mdxCrossCompilerCache: true, + rspackBundler: true, }; export const DEFAULT_FUTURE_CONFIG: FutureConfig = { @@ -222,6 +224,7 @@ const FASTER_CONFIG_SCHEMA = Joi.alternatives() mdxCrossCompilerCache: Joi.boolean().default( DEFAULT_FASTER_CONFIG.mdxCrossCompilerCache, ), + rspackBundler: Joi.boolean().default(DEFAULT_FASTER_CONFIG.rspackBundler), }), Joi.boolean() .required() diff --git a/packages/docusaurus/src/webpack/__tests__/base.test.ts b/packages/docusaurus/src/webpack/__tests__/base.test.ts index 68ebb4b6a5ce..aa8ec0fb6814 100644 --- a/packages/docusaurus/src/webpack/__tests__/base.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/base.test.ts @@ -15,8 +15,15 @@ import { DEFAULT_FASTER_CONFIG, DEFAULT_FUTURE_CONFIG, } from '../../server/configValidation'; +import {createConfigureWebpackUtils} from '../configure'; import type {Props} from '@docusaurus/types'; +function createTestConfigureWebpackUtils() { + return createConfigureWebpackUtils({ + siteConfig: {webpack: {jsLoader: 'babel'}, future: DEFAULT_FUTURE_CONFIG}, + }); +} + describe('babel transpilation exclude logic', () => { it('always transpiles client dir files', () => { const clientFiles = [ @@ -115,6 +122,7 @@ describe('base webpack config', () => { isServer: true, minify: true, faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), }) ).resolve?.alias ?? {}) as {[alias: string]: string}; // Make aliases relative so that test work on all computers @@ -125,7 +133,8 @@ describe('base webpack config', () => { }); it('uses svg rule', async () => { - const fileLoaderUtils = utils.getFileLoaderUtils(); + const isServer = true; + const fileLoaderUtils = utils.getFileLoaderUtils(isServer); const mockSvg = jest.spyOn(fileLoaderUtils.rules, 'svg'); jest .spyOn(utils, 'getFileLoaderUtils') @@ -136,6 +145,7 @@ describe('base webpack config', () => { isServer: false, minify: false, faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), }); expect(mockSvg).toHaveBeenCalled(); }); diff --git a/packages/docusaurus/src/webpack/__tests__/client.test.ts b/packages/docusaurus/src/webpack/__tests__/client.test.ts index 20b8447fac0f..0f4968bfc5c9 100644 --- a/packages/docusaurus/src/webpack/__tests__/client.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/client.test.ts @@ -9,12 +9,25 @@ import webpack from 'webpack'; import {createBuildClientConfig, createStartClientConfig} from '../client'; import {loadSetup} from '../../server/__tests__/testUtils'; +import {createConfigureWebpackUtils} from '../configure'; +import { + DEFAULT_FASTER_CONFIG, + DEFAULT_FUTURE_CONFIG, +} from '../../server/configValidation'; + +function createTestConfigureWebpackUtils() { + return createConfigureWebpackUtils({ + siteConfig: {webpack: {jsLoader: 'babel'}, future: DEFAULT_FUTURE_CONFIG}, + }); +} describe('webpack dev config', () => { it('simple start', async () => { const {props} = await loadSetup('simple-site'); const {clientConfig} = await createStartClientConfig({ props, + faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), minify: false, poll: false, }); @@ -25,6 +38,8 @@ describe('webpack dev config', () => { const {props} = await loadSetup('simple-site'); const {config} = await createBuildClientConfig({ props, + faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), minify: false, bundleAnalyzer: false, }); @@ -35,6 +50,8 @@ describe('webpack dev config', () => { const {props} = await loadSetup('custom-site'); const {clientConfig} = await createStartClientConfig({ props, + faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), minify: false, poll: false, }); @@ -45,6 +62,8 @@ describe('webpack dev config', () => { const {props} = await loadSetup('custom-site'); const {config} = await createBuildClientConfig({ props, + faster: DEFAULT_FASTER_CONFIG, + configureWebpackUtils: await createTestConfigureWebpackUtils(), minify: false, bundleAnalyzer: false, }); diff --git a/packages/docusaurus/src/webpack/__tests__/configure.test.ts b/packages/docusaurus/src/webpack/__tests__/configure.test.ts index c23178cec818..5efd153f9f35 100644 --- a/packages/docusaurus/src/webpack/__tests__/configure.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/configure.test.ts @@ -14,17 +14,22 @@ import { executePluginsConfigureWebpack, createConfigureWebpackUtils, } from '../configure'; +import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation'; import type {Configuration} from 'webpack'; import type {LoadedPlugin, Plugin} from '@docusaurus/types'; -const utils = createConfigureWebpackUtils({ - siteConfig: {webpack: {jsLoader: 'babel'}}, -}); +function createTestConfigureWebpackUtils() { + return createConfigureWebpackUtils({ + siteConfig: {webpack: {jsLoader: 'babel'}, future: DEFAULT_FUTURE_CONFIG}, + }); +} const isServer = false; describe('extending generated webpack config', () => { it('direct mutation on generated webpack config object', async () => { + const utils = await createTestConfigureWebpackUtils(); + // Fake generated webpack config let config: Configuration = { output: { @@ -52,7 +57,7 @@ describe('extending generated webpack config', () => { configureWebpack, config, isServer, - utils, + configureWebpackUtils: utils, content: { content: 42, }, @@ -69,6 +74,8 @@ describe('extending generated webpack config', () => { }); it('webpack-merge with user webpack config object', async () => { + const utils = await createTestConfigureWebpackUtils(); + let config: Configuration = { output: { path: __dirname, @@ -88,7 +95,7 @@ describe('extending generated webpack config', () => { configureWebpack, config, isServer, - utils, + configureWebpackUtils: utils, content: { content: 42, }, @@ -105,6 +112,8 @@ describe('extending generated webpack config', () => { }); it('webpack-merge with custom strategy', async () => { + const utils = await createTestConfigureWebpackUtils(); + const config: Configuration = { module: { rules: [{use: 'xxx'}, {use: 'yyy'}], @@ -126,7 +135,7 @@ describe('extending generated webpack config', () => { configureWebpack: createConfigureWebpack(), config, isServer, - utils, + configureWebpackUtils: utils, content: {content: 42}, }); expect(defaultStrategyMergeConfig).toEqual({ @@ -139,7 +148,7 @@ describe('extending generated webpack config', () => { configureWebpack: createConfigureWebpack({'module.rules': 'prepend'}), config, isServer, - utils, + configureWebpackUtils: utils, content: {content: 42}, }); expect(prependRulesStrategyConfig).toEqual({ @@ -154,7 +163,7 @@ describe('extending generated webpack config', () => { }), config, isServer, - utils, + configureWebpackUtils: utils, content: {content: 42}, }); expect(uselessMergeStrategyConfig).toEqual({ @@ -293,11 +302,13 @@ describe('executePluginsConfigureWebpack', () => { }); } - it('can merge Webpack aliases of 2 plugins into base config', () => { + it('can merge Webpack aliases of 2 plugins into base config', async () => { + const utils = await createTestConfigureWebpackUtils(); + const config = executePluginsConfigureWebpack({ config: {resolve: {alias: {'initial-alias': 'initial-alias-value'}}}, isServer, - utils, + configureWebpackUtils: utils, plugins: [ fakePlugin({ configureWebpack: () => { @@ -328,11 +339,13 @@ describe('executePluginsConfigureWebpack', () => { ); }); - it('can configurePostCSS() for all loaders added through configureWebpack()', () => { + it('can configurePostCSS() for all loaders added through configureWebpack()', async () => { + const utils = await createTestConfigureWebpackUtils(); + const config = executePluginsConfigureWebpack({ config: {}, isServer, - utils, + configureWebpackUtils: utils, plugins: [ fakePlugin({ configurePostCss: (postCssOptions) => { diff --git a/packages/docusaurus/src/webpack/__tests__/server.test.ts b/packages/docusaurus/src/webpack/__tests__/server.test.ts index 5ef069861d5b..8f4b7506176c 100644 --- a/packages/docusaurus/src/webpack/__tests__/server.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/server.test.ts @@ -10,6 +10,14 @@ import webpack from 'webpack'; import createServerConfig from '../server'; import {loadSetup} from '../../server/__tests__/testUtils'; +import {createConfigureWebpackUtils} from '../configure'; +import {DEFAULT_FUTURE_CONFIG} from '../../server/configValidation'; + +function createTestConfigureWebpackUtils() { + return createConfigureWebpackUtils({ + siteConfig: {webpack: {jsLoader: 'babel'}, future: DEFAULT_FUTURE_CONFIG}, + }); +} describe('webpack production config', () => { it('simple', async () => { @@ -17,6 +25,7 @@ describe('webpack production config', () => { const {props} = await loadSetup('simple-site'); const {config} = await createServerConfig({ props, + configureWebpackUtils: await createTestConfigureWebpackUtils(), }); webpack.validate(config); }); @@ -26,6 +35,7 @@ describe('webpack production config', () => { const {props} = await loadSetup('custom-site'); const {config} = await createServerConfig({ props, + configureWebpackUtils: await createTestConfigureWebpackUtils(), }); webpack.validate(config); }); diff --git a/packages/docusaurus/src/webpack/base.ts b/packages/docusaurus/src/webpack/base.ts index 2e2c7ee93eeb..14f053cdf086 100644 --- a/packages/docusaurus/src/webpack/base.ts +++ b/packages/docusaurus/src/webpack/base.ts @@ -7,17 +7,17 @@ import fs from 'fs-extra'; import path from 'path'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import {md5Hash, getFileLoaderUtils} from '@docusaurus/utils'; -import { - createJsLoaderFactory, - getStyleLoaders, - getCustomBabelConfigFilePath, -} from './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 {FasterConfig, Props} from '@docusaurus/types'; +import type { + ConfigureWebpackUtils, + FasterConfig, + Props, +} from '@docusaurus/types'; const CSS_REGEX = /\.css$/i; const CSS_MODULE_REGEX = /\.module\.css$/i; @@ -58,11 +58,13 @@ export async function createBaseConfig({ isServer, minify, faster, + configureWebpackUtils, }: { props: Props; isServer: boolean; minify: boolean; faster: FasterConfig; + configureWebpackUtils: ConfigureWebpackUtils; }): Promise { const { outDir, @@ -88,6 +90,10 @@ export async function createBaseConfig({ const createJsLoader = await createJsLoaderFactory({siteConfig}); + const CSSExtractPlugin = await getCSSExtractPlugin({ + currentBundler: configureWebpackUtils.currentBundler, + }); + return { mode, name, @@ -224,7 +230,7 @@ export async function createBaseConfig({ { test: CSS_REGEX, exclude: CSS_MODULE_REGEX, - use: getStyleLoaders(isServer, { + use: configureWebpackUtils.getStyleLoaders(isServer, { importLoaders: 1, sourceMap: !isProd, }), @@ -233,7 +239,7 @@ export async function createBaseConfig({ // using the extension .module.css { test: CSS_MODULE_REGEX, - use: getStyleLoaders(isServer, { + use: configureWebpackUtils.getStyleLoaders(isServer, { modules: { // Using the same CSS Module class pattern in dev/prod on purpose // See https://github.com/facebook/docusaurus/pull/10423 @@ -247,7 +253,7 @@ export async function createBaseConfig({ ], }, plugins: [ - new MiniCssExtractPlugin({ + new CSSExtractPlugin({ filename: isProd ? 'assets/css/[name].[contenthash:8].css' : '[name].css', diff --git a/packages/docusaurus/src/webpack/client.ts b/packages/docusaurus/src/webpack/client.ts index a3b01e77e743..4cebdbf0edc5 100644 --- a/packages/docusaurus/src/webpack/client.ts +++ b/packages/docusaurus/src/webpack/client.ts @@ -17,7 +17,11 @@ import ChunkAssetPlugin from './plugins/ChunkAssetPlugin'; import CleanWebpackPlugin from './plugins/CleanWebpackPlugin'; import ForceTerminatePlugin from './plugins/ForceTerminatePlugin'; import {createStaticDirectoriesCopyPlugin} from './plugins/StaticDirectoriesCopyPlugin'; -import type {FasterConfig, Props} from '@docusaurus/types'; +import type { + ConfigureWebpackUtils, + FasterConfig, + Props, +} from '@docusaurus/types'; import type {Configuration} from 'webpack'; async function createBaseClientConfig({ @@ -25,17 +29,20 @@ async function createBaseClientConfig({ hydrate, minify, faster, + configureWebpackUtils, }: { props: Props; hydrate: boolean; minify: boolean; faster: FasterConfig; + configureWebpackUtils: ConfigureWebpackUtils; }): Promise { const baseConfig = await createBaseConfig({ props, isServer: false, minify, faster, + configureWebpackUtils, }); return merge(baseConfig, { @@ -57,7 +64,10 @@ async function createBaseClientConfig({ new WebpackBar({ name: 'Client', }), - await createStaticDirectoriesCopyPlugin({props}), + await createStaticDirectoriesCopyPlugin({ + props, + currentBundler: configureWebpackUtils.currentBundler, + }), ].filter(Boolean), }); } @@ -68,11 +78,13 @@ export async function createStartClientConfig({ minify, poll, faster, + configureWebpackUtils, }: { props: Props; minify: boolean; poll: number | boolean | undefined; faster: FasterConfig; + configureWebpackUtils: ConfigureWebpackUtils; }): Promise<{clientConfig: Configuration}> { const {siteConfig, headTags, preBodyTags, postBodyTags} = props; @@ -82,6 +94,7 @@ export async function createStartClientConfig({ minify, hydrate: false, faster, + configureWebpackUtils, }), { watchOptions: { @@ -116,11 +129,13 @@ export async function createBuildClientConfig({ props, minify, faster, + configureWebpackUtils, bundleAnalyzer, }: { props: Props; minify: boolean; faster: FasterConfig; + configureWebpackUtils: ConfigureWebpackUtils; bundleAnalyzer: boolean; }): Promise<{config: Configuration; clientManifestPath: string}> { // Apply user webpack config. @@ -137,7 +152,13 @@ export async function createBuildClientConfig({ ); const config: Configuration = merge( - await createBaseClientConfig({props, minify, faster, hydrate}), + await createBaseClientConfig({ + props, + minify, + faster, + configureWebpackUtils, + hydrate, + }), { plugins: [ new ForceTerminatePlugin(), diff --git a/packages/docusaurus/src/webpack/configure.ts b/packages/docusaurus/src/webpack/configure.ts index 071a17359021..2f50bfbe05ce 100644 --- a/packages/docusaurus/src/webpack/configure.ts +++ b/packages/docusaurus/src/webpack/configure.ts @@ -10,8 +10,8 @@ import { customizeArray, customizeObject, } from 'webpack-merge'; -import {createJsLoaderFactory, getStyleLoaders} from './utils'; - +import {createJsLoaderFactory, createStyleLoadersFactory} from './utils'; +import {getCurrentBundler} from './currentBundler'; import type {Configuration, RuleSetRule} from 'webpack'; import type { Plugin, @@ -27,11 +27,16 @@ import type { export async function createConfigureWebpackUtils({ siteConfig, }: { - siteConfig: Parameters[0]['siteConfig']; + siteConfig: Parameters[0]['siteConfig'] & + Parameters[0]['siteConfig']; }): Promise { + const currentBundler = await getCurrentBundler({siteConfig}); + const getStyleLoaders = await createStyleLoadersFactory({currentBundler}); + const getJSLoader = await createJsLoaderFactory({siteConfig}); return { + currentBundler, getStyleLoaders, - getJSLoader: await createJsLoaderFactory({siteConfig}), + getJSLoader, }; } @@ -48,18 +53,18 @@ export function applyConfigureWebpack({ configureWebpack, config, isServer, - utils, + configureWebpackUtils, content, }: { configureWebpack: NonNullable; config: Configuration; isServer: boolean; - utils: ConfigureWebpackUtils; + configureWebpackUtils: ConfigureWebpackUtils; content: unknown; }): Configuration { if (typeof configureWebpack === 'function') { const {mergeStrategy, ...res} = - configureWebpack(config, isServer, utils, content) ?? {}; + configureWebpack(config, isServer, configureWebpackUtils, content) ?? {}; const customizeRules = mergeStrategy ?? {}; return mergeWithCustomize({ customizeArray: customizeArray(customizeRules), @@ -134,12 +139,12 @@ export function executePluginsConfigureWebpack({ plugins, config: configInput, isServer, - utils, + configureWebpackUtils, }: { plugins: LoadedPlugin[]; config: Configuration; isServer: boolean; - utils: ConfigureWebpackUtils; + configureWebpackUtils: ConfigureWebpackUtils; }): Configuration { let config = configInput; @@ -151,7 +156,7 @@ export function executePluginsConfigureWebpack({ configureWebpack: configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. config, isServer, - utils, + configureWebpackUtils, content: plugin.content, }); } diff --git a/packages/docusaurus/src/webpack/currentBundler.ts b/packages/docusaurus/src/webpack/currentBundler.ts new file mode 100644 index 000000000000..d0089aeb05a0 --- /dev/null +++ b/packages/docusaurus/src/webpack/currentBundler.ts @@ -0,0 +1,66 @@ +/** + * 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 webpack from 'webpack'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import logger from '@docusaurus/logger'; +import type {CurrentBundler, DocusaurusConfig} from '@docusaurus/types'; + +// We inject a site config slice because the Rspack flag might change place +type SiteConfigSlice = { + future: { + experimental_faster: Pick< + DocusaurusConfig['future']['experimental_faster'], + 'rspackBundler' + >; + }; +}; + +function isRspack(siteConfig: SiteConfigSlice): boolean { + return siteConfig.future.experimental_faster.rspackBundler; +} + +export async function getCurrentBundler({ + siteConfig, +}: { + siteConfig: SiteConfigSlice; +}): Promise { + if (isRspack(siteConfig)) { + // TODO add support for Rspack + logger.error( + 'Rspack bundler is not supported yet, will use Webpack instead', + ); + } + return { + name: 'webpack', + instance: webpack, + }; +} + +export async function getCSSExtractPlugin({ + currentBundler, +}: { + currentBundler: CurrentBundler; +}): Promise { + if (currentBundler.name === 'rspack') { + throw new Error('Rspack bundler is not supported yet'); + } + return MiniCssExtractPlugin; +} + +export async function getCopyPlugin({ + currentBundler, +}: { + currentBundler: CurrentBundler; +}): Promise { + if (currentBundler.name === 'rspack') { + throw new Error('Rspack bundler is not supported yet'); + } + // https://github.com/webpack-contrib/copy-webpack-plugin + return CopyWebpackPlugin; +} diff --git a/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts b/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts index 8efaa2fd456c..297d1bc99190 100644 --- a/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts +++ b/packages/docusaurus/src/webpack/plugins/StaticDirectoriesCopyPlugin.ts @@ -7,14 +7,21 @@ import path from 'path'; import fs from 'fs-extra'; -import CopyWebpackPlugin from 'copy-webpack-plugin'; -import type {Props} from '@docusaurus/types'; +import {getCopyPlugin} from '../currentBundler'; +import type {CurrentBundler, Props} from '@docusaurus/types'; +import type {WebpackPluginInstance} from 'webpack'; export async function createStaticDirectoriesCopyPlugin({ props, + currentBundler, }: { props: Props; -}): Promise { + currentBundler: CurrentBundler; +}): Promise { + const CopyPlugin = await getCopyPlugin({ + currentBundler, + }); + const { outDir, siteDir, @@ -44,7 +51,7 @@ export async function createStaticDirectoriesCopyPlugin({ return undefined; } - return new CopyWebpackPlugin({ + return new CopyPlugin({ patterns: staticDirectories.map((dir) => ({ from: dir, to: outDir, diff --git a/packages/docusaurus/src/webpack/server.ts b/packages/docusaurus/src/webpack/server.ts index 81fbc1df7192..e8df2b485c23 100644 --- a/packages/docusaurus/src/webpack/server.ts +++ b/packages/docusaurus/src/webpack/server.ts @@ -10,19 +10,22 @@ import merge from 'webpack-merge'; import {NODE_MAJOR_VERSION, NODE_MINOR_VERSION} from '@docusaurus/utils'; import WebpackBar from 'webpackbar'; import {createBaseConfig} from './base'; -import type {Props} from '@docusaurus/types'; +import type {ConfigureWebpackUtils, Props} from '@docusaurus/types'; import type {Configuration} from 'webpack'; -export default async function createServerConfig(params: { +export default async function createServerConfig({ + props, + configureWebpackUtils, +}: { props: Props; + configureWebpackUtils: ConfigureWebpackUtils; }): Promise<{config: Configuration; serverBundlePath: string}> { - const {props} = params; - const baseConfig = await createBaseConfig({ props, isServer: true, minify: false, faster: props.siteConfig.future.experimental_faster, + configureWebpackUtils, }); const outputFilename = 'server.bundle.js'; diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index a77984832b38..8711be52c544 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -10,11 +10,15 @@ import path from 'path'; import crypto from 'crypto'; import logger from '@docusaurus/logger'; import {BABEL_CONFIG_FILE_NAME} from '@docusaurus/utils'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import webpack, {type Configuration, type RuleSetRule} from 'webpack'; +import webpack, {type Configuration} from 'webpack'; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; import {importSwcJsLoaderFactory} from '../faster'; -import type {ConfigureWebpackUtils, DocusaurusConfig} from '@docusaurus/types'; +import {getCSSExtractPlugin} from './currentBundler'; +import type { + ConfigureWebpackUtils, + CurrentBundler, + DocusaurusConfig, +} from '@docusaurus/types'; import type {TransformOptions} from '@babel/core'; export function formatStatsErrorMessage( @@ -42,68 +46,75 @@ export function printStatsWarnings( } } -// Utility method to get style loaders -export function getStyleLoaders( - isServer: boolean, - cssOptionsArg: { - [key: string]: unknown; - } = {}, -): RuleSetRule[] { - const cssOptions: {[key: string]: unknown} = { - // TODO turn esModule on later, see https://github.com/facebook/docusaurus/pull/6424 - esModule: false, - ...cssOptionsArg, - }; +export async function createStyleLoadersFactory({ + currentBundler, +}: { + currentBundler: CurrentBundler; +}): Promise { + const CssExtractPlugin = await getCSSExtractPlugin({currentBundler}); - // 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 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, + }; - return [ - { - loader: MiniCssExtractPlugin.loader, - options: { - esModule: true, + // 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, }, - }, - { - 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'), - ], + // 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( diff --git a/project-words.txt b/project-words.txt index 6a218907e70f..522989634d7c 100644 --- a/project-words.txt +++ b/project-words.txt @@ -313,6 +313,7 @@ rmiz rsdoctor Rsdoctor RSDOCTOR +rspack Rspack rtcts rtlcss diff --git a/yarn.lock b/yarn.lock index d6dcab5945bb..c367069bdc63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5082,7 +5082,7 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7: +call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== @@ -8260,7 +8260,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -12816,7 +12816,7 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.12.3, object-inspect@^1.13.1, object-inspect@^1.9.0: +object-inspect@^1.12.3, object-inspect@^1.13.1: version "1.13.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==