diff --git a/README.md b/README.md index 97ffe29..a12a322 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ or in your modules, composables, or other plugins: const clientHints = useNuxtApp().$httpClientHints ``` +You can also use this module with [HTTP Client Hints for H3](https://github.com/userquin/http-client-hints/blob/main/src/h3.ts) (`http-client-hints/h3`) to add a custom [Nitro](https://github.com/unjs/nitro) image event handler to send back to the browser an optimized image from the original one. Check the [playground](https://github.com/userquin/nuxt-http-client-hints/tree/main/playground/server) server folder for an example using Nitro server handler in dev and production mode with [sharp](https://github.com/lovell/sharp). + That's it! You can now use HTTP Client Hints in your Nuxt app ✨ You can check the source code or the [JSDocs](https://www.jsdocs.io/package/nuxt-http-client-hints) for more information. diff --git a/package.json b/package.json index b4b5527..60efd5f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nuxt-http-client-hints", "type": "module", "version": "0.0.2", - "packageManager": "pnpm@9.12.1", + "packageManager": "pnpm@9.12.3", "description": "Nuxt HTTP Client Hints", "author": "userquin ", "license": "MIT", @@ -17,7 +17,7 @@ "Device Client Hints", "Network Client Hints", "Browser detection", - "nuxt module" + "Nuxt module" ], "sideEffects": false, "exports": { @@ -44,10 +44,11 @@ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit" }, "dependencies": { - "@nuxt/kit": "^3.13.2", - "detect-browser-es": "^0.1.1" + "detect-browser-es": "^0.1.1", + "http-client-hints": "^0.0.1" }, "devDependencies": { + "@nuxt/kit": "^3.13.2", "@nuxt/devtools": "^1.5.0", "@nuxt/eslint-config": "^0.5.7", "@nuxt/module-builder": "^0.8.4", diff --git a/playground/app.vue b/playground/app/app.vue similarity index 90% rename from playground/app.vue rename to playground/app/app.vue index c7774fe..ad92a53 100644 --- a/playground/app.vue +++ b/playground/app/app.vue @@ -8,7 +8,7 @@ - diff --git a/playground/app/plugins/hints.client.ts b/playground/app/plugins/hints.client.ts new file mode 100644 index 0000000..1e41ec0 --- /dev/null +++ b/playground/app/plugins/hints.client.ts @@ -0,0 +1,7 @@ +export default defineNuxtPlugin({ + setup(nuxt) { + nuxt.hook('http-client-hints:client-hints', (ssrClientHints) => { + console.log('http-client-hints:client-hints', ssrClientHints) + }) + }, +}) diff --git a/playground/plugins/plugin.server.ts b/playground/app/plugins/hints.server.ts similarity index 65% rename from playground/plugins/plugin.server.ts rename to playground/app/plugins/hints.server.ts index f4f8eff..8e289fc 100644 --- a/playground/plugins/plugin.server.ts +++ b/playground/app/plugins/hints.server.ts @@ -1,7 +1,7 @@ export default defineNuxtPlugin({ setup(nuxt) { nuxt.hook('http-client-hints:ssr-client-hints', (ssrClientHints) => { - console.log(ssrClientHints) + console.log('http-client-hints:ssr-client-hints', ssrClientHints) }) }, }) diff --git a/playground/modules/image.ts b/playground/modules/image.ts new file mode 100644 index 0000000..af8be27 --- /dev/null +++ b/playground/modules/image.ts @@ -0,0 +1,21 @@ +import { addDevServerHandler, defineNuxtModule } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(_, nuxt) { + if (nuxt.options.dev) { + addDevServerHandler({ + route: '', + handler: await import('../server/dev-image').then(m => m.default), + }) + } + else { + nuxt.hook('nitro:build:before', async (nitro) => { + nitro.options.handlers.unshift({ + route: '', + handler: '~~/server/image', + middleware: true, + }) + }) + } + }, +}) diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index b051933..b70a784 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -3,6 +3,10 @@ export default defineNuxtConfig({ devtools: { enabled: true }, modules: ['../src/module'], + future: { + compatibilityVersion: 4, + }, + httpClientHints: { detectBrowser: true, detectOS: 'windows-11', @@ -14,6 +18,21 @@ export default defineNuxtConfig({ viewportSize: true, prefersColorScheme: true, }, + serverImages: true, + }, + + nitro: { + /* handlers: [ + { + middleware: true, + // route: '', + handler: './server/image', + }, + ], */ + /* devHandlers: [{ + route: '', + handler: DevImage, + }], */ }, }) diff --git a/playground/package.json b/playground/package.json index d86949b..d176752 100644 --- a/playground/package.json +++ b/playground/package.json @@ -8,6 +8,11 @@ "generate": "nuxi generate" }, "dependencies": { - "nuxt": "^3.13.2" + "nuxt": "^3.13.2", + "sharp": "^0.33.5" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vue-tsc": "^2.1.6" } } diff --git a/playground/server/dev-image.ts b/playground/server/dev-image.ts new file mode 100644 index 0000000..fe97982 --- /dev/null +++ b/playground/server/dev-image.ts @@ -0,0 +1,71 @@ +import { lstat, readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { Readable } from 'node:stream' +import { lazyEventHandler, eventHandler, sendStream } from 'h3' +import sharp from 'sharp' +import { useNitro } from '@nuxt/kit' +import { extractImageClientHints } from 'http-client-hints/h3' +import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from 'http-client-hints/h3' + +export default lazyEventHandler(async () => { + const nitroOptions = useNitro().options + const { + serverImages, + ...rest + } = nitroOptions.appConfig.httpClientHints as ServerHttpClientHintsOptions + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + + return eventHandler(async (event) => { + console.log('dev-image', event.path) + const clientHints = await extractImageClientHints(event, options) + console.log('dev-image', event.path, clientHints?.httpClientHints.critical) + if (clientHints) { + const { + widthAvailable = false, + width = -1, + } = clientHints.httpClientHints.critical ?? {} + if (widthAvailable && width > -1) { + const image = await convertImage(event.path, width) + if (image) { + console.log('dev-image:Sec-CH-Width:', width) + event.node.res.setHeader('Vary', 'Sec-CH-Width') + return sendStream(event, Readable.from(image)) + } + } + } + }) + async function convertImage(path: string, width: number) { + /* try { + const image = await readAsset(path) + if (image) { + return await sharp(image).resize({ width }).toBuffer() + } + } + catch { + // just ignore + } + + // return undefined */ + if (path.startsWith('/')) { + path = path.slice(1) + } + const folders = nitroOptions.publicAssets + let image: string + for (const folder of folders) { + try { + console.log(folder.dir) + image = resolve(folder.dir, path) + const stats = await lstat(image) + if (stats.isFile()) { + return await sharp(await readFile(image)).resize({ width }).toBuffer() + } + } + catch { + // just ignore + } + } + } +}) diff --git a/playground/server/image.ts b/playground/server/image.ts new file mode 100644 index 0000000..6080ef3 --- /dev/null +++ b/playground/server/image.ts @@ -0,0 +1,83 @@ +import { lstat, readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { Readable } from 'node:stream' +import { fileURLToPath } from 'node:url' +import { lazyEventHandler, eventHandler, sendStream } from 'h3' +import sharp from 'sharp' +import { extractImageClientHints } from 'http-client-hints/h3' +import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from 'http-client-hints/h3' +// import { readAsset } from '#internal/nitro/virtual/public-assets-data' + +export default lazyEventHandler(() => { + const appConfig = useAppConfig() + const nitroApp = useNitroApp() + const { + serverImages, + ...rest + } = appConfig.httpClientHints as ServerHttpClientHintsOptions + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + + const publicFolder = resolve(fileURLToPath(import.meta.url), '../../public') + + const handler = eventHandler(async (event) => { + console.log('dev-image', event.path) + const clientHints = await extractImageClientHints(event, options) + console.log('dev-image', event.path, clientHints?.httpClientHints.critical) + if (clientHints) { + const { + widthAvailable = false, + width = -1, + } = clientHints.httpClientHints.critical ?? {} + if (widthAvailable && width > -1) { + const image = await convertImage(event.path, width) + if (image) { + console.log('dev-image:Sec-CH-Width:', width) + event.node.res.setHeader('Vary', 'Sec-CH-Width') + return sendStream(event, Readable.from(image)) + } + } + } + }) + + async function convertImage(path: string, width: number) { + /* try { + const image = await readAsset(path) + if (image) { + return await sharp(image).resize({ width }).toBuffer() + } + } + catch (e) { + // just ignore + console.error('WTF', e) + } */ + + // return undefined + if (path.startsWith('/')) { + path = path.slice(1) + } + // const folders = appConfig.publicAssets + // let image: string + // for (const folder of folders) { + try { + const image = resolve(publicFolder, path) + const stats = await lstat(image) + if (stats.isFile()) { + return await sharp(await readFile(image)).resize({ width }).toBuffer() + } + } + catch { + // just ignore + } + } + // } + + nitroApp.h3App.stack.unshift({ + route: '', + handler, + }) + + return eventHandler(() => {}) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb59867..e87b74c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,31 +8,34 @@ importers: .: dependencies: - '@nuxt/kit': - specifier: ^3.13.2 - version: 3.13.2(magicast@0.3.5)(rollup@4.24.0) detect-browser-es: specifier: ^0.1.1 version: 0.1.1 + http-client-hints: + specifier: ^0.0.1 + version: 0.0.1(h3@1.13.0) devDependencies: '@nuxt/devtools': specifier: ^1.5.0 - version: 1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + version: 1.5.2(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) '@nuxt/eslint-config': specifier: ^0.5.7 version: 0.5.7(eslint@9.12.0(jiti@2.3.3))(typescript@5.6.3) + '@nuxt/kit': + specifier: ^3.13.2 + version: 3.13.2(magicast@0.3.5)(rollup@3.29.5) '@nuxt/module-builder': specifier: ^0.8.4 - version: 0.8.4(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.24.0))(nuxi@3.14.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3)) + version: 0.8.4(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(nuxi@3.14.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3)) '@nuxt/schema': specifier: ^3.13.2 - version: 3.13.2(rollup@4.24.0) + version: 3.13.2(rollup@3.29.5) '@nuxt/test-utils': specifier: ^3.14.2 - version: 3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) + version: 3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) '@types/node': specifier: latest - version: 22.7.5 + version: 22.7.6 bumpp: specifier: ^9.2.0 version: 9.7.1(magicast@0.3.5) @@ -44,13 +47,29 @@ importers: version: 9.12.0(jiti@2.3.3) nuxt: specifier: ^3.13.0 - version: 3.13.2(@parcel/watcher@2.4.1)(@types/node@22.7.5)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) + version: 3.13.2(@parcel/watcher@2.4.1)(@types/node@22.7.6)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) typescript: specifier: ^5.6.3 version: 5.6.3 vitest: specifier: ^2.1.1 - version: 2.1.2(@types/node@22.7.5)(terser@5.34.1) + version: 2.1.2(@types/node@22.7.6)(terser@5.34.1) + vue-tsc: + specifier: ^2.1.6 + version: 2.1.6(typescript@5.6.3) + + playground: + dependencies: + nuxt: + specifier: ^3.13.2 + version: 3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.1)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) + sharp: + specifier: ^0.33.5 + version: 0.33.5 + devDependencies: + typescript: + specifier: ^5.6.3 + version: 5.6.3 vue-tsc: specifier: ^2.1.6 version: 2.1.6(typescript@5.6.3) @@ -216,6 +235,9 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@es-joy/jsdoccomment@0.48.0': resolution: {integrity: sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==} engines: {node: '>=16'} @@ -985,6 +1007,111 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -1417,8 +1544,11 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@22.7.6': + resolution: {integrity: sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==} + + '@types/node@22.8.1': + resolution: {integrity: sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1917,10 +2047,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -2455,6 +2592,14 @@ packages: picomatch: optional: true + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2650,6 +2795,14 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + http-client-hints@0.0.1: + resolution: {integrity: sha512-8YyxZQ8j/TZkgQSEiKKnCILDpD/h+tyDmeKHACh4jh5mjQRuwGoytdPTjCe4BSOTmVHuqEs82uwLkH6Z6yBhrw==} + peerDependencies: + h3: ^1.13.0 + peerDependenciesMeta: + h3: + optional: true + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2722,6 +2875,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -3720,6 +3876,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3744,6 +3904,9 @@ packages: simple-git@3.27.0: resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -3932,12 +4095,12 @@ packages: tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} - tinyglobby@0.2.6: - resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} - tinyglobby@0.2.9: - resolution: {integrity: sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==} + tinyglobby@0.2.6: + resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} engines: {node: '>=12.0.0'} tinypool@1.0.1: @@ -4647,6 +4810,11 @@ snapshots: dependencies: mime: 3.0.0 + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.7.0 + optional: true + '@es-joy/jsdoccomment@0.48.0': dependencies: comment-parser: 1.4.1 @@ -5060,6 +5228,81 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@ioredis/commands@1.2.0': {} '@isaacs/cliui@8.0.2': @@ -5148,12 +5391,24 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.5.2(magicast@0.3.5)(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))': + '@nuxt/devtools-kit@1.5.2(magicast@0.3.5)(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))': + dependencies: + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + '@nuxt/schema': 3.13.2(rollup@3.29.5) + execa: 7.2.0 + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + transitivePeerDependencies: + - magicast + - rollup + - supports-color + - webpack-sources + + '@nuxt/devtools-kit@1.5.2(magicast@0.3.5)(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) '@nuxt/schema': 3.13.2(rollup@4.24.0) execa: 7.2.0 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) transitivePeerDependencies: - magicast - rollup @@ -5173,13 +5428,61 @@ snapshots: rc9: 2.1.2 semver: 7.6.3 - '@nuxt/devtools@1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + '@nuxt/devtools@1.5.2(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@antfu/utils': 0.7.10 + '@nuxt/devtools-kit': 1.5.2(magicast@0.3.5)(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)) + '@nuxt/devtools-wizard': 1.5.2 + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + '@vue/devtools-core': 7.4.4(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@vue/devtools-kit': 7.4.4 + birpc: 0.2.19 + consola: 3.2.3 + cronstrue: 2.50.0 + destr: 2.0.3 + error-stack-parser-es: 0.1.5 + execa: 7.2.0 + fast-npm-meta: 0.2.2 + flatted: 3.3.1 + get-port-please: 3.1.2 + hookable: 5.5.3 + image-meta: 0.2.1 + is-installed-globally: 1.0.0 + launch-editor: 2.9.1 + local-pkg: 0.5.0 + magicast: 0.3.5 + nypm: 0.3.12 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + rc9: 2.1.2 + scule: 1.3.0 + semver: 7.6.3 + simple-git: 3.27.0 + sirv: 2.0.4 + tinyglobby: 0.2.10 + unimport: 3.13.1(rollup@3.29.5) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vite-plugin-inspect: 0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)) + vite-plugin-vue-inspector: 5.1.3(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)) + which: 3.0.1 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - rollup + - supports-color + - utf-8-validate + - vue + - webpack-sources + + '@nuxt/devtools@1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.5.2(magicast@0.3.5)(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)) + '@nuxt/devtools-kit': 1.5.2(magicast@0.3.5)(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)) '@nuxt/devtools-wizard': 1.5.2 '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) - '@vue/devtools-core': 7.4.4(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@vue/devtools-core': 7.4.4(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) '@vue/devtools-kit': 7.4.4 birpc: 0.2.19 consola: 3.2.3 @@ -5206,11 +5509,11 @@ snapshots: semver: 7.6.3 simple-git: 3.27.0 sirv: 2.0.4 - tinyglobby: 0.2.9 + tinyglobby: 0.2.10 unimport: 3.13.1(rollup@4.24.0) - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) - vite-plugin-inspect: 0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.24.0))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)) - vite-plugin-vue-inspector: 5.1.3(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) + vite-plugin-inspect: 0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)) + vite-plugin-vue-inspector: 5.1.3(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)) which: 3.0.1 ws: 8.18.0 transitivePeerDependencies: @@ -5253,6 +5556,34 @@ snapshots: - supports-color - typescript + '@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5)': + dependencies: + '@nuxt/schema': 3.13.2(rollup@3.29.5) + c12: 1.11.2(magicast@0.3.5) + consola: 3.2.3 + defu: 6.1.4 + destr: 2.0.3 + globby: 14.0.2 + hash-sum: 2.0.0 + ignore: 5.3.2 + jiti: 1.21.6 + klona: 2.0.6 + knitwork: 1.1.0 + mlly: 1.7.2 + pathe: 1.1.2 + pkg-types: 1.2.1 + scule: 1.3.0 + semver: 7.6.3 + ufo: 1.5.4 + unctx: 2.3.1 + unimport: 3.13.1(rollup@3.29.5) + untyped: 1.5.1 + transitivePeerDependencies: + - magicast + - rollup + - supports-color + - webpack-sources + '@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.24.0)': dependencies: '@nuxt/schema': 3.13.2(rollup@4.24.0) @@ -5281,9 +5612,9 @@ snapshots: - supports-color - webpack-sources - '@nuxt/module-builder@0.8.4(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.24.0))(nuxi@3.14.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))': + '@nuxt/module-builder@0.8.4(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(nuxi@3.14.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))': dependencies: - '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) citty: 0.1.6 consola: 3.2.3 defu: 6.1.4 @@ -5301,6 +5632,25 @@ snapshots: - vue-tsc - webpack-sources + '@nuxt/schema@3.13.2(rollup@3.29.5)': + dependencies: + compatx: 0.1.8 + consola: 3.2.3 + defu: 6.1.4 + hookable: 5.5.3 + pathe: 1.1.2 + pkg-types: 1.2.1 + scule: 1.3.0 + std-env: 3.7.0 + ufo: 1.5.4 + uncrypto: 0.1.3 + unimport: 3.13.1(rollup@3.29.5) + untyped: 1.5.1 + transitivePeerDependencies: + - rollup + - supports-color + - webpack-sources + '@nuxt/schema@3.13.2(rollup@4.24.0)': dependencies: compatx: 0.1.8 @@ -5320,9 +5670,9 @@ snapshots: - supports-color - webpack-sources - '@nuxt/telemetry@2.6.0(magicast@0.3.5)(rollup@4.24.0)': + '@nuxt/telemetry@2.6.0(magicast@0.3.5)(rollup@3.29.5)': dependencies: - '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) ci-info: 4.0.0 consola: 3.2.3 create-require: 1.1.1 @@ -5346,25 +5696,51 @@ snapshots: - supports-color - webpack-sources - '@nuxt/test-utils@3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3))': + '@nuxt/telemetry@2.6.0(magicast@0.3.5)(rollup@4.24.0)': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) - '@nuxt/schema': 3.13.2(rollup@4.24.0) - c12: 2.0.1(magicast@0.3.5) + ci-info: 4.0.0 consola: 3.2.3 + create-require: 1.1.1 defu: 6.1.4 destr: 2.0.3 - estree-walker: 3.0.3 - fake-indexeddb: 6.0.0 - get-port-please: 3.1.2 - h3: 1.13.0 - local-pkg: 0.5.0 - magic-string: 0.30.12 - nitropack: 2.9.7(magicast@0.3.5) - node-fetch-native: 1.6.4 + dotenv: 16.4.5 + git-url-parse: 15.0.0 + is-docker: 3.0.0 + jiti: 1.21.6 + mri: 1.2.0 + nanoid: 5.0.7 ofetch: 1.4.1 + package-manager-detector: 0.2.2 + parse-git-config: 3.0.0 pathe: 1.1.2 - perfect-debounce: 1.0.0 + rc9: 2.1.2 + std-env: 3.7.0 + transitivePeerDependencies: + - magicast + - rollup + - supports-color + - webpack-sources + + '@nuxt/test-utils@3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + '@nuxt/schema': 3.13.2(rollup@3.29.5) + c12: 2.0.1(magicast@0.3.5) + consola: 3.2.3 + defu: 6.1.4 + destr: 2.0.3 + estree-walker: 3.0.3 + fake-indexeddb: 6.0.0 + get-port-please: 3.1.2 + h3: 1.13.0 + local-pkg: 0.5.0 + magic-string: 0.30.12 + nitropack: 2.9.7(magicast@0.3.5) + node-fetch-native: 1.6.4 + ofetch: 1.4.1 + pathe: 1.1.2 + perfect-debounce: 1.0.0 radix3: 1.1.2 scule: 1.3.0 std-env: 3.7.0 @@ -5372,24 +5748,83 @@ snapshots: ufo: 1.5.4 unenv: 1.10.0 unplugin: 1.14.1 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) - vitest-environment-nuxt: 1.0.1(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vitest-environment-nuxt: 1.0.1(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) vue: 3.5.11(typescript@5.6.3) vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) optionalDependencies: - vitest: 2.1.2(@types/node@22.7.5)(terser@5.34.1) + vitest: 2.1.2(@types/node@22.7.6)(terser@5.34.1) transitivePeerDependencies: - magicast - rollup - supports-color - webpack-sources - '@nuxt/vite-builder@3.13.2(@types/node@22.7.5)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3))': + '@nuxt/vite-builder@3.13.2(@types/node@22.7.6)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + '@rollup/plugin-replace': 5.0.7(rollup@3.29.5) + '@vitejs/plugin-vue': 5.1.4(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@vitejs/plugin-vue-jsx': 4.0.1(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + autoprefixer: 10.4.20(postcss@8.4.47) + clear: 0.1.0 + consola: 3.2.3 + cssnano: 7.0.6(postcss@8.4.47) + defu: 6.1.4 + esbuild: 0.23.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + externality: 1.0.2 + get-port-please: 3.1.2 + h3: 1.13.0 + knitwork: 1.1.0 + magic-string: 0.30.12 + mlly: 1.7.2 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + postcss: 8.4.47 + rollup-plugin-visualizer: 5.12.0(rollup@3.29.5) + std-env: 3.7.0 + strip-literal: 2.1.0 + ufo: 1.5.4 + unenv: 1.10.0 + unplugin: 1.14.1 + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vite-node: 2.1.2(@types/node@22.7.6)(terser@5.34.1) + vite-plugin-checker: 0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) + vue: 3.5.11(typescript@5.6.3) + vue-bundle-renderer: 2.1.1 + transitivePeerDependencies: + - '@biomejs/biome' + - '@types/node' + - eslint + - less + - lightningcss + - magicast + - meow + - optionator + - rollup + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - typescript + - vls + - vti + - vue-tsc + - webpack-sources + + '@nuxt/vite-builder@3.13.2(@types/node@22.8.1)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3))': dependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) '@rollup/plugin-replace': 5.0.7(rollup@4.24.0) - '@vitejs/plugin-vue': 5.1.4(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) - '@vitejs/plugin-vue-jsx': 4.0.1(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@vitejs/plugin-vue': 5.1.4(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@vitejs/plugin-vue-jsx': 4.0.1(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) autoprefixer: 10.4.20(postcss@8.4.47) clear: 0.1.0 consola: 3.2.3 @@ -5415,9 +5850,9 @@ snapshots: ufo: 1.5.4 unenv: 1.10.0 unplugin: 1.14.1 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) - vite-node: 2.1.2(@types/node@22.7.5)(terser@5.34.1) - vite-plugin-checker: 0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) + vite-node: 2.1.2(@types/node@22.8.1)(terser@5.34.1) + vite-plugin-checker: 0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) vue: 3.5.11(typescript@5.6.3) vue-bundle-renderer: 2.1.1 transitivePeerDependencies: @@ -5692,11 +6127,15 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.8.1 '@types/json-schema@7.0.15': {} - '@types/node@22.7.5': + '@types/node@22.7.6': + dependencies: + undici-types: 6.19.8 + + '@types/node@22.8.1': dependencies: undici-types: 6.19.8 @@ -5831,19 +6270,34 @@ snapshots: - encoding - supports-color - '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': dependencies: '@babel/core': 7.25.8 '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.25.8) - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) vue: 3.5.11(typescript@5.6.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': dependencies: - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + '@babel/core': 7.25.8 + '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) + '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.25.8) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) + vue: 3.5.11(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + dependencies: + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vue: 3.5.11(typescript@5.6.3) + + '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + dependencies: + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) vue: 3.5.11(typescript@5.6.3) '@vitest/expect@2.1.2': @@ -5853,13 +6307,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) '@vitest/pretty-format@2.1.2': dependencies: @@ -5898,6 +6352,19 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.0.8 + '@vue-macros/common@1.14.0(rollup@3.29.5)(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@babel/types': 7.25.8 + '@rollup/pluginutils': 5.1.2(rollup@3.29.5) + '@vue/compiler-sfc': 3.5.11 + ast-kit: 1.2.1 + local-pkg: 0.5.0 + magic-string-ast: 0.6.2 + optionalDependencies: + vue: 3.5.11(typescript@5.6.3) + transitivePeerDependencies: + - rollup + '@vue-macros/common@1.14.0(rollup@4.24.0)(vue@3.5.11(typescript@5.6.3))': dependencies: '@babel/types': 7.25.8 @@ -5978,14 +6445,26 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/devtools-core@7.4.4(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + '@vue/devtools-core@7.4.4(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': dependencies: '@vue/devtools-kit': 7.4.4 '@vue/devtools-shared': 7.4.6 mitt: 3.0.1 nanoid: 3.3.7 pathe: 1.1.2 - vite-hot-client: 0.2.3(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)) + vite-hot-client: 0.2.3(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)) + vue: 3.5.11(typescript@5.6.3) + transitivePeerDependencies: + - vite + + '@vue/devtools-core@7.4.4(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@vue/devtools-kit': 7.4.4 + '@vue/devtools-shared': 7.4.6 + mitt: 3.0.1 + nanoid: 3.3.7 + pathe: 1.1.2 + vite-hot-client: 0.2.3(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)) vue: 3.5.11(typescript@5.6.3) transitivePeerDependencies: - vite @@ -6367,8 +6846,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + color-support@1.1.3: {} + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + colord@2.9.3: {} colorette@2.0.20: {} @@ -7032,6 +7521,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7241,6 +7734,12 @@ snapshots: html-tags@3.3.1: {} + http-client-hints@0.0.1(h3@1.13.0): + dependencies: + detect-browser-es: 0.1.1 + optionalDependencies: + h3: 1.13.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -7275,6 +7774,17 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + impound@0.1.0(rollup@3.29.5): + dependencies: + '@rollup/pluginutils': 5.1.2(rollup@3.29.5) + mlly: 1.7.2 + pathe: 1.1.2 + unenv: 1.10.0 + unplugin: 1.14.1 + transitivePeerDependencies: + - rollup + - webpack-sources + impound@0.1.0(rollup@4.24.0): dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.24.0) @@ -7319,6 +7829,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -7610,7 +8122,7 @@ snapshots: postcss: 8.4.47 postcss-nested: 6.2.0(postcss@8.4.47) semver: 7.6.3 - tinyglobby: 0.2.9 + tinyglobby: 0.2.10 optionalDependencies: typescript: 5.6.3 vue-tsc: 2.1.6(typescript@5.6.3) @@ -7780,14 +8292,127 @@ snapshots: nuxi@3.14.0: {} - nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.7.5)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): + nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.7.6)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): + dependencies: + '@nuxt/devalue': 2.0.2 + '@nuxt/devtools': 1.5.2(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + '@nuxt/schema': 3.13.2(rollup@3.29.5) + '@nuxt/telemetry': 2.6.0(magicast@0.3.5)(rollup@3.29.5) + '@nuxt/vite-builder': 3.13.2(@types/node@22.7.6)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@3.29.5)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) + '@unhead/dom': 1.11.7 + '@unhead/shared': 1.11.7 + '@unhead/ssr': 1.11.7 + '@unhead/vue': 1.11.7(vue@3.5.11(typescript@5.6.3)) + '@vue/shared': 3.5.11 + acorn: 8.12.1 + c12: 1.11.2(magicast@0.3.5) + chokidar: 3.6.0 + compatx: 0.1.8 + consola: 3.2.3 + cookie-es: 1.2.2 + defu: 6.1.4 + destr: 2.0.3 + devalue: 5.1.1 + errx: 0.1.0 + esbuild: 0.23.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + globby: 14.0.2 + h3: 1.13.0 + hookable: 5.5.3 + ignore: 5.3.2 + impound: 0.1.0(rollup@3.29.5) + jiti: 1.21.6 + klona: 2.0.6 + knitwork: 1.1.0 + magic-string: 0.30.12 + mlly: 1.7.2 + nanotar: 0.1.1 + nitropack: 2.9.7(magicast@0.3.5) + nuxi: 3.14.0 + nypm: 0.3.12 + ofetch: 1.4.1 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + radix3: 1.1.2 + scule: 1.3.0 + semver: 7.6.3 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinyglobby: 0.2.6 + ufo: 1.5.4 + ultrahtml: 1.5.3 + uncrypto: 0.1.3 + unctx: 2.3.1 + unenv: 1.10.0 + unhead: 1.11.7 + unimport: 3.13.1(rollup@3.29.5) + unplugin: 1.14.1 + unplugin-vue-router: 0.10.8(rollup@3.29.5)(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) + unstorage: 1.12.0(ioredis@5.4.1) + untyped: 1.5.1 + vue: 3.5.11(typescript@5.6.3) + vue-bundle-renderer: 2.1.1 + vue-devtools-stub: 0.1.0 + vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) + optionalDependencies: + '@parcel/watcher': 2.4.1 + '@types/node': 22.7.6 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@biomejs/biome' + - '@capacitor/preferences' + - '@libsql/client' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/kv' + - better-sqlite3 + - bufferutil + - drizzle-orm + - encoding + - eslint + - idb-keyval + - ioredis + - less + - lightningcss + - magicast + - meow + - optionator + - rollup + - sass + - sass-embedded + - stylelint + - stylus + - sugarss + - supports-color + - terser + - typescript + - uWebSockets.js + - utf-8-validate + - vite + - vls + - vti + - vue-tsc + - webpack-sources + - xml2js + + nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.1)(eslint@9.12.0(jiti@2.3.3))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) + '@nuxt/devtools': 1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue@3.5.11(typescript@5.6.3)) '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) '@nuxt/schema': 3.13.2(rollup@4.24.0) '@nuxt/telemetry': 2.6.0(magicast@0.3.5)(rollup@4.24.0) - '@nuxt/vite-builder': 3.13.2(@types/node@22.7.5)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) + '@nuxt/vite-builder': 3.13.2(@types/node@22.8.1)(eslint@9.12.0(jiti@2.3.3))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.0)(terser@5.34.1)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3))(vue@3.5.11(typescript@5.6.3)) '@unhead/dom': 1.11.7 '@unhead/shared': 1.11.7 '@unhead/ssr': 1.11.7 @@ -7848,7 +8473,7 @@ snapshots: vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) optionalDependencies: '@parcel/watcher': 2.4.1 - '@types/node': 22.7.5 + '@types/node': 22.8.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8348,6 +8973,15 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.25.7 + rollup-plugin-visualizer@5.12.0(rollup@3.29.5): + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 3.29.5 + rollup-plugin-visualizer@5.12.0(rollup@4.24.0): dependencies: open: 8.4.2 @@ -8446,6 +9080,32 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -8468,6 +9128,10 @@ snapshots: transitivePeerDependencies: - supports-color + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.28 @@ -8653,12 +9317,12 @@ snapshots: tinyexec@0.3.0: {} - tinyglobby@0.2.6: + tinyglobby@0.2.10: dependencies: - fdir: 6.4.0(picomatch@4.0.2) + fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 - tinyglobby@0.2.9: + tinyglobby@0.2.6: dependencies: fdir: 6.4.0(picomatch@4.0.2) picomatch: 4.0.2 @@ -8782,6 +9446,25 @@ snapshots: unicorn-magic@0.1.0: {} + unimport@3.13.1(rollup@3.29.5): + dependencies: + '@rollup/pluginutils': 5.1.2(rollup@3.29.5) + acorn: 8.12.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.12 + mlly: 1.7.2 + pathe: 1.1.2 + pkg-types: 1.2.1 + scule: 1.3.0 + strip-literal: 2.1.0 + unplugin: 1.14.1 + transitivePeerDependencies: + - rollup + - webpack-sources + unimport@3.13.1(rollup@4.24.0): dependencies: '@rollup/pluginutils': 5.1.2(rollup@4.24.0) @@ -8803,6 +9486,29 @@ snapshots: universalify@2.0.1: {} + unplugin-vue-router@0.10.8(rollup@3.29.5)(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): + dependencies: + '@babel/types': 7.25.8 + '@rollup/pluginutils': 5.1.2(rollup@3.29.5) + '@vue-macros/common': 1.14.0(rollup@3.29.5)(vue@3.5.11(typescript@5.6.3)) + ast-walker-scope: 0.6.2 + chokidar: 3.6.0 + fast-glob: 3.3.2 + json5: 2.2.3 + local-pkg: 0.5.0 + magic-string: 0.30.12 + mlly: 1.7.2 + pathe: 1.1.2 + scule: 1.3.0 + unplugin: 1.14.1 + yaml: 2.5.1 + optionalDependencies: + vue-router: 4.4.5(vue@3.5.11(typescript@5.6.3)) + transitivePeerDependencies: + - rollup + - vue + - webpack-sources + unplugin-vue-router@0.10.8(rollup@4.24.0)(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): dependencies: '@babel/types': 7.25.8 @@ -8896,16 +9602,37 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-hot-client@0.2.3(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)): + vite-hot-client@0.2.3(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)): dependencies: - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + + vite-hot-client@0.2.3(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)): + dependencies: + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) + + vite-node@2.1.2(@types/node@22.7.6)(terser@5.34.1): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + pathe: 1.1.2 + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser - vite-node@2.1.2(@types/node@22.7.5)(terser@5.34.1): + vite-node@2.1.2(@types/node@22.8.1)(terser@5.34.1): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) transitivePeerDependencies: - '@types/node' - less @@ -8917,7 +9644,30 @@ snapshots: - supports-color - terser - vite-plugin-checker@0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): + vite-plugin-checker@0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): + dependencies: + '@babel/code-frame': 7.25.7 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + chokidar: 3.6.0 + commander: 8.3.0 + fast-glob: 3.3.2 + fs-extra: 11.2.0 + npm-run-path: 4.0.1 + strip-ansi: 6.0.1 + tiny-invariant: 1.3.3 + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vscode-languageclient: 7.0.0 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + eslint: 9.12.0(jiti@2.3.3) + optionator: 0.9.4 + typescript: 5.6.3 + vue-tsc: 2.1.6(typescript@5.6.3) + + vite-plugin-checker@0.8.0(eslint@9.12.0(jiti@2.3.3))(optionator@0.9.4)(typescript@5.6.3)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): dependencies: '@babel/code-frame': 7.25.7 ansi-escapes: 4.3.2 @@ -8929,7 +9679,7 @@ snapshots: npm-run-path: 4.0.1 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -8940,7 +9690,25 @@ snapshots: typescript: 5.6.3 vue-tsc: 2.1.6(typescript@5.6.3) - vite-plugin-inspect@0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.24.0))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)): + vite-plugin-inspect@0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.2(rollup@3.29.5) + debug: 4.3.7 + error-stack-parser-es: 0.1.5 + fs-extra: 11.2.0 + open: 10.1.0 + perfect-debounce: 1.0.0 + picocolors: 1.1.0 + sirv: 2.0.4 + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + optionalDependencies: + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-inspect@0.8.7(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@3.29.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.2(rollup@4.24.0) @@ -8951,14 +9719,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.0 sirv: 2.0.4 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) optionalDependencies: - '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.0) + '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-vue-inspector@5.1.3(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)): + vite-plugin-vue-inspector@5.1.3(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)): dependencies: '@babel/core': 7.25.8 '@babel/plugin-proposal-decorators': 7.25.7(@babel/core@7.25.8) @@ -8969,23 +9737,48 @@ snapshots: '@vue/compiler-dom': 3.5.11 kolorist: 1.8.0 magic-string: 0.30.12 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) transitivePeerDependencies: - supports-color - vite@5.4.8(@types/node@22.7.5)(terser@5.34.1): + vite-plugin-vue-inspector@5.1.3(vite@5.4.8(@types/node@22.8.1)(terser@5.34.1)): + dependencies: + '@babel/core': 7.25.8 + '@babel/plugin-proposal-decorators': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-import-attributes': 7.25.7(@babel/core@7.25.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.8) + '@babel/plugin-transform-typescript': 7.25.7(@babel/core@7.25.8) + '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.25.8) + '@vue/compiler-dom': 3.5.11 + kolorist: 1.8.0 + magic-string: 0.30.12 + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) + transitivePeerDependencies: + - supports-color + + vite@5.4.8(@types/node@22.7.6)(terser@5.34.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.24.0 + optionalDependencies: + '@types/node': 22.7.6 + fsevents: 2.3.3 + terser: 5.34.1 + + vite@5.4.8(@types/node@22.8.1)(terser@5.34.1): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.8.1 fsevents: 2.3.3 terser: 5.34.1 - vitest-environment-nuxt@1.0.1(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): + vitest-environment-nuxt@1.0.1(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)): dependencies: - '@nuxt/test-utils': 3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) + '@nuxt/test-utils': 3.14.3(h3@1.13.0)(magicast@0.3.5)(nitropack@2.9.7(magicast@0.3.5))(rollup@3.29.5)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1))(vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1))(vue-router@4.4.5(vue@3.5.11(typescript@5.6.3)))(vue@3.5.11(typescript@5.6.3)) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' @@ -9007,10 +9800,10 @@ snapshots: - vue-router - webpack-sources - vitest@2.1.2(@types/node@22.7.5)(terser@5.34.1): + vitest@2.1.2(@types/node@22.7.6)(terser@5.34.1): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)(terser@5.34.1)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.6)(terser@5.34.1)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -9025,11 +9818,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.7.5)(terser@5.34.1) - vite-node: 2.1.2(@types/node@22.7.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) + vite-node: 2.1.2(@types/node@22.7.6)(terser@5.34.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 transitivePeerDependencies: - less - lightningcss diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..c48801d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - playground/ diff --git a/src/module.ts b/src/module.ts index 6f647c8..fa00941 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,9 +1,9 @@ import { createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import type { HookResult } from '@nuxt/schema' +import type { HttpClientHintsState } from 'http-client-hints' import { version } from '../package.json' import type { HttpClientHintsOptions as ModuleOptions } from './types' import { configure } from './utils/configuration' -import type { HttpClientHintsState } from './runtime/shared-types/types' export type { ModuleOptions } @@ -33,13 +33,12 @@ export default defineNuxtModule({ logger: useLogger(`nuxt:${NAME}`), options, resolvedOptions: { - detectBrowser: false, - detectOS: false, + detectBrowser: options.detectBrowser ?? false, + detectOS: options.detectOS ?? false, userAgent: [], network: [], device: [], }, - clientDependsOn: [], serverDependsOn: [], }, nuxt, diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 9f4ab95..4d7b6e2 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -1,346 +1,37 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { - ResolvedHttpClientHintsOptions, - CriticalInfo, - CriticalClientHintsConfiguration, -} from '../shared-types/types' -import { useHttpClientHintsState } from './state' -import { lookupHeader, writeClientHintHeaders, writeHeaders } from './headers' -import { browserFeatureAvailable } from './features' -import { - defineNuxtPlugin, - useCookie, - useRuntimeConfig, - useRequestHeaders, -} from '#imports' +import type { parseUserAgent } from 'detect-browser-es' +import { CriticalHintsHeaders, extractCriticalHints } from 'http-client-hints/critical' +import type { ResolvedHttpClientHintsOptions } from 'http-client-hints' +import { writeHeaders } from './headers' +import { useHttpClientHintsState } from './utils' +import { defineNuxtPlugin, useCookie, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -const AcceptClientHintsHeaders = { - prefersColorScheme: 'Sec-CH-Prefers-Color-Scheme', - prefersReducedMotion: 'Sec-CH-Prefers-Reduced-Motion', - prefersReducedTransparency: 'Sec-CH-Prefers-Reduced-Transparency', - viewportHeight: 'Sec-CH-Viewport-Height', - viewportWidth: 'Sec-CH-Viewport-Width', - width: 'Sec-CH-Width', - devicePixelRatio: 'Sec-CH-DPR', -} - -type AcceptClientHintsHeadersKey = keyof typeof AcceptClientHintsHeaders - -const AcceptClientHintsRequestHeaders = Object.entries(AcceptClientHintsHeaders).reduce((acc, [key, value]) => { - acc[key as AcceptClientHintsHeadersKey] = value.toLowerCase() as Lowercase - return acc -}, {} as Record>) - -const SecChUaMobile = 'Sec-CH-UA-Mobile'.toLowerCase() as Lowercase -const HttpRequestHeaders = Array.from(Object.values(AcceptClientHintsRequestHeaders)).concat('user-agent', 'cookie', SecChUaMobile) - const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:critical-server:plugin', - enforce: 'pre', + enforce: 'post', parallel: true, // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], async setup(nuxtApp) { + const ssrContext = nuxtApp.ssrContext! + const httpClientHints = ssrContext._httpClientHintsOptions as ResolvedHttpClientHintsOptions + const userAgent = ssrContext._httpClientHintsUserAgent as ReturnType const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions - const requestHeaders = useRequestHeaders(HttpRequestHeaders) - - // 1. extract browser info - const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - // 2. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.critical!, requestHeaders) - // 3. write client hints response headers - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.critical!) - state.value.critical = clientHintsRequest - // 4. send the theme cookie to the client when required - state.value.critical.colorSchemeCookie = writeThemeCookie( - clientHintsRequest, - httpClientHints.critical!, + const requestHeaders = useRequestHeaders(CriticalHintsHeaders) + state.value.critical = extractCriticalHints( + httpClientHints, + requestHeaders, + userAgent, + writeHeaders, + (cookieName, path, expires, themeName) => { + useCookie(cookieName, { + path, + expires, + sameSite: 'lax', + }).value = themeName + }, ) }, }) export default plugin - -type BrowserFeatureAvailable = (android: boolean, versions: number[]) => boolean -type BrowserFeatures = Record - -// Tests for Browser compatibility -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion#browser_compatibility -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Transparency -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme#browser_compatibility -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DPR#browser_compatibility -const chromiumBasedBrowserFeatures: BrowserFeatures = { - prefersColorScheme: (_, v) => v[0] >= 93, - prefersReducedMotion: (_, v) => v[0] >= 108, - prefersReducedTransparency: (_, v) => v[0] >= 119, - viewportHeight: (_, v) => v[0] >= 108, - viewportWidth: (_, v) => v[0] >= 108, - // TODO: check if this is correct, no entry in mozilla docs, using DPR - width: (_, v) => v[0] >= 46, - devicePixelRatio: (_, v) => v[0] >= 46, -} -const allowedBrowsers: [browser: Browser, features: BrowserFeatures][] = [ - // 'edge', - // 'edge-ios', - ['chrome', chromiumBasedBrowserFeatures], - ['edge-chromium', { - ...chromiumBasedBrowserFeatures, - devicePixelRatio: (_, v) => v[0] >= 79, - }], - ['chromium-webview', chromiumBasedBrowserFeatures], - ['opera', { - prefersColorScheme: (android, v) => v[0] >= (android ? 66 : 79), - prefersReducedMotion: (android, v) => v[0] >= (android ? 73 : 94), - prefersReducedTransparency: (_, v) => v[0] >= 79, - viewportHeight: (android, v) => v[0] >= (android ? 73 : 94), - viewportWidth: (android, v) => v[0] >= (android ? 73 : 94), - // TODO: check if this is correct, no entry in mozilla docs, using DPR - width: (_, v) => v[0] >= 33, - devicePixelRatio: (_, v) => v[0] >= 33, - }], -] - -const ClientHeaders = ['Accept-CH', 'Vary', 'Critical-CH'] - -function lookupClientHints( - userAgent: ReturnType, - criticalClientHintsConfiguration: CriticalClientHintsConfiguration, - headers: { [key in Lowercase]?: string | undefined }, -) { - const features: CriticalInfo = { - firstRequest: true, - prefersColorSchemeAvailable: false, - prefersReducedMotionAvailable: false, - prefersReducedTransparencyAvailable: false, - viewportHeightAvailable: false, - viewportWidthAvailable: false, - widthAvailable: false, - devicePixelRatioAvailable: false, - } - - if (userAgent == null || userAgent.type !== 'browser') - return features - - if (criticalClientHintsConfiguration.prefersColorScheme) - features.prefersColorSchemeAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'prefersColorScheme') - - if (criticalClientHintsConfiguration.prefersReducedMotion) - features.prefersReducedMotionAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'prefersReducedMotion') - - if (criticalClientHintsConfiguration.prefersReducedTransparency) - features.prefersReducedMotionAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'prefersReducedTransparency') - - if (criticalClientHintsConfiguration.viewportSize) { - features.viewportHeightAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'viewportHeight') - features.viewportWidthAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'viewportWidth') - } - - if (criticalClientHintsConfiguration.width) { - features.widthAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'width') - } - - if (features.viewportWidthAvailable || features.viewportHeightAvailable) { - // We don't need to include DPR on desktop browsers. - // Since sec-ch-ua-mobile is a low entropy header, we don't need to include it in Accept-CH, - // the user agent will send it always unless blocked by a user agent permission policy, check: - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA-Mobile - const mobileHeader = lookupHeader( - 'boolean', - SecChUaMobile, - headers, - ) - if (mobileHeader) - features.devicePixelRatioAvailable = browserFeatureAvailable(allowedBrowsers, userAgent, 'devicePixelRatio') - } - - return features -} - -function collectClientHints( - userAgent: ReturnType, - criticalClientHintsConfiguration: CriticalClientHintsConfiguration, - headers: { [key in Lowercase]?: string | undefined }, -) { - // collect client hints - const hints = lookupClientHints(userAgent, criticalClientHintsConfiguration, headers) - - if (criticalClientHintsConfiguration.prefersColorScheme) { - if (criticalClientHintsConfiguration.prefersColorSchemeOptions) { - const cookieName = criticalClientHintsConfiguration.prefersColorSchemeOptions.cookieName - const cookieValue = headers.cookie?.split(';').find(c => c.trim().startsWith(`${cookieName}=`)) - if (cookieValue) { - const value = cookieValue.split('=')?.[1].trim() - if (criticalClientHintsConfiguration.prefersColorSchemeOptions.themeNames.includes(value)) { - hints.colorSchemeFromCookie = value - hints.firstRequest = false - } - } - } - if (!hints.colorSchemeFromCookie) { - const value = hints.prefersColorSchemeAvailable - ? headers[AcceptClientHintsRequestHeaders.prefersColorScheme]?.toLowerCase() - : undefined - if (value === 'dark' || value === 'light' || value === 'no-preference') { - hints.prefersColorScheme = value - hints.firstRequest = false - } - - // update the color scheme cookie - if (criticalClientHintsConfiguration.prefersColorSchemeOptions) { - if (!value || value === 'no-preference') { - hints.colorSchemeFromCookie = criticalClientHintsConfiguration.prefersColorSchemeOptions.defaultTheme - } - else { - hints.colorSchemeFromCookie = value === 'dark' - ? criticalClientHintsConfiguration.prefersColorSchemeOptions.darkThemeName - : criticalClientHintsConfiguration.prefersColorSchemeOptions.lightThemeName - } - } - } - } - - if (hints.prefersReducedMotionAvailable && criticalClientHintsConfiguration.prefersReducedMotion) { - const value = headers[AcceptClientHintsRequestHeaders.prefersReducedMotion]?.toLowerCase() - if (value === 'no-preference' || value === 'reduce') { - hints.prefersReducedMotion = value - hints.firstRequest = false - } - } - - if (hints.prefersReducedTransparencyAvailable && criticalClientHintsConfiguration.prefersReducedTransparency) { - const value = headers[AcceptClientHintsRequestHeaders.prefersReducedTransparency]?.toLowerCase() - if (value) { - hints.prefersReducedTransparency = value === 'reduce' ? 'reduce' : 'no-preference' - hints.firstRequest = false - } - } - - if (hints.viewportHeightAvailable && criticalClientHintsConfiguration.viewportSize) { - const viewportHeight = lookupHeader( - 'int', - AcceptClientHintsRequestHeaders.viewportHeight, - headers, - ) - if (typeof viewportHeight === 'number') { - hints.firstRequest = false - hints.viewportHeight = viewportHeight - } - else { - hints.viewportHeight = criticalClientHintsConfiguration.clientHeight - } - } - else { - hints.viewportHeight = criticalClientHintsConfiguration.clientHeight - } - - if (hints.viewportWidthAvailable && criticalClientHintsConfiguration.viewportSize) { - const viewportWidth = lookupHeader( - 'int', - AcceptClientHintsRequestHeaders.viewportWidth, - headers, - ) - if (typeof viewportWidth === 'number') { - hints.firstRequest = false - hints.viewportWidth = viewportWidth - } - else { - hints.viewportWidth = criticalClientHintsConfiguration.clientWidth - } - } - else { - hints.viewportWidth = criticalClientHintsConfiguration.clientWidth - } - - if (hints.devicePixelRatioAvailable && criticalClientHintsConfiguration.viewportSize) { - const devicePixelRatio = lookupHeader( - 'float', - AcceptClientHintsRequestHeaders.devicePixelRatio, - headers, - ) - if (typeof devicePixelRatio === 'number') { - hints.firstRequest = false - try { - hints.devicePixelRatio = devicePixelRatio - if (!Number.isNaN(devicePixelRatio) && devicePixelRatio > 0) { - if (typeof hints.viewportWidth === 'number') - hints.viewportWidth = Math.round(hints.viewportWidth / devicePixelRatio) - if (typeof hints.viewportHeight === 'number') - hints.viewportHeight = Math.round(hints.viewportHeight / devicePixelRatio) - } - } - catch { - // just ignore - } - } - } - - if (hints.widthAvailable && criticalClientHintsConfiguration.width) { - const width = lookupHeader( - 'int', - AcceptClientHintsRequestHeaders.width, - headers, - ) - if (typeof width === 'number') { - hints.firstRequest = false - hints.width = width - } - } - - return hints -} - -function writeClientHintsResponseHeaders( - criticalInfo: CriticalInfo, - criticalClientHintsConfiguration: CriticalClientHintsConfiguration, -) { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Critical-CH - // Each header listed in the Critical-CH header should also be present in the Accept-CH and Vary headers. - const headers: Record = {} - - if (criticalClientHintsConfiguration.prefersColorScheme && criticalInfo.prefersColorSchemeAvailable) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.prefersColorScheme, headers) - - if (criticalClientHintsConfiguration.prefersReducedMotion && criticalInfo.prefersReducedMotionAvailable) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.prefersReducedMotion, headers) - - if (criticalClientHintsConfiguration.prefersReducedTransparency && criticalInfo.prefersReducedTransparencyAvailable) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.prefersReducedTransparency, headers) - - if (criticalClientHintsConfiguration.viewportSize && criticalInfo.viewportHeightAvailable && criticalInfo.viewportWidthAvailable) { - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.viewportHeight, headers) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.viewportWidth, headers) - if (criticalInfo.devicePixelRatioAvailable) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.devicePixelRatio, headers) - } - - if (criticalClientHintsConfiguration.width && criticalInfo.widthAvailable) - writeClientHintHeaders(ClientHeaders, AcceptClientHintsHeaders.width, headers) - - writeHeaders(headers) -} - -function writeThemeCookie( - criticalInfo: CriticalInfo, - criticalClientHintsConfiguration: CriticalClientHintsConfiguration, -) { - if (!criticalClientHintsConfiguration.prefersColorScheme || !criticalClientHintsConfiguration.prefersColorSchemeOptions) - return - - const cookieName = criticalClientHintsConfiguration.prefersColorSchemeOptions.cookieName - const themeName = criticalInfo.colorSchemeFromCookie ?? criticalClientHintsConfiguration.prefersColorSchemeOptions.defaultTheme - const path = criticalClientHintsConfiguration.prefersColorSchemeOptions.baseUrl - - const date = new Date() - const expires = new Date(date.setDate(date.getDate() + 365)) - if (!criticalInfo.firstRequest || !criticalClientHintsConfiguration.reloadOnFirstRequest) { - useCookie(cookieName, { - path, - expires, - sameSite: 'lax', - }).value = themeName - } - - return `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=Lax` -} diff --git a/src/runtime/plugins/detect.client.ts b/src/runtime/plugins/detect.client.ts index d164fb9..f9952d4 100644 --- a/src/runtime/plugins/detect.client.ts +++ b/src/runtime/plugins/detect.client.ts @@ -1,5 +1,5 @@ import { browserName, detect, asyncDetect, detectOS, parseUserAgent } from 'detect-browser-es' -import type { UserAgentHints } from '../shared-types/types' +import type { UserAgentHints } from 'http-client-hints' import { defineNuxtPlugin } from '#imports' import type { Plugin } from '#app' diff --git a/src/runtime/plugins/detect.server.ts b/src/runtime/plugins/detect.server.ts index 4494101..d60f1db 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -4,43 +4,37 @@ import { detect, detectOS, parseUserAgent, - serverResponseHeadersForUserAgentHints, } from 'detect-browser-es' import { appendHeader } from 'h3' -import type { ResolvedHttpClientHintsOptions, UserAgentHints } from '../shared-types/types' -import { useHttpClientHintsState } from './state' +import type { ResolvedHttpClientHintsOptions, UserAgentHints } from 'http-client-hints' +import { extractBrowserHints } from 'http-client-hints/detect' +import { useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useNuxtApp, useRequestEvent, useRequestHeaders, - useRuntimeConfig, } from '#imports' import type { Plugin } from '#app' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:detect-server:plugin', - enforce: 'pre', + enforce: 'post', parallel: true, // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], - async setup() { + async setup(nuxtApp) { const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + const httpClientHints = nuxtApp.ssrContext!._httpClientHintsOptions as ResolvedHttpClientHintsOptions const requestHeaders = useRequestHeaders() const userAgentHeader = requestHeaders['user-agent'] - if (httpClientHints.detectOS === 'windows-11') { - const hintsSet = new Set(httpClientHints.userAgent) - // Windows 11 detection requires platformVersion hint - if (!hintsSet.has('platformVersion')) { - hintsSet.add('platformVersion') - } - const hints = Array.from(hintsSet) - // write headers - const headers = serverResponseHeadersForUserAgentHints(hints) - if (headers) { + const browser = await extractBrowserHints( + httpClientHints, + requestHeaders, + userAgentHeader, + (headers) => { const nuxtApp = useNuxtApp() const callback = () => { const event = useRequestEvent(nuxtApp) @@ -57,21 +51,11 @@ const plugin: Plugin = defineNuxtPlugin({ unhook() return callback() }) - } - // detect browser info - const browserInfo = await asyncDetect({ - hints, - httpHeaders: requestHeaders, - }) - if (browserInfo) { - state.value.browser = JSON.parse(JSON.stringify(browserInfo)) - } - } - else if (userAgentHeader) { - const browserInfo = detect(userAgentHeader) - if (browserInfo) { - state.value.browser = JSON.parse(JSON.stringify(browserInfo)) - } + }, + ) + + if (browser) { + state.value.browser = JSON.parse(JSON.stringify(browser)) } return { diff --git a/src/runtime/plugins/device.server.ts b/src/runtime/plugins/device.server.ts index caf4f95..a28cdd7 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -1,129 +1,25 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { - DeviceInfo, - DeviceHints, - ResolvedHttpClientHintsOptions, -} from '../shared-types/types' -import { useHttpClientHintsState } from './state' -import { type GetHeaderType, lookupHeader, writeClientHintHeaders, writeHeaders } from './headers' -import { browserFeatureAvailable } from './features' -import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#imports' +import type { parseUserAgent } from 'detect-browser-es' +import { extractDeviceHints, DeviceHintsHeaders } from 'http-client-hints/device' +import type { ResolvedHttpClientHintsOptions } from 'http-client-hints' +import { useHttpClientHintsState } from './utils' +import { writeHeaders } from './headers' +import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -const DeviceClientHintsHeaders: Record = { - memory: 'Device-Memory', -} - -const DeviceClientHintsHeadersTypes: Record = { - memory: 'float', -} - -type DeviceClientHintsHeadersKey = keyof typeof DeviceClientHintsHeaders - -const AcceptClientHintsRequestHeaders = Object.entries(DeviceClientHintsHeaders).reduce((acc, [key, value]) => { - acc[key as DeviceClientHintsHeadersKey] = value.toLowerCase() as Lowercase - return acc -}, {} as Record>) - -const HttpRequestHeaders = Array.from(Object.values(DeviceClientHintsHeaders)).concat('user-agent') - const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:device-server:plugin', - enforce: 'pre', + enforce: 'post', parallel: true, // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { + const ssrContext = nuxtApp.ssrContext! + const httpClientHints = ssrContext._httpClientHintsOptions as ResolvedHttpClientHintsOptions + const userAgent = ssrContext._httpClientHintsUserAgent as ReturnType const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions - const requestHeaders = useRequestHeaders(HttpRequestHeaders) - - // 1. extract browser info - const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - // 2. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.device!, requestHeaders) - // 3. write client hints response headers - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.device!) - state.value.device = clientHintsRequest + const requestHeaders = useRequestHeaders(DeviceHintsHeaders) + state.value.device = extractDeviceHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, }) export default plugin - -type BrowserFeatureAvailable = (android: boolean, versions: number[]) => boolean -type BrowserFeatures = Record - -// Tests for Browser compatibility -// https://developer.mozilla.org/en-US/docs/Web/API/Device_Memory_API -const chromiumBasedBrowserFeatures: BrowserFeatures = { - memory: (_, v) => v[0] >= 63, -} -const allowedBrowsers: [browser: Browser, features: BrowserFeatures][] = [ - ['chrome', chromiumBasedBrowserFeatures], - ['edge-chromium', { - memory: (_, v) => v[0] >= 79, - }], - ['chromium-webview', chromiumBasedBrowserFeatures], - ['opera', { - memory: (android, v) => v[0] >= (android ? 50 : 46), - }], -] - -const ClientHeaders = ['Accept-CH'] - -function lookupClientHints( - userAgent: ReturnType, - deviceHints: DeviceHints[], -): DeviceInfo { - const features: DeviceInfo = { - memoryAvailable: false, - } - - if (userAgent == null || userAgent.type !== 'browser') - return features - - for (const hint of deviceHints) { - features[`${hint}Available`] = browserFeatureAvailable(allowedBrowsers, userAgent, hint) - } - - return features -} - -function collectClientHints( - userAgent: ReturnType, - deviceHints: DeviceHints[], - headers: { [key in Lowercase]?: string | undefined }, -) { - // collect client hints - const hints = lookupClientHints(userAgent, deviceHints) - - for (const hint of deviceHints) { - if (hints[`${hint}Available`]) { - const value = lookupHeader( - DeviceClientHintsHeadersTypes[hint], - AcceptClientHintsRequestHeaders[hint], - headers, - ) - if (typeof value !== 'undefined') { - hints[hint] = value as typeof hints[typeof hint] - } - } - } - - return hints -} - -function writeClientHintsResponseHeaders( - deviceInfo: DeviceInfo, - deviceHints: DeviceHints[], -) { - const headers: Record = {} - - for (const hint of deviceHints) { - if (deviceInfo[`${hint}Available`]) { - writeClientHintHeaders(ClientHeaders, DeviceClientHintsHeaders[hint], headers) - } - } - - writeHeaders(headers) -} diff --git a/src/runtime/plugins/features.ts b/src/runtime/plugins/features.ts deleted file mode 100644 index a3c8834..0000000 --- a/src/runtime/plugins/features.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' - -export function browserFeatureAvailable( - allowedBrowsers: [browser: Browser, features: Record boolean>][], - userAgent: ReturnType, - feature: T, -) { - if (userAgent == null || userAgent.type !== 'browser') - return false - - try { - const browserName = userAgent.name - const android = userAgent.os?.toLowerCase().startsWith('android') ?? false - const versions = userAgent.version.split('.').map(v => Number.parseInt(v)) - return allowedBrowsers.some(([name, check]) => { - if (browserName !== name) - return false - - try { - return check[feature](android, versions) - } - catch { - return false - } - }) - } - catch { - return false - } -} diff --git a/src/runtime/plugins/headers.ts b/src/runtime/plugins/headers.ts index 5d0f15a..5d63ead 100644 --- a/src/runtime/plugins/headers.ts +++ b/src/runtime/plugins/headers.ts @@ -1,12 +1,6 @@ import { appendHeader } from 'h3' import { useNuxtApp, useRequestEvent } from '#imports' -export function writeClientHintHeaders(headerNames: string[], key: string, headers: Record) { - headerNames.forEach((header) => { - headers[header] = (headers[header] ? headers[header] : []).concat(key) - }) -} - export function writeHeaders(headers: Record) { if (Object.keys(headers).length === 0) return @@ -26,48 +20,3 @@ export function writeHeaders(headers: Record) { return callback() }) } - -export type GetHeaderType = 'string' | 'int' | 'float' | 'boolean' -type GetHeaderReturnType = T extends 'string' - ? string - : T extends 'int' - ? number - : T extends 'float' - ? number - : T extends 'boolean' - ? boolean - : never - -export function lookupHeader( - type: T, - key: Lowercase, - headers: { [key in Lowercase]?: string | undefined }, -): GetHeaderReturnType | undefined { - const value = headers[key] - if (!value) - return undefined - - if (type === 'string') - return value as GetHeaderReturnType - - if (type === 'int' || type === 'float') { - try { - const numberValue = type === 'int' - ? Number.parseInt(value) - : Number.parseFloat(value) - return Number.isNaN(numberValue) - ? undefined - : numberValue as GetHeaderReturnType - } - catch { - return undefined - } - } - - if (type === 'boolean') { - const booleanValue = value === '?1' - return booleanValue as GetHeaderReturnType - } - - return undefined -} diff --git a/src/runtime/plugins/init.server.ts b/src/runtime/plugins/init.server.ts index 6664e42..0667088 100644 --- a/src/runtime/plugins/init.server.ts +++ b/src/runtime/plugins/init.server.ts @@ -1,5 +1,5 @@ import { parseUserAgent } from 'detect-browser-es' -import { useHttpClientHintsState } from './state' +import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' @@ -12,6 +12,7 @@ const plugin: Plugin = defineNuxtPlugin({ const ssrContext = nuxtApp.ssrContext! const requestHeaders = useRequestHeaders(['user-agent']) const userAgentHeader = requestHeaders['user-agent'] + ssrContext._httpClientHintsOptions = useHttpClientHintsOptions() ssrContext._httpClientHintsUserAgent = userAgentHeader ? parseUserAgent(userAgentHeader) : null diff --git a/src/runtime/plugins/network.server.ts b/src/runtime/plugins/network.server.ts index 503b6da..4fa20ba 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -1,148 +1,25 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { NetworkInfo, NetworkHints, ResolvedHttpClientHintsOptions } from '../shared-types/types' -import { useHttpClientHintsState } from './state' -import type { GetHeaderType } from './headers' -import { lookupHeader, writeClientHintHeaders, writeHeaders } from './headers' -import { browserFeatureAvailable } from './features' -import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#imports' +import type { parseUserAgent } from 'detect-browser-es' +import { extractNetworkHints, NetworkHintsHeaders } from 'http-client-hints/network' +import type { ResolvedHttpClientHintsOptions } from 'http-client-hints' +import { useHttpClientHintsState } from './utils' +import { writeHeaders } from './headers' +import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -const NetworkClientHintsHeaders: Record = { - savedata: 'Save-Data', - downlink: 'Downlink', - ect: 'ECT', - rtt: 'RTT', -} - -const NetworkClientHintsHeadersTypes: Record = { - savedata: 'string', - downlink: 'float', - ect: 'string', - rtt: 'int', -} - -type NetworkClientHintsHeadersKey = keyof typeof NetworkClientHintsHeaders - -const AcceptClientHintsRequestHeaders = Object.entries(NetworkClientHintsHeaders).reduce((acc, [key, value]) => { - acc[key as NetworkClientHintsHeadersKey] = value.toLowerCase() as Lowercase - return acc -}, {} as Record>) - -const HttpRequestHeaders = Array.from(Object.values(NetworkClientHintsHeaders)).concat('user-agent') - const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:network-server:plugin', - enforce: 'pre', + enforce: 'post', parallel: true, // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { + const ssrContext = nuxtApp.ssrContext! + const httpClientHints = ssrContext._httpClientHintsOptions as ResolvedHttpClientHintsOptions + const userAgent = ssrContext._httpClientHintsUserAgent as ReturnType const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions - const requestHeaders = useRequestHeaders(HttpRequestHeaders) - - // 1. extract browser info - const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - // 2. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.network!, requestHeaders) - // 3. write client hints response headers - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.network!) - state.value.network = clientHintsRequest + const requestHeaders = useRequestHeaders(NetworkHintsHeaders) + state.value.network = extractNetworkHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, }) export default plugin - -type BrowserFeatureAvailable = (android: boolean, versions: number[]) => boolean -type BrowserFeatures = Record - -// Tests for Browser compatibility -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Downlink -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ECT -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/RTT -const chromiumBasedBrowserFeatures: BrowserFeatures = { - savedata: (android, v) => v[0] >= 49, - downlink: (_, v) => v[0] >= 67, - ect: (_, v) => v[0] >= 67, - rtt: (_, v) => v[0] >= 67, -} -const allowedBrowsers: [browser: Browser, features: BrowserFeatures][] = [ - ['chrome', chromiumBasedBrowserFeatures], - ['edge-chromium', { - savedata: (_, v) => v[0] >= 79, - downlink: (_, v) => v[0] >= 79, - ect: (_, v) => v[0] >= 79, - rtt: (_, v) => v[0] >= 79, - }], - ['chromium-webview', chromiumBasedBrowserFeatures], - ['opera', { - savedata: (_, v) => v[0] >= 35, - downlink: (android, v) => v[0] >= (android ? 48 : 54), - ect: (android, v) => v[0] >= (android ? 48 : 54), - rtt: (android, v) => v[0] >= (android ? 48 : 54), - }], -] - -const ClientHeaders = ['Accept-CH', 'Vary'] - -function lookupClientHints( - userAgent: ReturnType, - networkHints: NetworkHints[], -) { - const features: NetworkInfo = { - savedataAvailable: false, - downlinkAvailable: false, - ectAvailable: false, - rttAvailable: false, - } - - if (userAgent == null || userAgent.type !== 'browser') - return features - - for (const hint of networkHints) { - features[`${hint}Available`] = browserFeatureAvailable(allowedBrowsers, userAgent, hint) - } - - return features -} - -function collectClientHints( - userAgent: ReturnType, - networkHints: NetworkHints[], - headers: { [key in Lowercase]?: string | undefined }, -) { - // collect client hints - const hints = lookupClientHints(userAgent, networkHints) - - for (const hint of networkHints) { - if (hints[`${hint}Available`]) { - const value = lookupHeader( - NetworkClientHintsHeadersTypes[hint], - AcceptClientHintsRequestHeaders[hint], - headers, - ) - if (typeof value !== 'undefined') { - // @ts-expect-error Type 'number | "on" | NetworkECT | undefined' is not assignable to type 'undefined'. - hints[hint] = value as typeof hints[typeof hint] - } - } - } - - return hints -} - -function writeClientHintsResponseHeaders( - networkInfo: NetworkInfo, - networkHints: NetworkHints[], -) { - const headers: Record = {} - - for (const hint of networkHints) { - if (networkInfo[`${hint}Available`]) { - writeClientHintHeaders(ClientHeaders, NetworkClientHintsHeaders[hint], headers) - } - } - - writeHeaders(headers) -} diff --git a/src/runtime/plugins/state.ts b/src/runtime/plugins/state.ts deleted file mode 100644 index 4535a4b..0000000 --- a/src/runtime/plugins/state.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { HttpClientHintsState } from '../shared-types/types' -import { useState } from '#imports' - -export function useHttpClientHintsState() { - return useState('http-client-hints:state', () => ({})) -} diff --git a/src/runtime/plugins/types.d.ts b/src/runtime/plugins/types.d.ts index 7000e24..bcb7d80 100644 --- a/src/runtime/plugins/types.d.ts +++ b/src/runtime/plugins/types.d.ts @@ -1,6 +1,6 @@ import type { DeepReadonly } from '@vue/reactivity' import type { Browser, asyncDetect, detect, detectOS, parseUserAgent } from 'detect-browser-es' -import type { HttpClientHintsState, UserAgentDataHints } from '../shared-types/types' +import type { HttpClientHintsState, UserAgentDataHints } from 'http-client-hints' declare module '#app' { interface NuxtApp { diff --git a/src/runtime/plugins/utils.ts b/src/runtime/plugins/utils.ts new file mode 100644 index 0000000..11973e3 --- /dev/null +++ b/src/runtime/plugins/utils.ts @@ -0,0 +1,18 @@ +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, +} from 'http-client-hints' +import type { ServerHttpClientHintsOptions } from 'http-client-hints/h3' +import { useAppConfig, useState } from '#imports' + +export function useHttpClientHintsState() { + return useState('http-client-hints:state', () => ({})) +} + +export function useHttpClientHintsOptions(): ResolvedHttpClientHintsOptions { + const { serverImages, ...rest } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions + return { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } +} diff --git a/src/runtime/shared-types/types.ts b/src/runtime/shared-types/types.ts deleted file mode 100644 index e8b0a87..0000000 --- a/src/runtime/shared-types/types.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { Browser, DetectedInfoType, OperatingSystem, UserAgentDataInfo } from 'detect-browser-es' - -/** - * @see https://github.com/WICG/ua-client-hints - */ -export type UserAgentHints = 'architecture' | 'bitness' | 'model' | 'platformVersion' | 'fullVersionList' -/** - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#client_hints - */ -export type NetworkHints = 'savedata' | 'downlink' | 'ect' | 'rtt' -/** - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#client_hints - */ -export type DeviceHints = 'memory' - -export interface CriticalInfoFeatures { - firstRequest: boolean - prefersColorSchemeAvailable: boolean - prefersReducedMotionAvailable: boolean - prefersReducedTransparencyAvailable: boolean - viewportHeightAvailable: boolean - viewportWidthAvailable: boolean - widthAvailable: boolean - devicePixelRatioAvailable: boolean -} -export interface CriticalInfo extends CriticalInfoFeatures { - prefersColorScheme?: 'dark' | 'light' | 'no-preference' - prefersReducedMotion?: 'no-preference' | 'reduce' - prefersReducedTransparency?: 'no-preference' | 'reduce' - viewportHeight?: number - viewportWidth?: number - width?: number - devicePixelRatio?: number - colorSchemeFromCookie?: string - colorSchemeCookie?: string -} - -export interface DeviceInfoFeatures { - memoryAvailable: boolean -} -export interface DeviceInfo extends DeviceInfoFeatures { - memory?: number -} - -export interface NetworkInfoFeatures { - savedataAvailable: boolean - downlinkAvailable: boolean - ectAvailable: boolean - rttAvailable: boolean -} -export type NetworkECT = 'slow-2g' | '2g' | '3g' | '4g' -export interface NetworkInfo extends NetworkInfoFeatures { - savedata?: 'on' - downlink?: number - ect?: NetworkECT - rtt?: number -} - -export interface BrowserInfo { - type: DetectedInfoType - bot?: boolean - name: Browser - version?: string | null - os: OperatingSystem | null - ua?: UserAgentDataInfo | null -} - -export interface HttpClientHintsState { - browser?: BrowserInfo - device?: DeviceInfo - network?: NetworkInfo - userAgent?: UserAgentDataInfo - critical?: CriticalInfo -} - -export interface CriticalClientHintsConfiguration { - /** - * Should the module reload the page on first request? - * - * @default false - */ - reloadOnFirstRequest?: boolean - /** - * Enable `Sec-CH-Width` for images? - * @see https://wicg.github.io/responsive-image-client-hints/#sec-ch-width - * @default false - */ - width?: boolean - /** - * Enable `Sec-CH-Viewport-Width` and `Sec-CH-Viewport-Height` headers? - * @see https://wicg.github.io/responsive-image-client-hints/#sec-ch-viewport-width - * @see https://wicg.github.io/responsive-image-client-hints/#sec-ch-viewport-height - * @default false - */ - viewportSize?: boolean - /** - * Enable `Sec-CH-Prefers-Color-Scheme` header? - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme - * @default false - */ - prefersColorScheme?: boolean - /** - * Enable `Sec-CH-Prefers-Reduced-Motion` header? - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion - * @default false - */ - prefersReducedMotion?: boolean - /** - * Enable `Sec-CH-Prefers-Reduced-Transparency` header? - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Transparency - * @default false - */ - prefersReducedTransparency?: boolean - /** - * Default client width when missing headers. - */ - clientWidth?: number - /** - * Default client height when missing headers. - */ - clientHeight?: number - /** - * The options for `prefersColorScheme`, `prefersColorScheme` must be enabled. - * - * 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. - */ - prefersColorSchemeOptions?: { - /** - * The default base URL for the theme cookie. - * @default '/' - */ - baseUrl: string - /** - * The default theme name. - */ - defaultTheme: string - /** - * The available theme names. - */ - themeNames: string[] - /** - * The name for the cookie. - * - * @default 'color-scheme' - */ - cookieName: string - /** - * The name for the dark theme. - * - * @default 'dark' - */ - darkThemeName: string - /** - * The name for the light theme. - * - * @default 'light' - */ - lightThemeName: string - /** - * Use the browser theme only? - * - * This flag can be used when your application provides a custom dark and light themes, - * but will not provide a theme switcher, that's, using by default the browser theme. - * - * @default false - */ - useBrowserThemeOnly: boolean - } -} - -export interface ResolvedHttpClientHintsOptions { - detectBrowser: boolean - detectOS: boolean | 'windows-11' - userAgent: UserAgentHints[] - network: NetworkHints[] - device: DeviceHints[] - /** - * Critical Client Hints. - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints#critical_client_hints - */ - critical?: CriticalClientHintsConfiguration -} diff --git a/src/types.ts b/src/types.ts index 4f16ba5..7a3b88d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import type { NetworkHints, CriticalClientHintsConfiguration, UserAgentHints, -} from './runtime/shared-types/types' +} from 'http-client-hints' export type { DeviceHints, NetworkHints, CriticalClientHintsConfiguration, UserAgentHints } @@ -38,4 +38,12 @@ export interface HttpClientHintsOptions { * @see https://wicg.github.io/responsive-image-client-hints */ critical?: CriticalClientHintsConfiguration + /** + * Enable server images (expose Nitro event handler)?. + * + * The Nitro event handler will export the `httpClientHints` object in the event context. + * + * If set to `true`, the event handler will apply to `/\.(png|jpeg|jpg|webp|avi)$/`. + */ + serverImages?: true | RegExp | RegExp[] } diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 04285e4..f1f6148 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,8 +1,8 @@ import type { Nuxt } from '@nuxt/schema' import type { Resolver } from '@nuxt/kit' import { addPlugin, addPluginTemplate } from '@nuxt/kit' +import type { ResolvedHttpClientHintsOptions } from 'http-client-hints' import type { HttpClientHintsOptions } from '../types' -import type { ResolvedHttpClientHintsOptions } from '../runtime/shared-types/types' type PluginType = 'detect' | 'user-agent' | 'network' | 'device' | 'critical' @@ -11,7 +11,6 @@ export interface HttpClientHintsContext { logger: ReturnType options: HttpClientHintsOptions resolvedOptions: ResolvedHttpClientHintsOptions - clientDependsOn: PluginType[] serverDependsOn: PluginType[] } @@ -21,7 +20,6 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { resolvedOptions, resolver, logger, - clientDependsOn, serverDependsOn, } = ctx @@ -54,10 +52,10 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { const clientOnly = nuxt.options._generate || !nuxt.options.ssr // we register the client detector only if needed and not in SSR mode - if ((options.detectBrowser || options.detectOS || resolvedOptions.userAgent.length) && clientOnly) { + if ((resolvedOptions.detectBrowser || resolvedOptions.detectOS || resolvedOptions.userAgent.length) && clientOnly) { nuxt.options.build.transpile.push(runtimeDir) nuxt.hook('prepare:types', ({ references }) => { - references.push({ path: resolver.resolve(runtimeDir, 'plugins/types') }) + references.push({ path: resolver.resolve(runtimeDir, 'plugins/types.d.ts') }) }) addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.client')) return @@ -69,8 +67,6 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { return } - nuxt.options.build.transpile.push(runtimeDir) - if (network) { if (network === true) { resolvedOptions.network.push('savedata', 'downlink', 'ect', 'rtt') @@ -109,19 +105,21 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { } nuxt.hook('prepare:types', ({ references }) => { - references.push({ path: resolver.resolve(runtimeDir, 'plugins/types') }) + references.push({ path: resolver.resolve(runtimeDir, 'plugins/types.d.ts') }) }) if (options.detectOS) { resolvedOptions.detectOS = options.detectOS } - nuxt.options.runtimeConfig.public.httpClientHints = resolvedOptions + // transpile runtime + nuxt.options.build.transpile.push(runtimeDir) + // transpile http client hints plugins + nuxt.options.build.transpile.push(/\/http-client-hints\.(client|server)\.mjs$/) addPlugin(resolver.resolve(runtimeDir, 'plugins/init.server')) - if (options.detectBrowser || options.detectOS || resolvedOptions.userAgent.length) { - clientDependsOn.push('detect') + if (resolvedOptions.detectBrowser || resolvedOptions.detectOS || resolvedOptions.userAgent.length) { serverDependsOn.push('detect') addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.client')) addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.server')) @@ -139,17 +137,30 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { addPlugin(resolver.resolve(runtimeDir, 'plugins/critical.server')) } - if (clientDependsOn.length) { - // @ts-expect-error missing at build time - addClientHintsPlugin('client', clientDependsOn.map(p => `http-client-hints:${p}-client:plugin`)) + const serverImages = options.serverImages + + const useServerImages = serverImages + ? serverImages === true + ? [/\.(png|jpeg|jpg|webp|avif|tiff|gif)$/] + : Array.isArray(serverImages) + ? serverImages + : [serverImages] + : undefined + + const { serverImages: _, ...rest } = resolvedOptions + nuxt.options.appConfig.httpClientHints = { + ...rest, + serverImages: useServerImages ? useServerImages.map(r => r.source) : [], } + + addClientHintsPlugin('client') // @ts-expect-error missing at build time addClientHintsPlugin('server', serverDependsOn.map(p => `http-client-hints:${p}-server:plugin`)) } function addClientHintsPlugin( mode: 'client' | 'server', - dependsOn: import('#app').NuxtAppLiterals['pluginName'][], + dependsOn: import('#app').NuxtAppLiterals['pluginName'][] = [], ) { const name = `http-client-hints:${mode}:plugin` addPluginTemplate({ @@ -158,12 +169,15 @@ function addClientHintsPlugin( mode: `${mode}`, write: false, getContents() { + const dependsOnString = dependsOn.length + ? ` + dependsOn: ${JSON.stringify(dependsOn)},` + : '' return `import { defineNuxtPlugin, readonly, useState } from '#imports' export default defineNuxtPlugin({ name: '${name}', - order: 'pre', - dependsOn: ${JSON.stringify(dependsOn)}, - parallel: false, + enforce: 'post', + parallel: false,${dependsOnString} async setup(nuxtApp) { const clientHints = useState('http-client-hints:state') await nuxtApp.hooks.callHook(