Skip to content

Commit 3eab6c0

Browse files
committed
feat(styles): add experimental cache for SASS compilation
1 parent f552645 commit 3eab6c0

File tree

9 files changed

+238
-42
lines changed

9 files changed

+238
-42
lines changed

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export default withPwa(defineConfig({
108108
items: [
109109
{ text: 'Vuetify Options', link: '/guide/configuration/vuetify-options' },
110110
{ text: 'SASS Customization', link: '/guide/configuration/sass' },
111+
{ text: 'Experimental Cache', link: '/guide/configuration/experimental-cache' },
111112
{ text: 'Blueprints', link: '/guide/configuration/blueprints' },
112113
{ text: 'Transform Asset URLs', link: '/guide/configuration/transform-assets' },
113114
],
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Experimental Cache
2+
3+
When using custom SASS configuration (`configFile`), Vuetify needs to compile SASS variables for every component. This process can be slow during development startup and HMR.
4+
5+
To improve performance, this module provides an experimental caching mechanism.
6+
7+
## Enabling Cache
8+
9+
You can enable the experimental cache in your `nuxt.config.ts`:
10+
11+
```ts
12+
export default defineNuxtConfig({
13+
vuetify: {
14+
moduleOptions: {
15+
styles: {
16+
configFile: 'assets/settings.scss',
17+
experimental: {
18+
cache: true
19+
}
20+
}
21+
}
22+
}
23+
})
24+
```
25+
26+
## How it works
27+
28+
When enabled, the module will:
29+
30+
1. Calculate a hash based on your:
31+
* Vuetify version
32+
* Vite version
33+
* Content of your SASS config file
34+
2. Check if a cache exists for this hash in `node_modules/.cache/vuetify-nuxt-module/styles`.
35+
3. If not found, it will pre-compile all Vuetify styles with your SASS variables into CSS files and store them in the cache directory.
36+
4. Serve these cached CSS files directly during development, bypassing the SASS compilation step.
37+
38+
## Benefits
39+
40+
* **Faster Startup**: Initial compilation happens once. Subsequent starts are instant.
41+
* **Faster HMR**: Changing Vue files doesn't trigger SASS recompilation for Vuetify styles.
42+
* **Persistent**: Cache survives server restarts.
43+
44+
## Invalidation
45+
46+
The cache is automatically invalidated (recreated) if:
47+
* You update Vuetify or Vite.
48+
* You modify your SASS configuration file.
49+
50+
You can also manually clear the cache by deleting the `node_modules/.cache/vuetify-nuxt-module` directory.

packages/vuetify-nuxt-module/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@
131131
"pathe",
132132
"perfect-debounce",
133133
"rollup",
134+
"sass",
135+
"sass-embedded",
134136
"upath",
135137
"ufo",
136138
"unconfig",

packages/vuetify-nuxt-module/src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export default defineNuxtModule<ModuleOptions>({
141141
componentsPromise: undefined!,
142142
labComponentsPromise: undefined!,
143143
vuetifyGte,
144+
vuetifyVersion: currentVersion || '0.0.0',
144145
viteVersion,
145146
}
146147

packages/vuetify-nuxt-module/src/utils/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export interface VuetifyNuxtContext {
3030
* @example ctx.vuetifyGte('3.4.0') // true if Vuetify version is 3.4.0 or greater
3131
*/
3232
vuetifyGte: (version: string) => boolean
33+
vuetifyVersion: string
3334
viteVersion: string
3435
enableRules?: boolean
3536
rulesConfiguration?: { fromLabs?: boolean, configFile?: string }
37+
stylesCachePath?: string
3638
}
3739

3840
export async function loadVuetifyConfiguration<U extends ExternalVuetifyOptions> (

packages/vuetify-nuxt-module/src/utils/configure-vite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function configureVite (configKey: string, nuxt: Nuxt, ctx: VuetifyNuxtCo
8282
viteInlineConfig.plugins.push(vuetifyImportPlugin({ autoImport }))
8383
// exclude styles plugin
8484
if ((ctx.moduleOptions.styles as any) !== false && ctx.moduleOptions.styles !== 'none') {
85-
viteInlineConfig.plugins.push(vuetifyStylesPlugin({ styles: ctx.moduleOptions.styles }, ctx.viteVersion, ctx.logger))
85+
viteInlineConfig.plugins.push(vuetifyStylesPlugin(ctx))
8686
}
8787
viteInlineConfig.plugins.push(vuetifyConfigurationPlugin(ctx), vuetifyIconsPlugin(ctx), vuetifyDateConfigurationPlugin(ctx))
8888
if (ctx.ssrClientHints.enabled) {

packages/vuetify-nuxt-module/src/utils/loader.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { prepareIcons } from './icons'
1111
import { mergeVuetifyModules } from './layers'
1212
import { cleanupBlueprint, detectDate, resolveVuetifyComponents } from './module'
1313
import { prepareSSRClientHints } from './ssr-client-hints'
14+
import { prepareVuetifyStyles } from './styles-compiler'
1415

1516
export async function load (
1617
options: VuetifyModuleOptions,
@@ -107,6 +108,8 @@ export async function load (
107108
}
108109
}
109110
}
111+
112+
await prepareVuetifyStyles(nuxt, ctx)
110113
}
111114

112115
export function registerWatcher (options: VuetifyModuleOptions, nuxt: Nuxt, ctx: VuetifyNuxtContext) {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import type { Nuxt } from '@nuxt/schema'
2+
import type { VuetifyNuxtContext } from './config'
3+
import { createHash } from 'node:crypto'
4+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
5+
import { dirname, join, relative, resolve } from 'node:path'
6+
import { pathToFileURL } from 'node:url'
7+
import { resolvePath } from '@nuxt/kit'
8+
9+
import { isObject, normalizePath, resolveVuetifyBase } from '@vuetify/loader-shared'
10+
11+
export async function prepareVuetifyStyles (nuxt: Nuxt, ctx: VuetifyNuxtContext) {
12+
const stylesConfig = ctx.moduleOptions.styles
13+
14+
if (!isObject(stylesConfig) || !('configFile' in stylesConfig)) {
15+
return
16+
}
17+
18+
if (stylesConfig.experimental?.cache === false) {
19+
return
20+
}
21+
22+
const vuetifyBase = resolveVuetifyBase()
23+
let configFile: string | undefined
24+
let configContent = ''
25+
26+
if (stylesConfig.configFile) {
27+
configFile = await resolvePath(stylesConfig.configFile)
28+
if (existsSync(configFile)) {
29+
configContent = readFileSync(configFile, 'utf8')
30+
// Add to watch list
31+
if (!ctx.vuetifyFilesToWatch.includes(configFile)) {
32+
ctx.vuetifyFilesToWatch.push(configFile)
33+
}
34+
}
35+
}
36+
37+
if (!configFile) {
38+
return
39+
}
40+
41+
// Calculate hash
42+
const hash = createHash('sha256')
43+
.update(ctx.vuetifyVersion)
44+
.update(ctx.viteVersion)
45+
.update(configContent)
46+
.update(configFile)
47+
.digest('hex')
48+
.slice(0, 8)
49+
50+
const stylesDir = resolve(nuxt.options.rootDir, 'node_modules/.cache/vuetify-nuxt-module/styles')
51+
const cacheDir = join(stylesDir, hash)
52+
ctx.stylesCachePath = cacheDir
53+
54+
// Cleanup old caches
55+
if (existsSync(stylesDir)) {
56+
const dirents = readdirSync(stylesDir, { withFileTypes: true })
57+
for (const dirent of dirents) {
58+
if (dirent.isDirectory() && dirent.name !== hash) {
59+
rmSync(join(stylesDir, dirent.name), { recursive: true, force: true })
60+
}
61+
}
62+
}
63+
64+
if (existsSync(cacheDir)) {
65+
return
66+
}
67+
68+
ctx.logger.info('Compiling Vuetify styles...')
69+
70+
// Load SASS compiler
71+
let sass: any
72+
try {
73+
sass = await import('sass')
74+
} catch {
75+
try {
76+
sass = await import('sass-embedded')
77+
} catch {
78+
ctx.logger.warn('Could not load "sass" or "sass-embedded". Skipping styles pre-compilation.')
79+
return
80+
}
81+
}
82+
83+
// Generate cache
84+
const files: string[] = []
85+
findCssFiles(join(vuetifyBase, 'lib/components'), files)
86+
findCssFiles(join(vuetifyBase, 'lib/styles'), files)
87+
88+
for (const file of files) {
89+
const relativePath = relative(vuetifyBase, file)
90+
const cacheFile = join(cacheDir, relativePath) // .css
91+
92+
// Check if .sass or .scss exists
93+
const sassFile = file.replace(/\.css$/, '.sass')
94+
const scssFile = file.replace(/\.css$/, '.scss')
95+
96+
let targetFile: string | undefined
97+
if (existsSync(sassFile)) {
98+
targetFile = sassFile
99+
} else if (existsSync(scssFile)) {
100+
targetFile = scssFile
101+
}
102+
103+
if (targetFile) {
104+
const dir = dirname(cacheFile)
105+
if (!existsSync(dir)) {
106+
mkdirSync(dir, { recursive: true })
107+
}
108+
109+
const content = `@use "${normalizePath(configFile)}";\n@use "${normalizePath(targetFile)}";\n`
110+
111+
try {
112+
const result = sass.compileString(content, {
113+
loadPaths: [
114+
dirname(configFile),
115+
dirname(targetFile),
116+
resolve(vuetifyBase, '..'),
117+
resolve(vuetifyBase, '../..'), // In case of monorepo/hoisting issues, but standard is enough
118+
vuetifyBase,
119+
],
120+
url: new URL(pathToFileURL(cacheFile).href),
121+
})
122+
writeFileSync(cacheFile, result.css, 'utf8')
123+
} catch (error) {
124+
ctx.logger.error(`Failed to compile ${targetFile}:`, error)
125+
}
126+
}
127+
}
128+
129+
// Create metadata.json
130+
const metadata = {
131+
hash,
132+
vuetifyVersion: ctx.vuetifyVersion,
133+
viteVersion: ctx.viteVersion,
134+
configFile,
135+
createdAt: new Date().toISOString(),
136+
}
137+
writeFileSync(join(cacheDir, 'metadata.json'), JSON.stringify(metadata, null, 2), 'utf8')
138+
}
139+
140+
function findCssFiles (dir: string, fileList: string[] = []) {
141+
if (!existsSync(dir)) {
142+
return fileList
143+
}
144+
const files = readdirSync(dir)
145+
for (const file of files) {
146+
const filePath = join(dir, file)
147+
const stat = statSync(filePath)
148+
if (stat.isDirectory()) {
149+
findCssFiles(filePath, fileList)
150+
} else {
151+
if (file.endsWith('.css')) {
152+
fileList.push(filePath)
153+
}
154+
}
155+
}
156+
return fileList
157+
}

0 commit comments

Comments
 (0)