Skip to content

Commit 7e52a16

Browse files
refactor(svelte-scoped)!: simplify user facing setup (#4942)
Co-authored-by: Henrik Berglund <[email protected]>
1 parent a95bd1f commit 7e52a16

File tree

8 files changed

+40
-102
lines changed

8 files changed

+40
-102
lines changed

docs/integrations/svelte-scoped.md

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -237,26 +237,6 @@ Add the `%unocss-svelte-scoped.global%` placeholder into your `<head>` tag. In S
237237
</head>
238238
```
239239

240-
If using SvelteKit, you also must add the following to the `transformPageChunk` hook in your `src/hooks.server.js` file:
241-
242-
```js [src/hooks.server.js]
243-
/** @type {import('@sveltejs/kit').Handle} */
244-
export async function handle({ event, resolve }) {
245-
const response = await resolve(event, {
246-
transformPageChunk: ({ html }) =>
247-
html.replace(
248-
'%unocss-svelte-scoped.global%',
249-
'unocss_svelte_scoped_global_styles'
250-
),
251-
})
252-
return response
253-
}
254-
```
255-
256-
This transformation must be in a file whose [path includes `hooks` and `server`](https://github.com/unocss/unocss/blob/main/packages-integrations/svelte-scoped/src/_vite/global.ts#L12) (e.g. `src/hooks.server.js`, `src/hooks.server.ts`) as `svelte-scoped` will be looking in your server hooks file to replace `unocss_svelte_scoped_global_styles` with your global styles. Make sure to not import this transformation from another file, such as when using [sequence](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks-sequence) from `@sveltejs/kit/hooks`.
257-
258-
_In a regular Svelte project, Vite's `transformIndexHtml` hook will do this automatically._
259-
260240
## Svelte Preprocessor
261241

262242
Use utility styles to build a component library that is not dependent on including a companion CSS file by using a preprocessor to place generated styles directly into built components. Check out the [SvelteKit Library example](https://github.com/unocss/unocss/tree/main/examples/sveltekit-preprocess) in Stackblitz:

examples/sveltekit-preprocess/src/hooks.server.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

examples/sveltekit-scoped/src/hooks.server.js

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const PLACEHOLDER_USER_SETS_IN_INDEX_HTML = '%unocss-svelte-scoped.global%'
2-
export const GLOBAL_STYLES_PLACEHOLDER = 'unocss_svelte_scoped_global_styles'
32

43
export const DEV_GLOBAL_STYLES_DATA_TITLE = 'unocss-svelte-scoped global styles' // If global styles are setup properly this is the string the Vite plugin will find in the <head> tag
4+
5+
export const GLOBAL_STYLES_CSS_FILE_NAME = 'unocss-svelte-scoped-global.css'

packages-integrations/svelte-scoped/src/_vite/global.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,15 @@
11
import type { UnoGenerator } from '@unocss/core'
22
import type { ViteDevServer } from 'vite'
33
import type { UnocssSvelteScopedViteOptions } from './types'
4-
import { DEV_GLOBAL_STYLES_DATA_TITLE, GLOBAL_STYLES_PLACEHOLDER, PLACEHOLDER_USER_SETS_IN_INDEX_HTML } from './constants'
4+
import { DEV_GLOBAL_STYLES_DATA_TITLE, PLACEHOLDER_USER_SETS_IN_INDEX_HTML } from './constants'
55
import { getReset } from './getReset'
66

7-
/**
8-
* It would be nice to parse the svelte config to learn if user set a custom hooks.server name but both of the following methods have problems:
9-
* - const svelteConfigRaw = readFileSync('./svelte.config.js', 'utf-8') // manual parsing could fail if people import hooks name from elsewhere or use unstandard syntax
10-
* - ({ default: svelteConfig } = await import(`${viteConfig.root}/svelte.config.js`)) // throws import errors when vitePreprocess is included in svelte.config.js on Windows (related to path issues)
11-
*/
12-
export function isServerHooksFile(path: string) {
13-
return path.includes('hooks') && path.includes('server')
14-
}
15-
16-
export function replaceGlobalStylesPlaceholder(code: string, stylesTag: string) {
17-
const captureQuoteMark = '(["\'`])'
18-
const matchCapturedQuoteMark = '\\1'
19-
const QUOTES_WITH_PLACEHOLDER_RE = new RegExp(captureQuoteMark + GLOBAL_STYLES_PLACEHOLDER + matchCapturedQuoteMark)
20-
21-
const escapedStylesTag = stylesTag
22-
.replaceAll(/\\(?![`$])/g, '\\\\')
23-
.replaceAll(/(?<!\\)([`$])/g, '\\$1')
24-
25-
return code.replace(QUOTES_WITH_PLACEHOLDER_RE, `\`${escapedStylesTag}\``)
26-
// preset-web-fonts doesn't heed the minify option and sends through newlines (\n) that break if we use regular quotes here. Always using a backtick here is easier than removing newlines, which are actually kind of useful in dev mode. I might consider turning minify off altogether in dev mode.
27-
}
28-
297
export async function generateGlobalCss(uno: UnoGenerator, injectReset?: UnocssSvelteScopedViteOptions['injectReset']): Promise<string> {
308
const { css } = await uno.generate('', { preflights: true, safelist: true, minify: true })
319
const reset = injectReset ? getReset(injectReset) : ''
3210
return reset + css
3311
}
3412

35-
const SVELTE_ERROR = `[unocss] You have not setup the svelte-scoped global styles correctly. You must place '${PLACEHOLDER_USER_SETS_IN_INDEX_HTML}' in your index.html file.
36-
`
37-
const SVELTE_KIT_ERROR = `[unocss] You have not setup the svelte-scoped global styles correctly. You must place '${PLACEHOLDER_USER_SETS_IN_INDEX_HTML}' in your app.html file. You also need to have a transformPageChunk hook in your hooks.server.js file with: \`html.replace('${PLACEHOLDER_USER_SETS_IN_INDEX_HTML}', '${GLOBAL_STYLES_PLACEHOLDER}')\`. You can see an example of the usage at https://github.com/unocss/unocss/tree/main/examples/sveltekit-scoped.`
38-
3913
export function checkTransformPageChunkHook(server: ViteDevServer, isSvelteKit: boolean) {
4014
server.middlewares.use((req, res, next) => {
4115
const originalWrite = res.write
@@ -45,7 +19,7 @@ export function checkTransformPageChunkHook(server: ViteDevServer, isSvelteKit:
4519
const str = typeof chunk === 'string' ? chunk : (chunk instanceof Buffer) ? chunk.toString() : ((Array.isArray(chunk) || 'at' in chunk) ? Buffer.from(chunk).toString() : (`${chunk}`))
4620

4721
if (str.includes('<head>') && !str.includes(DEV_GLOBAL_STYLES_DATA_TITLE))
48-
server.config.logger.error(isSvelteKit ? SVELTE_KIT_ERROR : SVELTE_ERROR, { timestamp: true })
22+
server.config.logger.error(`[unocss] You have not setup the svelte-scoped global styles correctly. You must place '${PLACEHOLDER_USER_SETS_IN_INDEX_HTML}' in your \`${isSvelteKit ? 'app.html' : 'index.html'}\` file.`, { timestamp: true })
4923

5024
// @ts-expect-error - TS doesn't like this
5125
return originalWrite.call(this, chunk, ...rest)

packages-integrations/svelte-scoped/src/_vite/globalStylesPlugin.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { Plugin, ResolvedConfig } from 'vite'
22
import type { SvelteScopedContext } from '../preprocess'
33
import type { UnocssSvelteScopedViteOptions } from './types'
4-
import { DEV_GLOBAL_STYLES_DATA_TITLE, PLACEHOLDER_USER_SETS_IN_INDEX_HTML } from './constants'
5-
import { checkTransformPageChunkHook, generateGlobalCss, isServerHooksFile, replaceGlobalStylesPlaceholder } from './global'
4+
import { DEV_GLOBAL_STYLES_DATA_TITLE, GLOBAL_STYLES_CSS_FILE_NAME, PLACEHOLDER_USER_SETS_IN_INDEX_HTML } from './constants'
5+
import { checkTransformPageChunkHook, generateGlobalCss } from './global'
66

77
export function GlobalStylesPlugin(ctx: SvelteScopedContext, injectReset?: UnocssSvelteScopedViteOptions['injectReset']): Plugin {
88
let isSvelteKit: boolean
99
let viteConfig: ResolvedConfig
1010
let unoCssFileReferenceId: string
11-
let unoCssHashedLinkTag: string
11+
let unoCssGlobalFileName: string
1212

1313
return {
1414
name: 'unocss:svelte-scoped:global-styles',
@@ -20,15 +20,32 @@ export function GlobalStylesPlugin(ctx: SvelteScopedContext, injectReset?: Unocs
2020
},
2121

2222
// serve
23-
configureServer: server => checkTransformPageChunkHook(server, isSvelteKit),
23+
configureServer: (server) => {
24+
checkTransformPageChunkHook(server, isSvelteKit)
25+
26+
server.middlewares.use(`${viteConfig.base ?? '/'}${GLOBAL_STYLES_CSS_FILE_NAME}`, async (_req, res) => {
27+
res.appendHeader('Content-Type', 'text/css')
28+
res.end(await generateGlobalCss(ctx.uno, injectReset))
29+
})
30+
},
2431

2532
// serve
2633
async transform(code, id) {
2734
await ctx.ready
28-
if (isSvelteKit && viteConfig.command === 'serve' && isServerHooksFile(id)) {
29-
const css = await generateGlobalCss(ctx.uno, injectReset)
30-
return {
31-
code: replaceGlobalStylesPlaceholder(code, `<style type="text/css" data-title="${DEV_GLOBAL_STYLES_DATA_TITLE}">${css}</style>`),
35+
36+
if (isSvelteKit) {
37+
// Check for outdated setup.
38+
if (id.includes('hooks') && id.includes('server') && code.includes('unocss_svelte_scoped_global_styles')) {
39+
this.warn(`[unocss] You are probably using an outdated setup for your sveltekit app. The server hook to handle an unocss styles placeholder is no longer needed.`)
40+
}
41+
42+
if (viteConfig.command === 'serve' && code.includes(PLACEHOLDER_USER_SETS_IN_INDEX_HTML)) {
43+
// This replaces inside a file generated from the `app.html`. The placeholder is wrapped inside double quotes, thus the escaping.
44+
const tag = `<link href=\\"${viteConfig.base ?? '/'}${GLOBAL_STYLES_CSS_FILE_NAME}\\" rel=\\"stylesheet\\" data-title=\\"${DEV_GLOBAL_STYLES_DATA_TITLE}\\" />`
45+
46+
return {
47+
code: code.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, tag),
48+
}
3249
}
3350
}
3451
},
@@ -39,35 +56,36 @@ export function GlobalStylesPlugin(ctx: SvelteScopedContext, injectReset?: Unocs
3956
const css = await generateGlobalCss(ctx.uno, injectReset)
4057
unoCssFileReferenceId = this.emitFile({
4158
type: 'asset',
42-
name: 'unocss-svelte-scoped-global.css',
59+
name: GLOBAL_STYLES_CSS_FILE_NAME,
4360
source: css,
4461
})
4562
}
4663
},
4764

4865
// build
4966
renderStart() {
50-
const unoCssFileName = this.getFileName(unoCssFileReferenceId)
51-
const base = viteConfig.base ?? '/'
52-
unoCssHashedLinkTag = `<link href="${base}${unoCssFileName}" rel="stylesheet" />`
67+
unoCssGlobalFileName = this.getFileName(unoCssFileReferenceId)
5368
},
5469

5570
// build
56-
renderChunk(code, chunk) {
57-
if (isSvelteKit && chunk.moduleIds.some(id => isServerHooksFile(id)))
58-
return replaceGlobalStylesPlaceholder(code, unoCssHashedLinkTag)
71+
renderChunk(code) {
72+
if (isSvelteKit && code.includes(PLACEHOLDER_USER_SETS_IN_INDEX_HTML)) {
73+
// This replaces inside a file generated from the `app.html`. The placeholder is wrapped inside double quotes, thus the escaping.
74+
const tag = `<link href=\\"${viteConfig.base ?? '/'}${unoCssGlobalFileName}\\" rel=\\"stylesheet\\" />`
75+
76+
return code.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, tag)
77+
}
5978
},
6079

6180
// serve and build
6281
async transformIndexHtml(html) {
6382
// SvelteKit ignores this hook, so we use the `renderChunk` and `transform` hooks instead for SvelteKit, but if they ever support running this hook inside their hooks.server.js file we can simplify to just using this hook.
6483
if (!isSvelteKit) {
6584
if (viteConfig.command === 'build')
66-
return html.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, unoCssHashedLinkTag)
85+
return html.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, `<link href="${viteConfig.base ?? '/'}${unoCssGlobalFileName}" rel="stylesheet" />`)
6786

6887
if (viteConfig.command === 'serve') {
69-
const css = await generateGlobalCss(ctx.uno, injectReset)
70-
return html.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, `<style type="text/css" data-title="${DEV_GLOBAL_STYLES_DATA_TITLE}">${css}</style>`)
88+
return html.replace(PLACEHOLDER_USER_SETS_IN_INDEX_HTML, `<link href="${viteConfig.base ?? '/'}${GLOBAL_STYLES_CSS_FILE_NAME}" rel="stylesheet" data-title="${DEV_GLOBAL_STYLES_DATA_TITLE}" />`)
7189
}
7290
}
7391
},

packages-integrations/svelte-scoped/test/fixtures/escaped-unicode.css

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages-integrations/svelte-scoped/test/index.test.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@ import presetIcons from '@unocss/preset-icons'
33
import presetTypography from '@unocss/preset-typography'
44
import presetWind3 from '@unocss/preset-wind3'
55

6-
import fs from 'fs-extra'
76
import { format as prettier } from 'prettier'
87
// @ts-expect-error missing types
98
import prettierSvelte from 'prettier-plugin-svelte'
109
import { preprocess } from 'svelte/compiler'
1110
import { describe, expect, it } from 'vitest'
12-
import { GLOBAL_STYLES_PLACEHOLDER } from '../src/_vite/constants'
13-
import { replaceGlobalStylesPlaceholder } from '../src/_vite/global'
1411
import UnocssSveltePreprocess from '../src/preprocess'
1512

1613
const defaultOptions: UnocssSveltePreprocessOptions = {
@@ -55,16 +52,3 @@ describe('svelte-preprocessor', () => {
5552
})
5653
}
5754
})
58-
59-
describe('svelte-scoped helpers', () => {
60-
it('escape template literal characters in placeholder replacement', async () => {
61-
const css = await fs.readFile('packages-integrations/svelte-scoped/test/fixtures/escaped-unicode.css', 'utf8')
62-
63-
const escaped = replaceGlobalStylesPlaceholder(
64-
`'${GLOBAL_STYLES_PLACEHOLDER}'`,
65-
`<style type="text/css">${css}</style>`,
66-
)
67-
68-
expect(escaped).toContain('"\\\\200B"')
69-
})
70-
})

0 commit comments

Comments
 (0)