Skip to content

Commit

Permalink
refactor(faster,bundler,core): improve js loader DX (#10655)
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber authored Nov 8, 2024
1 parent bdf55ed commit bcfa3b1
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 27 deletions.
64 changes: 55 additions & 9 deletions packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createJsLoaderFactory>[0]['siteConfig']
>,
) {
function testJsLoaderFactory(siteConfig?: {
webpack?: SiteConfigSlice['webpack'];
future?: PartialDeep<SiteConfigSlice['future']>;
}) {
return createJsLoaderFactory({
siteConfig: {
...siteConfig,
webpack: {
jsLoader: 'babel',
...siteConfig?.webpack,
},
webpack: siteConfig?.webpack,
future: fromPartial({
...siteConfig?.future,
experimental_faster: fromPartial({
Expand All @@ -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'},
Expand Down
26 changes: 12 additions & 14 deletions packages/docusaurus-bundler/src/loaders/jsLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,24 @@ export async function createJsLoaderFactory({
}): Promise<ConfigureWebpackUtils['getJSLoader']> {
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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/docusaurus-types/src/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions packages/docusaurus/src/server/__tests__/siteMessages.test.ts
Original file line number Diff line number Diff line change
@@ -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(`[]`);
});
});
});
17 changes: 14 additions & 3 deletions packages/docusaurus/src/server/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,7 +55,9 @@ export type LoadContextParams = {
localizePath?: boolean;
};

export type LoadSiteParams = LoadContextParams;
export type LoadSiteParams = LoadContextParams & {
isReload?: boolean;
};

export type Site = {
props: Props;
Expand Down Expand Up @@ -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<Site> {
export async function loadSite(params: LoadSiteParams): Promise<Site> {
const context = await PerfLogger.async('Load context', () =>
loadContext(params),
);
Expand All @@ -252,14 +255,22 @@ export async function loadSite(params: LoadContextParams): Promise<Site> {
globalData,
});

// For now, we don't re-emit messages on site reloads, it's too verbose
if (!params.isReload) {
await emitSiteMessages({site});
}

return site;
}

export async function reloadSite(site: Site): Promise<Site> {
// 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(
Expand Down
69 changes: 69 additions & 0 deletions packages/docusaurus/src/server/siteMessages.ts
Original file line number Diff line number Diff line change
@@ -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<SiteMessage[]>;

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<SiteMessage[]> {
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<void> {
const siteMessages = await collectAllSiteMessages(params);
printSiteMessages(siteMessages);
}

0 comments on commit bcfa3b1

Please sign in to comment.