Skip to content

Commit

Permalink
feat(svelte-scoped): Add theme() to transform style block pipeline (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-8 committed May 31, 2023
1 parent eaf091d commit 384879e
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 61 deletions.
4 changes: 4 additions & 0 deletions docs/integrations/svelte-scoped.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ will be transformed into:

In order for `rtl:ml-2` to work properly, the `[dir="rtl"]` selector is wrapped with `:global()` to keep the Svelte compiler from stripping it out automatically as the component has no element with that attribute. However, `div` can't be included in the `:global()` wrapper because that style would then affect every `div` in your app.

### Other style block directives

Using [theme()](https://unocss.dev/transformers/directives#theme) is also supported, but [@screen](https://unocss.dev/transformers/directives#screen) is **not**.

## Vite Plugin

In Svelte or SvelteKit apps, inject generated styles directly into your Svelte components, while placing the minimum necessary styles in a global stylesheet. Check out the [SvelteKit example](https://github.com/unocss/unocss/tree/main/examples/sveltekit-scoped) in Stackblitz:
Expand Down
10 changes: 6 additions & 4 deletions packages/svelte-scoped/src/preprocess/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { type UnoGenerator, type UserConfig, type UserConfigDefaults, createGene
import presetUno from '@unocss/preset-uno'
import { loadConfig } from '@unocss/config'
import { transformClasses } from './transformClasses'
import { transformApply } from './transformApply'
import { transformStyle } from './transformStyle'
import type { SvelteScopedContext, UnocssSveltePreprocessOptions } from './types'
import { themeRE } from './transformTheme'

export * from './types.d.js'

Expand All @@ -30,8 +31,9 @@ export default function UnocssSveltePreprocess(options: UnocssSveltePreprocessOp
const addSafelist = !!attributes['uno:safelist']

const checkForApply = options.applyVariables !== false
const hasThemeFn = content.match(themeRE)

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

Expand All @@ -46,8 +48,8 @@ export default function UnocssSveltePreprocess(options: UnocssSveltePreprocessOp
preflightsSafelistCss = css
}

if (checkForApply) {
return await transformApply({
if (checkForApply || hasThemeFn) {
return await transformStyle({
content,
prepend: preflightsSafelistCss,
uno,
Expand Down
27 changes: 5 additions & 22 deletions packages/svelte-scoped/src/preprocess/transformApply/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import presetUno from '@unocss/preset-uno'
import { format as prettier } from 'prettier'
import parserCSS from 'prettier/parser-postcss'
import { describe, expect, it } from 'vitest'
import MagicString from 'magic-string'
import { transformApply } from '.'

describe('transformApply', () => {
Expand All @@ -12,8 +13,10 @@ describe('transformApply', () => {
],
})

async function transform(content: string, prepend?: string) {
const transformed = (await transformApply({ content, uno, prepend }))?.code
async function transform(content: string) {
const s = new MagicString(content)

const transformed = (await transformApply({ s, uno, applyVariables: ['--at-apply'] })).toString()
return prettier(transformed || '', {
parser: 'css',
plugins: [parserCSS],
Expand Down Expand Up @@ -126,25 +129,5 @@ describe('transformApply', () => {
`)
})

it('prepends', async () => {
const style = `
.custom-class {
--at-apply: hidden;
}
`
const prepend = `
::backdrop {
--un-rotate: 0;
}`
expect(await transform(style, prepend)).toMatchInlineSnapshot(`
"::backdrop {
--un-rotate: 0;
}
.custom-class {
display: none;
}"
`)
})

// TODO: file issue: using two media queries like `sm:lt-lg:ml-1` produces $$ instead of the necessary "and" between media queries and so is broken - I think it is a bug elsewhere in UnoCSS. It may be connected to regexScopePlaceholder
})
40 changes: 5 additions & 35 deletions packages/svelte-scoped/src/preprocess/transformApply/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { toArray } from '@unocss/core'
import type { UnoGenerator } from '@unocss/core'
import MagicString from 'magic-string'
import type { Processed } from 'svelte/types/compiler/preprocess'
import type MagicString from 'magic-string'
import type { CssNode, Rule } from 'css-tree'
import { parse, walk } from 'css-tree'
import type { TransformApplyOptions } from '../types'
import { removeOuterQuotes } from './removeOuterQuotes'
import { writeUtilStyles } from './writeUtilStyles'
import { getUtils } from './getUtils'
Expand All @@ -14,44 +11,15 @@ interface TransformApplyContext {
uno: UnoGenerator
applyVariables: string[]
}
const DEFAULT_APPLY_VARIABLES = ['--at-apply']

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

const s = new MagicString(content)
await walkCss({ s, uno, applyVariables })

if (!s.hasChanged())
return

if (prepend)
s.prepend(prepend)

return {
code: s.toString(),
map: s.generateMap({ hires: true, source: filename || '' }),
}
}

async function walkCss(ctx: TransformApplyContext,
) {
export async function transformApply(ctx: TransformApplyContext): Promise<MagicString> {
const ast = parse(ctx.s.original, {
parseAtrulePrelude: false,
positions: true,
})

if (ast.type !== 'StyleSheet')
return
return ctx.s

const stack: Promise<void>[] = []

Expand All @@ -61,6 +29,8 @@ async function walkCss(ctx: TransformApplyContext,
})

await Promise.all(stack)

return ctx.s
}

/** transformerDirectives's handleApply function checks for style nesting (childNode.type === 'Raw') but we are not supporting it here as it is not valid syntax in Svelte style tags. If browser support becomes mainstream and Svelte updates in kind, we can support that. */
Expand Down
44 changes: 44 additions & 0 deletions packages/svelte-scoped/src/preprocess/transformStyle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { UnoGenerator } from '@unocss/core'
import { 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'

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

export async function transformStyle({ content, uno, prepend, applyVariables, filename }: {
content: string
uno: UnoGenerator
prepend?: string
applyVariables?: TransformApplyOptions['applyVariables']
filename?: string
}): 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)
await transformApply({ s, uno, applyVariables })

if (hasThemeFn)
transformTheme(s, uno.config.theme)

if (!s.hasChanged())
return

if (prepend)
s.prepend(prepend)

return {
code: s.toString(),
map: s.generateMap({ hires: true, source: filename || '' }),
}
}
75 changes: 75 additions & 0 deletions packages/svelte-scoped/src/preprocess/transformTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import MagicString from 'magic-string'

interface Theme { [key: string]: any }

export const themeRE = /theme\((.+?)\)/g

export function transformTheme(s: MagicString, theme: Theme): MagicString {
return s.replace(themeRE, (_, match) => {
const argumentsWithoutQuotes = match.slice(1, -1)
return getThemeValue(argumentsWithoutQuotes, theme)
})
}

export function getThemeValue(rawArguments: string, theme: Theme): string {
const keys = rawArguments.split('.')

let current = theme

for (const key of keys) {
if (current[key] === undefined)
throw new Error(`"${rawArguments}" is not found in your theme`)

else
current = current[key]
}

return current as unknown as string
}

if (import.meta.vitest) {
const { describe, expect, it } = import.meta.vitest

const theme = {
colors: {
blue: {
500: '#3b82f6',
},
},
spacing: {
sm: '0.875rem',
},
}

describe('transformTheme', () => {
it('loops over all occurrences and replaces', () => {
const code = `
div {
background: theme('colors.blue.500');
margin-right: theme("spacing.sm");
}`.trim()
expect(transformTheme(new MagicString(code), theme).toString()).toMatchInlineSnapshot(`
"div {
background: #3b82f6;
margin-right: 0.875rem;
}"
`)
})

it('does nothing if contains no arguments', () => {
const noArgument = 'div { background: theme() }'
expect(transformTheme(new MagicString(noArgument), theme).toString()).toBe(noArgument)
})
})

describe('getThemeValue', () => {
it('splits string into keys and traverses theme', () => {
expect(getThemeValue('colors.blue.500', theme)).toBe('#3b82f6')
expect(getThemeValue('spacing.sm', theme)).toBe('0.875rem')
})

it('throws error if not found', () => {
expect(async () => getThemeValue('size.lg', theme)).rejects.toMatchInlineSnapshot('[Error: "size.lg" is not found in your theme]')
})
})
}
9 changes: 9 additions & 0 deletions packages/svelte-scoped/test/cases/theme/Input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="mr-1" />

<style>
div {
--at-apply: mb-1;
background: theme("colors.blue.500");
margin-right: theme("spacing.sm");
}
</style>
12 changes: 12 additions & 0 deletions packages/svelte-scoped/test/cases/theme/OutputDev.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="_mr-1_gvdwv7" />

<style>
:global(._mr-1_gvdwv7) {
margin-right: 0.25rem;
}
div {
margin-bottom: 0.25rem;
background: #3b82f6;
margin-right: 0.875rem;
}
</style>
12 changes: 12 additions & 0 deletions packages/svelte-scoped/test/cases/theme/OutputProd.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="spu-xjcfo6" />

<style>
:global(.spu-xjcfo6) {
margin-right: 0.25rem;
}
div {
margin-bottom: 0.25rem;
background: #3b82f6;
margin-right: 0.875rem;
}
</style>

0 comments on commit 384879e

Please sign in to comment.