Skip to content
Open
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
1,466 changes: 530 additions & 936 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions scopes/preview/preview/rspack/rspack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ export function createRspackConfig(outputDir: string, entryFile: string): Config
mode,

devtool: shouldUseSourceMap ? 'source-map' : false,
experiments: {
css: true,
},

entry: {
main: entryFile,
Expand Down
3 changes: 0 additions & 3 deletions scopes/ui-foundation/ui/rspack/rspack.browser.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ export default function createRspackBrowserConfig(
mode: 'production',

devtool: shouldUseSourceMap ? 'source-map' : false,
experiments: {
css: true,
},

entry: Object.fromEntries(
(() => {
Expand Down
78 changes: 78 additions & 0 deletions scopes/ui-foundation/ui/rspack/rspack.common.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os from 'os';
import { join } from 'path';
import { promisify } from 'util';
import fs from 'fs-extra';
import { expect } from 'chai';
import { rspack } from '@rspack/core';
import { cssParser, styleRules, fontRule } from './rspack.common';

/**
* Regression coverage for the node_modules/first-party split in `styleRules()`. Rspack v2's CSS
* handler treats every `url()` as a module to read, which throws on an absolute (e.g. CDN)
* `https:` url - vendored stylesheets are exempted from `url` resolution for that reason (see
* `vendorCssParser` in rspack.common.ts). This build proves the exemption is scoped correctly:
* a first-party stylesheet's local relative url still goes through the real asset pipeline, and
* only a vendored (node_modules) stylesheet's absolute url is left untouched.
*/
describe('styleRules', () => {
let tmpDir: string;
let outDir: string;

before(() => {
tmpDir = fs.mkdtempSync(join(os.tmpdir(), 'bit-rspack-style-rules-'));
outDir = join(tmpDir, 'dist');

fs.outputFileSync(join(tmpDir, 'src/first-party.module.scss'), `.icon { background: url('./icon.svg'); }\n`);
// padded well past rspack's default inline-asset threshold (~8KB), so this emits as a
// separate file - like a real font/image asset would - instead of an inlined data: URI.
fs.outputFileSync(join(tmpDir, 'src/icon.svg'), `<svg><!-- ${'x'.repeat(9000)} --></svg>`);
fs.outputFileSync(
join(tmpDir, 'src/entry.js'),
`import './first-party.module.scss';\nimport 'vendor-pkg/vendor.module.scss';\n`
);
fs.outputFileSync(
join(tmpDir, 'node_modules/vendor-pkg/vendor.module.scss'),
`.brand { background: url('https://cdn.example.com/font.woff2'); }\n`
);
});

after(() => {
fs.removeSync(tmpDir);
});

it('resolves a first-party relative url() through the asset pipeline while leaving a vendored absolute url() untouched', async () => {
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
const compiler = rspack({
context: tmpDir,
entry: join(tmpDir, 'src/entry.js'),
mode: 'production',
output: { path: outDir, filename: 'bundle.js' },
module: {
parser: cssParser,
rules: [...styleRules({ sourceMap: false }), fontRule()],
},
} as any);

try {
const run = promisify(compiler.run.bind(compiler));
const stats = await run();
expect(stats?.hasErrors(), stats?.toString({ errorDetails: true })).to.be.false;

const assetNames = Object.keys((stats as any).compilation.assets);
const cssAssetName = assetNames.find((name) => name.endsWith('.css'));
expect(cssAssetName, `no .css asset emitted, got: ${assetNames.join(', ')}`).to.exist;
const css = fs.readFileSync(join(outDir, cssAssetName as string), 'utf8');

// vendored url() is left as literal text - not resolved, not fetched.
expect(css).to.include('https://cdn.example.com/font.woff2');

// first-party url() went through fontRule's asset pipeline instead of being left literal.
expect(css).to.not.include("url('./icon.svg')");
expect(css).to.not.include('url("./icon.svg")');
const emittedIconName = assetNames.find((name) => name.includes('static/fonts') && name.endsWith('.svg'));
expect(emittedIconName, `no emitted icon asset, got: ${assetNames.join(', ')}`).to.exist;
expect(fs.existsSync(join(outDir, emittedIconName as string))).to.be.true;
} finally {
await new Promise<void>((done) => compiler.close(() => done()));
}
});
});
85 changes: 58 additions & 27 deletions scopes/ui-foundation/ui/rspack/rspack.common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RuleSetRule } from '@rspack/core';
import type { RuleSetRule, RuleSetUseItem } from '@rspack/core';
import { fallbacks } from '@teambit/webpack';
import { excludeNodeModulesJs } from '@teambit/webpack.modules.exclude-node-modules-js';
import * as stylesRegexps from '@teambit/webpack.modules.style-regexps';
Expand Down Expand Up @@ -85,6 +85,15 @@ export const cssParser = {
'css/module': { namedExports: false },
} as const;

// Vendored stylesheets (under node_modules) reference assets we don't control - in this codebase
// that's always an absolute CDN url or a `data:` URI (see the design system's font package),
// never a local relative path. Rspack v2's CSS handler (unlike v1) tries to resolve every
// `url()` as a module to read, which fails on a remote https: url with "Unhandled scheme" (no
// plugin registered for reading over http). First-party source is expected to keep resolving
// local relative urls through the normal asset pipeline (fontRule, the image asset rule), so
// `url` handling is disabled only for node_modules-sourced stylesheets.
const vendorCssParser = { url: false as const };

export function swcRule(options?: { dev?: boolean; refresh?: boolean }): RuleSetRule {
return {
test: /\.(js|mjs|jsx|ts|tsx)$/,
Expand Down Expand Up @@ -141,6 +150,38 @@ interface StyleRulesOptions {
exportsOnly?: boolean;
}

/**
* Builds one style rule as a node_modules-scoped variant (vendor `url()`s left untouched, see
* `vendorCssParser`) and a first-party variant (default asset-pipeline `url()` resolution). The
* two are mutually exclusive by `include`/`exclude`, so exactly one ever matches a given file.
*/
function styleRule(
test: RegExp,
type: 'css' | 'css/module',
use: RuleSetUseItem[],
generator: object | undefined,
sideEffects: boolean | undefined
): RuleSetRule[] {
const vendorRule: RuleSetRule = {
test,
type,
use,
include: /node_modules/,
parser: vendorCssParser,
...(generator && { generator }),
...(sideEffects !== undefined && { sideEffects }),
};
const firstPartyRule: RuleSetRule = {
test,
type,
use,
exclude: /node_modules/,
...(generator && { generator }),
...(sideEffects !== undefined && { sideEffects }),
};
return [vendorRule, firstPartyRule];
}

/**
* Returns all 6 style rules: CSS, SCSS, LESS — each as modules and non-modules.
*/
Expand All @@ -163,31 +204,21 @@ export function styleRules(opts: StyleRulesOptions): RuleSetRule[] {
const sassLoader = { loader: require.resolve('sass-loader'), options: { sourceMap: true } };

return [
{
test: stylesRegexps.cssNoModulesRegex,
type: 'css',
use: [...postCss],
...(regularGenerator && { generator: regularGenerator }),
sideEffects: true,
},
{
test: stylesRegexps.cssModuleRegex,
type: 'css/module',
use: [...postCss],
generator: moduleGenerator,
},
{
test: stylesRegexps.sassNoModuleRegex,
type: 'css',
use: [...postCss, ...resolveUrl, sassLoader],
...(regularGenerator && { generator: regularGenerator }),
sideEffects: true,
},
{
test: stylesRegexps.sassModuleRegex,
type: 'css/module',
use: [...postCss, ...resolveUrl, sassLoader],
generator: moduleGenerator,
},
...styleRule(stylesRegexps.cssNoModulesRegex, 'css', [...postCss], regularGenerator, true),
...styleRule(stylesRegexps.cssModuleRegex, 'css/module', [...postCss], moduleGenerator, undefined),
...styleRule(
stylesRegexps.sassNoModuleRegex,
'css',
[...postCss, ...resolveUrl, sassLoader],
regularGenerator,
true
),
...styleRule(
stylesRegexps.sassModuleRegex,
'css/module',
[...postCss, ...resolveUrl, sassLoader],
moduleGenerator,
undefined
),
];
}
9 changes: 2 additions & 7 deletions scopes/ui-foundation/ui/rspack/rspack.dev.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { rspack, type Configuration } from '@rspack/core';
import type { Configuration as DevServerConfig } from '@rspack/dev-server';
import RefreshPlugin from '@rspack/plugin-react-refresh';
import { ReactRefreshRspackPlugin } from '@rspack/plugin-react-refresh';
import { fallbacksProvidePluginConfig } from '@teambit/webpack';
import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware';
import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
Expand Down Expand Up @@ -41,10 +41,6 @@ export function devConfig(workspaceDir, entryFiles, title): RspackConfigWithDevS

devtool: 'eval-cheap-module-source-map',

experiments: {
css: true,
},

// enable persistent cache
cache: true,

Expand Down Expand Up @@ -91,7 +87,6 @@ export function devConfig(workspaceDir, entryFiles, title): RspackConfigWithDevS
directory: resolveWorkspacePath(publicUrlOrPath),
staticOptions: {},
publicPath: publicUrlOrPath,
serveIndex: true,
watch: false,
},
],
Expand Down Expand Up @@ -171,7 +166,7 @@ export function devConfig(workspaceDir, entryFiles, title): RspackConfigWithDevS
},

plugins: [
new RefreshPlugin(),
new ReactRefreshRspackPlugin(),
new rspack.HtmlRspackPlugin({
inject: true,
templateContent: html(title || 'My component workspace')(),
Expand Down
3 changes: 0 additions & 3 deletions scopes/ui-foundation/ui/rspack/rspack.ssr.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export default function createRspackSsrConfig(
// this bundle ships inside the package, so it follows the browser config's opt-in: the `eval-*`
// devtools inline a base64 source map per module, which was 60% of the 37 MB `ssr/index.js`.
devtool: shouldUseSourceMap ? 'source-map' : false,
experiments: {
css: true,
},

optimization: {
minimize: true,
Expand Down
8 changes: 4 additions & 4 deletions workspace.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
"@pnpm/semver-diff": "1.1.0",
"@pnpm/types": "1101.9.0",
"@react-hook/latest": "1.0.3",
"@rspack/core": "^1.7.7",
"@rspack/dev-server": "1.2.1",
"@rspack/plugin-react-refresh": "1.6.0",
"@rspack/core": "^2.2.2",
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
"@rspack/dev-server": "2.2.1",
"@rspack/plugin-react-refresh": "2.0.2",
"@shikijs/engine-javascript": "^3.0.0",
"@shikijs/langs": "^3.0.0",
"@svgr/webpack": "8.1.0",
Expand Down Expand Up @@ -614,7 +614,7 @@
"resolve-url-loader": "5.0.0",
"rewire": "7.0.0",
"rimraf": "3.0.2",
"rspack-manifest-plugin": "^5.2.1",
"rspack-manifest-plugin": "^5.2.2",
"sass": "1.63.6",
"semver": "7.7.1",
"semver-intersect": "1.4.0",
Expand Down
Loading