Skip to content

Commit bcfa3b1

Browse files
authored
refactor(faster,bundler,core): improve js loader DX (#10655)
1 parent bdf55ed commit bcfa3b1

File tree

7 files changed

+218
-27
lines changed

7 files changed

+218
-27
lines changed

packages/docusaurus-bundler/src/loaders/__tests__/jsLoader.test.ts

+55-9
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ import {createJsLoaderFactory} from '../jsLoader';
1010

1111
import type {RuleSetRule} from 'webpack';
1212

13+
type SiteConfigSlice = Parameters<
14+
typeof createJsLoaderFactory
15+
>[0]['siteConfig'];
16+
1317
describe('createJsLoaderFactory', () => {
14-
function testJsLoaderFactory(
15-
siteConfig?: PartialDeep<
16-
Parameters<typeof createJsLoaderFactory>[0]['siteConfig']
17-
>,
18-
) {
18+
function testJsLoaderFactory(siteConfig?: {
19+
webpack?: SiteConfigSlice['webpack'];
20+
future?: PartialDeep<SiteConfigSlice['future']>;
21+
}) {
1922
return createJsLoaderFactory({
2023
siteConfig: {
2124
...siteConfig,
22-
webpack: {
23-
jsLoader: 'babel',
24-
...siteConfig?.webpack,
25-
},
25+
webpack: siteConfig?.webpack,
2626
future: fromPartial({
2727
...siteConfig?.future,
2828
experimental_faster: fromPartial({
@@ -43,6 +43,52 @@ describe('createJsLoaderFactory', () => {
4343
);
4444
});
4545

46+
it('createJsLoaderFactory accepts babel loader preset', async () => {
47+
const createJsLoader = await testJsLoaderFactory({
48+
webpack: {jsLoader: 'babel'},
49+
});
50+
expect(createJsLoader({isServer: true}).loader).toBe(
51+
require.resolve('babel-loader'),
52+
);
53+
expect(createJsLoader({isServer: false}).loader).toBe(
54+
require.resolve('babel-loader'),
55+
);
56+
});
57+
58+
it('createJsLoaderFactory accepts custom loader', async () => {
59+
const createJsLoader = await testJsLoaderFactory({
60+
webpack: {
61+
jsLoader: (isServer) => {
62+
return {loader: `my-loader-${isServer ? 'server' : 'client'}`};
63+
},
64+
},
65+
});
66+
expect(createJsLoader({isServer: true}).loader).toBe('my-loader-server');
67+
expect(createJsLoader({isServer: false}).loader).toBe('my-loader-client');
68+
});
69+
70+
it('createJsLoaderFactory rejects custom loader when using faster swc loader', async () => {
71+
await expect(() =>
72+
testJsLoaderFactory({
73+
future: {
74+
experimental_faster: {
75+
swcJsLoader: true,
76+
},
77+
},
78+
webpack: {
79+
jsLoader: (isServer) => {
80+
return {loader: `my-loader-${isServer ? 'server' : 'client'}`};
81+
},
82+
},
83+
}),
84+
).rejects.toThrowErrorMatchingInlineSnapshot(`
85+
"You can't use siteConfig.webpack.jsLoader and siteConfig.future.experimental_faster.swcJsLoader at the same time.
86+
To avoid any configuration ambiguity, you must make an explicit choice:
87+
- If you want to use Docusaurus Faster and SWC (recommended), remove siteConfig.webpack.jsLoader
88+
- If you want to use a custom JS loader, use siteConfig.future.experimental_faster.swcJsLoader: false"
89+
`);
90+
});
91+
4692
it('createJsLoaderFactory accepts loaders with preset', async () => {
4793
const createJsLoader = await testJsLoaderFactory({
4894
webpack: {jsLoader: 'babel'},

packages/docusaurus-bundler/src/loaders/jsLoader.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,24 @@ export async function createJsLoaderFactory({
6262
}): Promise<ConfigureWebpackUtils['getJSLoader']> {
6363
const currentBundler = await getCurrentBundler({siteConfig});
6464
const isSWCLoader = siteConfig.future.experimental_faster.swcJsLoader;
65-
if (currentBundler.name === 'rspack') {
66-
return isSWCLoader
65+
if (isSWCLoader) {
66+
if (siteConfig.webpack?.jsLoader) {
67+
throw new Error(
68+
`You can't use siteConfig.webpack.jsLoader and siteConfig.future.experimental_faster.swcJsLoader at the same time.
69+
To avoid any configuration ambiguity, you must make an explicit choice:
70+
- If you want to use Docusaurus Faster and SWC (recommended), remove siteConfig.webpack.jsLoader
71+
- If you want to use a custom JS loader, use siteConfig.future.experimental_faster.swcJsLoader: false`,
72+
);
73+
}
74+
return currentBundler.name === 'rspack'
6775
? createRspackSwcJsLoaderFactory()
68-
: BabelJsLoaderFactory;
76+
: createSwcJsLoaderFactory();
6977
}
78+
7079
const jsLoader = siteConfig.webpack?.jsLoader ?? 'babel';
71-
if (
72-
jsLoader instanceof Function &&
73-
siteConfig.future?.experimental_faster.swcJsLoader
74-
) {
75-
throw new Error(
76-
"You can't use a custom webpack.jsLoader and experimental_faster.swcJsLoader at the same time",
77-
);
78-
}
7980
if (jsLoader instanceof Function) {
8081
return ({isServer}) => jsLoader(isServer);
8182
}
82-
if (siteConfig.future?.experimental_faster.swcJsLoader) {
83-
return createSwcJsLoaderFactory();
84-
}
8583
if (jsLoader === 'babel') {
8684
return BabelJsLoaderFactory;
8785
}

packages/docusaurus-types/src/config.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ export type DocusaurusConfig = {
431431
// TODO Docusaurus v4
432432
// Use an object type ({isServer}) so that it conforms to jsLoaderFactory
433433
// Eventually deprecate this if swc loader becomes stable?
434-
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule);
434+
jsLoader?: 'babel' | ((isServer: boolean) => RuleSetRule);
435435
};
436436
/** Markdown-related options. */
437437
markdown: MarkdownConfig;

packages/docusaurus/src/server/__tests__/__fixtures__/siteMessages/siteWithBabelConfigFile/babel.config.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import path from 'path';
9+
import {fromPartial} from '@total-typescript/shoehorn';
10+
import {collectAllSiteMessages} from '../siteMessages';
11+
12+
function siteDirFixture(name: string) {
13+
return path.resolve(__dirname, '__fixtures__', 'siteMessages', name);
14+
}
15+
16+
describe('collectAllSiteMessages', () => {
17+
describe('uselessBabelConfigMessages', () => {
18+
async function getMessagesFor({
19+
siteDir,
20+
swcJsLoader,
21+
}: {
22+
siteDir: string;
23+
swcJsLoader: boolean;
24+
}) {
25+
return collectAllSiteMessages(
26+
fromPartial({
27+
site: {
28+
props: {
29+
siteDir,
30+
siteConfig: {
31+
future: {
32+
experimental_faster: {
33+
swcJsLoader,
34+
},
35+
},
36+
},
37+
},
38+
},
39+
}),
40+
);
41+
}
42+
43+
it('warns for useless babel config file when SWC enabled', async () => {
44+
const messages = await getMessagesFor({
45+
siteDir: siteDirFixture('siteWithBabelConfigFile'),
46+
swcJsLoader: true,
47+
});
48+
expect(messages).toMatchInlineSnapshot(`
49+
[
50+
{
51+
"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\`.",
52+
"type": "warning",
53+
},
54+
]
55+
`);
56+
});
57+
58+
it('does not warn for babel config file when SWC disabled', async () => {
59+
const messages = await getMessagesFor({
60+
siteDir: siteDirFixture('siteWithBabelConfigFile'),
61+
swcJsLoader: false,
62+
});
63+
expect(messages).toMatchInlineSnapshot(`[]`);
64+
});
65+
});
66+
});

packages/docusaurus/src/server/site.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import {generateSiteFiles} from './codegen/codegen';
2828
import {getRoutesPaths, handleDuplicateRoutes} from './routes';
2929
import {createSiteStorage} from './storage';
30+
import {emitSiteMessages} from './siteMessages';
3031
import type {LoadPluginsResult} from './plugins/plugins';
3132
import type {
3233
DocusaurusConfig,
@@ -54,7 +55,9 @@ export type LoadContextParams = {
5455
localizePath?: boolean;
5556
};
5657

57-
export type LoadSiteParams = LoadContextParams;
58+
export type LoadSiteParams = LoadContextParams & {
59+
isReload?: boolean;
60+
};
5861

5962
export type Site = {
6063
props: Props;
@@ -236,7 +239,7 @@ async function createSiteFiles({
236239
* lifecycles to generate content and other data. It is side-effect-ful because
237240
* it generates temp files in the `.docusaurus` folder for the bundler.
238241
*/
239-
export async function loadSite(params: LoadContextParams): Promise<Site> {
242+
export async function loadSite(params: LoadSiteParams): Promise<Site> {
240243
const context = await PerfLogger.async('Load context', () =>
241244
loadContext(params),
242245
);
@@ -252,14 +255,22 @@ export async function loadSite(params: LoadContextParams): Promise<Site> {
252255
globalData,
253256
});
254257

258+
// For now, we don't re-emit messages on site reloads, it's too verbose
259+
if (!params.isReload) {
260+
await emitSiteMessages({site});
261+
}
262+
255263
return site;
256264
}
257265

258266
export async function reloadSite(site: Site): Promise<Site> {
259267
// TODO this can be optimized, for example:
260268
// - plugins loading same data as before should not recreate routes/bundles
261269
// - codegen does not need to re-run if nothing changed
262-
return loadSite(site.params);
270+
return loadSite({
271+
...site.params,
272+
isReload: true,
273+
});
263274
}
264275

265276
export async function reloadSitePlugin(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import path from 'path';
9+
import _ from 'lodash';
10+
import {getCustomBabelConfigFilePath} from '@docusaurus/babel';
11+
import logger from '@docusaurus/logger';
12+
import type {Site} from './site';
13+
14+
type Params = {site: Site};
15+
16+
type SiteMessage = {type: 'warning' | 'error'; message: string};
17+
18+
type SiteMessageCreator = (params: Params) => Promise<SiteMessage[]>;
19+
20+
const uselessBabelConfigMessages: SiteMessageCreator = async ({site}) => {
21+
const {
22+
props: {siteDir, siteConfig},
23+
} = site;
24+
if (siteConfig.future.experimental_faster.swcJsLoader) {
25+
const babelConfigFilePath = await getCustomBabelConfigFilePath(siteDir);
26+
if (babelConfigFilePath) {
27+
return [
28+
{
29+
type: 'warning',
30+
message: `Your site is using the SWC js loader. You can safely remove the Babel config file at ${logger.code(
31+
path.relative(process.cwd(), babelConfigFilePath),
32+
)}.`,
33+
},
34+
];
35+
}
36+
}
37+
return [];
38+
};
39+
40+
export async function collectAllSiteMessages(
41+
params: Params,
42+
): Promise<SiteMessage[]> {
43+
const messageCreators: SiteMessageCreator[] = [uselessBabelConfigMessages];
44+
return (
45+
await Promise.all(
46+
messageCreators.map((createMessages) => createMessages(params)),
47+
)
48+
).flat();
49+
}
50+
51+
function printSiteMessages(siteMessages: SiteMessage[]): void {
52+
const [errors, warnings] = _.partition(
53+
siteMessages,
54+
(sm) => sm.type === 'error',
55+
);
56+
if (errors.length > 0) {
57+
logger.error(`Docusaurus site errors:
58+
- ${errors.map((sm) => sm.message).join('\n- ')}`);
59+
}
60+
if (warnings.length > 0) {
61+
logger.warn(`Docusaurus site warnings:
62+
- ${warnings.map((sm) => sm.message).join('\n- ')}`);
63+
}
64+
}
65+
66+
export async function emitSiteMessages(params: Params): Promise<void> {
67+
const siteMessages = await collectAllSiteMessages(params);
68+
printSiteMessages(siteMessages);
69+
}

0 commit comments

Comments
 (0)