From bcfa3b1128a11264d16ec66877e7c5ec8ee2d5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 8 Nov 2024 14:33:18 +0100 Subject: [PATCH] refactor(faster,bundler,core): improve js loader DX (#10655) --- .../src/loaders/__tests__/jsLoader.test.ts | 64 ++++++++++++++--- .../src/loaders/jsLoader.ts | 26 ++++--- packages/docusaurus-types/src/config.d.ts | 2 +- .../siteWithBabelConfigFile/babel.config.js | 1 + .../src/server/__tests__/siteMessages.test.ts | 66 ++++++++++++++++++ packages/docusaurus/src/server/site.ts | 17 ++++- .../docusaurus/src/server/siteMessages.ts | 69 +++++++++++++++++++ 7 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js create mode 100644 packages/docusaurus/src/server/__tests__/siteMessages.test.ts create mode 100644 packages/docusaurus/src/server/siteMessages.ts diff --git a/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts b/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts index 22016169b883..a3c7ea43e492 100644 --- a/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts +++ b/packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts @@ -10,19 +10,19 @@ import {createJsLoaderFactory} from '../jsLoader'; import type {RuleSetRule} from 'webpack'; +type SiteConfigSlice = Parameters< + typeof createJsLoaderFactory +>[0]['siteConfig']; + describe('createJsLoaderFactory', () => { - function testJsLoaderFactory( - siteConfig?: PartialDeep< - Parameters[0]['siteConfig'] - >, - ) { + function testJsLoaderFactory(siteConfig?: { + webpack?: SiteConfigSlice['webpack']; + future?: PartialDeep; + }) { return createJsLoaderFactory({ siteConfig: { ...siteConfig, - webpack: { - jsLoader: 'babel', - ...siteConfig?.webpack, - }, + webpack: siteConfig?.webpack, future: fromPartial({ ...siteConfig?.future, experimental_faster: fromPartial({ @@ -43,6 +43,52 @@ describe('createJsLoaderFactory', () => { ); }); + it('createJsLoaderFactory accepts babel loader preset', async () => { + const createJsLoader = await testJsLoaderFactory({ + webpack: {jsLoader: 'babel'}, + }); + expect(createJsLoader({isServer: true}).loader).toBe( + require.resolve('babel-loader'), + ); + expect(createJsLoader({isServer: false}).loader).toBe( + require.resolve('babel-loader'), + ); + }); + + it('createJsLoaderFactory accepts custom loader', async () => { + const createJsLoader = await testJsLoaderFactory({ + webpack: { + jsLoader: (isServer) => { + return {loader: `my-loader-${isServer ? 'server' : 'client'}`}; + }, + }, + }); + expect(createJsLoader({isServer: true}).loader).toBe('my-loader-server'); + expect(createJsLoader({isServer: false}).loader).toBe('my-loader-client'); + }); + + it('createJsLoaderFactory rejects custom loader when using faster swc loader', async () => { + await expect(() => + testJsLoaderFactory({ + future: { + experimental_faster: { + swcJsLoader: true, + }, + }, + webpack: { + jsLoader: (isServer) => { + return {loader: `my-loader-${isServer ? 'server' : 'client'}`}; + }, + }, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "You can't use siteConfig.webpack.jsLoader and siteConfig.future.experimental_faster.swcJsLoader at the same time. + To avoid any configuration ambiguity, you must make an explicit choice: + - If you want to use Docusaurus Faster and SWC (recommended), remove siteConfig.webpack.jsLoader + - If you want to use a custom JS loader, use siteConfig.future.experimental_faster.swcJsLoader: false" + `); + }); + it('createJsLoaderFactory accepts loaders with preset', async () => { const createJsLoader = await testJsLoaderFactory({ webpack: {jsLoader: 'babel'}, diff --git a/packages/docusaurus-bundler/src/loaders/jsLoader.ts b/packages/docusaurus-bundler/src/loaders/jsLoader.ts index b6e0088ebd88..6f9213454d5d 100644 --- a/packages/docusaurus-bundler/src/loaders/jsLoader.ts +++ b/packages/docusaurus-bundler/src/loaders/jsLoader.ts @@ -62,26 +62,24 @@ export async function createJsLoaderFactory({ }): Promise { const currentBundler = await getCurrentBundler({siteConfig}); const isSWCLoader = siteConfig.future.experimental_faster.swcJsLoader; - if (currentBundler.name === 'rspack') { - return isSWCLoader + if (isSWCLoader) { + if (siteConfig.webpack?.jsLoader) { + throw new Error( + `You can't use siteConfig.webpack.jsLoader and siteConfig.future.experimental_faster.swcJsLoader at the same time. +To avoid any configuration ambiguity, you must make an explicit choice: +- If you want to use Docusaurus Faster and SWC (recommended), remove siteConfig.webpack.jsLoader +- If you want to use a custom JS loader, use siteConfig.future.experimental_faster.swcJsLoader: false`, + ); + } + return currentBundler.name === 'rspack' ? createRspackSwcJsLoaderFactory() - : BabelJsLoaderFactory; + : createSwcJsLoaderFactory(); } + 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 createSwcJsLoaderFactory(); - } if (jsLoader === 'babel') { return BabelJsLoaderFactory; } diff --git a/packages/docusaurus-types/src/config.d.ts b/packages/docusaurus-types/src/config.d.ts index 575be4a6fd57..8289e8b22282 100644 --- a/packages/docusaurus-types/src/config.d.ts +++ b/packages/docusaurus-types/src/config.d.ts @@ -431,7 +431,7 @@ export type DocusaurusConfig = { // 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: MarkdownConfig; diff --git a/packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js b/packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js new file mode 100644 index 000000000000..2b3d14f63f53 --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js @@ -0,0 +1 @@ +content doesn't matter diff --git a/packages/docusaurus/src/server/__tests__/siteMessages.test.ts b/packages/docusaurus/src/server/__tests__/siteMessages.test.ts new file mode 100644 index 000000000000..d50adc7ccada --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/siteMessages.test.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 path from 'path'; +import {fromPartial} from '@total-typescript/shoehorn'; +import {collectAllSiteMessages} from '../siteMessages'; + +function siteDirFixture(name: string) { + return path.resolve(__dirname, '__fixtures__', 'siteMessages', name); +} + +describe('collectAllSiteMessages', () => { + describe('uselessBabelConfigMessages', () => { + async function getMessagesFor({ + siteDir, + swcJsLoader, + }: { + siteDir: string; + swcJsLoader: boolean; + }) { + return collectAllSiteMessages( + fromPartial({ + site: { + props: { + siteDir, + siteConfig: { + future: { + experimental_faster: { + swcJsLoader, + }, + }, + }, + }, + }, + }), + ); + } + + it('warns for useless babel config file when SWC enabled', async () => { + const messages = await getMessagesFor({ + siteDir: siteDirFixture('siteWithBabelConfigFile'), + swcJsLoader: true, + }); + expect(messages).toMatchInlineSnapshot(` + [ + { + "message": "Your site is using the SWC js loader. You can safely remove the Babel config file at \`packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js\`.", + "type": "warning", + }, + ] + `); + }); + + it('does not warn for babel config file when SWC disabled', async () => { + const messages = await getMessagesFor({ + siteDir: siteDirFixture('siteWithBabelConfigFile'), + swcJsLoader: false, + }); + expect(messages).toMatchInlineSnapshot(`[]`); + }); + }); +}); diff --git a/packages/docusaurus/src/server/site.ts b/packages/docusaurus/src/server/site.ts index fdb5610a6d3b..d8fd3f242c1c 100644 --- a/packages/docusaurus/src/server/site.ts +++ b/packages/docusaurus/src/server/site.ts @@ -27,6 +27,7 @@ import { import {generateSiteFiles} from './codegen/codegen'; import {getRoutesPaths, handleDuplicateRoutes} from './routes'; import {createSiteStorage} from './storage'; +import {emitSiteMessages} from './siteMessages'; import type {LoadPluginsResult} from './plugins/plugins'; import type { DocusaurusConfig, @@ -54,7 +55,9 @@ export type LoadContextParams = { localizePath?: boolean; }; -export type LoadSiteParams = LoadContextParams; +export type LoadSiteParams = LoadContextParams & { + isReload?: boolean; +}; export type Site = { props: Props; @@ -236,7 +239,7 @@ async function createSiteFiles({ * lifecycles to generate content and other data. It is side-effect-ful because * it generates temp files in the `.docusaurus` folder for the bundler. */ -export async function loadSite(params: LoadContextParams): Promise { +export async function loadSite(params: LoadSiteParams): Promise { const context = await PerfLogger.async('Load context', () => loadContext(params), ); @@ -252,6 +255,11 @@ export async function loadSite(params: LoadContextParams): Promise { globalData, }); + // For now, we don't re-emit messages on site reloads, it's too verbose + if (!params.isReload) { + await emitSiteMessages({site}); + } + return site; } @@ -259,7 +267,10 @@ export async function reloadSite(site: Site): Promise { // TODO this can be optimized, for example: // - plugins loading same data as before should not recreate routes/bundles // - codegen does not need to re-run if nothing changed - return loadSite(site.params); + return loadSite({ + ...site.params, + isReload: true, + }); } export async function reloadSitePlugin( diff --git a/packages/docusaurus/src/server/siteMessages.ts b/packages/docusaurus/src/server/siteMessages.ts new file mode 100644 index 000000000000..e2b16d8c57cd --- /dev/null +++ b/packages/docusaurus/src/server/siteMessages.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import path from 'path'; +import _ from 'lodash'; +import {getCustomBabelConfigFilePath} from '@docusaurus/babel'; +import logger from '@docusaurus/logger'; +import type {Site} from './site'; + +type Params = {site: Site}; + +type SiteMessage = {type: 'warning' | 'error'; message: string}; + +type SiteMessageCreator = (params: Params) => Promise; + +const uselessBabelConfigMessages: SiteMessageCreator = async ({site}) => { + const { + props: {siteDir, siteConfig}, + } = site; + if (siteConfig.future.experimental_faster.swcJsLoader) { + const babelConfigFilePath = await getCustomBabelConfigFilePath(siteDir); + if (babelConfigFilePath) { + return [ + { + type: 'warning', + message: `Your site is using the SWC js loader. You can safely remove the Babel config file at ${logger.code( + path.relative(process.cwd(), babelConfigFilePath), + )}.`, + }, + ]; + } + } + return []; +}; + +export async function collectAllSiteMessages( + params: Params, +): Promise { + const messageCreators: SiteMessageCreator[] = [uselessBabelConfigMessages]; + return ( + await Promise.all( + messageCreators.map((createMessages) => createMessages(params)), + ) + ).flat(); +} + +function printSiteMessages(siteMessages: SiteMessage[]): void { + const [errors, warnings] = _.partition( + siteMessages, + (sm) => sm.type === 'error', + ); + if (errors.length > 0) { + logger.error(`Docusaurus site errors: +- ${errors.map((sm) => sm.message).join('\n- ')}`); + } + if (warnings.length > 0) { + logger.warn(`Docusaurus site warnings: +- ${warnings.map((sm) => sm.message).join('\n- ')}`); + } +} + +export async function emitSiteMessages(params: Params): Promise { + const siteMessages = await collectAllSiteMessages(params); + printSiteMessages(siteMessages); +}