Skip to content

Commit

Permalink
fix(svelte-scoped): global wrap preflights and safelist classes when …
Browse files Browse the repository at this point in the history
…used in component libraries (#2695)

Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
jacob-8 and antfu committed Jun 4, 2023
1 parent a8ad835 commit fe86c0f
Show file tree
Hide file tree
Showing 9 changed files with 789 additions and 54 deletions.
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
2 changes: 1 addition & 1 deletion packages/svelte-scoped/test/cases/preflights/Input.svelte
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>

0 comments on commit fe86c0f

Please sign in to comment.