-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
feat(css): allow scoping css to importers exports #19418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
a97dde2
83d1dd4
ec1bb0e
371ef79
a4110b8
4cc87b9
c7bbf7e
ac0a5bf
1cc25ca
fd8d75b
3b50c0d
e0a7001
1732664
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,6 +323,20 @@ export interface Plugin<A = any> extends RollupPlugin<A> { | |
| > | ||
| } | ||
|
|
||
| export interface CustomPluginOptionsVite { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved it here because it seemed
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this might probably be unintentional from Rollup side due to rollup/rollup#5591 🤔
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made a PR here 👍 |
||
| /** | ||
| * If this is a CSS Rollup module, you can scope to its importer's exports | ||
| * so that if those exports are treeshaken away, the CSS module will also | ||
| * be treeshaken. | ||
| * | ||
| * Example config if the CSS id is `/src/App.vue?vue&type=style&lang.css`: | ||
| * ```js | ||
| * cssScopeTo: ['/src/App.vue', 'default'] | ||
| * ``` | ||
| */ | ||
| cssScopeTo?: [string, string | undefined] | ||
|
||
| } | ||
|
|
||
| export type HookHandler<T> = T extends ObjectHook<infer H> ? H : T | ||
|
|
||
| export type PluginWithRequiredHook<K extends keyof Plugin> = Plugin & { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,7 +54,7 @@ import { | |
| SPECIAL_QUERY_RE, | ||
| } from '../constants' | ||
| import type { ResolvedConfig } from '../config' | ||
| import type { Plugin } from '../plugin' | ||
| import type { CustomPluginOptionsVite, Plugin } from '../plugin' | ||
| import { checkPublicFile } from '../publicDir' | ||
| import { | ||
| arraify, | ||
|
|
@@ -445,6 +445,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { | |
| export function cssPostPlugin(config: ResolvedConfig): Plugin { | ||
| // styles initialization in buildStart causes a styling loss in watch | ||
| const styles: Map<string, string> = new Map<string, string>() | ||
| const scopedStyles = new Map<string, Map<string | undefined, string[]>>() | ||
| // queue to emit css serially to guarantee the files are emitted in a deterministic order | ||
| let codeSplitEmitQueue = createSerialPromiseQueue<string>() | ||
| const urlEmitQueue = createSerialPromiseQueue<unknown>() | ||
|
|
@@ -607,12 +608,29 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { | |
| code = '' | ||
| } | ||
|
|
||
| const cssScopeTo = ( | ||
| this.getModuleInfo(id)?.meta?.vite as | ||
| | CustomPluginOptionsVite | ||
| | undefined | ||
| )?.cssScopeTo | ||
| if (cssScopeTo) { | ||
| const [file, exp] = cssScopeTo | ||
| if (!scopedStyles.has(file)) { | ||
| scopedStyles.set(file, new Map()) | ||
| } | ||
| if (!scopedStyles.get(file)!.has(exp)) { | ||
| scopedStyles.get(file)!.set(exp, []) | ||
| } | ||
| scopedStyles.get(file)!.get(exp)!.push(css) | ||
| } | ||
sapphi-red marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return { | ||
| code, | ||
| map: { mappings: '' }, | ||
| // avoid the css module from being tree-shaken so that we can retrieve | ||
| // it in renderChunk() | ||
| moduleSideEffects: modulesCode || inlined ? false : 'no-treeshake', | ||
| moduleSideEffects: | ||
| modulesCode || inlined || cssScopeTo ? false : 'no-treeshake', | ||
| } | ||
| }, | ||
|
|
||
|
|
@@ -632,6 +650,16 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { | |
| isPureCssChunk = false | ||
| } | ||
| } | ||
| } else if (scopedStyles.has(id)) { | ||
| const renderedExports = chunk.modules[id]!.renderedExports | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, good catch. Let me ask them. |
||
| // If this module is has a scoped style, check for the rendered exports | ||
| // and include the corresponding CSS. | ||
sapphi-red marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for (const [exp, styles] of scopedStyles.get(id)!) { | ||
| if (exp === undefined || renderedExports.includes(exp)) { | ||
| // TODO: do we need to care the order? | ||
| chunkCSS += styles.join('') | ||
sapphi-red marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } else if (!isJsChunkEmpty) { | ||
| // if the module does not have a style, then it's not a pure css chunk. | ||
| // this is true because in the `transform` hook above, only modules | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .treeshake-scoped-a { | ||
| color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import './a-scoped.css' // should be treeshaken away if `a` is not used | ||
|
|
||
| export default function a() { | ||
| return 'treeshake-scoped-a' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <h1>treeshake-scoped (another)</h1> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bluwy This file and the files in the barrel directory covers this case (#16058 (comment)).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading the implementation, I believe you have the scoped CSS side effects set to false to fix this, but keep track of the scoped css styles map separately to bring it back? That's a neat trick. I guess a limitation of this is that it's harder to scope to multiple modules like you mentioned, but I think it's fine for now. |
||
| <p class="scoped-another">Imported scoped CSS</p> | ||
|
|
||
| <script type="module"> | ||
| import { b } from './barrel/index.js' | ||
| document.querySelector('.scoped-another').classList.add(b()) | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .treeshake-scoped-b { | ||
| color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import './b-scoped.css' // should be treeshaken away if `b` is not used | ||
|
|
||
| export default function b() { | ||
| return 'treeshake-scoped-b' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .treeshake-scoped-barrel-a { | ||
| text-decoration-line: underline; | ||
| text-decoration-color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import './a-scoped.css' | ||
|
|
||
| export function a() { | ||
| return 'treeshake-scoped-barrel-a' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| .treeshake-scoped-barrel-b { | ||
| text-decoration-line: underline; | ||
| text-decoration-color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import './b-scoped.css' | ||
|
|
||
| export function b() { | ||
| return 'treeshake-scoped-barrel-b' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './a' | ||
| export * from './b' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .treeshake-scoped-c { | ||
| color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import './c-scoped.css' // should be treeshaken away if `b` is not used | ||
|
|
||
| export default function c() { | ||
| return 'treeshake-scoped-c' | ||
| } | ||
|
|
||
| export function cUsed() { | ||
| // used but does not depend on scoped css | ||
| return 'c-used' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .treeshake-scoped-d { | ||
| color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import './d-scoped.css' // should be treeshaken away if `d` is not used | ||
|
|
||
| export default function d() { | ||
| return 'treeshake-scoped-d' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <h1>treeshake-scoped</h1> | ||
| <p class="scoped-another">Imported scoped CSS</p> | ||
|
|
||
| <script type="module"> | ||
| import { d } from './index.js' | ||
| import { a } from './barrel/index.js' | ||
| document.querySelector('.scoped-another').classList.add(d(), a()) | ||
| </script> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { default as a } from './a.js' | ||
| export { default as b } from './b.js' | ||
| export { default as c, cUsed } from './c.js' | ||
| export { default as d } from './d.js' |
Uh oh!
There was an error while loading. Please reload this page.