Skip to content
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

fix(svelte-scoped): global wrap preflights and safelist classes when used in component libraries #2695

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 11 additions & 19 deletions docs/integrations/svelte-scoped.md
Expand Up @@ -314,33 +314,25 @@ Setup your `uno.config.ts` file as described [below](#configuration).

### Preflights

When using the preprocessor you have the option to include preflights in your component by adding `uno:preflights` as a style attribute.
When using the preprocessor you have the option to include preflights in a component by adding `uno:preflights` as a style attribute.

```html
<style uno:preflights></style>
```

Adding preflights into individual components is unnecessary if your classes do not depend on preflights or your built components are being consumed only in apps that already include preflights.
Any special preflights that start with a period, such as `.prose :where(a):not(:where(.not-prose, .not-prose *))`, will be wrapped with `:global()` to avoid being automatically stripped out by the Svelte compiler.

*Adding preflights into individual components is unnecessary if your classes do not depend on preflights or your built components are being consumed only in apps that already include preflights.*

### Safelist

When using the preprocessor you have the option to include safelist classes in your component by adding `uno:safelist` as a style attribute.
When using the preprocessor you have the option to include safelist classes in a component by adding `uno:safelist` as a style attribute.

```html
<style uno:safelist global></style>
<style uno:safelist></style>
```

To avoid having the Svelte compiler then strip them out because they're not found in the component, you'll also need to add the `global` modifier which will require https://github.com/sveltejs/svelte-preprocess. It's probably easier to just use `--at-apply` instead:

```svelte
<div class:computed-foo={condition} />

<style>
:global(.computed-foo) {
--at-apply: mb-1 mr-2;
}
</style>
```
Your safelist styles will be wrapped with `:global()` to avoid being automatically stripped out by the Svelte compiler.

## Configuration

Expand Down Expand Up @@ -368,16 +360,16 @@ Do to the nature of having a few necessary styles in a global stylesheet and eve
| Preset | Supported | Notes |
| --- | :-- | :-- |
| [@unocss/preset-uno](https://unocss.dev/presets/uno), [@unocss/preset-mini](https://unocss.dev/presets/mini), [@unocss/preset-wind](https://unocss.dev/presets/wind), [@unocss/preset-icons](https://github.com/unocss/unocss/tree/main/packages/preset-icons), [@unocss/web-fonts](https://github.com/unocss/unocss/tree/main/packages/preset-icons) | ✅ | These and all community plugins, e.g. [unocss-preset-forms](https://github.com/Julien-R44/unocss-preset-forms), that only rely on rules/variants/preflights will work. |
| [@unocss/preset-typography](https://github.com/unocss/unocss/tree/main/packages/preset-typography) | ✅ | Using the `.prose` class adds a large amount of rulesets which Svelte Scoped will not properly surround with `:global()` wrappers so add the `prose` class to your safelist when using this preset. All other classes from this preset, e.g. `prose-pink`, can be component scoped. |
| [@unocss/preset-typography](https://github.com/unocss/unocss/tree/main/packages/preset-typography) | ✅ | Due to how this preset adds rulesets to your preflights you must add the `prose` class to your safelist when using this preset, otherwise the preflights will never be triggered. All other classes from this preset, e.g. `prose-pink`, can be component scoped. |
| [@unocss/preset-rem-to-px](https://github.com/unocss/unocss/tree/main/packages/preset-rem-to-px) | ✅ | This and all presets like it that only modify style output will work. |
| [@unocss/preset-attributify](https://github.com/unocss/unocss/tree/main/packages/preset-attributify) | - | Preset won't work. Instead use [unplugin-attributify-to-class](https://github.com/MellowCo/unplugin-attributify-to-class) Vite plugin (`attributifyToClass({ include: [/\.svelte$/]})`) before the Svelte Scoped Vite plugin |
| [@unocss/preset-tagify](https://github.com/unocss/unocss/tree/main/packages/preset-tagify) | - | Presets that add custom extractors will not work. Create a preprocessor to convert `<text-red>Hi</text-red>` to `<span class="text-red">Hi</span>`, then create a PR to add the link here. |

For other presets, if they don't rely on traditional `class="..."` usage you will need to first preprocess those class names into the `class="..."` attribute. If they add extremely complex styles like typography's `.prose` class then you may need to place the complex class names into your safelist.
For other presets, if they don't rely on traditional `class="..."` usage you will need to first preprocess those class names into the `class="..."` attribute. If they add presets like typography's `.prose` class then you will need to place the classes which trigger the preset additions into your safelist.

## Scoped utility classes unleashes creativity
## Scoped utility classes unleash creativity

Some advice on when you might want to use scoped styles: A global css file that includes everything is great for smaller apps, but there will come a point in a large project's life when every time you start to write a class like `.md:max-w-[50vw]` that you know is only going to be used once you start to cringe as you feel the size of your global style sheet getting larger and larger. This inhibits creativity. Sure, you could use `--at-apply: md:max-w-[50vw]` in the style block but that gets tedious and styles in context are so useful. Furthermore, if you would like to include a great variety of icons in your project, you will begin to feel the weight of adding them to the global stylesheet. When each component bears the weight of its own styles and icons you can continue to expand your project without having to analyze the cost benefit of each new addition.
Some advice on when you might want to use scoped styles: If you have come to the point in a large project's life when every time you use a class like `.md:max-w-[50vw]` that you know is only used once you cringe as you feel the size of your global style sheet getting larger and larger, then give this package a try. Hesitancy to use exactly the class you need inhibits creativity. Sure, you could use `--at-apply: md:max-w-[50vw]` in the style block but that gets tedious and styles in context are useful. Furthermore, if you would like to include a great variety of icons in your project, you will begin to feel the weight of adding them to the global stylesheet. When each component bears the weight of its own styles and icons you can continue to expand your project without having to analyze the cost benefit of each new addition.

## License

Expand Down
33 changes: 20 additions & 13 deletions packages/svelte-scoped/src/_preprocess/index.ts
Expand Up @@ -3,9 +3,10 @@ import { type UnoGenerator, type UserConfig, type UserConfigDefaults, createGene
import presetUno from '@unocss/preset-uno'
import { loadConfig } from '@unocss/config'
import { transformClasses } from './transformClasses'
import { transformStyle } from './transformStyle'
import { checkForApply, transformStyle } from './transformStyle'
import type { SvelteScopedContext, UnocssSveltePreprocessOptions } from './types'
import { themeRE } from './transformTheme'
import { wrapSelectorsWithGlobal } from './transformClasses/wrapGlobal'

export function UnocssSveltePreprocess(options: UnocssSveltePreprocessOptions = {}, unoContextFromVite?: SvelteScopedContext, isViteBuild?: () => boolean): PreprocessorGroup {
if (!options.classPrefix)
Expand All @@ -25,13 +26,20 @@ export function UnocssSveltePreprocess(options: UnocssSveltePreprocessOptions =
},

style: async ({ content, attributes, filename }) => {
const addPreflights = !!attributes['uno:preflights']
const addSafelist = !!attributes['uno:safelist']
let addPreflights = !!attributes['uno:preflights']
let addSafelist = !!attributes['uno:safelist']

const checkForApply = options.applyVariables !== false
const hasThemeFn = content.match(themeRE)
if (unoContextFromVite && (addPreflights || addSafelist)) {
// Svelte 4 style preprocessors will be able to remove attributes after handling them, but for now we must ignore them when using the Vite plugin to avoid a SvelteKit app double-processing that which a component library already processed.
addPreflights = false
addSafelist = false
warnOnce('Notice for those transitioning to @unocss/svelte-scoped/vite: uno:preflights and uno:safelist are only for use in component libraries. Please see the documentation for how to add preflights and safelist into your head tag. If you are consuming a component library built by @unocss/svelte-scoped/preprocess, you can ignore this upgrade notice.') // remove notice in future
}

const { hasApply, applyVariables } = checkForApply(content, options.applyVariables)
const hasThemeFn = !!content.match(themeRE)

const changeNeeded = addPreflights || addSafelist || checkForApply || hasThemeFn
const changeNeeded = addPreflights || addSafelist || hasApply || hasThemeFn
if (!changeNeeded)
return

Expand All @@ -40,24 +48,23 @@ export function UnocssSveltePreprocess(options: UnocssSveltePreprocessOptions =

let preflightsSafelistCss = ''
if (addPreflights || addSafelist) {
if (unoContextFromVite)
warnOnce('Do not place preflights or safelist within an individual component as they already placed in your global styles injected into the head tag. These options are only for component libraries.')
const { css } = await uno.generate([], { preflights: addPreflights, safelist: addSafelist, minify: true })
preflightsSafelistCss = css
preflightsSafelistCss = wrapSelectorsWithGlobal(css)
}

if (checkForApply || hasThemeFn) {
if (hasApply || hasThemeFn) {
return await transformStyle({
content,
prepend: preflightsSafelistCss,
uno,
applyVariables: options.applyVariables,
filename,
prepend: preflightsSafelistCss,
applyVariables,
hasThemeFn,
})
}

if (preflightsSafelistCss)
return { code: preflightsSafelistCss }
return { code: preflightsSafelistCss + content }
},
}
}
Expand Down
Expand Up @@ -17,7 +17,7 @@ if (import.meta.vitest) {
const { describe, expect, it } = import.meta.vitest

describe('wrapSelectorsWithGlobal', () => {
it('should wrap multiple selectors with :global()', () => {
it('wraps multiple selectors with :global()', () => {
const css = '.my-class{color:red;}[dir="rtl"] .mb-1{margin-bottom:0.25em;}'
const expected = ':global(.my-class){color:red;}:global([dir="rtl"] .mb-1){margin-bottom:0.25em;}'

Expand All @@ -29,9 +29,7 @@ if (import.meta.vitest) {
0% { opacity: 0; }
100% { opacity: 1; }
}`
const expected = css

expect(wrapSelectorsWithGlobal(css)).toBe(expected)
expect(wrapSelectorsWithGlobal(css)).toBe(css)
})

it('should not wrap @media selectors (selectors inside parenthesis)', () => {
Expand All @@ -40,6 +38,13 @@ if (import.meta.vitest) {

expect(wrapSelectorsWithGlobal(css)).toBe(expected)
})

it('should not wrap selectors starting with * or ::', () => {
const cssWithAsterisk = '*,::before,::after { --un-rotate: 0;}'
expect(wrapSelectorsWithGlobal(cssWithAsterisk)).toBe(cssWithAsterisk)
const cssWithDoubleColon = '::backdrop { --un-rotate: 0;}'
expect(wrapSelectorsWithGlobal(cssWithDoubleColon)).toBe(cssWithDoubleColon)
})
})
}

Expand Down
34 changes: 18 additions & 16 deletions packages/svelte-scoped/src/_preprocess/transformStyle.ts
@@ -1,31 +1,33 @@
import type { UnoGenerator } from '@unocss/core'
import { toArray } from '@unocss/core'
import { type UnoGenerator, toArray } from '@unocss/core'
import type { Processed } from 'svelte/types/compiler/preprocess'
import MagicString from 'magic-string'
import type { TransformApplyOptions } from './types'
import { transformApply } from './transformApply'
import { themeRE, transformTheme } from './transformTheme'
import { transformTheme } from './transformTheme'
import type { TransformApplyOptions } from './types'

const DEFAULT_APPLY_VARIABLES = ['--at-apply']

export async function transformStyle({ content, uno, prepend, applyVariables, filename }: {
export function checkForApply(content: string, _applyVariables: TransformApplyOptions['applyVariables']) {
if (_applyVariables === false)
return { hasApply: false, applyVariables: [] }
const applyVariables = toArray(_applyVariables || DEFAULT_APPLY_VARIABLES)
return {
hasApply: content.includes('@apply') || applyVariables.some(v => content.includes(v)),
applyVariables,
}
}

export async function transformStyle({ content, uno, prepend, filename, applyVariables, hasThemeFn }: {
content: string
uno: UnoGenerator
prepend?: string
applyVariables?: TransformApplyOptions['applyVariables']
filename?: string
prepend: string
applyVariables: string[]
hasThemeFn: boolean
}): Promise<Processed | void> {
applyVariables = toArray(applyVariables || DEFAULT_APPLY_VARIABLES)
const hasApply = content.includes('@apply') || applyVariables.some(v => content.includes(v))

const hasThemeFn = content.match(themeRE)

if (!hasApply && !hasThemeFn)
return

const s = new MagicString(content)

if (hasApply)
if (applyVariables?.length)
await transformApply({ s, uno, applyVariables })

if (hasThemeFn)
Expand Down
Expand Up @@ -2,6 +2,6 @@

<style uno:preflights>
div {
--at-apply: bg-red-100 hover:bg-red-200 p-3 rounded dark:bg-red-700 dark:hover:bg-red-600;
--at-apply: "bg-red-100 hover:bg-red-200 p-3 rounded dark:bg-red-700 dark:hover:bg-red-600";
}
</style>
3 changes: 3 additions & 0 deletions packages/svelte-scoped/test/cases/prose/Input.svelte
@@ -0,0 +1,3 @@
<div class="uno-prose" />

<style uno:preflights uno:safelist></style>