vuetify-nuxt-module 1.0.0-beta.1 → 1.0.0-beta.10

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
@@ -51,8 +51,11 @@
51
51
 
52
52
  > Requires Vite, will not work with Webpack
53
53
 
54
+ `vuetify` is a peer dependency (Vuetify 3 or 4) — install it alongside the module:
55
+
54
56
  ```bash
55
- npx nuxi@latest module add vuetify-nuxt-module
57
+ npm install -D vuetify
58
+ npx nuxt module add vuetify-nuxt-module
56
59
  ```
57
60
 
58
61
  [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/userquin/vuetify-nuxt-module)
@@ -36,6 +36,9 @@ declare module 'virtual:vuetify-ssr-client-hints-configuration' {
36
36
  defaultTheme: string
37
37
  themeNames: string[]
38
38
  cookieName: string
39
+ cookieDomain?: string
40
+ cookieSecure?: boolean
41
+ cookieSameSite: 'lax' | 'strict' | 'none'
39
42
  darkThemeName: string
40
43
  lightThemeName: string
41
44
  useBrowserThemeOnly: boolean
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
  *
@@ -251,6 +256,18 @@ interface MOptions {
251
256
  * Path to the custom Vuetify SASS configuration file.
252
257
  */
253
258
  configFile: string;
259
+ /**
260
+ * Caching options forwarded to `@vuetify/unplugin-styles`.
261
+ *
262
+ * @default true
263
+ */
264
+ cache?: boolean | {
265
+ path?: string;
266
+ sassOptions?: Record<string, unknown>;
267
+ };
268
+ /**
269
+ * @deprecated Use `styles.cache` instead.
270
+ */
254
271
  experimental?: {
255
272
  cache?: boolean;
256
273
  };
@@ -350,9 +367,43 @@ interface MOptions {
350
367
  /**
351
368
  * The name for the cookie.
352
369
  *
370
+ * @deprecated Use `cookie.name` instead.
353
371
  * @default 'color-scheme'
354
372
  */
355
373
  cookieName?: string;
374
+ /**
375
+ * Cookie attributes for the color scheme cookie.
376
+ */
377
+ cookie?: {
378
+ /**
379
+ * The name for the cookie.
380
+ *
381
+ * @default 'color-scheme'
382
+ */
383
+ name?: string;
384
+ /**
385
+ * The domain for the color scheme cookie.
386
+ *
387
+ * Useful to share the cookie across subdomains, e.g. `.example.com`.
388
+ *
389
+ * @default undefined
390
+ */
391
+ domain?: string;
392
+ /**
393
+ * Mark the cookie as `Secure`.
394
+ *
395
+ * Forced to `true` when `sameSite` is `'none'`.
396
+ *
397
+ * @default undefined
398
+ */
399
+ secure?: boolean;
400
+ /**
401
+ * The `SameSite` attribute for the cookie.
402
+ *
403
+ * @default 'lax'
404
+ */
405
+ sameSite?: 'lax' | 'strict' | 'none';
406
+ };
356
407
  /**
357
408
  * The name for the dark theme.
358
409
  *
@@ -447,6 +498,9 @@ interface SSRClientHintsConfiguration {
447
498
  defaultTheme: string;
448
499
  themeNames: string[];
449
500
  cookieName: string;
501
+ cookieDomain?: string;
502
+ cookieSecure?: boolean;
503
+ cookieSameSite: 'lax' | 'strict' | 'none';
450
504
  darkThemeName: string;
451
505
  lightThemeName: string;
452
506
  };
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.1",
7
+ "version": "1.0.0-beta.10",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,24 +1,20 @@
1
- import { addPluginTemplate, addTemplate, resolvePath, extendWebpackConfig, isNuxtMajorVersion, addImports, addPlugin, addVitePlugin, useLogger, defineNuxtModule, getNuxtVersion, hasNuxtModule, createResolver } from '@nuxt/kit';
2
- import { isPackageExists, getPackageInfo } from 'local-pkg';
1
+ import { existsSync, statSync, readFileSync } from 'node:fs';
2
+ import { pathToFileURL, fileURLToPath } from 'node:url';
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';
3
5
  import semver from 'semver';
4
6
  import { createFilter, version as version$1 } from 'vite';
5
- import fs, { existsSync, statSync, readFileSync, readdirSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
6
- import { isAbsolute, resolve, relative } from 'pathe';
7
7
  import defu from 'defu';
8
- import { transformAssetUrls } from 'vite-plugin-vuetify';
9
- import { pathToFileURL } from 'node:url';
10
- import { generateImports, resolveVuetifyBase, isObject, normalizePath } from '@vuetify/loader-shared';
11
- import destr from 'destr';
8
+ import { transformAssetUrls, generateImports } from '@vuetify/loader-shared';
9
+ import Styles from '@vuetify/unplugin-styles/vite';
10
+ import { isPackageExists } from 'local-pkg';
12
11
  import { parseQuery, parseURL } from 'ufo';
13
- import fsp, { readFile } from 'node:fs/promises';
14
- import path from 'upath';
15
- import { resolve as resolve$1, dirname, join, relative as relative$1 } from 'node:path';
12
+ import { readFile } from 'node:fs/promises';
16
13
  import { debounce } from 'perfect-debounce';
17
14
  import process from 'node:process';
18
15
  import { createConfigLoader } from 'unconfig';
19
- import { createHash } from 'node:crypto';
20
16
 
21
- const version = "1.0.0-beta.1";
17
+ const version = "1.0.0-beta.10";
22
18
 
23
19
  const VIRTUAL_VUETIFY_CONFIGURATION = "virtual:vuetify-configuration";
24
20
  const RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION = `\0${VIRTUAL_VUETIFY_CONFIGURATION}`;
@@ -128,6 +124,18 @@ function normalizeTransformAssetUrlsAttrs(attrs) {
128
124
  return [...result];
129
125
  }
130
126
 
127
+ function resolveVuetifyConfigFile(configFile, nuxt) {
128
+ if (typeof configFile === "string" && !isAbsolute(configFile)) {
129
+ for (const layer of nuxt.options._layers) {
130
+ const resolved = resolve(layer.config.srcDir, configFile);
131
+ if (existsSync(resolved)) {
132
+ return resolved;
133
+ }
134
+ }
135
+ }
136
+ return configFile;
137
+ }
138
+
131
139
  function addVuetifyNuxtPlugins(nuxt, ctx) {
132
140
  addVuetifyNuxtPlugin(nuxt, ctx, "client");
133
141
  addVuetifyNuxtPlugin(nuxt, ctx, "server");
@@ -198,17 +206,6 @@ export default defineNuxtPlugin({
198
206
  function getTemplate(source, settings) {
199
207
  return [settings ? `@use '${settings}';` : "", `@use '${source}';`].filter(Boolean).join("\n");
200
208
  }
201
- function resolveVuetifyConfigFile(configFile, nuxt) {
202
- if (typeof configFile === "string" && !isAbsolute(configFile)) {
203
- for (const layer of nuxt.options._layers) {
204
- const resolved = resolve(layer.config.rootDir, configFile);
205
- if (existsSync(resolved)) {
206
- return resolved;
207
- }
208
- }
209
- }
210
- return configFile;
211
- }
212
209
  async function configureNuxt(configKey, nuxt, ctx) {
213
210
  const {
214
211
  styles,
@@ -223,9 +220,14 @@ async function configureNuxt(configKey, nuxt, ctx) {
223
220
  nuxt.options.css ??= [];
224
221
  if (typeof styles === "object" && "configFile" in styles) {
225
222
  const configFile = resolveVuetifyConfigFile(styles.configFile, nuxt);
223
+ ctx.stylesConfigFile = await resolvePath(configFile);
226
224
  const a = addTemplate({
227
- filename: "vuetify.settings.scss",
228
- getContents: async () => getTemplate("vuetify/styles", await resolvePath(configFile))
225
+ // Write to disk: without `write` Nuxt serves the template from its
226
+ // virtual FS, which 404s on Windows/SSR when the browser requests the
227
+ // real file (#363). Nest under `vuetify/` to match unplugin-styles.
228
+ write: true,
229
+ filename: "vuetify/vuetify.settings.scss",
230
+ getContents: async () => getTemplate("vuetify/styles", ctx.stylesConfigFile)
229
231
  });
230
232
  nuxt.options.css.push(a.dst);
231
233
  } else if (ctx.vuetifyGte("4.0.0")) {
@@ -592,6 +594,70 @@ ${useLocales.map((locale) => {
592
594
  };
593
595
  }
594
596
 
597
+ const VUETIFY_TO_DATE_FNS = {
598
+ en: "enUS",
599
+ fa: "faIR",
600
+ no: "nb",
601
+ srCyrl: "sr",
602
+ zhHans: "zhCN",
603
+ zhHant: "zhTW"
604
+ };
605
+ const DATE_FNS_SUPPORTED = /* @__PURE__ */ new Set([
606
+ "af",
607
+ "ar",
608
+ "az",
609
+ "bg",
610
+ "ca",
611
+ "ckb",
612
+ "cs",
613
+ "da",
614
+ "de",
615
+ "el",
616
+ "enUS",
617
+ "es",
618
+ "et",
619
+ "faIR",
620
+ "fi",
621
+ "fr",
622
+ "he",
623
+ "hr",
624
+ "hu",
625
+ "id",
626
+ "it",
627
+ "ja",
628
+ "km",
629
+ "ko",
630
+ "lt",
631
+ "lv",
632
+ "nb",
633
+ "nl",
634
+ "pl",
635
+ "pt",
636
+ "ro",
637
+ "ru",
638
+ "sk",
639
+ "sl",
640
+ "sr",
641
+ "srLatn",
642
+ "sv",
643
+ "th",
644
+ "tr",
645
+ "uk",
646
+ "vi",
647
+ "zhCN",
648
+ "zhTW"
649
+ ]);
650
+ function resolveDateFnsLocaleName(code) {
651
+ if (!code) {
652
+ return { name: "enUS", fallback: true };
653
+ }
654
+ const candidate = VUETIFY_TO_DATE_FNS[code] ?? code;
655
+ if (DATE_FNS_SUPPORTED.has(candidate)) {
656
+ return { name: candidate, fallback: false };
657
+ }
658
+ return { name: "enUS", fallback: true };
659
+ }
660
+
595
661
  function vuetifyDateConfigurationPlugin(ctx) {
596
662
  return {
597
663
  name: "vuetify:date-configuration:nuxt",
@@ -615,34 +681,41 @@ export function dateConfiguration() {
615
681
  `;
616
682
  }
617
683
  const { adapter: _adapter, ...newDateOptions } = ctx.vuetifyOptions.date ?? {};
618
- return `${buildImports()}
684
+ let dateFnsLocale;
685
+ if (ctx.dateAdapter === "date-fns") {
686
+ const resolved = resolveDateFnsLocaleName(ctx.vuetifyOptions.locale?.locale);
687
+ dateFnsLocale = resolved.name;
688
+ if (resolved.fallback) {
689
+ 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.`);
690
+ }
691
+ }
692
+ return `${buildImports(dateFnsLocale)}
619
693
  export const enabled = true
620
694
  export const isDev = ${ctx.isDev}
621
695
  export const i18n = ${ctx.i18n}
622
696
  export const adapter = '${ctx.dateAdapter}'
623
697
  export function dateConfiguration() {
624
698
  const options = JSON.parse('${JSON.stringify(newDateOptions)}')
625
- ${buildAdapter()}
699
+ ${buildAdapter(dateFnsLocale)}
626
700
  return options
627
701
  }
628
702
  `;
629
703
  }
630
704
  }
631
705
  };
632
- function buildAdapter() {
706
+ function buildAdapter(dateFnsLocale) {
633
707
  if (ctx.dateAdapter === "custom" || ctx.dateAdapter === "vuetify" && ctx.vuetifyGte("3.4.0")) {
634
708
  return "";
635
709
  }
636
710
  if (ctx.dateAdapter === "vuetify") {
637
711
  return "options.adapter = VuetifyDateAdapter";
638
712
  }
639
- const locale = ctx.vuetifyOptions.locale?.locale ?? "en";
640
713
  if (ctx.dateAdapter === "date-fns") {
641
- return `options.adapter = new Adapter({ locale: ${locale} })`;
714
+ return `options.adapter = new Adapter({ locale: ${dateFnsLocale} })`;
642
715
  }
643
716
  return "options.adapter = Adapter";
644
717
  }
645
- function buildImports() {
718
+ function buildImports(dateFnsLocale) {
646
719
  if (ctx.dateAdapter === "custom" || ctx.dateAdapter === "vuetify" && ctx.vuetifyGte("3.4.0")) {
647
720
  return "";
648
721
  }
@@ -651,7 +724,7 @@ export function dateConfiguration() {
651
724
  }
652
725
  const imports = [`import Adapter from '@date-io/${ctx.dateAdapter}'`];
653
726
  if (ctx.dateAdapter === "date-fns") {
654
- imports.push(`import { ${ctx.vuetifyOptions.locale?.locale ?? "en"} } from 'date-fns/locale'`);
727
+ imports.push(`import { ${dateFnsLocale} } from 'date-fns/locale'`);
655
728
  }
656
729
  return imports.join("\n");
657
730
  }
@@ -837,10 +910,24 @@ function parseId2(id) {
837
910
  id = id.replace(/^(virtual:nuxt:|virtual:)/, "");
838
911
  return parseURL(decodeURIComponent(isAbsolute(id) ? pathToFileURL(id).href : id));
839
912
  }
913
+ function reviver(key, value) {
914
+ if (key === "__proto__" || key === "constructor") {
915
+ return void 0;
916
+ }
917
+ return value;
918
+ }
919
+ function parseProps(value) {
920
+ try {
921
+ const parsed = JSON.parse(value, reviver);
922
+ return parsed && typeof parsed === "object" ? parsed : void 0;
923
+ } catch {
924
+ return void 0;
925
+ }
926
+ }
840
927
  function parseId(id) {
841
928
  const { search, pathname } = parseId2(id);
842
929
  const query = parseQuery(search);
843
- const urlProps = query.props ? destr(query.props) : void 0;
930
+ const urlProps = query.props ? parseProps(query.props) : void 0;
844
931
  return {
845
932
  query: urlProps,
846
933
  path: pathname ?? id
@@ -901,122 +988,7 @@ function vuetifySSRClientHintsPlugin(ctx) {
901
988
  };
902
989
  }
903
990
 
904
- function vuetifyStylesPlugin(ctx) {
905
- let configFile;
906
- const options = { styles: ctx.moduleOptions.styles };
907
- const vuetifyBase = resolveVuetifyBase();
908
- const noneFiles = /* @__PURE__ */ new Set();
909
- let isNone = false;
910
- let sassVariables = false;
911
- let fileImport = false;
912
- const PREFIX = "vuetify-styles/";
913
- const SSR_PREFIX = `/@${PREFIX}`;
914
- const resolveCss = resolveCssFactory();
915
- const toPath = (file) => fileImport ? pathToFileURL(file).href : normalizePath(file);
916
- return {
917
- name: "vuetify:styles:nuxt",
918
- enforce: "pre",
919
- async configResolved(config) {
920
- if (config.plugins.some((plugin) => plugin.name === "vuetify:styles")) {
921
- throw new Error("Remove vite-plugin-vuetify from your Nuxt config file, this module registers a modified version.");
922
- }
923
- if (isObject(options.styles) && "configFile" in options.styles) {
924
- sassVariables = true;
925
- fileImport = semver.gt(ctx.viteVersion, "5.4.2");
926
- configFile = await resolvePath(options.styles.configFile);
927
- } else {
928
- isNone = options.styles === "none";
929
- }
930
- },
931
- async resolveId(source, importer, { custom, ssr }) {
932
- if (!sassVariables) {
933
- return;
934
- }
935
- if (source.startsWith(PREFIX) || source.startsWith(SSR_PREFIX)) {
936
- if (/\.s[ca]ss$/.test(source)) {
937
- return source;
938
- }
939
- const idx = source.indexOf("?");
940
- return idx === -1 ? source : source.slice(0, idx);
941
- }
942
- if (importer && source.endsWith(".css") && isSubdir(vuetifyBase, path.isAbsolute(source) ? source : importer)) {
943
- let resolutionId;
944
- if (source.startsWith(".")) {
945
- resolutionId = path.resolve(path.dirname(importer), source);
946
- } else if (path.isAbsolute(source)) {
947
- resolutionId = source;
948
- } else {
949
- const resolution = await this.resolve(source, importer, { skipSelf: true, custom });
950
- if (resolution) {
951
- resolutionId = resolution.id;
952
- }
953
- }
954
- if (!resolutionId) {
955
- return;
956
- }
957
- const target = await resolveCss(resolutionId);
958
- if (isNone) {
959
- noneFiles.add(target);
960
- return target;
961
- }
962
- if (ctx.stylesCachePath) {
963
- const relative = path.relative(vuetifyBase, target);
964
- const cacheFile = path.resolve(ctx.stylesCachePath, relative.replace(/\.s[ac]ss$/, ".css"));
965
- if (fs.existsSync(cacheFile)) {
966
- return cacheFile;
967
- }
968
- }
969
- return `${ssr ? SSR_PREFIX : PREFIX}${path.relative(vuetifyBase, target)}`;
970
- }
971
- return void 0;
972
- },
973
- load(id) {
974
- if (sassVariables) {
975
- const target = id.startsWith(PREFIX) ? path.resolve(vuetifyBase, id.slice(PREFIX.length)) : id.startsWith(SSR_PREFIX) ? path.resolve(vuetifyBase, id.slice(SSR_PREFIX.length)) : void 0;
976
- if (target) {
977
- const suffix = /\.scss/.test(target) ? ";\n" : "\n";
978
- return {
979
- code: `@use "${toPath(configFile)}"${suffix}@use "${toPath(target)}"${suffix}`,
980
- map: {
981
- mappings: ""
982
- }
983
- };
984
- }
985
- }
986
- return isNone && noneFiles.has(id) ? "" : void 0;
987
- }
988
- };
989
- }
990
- function resolveCssFactory() {
991
- const mappings = /* @__PURE__ */ new Map();
992
- return async (source) => {
993
- let mapping = mappings.get(source);
994
- if (!mapping) {
995
- try {
996
- mapping = source.replace(/\.css$/, ".sass");
997
- await fsp.access(mapping, fs.constants.R_OK);
998
- } catch (error) {
999
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
1000
- throw error;
1001
- }
1002
- try {
1003
- mapping = source.replace(/\.css$/, ".scss");
1004
- await fsp.access(mapping, fs.constants.R_OK);
1005
- } catch {
1006
- mapping = source;
1007
- }
1008
- }
1009
- mappings.set(source, mapping);
1010
- }
1011
- return mapping;
1012
- };
1013
- }
1014
- function isSubdir(root, test) {
1015
- const relative$1 = relative(root, test);
1016
- return relative$1 && !relative$1.startsWith("..") && !isAbsolute(relative$1);
1017
- }
1018
-
1019
- function detectDate() {
991
+ function detectDate(paths) {
1020
992
  const result = [];
1021
993
  for (const adapter of [
1022
994
  "date-fns",
@@ -1028,12 +1000,15 @@ function detectDate() {
1028
1000
  "jalaali",
1029
1001
  "hijri"
1030
1002
  ]) {
1031
- if (isPackageExists(`@date-io/${adapter}`)) {
1003
+ if (isPackageExists(`@date-io/${adapter}`, { paths })) {
1032
1004
  result.push(adapter);
1033
1005
  }
1034
1006
  }
1035
1007
  return result;
1036
1008
  }
1009
+ function multipleDateAdaptersError(adapters) {
1010
+ return `Multiple date adapters found: ${adapters.map((adapter) => `@date-io/${adapter}`).join(", ")}, please specify the adapter to use in the "vuetifyOptions.date.adapter" option.`;
1011
+ }
1037
1012
  function cleanupBlueprint(vuetifyOptions) {
1038
1013
  const blueprint = vuetifyOptions.blueprint;
1039
1014
  if (blueprint) {
@@ -1056,12 +1031,10 @@ function checkVuetifyPlugins(config) {
1056
1031
  throw new Error("Remove vite-plugin-vuetify plugin from Vite Plugins entry in Nuxt config file!");
1057
1032
  }
1058
1033
  }
1059
- function resolveVuetifyComponents(resolver) {
1060
- const vuetifyBase = resolveVuetifyBase();
1034
+ function resolveVuetifyComponents(resolver, vuetifyBase) {
1061
1035
  const componentsPromise = importMapResolver();
1062
1036
  const labComponentsPromise = importMapLabResolver();
1063
1037
  return {
1064
- vuetifyBase,
1065
1038
  componentsPromise,
1066
1039
  labComponentsPromise
1067
1040
  };
@@ -1073,6 +1046,9 @@ function resolveVuetifyComponents(resolver) {
1073
1046
  }
1074
1047
  }
1075
1048
 
1049
+ function resolveStylesCache(stylesOption) {
1050
+ return stylesOption.cache ?? stylesOption.experimental?.cache;
1051
+ }
1076
1052
  function configureVite(configKey, nuxt, ctx) {
1077
1053
  nuxt.hook("vite:extend", ({ config }) => checkVuetifyPlugins(config));
1078
1054
  nuxt.hook("vite:extendConfig", (viteInlineConfig) => {
@@ -1081,7 +1057,7 @@ function configureVite(configKey, nuxt, ctx) {
1081
1057
  if (!ctx.moduleOptions.disableModernSassCompiler) {
1082
1058
  const enableModernSassCompiler = semver.gte(ctx.viteVersion, "5.4.0") && semver.lt(ctx.viteVersion, "7.0.0-0");
1083
1059
  if (enableModernSassCompiler) {
1084
- const sassEmbedded = isPackageExists("sass-embedded");
1060
+ const sassEmbedded = isPackageExists("sass-embedded", { paths: ctx.resolvePaths });
1085
1061
  if (sassEmbedded) {
1086
1062
  viteInlineConfig.css ??= {};
1087
1063
  viteInlineConfig.css.preprocessorOptions ??= {};
@@ -1125,8 +1101,18 @@ function configureVite(configKey, nuxt, ctx) {
1125
1101
  autoImport.ignore = Array.isArray(ignoreDirectives) ? ignoreDirectives : [ignoreDirectives];
1126
1102
  }
1127
1103
  viteInlineConfig.plugins.push(vuetifyImportPlugin({ autoImport }));
1128
- if (ctx.moduleOptions.styles !== false && ctx.moduleOptions.styles !== "none") {
1129
- viteInlineConfig.plugins.push(vuetifyStylesPlugin(ctx));
1104
+ const stylesOption = ctx.moduleOptions.styles;
1105
+ if (stylesOption === "none") {
1106
+ viteInlineConfig.plugins.push(Styles({ styles: "none" }));
1107
+ } else if (typeof stylesOption === "object" && stylesOption !== null && "configFile" in stylesOption) {
1108
+ if (!ctx.stylesConfigFile) {
1109
+ throw new Error("vuetify-nuxt-module: styles.configFile could not be resolved");
1110
+ }
1111
+ const cache = resolveStylesCache(stylesOption);
1112
+ viteInlineConfig.plugins.push(Styles({
1113
+ settings: ctx.stylesConfigFile,
1114
+ ...cache === void 0 ? {} : { cache }
1115
+ }));
1130
1116
  }
1131
1117
  viteInlineConfig.plugins.push(vuetifyConfigurationPlugin(ctx), vuetifyIconsPlugin(ctx), vuetifyDateConfigurationPlugin(ctx));
1132
1118
  if (ctx.ssrClientHints.enabled) {
@@ -1165,7 +1151,8 @@ const disabledResolvedIcons = Object.freeze({
1165
1151
  local: [],
1166
1152
  svg: {}
1167
1153
  });
1168
- function prepareIcons(unocssPresent, logger, vuetifyOptions) {
1154
+ function prepareIcons(unocssPresent, logger, vuetifyOptions, paths) {
1155
+ const iconPackageExists = (name) => isPackageExists(name, { paths });
1169
1156
  if (vuetifyOptions.icons === false) {
1170
1157
  return disabledResolvedIcons;
1171
1158
  }
@@ -1209,7 +1196,7 @@ function prepareIcons(unocssPresent, logger, vuetifyOptions) {
1209
1196
  }
1210
1197
  resolvedIcons.imports.push(`import {${name === defaultSet ? "aliases," : ""}${name}} from 'vuetify/iconsets/${name}'`);
1211
1198
  resolvedIcons.sets.push(name);
1212
- if (isPackageExists(iconsPackageNames[name].name)) {
1199
+ if (iconPackageExists(iconsPackageNames[name].name)) {
1213
1200
  resolvedIcons.local.push(iconsPackageNames[name].css);
1214
1201
  } else {
1215
1202
  resolvedIcons.cdn.push([name, cdn ?? iconsCDN[name]]);
@@ -1228,18 +1215,18 @@ function prepareIcons(unocssPresent, logger, vuetifyOptions) {
1228
1215
  if (!faSvg) {
1229
1216
  faSvg = {};
1230
1217
  }
1231
- let faSvgExists = isPackageExists("@fortawesome/fontawesome-svg-core");
1218
+ let faSvgExists = iconPackageExists("@fortawesome/fontawesome-svg-core");
1232
1219
  if (!faSvgExists) {
1233
1220
  logger.warn("Missing @fortawesome/fontawesome-svg-core dependency, install it!");
1234
1221
  }
1235
- faSvgExists = isPackageExists("@fortawesome/vue-fontawesome");
1222
+ faSvgExists = iconPackageExists("@fortawesome/vue-fontawesome");
1236
1223
  if (faSvgExists) {
1237
1224
  if (!faSvg.libraries?.length) {
1238
1225
  faSvg.libraries = [[false, "fas", "@fortawesome/free-solid-svg-icons"]];
1239
1226
  }
1240
1227
  for (const p in faSvg.libraries) {
1241
1228
  const [_defaultExport, _name, library] = faSvg.libraries[p];
1242
- if (!isPackageExists(library)) {
1229
+ if (!iconPackageExists(library)) {
1243
1230
  faSvgExists = false;
1244
1231
  logger.warn(`Missing library ${library} dependency, install it!`);
1245
1232
  }
@@ -1266,7 +1253,7 @@ function prepareIcons(unocssPresent, logger, vuetifyOptions) {
1266
1253
  if (!mdiSvg) {
1267
1254
  mdiSvg = {};
1268
1255
  }
1269
- const mdiSvgExists = isPackageExists("@mdi/js");
1256
+ const mdiSvgExists = iconPackageExists("@mdi/js");
1270
1257
  if (mdiSvgExists) {
1271
1258
  resolvedIcons.svg.mdi = true;
1272
1259
  resolvedIcons.aliasesImportPresent ||= defaultSet === "mdi-svg";
@@ -1314,7 +1301,7 @@ async function loadVuetifyConfiguration(cwd = process.cwd(), configOrPath = cwd,
1314
1301
  inlineConfig = configOrPath;
1315
1302
  configOrPath = process.cwd();
1316
1303
  }
1317
- const resolved = resolve$1(cwd, configOrPath);
1304
+ const resolved = resolve(cwd, configOrPath);
1318
1305
  let isFile = false;
1319
1306
  if (existsSync(resolved) && statSync(resolved).isFile()) {
1320
1307
  isFile = true;
@@ -1362,6 +1349,29 @@ async function loadVuetifyConfiguration(cwd = process.cwd(), configOrPath = cwd,
1362
1349
  return result;
1363
1350
  }
1364
1351
 
1352
+ const MODULE_DEFAULTS = {
1353
+ moduleOptions: {
1354
+ importComposables: true,
1355
+ includeTransformAssetsUrls: true,
1356
+ styles: true,
1357
+ rulesConfiguration: {
1358
+ fromLabs: true
1359
+ }
1360
+ },
1361
+ vuetifyOptions: {
1362
+ labComponents: false,
1363
+ directives: false
1364
+ }
1365
+ };
1366
+ function finalizeConfiguration(moduleOptions) {
1367
+ if (moduleOptions.length > 1) {
1368
+ const [app, ...rest] = moduleOptions;
1369
+ const configuration = defu(app, ...rest, MODULE_DEFAULTS);
1370
+ dedupeIcons(configuration, moduleOptions.toReversed());
1371
+ return configuration;
1372
+ }
1373
+ return defu(moduleOptions[0] ?? {}, MODULE_DEFAULTS);
1374
+ }
1365
1375
  async function mergeVuetifyModules(options, nuxt) {
1366
1376
  const moduleOptions = [];
1367
1377
  const vuetifyConfigurationFilesToWatch = /* @__PURE__ */ new Set();
@@ -1403,23 +1413,10 @@ async function mergeVuetifyModules(options, nuxt) {
1403
1413
  moduleOptions: options.moduleOptions,
1404
1414
  vuetifyOptions: resolvedOptions.config
1405
1415
  });
1406
- if (moduleOptions.length > 1) {
1407
- const [app, ...rest] = moduleOptions;
1408
- const configuration = defu(app, ...rest);
1409
- dedupeIcons(configuration, moduleOptions.toReversed());
1410
- return {
1411
- configuration,
1412
- vuetifyConfigurationFilesToWatch
1413
- };
1414
- } else {
1415
- return {
1416
- configuration: {
1417
- moduleOptions: options.moduleOptions,
1418
- vuetifyOptions: resolvedOptions.config
1419
- },
1420
- vuetifyConfigurationFilesToWatch
1421
- };
1422
- }
1416
+ return {
1417
+ configuration: finalizeConfiguration(moduleOptions),
1418
+ vuetifyConfigurationFilesToWatch
1419
+ };
1423
1420
  }
1424
1421
  function dedupeIcons(configuration, moduleOptions) {
1425
1422
  const vuetifyOptions = configuration.vuetifyOptions;
@@ -1452,6 +1449,28 @@ const disabledClientHints = Object.freeze({
1452
1449
  prefersColorScheme: false,
1453
1450
  prefersReducedMotion: false
1454
1451
  });
1452
+ function resolveColorSchemeCookie(options, logger) {
1453
+ if (options.cookieName !== void 0) {
1454
+ logger.warn("[vuetify-nuxt-module] `prefersColorSchemeOptions.cookieName` is deprecated, use `prefersColorSchemeOptions.cookie.name` instead.");
1455
+ }
1456
+ const cookieSameSite = options.cookie?.sameSite ?? "lax";
1457
+ return {
1458
+ cookieName: options.cookie?.name ?? options.cookieName ?? "color-scheme",
1459
+ cookieDomain: options.cookie?.domain,
1460
+ cookieSecure: cookieSameSite === "none" ? true : options.cookie?.secure,
1461
+ cookieSameSite
1462
+ };
1463
+ }
1464
+ function resolveDefaultTheme(defaultTheme, themes, lightThemeName, logger) {
1465
+ if (defaultTheme === "system") {
1466
+ 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.`);
1467
+ return lightThemeName;
1468
+ }
1469
+ if (!themes[defaultTheme]) {
1470
+ throw new Error(`Missing default theme ${defaultTheme} in the Vuetify themes!`);
1471
+ }
1472
+ return defaultTheme;
1473
+ }
1455
1474
  function prepareSSRClientHints(baseUrl, ctx) {
1456
1475
  if (!ctx.isSSR || ctx.isNuxtGenerate) {
1457
1476
  return disabledClientHints;
@@ -1478,9 +1497,6 @@ function prepareSSRClientHints(baseUrl, ctx) {
1478
1497
  if (!defaultTheme) {
1479
1498
  throw new Error("Vuetify default theme is missing in theme!");
1480
1499
  }
1481
- if (!themes[defaultTheme]) {
1482
- throw new Error(`Missing default theme ${defaultTheme} in the Vuetify themes!`);
1483
- }
1484
1500
  const darkThemeName = ssrClientHintsConfiguration.prefersColorSchemeOptions?.darkThemeName ?? "dark";
1485
1501
  if (!themes[darkThemeName]) {
1486
1502
  throw new Error(`Missing theme ${darkThemeName} in the Vuetify themes!`);
@@ -1492,137 +1508,21 @@ function prepareSSRClientHints(baseUrl, ctx) {
1492
1508
  if (darkThemeName === lightThemeName) {
1493
1509
  throw new Error("Vuetify dark theme and light theme are the same, change darkThemeName or lightThemeName!");
1494
1510
  }
1511
+ const pcsOptions = ssrClientHintsConfiguration.prefersColorSchemeOptions;
1512
+ const effectiveDefaultTheme = resolveDefaultTheme(defaultTheme, themes, lightThemeName, ctx.logger);
1495
1513
  clientHints.prefersColorSchemeOptions = {
1496
1514
  baseUrl,
1497
- defaultTheme,
1515
+ defaultTheme: effectiveDefaultTheme,
1498
1516
  themeNames: Array.from(Object.keys(themes)),
1499
- cookieName: ssrClientHintsConfiguration.prefersColorSchemeOptions?.cookieName ?? "color-scheme",
1517
+ ...resolveColorSchemeCookie(pcsOptions, ctx.logger),
1500
1518
  darkThemeName,
1501
1519
  lightThemeName,
1502
- useBrowserThemeOnly: ssrClientHintsConfiguration.prefersColorSchemeOptions?.useBrowserThemeOnly ?? false
1520
+ useBrowserThemeOnly: pcsOptions?.useBrowserThemeOnly ?? false
1503
1521
  };
1504
1522
  }
1505
1523
  return clientHints;
1506
1524
  }
1507
1525
 
1508
- async function prepareVuetifyStyles(nuxt, ctx) {
1509
- const stylesConfig = ctx.moduleOptions.styles;
1510
- if (!isObject(stylesConfig) || !("configFile" in stylesConfig)) {
1511
- return;
1512
- }
1513
- if (stylesConfig.experimental?.cache === false) {
1514
- return;
1515
- }
1516
- const vuetifyBase = resolveVuetifyBase();
1517
- let configFile;
1518
- let configContent = "";
1519
- if (stylesConfig.configFile) {
1520
- configFile = await resolvePath(stylesConfig.configFile);
1521
- if (existsSync(configFile)) {
1522
- configContent = readFileSync(configFile, "utf8");
1523
- if (!ctx.vuetifyFilesToWatch.includes(configFile)) {
1524
- ctx.vuetifyFilesToWatch.push(configFile);
1525
- }
1526
- }
1527
- }
1528
- if (!configFile) {
1529
- return;
1530
- }
1531
- const hash = createHash("sha256").update(ctx.vuetifyVersion).update(ctx.viteVersion).update(configContent).update(configFile).digest("hex").slice(0, 8);
1532
- const stylesDir = resolve$1(nuxt.options.rootDir, "node_modules/.cache/vuetify-nuxt-module/styles");
1533
- const cacheDir = join(stylesDir, hash);
1534
- ctx.stylesCachePath = cacheDir;
1535
- if (existsSync(stylesDir)) {
1536
- const dirents = readdirSync(stylesDir, { withFileTypes: true });
1537
- for (const dirent of dirents) {
1538
- if (dirent.isDirectory() && dirent.name !== hash) {
1539
- rmSync(join(stylesDir, dirent.name), { recursive: true, force: true });
1540
- }
1541
- }
1542
- }
1543
- if (existsSync(cacheDir)) {
1544
- return;
1545
- }
1546
- ctx.logger.info("Compiling Vuetify styles...");
1547
- let sass;
1548
- try {
1549
- sass = await import('sass');
1550
- } catch {
1551
- try {
1552
- sass = await import('sass-embedded');
1553
- } catch {
1554
- ctx.logger.warn('Could not load "sass" or "sass-embedded". Skipping styles pre-compilation.');
1555
- return;
1556
- }
1557
- }
1558
- const files = [];
1559
- findCssFiles(join(vuetifyBase, "lib/components"), files);
1560
- findCssFiles(join(vuetifyBase, "lib/styles"), files);
1561
- for (const file of files) {
1562
- const relativePath = relative$1(vuetifyBase, file);
1563
- const cacheFile = join(cacheDir, relativePath);
1564
- const sassFile = file.replace(/\.css$/, ".sass");
1565
- const scssFile = file.replace(/\.css$/, ".scss");
1566
- let targetFile;
1567
- if (existsSync(sassFile)) {
1568
- targetFile = sassFile;
1569
- } else if (existsSync(scssFile)) {
1570
- targetFile = scssFile;
1571
- }
1572
- if (targetFile) {
1573
- const dir = dirname(cacheFile);
1574
- if (!existsSync(dir)) {
1575
- mkdirSync(dir, { recursive: true });
1576
- }
1577
- const content = `@use "${normalizePath(configFile)}";
1578
- @use "${normalizePath(targetFile)}";
1579
- `;
1580
- try {
1581
- const result = sass.compileString(content, {
1582
- loadPaths: [
1583
- dirname(configFile),
1584
- dirname(targetFile),
1585
- resolve$1(vuetifyBase, ".."),
1586
- resolve$1(vuetifyBase, "../.."),
1587
- // In case of monorepo/hoisting issues, but standard is enough
1588
- vuetifyBase
1589
- ],
1590
- url: new URL(pathToFileURL(cacheFile).href)
1591
- });
1592
- writeFileSync(cacheFile, result.css, "utf8");
1593
- } catch (error) {
1594
- ctx.logger.error(`Failed to compile ${targetFile}:`, error);
1595
- }
1596
- }
1597
- }
1598
- const metadata = {
1599
- hash,
1600
- vuetifyVersion: ctx.vuetifyVersion,
1601
- viteVersion: ctx.viteVersion,
1602
- configFile,
1603
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1604
- };
1605
- writeFileSync(join(cacheDir, "metadata.json"), JSON.stringify(metadata, null, 2), "utf8");
1606
- }
1607
- function findCssFiles(dir, fileList = []) {
1608
- if (!existsSync(dir)) {
1609
- return fileList;
1610
- }
1611
- const files = readdirSync(dir);
1612
- for (const file of files) {
1613
- const filePath = join(dir, file);
1614
- const stat = statSync(filePath);
1615
- if (stat.isDirectory()) {
1616
- findCssFiles(filePath, fileList);
1617
- } else {
1618
- if (file.endsWith(".css")) {
1619
- fileList.push(filePath);
1620
- }
1621
- }
1622
- }
1623
- return fileList;
1624
- }
1625
-
1626
1526
  async function load(options, nuxt, ctx) {
1627
1527
  const {
1628
1528
  configuration,
@@ -1632,7 +1532,7 @@ async function load(options, nuxt, ctx) {
1632
1532
  const {
1633
1533
  componentsPromise,
1634
1534
  labComponentsPromise
1635
- } = resolveVuetifyComponents(ctx.resolver);
1535
+ } = resolveVuetifyComponents(ctx.resolver, ctx.vuetifyBase);
1636
1536
  ctx.componentsPromise = componentsPromise;
1637
1537
  ctx.labComponentsPromise = labComponentsPromise;
1638
1538
  }
@@ -1648,9 +1548,9 @@ async function load(options, nuxt, ctx) {
1648
1548
  const dateOptions = vuetifyOptions.date;
1649
1549
  if (dateOptions) {
1650
1550
  const adapter = dateOptions.adapter;
1651
- const date = detectDate();
1551
+ const date = detectDate(ctx.resolvePaths);
1652
1552
  if (!adapter && date.length > 1) {
1653
- throw new Error(`Multiple date adapters found: ${date.map((d) => `@date-io/${d[0]}`).join(", ")}, please specify the adapter to use in the "vuetifyOptions.date.adapter" option.`);
1553
+ throw new Error(multipleDateAdaptersError(date));
1654
1554
  }
1655
1555
  if (adapter) {
1656
1556
  if (adapter === "vuetify" || adapter === "custom") {
@@ -1677,8 +1577,11 @@ async function load(options, nuxt, ctx) {
1677
1577
  ctx.enableRules = ctx.moduleOptions.enableRules;
1678
1578
  ctx.rulesConfiguration = ctx.moduleOptions.rulesConfiguration;
1679
1579
  ctx.vuetifyFilesToWatch = Array.from(vuetifyConfigurationFilesToWatch);
1680
- ctx.icons = prepareIcons(ctx.unocss, ctx.logger, vuetifyAppOptions);
1580
+ ctx.icons = prepareIcons(ctx.unocss, ctx.logger, vuetifyAppOptions, ctx.resolvePaths);
1681
1581
  ctx.ssrClientHints = prepareSSRClientHints(nuxt.options.app.baseURL ?? "/", ctx);
1582
+ if (ctx.isSSR && !ctx.ssrClientHints.prefersColorScheme && ctx.vuetifyOptions.theme && typeof ctx.vuetifyOptions.theme === "object" && ctx.vuetifyOptions.theme.defaultTheme === "system") {
1583
+ 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.');
1584
+ }
1682
1585
  if (ctx.icons.enabled) {
1683
1586
  if (ctx.icons.local) {
1684
1587
  for (const css of ctx.icons.local) {
@@ -1698,13 +1601,12 @@ async function load(options, nuxt, ctx) {
1698
1601
  }
1699
1602
  }
1700
1603
  }
1701
- await prepareVuetifyStyles(nuxt, ctx);
1702
1604
  }
1703
1605
  function registerWatcher(options, nuxt, ctx) {
1704
1606
  if (nuxt.options.dev) {
1705
1607
  let pageReload;
1706
1608
  nuxt.hooks.hook("builder:watch", (_event, path) => {
1707
- path = relative$1(nuxt.options.srcDir, resolve$1(nuxt.options.srcDir, path));
1609
+ path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path));
1708
1610
  if (!pageReload && ctx.vuetifyFilesToWatch.includes(path)) {
1709
1611
  return nuxt.callHook("restart");
1710
1612
  }
@@ -1750,24 +1652,6 @@ const module$1 = defineNuxtModule({
1750
1652
  },
1751
1653
  version
1752
1654
  },
1753
- /**
1754
- * Default configuration options of the Nuxt module
1755
- */
1756
- defaults: () => ({
1757
- vuetifyOptions: {
1758
- labComponents: false,
1759
- directives: false
1760
- },
1761
- moduleOptions: {
1762
- importComposables: true,
1763
- includeTransformAssetsUrls: true,
1764
- styles: true,
1765
- disableVuetifyStyles: false,
1766
- rulesConfiguration: {
1767
- fromLabs: true
1768
- }
1769
- }
1770
- }),
1771
1655
  /**
1772
1656
  * Sets up the Vuetify Nuxt module.
1773
1657
  *
@@ -1778,8 +1662,23 @@ const module$1 = defineNuxtModule({
1778
1662
  if (isNuxtMajorVersion(2, nuxt)) {
1779
1663
  logger.error(`Cannot support nuxt version: ${getNuxtVersion(nuxt)}`);
1780
1664
  }
1781
- const vuetifyPkg = await getPackageInfo("vuetify");
1782
- const currentVersion = vuetifyPkg?.version;
1665
+ let vuetifyPkgPath = await findPath("vuetify/package.json");
1666
+ if (!vuetifyPkgPath) {
1667
+ const metaResolve = import.meta.resolve;
1668
+ if (metaResolve) {
1669
+ try {
1670
+ vuetifyPkgPath = fileURLToPath(metaResolve("vuetify/package.json"));
1671
+ } catch {
1672
+ }
1673
+ }
1674
+ }
1675
+ if (!vuetifyPkgPath) {
1676
+ throw new Error(
1677
+ '[vuetify-nuxt-module] Could not resolve "vuetify". Please add it as a dependency of your project (e.g. `npm add vuetify`).'
1678
+ );
1679
+ }
1680
+ const vuetifyBase = dirname(vuetifyPkgPath);
1681
+ const currentVersion = JSON.parse(readFileSync(vuetifyPkgPath, "utf8")).version;
1783
1682
  const vuetifyGte = (version2) => !!currentVersion && semver.gte(currentVersion, version2);
1784
1683
  const viteVersion = version$1;
1785
1684
  const ctx = {
@@ -1799,6 +1698,8 @@ const module$1 = defineNuxtModule({
1799
1698
  labComponentsPromise: void 0,
1800
1699
  vuetifyGte,
1801
1700
  vuetifyVersion: currentVersion || "0.0.0",
1701
+ vuetifyBase,
1702
+ resolvePaths: [nuxt.options.rootDir, ...nuxt.options.modulesDir],
1802
1703
  viteVersion
1803
1704
  };
1804
1705
  await load(options, nuxt, ctx);
@@ -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
+ }
@@ -4,6 +4,9 @@ import { useI18n } from "vue-i18n";
4
4
  function inferDecimalSeparator(n) {
5
5
  return n(0.1).includes(",") ? "," : ".";
6
6
  }
7
+ function inferNumericGroupSeparator(n) {
8
+ return n(1e4, { useGrouping: true }).replace(/\p{Nd}/gu, "") || " ";
9
+ }
7
10
  export function createAdapter(vuetifyOptions) {
8
11
  vuetifyOptions.locale = {};
9
12
  const nuxtApp = useNuxtApp();
@@ -32,7 +35,8 @@ export function createAdapter(vuetifyOptions) {
32
35
  t: (key, ...params) => i18n.t(key, params),
33
36
  n: i18n.n,
34
37
  provide: createProvideFunction({ current: currentLocale, fallback, messages }),
35
- decimalSeparator: toRef(() => inferDecimalSeparator(i18n.n))
38
+ decimalSeparator: toRef(() => inferDecimalSeparator(i18n.n)),
39
+ numericGroupSeparator: toRef(() => inferNumericGroupSeparator(i18n.n))
36
40
  };
37
41
  }
38
42
  function createProvideFunction(data) {
@@ -61,6 +65,7 @@ function createProvideFunction(data) {
61
65
  t,
62
66
  n,
63
67
  decimalSeparator: toRef(() => props.decimalSeparator ?? inferDecimalSeparator(n)),
68
+ numericGroupSeparator: toRef(() => inferNumericGroupSeparator(n)),
64
69
  provide: createProvideFunction({ current: currentLocale, fallback: data.fallback, messages: data.messages })
65
70
  };
66
71
  };
@@ -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 }) => {
@@ -119,12 +124,21 @@ function useSSRClientHints() {
119
124
  const {
120
125
  baseUrl,
121
126
  cookieName,
127
+ cookieDomain,
128
+ cookieSecure,
129
+ cookieSameSite,
122
130
  defaultTheme
123
131
  } = ssrClientHintsConfiguration.prefersColorSchemeOptions;
124
132
  const cookieNamePrefix = `${cookieName}=`;
125
133
  initial.value.colorSchemeFromCookie = document.cookie?.split(";")?.find((c) => c.trim().startsWith(cookieNamePrefix))?.split("=")[1] ?? defaultTheme;
126
134
  const date = /* @__PURE__ */ new Date();
127
135
  const expires = new Date(date.setDate(date.getDate() + 365));
128
- initial.value.colorSchemeCookie = `${cookieName}=${initial.value.colorSchemeFromCookie}; Path=${baseUrl}; Expires=${expires.toUTCString()}; SameSite=Lax`;
136
+ initial.value.colorSchemeCookie = `${cookieName}=${initial.value.colorSchemeFromCookie}; Path=${baseUrl}; Expires=${expires.toUTCString()}; SameSite=${cookieSameSite[0].toUpperCase()}${cookieSameSite.slice(1)}`;
137
+ if (cookieDomain) {
138
+ initial.value.colorSchemeCookie += `; Domain=${cookieDomain}`;
139
+ }
140
+ if (cookieSecure) {
141
+ initial.value.colorSchemeCookie += "; Secure";
142
+ }
129
143
  return initial;
130
144
  }
@@ -273,15 +273,27 @@ function writeThemeCookie(clientHintsRequest, ssrClientHintsConfiguration2) {
273
273
  const cookieName = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieName;
274
274
  const themeName = clientHintsRequest.colorSchemeFromCookie ?? ssrClientHintsConfiguration2.prefersColorSchemeOptions.defaultTheme;
275
275
  const path = ssrClientHintsConfiguration2.prefersColorSchemeOptions.baseUrl;
276
+ const domain = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieDomain;
277
+ const secure = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieSecure;
278
+ const sameSite = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieSameSite;
276
279
  const date = /* @__PURE__ */ new Date();
277
280
  const expires = new Date(date.setDate(date.getDate() + 365));
278
281
  if (!clientHintsRequest.firstRequest || !ssrClientHintsConfiguration2.reloadOnFirstRequest) {
279
282
  useCookie(cookieName, {
280
283
  path,
284
+ domain,
281
285
  expires,
282
- sameSite: "lax"
286
+ sameSite,
287
+ secure
283
288
  }).value = themeName;
284
289
  }
285
- return `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=Lax`;
290
+ let cookie = `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=${sameSite[0].toUpperCase()}${sameSite.slice(1)}`;
291
+ if (domain) {
292
+ cookie += `; Domain=${domain}`;
293
+ }
294
+ if (secure) {
295
+ cookie += "; Secure";
296
+ }
297
+ return cookie;
286
298
  }
287
299
  export default plugin;
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.1",
4
+ "version": "1.0.0-beta.10",
5
5
  "description": "Zero-Config Nuxt Module for Vuetify",
6
6
  "author": "userquin <userquin@gmail.com>",
7
7
  "license": "MIT",
@@ -52,17 +52,18 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@nuxt/kit": "^4.3.1",
55
+ "@vuetify/loader-shared": "^2.1.2",
56
+ "@vuetify/unplugin-styles": "^1.0.0-beta.11",
55
57
  "defu": "^6.1.4",
56
- "destr": "^2.0.5",
57
58
  "local-pkg": "^1.1.2",
58
59
  "pathe": "^2.0.3",
59
60
  "perfect-debounce": "^2.1.0",
60
61
  "semver": "^7.7.4",
61
62
  "ufo": "^1.6.3",
62
- "unconfig": "^7.5.0",
63
- "upath": "^2.0.1",
64
- "vite-plugin-vuetify": "^2.1.3",
65
- "vuetify": "^4.0.1"
63
+ "unconfig": "^7.5.0"
64
+ },
65
+ "peerDependencies": {
66
+ "vuetify": "^3.4.0 || ^4.0.0"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@antfu/eslint-config": "^7.6.1",
@@ -87,21 +88,23 @@
87
88
  "eslint": "^10.0.2",
88
89
  "luxon": "^3.7.2",
89
90
  "nuxt": "^4.3.1",
91
+ "playwright-core": "^1.58.0",
90
92
  "publint": "^0.3.18",
91
93
  "rimraf": "^6.1.3",
92
94
  "sass": "^1.97.3",
93
95
  "typescript": "^5.9.3",
94
96
  "vite": "7.3.1",
95
97
  "vitest": "^4.0.18",
96
- "vue-tsc": "^3.2.5"
98
+ "vue-tsc": "^3.2.5",
99
+ "vuetify": "^4.0.1"
97
100
  },
98
101
  "build": {
99
102
  "externals": [
100
103
  "@vuetify/loader-shared",
104
+ "@vuetify/unplugin-styles",
101
105
  "node:child_process",
102
106
  "node:fs",
103
107
  "consola",
104
- "destr",
105
108
  "esbuild",
106
109
  "local-pkg",
107
110
  "pathe",
@@ -109,11 +112,9 @@
109
112
  "rollup",
110
113
  "sass",
111
114
  "sass-embedded",
112
- "upath",
113
115
  "ufo",
114
116
  "unconfig",
115
117
  "vite",
116
- "vite-plugin-vuetify",
117
118
  "vuetify"
118
119
  ]
119
120
  },
@@ -139,8 +140,9 @@
139
140
  "lint": "eslint .",
140
141
  "lint:fix": "nr lint --fix",
141
142
  "publint": "publint",
142
- "test": "vitest run",
143
- "test:watch": "vitest watch",
143
+ "test": "vitest run --exclude 'test/e2e/**'",
144
+ "test:watch": "vitest watch --exclude 'test/e2e/**'",
145
+ "test:e2e": "vitest run test/e2e",
144
146
  "release": "bumpp"
145
147
  }
146
148
  }