Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions e2e/cases/css/inline-query/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { build, dev, rspackOnlyTest } from '@e2e/helper';
import { expect } from '@playwright/test';

Expand Down
43 changes: 43 additions & 0 deletions e2e/cases/sass/inline-query/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { build, dev } from '@e2e/helper';
import { expect, test } from '@playwright/test';

test('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');

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();
});

test('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}',
);

await rsbuild.close();
});
File renamed without changes.
5 changes: 5 additions & 0 deletions e2e/cases/sass/inline-query/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import aInline from './a.scss?inline';
import bInline from './b.module.scss?inline';

window.aInline = aInline;
window.bInline = bInline;
5 changes: 5 additions & 0 deletions e2e/cases/sass/raw-query/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { pluginSass } from '@rsbuild/plugin-sass';

export default {
plugins: [pluginSass()],
};
5 changes: 5 additions & 0 deletions e2e/cases/sass/raw-query/src/a.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.header {
&-class {
color: red;
}
}
5 changes: 5 additions & 0 deletions e2e/cases/sass/raw-query/src/b.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.title {
&-class {
font-size: 14px;
}
}
File renamed without changes.
2 changes: 2 additions & 0 deletions packages/core/src/configChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export const CHAIN_ID = {
SASS: 'sass',
/** Rule for raw Sass */
SASS_RAW: 'sass-raw',
/** Rule for inline Sass */
SASS_INLINE: 'sass-inline',
/** Rule for stylus */
STYLUS: 'stylus',
/** Rule for raw stylus */
Expand Down
8 changes: 8 additions & 0 deletions packages/core/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ declare module '*.css?inline' {
const content: string;
export default content;
}
declare module '*.scss?inline' {
const content: string;
export default content;
}
declare module '*.sass?inline' {
const content: string;
export default content;
}

/**
* Raw CSS
Expand Down
119 changes: 69 additions & 50 deletions packages/plugin-sass/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const pluginSass = (
name: PLUGIN_SASS_NAME,

setup(api) {
const { rewriteUrls = true } = pluginOptions;
const { rewriteUrls = true, include = /\.s(?:a|c)ss$/ } = pluginOptions;

api.onAfterCreateCompiler(({ compiler }) => {
patchCompilerGlobalLocation(compiler);
Expand All @@ -117,73 +117,92 @@ export const pluginSass = (
rewriteUrls ? true : isUseSourceMap,
);

const ruleId = findRuleId(chain, CHAIN_ID.RULE.SASS);
const test = pluginOptions.include ?? /\.s(?:a|c)ss$/;
const rule = chain.module
.rule(ruleId)
.test(test)
.rule(findRuleId(chain, CHAIN_ID.RULE.SASS))
.test(include)
// exclude `import './foo.scss?raw'`
.resourceQuery({ not: /raw|inline/ })
.sideEffects(true)
.resolve.preferRelative(true)
.end()
// exclude `import './foo.scss?raw'`
.resourceQuery({ not: /raw/ });
.end();

const inlineRule = chain.module
.rule(findRuleId(chain, CHAIN_ID.RULE.SASS_INLINE))
.test(include)
.resourceQuery(/inline/);

// Support for importing raw Sass files
chain.module
.rule(CHAIN_ID.RULE.SASS_RAW)
.test(test)
.test(include)
.type('asset/source')
.resourceQuery(/raw/);

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',
) => Promise<void>,
) =>
Promise.all([callback(rule, 'normal'), callback(inlineRule, 'inline')]);

const sassLoaderPath = path.join(
__dirname,
'../compiled/sass-loader/index.js',
);

if (pluginOptions.exclude) {
rule.exclude.add(pluginOptions.exclude);
}
const resolveUrlLoaderPath = path.join(
__dirname,
'../compiled/resolve-url-loader/index.js',
);

// Copy the builtin CSS rules
const cssRule = chain.module.rules.get(CHAIN_ID.RULE.CSS);
rule.dependency(cssRule.get('dependency'));
const resolveUrlLoaderOptions = {
join: await getResolveUrlJoinFn(),
// 'resolve-url-loader' relies on 'adjust-sourcemap-loader',
// it has performance regression issues in some scenarios,
// so we need to disable the sourceMap option.
sourceMap: false,
};

await updateRules(async (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(
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<Record<string, any>>({}, options);
for (const id of Object.keys(cssRule.uses.entries())) {
const loader = cssRule.uses.get(id);
const options = loader.get('options') ?? {};
const clonedOptions = deepmerge<Record<string, any>>({}, options);

if (id === CHAIN_ID.USE.CSS) {
// add resolve-url-loader
if (rewriteUrls) {
clonedOptions.importLoaders += 1;
if (id === CHAIN_ID.USE.CSS) {
// add sass-loader and resolve-url-loader
clonedOptions.importLoaders += rewriteUrls ? 2 : 1;
}
// add sass-loader
clonedOptions.importLoaders += 1;

rule.use(id).loader(loader.get('loader')).options(clonedOptions);
}

rule.use(id).loader(loader.get('loader')).options(clonedOptions);
}
// use `resolve-url-loader` to rewrite urls
if (rewriteUrls) {
rule
.use(CHAIN_ID.USE.RESOLVE_URL)
.loader(resolveUrlLoaderPath)
.options(resolveUrlLoaderOptions)
.end();
}

// use `resolve-url-loader` to rewrite urls
if (rewriteUrls)
rule
.use(CHAIN_ID.USE.RESOLVE_URL)
.loader(
path.join(__dirname, '../compiled/resolve-url-loader/index.js'),
)
.options({
join: await getResolveUrlJoinFn(),
// 'resolve-url-loader' relies on 'adjust-sourcemap-loader',
// it has performance regression issues in some scenarios,
// so we need to disable the sourceMap option.
sourceMap: false,
})
.end();

rule
.use(CHAIN_ID.USE.SASS)
.loader(path.join(__dirname, '../compiled/sass-loader/index.js'))
.options(options);
rule.use(CHAIN_ID.USE.SASS).loader(sassLoaderPath).options(options);
});
});
},
});
Loading
Loading