vuetify-nuxt-module 1.0.0-beta.8 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,32 +26,29 @@
26
26
 
27
27
  ## 🚀 Features
28
28
 
29
- - 📖 [**Documentation & guides**](https://nuxt.vuetifyjs.com/)
29
+ > 📖 Full [**documentation & guides**](https://nuxt.vuetifyjs.com/)
30
+
30
31
  - 👌 **Zero-Config**: sensible built-in default [Vuetify](https://vuetifyjs.com/) configuration for common use cases
31
- - 🔌 **Extensible**: expose the ability to customize the Vuetify configuration via [Nuxt Runtime Hooks](https://nuxt.com/docs/guide/going-further/hooks#usage-with-plugins)
32
- - **Fully Tree Shakable**: by default, only the needed Vuetify components are imported
33
- - 🛠️ **Versatile**: custom Vuetify [directives](https://vuetifyjs.com/en/getting-started/installation/#manual-steps) and [labs components](https://vuetifyjs.com/en/labs/introduction/) registration
34
- - **Configurable Styles**: configure your variables using [Vuetify SASS Variables](https://vuetifyjs.com/en/features/sass-variables/)
35
- - 💥 **SSR**: automatic SSR detection and configuration including [HTTP Client hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints)
36
- - 🔩 **Nuxt Layers and Module Hooks**: load your Vuetify configuration using [Nuxt Layers](https://nuxt.com/docs/getting-started/layers#layers) or using a custom module via `vuetify:registerModule` [Nuxt Module Hook](https://nuxt.com/docs/guide/going-further/hooks#nuxt-hooks-build-time)
37
- - 📥 **Vuetify Configuration File**: configure your Vuetify options using a custom `vuetify.config` file, no dev server restart needed
38
- - 🔥 **Pure CSS Icons**: no more font/js icons, use the new `unocss-mdi` icon set or build your own with UnoCSS Preset Icons
39
- - 😃 **Icon Fonts**: configure the [icon font](https://vuetifyjs.com/en/features/icon-fonts/) you want to use, the module will automatically import it for you using CDN or local dependencies
40
- - 🎭 **SVG Icons**: ready to use [@mdi/js](https://www.npmjs.com/package/@mdi/js) and [@fortawesome/vue-fontawesome](https://www.npmjs.com/package/@fortawesome/vue-fontawesome) SVG icons packs
41
- - 📦 **Multiple Icon Sets**: register [multiple icon sets](https://vuetifyjs.com/en/features/icon-fonts/#multiple-icon-sets)
42
- - 🌍 **I18n Ready**: install [@nuxtjs/i18n](https://i18n.nuxtjs.org/) Nuxt module, and you're ready to use Vuetify [internationalization](https://vuetifyjs.com/en/features/internationalization/) features
43
- - 📆 **Date Components**: use Vuetify components [that require date functionality](https://vuetifyjs.com/en/features/dates/) installing and configuring one of the [@date-io](https://github.com/dmtrKovalenko/date-io#projects) adapters
44
- - 💬 **Auto-Import Vuetify Locale Messages**: add [Vuetify Locale Messages](https://vuetifyjs.com/en/features/internationalization/#getting-started) adding just the locales you want to use, no more imports needed
45
- - ⚙️ **Auto-Import Vuetify Composables**: you don't need to import Vuetify composables manually, they are automatically imported for you
46
- - 🎨 **Vuetify Blueprints**: use [Vuetify Blueprints](https://vuetifyjs.com/en/features/blueprints/) to quickly scaffold components
47
- - 👀 **Nuxt DevTools**: ready to inspect your Vuetify styles with the [Nuxt DevTools](https://github.com/nuxt/devtools) inspector
32
+ - **Fully Tree Shakable**: by default, only the Vuetify components you use are imported
33
+ - 🪄 **Auto-Import**: Vuetify components and composables are auto-imported no manual imports needed
34
+ - 🔌 **Extensible**: customize the Vuetify configuration via [Nuxt Runtime Hooks](https://nuxt.com/docs/guide/going-further/hooks#usage-with-plugins), [Nuxt Layers](https://nuxt.com/docs/getting-started/layers#layers), the `vuetify:registerModule` [module hook](https://nuxt.com/docs/guide/going-further/hooks#nuxt-hooks-build-time), or a dedicated `vuetify.config` file
35
+ - 💥 **SSR**: automatic SSR detection and configuration, including [HTTP Client Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints)
36
+ - **Configurable Styles**: configure your variables using [Vuetify SASS Variables](https://vuetifyjs.com/en/features/sass-variables/)
37
+ - 🛠️ **Directives & Labs**: optional [directives](https://vuetifyjs.com/en/getting-started/installation/#manual-steps) and [labs components](https://vuetifyjs.com/en/labs/introduction/) registration
38
+ - 🎭 **Icons**: pure-CSS icons (UnoCSS), [icon fonts](https://vuetifyjs.com/en/features/icon-fonts/) (CDN or local), SVG packs ([@mdi/js](https://www.npmjs.com/package/@mdi/js), [FontAwesome](https://www.npmjs.com/package/@fortawesome/vue-fontawesome)), and [multiple icon sets](https://vuetifyjs.com/en/features/icon-fonts/#multiple-icon-sets)
39
+ - 🌍 **I18n**: integrate [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for Vuetify [internationalization](https://vuetifyjs.com/en/features/internationalization/), with auto-imported Vuetify locale messages
40
+ - 📆 **Date Components**: use Vuetify [date components](https://vuetifyjs.com/en/features/dates/) via the [@date-io](https://github.com/dmtrKovalenko/date-io#projects) adapters
41
+ - 🎨 **Blueprints**: scaffold quickly with [Vuetify Blueprints](https://vuetifyjs.com/en/features/blueprints/)
48
42
  - 🦾 **Type Strong**: written in [TypeScript](https://www.typescriptlang.org/)
49
43
 
50
44
  ## 📦 Install
51
45
 
52
46
  > Requires Vite, will not work with Webpack
53
47
 
48
+ `vuetify` is a peer dependency (Vuetify 3 or 4) — install it alongside the module:
49
+
54
50
  ```bash
51
+ npm install -D vuetify
55
52
  npx nuxt module add vuetify-nuxt-module
56
53
  ```
57
54
 
package/dist/module.d.mts CHANGED
@@ -26,8 +26,13 @@ interface DateOptions {
26
26
  adapter?: DateAdapter;
27
27
  /**
28
28
  * Formats.
29
+ *
30
+ * Only serializable `Intl.DateTimeFormatOptions` values are supported here:
31
+ * the date configuration is statically serialized to a virtual module, so
32
+ * function-valued formats cannot be expressed (see #313, #331). Use a custom
33
+ * date adapter if you need function formats.
29
34
  */
30
- formats?: Record<string, string>;
35
+ formats?: Record<string, Intl.DateTimeFormatOptions>;
31
36
  /**
32
37
  * Locales.
33
38
  *
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.15.0"
6
6
  },
7
- "version": "1.0.0-beta.8",
7
+ "version": "1.0.0-rc.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,22 +1,19 @@
1
1
  import { existsSync, statSync, readFileSync } from 'node:fs';
2
2
  import { pathToFileURL, fileURLToPath } from 'node:url';
3
3
  import { addPluginTemplate, resolvePath, addTemplate, extendWebpackConfig, isNuxtMajorVersion, addImports, addPlugin, addVitePlugin, useLogger, defineNuxtModule, getNuxtVersion, findPath, hasNuxtModule, createResolver } from '@nuxt/kit';
4
- import { isAbsolute, resolve, dirname, relative } from 'pathe';
4
+ import { isAbsolute, resolve, dirname } from 'pathe';
5
5
  import semver from 'semver';
6
6
  import { createFilter, version as version$1 } from 'vite';
7
7
  import defu from 'defu';
8
- import { transformAssetUrls } from 'vite-plugin-vuetify';
8
+ import { transformAssetUrls, generateImports } from '@vuetify/loader-shared';
9
9
  import Styles from '@vuetify/unplugin-styles/vite';
10
10
  import { isPackageExists } from 'local-pkg';
11
- import { generateImports } from '@vuetify/loader-shared';
12
- import destr from 'destr';
13
11
  import { parseQuery, parseURL } from 'ufo';
14
12
  import { readFile } from 'node:fs/promises';
15
- import { debounce } from 'perfect-debounce';
16
13
  import process from 'node:process';
17
14
  import { createConfigLoader } from 'unconfig';
18
15
 
19
- const version = "1.0.0-beta.8";
16
+ const version = "1.0.0-rc.1";
20
17
 
21
18
  const VIRTUAL_VUETIFY_CONFIGURATION = "virtual:vuetify-configuration";
22
19
  const RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION = `\0${VIRTUAL_VUETIFY_CONFIGURATION}`;
@@ -356,6 +353,11 @@ function vuetifyConfigurationPlugin(ctx) {
356
353
  },
357
354
  async load(id) {
358
355
  if (id === RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION) {
356
+ if (ctx.isDev && ctx.canHmrConfig) {
357
+ for (const file of ctx.vuetifyFilesToWatch) {
358
+ this.addWatchFile(file);
359
+ }
360
+ }
359
361
  const {
360
362
  directives: _directives,
361
363
  date: _date,
@@ -377,7 +379,12 @@ function vuetifyConfigurationPlugin(ctx) {
377
379
  }
378
380
  const result = await buildConfiguration(ctx);
379
381
  const deepCopy = result.messages.length > 0;
380
- return `${result.imports}
382
+ let configDepImports = "";
383
+ if (ctx.isDev && ctx.canHmrConfig && this.environment?.name === "ssr") {
384
+ configDepImports = ctx.vuetifyFilesToWatch.map((file) => `import ${JSON.stringify(file)}`).join("\n");
385
+ }
386
+ return `${configDepImports}
387
+ ${result.imports}
381
388
 
382
389
  export const isDev = ${ctx.isDev}
383
390
  export function vuetifyConfiguration() {
@@ -596,6 +603,70 @@ ${useLocales.map((locale) => {
596
603
  };
597
604
  }
598
605
 
606
+ const VUETIFY_TO_DATE_FNS = {
607
+ en: "enUS",
608
+ fa: "faIR",
609
+ no: "nb",
610
+ srCyrl: "sr",
611
+ zhHans: "zhCN",
612
+ zhHant: "zhTW"
613
+ };
614
+ const DATE_FNS_SUPPORTED = /* @__PURE__ */ new Set([
615
+ "af",
616
+ "ar",
617
+ "az",
618
+ "bg",
619
+ "ca",
620
+ "ckb",
621
+ "cs",
622
+ "da",
623
+ "de",
624
+ "el",
625
+ "enUS",
626
+ "es",
627
+ "et",
628
+ "faIR",
629
+ "fi",
630
+ "fr",
631
+ "he",
632
+ "hr",
633
+ "hu",
634
+ "id",
635
+ "it",
636
+ "ja",
637
+ "km",
638
+ "ko",
639
+ "lt",
640
+ "lv",
641
+ "nb",
642
+ "nl",
643
+ "pl",
644
+ "pt",
645
+ "ro",
646
+ "ru",
647
+ "sk",
648
+ "sl",
649
+ "sr",
650
+ "srLatn",
651
+ "sv",
652
+ "th",
653
+ "tr",
654
+ "uk",
655
+ "vi",
656
+ "zhCN",
657
+ "zhTW"
658
+ ]);
659
+ function resolveDateFnsLocaleName(code) {
660
+ if (!code) {
661
+ return { name: "enUS", fallback: true };
662
+ }
663
+ const candidate = VUETIFY_TO_DATE_FNS[code] ?? code;
664
+ if (DATE_FNS_SUPPORTED.has(candidate)) {
665
+ return { name: candidate, fallback: false };
666
+ }
667
+ return { name: "enUS", fallback: true };
668
+ }
669
+
599
670
  function vuetifyDateConfigurationPlugin(ctx) {
600
671
  return {
601
672
  name: "vuetify:date-configuration:nuxt",
@@ -607,6 +678,11 @@ function vuetifyDateConfigurationPlugin(ctx) {
607
678
  },
608
679
  async load(id) {
609
680
  if (id === RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION) {
681
+ if (ctx.isDev && ctx.canHmrConfig) {
682
+ for (const file of ctx.vuetifyFilesToWatch) {
683
+ this.addWatchFile(file);
684
+ }
685
+ }
610
686
  if (!ctx.dateAdapter) {
611
687
  return `
612
688
  export const enabled = false
@@ -619,34 +695,41 @@ export function dateConfiguration() {
619
695
  `;
620
696
  }
621
697
  const { adapter: _adapter, ...newDateOptions } = ctx.vuetifyOptions.date ?? {};
622
- return `${buildImports()}
698
+ let dateFnsLocale;
699
+ if (ctx.dateAdapter === "date-fns") {
700
+ const resolved = resolveDateFnsLocaleName(ctx.vuetifyOptions.locale?.locale);
701
+ dateFnsLocale = resolved.name;
702
+ if (resolved.fallback) {
703
+ ctx.logger.warn(`[vuetify-nuxt-module] date-fns locale for "${ctx.vuetifyOptions.locale?.locale ?? "(unset)"}" not found, falling back to "enUS". Set "vuetifyOptions.locale.locale" to a supported locale.`);
704
+ }
705
+ }
706
+ return `${buildImports(dateFnsLocale)}
623
707
  export const enabled = true
624
708
  export const isDev = ${ctx.isDev}
625
709
  export const i18n = ${ctx.i18n}
626
710
  export const adapter = '${ctx.dateAdapter}'
627
711
  export function dateConfiguration() {
628
712
  const options = JSON.parse('${JSON.stringify(newDateOptions)}')
629
- ${buildAdapter()}
713
+ ${buildAdapter(dateFnsLocale)}
630
714
  return options
631
715
  }
632
716
  `;
633
717
  }
634
718
  }
635
719
  };
636
- function buildAdapter() {
720
+ function buildAdapter(dateFnsLocale) {
637
721
  if (ctx.dateAdapter === "custom" || ctx.dateAdapter === "vuetify" && ctx.vuetifyGte("3.4.0")) {
638
722
  return "";
639
723
  }
640
724
  if (ctx.dateAdapter === "vuetify") {
641
725
  return "options.adapter = VuetifyDateAdapter";
642
726
  }
643
- const locale = ctx.vuetifyOptions.locale?.locale ?? "en";
644
727
  if (ctx.dateAdapter === "date-fns") {
645
- return `options.adapter = new Adapter({ locale: ${locale} })`;
728
+ return `options.adapter = new Adapter({ locale: ${dateFnsLocale} })`;
646
729
  }
647
730
  return "options.adapter = Adapter";
648
731
  }
649
- function buildImports() {
732
+ function buildImports(dateFnsLocale) {
650
733
  if (ctx.dateAdapter === "custom" || ctx.dateAdapter === "vuetify" && ctx.vuetifyGte("3.4.0")) {
651
734
  return "";
652
735
  }
@@ -655,7 +738,7 @@ export function dateConfiguration() {
655
738
  }
656
739
  const imports = [`import Adapter from '@date-io/${ctx.dateAdapter}'`];
657
740
  if (ctx.dateAdapter === "date-fns") {
658
- imports.push(`import { ${ctx.vuetifyOptions.locale?.locale ?? "en"} } from 'date-fns/locale'`);
741
+ imports.push(`import { ${dateFnsLocale} } from 'date-fns/locale'`);
659
742
  }
660
743
  return imports.join("\n");
661
744
  }
@@ -672,6 +755,11 @@ function vuetifyIconsPlugin(ctx) {
672
755
  },
673
756
  async load(id) {
674
757
  if (id === RESOLVED_VIRTUAL_VUETIFY_ICONS_CONFIGURATION) {
758
+ if (ctx.isDev && ctx.canHmrConfig) {
759
+ for (const file of ctx.vuetifyFilesToWatch) {
760
+ this.addWatchFile(file);
761
+ }
762
+ }
675
763
  const {
676
764
  enabled,
677
765
  unocss,
@@ -841,10 +929,24 @@ function parseId2(id) {
841
929
  id = id.replace(/^(virtual:nuxt:|virtual:)/, "");
842
930
  return parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id));
843
931
  }
932
+ function reviver(key, value) {
933
+ if (key === "__proto__" || key === "constructor") {
934
+ return void 0;
935
+ }
936
+ return value;
937
+ }
938
+ function parseProps(value) {
939
+ try {
940
+ const parsed = JSON.parse(value, reviver);
941
+ return parsed && typeof parsed === "object" ? parsed : void 0;
942
+ } catch {
943
+ return void 0;
944
+ }
945
+ }
844
946
  function parseId(id) {
845
947
  const { search, pathname } = parseId2(id);
846
948
  const query = parseQuery(search);
847
- const urlProps = query.props ? destr(query.props) : void 0;
949
+ const urlProps = query.props ? parseProps(query.props) : void 0;
848
950
  return {
849
951
  query: urlProps,
850
952
  path: pathname ?? id
@@ -1266,6 +1368,29 @@ async function loadVuetifyConfiguration(cwd = process.cwd(), configOrPath = cwd,
1266
1368
  return result;
1267
1369
  }
1268
1370
 
1371
+ const MODULE_DEFAULTS = {
1372
+ moduleOptions: {
1373
+ importComposables: true,
1374
+ includeTransformAssetsUrls: true,
1375
+ styles: true,
1376
+ rulesConfiguration: {
1377
+ fromLabs: true
1378
+ }
1379
+ },
1380
+ vuetifyOptions: {
1381
+ labComponents: false,
1382
+ directives: false
1383
+ }
1384
+ };
1385
+ function finalizeConfiguration(moduleOptions) {
1386
+ if (moduleOptions.length > 1) {
1387
+ const [app, ...rest] = moduleOptions;
1388
+ const configuration = defu(app, ...rest, MODULE_DEFAULTS);
1389
+ dedupeIcons(configuration, moduleOptions.toReversed());
1390
+ return configuration;
1391
+ }
1392
+ return defu(moduleOptions[0] ?? {}, MODULE_DEFAULTS);
1393
+ }
1269
1394
  async function mergeVuetifyModules(options, nuxt) {
1270
1395
  const moduleOptions = [];
1271
1396
  const vuetifyConfigurationFilesToWatch = /* @__PURE__ */ new Set();
@@ -1293,37 +1418,18 @@ async function mergeVuetifyModules(options, nuxt) {
1293
1418
  options.vuetifyOptions
1294
1419
  );
1295
1420
  if (nuxt.options.dev && resolvedOptions.sources.length > 0) {
1296
- if (nuxt.options.ssr) {
1297
- for (const s of resolvedOptions.sources) {
1298
- nuxt.options.watch.push(s.replace(/\\/g, "/"));
1299
- }
1300
- } else {
1301
- for (const s of resolvedOptions.sources) {
1302
- vuetifyConfigurationFilesToWatch.add(s.replace(/\\/g, "/"));
1303
- }
1421
+ for (const s of resolvedOptions.sources) {
1422
+ vuetifyConfigurationFilesToWatch.add(s.replace(/\\/g, "/"));
1304
1423
  }
1305
1424
  }
1306
1425
  moduleOptions.unshift({
1307
1426
  moduleOptions: options.moduleOptions,
1308
1427
  vuetifyOptions: resolvedOptions.config
1309
1428
  });
1310
- if (moduleOptions.length > 1) {
1311
- const [app, ...rest] = moduleOptions;
1312
- const configuration = defu(app, ...rest);
1313
- dedupeIcons(configuration, moduleOptions.toReversed());
1314
- return {
1315
- configuration,
1316
- vuetifyConfigurationFilesToWatch
1317
- };
1318
- } else {
1319
- return {
1320
- configuration: {
1321
- moduleOptions: options.moduleOptions,
1322
- vuetifyOptions: resolvedOptions.config
1323
- },
1324
- vuetifyConfigurationFilesToWatch
1325
- };
1326
- }
1429
+ return {
1430
+ configuration: finalizeConfiguration(moduleOptions),
1431
+ vuetifyConfigurationFilesToWatch
1432
+ };
1327
1433
  }
1328
1434
  function dedupeIcons(configuration, moduleOptions) {
1329
1435
  const vuetifyOptions = configuration.vuetifyOptions;
@@ -1368,6 +1474,16 @@ function resolveColorSchemeCookie(options, logger) {
1368
1474
  cookieSameSite
1369
1475
  };
1370
1476
  }
1477
+ function resolveDefaultTheme(defaultTheme, themes, lightThemeName, logger) {
1478
+ if (defaultTheme === "system") {
1479
+ logger.warn(`Vuetify "system" theme cannot be resolved during SSR; using "${lightThemeName}" as the server-side fallback. The browser preference is applied on the client via prefersColorScheme client hints. To avoid a flash of the wrong theme, set explicit dark/light themes and enable moduleOptions.ssrClientHints.prefersColorSchemeOptions.useBrowserThemeOnly.`);
1480
+ return lightThemeName;
1481
+ }
1482
+ if (!themes[defaultTheme]) {
1483
+ throw new Error(`Missing default theme ${defaultTheme} in the Vuetify themes!`);
1484
+ }
1485
+ return defaultTheme;
1486
+ }
1371
1487
  function prepareSSRClientHints(baseUrl, ctx) {
1372
1488
  if (!ctx.isSSR || ctx.isNuxtGenerate) {
1373
1489
  return disabledClientHints;
@@ -1394,9 +1510,6 @@ function prepareSSRClientHints(baseUrl, ctx) {
1394
1510
  if (!defaultTheme) {
1395
1511
  throw new Error("Vuetify default theme is missing in theme!");
1396
1512
  }
1397
- if (!themes[defaultTheme]) {
1398
- throw new Error(`Missing default theme ${defaultTheme} in the Vuetify themes!`);
1399
- }
1400
1513
  const darkThemeName = ssrClientHintsConfiguration.prefersColorSchemeOptions?.darkThemeName ?? "dark";
1401
1514
  if (!themes[darkThemeName]) {
1402
1515
  throw new Error(`Missing theme ${darkThemeName} in the Vuetify themes!`);
@@ -1409,9 +1522,10 @@ function prepareSSRClientHints(baseUrl, ctx) {
1409
1522
  throw new Error("Vuetify dark theme and light theme are the same, change darkThemeName or lightThemeName!");
1410
1523
  }
1411
1524
  const pcsOptions = ssrClientHintsConfiguration.prefersColorSchemeOptions;
1525
+ const effectiveDefaultTheme = resolveDefaultTheme(defaultTheme, themes, lightThemeName, ctx.logger);
1412
1526
  clientHints.prefersColorSchemeOptions = {
1413
1527
  baseUrl,
1414
- defaultTheme,
1528
+ defaultTheme: effectiveDefaultTheme,
1415
1529
  themeNames: Array.from(Object.keys(themes)),
1416
1530
  ...resolveColorSchemeCookie(pcsOptions, ctx.logger),
1417
1531
  darkThemeName,
@@ -1422,7 +1536,7 @@ function prepareSSRClientHints(baseUrl, ctx) {
1422
1536
  return clientHints;
1423
1537
  }
1424
1538
 
1425
- async function load(options, nuxt, ctx) {
1539
+ async function load(options, nuxt, ctx, reload = false) {
1426
1540
  const {
1427
1541
  configuration,
1428
1542
  vuetifyConfigurationFilesToWatch
@@ -1467,9 +1581,11 @@ async function load(options, nuxt, ctx) {
1467
1581
  ctx.dateAdapter = date[0];
1468
1582
  }
1469
1583
  }
1470
- const oldIcons = ctx.icons;
1471
- if (oldIcons && oldIcons.cdn?.length && nuxt.options.app.head.link) {
1472
- nuxt.options.app.head.link = nuxt.options.app.head.link.filter((link) => !link.key || !oldIcons.cdn.some(([key]) => link.key === key));
1584
+ if (!reload) {
1585
+ const oldIcons = ctx.icons;
1586
+ if (oldIcons && oldIcons.cdn?.length && nuxt.options.app.head.link) {
1587
+ nuxt.options.app.head.link = nuxt.options.app.head.link.filter((link) => !link.key || !oldIcons.cdn.some(([key]) => link.key === key));
1588
+ }
1473
1589
  }
1474
1590
  ctx.moduleOptions = configuration.moduleOptions;
1475
1591
  ctx.vuetifyOptions = configuration.vuetifyOptions;
@@ -1478,7 +1594,10 @@ async function load(options, nuxt, ctx) {
1478
1594
  ctx.vuetifyFilesToWatch = Array.from(vuetifyConfigurationFilesToWatch);
1479
1595
  ctx.icons = prepareIcons(ctx.unocss, ctx.logger, vuetifyAppOptions, ctx.resolvePaths);
1480
1596
  ctx.ssrClientHints = prepareSSRClientHints(nuxt.options.app.baseURL ?? "/", ctx);
1481
- if (ctx.icons.enabled) {
1597
+ if (ctx.isSSR && !ctx.ssrClientHints.prefersColorScheme && ctx.vuetifyOptions.theme && typeof ctx.vuetifyOptions.theme === "object" && ctx.vuetifyOptions.theme.defaultTheme === "system") {
1598
+ ctx.logger.warn('`theme.defaultTheme: "system"` cannot be resolved during SSR/SSG: the server has no access to the OS color-scheme preference, so the first paint defaults to light and may flash on dark systems. Set explicit dark/light themes and enable `moduleOptions.ssrClientHints.prefersColorScheme` (optionally `prefersColorSchemeOptions.useBrowserThemeOnly`). See the SSR guide.');
1599
+ }
1600
+ if (!reload && ctx.icons.enabled) {
1482
1601
  if (ctx.icons.local) {
1483
1602
  for (const css of ctx.icons.local) {
1484
1603
  nuxt.options.css.push(css);
@@ -1498,43 +1617,67 @@ async function load(options, nuxt, ctx) {
1498
1617
  }
1499
1618
  }
1500
1619
  }
1501
- function registerWatcher(options, nuxt, ctx) {
1502
- if (nuxt.options.dev) {
1503
- let pageReload;
1504
- nuxt.hooks.hook("builder:watch", (_event, path) => {
1505
- path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path));
1506
- if (!pageReload && ctx.vuetifyFilesToWatch.includes(path)) {
1507
- return nuxt.callHook("restart");
1620
+ function bindInvalidator(graph) {
1621
+ return () => {
1622
+ for (const id of RESOLVED_VIRTUAL_MODULES) {
1623
+ const mod = graph.getModuleById(id);
1624
+ if (mod) {
1625
+ graph.invalidateModule(mod);
1508
1626
  }
1509
- });
1510
- nuxt.hook("vite:serverCreated", (server, { isClient }) => {
1511
- if (!server.ws || !isClient) {
1512
- return;
1513
- }
1514
- pageReload = debounce(async () => {
1515
- const modules = [];
1516
- for (const v of RESOLVED_VIRTUAL_MODULES) {
1517
- const module = server.moduleGraph.getModuleById(v);
1518
- if (module) {
1519
- modules.push(module);
1520
- }
1521
- }
1522
- await load(options, nuxt, ctx);
1523
- if (modules.length > 0) {
1524
- await Promise.all(modules.map((m) => server.reloadModule(m)));
1525
- }
1526
- }, 50, { trailing: false });
1527
- });
1528
- addVitePlugin({
1529
- name: "vuetify:configuration:watch",
1530
- enforce: "pre",
1531
- handleHotUpdate({ file }) {
1532
- if (pageReload && ctx.vuetifyFilesToWatch.includes(file)) {
1533
- return pageReload();
1534
- }
1627
+ }
1628
+ };
1629
+ }
1630
+ function registerWatcher(options, nuxt, ctx) {
1631
+ if (!nuxt.options.dev) {
1632
+ return;
1633
+ }
1634
+ if (!ctx.canHmrConfig) {
1635
+ for (const file of ctx.vuetifyFilesToWatch) {
1636
+ nuxt.options.watch.push(file);
1637
+ }
1638
+ return;
1639
+ }
1640
+ let clientServer;
1641
+ let invalidateSsrModules;
1642
+ nuxt.hook("vite:serverCreated", (server, { isClient }) => {
1643
+ if (!isClient) {
1644
+ invalidateSsrModules = bindInvalidator(server.moduleGraph);
1645
+ return;
1646
+ }
1647
+ if (!server.ws) {
1648
+ return;
1649
+ }
1650
+ clientServer = server;
1651
+ const ssrEnv = server.environments?.ssr;
1652
+ if (ssrEnv) {
1653
+ invalidateSsrModules ??= bindInvalidator(ssrEnv.moduleGraph);
1654
+ }
1655
+ server.watcher.add(ctx.vuetifyFilesToWatch);
1656
+ });
1657
+ async function reloadConfig() {
1658
+ await load(options, nuxt, ctx, true);
1659
+ invalidateSsrModules?.();
1660
+ clientServer?.ws.send({ type: "full-reload" });
1661
+ }
1662
+ addVitePlugin({
1663
+ name: "vuetify:configuration:watch",
1664
+ enforce: "pre",
1665
+ async handleHotUpdate({ file }) {
1666
+ if (clientServer && ctx.vuetifyFilesToWatch.includes(file)) {
1667
+ await reloadConfig();
1668
+ return [];
1535
1669
  }
1536
- });
1670
+ }
1671
+ });
1672
+ }
1673
+
1674
+ const MIN_NUXT_VERSION_FOR_SSR_CONFIG_HMR = "3.18.0";
1675
+ function supportsSsrConfigHmr(nuxtVersion) {
1676
+ const parsed = semver.parse(nuxtVersion) ?? semver.coerce(nuxtVersion);
1677
+ if (!parsed) {
1678
+ return false;
1537
1679
  }
1680
+ return semver.gte(parsed.version, MIN_NUXT_VERSION_FOR_SSR_CONFIG_HMR);
1538
1681
  }
1539
1682
 
1540
1683
  const CONFIG_KEY = "vuetify";
@@ -1548,24 +1691,6 @@ const module$1 = defineNuxtModule({
1548
1691
  },
1549
1692
  version
1550
1693
  },
1551
- /**
1552
- * Default configuration options of the Nuxt module
1553
- */
1554
- defaults: () => ({
1555
- vuetifyOptions: {
1556
- labComponents: false,
1557
- directives: false
1558
- },
1559
- moduleOptions: {
1560
- importComposables: true,
1561
- includeTransformAssetsUrls: true,
1562
- styles: true,
1563
- disableVuetifyStyles: false,
1564
- rulesConfiguration: {
1565
- fromLabs: true
1566
- }
1567
- }
1568
- }),
1569
1694
  /**
1570
1695
  * Sets up the Vuetify Nuxt module.
1571
1696
  *
@@ -1601,6 +1726,7 @@ const module$1 = defineNuxtModule({
1601
1726
  moduleOptions: void 0,
1602
1727
  vuetifyOptions: void 0,
1603
1728
  vuetifyFilesToWatch: [],
1729
+ canHmrConfig: !nuxt.options.ssr || supportsSsrConfigHmr(getNuxtVersion(nuxt)),
1604
1730
  isSSR: nuxt.options.ssr,
1605
1731
  isDev: nuxt.options.dev,
1606
1732
  isNuxtGenerate: !!nuxt.options.nitro.static,
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Session cookie marking that the one-time `reloadOnFirstRequest` reload has
3
+ * already happened this browser session. Prevents an infinite reload loop when
4
+ * a browser requests client hints but never delivers them (e.g. Brave Shields
5
+ * strip `Sec-CH-*`), since `firstRequest` would otherwise stay `true` forever (#334).
6
+ */
7
+ export declare const RELOAD_GUARD_COOKIE = "vuetify-nuxt-client-hints-reloaded";
8
+ /** True when the guard cookie is present in a `document.cookie` string. */
9
+ export declare function hasReloadGuardCookie(cookie: string): boolean;
10
+ /** Build a session guard cookie (no expiry → cleared on browser close). */
11
+ export declare function buildReloadGuardCookie(path: string): string;
12
+ /** Whether to perform the first-request reload: only once per session. */
13
+ export declare function shouldReloadOnFirstRequest(firstRequest: boolean, reloadOnFirstRequest: boolean, alreadyReloaded: boolean): boolean;
@@ -0,0 +1,11 @@
1
+ export const RELOAD_GUARD_COOKIE = "vuetify-nuxt-client-hints-reloaded";
2
+ export function hasReloadGuardCookie(cookie) {
3
+ const prefix = `${RELOAD_GUARD_COOKIE}=`;
4
+ return cookie.split(";").some((c) => c.trim().startsWith(prefix));
5
+ }
6
+ export function buildReloadGuardCookie(path) {
7
+ return `${RELOAD_GUARD_COOKIE}=1; Path=${path}; SameSite=Lax`;
8
+ }
9
+ export function shouldReloadOnFirstRequest(firstRequest, reloadOnFirstRequest, alreadyReloaded) {
10
+ return firstRequest && reloadOnFirstRequest && !alreadyReloaded;
11
+ }
@@ -2,6 +2,7 @@ import { defineNuxtPlugin, useNuxtApp, useState } from "#imports";
2
2
  import { ssrClientHintsConfiguration } from "virtual:vuetify-ssr-client-hints-configuration";
3
3
  import { reactive, ref, watch } from "vue";
4
4
  import { VuetifyHTTPClientHints } from "./client-hints.js";
5
+ import { buildReloadGuardCookie, hasReloadGuardCookie, shouldReloadOnFirstRequest } from "./first-request-reload-guard.js";
5
6
  const plugin = defineNuxtPlugin({
6
7
  name: "vuetify:client-hints:client:plugin",
7
8
  order: -25,
@@ -22,7 +23,11 @@ const plugin = defineNuxtPlugin({
22
23
  prefersColorScheme,
23
24
  prefersColorSchemeOptions
24
25
  } = ssrClientHintsConfiguration;
25
- if (firstRequest && reloadOnFirstRequest) {
26
+ if (shouldReloadOnFirstRequest(firstRequest, reloadOnFirstRequest, hasReloadGuardCookie(document.cookie))) {
27
+ const markAndReload = () => {
28
+ document.cookie = buildReloadGuardCookie(prefersColorSchemeOptions?.baseUrl ?? "/");
29
+ window.location.reload();
30
+ };
26
31
  if (prefersColorScheme) {
27
32
  const themeCookie = state.value.colorSchemeCookie;
28
33
  if (prefersColorSchemeOptions && themeCookie) {
@@ -32,19 +37,19 @@ const plugin = defineNuxtPlugin({
32
37
  const cookieEntry = `${parseCookieName}${state.value.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};`;
33
38
  const newThemeName = prefersDark ? prefersColorSchemeOptions.darkThemeName : prefersColorSchemeOptions.lightThemeName;
34
39
  document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`);
35
- window.location.reload();
40
+ markAndReload();
36
41
  } else if (prefersColorSchemeAvailable) {
37
- window.location.reload();
42
+ markAndReload();
38
43
  }
39
44
  }
40
45
  if (prefersReducedMotion && prefersReducedMotionAvailable) {
41
- window.location.reload();
46
+ markAndReload();
42
47
  }
43
48
  if (viewportSize && viewportHeightAvailable) {
44
- window.location.reload();
49
+ markAndReload();
45
50
  }
46
51
  if (viewportSize && viewportWidthAvailable) {
47
- window.location.reload();
52
+ markAndReload();
48
53
  }
49
54
  }
50
55
  nuxtApp.hook("vuetify:before-create", ({ vuetifyOptions }) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vuetify-nuxt-module",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.8",
4
+ "version": "1.0.0-rc.1",
5
5
  "description": "Zero-Config Nuxt Module for Vuetify",
6
6
  "author": "userquin <userquin@gmail.com>",
7
7
  "license": "MIT",
@@ -51,50 +51,52 @@
51
51
  "*.mjs"
52
52
  ],
53
53
  "dependencies": {
54
- "@nuxt/kit": "^4.3.1",
54
+ "@nuxt/kit": "^4.4.8",
55
+ "@vuetify/loader-shared": "^2.1.2",
55
56
  "@vuetify/unplugin-styles": "^1.0.0-beta.11",
56
- "defu": "^6.1.4",
57
- "destr": "^2.0.5",
58
- "local-pkg": "^1.1.2",
57
+ "defu": "^6.1.7",
58
+ "local-pkg": "^1.2.1",
59
59
  "pathe": "^2.0.3",
60
60
  "perfect-debounce": "^2.1.0",
61
- "semver": "^7.7.4",
62
- "ufo": "^1.6.3",
63
- "unconfig": "^7.5.0",
64
- "vite-plugin-vuetify": "^2.1.3",
65
- "vuetify": "^4.0.1"
61
+ "semver": "^7.8.4",
62
+ "ufo": "^1.6.4",
63
+ "unconfig": "^7.5.0"
64
+ },
65
+ "peerDependencies": {
66
+ "vuetify": "^3.4.0 || ^4.0.0"
66
67
  },
67
68
  "devDependencies": {
68
- "@antfu/eslint-config": "^7.6.1",
69
- "@antfu/ni": "^28.2.0",
69
+ "@antfu/eslint-config": "^7.7.3",
70
+ "@antfu/ni": "^28.3.0",
70
71
  "@date-io/luxon": "^3.2.0",
71
72
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
72
73
  "@fortawesome/free-solid-svg-icons": "^7.2.0",
73
- "@fortawesome/vue-fontawesome": "^3.1.3",
74
- "@iconify-json/carbon": "^1.2.19",
74
+ "@fortawesome/vue-fontawesome": "^3.2.0",
75
+ "@iconify-json/carbon": "^1.2.23",
75
76
  "@iconify-json/mdi": "^1.2.3",
76
77
  "@mdi/js": "^7.4.47",
77
78
  "@nuxt/devtools": "latest",
78
79
  "@nuxt/module-builder": "^1.0.2",
79
- "@nuxt/schema": "^4.3.1",
80
- "@nuxt/test-utils": "^4.0.0",
81
- "@nuxtjs/i18n": "^10.2.3",
80
+ "@nuxt/schema": "^4.4.8",
81
+ "@nuxt/test-utils": "^4.0.3",
82
+ "@nuxtjs/i18n": "^10.4.0",
82
83
  "@parcel/watcher": "^2.5.6",
83
- "@types/node": "^25.3.3",
84
+ "@types/node": "^25.9.3",
84
85
  "@types/semver": "^7.7.1",
85
- "@unocss/nuxt": "^66.6.5",
86
- "bumpp": "^10.4.1",
87
- "eslint": "^10.0.2",
86
+ "@unocss/nuxt": "^66.7.2",
87
+ "bumpp": "^11.1.0",
88
+ "eslint": "^10.5.0",
88
89
  "luxon": "^3.7.2",
89
- "nuxt": "^4.3.1",
90
- "playwright-core": "^1.58.0",
91
- "publint": "^0.3.18",
90
+ "nuxt": "^4.4.8",
91
+ "playwright-core": "^1.61.0",
92
+ "publint": "^0.3.21",
92
93
  "rimraf": "^6.1.3",
93
- "sass": "^1.97.3",
94
+ "sass": "^1.101.0",
94
95
  "typescript": "^5.9.3",
95
- "vite": "7.3.1",
96
- "vitest": "^4.0.18",
97
- "vue-tsc": "^3.2.5"
96
+ "vite": "7.3.5",
97
+ "vitest": "^4.1.9",
98
+ "vue-tsc": "^3.3.5",
99
+ "vuetify": "^4.1.2"
98
100
  },
99
101
  "build": {
100
102
  "externals": [
@@ -103,7 +105,6 @@
103
105
  "node:child_process",
104
106
  "node:fs",
105
107
  "consola",
106
- "destr",
107
108
  "esbuild",
108
109
  "local-pkg",
109
110
  "pathe",
@@ -114,7 +115,6 @@
114
115
  "ufo",
115
116
  "unconfig",
116
117
  "vite",
117
- "vite-plugin-vuetify",
118
118
  "vuetify"
119
119
  ]
120
120
  },