vuetify-nuxt-module 0.5.9 → 0.5.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 +1 -1
- package/configuration.d.ts +21 -0
- package/dist/module.d.ts +131 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +99 -4
- package/dist/runtime/plugins/client-hints.d.ts +19 -0
- package/dist/runtime/plugins/client-hints.mjs +1 -0
- package/dist/runtime/plugins/detect-browser.d.ts +51 -0
- package/dist/runtime/plugins/detect-browser.mjs +180 -0
- package/dist/runtime/plugins/vuetify-client-hints.client.d.ts +2 -0
- package/dist/runtime/plugins/vuetify-client-hints.client.mjs +86 -0
- package/dist/runtime/plugins/vuetify-client-hints.server.d.ts +2 -0
- package/dist/runtime/plugins/vuetify-client-hints.server.mjs +230 -0
- package/dist/types.d.ts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
- ⚡ **Fully Tree Shakable**: by default, only the needed Vuetify components are imported
|
|
33
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
34
|
- ✨ **Configurable Styles**: configure your variables using [Vuetify SASS Variables](https://vuetifyjs.com/en/features/sass-variables/)
|
|
35
|
-
- 💥 **SSR**: automatic SSR detection and configuration
|
|
35
|
+
- 💥 **SSR**: automatic SSR detection and configuration including [HTTP Client hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints)
|
|
36
36
|
- 🔩 **Nuxt Layers and 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 Hook](https://nuxt.com/docs/guide/going-further/hooks#nuxt-hooks-build-time)
|
|
37
37
|
- 📥 **Vuetify Configuration File**: configure your Vuetify options using a custom `vuetify.config` file, no dev server restart needed
|
|
38
38
|
- 🔥 **Pure CSS Icons**: no more font/js icons, use the new `unocss-mdi` icon set or build your own with UnoCSS Preset Icons
|
package/configuration.d.ts
CHANGED
|
@@ -22,3 +22,24 @@ declare module 'virtual:vuetify-icons-configuration' {
|
|
|
22
22
|
export const isDev: boolean
|
|
23
23
|
export function iconsConfiguration(): IconOptions
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
declare module 'virtual:vuetify-ssr-client-hints-configuration' {
|
|
27
|
+
export interface SSRClientHintsConfiguration {
|
|
28
|
+
reloadOnFirstRequest: boolean
|
|
29
|
+
viewportSize: boolean
|
|
30
|
+
prefersColorScheme: boolean
|
|
31
|
+
prefersReducedMotion: boolean
|
|
32
|
+
clientWidth?: number
|
|
33
|
+
clientHeight?: number
|
|
34
|
+
prefersColorSchemeOptions?: {
|
|
35
|
+
baseUrl: string
|
|
36
|
+
defaultTheme: string
|
|
37
|
+
themeNames: string[]
|
|
38
|
+
cookieName: string
|
|
39
|
+
darkThemeName: string
|
|
40
|
+
lightThemeName: string
|
|
41
|
+
useBrowserThemeOnly: boolean
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const ssrClientHintsConfiguration: SSRClientHintsConfiguration
|
|
45
|
+
}
|
package/dist/module.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as vuetify_locale from 'vuetify/locale';
|
|
|
5
5
|
import * as vuetify_labs_components from 'vuetify/labs/components';
|
|
6
6
|
import * as vuetify_directives from 'vuetify/directives';
|
|
7
7
|
import * as vuetify_components from 'vuetify/components';
|
|
8
|
+
import { UnwrapNestedRefs } from 'vue';
|
|
8
9
|
|
|
9
10
|
type DateAdapter = 'vuetify' | 'date-fns' | 'moment' | 'luxon' | 'dayjs' | 'js-joda' | 'date-fns-jalali' | 'jalaali' | 'hijri' | 'custom';
|
|
10
11
|
/**
|
|
@@ -204,6 +205,78 @@ interface MOptions {
|
|
|
204
205
|
* @default false
|
|
205
206
|
*/
|
|
206
207
|
includeTransformAssetsUrls?: boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Vuetify SSR client hints.
|
|
210
|
+
*
|
|
211
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints
|
|
212
|
+
*/
|
|
213
|
+
ssrClientHints?: {
|
|
214
|
+
/**
|
|
215
|
+
* Should the module reload the page on first request?
|
|
216
|
+
*
|
|
217
|
+
* @default false
|
|
218
|
+
*/
|
|
219
|
+
reloadOnFirstRequest?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* Enable `Sec-CH-Viewport-Width` and `Sec-CH-Viewport-Height` headers?
|
|
222
|
+
*
|
|
223
|
+
* @see https://wicg.github.io/responsive-image-client-hints/#sec-ch-viewport-width
|
|
224
|
+
* @see https://wicg.github.io/responsive-image-client-hints/#sec-ch-viewport-height
|
|
225
|
+
*
|
|
226
|
+
* @default false
|
|
227
|
+
*/
|
|
228
|
+
viewportSize?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Enable `Sec-CH-Prefers-Color-Scheme` header?
|
|
231
|
+
*
|
|
232
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme
|
|
233
|
+
*
|
|
234
|
+
* @default false
|
|
235
|
+
*/
|
|
236
|
+
prefersColorScheme?: boolean;
|
|
237
|
+
/**
|
|
238
|
+
* The options for `prefersColorScheme`, `prefersColorScheme` must be enabled.
|
|
239
|
+
*
|
|
240
|
+
* If you want the module to handle the color scheme for you, you should configure this option, otherwise you'll need to add your custom implementation.
|
|
241
|
+
*/
|
|
242
|
+
prefersColorSchemeOptions?: {
|
|
243
|
+
/**
|
|
244
|
+
* The name for the cookie.
|
|
245
|
+
*
|
|
246
|
+
* @default 'color-scheme'
|
|
247
|
+
*/
|
|
248
|
+
cookieName?: string;
|
|
249
|
+
/**
|
|
250
|
+
* The name for the dark theme.
|
|
251
|
+
*
|
|
252
|
+
* @default 'dark'
|
|
253
|
+
*/
|
|
254
|
+
darkThemeName?: string;
|
|
255
|
+
/**
|
|
256
|
+
* The name for the light theme.
|
|
257
|
+
*
|
|
258
|
+
* @default 'light'
|
|
259
|
+
*/
|
|
260
|
+
lightThemeName?: string;
|
|
261
|
+
/**
|
|
262
|
+
* Use the browser theme only?
|
|
263
|
+
*
|
|
264
|
+
* This flag can be used when your application provides a custom dark and light themes,
|
|
265
|
+
* but will not provide a theme switcher, that's, using by default the browser theme.
|
|
266
|
+
*
|
|
267
|
+
* @default false
|
|
268
|
+
*/
|
|
269
|
+
useBrowserThemeOnly?: boolean;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Enable `Sec-CH-Prefers-Reduced-Motion` header?
|
|
273
|
+
*
|
|
274
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion
|
|
275
|
+
*
|
|
276
|
+
* @default false
|
|
277
|
+
*/
|
|
278
|
+
prefersReducedMotion?: boolean;
|
|
279
|
+
};
|
|
207
280
|
}
|
|
208
281
|
interface ModuleOptions {
|
|
209
282
|
moduleOptions?: MOptions;
|
|
@@ -223,6 +296,54 @@ interface InlineModuleOptions extends Omit<ModuleOptions, 'vuetifyOptions'> {
|
|
|
223
296
|
interface ExternalVuetifyOptions extends VOptions {
|
|
224
297
|
config?: boolean;
|
|
225
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Request headers received from the client in SSR.
|
|
301
|
+
*/
|
|
302
|
+
interface SSRClientHints {
|
|
303
|
+
/**
|
|
304
|
+
* Is the first request the browser hits the server?
|
|
305
|
+
*/
|
|
306
|
+
firstRequest: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* The browser supports prefer-color-scheme client hints?
|
|
309
|
+
*/
|
|
310
|
+
prefersColorSchemeAvailable: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* The browser supports prefer-reduced-motion client hints?
|
|
313
|
+
*/
|
|
314
|
+
prefersReducedMotionAvailable: boolean;
|
|
315
|
+
/**
|
|
316
|
+
* The browser supports viewport-height client hints?
|
|
317
|
+
*/
|
|
318
|
+
viewportHeightAvailable: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* The browser supports viewport-width client hints?
|
|
321
|
+
*/
|
|
322
|
+
viewportWidthAvailable: boolean;
|
|
323
|
+
prefersColorScheme?: 'dark' | 'light' | 'no-preference';
|
|
324
|
+
prefersReducedMotion?: 'no-preference' | 'reduce';
|
|
325
|
+
viewportHeight?: number;
|
|
326
|
+
viewportWidth?: number;
|
|
327
|
+
/**
|
|
328
|
+
* The theme name from the cookie.
|
|
329
|
+
*/
|
|
330
|
+
colorSchemeFromCookie?: string;
|
|
331
|
+
colorSchemeCookie?: string;
|
|
332
|
+
}
|
|
333
|
+
interface SSRClientHintsConfiguration {
|
|
334
|
+
enabled: boolean;
|
|
335
|
+
viewportSize: boolean;
|
|
336
|
+
prefersColorScheme: boolean;
|
|
337
|
+
prefersReducedMotion: boolean;
|
|
338
|
+
prefersColorSchemeOptions?: {
|
|
339
|
+
baseUrl: string;
|
|
340
|
+
defaultTheme: string;
|
|
341
|
+
themeNames: string[];
|
|
342
|
+
cookieName: string;
|
|
343
|
+
darkThemeName: string;
|
|
344
|
+
lightThemeName: string;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
226
347
|
declare module '@nuxt/schema' {
|
|
227
348
|
interface NuxtConfig {
|
|
228
349
|
vuetify?: ModuleOptions;
|
|
@@ -234,6 +355,10 @@ declare module '@nuxt/schema' {
|
|
|
234
355
|
declare module '#app' {
|
|
235
356
|
interface NuxtApp {
|
|
236
357
|
$vuetify: ReturnType<typeof vuetify['createVuetify']>;
|
|
358
|
+
/**
|
|
359
|
+
* Request headers received from the client in SSR.
|
|
360
|
+
*/
|
|
361
|
+
$ssrClientHints: UnwrapNestedRefs<SSRClientHints>;
|
|
237
362
|
}
|
|
238
363
|
interface RuntimeNuxtHooks {
|
|
239
364
|
'vuetify:configuration': (options: {
|
|
@@ -244,9 +369,14 @@ declare module '#app' {
|
|
|
244
369
|
isDev: boolean;
|
|
245
370
|
vuetifyOptions: VuetifyOptions;
|
|
246
371
|
}) => Promise<void> | void;
|
|
372
|
+
'vuetify:ssr-client-hints': (options: {
|
|
373
|
+
vuetifyOptions: VuetifyOptions;
|
|
374
|
+
ssrClientHints: SSRClientHints;
|
|
375
|
+
ssrClientHintsConfiguration: SSRClientHintsConfiguration;
|
|
376
|
+
}) => Promise<void> | void;
|
|
247
377
|
}
|
|
248
378
|
}
|
|
249
379
|
|
|
250
380
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
251
381
|
|
|
252
|
-
export { ComponentName, Components, DateAdapter, DateOptions, DirectiveName, Directives, ExternalVuetifyOptions, FontAwesomeSvgIconSet, FontIconSet, IconFontName, IconSetName, IconsOptions, InlineModuleOptions, JSSVGIconSet, LabComponentName, LabComponents, MOptions, ModuleOptions, VOptions, VuetifyLocale, _default as default };
|
|
382
|
+
export { ComponentName, Components, DateAdapter, DateOptions, DirectiveName, Directives, ExternalVuetifyOptions, FontAwesomeSvgIconSet, FontIconSet, IconFontName, IconSetName, IconsOptions, InlineModuleOptions, JSSVGIconSet, LabComponentName, LabComponents, MOptions, ModuleOptions, SSRClientHints, SSRClientHintsConfiguration, VOptions, VuetifyLocale, _default as default };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify';
|
|
|
12
12
|
import { isAbsolute, join, relative } from 'pathe';
|
|
13
13
|
import { normalizePath as normalizePath$1 } from 'vite';
|
|
14
14
|
|
|
15
|
-
const version = "0.5.
|
|
15
|
+
const version = "0.5.10";
|
|
16
16
|
|
|
17
17
|
const VIRTUAL_VUETIFY_CONFIGURATION = "virtual:vuetify-configuration";
|
|
18
18
|
const RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION = `/@nuxt-vuetify-configuration/${VIRTUAL_VUETIFY_CONFIGURATION.slice("virtual:".length)}`;
|
|
@@ -20,10 +20,13 @@ const VIRTUAL_VUETIFY_DATE_CONFIGURATION = "virtual:vuetify-date-configuration";
|
|
|
20
20
|
const RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION = `/@nuxt-vuetify-configuration/${VIRTUAL_VUETIFY_DATE_CONFIGURATION.slice("virtual:".length)}`;
|
|
21
21
|
const VIRTUAL_VUETIFY_ICONS_CONFIGURATION = "virtual:vuetify-icons-configuration";
|
|
22
22
|
const RESOLVED_VIRTUAL_VUETIFY_ICONS_CONFIGURATION = `/@nuxt-vuetify-configuration/${VIRTUAL_VUETIFY_ICONS_CONFIGURATION.slice("virtual:".length)}`;
|
|
23
|
+
const VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION = "virtual:vuetify-ssr-client-hints-configuration";
|
|
24
|
+
const RESOLVED_VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION = `/@nuxt-vuetify-configuration/${VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION.slice("virtual:".length)}`;
|
|
23
25
|
const RESOLVED_VIRTUAL_MODULES = [
|
|
24
26
|
RESOLVED_VIRTUAL_VUETIFY_DATE_CONFIGURATION,
|
|
25
27
|
RESOLVED_VIRTUAL_VUETIFY_ICONS_CONFIGURATION,
|
|
26
|
-
RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION
|
|
28
|
+
RESOLVED_VIRTUAL_VUETIFY_CONFIGURATION,
|
|
29
|
+
RESOLVED_VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION
|
|
27
30
|
];
|
|
28
31
|
|
|
29
32
|
async function loadVuetifyConfiguration(cwd = process.cwd(), configOrPath = cwd, defaults = {}, extraConfigSources = []) {
|
|
@@ -335,6 +338,58 @@ function convertFontSetsToObjectNotation(sets) {
|
|
|
335
338
|
return result;
|
|
336
339
|
}
|
|
337
340
|
|
|
341
|
+
const disabledClientHints = Object.freeze({
|
|
342
|
+
enabled: false,
|
|
343
|
+
reloadOnFirstRequest: false,
|
|
344
|
+
viewportSize: false,
|
|
345
|
+
prefersColorScheme: false,
|
|
346
|
+
prefersReducedMotion: false
|
|
347
|
+
});
|
|
348
|
+
function prepareSSRClientHints(baseUrl, ctx) {
|
|
349
|
+
if (!ctx.isSSR || ctx.isNuxtGenerate)
|
|
350
|
+
return disabledClientHints;
|
|
351
|
+
const { ssrClientHints: ssrClientHintsConfiguration } = ctx.moduleOptions;
|
|
352
|
+
const clientHints = {
|
|
353
|
+
enabled: false,
|
|
354
|
+
reloadOnFirstRequest: ssrClientHintsConfiguration?.reloadOnFirstRequest ?? false,
|
|
355
|
+
viewportSize: ssrClientHintsConfiguration?.viewportSize ?? false,
|
|
356
|
+
prefersColorScheme: ssrClientHintsConfiguration?.prefersColorScheme ?? false,
|
|
357
|
+
prefersReducedMotion: ssrClientHintsConfiguration?.prefersReducedMotion ?? false
|
|
358
|
+
};
|
|
359
|
+
clientHints.enabled = clientHints.viewportSize || clientHints.prefersColorScheme || clientHints.prefersReducedMotion;
|
|
360
|
+
if (clientHints.enabled && clientHints.prefersColorScheme && ssrClientHintsConfiguration?.prefersColorSchemeOptions) {
|
|
361
|
+
const theme = ctx.vuetifyOptions.theme;
|
|
362
|
+
if (!theme)
|
|
363
|
+
throw new Error("Vuetify theme is disabled");
|
|
364
|
+
const themes = theme.themes;
|
|
365
|
+
if (!themes)
|
|
366
|
+
throw new Error("Vuetify themes is missing in theme!");
|
|
367
|
+
const defaultTheme = theme.defaultTheme;
|
|
368
|
+
if (!defaultTheme)
|
|
369
|
+
throw new Error("Vuetify default theme is missing in theme!");
|
|
370
|
+
if (!themes[defaultTheme])
|
|
371
|
+
throw new Error(`Missing default theme ${defaultTheme} in the Vuetify themes!`);
|
|
372
|
+
const darkThemeName = ssrClientHintsConfiguration.prefersColorSchemeOptions?.darkThemeName ?? "dark";
|
|
373
|
+
if (!themes[darkThemeName])
|
|
374
|
+
throw new Error(`Missing theme ${darkThemeName} in the Vuetify themes!`);
|
|
375
|
+
const lightThemeName = ssrClientHintsConfiguration.prefersColorSchemeOptions?.lightThemeName ?? "light";
|
|
376
|
+
if (!themes[lightThemeName])
|
|
377
|
+
throw new Error(`Missing theme ${lightThemeName} in the Vuetify themes!`);
|
|
378
|
+
if (darkThemeName === lightThemeName)
|
|
379
|
+
throw new Error("Vuetify dark theme and light theme are the same, change darkThemeName or lightThemeName!");
|
|
380
|
+
clientHints.prefersColorSchemeOptions = {
|
|
381
|
+
baseUrl,
|
|
382
|
+
defaultTheme,
|
|
383
|
+
themeNames: Array.from(Object.keys(themes)),
|
|
384
|
+
cookieName: ssrClientHintsConfiguration.prefersColorSchemeOptions?.cookieName ?? "color-scheme",
|
|
385
|
+
darkThemeName,
|
|
386
|
+
lightThemeName,
|
|
387
|
+
useBrowserThemeOnly: ssrClientHintsConfiguration.prefersColorSchemeOptions?.useBrowserThemeOnly ?? false
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return clientHints;
|
|
391
|
+
}
|
|
392
|
+
|
|
338
393
|
async function load(options, nuxt, ctx) {
|
|
339
394
|
var _a;
|
|
340
395
|
const {
|
|
@@ -386,6 +441,7 @@ async function load(options, nuxt, ctx) {
|
|
|
386
441
|
ctx.vuetifyOptions = configuration.vuetifyOptions;
|
|
387
442
|
ctx.vuetifyFilesToWatch = Array.from(vuetifyConfigurationFilesToWatch);
|
|
388
443
|
ctx.icons = prepareIcons(ctx.unocss, ctx.logger, vuetifyAppOptions);
|
|
444
|
+
ctx.ssrClientHints = prepareSSRClientHints(nuxt.options.app.baseURL ?? "/", ctx);
|
|
389
445
|
if (ctx.icons.enabled) {
|
|
390
446
|
ctx.icons.local?.forEach((css) => nuxt.options.css.push(css));
|
|
391
447
|
if (ctx.icons.cdn?.length) {
|
|
@@ -904,7 +960,7 @@ ${unocss}
|
|
|
904
960
|
if (ctx.icons.unocss && ctx.icons.unocssAliases) {
|
|
905
961
|
ctx.icons.imports.unshift("// @unocss-include");
|
|
906
962
|
const prefix = `${ctx.icons.unocssIconPrefix}mdi:`;
|
|
907
|
-
unocss = `const aliases = ${JSON.stringify({
|
|
963
|
+
unocss = `const aliases = JSON.parse('${JSON.stringify({
|
|
908
964
|
collapse: `${prefix}chevron-up`,
|
|
909
965
|
complete: `${prefix}check`,
|
|
910
966
|
cancel: `${prefix}close-circle`,
|
|
@@ -943,7 +999,7 @@ ${unocss}
|
|
|
943
999
|
plus: `${prefix}plus`,
|
|
944
1000
|
minus: `${prefix}minus`,
|
|
945
1001
|
calendar: `${prefix}calendar`
|
|
946
|
-
})}
|
|
1002
|
+
})}');
|
|
947
1003
|
`;
|
|
948
1004
|
}
|
|
949
1005
|
return {
|
|
@@ -1004,6 +1060,31 @@ export function dateConfiguration() {
|
|
|
1004
1060
|
}
|
|
1005
1061
|
}
|
|
1006
1062
|
|
|
1063
|
+
function vuetifySSRClientHintsPlugin(ctx) {
|
|
1064
|
+
return {
|
|
1065
|
+
name: "vuetify:ssr-client-hints:nuxt",
|
|
1066
|
+
enforce: "pre",
|
|
1067
|
+
resolveId(id) {
|
|
1068
|
+
if (id === VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION)
|
|
1069
|
+
return RESOLVED_VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION;
|
|
1070
|
+
},
|
|
1071
|
+
async load(id) {
|
|
1072
|
+
if (id === RESOLVED_VIRTUAL_VUETIFY_SSR_CLIENT_HINTS_CONFIGURATION) {
|
|
1073
|
+
const data = {
|
|
1074
|
+
reloadOnFirstRequest: ctx.ssrClientHints.reloadOnFirstRequest,
|
|
1075
|
+
viewportSize: ctx.ssrClientHints.viewportSize,
|
|
1076
|
+
prefersColorScheme: ctx.ssrClientHints.prefersColorScheme,
|
|
1077
|
+
prefersReducedMotion: ctx.ssrClientHints.prefersReducedMotion,
|
|
1078
|
+
clientWidth: ctx.vuetifyOptions.ssr?.clientWidth,
|
|
1079
|
+
clientHeight: ctx.vuetifyOptions.ssr?.clientHeight,
|
|
1080
|
+
prefersColorSchemeOptions: ctx.ssrClientHints.prefersColorSchemeOptions
|
|
1081
|
+
};
|
|
1082
|
+
return `export const ssrClientHintsConfiguration = JSON.parse('${JSON.stringify(data)}');`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1007
1088
|
function configureVite(configKey, nuxt, ctx) {
|
|
1008
1089
|
nuxt.hook("vite:extend", ({ config }) => checkVuetifyPlugins(config));
|
|
1009
1090
|
nuxt.hook("vite:extendConfig", (viteInlineConfig) => {
|
|
@@ -1020,6 +1101,8 @@ function configureVite(configKey, nuxt, ctx) {
|
|
|
1020
1101
|
viteInlineConfig.plugins.push(vuetifyConfigurationPlugin(ctx));
|
|
1021
1102
|
viteInlineConfig.plugins.push(vuetifyIconsPlugin(ctx));
|
|
1022
1103
|
viteInlineConfig.plugins.push(vuetifyDateConfigurationPlugin(ctx));
|
|
1104
|
+
if (ctx.ssrClientHints.enabled)
|
|
1105
|
+
viteInlineConfig.plugins.push(vuetifySSRClientHintsPlugin(ctx));
|
|
1023
1106
|
});
|
|
1024
1107
|
}
|
|
1025
1108
|
|
|
@@ -1075,6 +1158,16 @@ function configureNuxt(configKey, nuxt, ctx) {
|
|
|
1075
1158
|
meta: { docsUrl: `https://vuetifyjs.com/en/api/${toKebabCase(name)}/` }
|
|
1076
1159
|
})));
|
|
1077
1160
|
}
|
|
1161
|
+
if (ctx.ssrClientHints.enabled) {
|
|
1162
|
+
addPlugin({
|
|
1163
|
+
src: ctx.resolver.resolve(runtimeDir, "plugins/vuetify-client-hints.client"),
|
|
1164
|
+
mode: "client"
|
|
1165
|
+
});
|
|
1166
|
+
addPlugin({
|
|
1167
|
+
src: ctx.resolver.resolve(runtimeDir, "plugins/vuetify-client-hints.server"),
|
|
1168
|
+
mode: "server"
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1078
1171
|
addPlugin({
|
|
1079
1172
|
src: ctx.resolver.resolve(runtimeDir, `plugins/vuetify${ctx.i18n ? "-sync" : ""}`)
|
|
1080
1173
|
});
|
|
@@ -1130,9 +1223,11 @@ const module = defineNuxtModule({
|
|
|
1130
1223
|
vuetifyFilesToWatch: [],
|
|
1131
1224
|
isSSR: nuxt.options.ssr,
|
|
1132
1225
|
isDev: nuxt.options.dev,
|
|
1226
|
+
isNuxtGenerate: nuxt.options._generate,
|
|
1133
1227
|
unocss: hasNuxtModule("@unocss/nuxt", nuxt),
|
|
1134
1228
|
i18n: hasNuxtModule("@nuxtjs/i18n", nuxt),
|
|
1135
1229
|
icons: void 0,
|
|
1230
|
+
ssrClientHints: void 0,
|
|
1136
1231
|
componentsPromise: void 0,
|
|
1137
1232
|
labComponentsPromise: void 0
|
|
1138
1233
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ClientHintRequestFeatures {
|
|
2
|
+
firstRequest: boolean;
|
|
3
|
+
prefersColorSchemeAvailable: boolean;
|
|
4
|
+
prefersReducedMotionAvailable: boolean;
|
|
5
|
+
viewportHeightAvailable: boolean;
|
|
6
|
+
viewportWidthAvailable: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ClientHintsRequest extends ClientHintRequestFeatures {
|
|
9
|
+
prefersColorScheme?: 'dark' | 'light' | 'no-preference';
|
|
10
|
+
prefersReducedMotion?: 'no-preference' | 'reduce';
|
|
11
|
+
viewportHeight?: number;
|
|
12
|
+
viewportWidth?: number;
|
|
13
|
+
colorSchemeFromCookie?: string;
|
|
14
|
+
colorSchemeCookie?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SSRClientHints {
|
|
17
|
+
ssrClientHints: ClientHintsRequest;
|
|
18
|
+
}
|
|
19
|
+
export declare const VuetifyHTTPClientHints = "vuetify:nuxt:ssr-client-hints";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VuetifyHTTPClientHints = "vuetify:nuxt:ssr-client-hints";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export type DetectedInfoType = 'browser' | 'node' | 'bot-device' | 'bot' | 'react-native';
|
|
3
|
+
interface DetectedInfo<T extends DetectedInfoType, N extends string, O, V = null> {
|
|
4
|
+
readonly type: T;
|
|
5
|
+
readonly name: N;
|
|
6
|
+
readonly version: V;
|
|
7
|
+
readonly os: O;
|
|
8
|
+
}
|
|
9
|
+
export declare class BrowserInfo implements DetectedInfo<'browser', Browser, OperatingSystem | null, string> {
|
|
10
|
+
readonly name: Browser;
|
|
11
|
+
readonly version: string;
|
|
12
|
+
readonly os: OperatingSystem | null;
|
|
13
|
+
readonly type = "browser";
|
|
14
|
+
constructor(name: Browser, version: string, os: OperatingSystem | null);
|
|
15
|
+
}
|
|
16
|
+
export declare class NodeInfo implements DetectedInfo<'node', 'node', NodeJS.Platform, string> {
|
|
17
|
+
readonly version: string;
|
|
18
|
+
readonly type = "node";
|
|
19
|
+
readonly name: "node";
|
|
20
|
+
readonly os: NodeJS.Platform;
|
|
21
|
+
constructor(version: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class SearchBotDeviceInfo implements DetectedInfo<'bot-device', Browser, OperatingSystem | null, string> {
|
|
24
|
+
readonly name: Browser;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly os: OperatingSystem | null;
|
|
27
|
+
readonly bot: string;
|
|
28
|
+
readonly type = "bot-device";
|
|
29
|
+
constructor(name: Browser, version: string, os: OperatingSystem | null, bot: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class BotInfo implements DetectedInfo<'bot', 'bot', null, null> {
|
|
32
|
+
readonly type = "bot";
|
|
33
|
+
readonly bot: true;
|
|
34
|
+
readonly name: "bot";
|
|
35
|
+
readonly version: null;
|
|
36
|
+
readonly os: null;
|
|
37
|
+
}
|
|
38
|
+
export declare class ReactNativeInfo implements DetectedInfo<'react-native', 'react-native', null, null> {
|
|
39
|
+
readonly type = "react-native";
|
|
40
|
+
readonly name: "react-native";
|
|
41
|
+
readonly version: null;
|
|
42
|
+
readonly os: null;
|
|
43
|
+
}
|
|
44
|
+
export type Browser = 'aol' | 'arc' | 'brave' | 'edge' | 'edge-ios' | 'yandexbrowser' | 'kakaotalk' | 'samsung' | 'silk' | 'miui' | 'beaker' | 'edge-chromium' | 'chrome' | 'chromium-webview' | 'phantomjs' | 'crios' | 'firefox' | 'fxios' | 'opera-mini' | 'opera' | 'pie' | 'netfront' | 'ie' | 'bb10' | 'android' | 'ios' | 'safari' | 'facebook' | 'instagram' | 'ios-webview' | 'curl' | 'searchbot';
|
|
45
|
+
export type OperatingSystem = 'iOS' | 'Android OS' | 'BlackBerry OS' | 'Windows Mobile' | 'Amazon OS' | 'Windows 3.11' | 'Windows 95' | 'Windows 98' | 'Windows 2000' | 'Windows XP' | 'Windows Server 2003' | 'Windows Vista' | 'Windows 7' | 'Windows 8' | 'Windows 8.1' | 'Windows 10' | 'Windows ME' | 'Windows CE' | 'Open BSD' | 'Sun OS' | 'Linux' | 'Mac OS' | 'QNX' | 'BeOS' | 'OS/2' | 'Chrome OS';
|
|
46
|
+
export declare function detect(userAgent?: string): BrowserInfo | SearchBotDeviceInfo | BotInfo | NodeInfo | ReactNativeInfo | null;
|
|
47
|
+
export declare function browserName(ua: string): Browser | null;
|
|
48
|
+
export declare function parseUserAgent(ua: string): BrowserInfo | SearchBotDeviceInfo | BotInfo | null;
|
|
49
|
+
export declare function detectOS(ua: string): OperatingSystem | null;
|
|
50
|
+
export declare function getNodeVersion(): NodeInfo | null;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
export class BrowserInfo {
|
|
3
|
+
constructor(name, version, os) {
|
|
4
|
+
this.name = name;
|
|
5
|
+
this.version = version;
|
|
6
|
+
this.os = os;
|
|
7
|
+
}
|
|
8
|
+
type = "browser";
|
|
9
|
+
}
|
|
10
|
+
export class NodeInfo {
|
|
11
|
+
constructor(version) {
|
|
12
|
+
this.version = version;
|
|
13
|
+
}
|
|
14
|
+
type = "node";
|
|
15
|
+
name = "node";
|
|
16
|
+
os = process.platform;
|
|
17
|
+
}
|
|
18
|
+
export class SearchBotDeviceInfo {
|
|
19
|
+
constructor(name, version, os, bot) {
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.version = version;
|
|
22
|
+
this.os = os;
|
|
23
|
+
this.bot = bot;
|
|
24
|
+
}
|
|
25
|
+
type = "bot-device";
|
|
26
|
+
}
|
|
27
|
+
export class BotInfo {
|
|
28
|
+
type = "bot";
|
|
29
|
+
bot = true;
|
|
30
|
+
// NOTE: deprecated test name instead
|
|
31
|
+
name = "bot";
|
|
32
|
+
version = null;
|
|
33
|
+
os = null;
|
|
34
|
+
}
|
|
35
|
+
export class ReactNativeInfo {
|
|
36
|
+
type = "react-native";
|
|
37
|
+
name = "react-native";
|
|
38
|
+
version = null;
|
|
39
|
+
os = null;
|
|
40
|
+
}
|
|
41
|
+
const SEARCHBOX_UA_REGEX = /alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/;
|
|
42
|
+
const SEARCHBOT_OS_REGEX = /(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/;
|
|
43
|
+
const REQUIRED_VERSION_PARTS = 3;
|
|
44
|
+
const userAgentRules = [
|
|
45
|
+
["aol", /AOLShield\/([0-9\._]+)/],
|
|
46
|
+
["brave", /Brave\/([0-9\._]+)/],
|
|
47
|
+
["edge", /Edge\/([0-9\._]+)/],
|
|
48
|
+
["edge-ios", /EdgiOS\/([0-9\._]+)/],
|
|
49
|
+
["yandexbrowser", /YaBrowser\/([0-9\._]+)/],
|
|
50
|
+
["kakaotalk", /KAKAOTALK\s([0-9\.]+)/],
|
|
51
|
+
["samsung", /SamsungBrowser\/([0-9\.]+)/],
|
|
52
|
+
["silk", /\bSilk\/([0-9._-]+)\b/],
|
|
53
|
+
["miui", /MiuiBrowser\/([0-9\.]+)$/],
|
|
54
|
+
["beaker", /BeakerBrowser\/([0-9\.]+)/],
|
|
55
|
+
["edge-chromium", /EdgA?\/([0-9\.]+)/],
|
|
56
|
+
[
|
|
57
|
+
"chromium-webview",
|
|
58
|
+
/(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/
|
|
59
|
+
],
|
|
60
|
+
["chrome", /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],
|
|
61
|
+
["phantomjs", /PhantomJS\/([0-9\.]+)(:?\s|$)/],
|
|
62
|
+
["crios", /CriOS\/([0-9\.]+)(:?\s|$)/],
|
|
63
|
+
["firefox", /Firefox\/([0-9\.]+)(?:\s|$)/],
|
|
64
|
+
["fxios", /FxiOS\/([0-9\.]+)/],
|
|
65
|
+
["opera-mini", /Opera Mini.*Version\/([0-9\.]+)/],
|
|
66
|
+
["opera", /Opera\/([0-9\.]+)(?:\s|$)/],
|
|
67
|
+
["opera", /OPR\/([0-9\.]+)(:?\s|$)/],
|
|
68
|
+
["pie", /^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/],
|
|
69
|
+
["pie", /^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/],
|
|
70
|
+
["netfront", /^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/],
|
|
71
|
+
["ie", /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],
|
|
72
|
+
["ie", /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],
|
|
73
|
+
["ie", /MSIE\s(7\.0)/],
|
|
74
|
+
["bb10", /BB10;\sTouch.*Version\/([0-9\.]+)/],
|
|
75
|
+
["android", /Android\s([0-9\.]+)/],
|
|
76
|
+
["ios", /Version\/([0-9\._]+).*Mobile.*Safari.*/],
|
|
77
|
+
["safari", /Version\/([0-9\._]+).*Safari/],
|
|
78
|
+
["facebook", /FB[AS]V\/([0-9\.]+)/],
|
|
79
|
+
["instagram", /Instagram\s([0-9\.]+)/],
|
|
80
|
+
["ios-webview", /AppleWebKit\/([0-9\.]+).*Mobile/],
|
|
81
|
+
["ios-webview", /AppleWebKit\/([0-9\.]+).*Gecko\)$/],
|
|
82
|
+
["curl", /^curl\/([0-9\.]+)$/],
|
|
83
|
+
["searchbot", SEARCHBOX_UA_REGEX]
|
|
84
|
+
];
|
|
85
|
+
const operatingSystemRules = [
|
|
86
|
+
["iOS", /iP(hone|od|ad)/],
|
|
87
|
+
["Android OS", /Android/],
|
|
88
|
+
["BlackBerry OS", /BlackBerry|BB10/],
|
|
89
|
+
["Windows Mobile", /IEMobile/],
|
|
90
|
+
["Amazon OS", /Kindle/],
|
|
91
|
+
["Windows 3.11", /Win16/],
|
|
92
|
+
["Windows 95", /(Windows 95)|(Win95)|(Windows_95)/],
|
|
93
|
+
["Windows 98", /(Windows 98)|(Win98)/],
|
|
94
|
+
["Windows 2000", /(Windows NT 5.0)|(Windows 2000)/],
|
|
95
|
+
["Windows XP", /(Windows NT 5.1)|(Windows XP)/],
|
|
96
|
+
["Windows Server 2003", /(Windows NT 5.2)/],
|
|
97
|
+
["Windows Vista", /(Windows NT 6.0)/],
|
|
98
|
+
["Windows 7", /(Windows NT 6.1)/],
|
|
99
|
+
["Windows 8", /(Windows NT 6.2)/],
|
|
100
|
+
["Windows 8.1", /(Windows NT 6.3)/],
|
|
101
|
+
["Windows 10", /(Windows NT 10.0)/],
|
|
102
|
+
["Windows ME", /Windows ME/],
|
|
103
|
+
["Windows CE", /Windows CE|WinCE|Microsoft Pocket Internet Explorer/],
|
|
104
|
+
["Open BSD", /OpenBSD/],
|
|
105
|
+
["Sun OS", /SunOS/],
|
|
106
|
+
["Chrome OS", /CrOS/],
|
|
107
|
+
["Linux", /(Linux)|(X11)/],
|
|
108
|
+
["Mac OS", /(Mac_PowerPC)|(Macintosh)/],
|
|
109
|
+
["QNX", /QNX/],
|
|
110
|
+
["BeOS", /BeOS/],
|
|
111
|
+
["OS/2", /OS\/2/]
|
|
112
|
+
];
|
|
113
|
+
export function detect(userAgent) {
|
|
114
|
+
if (userAgent)
|
|
115
|
+
return parseUserAgent(userAgent);
|
|
116
|
+
if (typeof document === "undefined" && typeof navigator !== "undefined" && navigator.product === "ReactNative")
|
|
117
|
+
return new ReactNativeInfo();
|
|
118
|
+
if (typeof navigator !== "undefined")
|
|
119
|
+
return parseUserAgent(navigator.userAgent);
|
|
120
|
+
return getNodeVersion();
|
|
121
|
+
}
|
|
122
|
+
function matchUserAgent(ua) {
|
|
123
|
+
return ua !== "" && userAgentRules.reduce(
|
|
124
|
+
(matched, [browser, regex]) => {
|
|
125
|
+
if (matched)
|
|
126
|
+
return matched;
|
|
127
|
+
const uaMatch = regex.exec(ua);
|
|
128
|
+
return !!uaMatch && [browser, uaMatch];
|
|
129
|
+
},
|
|
130
|
+
false
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
export function browserName(ua) {
|
|
134
|
+
const data = matchUserAgent(ua);
|
|
135
|
+
return data ? data[0] : null;
|
|
136
|
+
}
|
|
137
|
+
export function parseUserAgent(ua) {
|
|
138
|
+
const matchedRule = matchUserAgent(ua);
|
|
139
|
+
if (!matchedRule)
|
|
140
|
+
return null;
|
|
141
|
+
const [name, match] = matchedRule;
|
|
142
|
+
if (name === "searchbot")
|
|
143
|
+
return new BotInfo();
|
|
144
|
+
let versionParts = match[1] && match[1].split(".").join("_").split("_").slice(0, 3);
|
|
145
|
+
if (versionParts) {
|
|
146
|
+
if (versionParts.length < REQUIRED_VERSION_PARTS) {
|
|
147
|
+
versionParts = [
|
|
148
|
+
...versionParts,
|
|
149
|
+
...createVersionParts(REQUIRED_VERSION_PARTS - versionParts.length)
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
versionParts = [];
|
|
154
|
+
}
|
|
155
|
+
const version = versionParts.join(".");
|
|
156
|
+
const os = detectOS(ua);
|
|
157
|
+
const searchBotMatch = SEARCHBOT_OS_REGEX.exec(ua);
|
|
158
|
+
if (searchBotMatch && searchBotMatch[1])
|
|
159
|
+
return new SearchBotDeviceInfo(name, version, os, searchBotMatch[1]);
|
|
160
|
+
return new BrowserInfo(name, version, os);
|
|
161
|
+
}
|
|
162
|
+
export function detectOS(ua) {
|
|
163
|
+
for (let ii = 0, count = operatingSystemRules.length; ii < count; ii++) {
|
|
164
|
+
const [os, regex] = operatingSystemRules[ii];
|
|
165
|
+
const match = regex.exec(ua);
|
|
166
|
+
if (match)
|
|
167
|
+
return os;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
export function getNodeVersion() {
|
|
172
|
+
const isNode = typeof process !== "undefined" && process.version;
|
|
173
|
+
return isNode ? new NodeInfo(process.version.slice(1)) : null;
|
|
174
|
+
}
|
|
175
|
+
function createVersionParts(count) {
|
|
176
|
+
const output = [];
|
|
177
|
+
for (let ii = 0; ii < count; ii++)
|
|
178
|
+
output.push("0");
|
|
179
|
+
return output;
|
|
180
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ssrClientHintsConfiguration } from "virtual:vuetify-ssr-client-hints-configuration";
|
|
2
|
+
import { VuetifyHTTPClientHints } from "./client-hints.mjs";
|
|
3
|
+
import { defineNuxtPlugin } from "#imports";
|
|
4
|
+
import { useNuxtApp } from "#app";
|
|
5
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
|
+
const state = useState(VuetifyHTTPClientHints);
|
|
7
|
+
const {
|
|
8
|
+
firstRequest,
|
|
9
|
+
prefersColorSchemeAvailable,
|
|
10
|
+
prefersReducedMotionAvailable,
|
|
11
|
+
viewportHeightAvailable,
|
|
12
|
+
viewportWidthAvailable
|
|
13
|
+
} = state.value.ssrClientHints;
|
|
14
|
+
const {
|
|
15
|
+
reloadOnFirstRequest,
|
|
16
|
+
viewportSize,
|
|
17
|
+
prefersReducedMotion,
|
|
18
|
+
prefersColorScheme,
|
|
19
|
+
prefersColorSchemeOptions
|
|
20
|
+
} = ssrClientHintsConfiguration;
|
|
21
|
+
if (firstRequest && reloadOnFirstRequest) {
|
|
22
|
+
if (prefersColorScheme) {
|
|
23
|
+
const themeCookie = state.value.ssrClientHints.colorSchemeCookie;
|
|
24
|
+
if (prefersColorSchemeOptions && themeCookie) {
|
|
25
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
26
|
+
const cookieName = prefersColorSchemeOptions.cookieName;
|
|
27
|
+
const parseCookieName = `${cookieName}=`;
|
|
28
|
+
const cookieEntry = `${parseCookieName}${state.value.ssrClientHints.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};`;
|
|
29
|
+
const newThemeName = prefersDark ? prefersColorSchemeOptions.darkThemeName : prefersColorSchemeOptions.lightThemeName;
|
|
30
|
+
document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`);
|
|
31
|
+
window.location.reload();
|
|
32
|
+
} else if (prefersColorSchemeAvailable) {
|
|
33
|
+
window.location.reload();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (prefersReducedMotion && prefersReducedMotionAvailable)
|
|
37
|
+
window.location.reload();
|
|
38
|
+
if (viewportSize && viewportHeightAvailable)
|
|
39
|
+
window.location.reload();
|
|
40
|
+
if (viewportSize && viewportWidthAvailable)
|
|
41
|
+
window.location.reload();
|
|
42
|
+
}
|
|
43
|
+
if (viewportSize || prefersColorScheme && prefersColorSchemeOptions) {
|
|
44
|
+
nuxtApp.hook("vuetify:before-create", ({ vuetifyOptions }) => {
|
|
45
|
+
if (viewportSize) {
|
|
46
|
+
const clientWidth = state.value.ssrClientHints.viewportWidth;
|
|
47
|
+
const clientHeight = state.value.ssrClientHints.viewportHeight;
|
|
48
|
+
vuetifyOptions.ssr = typeof clientWidth === "number" ? {
|
|
49
|
+
clientWidth,
|
|
50
|
+
clientHeight
|
|
51
|
+
} : true;
|
|
52
|
+
}
|
|
53
|
+
if (prefersColorScheme && prefersColorSchemeOptions)
|
|
54
|
+
vuetifyOptions.theme.defaultTheme = state.value.ssrClientHints.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme;
|
|
55
|
+
});
|
|
56
|
+
if (prefersColorScheme && prefersColorSchemeOptions) {
|
|
57
|
+
const themeCookie = state.value.ssrClientHints.colorSchemeCookie;
|
|
58
|
+
if (themeCookie) {
|
|
59
|
+
nuxtApp.hook("app:beforeMount", () => {
|
|
60
|
+
const vuetify = useNuxtApp().$vuetify;
|
|
61
|
+
const cookieName = prefersColorSchemeOptions.cookieName;
|
|
62
|
+
const parseCookieName = `${cookieName}=`;
|
|
63
|
+
const cookieEntry = `${parseCookieName}${state.value.ssrClientHints.colorSchemeFromCookie ?? prefersColorSchemeOptions.defaultTheme};`;
|
|
64
|
+
watch(vuetify.theme.global.name, (newThemeName) => {
|
|
65
|
+
document.cookie = themeCookie.replace(cookieEntry, `${cookieName}=${newThemeName};`);
|
|
66
|
+
});
|
|
67
|
+
if (prefersColorSchemeOptions.useBrowserThemeOnly) {
|
|
68
|
+
const { darkThemeName, lightThemeName } = prefersColorSchemeOptions;
|
|
69
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
|
|
70
|
+
const prefersLight = window.matchMedia("(prefers-color-scheme: light)");
|
|
71
|
+
prefersDark.addEventListener("change", (e) => switchTheme(e, darkThemeName, vuetify));
|
|
72
|
+
prefersLight.addEventListener("change", (e) => switchTheme(e, lightThemeName, vuetify));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
provide: reactive({
|
|
80
|
+
ssrClientHints: state
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
function switchTheme(e, newThemeName, vuetify) {
|
|
85
|
+
e.matches && (vuetify.theme.global.name.value = newThemeName);
|
|
86
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { ssrClientHintsConfiguration } from "virtual:vuetify-ssr-client-hints-configuration";
|
|
2
|
+
import { parseUserAgent } from "./detect-browser.mjs";
|
|
3
|
+
import { VuetifyHTTPClientHints } from "./client-hints.mjs";
|
|
4
|
+
import { defineNuxtPlugin, useNuxtApp } from "#imports";
|
|
5
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
6
|
+
const event = useRequestEvent();
|
|
7
|
+
const state = useState(VuetifyHTTPClientHints);
|
|
8
|
+
const request = event.node.req;
|
|
9
|
+
const response = event.node.res;
|
|
10
|
+
const requestHeaders = request.headers ?? {};
|
|
11
|
+
const userAgentHeader = readClientHeader("user-agent", requestHeaders);
|
|
12
|
+
const userAgent = userAgentHeader ? parseUserAgent(userAgentHeader) : null;
|
|
13
|
+
const clientHintsRequest = collectClientHints(userAgent, ssrClientHintsConfiguration, requestHeaders);
|
|
14
|
+
writeClientHintsResponseHeaders(clientHintsRequest, ssrClientHintsConfiguration, response);
|
|
15
|
+
state.value = {
|
|
16
|
+
ssrClientHints: clientHintsRequest
|
|
17
|
+
};
|
|
18
|
+
state.value.ssrClientHints.colorSchemeCookie = writeThemeCookie(
|
|
19
|
+
clientHintsRequest,
|
|
20
|
+
ssrClientHintsConfiguration
|
|
21
|
+
);
|
|
22
|
+
nuxtApp.hook("vuetify:before-create", async ({ vuetifyOptions }) => {
|
|
23
|
+
const clientWidth = clientHintsRequest.viewportWidth;
|
|
24
|
+
const clientHeight = clientHintsRequest.viewportHeight;
|
|
25
|
+
vuetifyOptions.ssr = typeof clientWidth === "number" ? {
|
|
26
|
+
clientWidth,
|
|
27
|
+
clientHeight
|
|
28
|
+
} : true;
|
|
29
|
+
if (clientHintsRequest.colorSchemeFromCookie)
|
|
30
|
+
vuetifyOptions.theme.defaultTheme = clientHintsRequest.colorSchemeFromCookie;
|
|
31
|
+
await nuxtApp.hooks.callHook("vuetify:ssr-client-hints", {
|
|
32
|
+
vuetifyOptions,
|
|
33
|
+
ssrClientHintsConfiguration,
|
|
34
|
+
ssrClientHints: state.value
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
provide: reactive({
|
|
39
|
+
ssrClientHints: state
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
const AcceptClientHintsHeaders = {
|
|
44
|
+
prefersColorScheme: "Sec-CH-Prefers-Color-Scheme",
|
|
45
|
+
prefersReducedMotion: "Sec-CH-Prefers-Reduced-Motion",
|
|
46
|
+
viewportHeight: "Sec-CH-Viewport-Height",
|
|
47
|
+
viewportWidth: "Sec-CH-Viewport-Width"
|
|
48
|
+
};
|
|
49
|
+
const chromiumBasedBrowserFeatures = {
|
|
50
|
+
prefersColorScheme: (_, v) => v[0] >= 93,
|
|
51
|
+
prefersReducedMotion: (_, v) => v[0] >= 108,
|
|
52
|
+
viewportHeight: (_, v) => v[0] >= 108,
|
|
53
|
+
viewportWidth: (_, v) => v[0] >= 108
|
|
54
|
+
};
|
|
55
|
+
const allowedBrowsers = [
|
|
56
|
+
// 'edge',
|
|
57
|
+
// 'edge-ios',
|
|
58
|
+
["chrome", chromiumBasedBrowserFeatures],
|
|
59
|
+
["edge-chromium", chromiumBasedBrowserFeatures],
|
|
60
|
+
["chromium-webview", chromiumBasedBrowserFeatures],
|
|
61
|
+
["opera", {
|
|
62
|
+
prefersColorScheme: (android, v) => v[0] >= (android ? 66 : 79),
|
|
63
|
+
prefersReducedMotion: (android, v) => v[0] >= (android ? 73 : 94),
|
|
64
|
+
viewportHeight: (android, v) => v[0] >= (android ? 73 : 94),
|
|
65
|
+
viewportWidth: (android, v) => v[0] >= (android ? 73 : 94)
|
|
66
|
+
}]
|
|
67
|
+
];
|
|
68
|
+
const AcceptClientHintsRequestHeaders = Object.entries(AcceptClientHintsHeaders).reduce((acc, [key, value]) => {
|
|
69
|
+
acc[key] = value.toLowerCase();
|
|
70
|
+
return acc;
|
|
71
|
+
}, {});
|
|
72
|
+
const ClientHeaders = ["Accept-CH", "Vary", "Critical-CH"];
|
|
73
|
+
function readClientHeader(name, headers) {
|
|
74
|
+
const value = headers[name];
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value[0];
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function browserFeatureAvailable(userAgent, feature) {
|
|
80
|
+
if (userAgent == null || userAgent.type !== "browser")
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const browserName = userAgent.name;
|
|
84
|
+
const android = userAgent.os?.toLowerCase().startsWith("android") ?? false;
|
|
85
|
+
const versions = userAgent.version.split(".").map((v) => Number.parseInt(v));
|
|
86
|
+
return allowedBrowsers.some(([name, check]) => {
|
|
87
|
+
if (browserName !== name)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
return check[feature](android, versions);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function lookupClientHints(userAgent, ssrClientHintsConfiguration2) {
|
|
100
|
+
const features = {
|
|
101
|
+
firstRequest: true,
|
|
102
|
+
prefersColorSchemeAvailable: false,
|
|
103
|
+
prefersReducedMotionAvailable: false,
|
|
104
|
+
viewportHeightAvailable: false,
|
|
105
|
+
viewportWidthAvailable: false
|
|
106
|
+
};
|
|
107
|
+
if (userAgent == null || userAgent.type !== "browser")
|
|
108
|
+
return features;
|
|
109
|
+
if (ssrClientHintsConfiguration2.prefersColorScheme)
|
|
110
|
+
features.prefersColorSchemeAvailable = browserFeatureAvailable(userAgent, "prefersColorScheme");
|
|
111
|
+
if (ssrClientHintsConfiguration2.prefersReducedMotion)
|
|
112
|
+
features.prefersReducedMotionAvailable = browserFeatureAvailable(userAgent, "prefersReducedMotion");
|
|
113
|
+
if (ssrClientHintsConfiguration2.viewportSize) {
|
|
114
|
+
features.viewportHeightAvailable = browserFeatureAvailable(userAgent, "viewportHeight");
|
|
115
|
+
features.viewportWidthAvailable = browserFeatureAvailable(userAgent, "viewportWidth");
|
|
116
|
+
}
|
|
117
|
+
return features;
|
|
118
|
+
}
|
|
119
|
+
function collectClientHints(userAgent, ssrClientHintsConfiguration2, headers) {
|
|
120
|
+
const hints = lookupClientHints(userAgent, ssrClientHintsConfiguration2);
|
|
121
|
+
if (ssrClientHintsConfiguration2.prefersColorScheme) {
|
|
122
|
+
if (ssrClientHintsConfiguration2.prefersColorSchemeOptions) {
|
|
123
|
+
const cookieName = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieName;
|
|
124
|
+
const cookieValue = readClientHeader("cookie", headers)?.split(";").find((c) => c.trim().startsWith(`${cookieName}=`));
|
|
125
|
+
if (cookieValue) {
|
|
126
|
+
const value = cookieValue.split("=")?.[1].trim();
|
|
127
|
+
if (ssrClientHintsConfiguration2.prefersColorSchemeOptions.themeNames.includes(value)) {
|
|
128
|
+
hints.colorSchemeFromCookie = value;
|
|
129
|
+
hints.firstRequest = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!hints.colorSchemeFromCookie) {
|
|
134
|
+
const value = hints.prefersColorSchemeAvailable ? readClientHeader(AcceptClientHintsRequestHeaders.prefersColorScheme, headers)?.toLowerCase() : void 0;
|
|
135
|
+
if (value === "dark" || value === "light" || value === "no-preference") {
|
|
136
|
+
hints.prefersColorScheme = value;
|
|
137
|
+
hints.firstRequest = false;
|
|
138
|
+
}
|
|
139
|
+
if (ssrClientHintsConfiguration2.prefersColorSchemeOptions) {
|
|
140
|
+
if (!value || value === "no-preference") {
|
|
141
|
+
hints.colorSchemeFromCookie = ssrClientHintsConfiguration2.prefersColorSchemeOptions.defaultTheme;
|
|
142
|
+
} else {
|
|
143
|
+
hints.colorSchemeFromCookie = value === "dark" ? ssrClientHintsConfiguration2.prefersColorSchemeOptions.darkThemeName : ssrClientHintsConfiguration2.prefersColorSchemeOptions.lightThemeName;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (hints.prefersReducedMotionAvailable && ssrClientHintsConfiguration2.prefersReducedMotion) {
|
|
149
|
+
const value = readClientHeader(AcceptClientHintsRequestHeaders.prefersReducedMotion, headers)?.toLowerCase();
|
|
150
|
+
if (value === "no-preference" || value === "reduce") {
|
|
151
|
+
hints.prefersReducedMotion = value;
|
|
152
|
+
hints.firstRequest = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (hints.viewportHeightAvailable && ssrClientHintsConfiguration2.viewportSize) {
|
|
156
|
+
const header = readClientHeader(AcceptClientHintsRequestHeaders.viewportHeight, headers);
|
|
157
|
+
if (header) {
|
|
158
|
+
hints.firstRequest = false;
|
|
159
|
+
try {
|
|
160
|
+
hints.viewportHeight = Number.parseInt(header);
|
|
161
|
+
} catch {
|
|
162
|
+
hints.viewportHeight = ssrClientHintsConfiguration2.clientHeight;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
hints.viewportHeight = ssrClientHintsConfiguration2.clientHeight;
|
|
167
|
+
}
|
|
168
|
+
if (hints.viewportWidthAvailable && ssrClientHintsConfiguration2.viewportSize) {
|
|
169
|
+
const header = readClientHeader(AcceptClientHintsRequestHeaders.viewportWidth, headers);
|
|
170
|
+
if (header) {
|
|
171
|
+
hints.firstRequest = false;
|
|
172
|
+
try {
|
|
173
|
+
hints.viewportWidth = Number.parseInt(header);
|
|
174
|
+
} catch {
|
|
175
|
+
hints.viewportWidth = ssrClientHintsConfiguration2.clientWidth;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
hints.viewportWidth = ssrClientHintsConfiguration2.clientWidth;
|
|
180
|
+
}
|
|
181
|
+
return hints;
|
|
182
|
+
}
|
|
183
|
+
function writeClientHintHeaders(key, headers) {
|
|
184
|
+
ClientHeaders.forEach((header) => {
|
|
185
|
+
headers[header] = (headers[header] ? headers[header] : []).concat(key);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function writeClientHintsResponseHeaders(clientHintsRequest, ssrClientHintsConfiguration2, response) {
|
|
189
|
+
const headers = {};
|
|
190
|
+
if (ssrClientHintsConfiguration2.prefersColorScheme && clientHintsRequest.prefersColorSchemeAvailable)
|
|
191
|
+
writeClientHintHeaders(AcceptClientHintsHeaders.prefersColorScheme, headers);
|
|
192
|
+
if (ssrClientHintsConfiguration2.prefersReducedMotion && clientHintsRequest.prefersReducedMotionAvailable)
|
|
193
|
+
writeClientHintHeaders(AcceptClientHintsHeaders.prefersReducedMotion, headers);
|
|
194
|
+
if (ssrClientHintsConfiguration2.viewportSize && clientHintsRequest.viewportHeightAvailable && clientHintsRequest.viewportWidthAvailable) {
|
|
195
|
+
writeClientHintHeaders(AcceptClientHintsHeaders.viewportHeight, headers);
|
|
196
|
+
writeClientHintHeaders(AcceptClientHintsHeaders.viewportWidth, headers);
|
|
197
|
+
}
|
|
198
|
+
if (Object.keys(headers).length === 0)
|
|
199
|
+
return;
|
|
200
|
+
withNuxtAppRendered(() => {
|
|
201
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
202
|
+
response.setHeader(key, value);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function withNuxtAppRendered(callback) {
|
|
207
|
+
const nuxtApp = useNuxtApp();
|
|
208
|
+
const unhook = nuxtApp.hooks.hookOnce("app:rendered", callback);
|
|
209
|
+
nuxtApp.hooks.hookOnce("app:error", () => {
|
|
210
|
+
unhook();
|
|
211
|
+
return callback();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function writeThemeCookie(clientHintsRequest, ssrClientHintsConfiguration2) {
|
|
215
|
+
if (!ssrClientHintsConfiguration2.prefersColorScheme || !ssrClientHintsConfiguration2.prefersColorSchemeOptions)
|
|
216
|
+
return;
|
|
217
|
+
const cookieName = ssrClientHintsConfiguration2.prefersColorSchemeOptions.cookieName;
|
|
218
|
+
const themeName = clientHintsRequest.colorSchemeFromCookie ?? ssrClientHintsConfiguration2.prefersColorSchemeOptions.defaultTheme;
|
|
219
|
+
const path = ssrClientHintsConfiguration2.prefersColorSchemeOptions.baseUrl;
|
|
220
|
+
const date = /* @__PURE__ */ new Date();
|
|
221
|
+
const expires = new Date(date.setDate(date.getDate() + 365));
|
|
222
|
+
if (!clientHintsRequest.firstRequest || !ssrClientHintsConfiguration2.reloadOnFirstRequest) {
|
|
223
|
+
useCookie(cookieName, {
|
|
224
|
+
path,
|
|
225
|
+
expires,
|
|
226
|
+
sameSite: "lax"
|
|
227
|
+
}).value = themeName;
|
|
228
|
+
}
|
|
229
|
+
return `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=Lax`;
|
|
230
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ declare module 'nuxt/schema' {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
export { ComponentName, Components, DateAdapter, DateOptions, DirectiveName, Directives, ExternalVuetifyOptions, FontAwesomeSvgIconSet, FontIconSet, IconFontName, IconSetName, IconsOptions, InlineModuleOptions, JSSVGIconSet, LabComponentName, LabComponents, MOptions, ModuleOptions, VOptions, VuetifyLocale, default } from './module'
|
|
15
|
+
export { ComponentName, Components, DateAdapter, DateOptions, DirectiveName, Directives, ExternalVuetifyOptions, FontAwesomeSvgIconSet, FontIconSet, IconFontName, IconSetName, IconsOptions, InlineModuleOptions, JSSVGIconSet, LabComponentName, LabComponents, MOptions, ModuleOptions, SSRClientHints, SSRClientHintsConfiguration, VOptions, VuetifyLocale, default } from './module'
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vuetify-nuxt-module",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
5
|
-
"packageManager": "pnpm@8.7.
|
|
4
|
+
"version": "0.5.10",
|
|
5
|
+
"packageManager": "pnpm@8.7.6",
|
|
6
6
|
"description": "Zero-Config Nuxt Module for Vuetify",
|
|
7
7
|
"author": "userquin <userquin@gmail.com>",
|
|
8
8
|
"license": "MIT",
|
|
@@ -128,4 +128,4 @@
|
|
|
128
128
|
"installDependencies": false,
|
|
129
129
|
"startCommand": "node .stackblitz.js && pnpm install && pnpm run dev"
|
|
130
130
|
}
|
|
131
|
-
}
|
|
131
|
+
}
|