Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/utils/configure-nuxt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Nuxt } from '@nuxt/schema'
import { addImports, addPlugin, extendWebpackConfig, isNuxtMajorVersion } from '@nuxt/kit'
import { addImports, addPlugin, extendWebpackConfig, isNuxtMajorVersion, resolveAlias } from '@nuxt/kit'
import { RESOLVED_VIRTUAL_MODULES } from '../vite/constants'
import type { VuetifyNuxtContext } from './config'
import { addVuetifyNuxtPlugins } from './vuetify-nuxt-plugins'
Expand Down Expand Up @@ -30,7 +30,11 @@ export function configureNuxt(
if (!disableVuetifyStyles) {
nuxt.options.css ??= []
// always add vuetify/styles
nuxt.options.css.unshift('vuetify/styles')
if (typeof styles === 'object') {
nuxt.options.css.unshift(`virtual:vuetify-custom-styles?settings=${resolveAlias(styles.configFile)}`)
} else {
nuxt.options.css.unshift('vuetify/styles')
}
}

// transpile always vuetify and runtime folder
Expand Down
195 changes: 80 additions & 115 deletions src/vite/vuetify-styles-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,134 +1,99 @@
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import type { Plugin } from 'vite'
import { isObject, normalizePath, resolveVuetifyBase } from '@vuetify/loader-shared'
import { isAbsolute, relative as relativePath } from 'pathe'
import type { Options } from '@vuetify/loader-shared'
import path from 'upath'
import semver from 'semver'
import type { VuetifyNuxtContext } from '../utils/config'
import path from 'pathe'

export function vuetifyStylesPlugin(
options: Options,
viteVersion: VuetifyNuxtContext['viteVersion'],
_logger: ReturnType<typeof import('@nuxt/kit')['useLogger']>,
) {
let configFile: string | undefined
// let cacheDir: string | undefined
const vuetifyBase = resolveVuetifyBase()
const noneFiles = new Set<string>()
let isNone = false
let sassVariables = false
let fileImport = false
const PREFIX = 'vuetify-styles/'
const SSR_PREFIX = `/@${PREFIX}`
const resolveCss = resolveCssFactory()
const pluginName = 'vuetify:styles:nuxt'

return <Plugin>{
name: 'vuetify:styles:nuxt',
enforce: 'pre',
configResolved(config) {
if (config.plugins.findIndex(plugin => plugin.name === 'vuetify:styles') > -1)
throw new Error('Remove vite-plugin-vuetify from your Nuxt config file, this module registers a modified version.')
const RE_VUETIFY_STYLE_IMPORT = /^(?:\/)?virtual:vuetify-custom-styles(?:\?|$)/

if (isObject(options.styles)) {
sassVariables = true
// use file import when vite version > 5.4.2
// check https://github.com/vitejs/vite/pull/17909
fileImport = semver.gt(viteVersion, '5.4.2')
if (path.isAbsolute(options.styles.configFile))
configFile = path.resolve(options.styles.configFile)
else
configFile = path.resolve(path.join(config.root || process.cwd(), options.styles.configFile))
const VIRTUAL_VUETIFY_STYLE = 'virtual:vuetify-style.scss'
const RE_VIRTUAL_VUETIFY_STYLE = new RegExp(VIRTUAL_VUETIFY_STYLE)

configFile = fileImport
? pathToFileURL(configFile).href
: normalizePath(configFile)
}
else {
isNone = options.styles === 'none'
}
},
async resolveId(source, importer, { custom, ssr }) {
if (source.startsWith(PREFIX) || source.startsWith(SSR_PREFIX)) {
if (source.match(/\.s[ca]ss$/))
return source
const SCSS_VUETIFY_STYLE_PATH = 'vuetify/lib/styles/main.scss'
const SASS_VUETIFY_STYLE_PATH = 'vuetify/lib/styles/main.sass'
const CSS_VUETIFY_STYLE_PATH = 'vuetify/lib/styles/main.css'

const idx = source.indexOf('?')
return idx > -1 ? source.slice(0, idx) : source
}
const SOURCE_PARAM = 'source'
const SETTINGS_PARAM = 'settings'

if (
source === 'vuetify/styles' || (
importer
&& source.endsWith('.css')
&& isSubdir(vuetifyBase, path.isAbsolute(source) ? source : importer)
)
) {
if (options.styles === 'sass')
return this.resolve(await resolveCss(source), importer, { skipSelf: true, custom })
const vuetifyPaths = {
scss: SCSS_VUETIFY_STYLE_PATH,
sass: SASS_VUETIFY_STYLE_PATH,
css: CSS_VUETIFY_STYLE_PATH,
}

const resolution = await this.resolve(source, importer, { skipSelf: true, custom })
if (!resolution)
return undefined
export interface Options {
styles: {
configFile: string
}
}

const target = await resolveCss(resolution.id)
if (isNone) {
noneFiles.add(target)
return target
}
export function vuetifyStylesPlugin(options: Options, _viteVersion: string, _logger: any): Plugin {
let appRoot: string | null = null

return `${ssr ? SSR_PREFIX : PREFIX}${path.relative(vuetifyBase, target)}`
return {
name: pluginName,
configResolved(config) {
appRoot = config.root
},
resolveId: {
filter: {
id: RE_VUETIFY_STYLE_IMPORT
},
async handler(id, importer) {
const inlineSource = new URLSearchParams(id.split('?')[1]).get(SETTINGS_PARAM)
if (inlineSource) {
const resolveDir = importer || appRoot || ''
const params = new URLSearchParams(id.split('?')[1])
params.set(SOURCE_PARAM, resolveDir)
params.set(SETTINGS_PARAM, inlineSource)
return VIRTUAL_VUETIFY_STYLE + '?' + params.toString()
}
return this.resolve(vuetifyPaths.css, 'vuetify', { skipSelf: true })
}

return undefined
},
load(id) {
if (sassVariables) {
const target = id.startsWith(PREFIX)
? path.resolve(vuetifyBase, id.slice(PREFIX.length))
: id.startsWith(SSR_PREFIX)
? path.resolve(vuetifyBase, id.slice(SSR_PREFIX.length))
: undefined
load: {
filter: {
id: RE_VIRTUAL_VUETIFY_STYLE
},
async handler(id) {
const params = new URLSearchParams(id.split('?')[1])
const source = params.get(SOURCE_PARAM)
const settings = params.get(SETTINGS_PARAM)

const VUETIFY = await this.resolve(vuetifyPaths.sass, 'vuetify') ?? await this.resolve(vuetifyPaths.scss, 'vuetify')

if (!VUETIFY) {
console.error(`[${pluginName}:load] vuetify not found`, id)
return
}

if (source && settings) {
let settingsPath = null
try {
if (source.startsWith('virtual:') || source.endsWith('.html') || source === 'undefined') {
settingsPath = await this.resolve(settings, appRoot || '')
} else {
settingsPath = await this.resolve(settings, source)
}
} catch (e) {
console.error(`[${pluginName}:load]`, e)
settingsPath = await this.resolve(settings, appRoot || '')
}

if (!settingsPath) {
console.error(`[${pluginName}:load] settingsPath not found`, settings, source)
return
}

this.addWatchFile(settingsPath.id)

if (target) {
const suffix = target.match(/\.scss/) ? ';\n' : '\n'
return {
code: `@use "${configFile}"${suffix}@use "${fileImport ? pathToFileURL(target).href : normalizePath(target)}"${suffix}`,
map: {
mappings: '',
},
code: `@use '${path.normalize(settingsPath.id)}';
@use '${path.normalize(VUETIFY!.id)}';
`,
}
}
}
return isNone && noneFiles.has(id) ? '' : undefined
},
},
}
}

function resolveCssFactory() {
const mappings = new Map<string, string>()
return async (source: string) => {
let mapping = mappings.get(source)
if (!mapping) {
try {
mapping = source.replace(/\.css$/, '.sass')
await fsp.access(mapping, fs.constants.R_OK)
}
catch (err) {
if (!(err instanceof Error && 'code' in err && err.code === 'ENOENT'))
throw err
mapping = source.replace(/\.css$/, '.scss')
}
mappings.set(source, mapping)
}
return mapping
}
}

function isSubdir(root: string, test: string) {
const relative = relativePath(root, test)
return relative && !relative.startsWith('..') && !isAbsolute(relative)
}