feat: make postcssIsolateStyles idempotent

BREAKING CHANGE: `includeFiles` option in `postcssIsolateStyles` now defaults to `[/vp-doc\.css/, /base\.css/]` You can remove explicit `includeFiles` if you were using it just to run it on `vp-doc.css`. To revert back to older behavior pass `includeFiles: [/base\.css/]`. The underlying implementation is changed and `transform` and `exclude` options are no longer supported. Use `postcss-prefix-selector` directly if you've advanced use cases.
pull/4910/head
Divyansh Singh 3 weeks ago
parent 991c780a07
commit 0944777893

@ -1,43 +1,26 @@
import {
postcssIsolateStyles,
splitSelectorPseudo
} from 'node/postcss/isolateStyles'
import { postcssIsolateStyles } from 'node/postcss/isolateStyles'
import postcss from 'postcss'
// helper to run plugin transform on selector
function apply(
prefixPlugin: ReturnType<typeof postcssIsolateStyles>,
selector: string
) {
// `prepare` is available on the runtime plugin but missing from the types, thus cast to `any`
const { Rule } = (prefixPlugin as any).prepare({
root: { source: { input: { file: 'foo/base.css' } } }
})
const rule = { selectors: [selector] }
Rule(rule, { result: {} })
return rule.selectors[0]
function apply(selector: string) {
const { root } = postcss([postcssIsolateStyles()]).process(`${selector} {}`)
return (root.nodes[0] as any).selector
}
describe('node/postcss/isolateStyles', () => {
const plugin = postcssIsolateStyles()
test('splitSelectorPseudo skips escaped colon', () => {
const input = '.foo\\:bar'
const [selector, pseudo] = splitSelectorPseudo(input)
expect(selector).toBe(input)
expect(pseudo).toBe('')
expect(apply('.foo\\:bar')).toBe(
'.foo\\:bar:not(:where(.vp-raw, .vp-raw *))'
)
})
test('splitSelectorPseudo splits on pseudo selectors', () => {
const input = '.button:hover'
const [selector, pseudo] = splitSelectorPseudo(input)
expect(selector).toBe('.button')
expect(pseudo).toBe(':hover')
expect(apply('.button:hover')).toBe(
'.button:not(:where(.vp-raw, .vp-raw *)):hover'
)
})
it('postcssIsolateStyles inserts :not(...) in the right place', () => {
const input = '.disabled\\:opacity-50:disabled'
const result = apply(plugin, input)
expect(result).toBe(
test('postcssIsolateStyles inserts :not(...) in the right place', () => {
expect(apply('.disabled\\:opacity-50:disabled')).toBe(
'.disabled\\:opacity-50:not(:where(.vp-raw, .vp-raw *)):disabled'
)
})

@ -277,11 +277,11 @@ Wraps in a `<div class="vp-raw">`
}
```
It uses [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) under the hood. You can pass its options like this:
You can pass its options like this:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // defaults to /base\.css/
includeFiles: [/custom\.css/] // defaults to [/vp-doc\.css/, /base\.css/]
})
```

@ -256,11 +256,11 @@ La clase `vp-raw` también puede ser usada directamente en elementos. El aislami
}
```
El utiliza [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) internamente. Puede pasar opciones así:
Puede pasar opciones así:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // o padrão é /base\.css/
includeFiles: [/custom\.css/] // o padrão é [/vp-doc\.css/, /base\.css/]
})
```

@ -255,11 +255,11 @@ export default defineConfig({
}
```
این از [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) استفاده می‌کند. می‌توانید گزینه‌های آن را به این صورت پاس بدهید:
می‌توانید گزینه‌های آن را به این صورت پاس بدهید:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // به طور پیش‌فرض /base\.css/
includeFiles: [/custom\.css/] // به طور پیش‌فرض [/vp-doc\.css/, /base\.css/]
})
```

@ -255,11 +255,11 @@ export default defineConfig({
}
```
이것은 기본적으로 [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector)를 사용합니다. 다음과 같이 옵션을 전달할 수 있습니다:
다음과 같이 옵션을 전달할 수 있습니다:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // 기본값은 /base\.css/
includeFiles: [/custom\.css/] // 기본값은 [/vp-doc\.css/, /base\.css/]
})
```

@ -15,7 +15,7 @@
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.7.1",
"vitepress": "workspace:*",
"vitepress-plugin-group-icons": "^1.6.2",
"vitepress-plugin-llms": "^1.7.2"
"vitepress-plugin-group-icons": "^1.6.3",
"vitepress-plugin-llms": "^1.7.3"
}
}

@ -255,11 +255,11 @@ A classe `vp-raw` também pode ser usada diretamente em elementos. O isolamento
}
```
Ele utiliza [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) internamente. Você pode passar opções assim:
Você pode passar opções assim:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // o padrão é /base\.css/
includeFiles: [/custom\.css/] // o padrão é [/vp-doc\.css/, /base\.css/]
})
```

@ -281,11 +281,11 @@ console.log('Привет, VitePress!')
}
```
Он использует [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector) под капотом. Вы можете передать ему параметры следующим образом:
Вы можете передать ему параметры следующим образом:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // по умолчанию /base\.css/
includeFiles: [/custom\.css/] // по умолчанию [/vp-doc\.css/, /base\.css/]
})
```

@ -255,11 +255,11 @@ Wraps in a `<div class="vp-raw">`
}
```
它在底层使用 [`postcss-prefix-selector`](https://github.com/RadValentin/postcss-prefix-selector)。你可以像这样传递它的选项:
你可以像这样传递它的选项:
```js
postcssIsolateStyles({
includeFiles: [/vp-doc\.css/] // 默认为 /base\.css/
includeFiles: [/custom\.css/] // 默认为 [/vp-doc\.css/, /base\.css/]
})
```

@ -111,19 +111,19 @@
"mark.js": "8.11.1",
"minisearch": "^7.1.2",
"shiki": "^3.9.2",
"vite": "^7.1.1",
"vite": "^7.1.2",
"vue": "^3.5.18"
},
"devDependencies": {
"@clack/prompts": "^1.0.0-alpha.1",
"@iconify/utils": "^3.0.0",
"@mdit-vue/plugin-component": "^3.0.0",
"@mdit-vue/plugin-frontmatter": "^3.0.0",
"@mdit-vue/plugin-headers": "^3.0.0",
"@mdit-vue/plugin-sfc": "^3.0.0",
"@mdit-vue/plugin-title": "^3.0.0",
"@mdit-vue/plugin-toc": "^3.0.0",
"@mdit-vue/shared": "^3.0.0",
"@clack/prompts": "^1.0.0-alpha.3",
"@iconify/utils": "^3.0.1",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
"@mdit-vue/plugin-sfc": "^3.0.2",
"@mdit-vue/plugin-title": "^3.0.2",
"@mdit-vue/plugin-toc": "^3.0.2",
"@mdit-vue/shared": "^3.0.2",
"@polka/compression": "^1.0.0-next.28",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.6",
@ -141,13 +141,12 @@
"@types/minimist": "^1.2.5",
"@types/node": "^24.2.1",
"@types/picomatch": "^4.0.2",
"@types/postcss-prefix-selector": "^1.16.3",
"@types/prompts": "^2.4.9",
"chokidar": "^4.0.3",
"conventional-changelog-cli": "^5.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.4.1",
"esbuild": "^0.25.8",
"esbuild": "^0.25.9",
"execa": "^9.6.0",
"fs-extra": "^11.3.1",
"get-port": "^7.1.0",
@ -165,7 +164,7 @@
"minimist": "^1.2.8",
"nanoid": "^5.1.5",
"ora": "^8.2.0",
"oxc-minify": "^0.81.0",
"oxc-minify": "^0.82.1",
"p-map": "^7.0.3",
"package-directory": "^8.1.0",
"path-to-regexp": "^6.3.0",
@ -174,7 +173,7 @@
"playwright-chromium": "^1.54.2",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-prefix-selector": "^2.1.1",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.6.2",
"prompts": "^2.4.2",
"punycode": "^2.3.1",
@ -194,7 +193,7 @@
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",
"oxc-minify": "^0.81.0",
"oxc-minify": "^0.82.1",
"postcss": "^8"
},
"peerDependenciesMeta": {

File diff suppressed because it is too large Load Diff

@ -1,21 +1,38 @@
import postcssPrefixSelector from 'postcss-prefix-selector'
import type { Plugin } from 'postcss'
import selectorParser from 'postcss-selector-parser'
export function postcssIsolateStyles(
options: Parameters<typeof postcssPrefixSelector>[0] = {}
): ReturnType<typeof postcssPrefixSelector> {
return postcssPrefixSelector({
prefix: ':not(:where(.vp-raw, .vp-raw *))',
includeFiles: [/base\.css/],
transform(prefix, _selector) {
// split selector from its pseudo part if the trailing colon is not escaped
const [selector, pseudo] = splitSelectorPseudo(_selector)
return selector + prefix + pseudo
},
...options
})
type Options = {
includeFiles?: RegExp[]
ignoreFiles?: RegExp[]
prefix?: string
}
export function splitSelectorPseudo(selector: string): [string, string] {
const [base, pseudo = ''] = selector.split(/(?<!\\)(:\S*)$/)
return [base, pseudo]
export function postcssIsolateStyles({
includeFiles = [/vp-doc\.css/, /base\.css/],
ignoreFiles,
prefix = ':not(:where(.vp-raw, .vp-raw *))'
}: Options = {}): Plugin {
const prefixNodes = selectorParser().astSync(prefix).first.nodes
return /* prettier-ignore */ {
postcssPlugin: 'postcss-isolate-styles',
Once(root) {
const file = root.source?.input.file
if (file && includeFiles?.length && !includeFiles.some((re) => re.test(file))) return
if (file && ignoreFiles?.length && ignoreFiles.some((re) => re.test(file))) return
root.walkRules((rule) => {
if (!rule.selector || rule.selector.includes(prefix)) return
if (rule.parent?.type === 'atrule' && /\bkeyframes$/i.test(rule.parent.name)) return
rule.selector = selectorParser((selectors) => {
selectors.each((sel) => {
if (!sel.nodes.length) return
const insertionIndex = sel.nodes.findLastIndex((n) => n.type !== 'pseudo') + 1
sel.nodes.splice(insertionIndex, 0, ...prefixNodes.map((n) => n.clone() as any))
})
}).processSync(rule.selector)
})
}
}
}

Loading…
Cancel
Save