Skip to content

fix(webpack-plugin,rsbuild-plugin): keep every extracted style in the built stylesheets - #704

Open
owjs3901 wants to merge 1 commit into
mainfrom
fix/css-snapshot
Open

owjs3901 wants to merge 1 commit into
mainfrom
fix/css-snapshot

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

문제

#686의 P0 두 건입니다. webpack과 Rsbuild 빌드 결과에서 스타일이 빠지거나 오래된 CSS가 나왔습니다.

  • PLG-04: bundler는 stylesheet 모듈을 처음 import될 때 한 번만 만듭니다. 그 뒤에 추출된 파일의 스타일은 출력에 들어가지 않았습니다.
    • 공유 base(devup-ui.css)가 특히 문제입니다. 각 파일의 stylesheet는 base를 @import하는데, css-loader는 이 @import를 인라인 -! 요청으로 바꾸고 이 요청은 플러그인 로더/transform을 거치지 않습니다. 그래서 base는 그 시점의 디스크 파일에서 그대로 읽혔습니다.
    • 재현 (Rsbuild, 실제 CssExtractRspackPlugin + css-loader 파이프라인): 늦게 로드된 파일의 전역 스타일(html{color:blue})이 빠짐.
    • 재현 (webpack): main 플러그인으로 같은 결과.
  • PLG-03: Rsbuild가 모든 devup CSS 모듈을 모듈 전역 문자열 하나로 반환했고, 이 문자열은 더 길어질 때만 갱신됐습니다. 그래서 CSS가 같은 길이이거나 짧아지는 수정은 반영되지 않았고, 같은 프로세스의 다음 빌드가 이전 빌드의 CSS를 이어받았습니다.

변경

  • 사전 추출 (prewarm): 빌드 전에 entry에서 도달하는 src 아래 파일을 경로 순서로 추출합니다. 도달하지 않는 파일(story, 사용하지 않는 파일)의 스타일은 들어가지 않습니다.
    • plugin-utils: computeReachableFiles를 추가했습니다. entry에서 static/dynamic import를 따라 도달하는 파일을 구합니다.
    • webpack: collapse일 때만 src 전체를 추출하던 동작을, 모든 production 빌드에서 entry 도달 파일을 추출하도록 바꿨습니다.
    • Rsbuild: onBeforeBuild에서 transform과 같은 파일 이름·옵션으로 추출합니다.
  • 디스크 base 갱신: 컴파일을 시작할 때 디스크의 공유 base를 현재 내용으로 씁니다.
  • 추가 pass: finishModules에서 두 가지를 확인합니다. 하나라도 해당하면 최신 CSS를 파일로 쓰고 needAdditionalPass로 한 번 더 컴파일합니다(실행당 최대 1회). 이때 첫 pass는 processAssets(REPORT 단계)에서 산출물을 모두 지워 아무것도 쓰지 않으므로, 오래된 CSS 파일이 dist에 남지 않습니다.
    • 제공한 stylesheet가 현재와 달라졌는지
    • 컴파일 중에 base가 바뀌었는지
  • 두 번째 pass에서는 이미 모든 파일이 추출된 상태라 stylesheet가 완전합니다. src 밖의 파일(monorepo 패키지, include 라이브러리)도 이 경로로 보장됩니다.
  • webpack css-loader: cacheable(false)로 설정하고, 제공한 CSS를 컴파일 단위로 기록합니다. 플러그인과 로더는 서로 다른 모듈 인스턴스이므로 기록은 globalThis에 둡니다.
  • Rsbuild CSS transform (PLG-03): 로드 시점의 getCss(fileNum, !atomMode)를 반환하고, 모듈 전역 globalCss와 길이 비교를 제거했습니다.
  • watch/dev: 기존처럼 로더가 쓰는 파일로 watcher가 다시 빌드합니다. 추가 pass와 사전 추출은 하지 않습니다.
  • 검토한 대안
    • rebuildModule: CSS 추출 파이프라인에서 rspack 2.2.1이 panic("should have event sender")해서 제외했습니다.
    • 산출물 문자열 치환: css-loader 변환에 의존해서 제외했습니다.

새로 생기는 오류

없음.

검증

  • tsc(@typescript/native)와 ESLint 통과

  • 루트 bun test 5485 pass, 커버리지 100%

  • 실제 빌드 (임시 fixture, 늦은 파일은 delay loader로 재현)

    번들러 상황 컴파일 횟수 결과
    Rsbuild 2.2.1, per-file·single src 안에서만 늦음 1 모든 규칙 포함
    Rsbuild 2.2.1, per-file·single src 밖 파일이 늦음 2 모든 규칙 포함, CSS 파일 1개
    webpack 5.110 (experiments.css), per-file·single src 밖 파일이 늦음 2 모든 규칙 포함, CSS 파일 1개
    webpack, main 플러그인 src 밖 파일이 늦음 1 color:blue 누락 (버그 재현)
  • apps/rsbuild 데모 빌드: 1 pass, CSS 정상

  • rspack 실험으로 needAdditionalPass가 CssExtractRspackPlugin + css-loader에서 두 번째 pass에 최신 CSS를 내보내는 것을 확인

Refs #686

… built stylesheets

A stylesheet module is built on its first import, and the shared base is read from disk past the plugin, so styles extracted later were missing from webpack and Rsbuild output; Rsbuild also served a module-wide string kept only when it grew. The files the entries reach are now extracted before the build, the base on disk is kept current, and a compilation whose stylesheets changed is run once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet.

Refs #686

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.82 → 1.0.83 - bindings/devup-ui-wasm/package.json

Patch

  • globalCss: theme tokens ($text) resolve to var(--text) in global rules instead of being written as invalid $text; imports given as { url, query } objects with unquoted keys are emitted instead of dropped; an '@layer name' key puts the rules it holds in that cascade layer (layers nest as base.reset) instead of flattening them into invalid declarations, and a layer name that is not one identifier is a build error
  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

@devup-ui/bun-plugin@1.0.21 → 1.0.22 - packages/bun-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/components@0.1.59 → 0.1.60 - packages/components/package.json

Patch

  • Auto-update: depends on '@devup-ui/vite-plugin' via a local workspace dependency

@devup-ui/eslint-plugin@1.0.21 → 1.0.22 - packages/eslint-plugin/package.json

Patch

  • no-duplicate-value, no-useless-responsive, no-useless-tailing-nulls, no-typography-token-prefix and prefer-media-shorthand only report and fix values the build reads as styles: style props of Box, Flex and the other style components and the arguments of css, globalCss and keyframes, through style objects, responsive arrays, conditions and spreads. Arrays and keys in props the component passes through (data-, aria-, event handlers, HTML attributes, props, styleVars), in arguments of other functions and under imports/fontFaces/params are left alone, where autofix used to rewrite them; styles of a component nested in another's prop are checked too. css-utils-literal-only reads a css() or keyframes() result held in a const of any scope as static, as the build does

@devup-ui/next-plugin@1.0.89 → 1.0.90 - packages/next-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/plugin-utils@1.0.16 → 1.0.17 - packages/plugin-utils/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles
  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

@devup-ui/rsbuild-plugin@1.0.66 → 1.0.67 - packages/rsbuild-plugin/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@devup-ui/vite-plugin@1.0.72 → 1.0.73 - packages/vite-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/plugin-utils' via a local workspace dependency

@devup-ui/webpack-plugin@1.0.70 → 1.0.71 - packages/webpack-plugin/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
packages/plugin-utils/src/import-graph.ts 100.00% <100.00%> (ø)
packages/rsbuild-plugin/src/plugin.ts 100.00% <100.00%> (ø)
packages/webpack-plugin/src/css-loader.ts 100.00% <100.00%> (ø)
packages/webpack-plugin/src/plugin.ts 100.00% <100.00%> (ø)
packages/webpack-plugin/src/served-css.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant