Skip to content

Commit 97dc1f2

Browse files
committed
feat: css external
1 parent b1cffee commit 97dc1f2

File tree

25 files changed

+310
-92
lines changed

25 files changed

+310
-92
lines changed

packages/core/src/config.ts

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,8 @@ const composeBundlelessExternalConfig = (
977977
} => {
978978
if (bundle) return { config: {} };
979979

980-
const isStyleRedirected = redirect.style ?? true;
980+
const styleRedirectPath = redirect.style?.path ?? true;
981+
const styleRedirectExtension = redirect.style?.extension ?? true;
981982
const jsRedirectPath = redirect.js?.path ?? true;
982983
const jsRedirectExtension = redirect.js?.extension ?? true;
983984

@@ -996,50 +997,57 @@ const composeBundlelessExternalConfig = (
996997
if (!request || !getResolve || !context || !contextInfo) {
997998
return callback();
998999
}
1000+
const { issuer } = contextInfo;
9991001

10001002
if (!resolver) {
10011003
resolver = (await getResolve()) as RspackResolver;
10021004
}
10031005

1006+
async function redirectPath(request: string): Promise<string> {
1007+
try {
1008+
let resolvedRequest = request;
1009+
resolvedRequest = await resolver!(context!, resolvedRequest);
1010+
resolvedRequest = normalizeSlash(
1011+
path.relative(path.dirname(issuer), resolvedRequest),
1012+
);
1013+
// Requests that fall through here cannot be matched by any other externals config ahead.
1014+
// Treat all these requests as relative import of source code. Node.js won't add the
1015+
// leading './' to the relative path resolved by `path.relative`. So add manually it here.
1016+
if (resolvedRequest[0] !== '.') {
1017+
resolvedRequest = `./${resolvedRequest}`;
1018+
}
1019+
return resolvedRequest;
1020+
} catch (e) {
1021+
// e.g. A react component library importing and using 'react' but while not defining
1022+
// it in devDependencies and peerDependencies. Preserve 'react' as-is if so.
1023+
logger.warn(
1024+
`Failed to resolve module ${color.green(`"${request}"`)} from ${color.green(issuer)}. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.`,
1025+
);
1026+
return request;
1027+
}
1028+
}
1029+
10041030
// Issuer is not empty string when the module is imported by another module.
10051031
// Prevent from externalizing entry modules here.
1006-
if (contextInfo.issuer) {
1032+
if (issuer) {
10071033
let resolvedRequest: string = request;
10081034

1009-
const cssExternal = cssExternalHandler(
1035+
const cssExternal = await cssExternalHandler(
10101036
resolvedRequest,
10111037
callback,
10121038
jsExtension,
10131039
cssModulesAuto,
1014-
isStyleRedirected,
1040+
styleRedirectPath,
1041+
styleRedirectExtension,
1042+
redirectPath,
10151043
);
10161044

10171045
if (cssExternal !== false) {
10181046
return cssExternal;
10191047
}
10201048

10211049
if (jsRedirectPath) {
1022-
try {
1023-
resolvedRequest = await resolver(context, resolvedRequest);
1024-
resolvedRequest = normalizeSlash(
1025-
path.relative(
1026-
path.dirname(contextInfo.issuer),
1027-
resolvedRequest,
1028-
),
1029-
);
1030-
// Requests that fall through here cannot be matched by any other externals config ahead.
1031-
// Treat all these requests as relative import of source code. Node.js won't add the
1032-
// leading './' to the relative path resolved by `path.relative`. So add manually it here.
1033-
if (resolvedRequest[0] !== '.') {
1034-
resolvedRequest = `./${resolvedRequest}`;
1035-
}
1036-
} catch (e) {
1037-
// e.g. A react component library importing and using 'react' but while not defining
1038-
// it in devDependencies and peerDependencies. Preserve 'react' as-is if so.
1039-
logger.warn(
1040-
`Failed to resolve module ${color.green(`"${resolvedRequest}"`)} from ${color.green(contextInfo.issuer)}. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.`,
1041-
);
1042-
}
1050+
resolvedRequest = await redirectPath(resolvedRequest);
10431051
}
10441052

10451053
// Node.js ECMAScript module loader does no extension searching.

packages/core/src/css/cssConfig.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,42 @@ export function isCssGlobalFile(
7979

8080
type ExternalCallback = (arg0?: undefined, arg1?: string) => void;
8181

82-
export function cssExternalHandler(
82+
export async function cssExternalHandler(
8383
request: string,
8484
callback: ExternalCallback,
8585
jsExtension: string,
8686
auto: CssLoaderOptionsAuto,
87-
isStyleRedirect: boolean,
88-
): void | false {
89-
const isCssModulesRequest = isCssModulesFile(request, auto);
90-
87+
styleRedirectPath: boolean,
88+
styleRedirectExtension: boolean,
89+
redirectPath: (request: string, emitWarning: boolean) => Promise<string>,
90+
): Promise<false | void> {
9191
// cssExtract would execute the file handled by css-loader, so we cannot external the "helper import" from css-loader
9292
// do not external @rsbuild/core/compiled/css-loader/noSourceMaps.js, sourceMaps.js, api.mjs etc.
9393
if (/compiled\/css-loader\//.test(request)) {
9494
return callback();
9595
}
9696

97+
let resolvedRequest = request;
98+
99+
if (styleRedirectPath) {
100+
resolvedRequest = await redirectPath(request, false);
101+
}
102+
97103
// 1. css modules: import './CounterButton.module.scss' -> import './CounterButton.module.mjs'
98104
// 2. css global: import './CounterButton.scss' -> import './CounterButton.css'
99-
if (request[0] === '.' && isCssFile(request)) {
105+
if (resolvedRequest[0] === '.' && isCssFile(resolvedRequest)) {
100106
// preserve import './CounterButton.module.scss'
101-
if (!isStyleRedirect) {
102-
return callback(undefined, request);
107+
if (!styleRedirectExtension) {
108+
return callback(undefined, resolvedRequest);
103109
}
110+
const isCssModulesRequest = isCssModulesFile(resolvedRequest, auto);
104111
if (isCssModulesRequest) {
105-
return callback(undefined, request.replace(/\.[^.]+$/, jsExtension));
112+
return callback(
113+
undefined,
114+
resolvedRequest.replace(/\.[^.]+$/, jsExtension),
115+
);
106116
}
107-
return callback(undefined, request.replace(/\.[^.]+$/, '.css'));
117+
return callback(undefined, resolvedRequest.replace(/\.[^.]+$/, '.css'));
108118
}
109119

110120
return false;

packages/core/src/types/config.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ export type JsRedirect = {
9191
extension?: boolean;
9292
};
9393

94+
export type StyleRedirect = {
95+
/**
96+
* Whether to automatically redirect the import paths of JavaScript output files.
97+
* @defaultValue `true`
98+
*/
99+
path?: boolean;
100+
/**
101+
* Whether to automatically add the file extension to import paths based on the JavaScript output files.
102+
* @defaultValue `true`
103+
*/
104+
extension?: boolean;
105+
};
106+
94107
// @ts-expect-error TODO: support dts redirect in the future
95108
type DtsRedirect = {
96109
path?: boolean;
@@ -101,7 +114,7 @@ export type Redirect = {
101114
/** Controls the redirect of the import paths of output JavaScript files. */
102115
js?: JsRedirect;
103116
/** Whether to redirect the import path of the style file. */
104-
style?: boolean;
117+
style?: StyleRedirect;
105118
// TODO: support other redirects
106119
// asset?: boolean;
107120
// dts?: DtsRedirect;

pnpm-lock.yaml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/redirect/jsNotResolved.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ test('redirect.js default', async () => {
1818
`
1919
[
2020
"warn Failed to resolve module "./bar.js" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
21+
"warn Failed to resolve module "./bar.js" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
22+
"warn Failed to resolve module "./foo" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
2123
"warn Failed to resolve module "./foo" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
2224
"warn Failed to resolve module "lodash" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
25+
"warn Failed to resolve module "lodash" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
2326
]
2427
`,
2528
);
@@ -81,8 +84,11 @@ test('redirect.js.extension: false', async () => {
8184
`
8285
[
8386
"warn Failed to resolve module "./bar.js" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
87+
"warn Failed to resolve module "./bar.js" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
88+
"warn Failed to resolve module "./foo" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
8489
"warn Failed to resolve module "./foo" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
8590
"warn Failed to resolve module "lodash" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
91+
"warn Failed to resolve module "lodash" from <ROOT>/tests/integration/redirect/js-not-resolve/src/index.js. If it's an npm package, consider adding it to dependencies or peerDependencies in package.json to make it externalized.",
8692
]
8793
`,
8894
);

tests/integration/redirect/style-false/rslib.config.ts renamed to tests/integration/redirect/style-extension/rslib.config.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@ export default defineConfig({
66
generateBundleEsmConfig({
77
bundle: false,
88
redirect: {
9-
style: false,
9+
style: {
10+
extension: false,
11+
},
1012
},
1113
}),
1214
generateBundleCjsConfig({
1315
bundle: false,
1416
redirect: {
15-
style: false,
17+
style: {
18+
extension: false,
19+
},
1620
},
1721
}),
1822
],
1923
source: {
2024
entry: {
21-
index: ['./src/index.ts'],
25+
index: ['./src/**', '!./src/**/*.less'],
2226
},
2327
},
2428
output: {
2529
target: 'web',
26-
copy: [{ from: './src/index.less' }, { from: './src/style.module.less' }],
30+
copy: [{ from: './**/*.less', context: './src' }],
2731
},
2832
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './index.less';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// @ts-ignore env.d.ts
2+
import styles from './index.module.less';
3+
4+
styles;

tests/integration/redirect/style-false/src/index.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "redirect-js-import-css-test",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module"
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineConfig } from '@rslib/core';
2+
import { generateBundleCjsConfig, generateBundleEsmConfig } from 'test-helper';
3+
4+
export default defineConfig({
5+
lib: [
6+
generateBundleEsmConfig({
7+
bundle: false,
8+
}),
9+
generateBundleCjsConfig({
10+
bundle: false,
11+
}),
12+
],
13+
source: {
14+
entry: {
15+
index: ['./src/**'],
16+
},
17+
},
18+
output: {
19+
target: 'web',
20+
},
21+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo {
2+
color: red;
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@/css/index.css';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.foo {
2+
color: red;
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// @ts-ignore env.d.ts
2+
import styles from '@/module/index.module.css';
3+
4+
styles;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "@rslib/tsconfig/base",
3+
"compilerOptions": {
4+
"baseUrl": "./",
5+
"paths": {
6+
"@/*": ["./src/*"]
7+
}
8+
},
9+
"include": ["src"]
10+
}

tests/integration/redirect/style.test.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import path from 'node:path';
2+
import { buildAndGetResults, getFileBySuffix } from 'test-helper';
3+
import { expect, test } from 'vitest';
4+
5+
test('should extract css successfully when using redirect.style = false', async () => {
6+
const fixturePath = path.resolve(__dirname, './style-extension');
7+
const { contents } = await buildAndGetResults({ fixturePath });
8+
const esmFiles = Object.keys(contents.esm);
9+
expect(esmFiles).toMatchInlineSnapshot(`
10+
[
11+
"<ROOT>/tests/integration/redirect/style-extension/dist/esm/less/index.js",
12+
"<ROOT>/tests/integration/redirect/style-extension/dist/esm/module/index.js",
13+
]
14+
`);
15+
const lessIndexJs = getFileBySuffix(contents.esm, 'less/index.js');
16+
expect(lessIndexJs).toMatchInlineSnapshot(`
17+
"import "./index.less";
18+
"
19+
`);
20+
const lessModuleIndexJs = getFileBySuffix(contents.esm, 'module/index.js');
21+
expect(lessModuleIndexJs).toMatchInlineSnapshot(`
22+
"import * as __WEBPACK_EXTERNAL_MODULE__index_module_less__ from "./index.module.less";
23+
__WEBPACK_EXTERNAL_MODULE__index_module_less__["default"];
24+
"
25+
`);
26+
27+
const cjsFiles = Object.keys(contents.cjs);
28+
expect(cjsFiles).toMatchInlineSnapshot(`
29+
[
30+
"<ROOT>/tests/integration/redirect/style-extension/dist/cjs/less/index.cjs",
31+
"<ROOT>/tests/integration/redirect/style-extension/dist/cjs/module/index.cjs",
32+
]
33+
`);
34+
const lessIndexCjs = getFileBySuffix(contents.cjs, 'less/index.cjs');
35+
expect(lessIndexCjs).toContain('require("./index.less");');
36+
const lessModuleIndexCjs = getFileBySuffix(contents.cjs, 'module/index.cjs');
37+
expect(lessModuleIndexCjs).toContain('require("./index.module.less")');
38+
});

0 commit comments

Comments
 (0)