diff --git a/src/utils/configure-nuxt.ts b/src/utils/configure-nuxt.ts index 1097821..a830d24 100644 --- a/src/utils/configure-nuxt.ts +++ b/src/utils/configure-nuxt.ts @@ -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' @@ -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 diff --git a/src/vite/vuetify-styles-plugin.ts b/src/vite/vuetify-styles-plugin.ts index a1d7456..10dce5b 100644 --- a/src/vite/vuetify-styles-plugin.ts +++ b/src/vite/vuetify-styles-plugin.ts @@ -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, -) { - let configFile: string | undefined - // let cacheDir: string | undefined - const vuetifyBase = resolveVuetifyBase() - const noneFiles = new Set() - 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 { - 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() - 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) -}