diff --git a/e2e/cases/less/inline-query/index.test.ts b/e2e/cases/less/inline-query/index.test.ts new file mode 100644 index 0000000000..4d301508b8 --- /dev/null +++ b/e2e/cases/less/inline-query/index.test.ts @@ -0,0 +1,43 @@ +import { build, dev, rspackOnlyTest } from '@e2e/helper'; +import { expect } from '@playwright/test'; + +rspackOnlyTest( + 'should allow to import inline Less files in development mode', + async ({ page }) => { + const rsbuild = await dev({ + cwd: __dirname, + page, + }); + + const aInline: string = await page.evaluate('window.aInline'); + const bInline: string = await page.evaluate('window.bInline'); + + expect( + aInline.includes('.header-class') && aInline.includes('color: red'), + ).toBe(true); + expect( + bInline.includes('.title-class') && bInline.includes('font-size: 14px'), + ).toBe(true); + + await rsbuild.close(); + }, +); + +rspackOnlyTest( + 'should allow to import inline Less files in production mode', + async ({ page }) => { + const rsbuild = await build({ + cwd: __dirname, + page, + }); + + expect(await page.evaluate('window.aInline')).toBe( + '.header-class{color:red}', + ); + expect(await page.evaluate('window.bInline')).toBe( + '.title-class{font-size:14px}', + ); + + await rsbuild.close(); + }, +); diff --git a/e2e/cases/less/raw/rsbuild.config.ts b/e2e/cases/less/inline-query/rsbuild.config.ts similarity index 100% rename from e2e/cases/less/raw/rsbuild.config.ts rename to e2e/cases/less/inline-query/rsbuild.config.ts diff --git a/e2e/cases/less/raw/src/a.less b/e2e/cases/less/inline-query/src/a.less similarity index 100% rename from e2e/cases/less/raw/src/a.less rename to e2e/cases/less/inline-query/src/a.less diff --git a/e2e/cases/less/raw/src/b.module.less b/e2e/cases/less/inline-query/src/b.module.less similarity index 100% rename from e2e/cases/less/raw/src/b.module.less rename to e2e/cases/less/inline-query/src/b.module.less diff --git a/e2e/cases/less/inline-query/src/index.js b/e2e/cases/less/inline-query/src/index.js new file mode 100644 index 0000000000..c4aed4af9e --- /dev/null +++ b/e2e/cases/less/inline-query/src/index.js @@ -0,0 +1,5 @@ +import aInline from './a.less?inline'; +import bInline from './b.module.less?inline'; + +window.aInline = aInline; +window.bInline = bInline; diff --git a/e2e/cases/less/raw/index.test.ts b/e2e/cases/less/raw-query/index.test.ts similarity index 100% rename from e2e/cases/less/raw/index.test.ts rename to e2e/cases/less/raw-query/index.test.ts diff --git a/e2e/cases/less/raw-query/rsbuild.config.ts b/e2e/cases/less/raw-query/rsbuild.config.ts new file mode 100644 index 0000000000..cc23b722e5 --- /dev/null +++ b/e2e/cases/less/raw-query/rsbuild.config.ts @@ -0,0 +1,5 @@ +import { pluginLess } from '@rsbuild/plugin-less'; + +export default { + plugins: [pluginLess()], +}; diff --git a/e2e/cases/less/raw-query/src/a.less b/e2e/cases/less/raw-query/src/a.less new file mode 100644 index 0000000000..ce27061885 --- /dev/null +++ b/e2e/cases/less/raw-query/src/a.less @@ -0,0 +1,5 @@ +.header { + &-class { + color: red; + } +} diff --git a/e2e/cases/less/raw-query/src/b.module.less b/e2e/cases/less/raw-query/src/b.module.less new file mode 100644 index 0000000000..b640e3f1b5 --- /dev/null +++ b/e2e/cases/less/raw-query/src/b.module.less @@ -0,0 +1,5 @@ +.title { + &-class { + font-size: 14px; + } +} diff --git a/e2e/cases/less/raw/src/index.js b/e2e/cases/less/raw-query/src/index.js similarity index 100% rename from e2e/cases/less/raw/src/index.js rename to e2e/cases/less/raw-query/src/index.js diff --git a/e2e/cases/sass/inline-query/index.test.ts b/e2e/cases/sass/inline-query/index.test.ts index 8a2753a706..a644791d32 100644 --- a/e2e/cases/sass/inline-query/index.test.ts +++ b/e2e/cases/sass/inline-query/index.test.ts @@ -1,43 +1,43 @@ -import { readFileSync } from 'node:fs'; -import path from 'node:path'; -import { build, dev } from '@e2e/helper'; -import { expect, test } from '@playwright/test'; +import { build, dev, rspackOnlyTest } from '@e2e/helper'; +import { expect } from '@playwright/test'; -test('should allow to import inline Sass files in development mode', async ({ - page, -}) => { - const rsbuild = await dev({ - cwd: __dirname, - page, - }); +rspackOnlyTest( + 'should allow to import inline Sass files in development mode', + async ({ page }) => { + const rsbuild = await dev({ + cwd: __dirname, + page, + }); - const aInline: string = await page.evaluate('window.aInline'); - const bInline: string = await page.evaluate('window.bInline'); + const aInline: string = await page.evaluate('window.aInline'); + const bInline: string = await page.evaluate('window.bInline'); - expect( - aInline.includes('.header-class') && aInline.includes('color: red'), - ).toBe(true); - expect( - bInline.includes('.title-class') && bInline.includes('font-size: 14px'), - ).toBe(true); + expect( + aInline.includes('.header-class') && aInline.includes('color: red'), + ).toBe(true); + expect( + bInline.includes('.title-class') && bInline.includes('font-size: 14px'), + ).toBe(true); - await rsbuild.close(); -}); + await rsbuild.close(); + }, +); -test('should allow to import inline Sass files in production mode', async ({ - page, -}) => { - const rsbuild = await build({ - cwd: __dirname, - page, - }); +rspackOnlyTest( + 'should allow to import inline Sass files in production mode', + async ({ page }) => { + const rsbuild = await build({ + cwd: __dirname, + page, + }); - expect(await page.evaluate('window.aInline')).toBe( - '.header-class{color:red}', - ); - expect(await page.evaluate('window.bInline')).toBe( - '.title-class{font-size:14px}', - ); + expect(await page.evaluate('window.aInline')).toBe( + '.header-class{color:red}', + ); + expect(await page.evaluate('window.bInline')).toBe( + '.title-class{font-size:14px}', + ); - await rsbuild.close(); -}); + await rsbuild.close(); + }, +); diff --git a/packages/core/src/configChain.ts b/packages/core/src/configChain.ts index 50ca5de33f..2c6c1a8cc4 100644 --- a/packages/core/src/configChain.ts +++ b/packages/core/src/configChain.ts @@ -57,6 +57,8 @@ export const CHAIN_ID = { LESS: 'less', /** Rule for raw Less */ LESS_RAW: 'less-raw', + /** Rule for inline Less */ + LESS_INLINE: 'less-inline', /** Rule for Sass */ SASS: 'sass', /** Rule for raw Sass */ diff --git a/packages/core/types.d.ts b/packages/core/types.d.ts index 07661e0858..1ba8ddf13e 100644 --- a/packages/core/types.d.ts +++ b/packages/core/types.d.ts @@ -199,6 +199,10 @@ declare module '*.sass?inline' { const content: string; export default content; } +declare module '*.less?inline' { + const content: string; + export default content; +} /** * Raw CSS diff --git a/packages/plugin-less/src/index.ts b/packages/plugin-less/src/index.ts index 15b023d3e0..f9bdb1fed9 100644 --- a/packages/plugin-less/src/index.ts +++ b/packages/plugin-less/src/index.ts @@ -135,24 +135,29 @@ export const pluginLess = ( name: PLUGIN_LESS_NAME, setup(api) { + const { include = /\.less$/ } = pluginOptions; + api.modifyBundlerChain(async (chain, { CHAIN_ID, environment }) => { const { config } = environment; - const ruleId = findRuleId(chain, CHAIN_ID.RULE.LESS); - const test = pluginOptions.include ?? /\.less$/; const rule = chain.module - .rule(ruleId) - .test(test) + .rule(findRuleId(chain, CHAIN_ID.RULE.LESS)) + .test(include) + // exclude `import './foo.less?raw'` and `import './foo.less?inline'` + .resourceQuery({ not: /raw|inline/ }) .sideEffects(true) .resolve.preferRelative(true) - .end() - // exclude `import './foo.less?raw'` - .resourceQuery({ not: /raw/ }); + .end(); + + const inlineRule = chain.module + .rule(findRuleId(chain, CHAIN_ID.RULE.LESS_INLINE)) + .test(include) + .resourceQuery(/inline/); // Support for importing raw Less files chain.module .rule(CHAIN_ID.RULE.LESS_RAW) - .test(test) + .test(include) .type('asset/source') .resourceQuery(/raw/); @@ -163,35 +168,48 @@ export const pluginLess = ( api.context.rootPath, ); - for (const item of excludes) { - rule.exclude.add(item); - } + // Update the normal rule and the inline rule + const updateRules = ( + callback: (rule: RspackChain.Rule, type: 'normal' | 'inline') => void, + ) => { + callback(rule, 'normal'); + callback(inlineRule, 'inline'); + }; + + const lessLoaderPath = path.join( + __dirname, + '../compiled/less-loader/index.js', + ); - if (pluginOptions.exclude) { - rule.exclude.add(pluginOptions.exclude); - } + updateRules((rule, type) => { + for (const item of excludes) { + rule.exclude.add(item); + } + if (pluginOptions.exclude) { + rule.exclude.add(pluginOptions.exclude); + } - // Copy the builtin CSS rules - const cssRule = chain.module.rules.get(CHAIN_ID.RULE.CSS); - rule.dependency(cssRule.get('dependency')); + // Copy the builtin CSS rules + const cssRule = chain.module.rules.get( + type === 'normal' ? CHAIN_ID.RULE.CSS : CHAIN_ID.RULE.CSS_INLINE, + ); + rule.dependency(cssRule.get('dependency')); - for (const id of Object.keys(cssRule.uses.entries())) { - const loader = cssRule.uses.get(id); - const options = loader.get('options') ?? {}; - const clonedOptions = deepmerge>({}, options); + for (const id of Object.keys(cssRule.uses.entries())) { + const loader = cssRule.uses.get(id); + const options = loader.get('options') ?? {}; + const clonedOptions = deepmerge>({}, options); - if (id === CHAIN_ID.USE.CSS) { - // add less-loader - clonedOptions.importLoaders += 1; - } + if (id === CHAIN_ID.USE.CSS) { + // add less-loader + clonedOptions.importLoaders += 1; + } - rule.use(id).loader(loader.get('loader')).options(clonedOptions); - } + rule.use(id).loader(loader.get('loader')).options(clonedOptions); + } - rule - .use(CHAIN_ID.USE.LESS) - .loader(path.join(__dirname, '../compiled/less-loader/index.js')) - .options(options); + rule.use(CHAIN_ID.USE.LESS).loader(lessLoaderPath).options(options); + }); }); }, }); diff --git a/packages/plugin-less/tests/__snapshots__/index.test.ts.snap b/packages/plugin-less/tests/__snapshots__/index.test.ts.snap index 02f6df831b..c2d712018a 100644 --- a/packages/plugin-less/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-less/tests/__snapshots__/index.test.ts.snap @@ -10,7 +10,7 @@ exports[`plugin-less > should add less-loader 1`] = ` "preferRelative": true, }, "resourceQuery": { - "not": /raw/, + "not": /raw\\|inline/, }, "sideEffects": true, "test": /\\\\\\.less\\$/, @@ -58,6 +58,45 @@ exports[`plugin-less > should add less-loader 1`] = ` }, ], }, + { + "resourceQuery": /inline/, + "test": /\\\\\\.less\\$/, + "use": [ + { + "loader": "/packages/core/compiled/css-loader/index.js", + "options": { + "exportType": "string", + "importLoaders": 2, + "modules": false, + "sourceMap": false, + }, + }, + { + "loader": "builtin:lightningcss-loader", + "options": { + "targets": [ + "chrome >= 87", + "edge >= 88", + "firefox >= 78", + "safari >= 14", + ], + }, + }, + { + "loader": "/packages/plugin-less/compiled/less-loader/index.js", + "options": { + "implementation": "/packages/plugin-less/compiled/less/index.js", + "lessOptions": { + "javascriptEnabled": true, + "paths": [ + "/node_modules", + ], + }, + "sourceMap": false, + }, + }, + ], + }, { "resourceQuery": /raw/, "test": /\\\\\\.less\\$/, @@ -76,7 +115,7 @@ exports[`plugin-less > should add less-loader and css-loader when injectStyles 1 "preferRelative": true, }, "resourceQuery": { - "not": /raw/, + "not": /raw\\|inline/, }, "sideEffects": true, "test": /\\\\\\.less\\$/, @@ -124,6 +163,45 @@ exports[`plugin-less > should add less-loader and css-loader when injectStyles 1 }, ], }, + { + "resourceQuery": /inline/, + "test": /\\\\\\.less\\$/, + "use": [ + { + "loader": "/packages/core/compiled/css-loader/index.js", + "options": { + "exportType": "string", + "importLoaders": 2, + "modules": false, + "sourceMap": false, + }, + }, + { + "loader": "builtin:lightningcss-loader", + "options": { + "targets": [ + "chrome >= 87", + "edge >= 88", + "firefox >= 78", + "safari >= 14", + ], + }, + }, + { + "loader": "/packages/plugin-less/compiled/less-loader/index.js", + "options": { + "implementation": "/packages/plugin-less/compiled/less/index.js", + "lessOptions": { + "javascriptEnabled": true, + "paths": [ + "/node_modules", + ], + }, + "sourceMap": false, + }, + }, + ], + }, { "resourceQuery": /raw/, "test": /\\\\\\.less\\$/, @@ -145,7 +223,7 @@ exports[`plugin-less > should add less-loader with excludes 1`] = ` "preferRelative": true, }, "resourceQuery": { - "not": /raw/, + "not": /raw\\|inline/, }, "sideEffects": true, "test": /\\\\\\.less\\$/, @@ -193,6 +271,48 @@ exports[`plugin-less > should add less-loader with excludes 1`] = ` }, ], }, + { + "exclude": [ + /node_modules/, + ], + "resourceQuery": /inline/, + "test": /\\\\\\.less\\$/, + "use": [ + { + "loader": "/packages/core/compiled/css-loader/index.js", + "options": { + "exportType": "string", + "importLoaders": 2, + "modules": false, + "sourceMap": false, + }, + }, + { + "loader": "builtin:lightningcss-loader", + "options": { + "targets": [ + "chrome >= 87", + "edge >= 88", + "firefox >= 78", + "safari >= 14", + ], + }, + }, + { + "loader": "/packages/plugin-less/compiled/less-loader/index.js", + "options": { + "implementation": "/packages/plugin-less/compiled/less/index.js", + "lessOptions": { + "javascriptEnabled": true, + "paths": [ + "/node_modules", + ], + }, + "sourceMap": false, + }, + }, + ], + }, { "resourceQuery": /raw/, "test": /\\\\\\.less\\$/, @@ -211,7 +331,7 @@ exports[`plugin-less > should add less-loader with tools.less 1`] = ` "preferRelative": true, }, "resourceQuery": { - "not": /raw/, + "not": /raw\\|inline/, }, "sideEffects": true, "test": /\\\\\\.less\\$/, @@ -259,6 +379,45 @@ exports[`plugin-less > should add less-loader with tools.less 1`] = ` }, ], }, + { + "resourceQuery": /inline/, + "test": /\\\\\\.less\\$/, + "use": [ + { + "loader": "/packages/core/compiled/css-loader/index.js", + "options": { + "exportType": "string", + "importLoaders": 2, + "modules": false, + "sourceMap": false, + }, + }, + { + "loader": "builtin:lightningcss-loader", + "options": { + "targets": [ + "chrome >= 87", + "edge >= 88", + "firefox >= 78", + "safari >= 14", + ], + }, + }, + { + "loader": "/packages/plugin-less/compiled/less-loader/index.js", + "options": { + "implementation": "/packages/plugin-less/compiled/less/index.js", + "lessOptions": { + "javascriptEnabled": false, + "paths": [ + "/node_modules", + ], + }, + "sourceMap": false, + }, + }, + ], + }, { "resourceQuery": /raw/, "test": /\\\\\\.less\\$/, @@ -277,7 +436,7 @@ exports[`plugin-less > should allow to use Less plugins 1`] = ` "preferRelative": true, }, "resourceQuery": { - "not": /raw/, + "not": /raw\\|inline/, }, "sideEffects": true, "test": /\\\\\\.less\\$/, @@ -332,6 +491,52 @@ exports[`plugin-less > should allow to use Less plugins 1`] = ` }, ], }, + { + "resourceQuery": /inline/, + "test": /\\\\\\.less\\$/, + "use": [ + { + "loader": "/packages/core/compiled/css-loader/index.js", + "options": { + "exportType": "string", + "importLoaders": 2, + "modules": false, + "sourceMap": false, + }, + }, + { + "loader": "builtin:lightningcss-loader", + "options": { + "targets": [ + "chrome >= 87", + "edge >= 88", + "firefox >= 78", + "safari >= 14", + ], + }, + }, + { + "loader": "/packages/plugin-less/compiled/less-loader/index.js", + "options": { + "implementation": "/packages/plugin-less/compiled/less/index.js", + "lessOptions": { + "javascriptEnabled": true, + "paths": [ + "/node_modules", + ], + "plugins": [ + MockPlugin { + "options": { + "foo": "bar", + }, + }, + ], + }, + "sourceMap": false, + }, + }, + ], + }, { "resourceQuery": /raw/, "test": /\\\\\\.less\\$/, diff --git a/packages/plugin-less/tests/index.test.ts b/packages/plugin-less/tests/index.test.ts index f5a7443516..7712a9ce15 100644 --- a/packages/plugin-less/tests/index.test.ts +++ b/packages/plugin-less/tests/index.test.ts @@ -108,7 +108,7 @@ describe('plugin-less', () => { }); const bundlerConfigs = await rsbuild.initConfigs(); - expect(matchRules(bundlerConfigs[0], 'a.less').length).toBe(1); - expect(matchRules(bundlerConfigs[0], 'b.less').length).toBe(3); + expect(matchRules(bundlerConfigs[0], 'a.less').length).toBe(2); + expect(matchRules(bundlerConfigs[0], 'b.less').length).toBe(5); }); }); diff --git a/packages/plugin-sass/src/index.ts b/packages/plugin-sass/src/index.ts index 8f2ca91d32..0eb5e9fd83 100644 --- a/packages/plugin-sass/src/index.ts +++ b/packages/plugin-sass/src/index.ts @@ -120,7 +120,7 @@ export const pluginSass = ( const rule = chain.module .rule(findRuleId(chain, CHAIN_ID.RULE.SASS)) .test(include) - // exclude `import './foo.scss?raw'` + // exclude `import './foo.scss?raw'` and `import './foo.scss?inline'` .resourceQuery({ not: /raw|inline/ }) .sideEffects(true) .resolve.preferRelative(true)