mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 21:55:11 +00:00
feat(nuxt, vite): inline global and component styles in server response (#7160)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
This commit is contained in:
parent
fe3fc34b12
commit
de62520990
@ -97,6 +97,7 @@ export async function initNitro (nuxt: Nuxt) {
|
|||||||
},
|
},
|
||||||
replace: {
|
replace: {
|
||||||
'process.env.NUXT_NO_SSR': nuxt.options.ssr === false,
|
'process.env.NUXT_NO_SSR': nuxt.options.ssr === false,
|
||||||
|
'process.env.NUXT_INLINE_STYLES': !!nuxt.options.experimental.inlineSSRStyles,
|
||||||
'process.dev': nuxt.options.dev,
|
'process.dev': nuxt.options.dev,
|
||||||
__VUE_PROD_DEVTOOLS__: false
|
__VUE_PROD_DEVTOOLS__: false
|
||||||
},
|
},
|
||||||
@ -110,6 +111,10 @@ export async function initNitro (nuxt: Nuxt) {
|
|||||||
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nuxt.options.experimental.inlineSSRStyles) {
|
||||||
|
nitroConfig.virtual!['#build/dist/server/styles.mjs'] = 'export default {}'
|
||||||
|
}
|
||||||
|
|
||||||
// Register nuxt protection patterns
|
// Register nuxt protection patterns
|
||||||
nitroConfig.rollupConfig!.plugins!.push(ImportProtectionPlugin.rollup({
|
nitroConfig.rollupConfig!.plugins!.push(ImportProtectionPlugin.rollup({
|
||||||
rootDir: nuxt.options.rootDir,
|
rootDir: nuxt.options.rootDir,
|
||||||
|
@ -37,6 +37,9 @@ const getClientManifest: () => Promise<Manifest> = () => import('#build/dist/ser
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const getSSRStyles = (): Promise<Record<string, () => Promise<string[]>>> => import('#build/dist/server/styles.mjs').then(r => r.default || r)
|
||||||
|
|
||||||
// -- SSR Renderer --
|
// -- SSR Renderer --
|
||||||
const getSSRRenderer = lazyCachedFunction(async () => {
|
const getSSRRenderer = lazyCachedFunction(async () => {
|
||||||
// Load client manifest
|
// Load client manifest
|
||||||
@ -137,6 +140,11 @@ export default defineRenderHandler(async (event) => {
|
|||||||
// Render meta
|
// Render meta
|
||||||
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
|
const renderedMeta = await ssrContext.renderMeta?.() ?? {}
|
||||||
|
|
||||||
|
// Render inline styles
|
||||||
|
const inlinedStyles = process.env.NUXT_INLINE_STYLES
|
||||||
|
? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? [])
|
||||||
|
: ''
|
||||||
|
|
||||||
// Create render context
|
// Create render context
|
||||||
const htmlContext: NuxtRenderHTMLContext = {
|
const htmlContext: NuxtRenderHTMLContext = {
|
||||||
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]),
|
||||||
@ -144,6 +152,7 @@ export default defineRenderHandler(async (event) => {
|
|||||||
renderedMeta.headTags,
|
renderedMeta.headTags,
|
||||||
_rendered.renderResourceHints(),
|
_rendered.renderResourceHints(),
|
||||||
_rendered.renderStyles(),
|
_rendered.renderStyles(),
|
||||||
|
inlinedStyles,
|
||||||
ssrContext.styles
|
ssrContext.styles
|
||||||
]),
|
]),
|
||||||
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
|
bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs!]),
|
||||||
@ -210,3 +219,16 @@ function renderHTMLDocument (html: NuxtRenderHTMLContext) {
|
|||||||
<body ${joinAttrs(html.bodyAttrs)}>${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}</body>
|
<body ${joinAttrs(html.bodyAttrs)}>${joinTags(html.bodyPreprend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}</body>
|
||||||
</html>`
|
</html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderInlineStyles (usedModules: Set<string> | string[]) {
|
||||||
|
const styleMap = await getSSRStyles()
|
||||||
|
const inlinedStyles = new Set<string>()
|
||||||
|
for (const mod of usedModules) {
|
||||||
|
if (mod in styleMap) {
|
||||||
|
for (const style of await styleMap[mod]()) {
|
||||||
|
inlinedStyles.add(`<style>${style}</style>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(inlinedStyles).join('')
|
||||||
|
}
|
||||||
|
@ -48,9 +48,26 @@ export default defineUntypedSchema({
|
|||||||
/**
|
/**
|
||||||
* Split server bundle into multiple chunks and dynamically import them.
|
* Split server bundle into multiple chunks and dynamically import them.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @see https://github.com/nuxt/framework/issues/6432
|
* @see https://github.com/nuxt/framework/issues/6432
|
||||||
*/
|
*/
|
||||||
viteServerDynamicImports: true
|
viteServerDynamicImports: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline styles when rendering HTML (currently vite only).
|
||||||
|
*
|
||||||
|
* You can also pass a function that receives the path of a Vue component
|
||||||
|
* and returns a boolean indicating whether to inline the styles for that component.
|
||||||
|
*
|
||||||
|
* @type {boolean | ((id?: string) => boolean)}
|
||||||
|
*/
|
||||||
|
inlineSSRStyles: {
|
||||||
|
$resolve(val, get) {
|
||||||
|
if (val === false || get('dev') || get('ssr') === false || get('builder') === '@nuxt/webpack-builder') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Enabled by default for vite prod with ssr
|
||||||
|
return val ?? true
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
101
packages/vite/src/plugins/ssr-styles.ts
Normal file
101
packages/vite/src/plugins/ssr-styles.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { pathToFileURL } from 'node:url'
|
||||||
|
import { Plugin } from 'vite'
|
||||||
|
import { findStaticImports } from 'mlly'
|
||||||
|
import { dirname, relative } from 'pathe'
|
||||||
|
import { genObjectFromRawEntries } from 'knitwork'
|
||||||
|
import { filename } from 'pathe/utils'
|
||||||
|
import { parseQuery, parseURL } from 'ufo'
|
||||||
|
import { isCSS } from '../utils'
|
||||||
|
|
||||||
|
interface SSRStylePluginOptions {
|
||||||
|
srcDir: string
|
||||||
|
shouldInline?: (id?: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
||||||
|
const cssMap: Record<string, string[]> = {}
|
||||||
|
const idRefMap: Record<string, string> = {}
|
||||||
|
const globalStyles = new Set<string>()
|
||||||
|
|
||||||
|
const relativeToSrcDir = (path: string) => relative(options.srcDir, path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'ssr-styles',
|
||||||
|
generateBundle (outputOptions) {
|
||||||
|
const emitted: Record<string, string> = {}
|
||||||
|
for (const file in cssMap) {
|
||||||
|
if (!cssMap[file].length) { continue }
|
||||||
|
|
||||||
|
const base = typeof outputOptions.assetFileNames === 'string'
|
||||||
|
? outputOptions.assetFileNames
|
||||||
|
: outputOptions.assetFileNames({
|
||||||
|
type: 'asset',
|
||||||
|
name: `${filename(file)}-styles.mjs`,
|
||||||
|
source: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
emitted[file] = this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
name: `${filename(file)}-styles.mjs`,
|
||||||
|
source: [
|
||||||
|
...cssMap[file].map((css, i) => `import style_${i} from './${relative(dirname(base), this.getFileName(css))}';`),
|
||||||
|
`export default [${cssMap[file].map((_, i) => `style_${i}`).join(', ')}]`
|
||||||
|
].join('\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalStylesArray = Array.from(globalStyles).map(css => idRefMap[css] && this.getFileName(idRefMap[css])).filter(Boolean)
|
||||||
|
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: 'styles.mjs',
|
||||||
|
source:
|
||||||
|
[
|
||||||
|
...globalStylesArray.map((css, i) => `import style_${i} from './${css}';`),
|
||||||
|
`const globalStyles = [${globalStylesArray.map((_, i) => `style_${i}`).join(', ')}]`,
|
||||||
|
'const resolveStyles = r => globalStyles.concat(r.default || r || [])',
|
||||||
|
`export default ${genObjectFromRawEntries(
|
||||||
|
Object.entries(emitted).map(([key, value]) => [key, `() => import('./${this.getFileName(value)}').then(resolveStyles)`])
|
||||||
|
)}`
|
||||||
|
].join('\n')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
renderChunk (_code, chunk) {
|
||||||
|
if (!chunk.isEntry) { return null }
|
||||||
|
// Entry
|
||||||
|
for (const mod in chunk.modules) {
|
||||||
|
if (isCSS(mod) && !mod.includes('&used')) {
|
||||||
|
globalStyles.add(relativeToSrcDir(mod))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
async transform (code, id) {
|
||||||
|
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||||
|
const query = parseQuery(search)
|
||||||
|
if (!pathname.match(/\.(vue|((c|m)?j|t)sx?)$/g) || query.macro) { return }
|
||||||
|
if (options.shouldInline && !options.shouldInline(id)) { return }
|
||||||
|
|
||||||
|
const relativeId = relativeToSrcDir(id)
|
||||||
|
cssMap[relativeId] = cssMap[relativeId] || []
|
||||||
|
|
||||||
|
let styleCtr = 0
|
||||||
|
for (const i of findStaticImports(code)) {
|
||||||
|
const { type } = parseQuery(i.specifier)
|
||||||
|
if (type !== 'style' && !i.specifier.endsWith('.css')) { continue }
|
||||||
|
|
||||||
|
const resolved = await this.resolve(i.specifier, id)
|
||||||
|
if (!resolved) { continue }
|
||||||
|
|
||||||
|
const ref = this.emitFile({
|
||||||
|
type: 'chunk',
|
||||||
|
name: `${filename(id)}-styles-${++styleCtr}.mjs`,
|
||||||
|
id: resolved.id + '?inline&used'
|
||||||
|
})
|
||||||
|
|
||||||
|
idRefMap[relativeToSrcDir(resolved.id)] = ref
|
||||||
|
cssMap[relativeId].push(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,10 +9,11 @@ import { ViteBuildContext, ViteOptions } from './vite'
|
|||||||
import { wpfs } from './utils/wpfs'
|
import { wpfs } from './utils/wpfs'
|
||||||
import { cacheDirPlugin } from './plugins/cache-dir'
|
import { cacheDirPlugin } from './plugins/cache-dir'
|
||||||
import { initViteNodeServer } from './vite-node'
|
import { initViteNodeServer } from './vite-node'
|
||||||
|
import { ssrStylesPlugin } from './plugins/ssr-styles'
|
||||||
|
|
||||||
export async function buildServer (ctx: ViteBuildContext) {
|
export async function buildServer (ctx: ViteBuildContext) {
|
||||||
const useAsyncEntry = ctx.nuxt.options.experimental.asyncEntry ||
|
const useAsyncEntry = ctx.nuxt.options.experimental.asyncEntry ||
|
||||||
(ctx.nuxt.options.vite.devBundler === 'vite-node' && ctx.nuxt.options.dev)
|
(ctx.nuxt.options.vite.devBundler === 'vite-node' && ctx.nuxt.options.dev)
|
||||||
ctx.entry = resolve(ctx.nuxt.options.appDir, useAsyncEntry ? 'entry.async' : 'entry')
|
ctx.entry = resolve(ctx.nuxt.options.appDir, useAsyncEntry ? 'entry.async' : 'entry')
|
||||||
|
|
||||||
const _resolve = (id: string) => resolveModule(id, { paths: ctx.nuxt.options.modulesDir })
|
const _resolve = (id: string) => resolveModule(id, { paths: ctx.nuxt.options.modulesDir })
|
||||||
@ -111,6 +112,15 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
]
|
]
|
||||||
} as ViteOptions)
|
} as ViteOptions)
|
||||||
|
|
||||||
|
if (ctx.nuxt.options.experimental.inlineSSRStyles) {
|
||||||
|
serverConfig.plugins!.push(ssrStylesPlugin({
|
||||||
|
srcDir: ctx.nuxt.options.srcDir,
|
||||||
|
shouldInline: typeof ctx.nuxt.options.experimental.inlineSSRStyles === 'function'
|
||||||
|
? ctx.nuxt.options.experimental.inlineSSRStyles
|
||||||
|
: undefined
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// Add type-checking
|
// Add type-checking
|
||||||
if (ctx.nuxt.options.typescript.typeCheck === true || (ctx.nuxt.options.typescript.typeCheck === 'build' && !ctx.nuxt.options.dev)) {
|
if (ctx.nuxt.options.typescript.typeCheck === true || (ctx.nuxt.options.typescript.typeCheck === 'build' && !ctx.nuxt.options.dev)) {
|
||||||
const checker = await import('vite-plugin-checker').then(r => r.default)
|
const checker = await import('vite-plugin-checker').then(r => r.default)
|
||||||
|
@ -393,6 +393,29 @@ describe('automatically keyed composables', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK) {
|
||||||
|
describe('inlining component styles', () => {
|
||||||
|
it('should inline styles', async () => {
|
||||||
|
const html = await $fetch('/styles')
|
||||||
|
for (const style of [
|
||||||
|
'{--assets:"assets"}', // <script>
|
||||||
|
'{--scoped:"scoped"}', // <style lang=css>
|
||||||
|
'{--postcss:"postcss"}', // <style lang=postcss>
|
||||||
|
'{--global:"global"}', // entryfile dependency
|
||||||
|
'{--plugin:"plugin"}', // plugin dependency
|
||||||
|
'{--functional:"functional"}' // functional component with css import
|
||||||
|
]) {
|
||||||
|
expect(html).toContain(style)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it.todo('does not render style hints for inlined styles')
|
||||||
|
it.todo('renders client-only styles?', async () => {
|
||||||
|
const html = await $fetch('/styles')
|
||||||
|
expect(html).toContain('{--client-only:"client-only"}')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe('prefetching', () => {
|
describe('prefetching', () => {
|
||||||
it('should prefetch components', async () => {
|
it('should prefetch components', async () => {
|
||||||
await expectNoClientErrors('/prefetch/components')
|
await expectNoClientErrors('/prefetch/components')
|
||||||
@ -437,8 +460,8 @@ describe('dynamic paths', () => {
|
|||||||
|
|
||||||
it('should work with no overrides', async () => {
|
it('should work with no overrides', async () => {
|
||||||
const html: string = await $fetch('/assets')
|
const html: string = await $fetch('/assets')
|
||||||
for (const match of html.matchAll(/(href|src)="(.*?)"/g)) {
|
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) {
|
||||||
const url = match[2]
|
const url = match[2] || match[3]
|
||||||
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy()
|
expect(url.startsWith('/_nuxt/') || url === '/public.svg').toBeTruthy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -450,7 +473,7 @@ describe('dynamic paths', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const html: string = await $fetch('/assets')
|
const html: string = await $fetch('/assets')
|
||||||
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"/g)).map(m => m[2])
|
const urls = Array.from(html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)).map(m => m[2] || m[3])
|
||||||
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
|
const cssURL = urls.find(u => /_nuxt\/assets.*\.css$/.test(u))
|
||||||
expect(cssURL).toBeDefined()
|
expect(cssURL).toBeDefined()
|
||||||
const css: string = await $fetch(cssURL!)
|
const css: string = await $fetch(cssURL!)
|
||||||
@ -471,8 +494,8 @@ describe('dynamic paths', () => {
|
|||||||
await startServer()
|
await startServer()
|
||||||
|
|
||||||
const html = await $fetch('/foo/assets')
|
const html = await $fetch('/foo/assets')
|
||||||
for (const match of html.matchAll(/(href|src)="(.`*?)"/g)) {
|
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) {
|
||||||
const url = match[2]
|
const url = match[2] || match[3]
|
||||||
expect(
|
expect(
|
||||||
url.startsWith('/foo/_other/') ||
|
url.startsWith('/foo/_other/') ||
|
||||||
url === '/foo/public.svg' ||
|
url === '/foo/public.svg' ||
|
||||||
@ -488,8 +511,8 @@ describe('dynamic paths', () => {
|
|||||||
await startServer()
|
await startServer()
|
||||||
|
|
||||||
const html = await $fetch('/assets')
|
const html = await $fetch('/assets')
|
||||||
for (const match of html.matchAll(/(href|src)="(.*?)"/g)) {
|
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) {
|
||||||
const url = match[2]
|
const url = match[2] || match[3]
|
||||||
expect(
|
expect(
|
||||||
url.startsWith('./_nuxt/') ||
|
url.startsWith('./_nuxt/') ||
|
||||||
url === './public.svg' ||
|
url === './public.svg' ||
|
||||||
@ -516,8 +539,8 @@ describe('dynamic paths', () => {
|
|||||||
await startServer()
|
await startServer()
|
||||||
|
|
||||||
const html = await $fetch('/foo/assets')
|
const html = await $fetch('/foo/assets')
|
||||||
for (const match of html.matchAll(/(href|src)="(.*?)"/g)) {
|
for (const match of html.matchAll(/(href|src)="(.*?)"|url\(([^)]*?)\)/g)) {
|
||||||
const url = match[2]
|
const url = match[2] || match[3]
|
||||||
expect(
|
expect(
|
||||||
url.startsWith('https://example.com/_cdn/') ||
|
url.startsWith('https://example.com/_cdn/') ||
|
||||||
url === 'https://example.com/public.svg' ||
|
url === 'https://example.com/public.svg' ||
|
||||||
|
3
test/fixtures/basic/assets/assets.css
vendored
Normal file
3
test/fixtures/basic/assets/assets.css
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
--assets: 'assets';
|
||||||
|
}
|
3
test/fixtures/basic/assets/functional.css
vendored
Normal file
3
test/fixtures/basic/assets/functional.css
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
--functional: 'functional';
|
||||||
|
}
|
3
test/fixtures/basic/assets/global.css
vendored
Normal file
3
test/fixtures/basic/assets/global.css
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
--global: 'global';
|
||||||
|
}
|
3
test/fixtures/basic/assets/plugin.css
vendored
Normal file
3
test/fixtures/basic/assets/plugin.css
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
--plugin: 'plugin';
|
||||||
|
}
|
@ -15,3 +15,9 @@ export default defineNuxtComponent({
|
|||||||
<slot name="test" />
|
<slot name="test" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--client-only: 'client-only';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
5
test/fixtures/basic/components/FunctionalComponent.ts
vendored
Normal file
5
test/fixtures/basic/components/FunctionalComponent.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import '~/assets/functional.css'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
render: () => 'hi'
|
||||||
|
})
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -12,6 +12,7 @@ export default defineNuxtConfig({
|
|||||||
buildDir: process.env.NITRO_BUILD_DIR,
|
buildDir: process.env.NITRO_BUILD_DIR,
|
||||||
builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite',
|
builder: process.env.TEST_WITH_WEBPACK ? 'webpack' : 'vite',
|
||||||
theme: './extends/bar',
|
theme: './extends/bar',
|
||||||
|
css: ['~/assets/global.css'],
|
||||||
extends: [
|
extends: [
|
||||||
'./extends/node_modules/foo'
|
'./extends/node_modules/foo'
|
||||||
],
|
],
|
||||||
|
22
test/fixtures/basic/pages/styles.vue
vendored
Normal file
22
test/fixtures/basic/pages/styles.vue
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ClientOnlyScript />
|
||||||
|
<FunctionalComponent />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import '~/assets/assets.css'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
:root {
|
||||||
|
--postcss: 'postcss';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div {
|
||||||
|
--scoped: 'scoped';
|
||||||
|
}
|
||||||
|
</style>
|
5
test/fixtures/basic/plugins/style.ts
vendored
Normal file
5
test/fixtures/basic/plugins/style.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import '~/assets/plugin.css'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
//
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user