From 835b9b1ebaf77085856fbec3a0bcd3170b90aa39 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 13 Oct 2024 21:22:48 +0200 Subject: [PATCH 01/18] feat: add Nitro event handler --- package.json | 5 + playground/nuxt.config.ts | 1 + src/runtime/plugins/critical.server.ts | 346 ++------------------- src/runtime/plugins/detect.server.ts | 40 +-- src/runtime/plugins/device.server.ts | 117 +------ src/runtime/plugins/headers.ts | 51 --- src/runtime/plugins/network.server.ts | 138 +------- src/runtime/server/index.ts | 56 ++++ src/runtime/shared-types/types.ts | 2 +- src/runtime/utils/critical.ts | 331 ++++++++++++++++++++ src/runtime/utils/detect.ts | 42 +++ src/runtime/utils/device.ts | 117 +++++++ src/runtime/{plugins => utils}/features.ts | 0 src/runtime/utils/headers.ts | 50 +++ src/runtime/utils/network.ts | 139 +++++++++ src/types.ts | 8 + src/utils/configuration.ts | 76 ++++- 17 files changed, 868 insertions(+), 651 deletions(-) create mode 100644 src/runtime/server/index.ts create mode 100644 src/runtime/utils/critical.ts create mode 100644 src/runtime/utils/detect.ts create mode 100644 src/runtime/utils/device.ts rename src/runtime/{plugins => utils}/features.ts (100%) create mode 100644 src/runtime/utils/headers.ts create mode 100644 src/runtime/utils/network.ts diff --git a/package.json b/package.json index b4b5527..02dea82 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,11 @@ "vitest": "^2.1.1", "vue-tsc": "^2.1.6" }, + "build": { + "externals": [ + "defu" + ] + }, "stackblitz": { "installDependencies": false, "startCommand": "pnpm install && pnpm dev:prepare && pnpm dev" diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index b051933..59ec2a7 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -14,6 +14,7 @@ export default defineNuxtConfig({ viewportSize: true, prefersColorScheme: true, }, + serverImages: true, }, }) diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 9f4ab95..86664bc 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -1,40 +1,11 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { - ResolvedHttpClientHintsOptions, - CriticalInfo, - CriticalClientHintsConfiguration, -} from '../shared-types/types' +import type { parseUserAgent } from 'detect-browser-es' +import { CriticalHintsHeaders, extractCriticalHints } from '../utils/critical' +import type { ResolvedHttpClientHintsOptions } 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 { writeHeaders } from './headers' +import { defineNuxtPlugin, useCookie, useRequestHeaders, useRuntimeConfig } 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', @@ -44,303 +15,22 @@ const plugin: Plugin = defineNuxtPlugin({ async setup(nuxtApp) { const state = useHttpClientHintsState() const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions - const requestHeaders = useRequestHeaders(HttpRequestHeaders) - - // 1. extract browser info + const requestHeaders = useRequestHeaders(CriticalHintsHeaders) 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!, + 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.server.ts b/src/runtime/plugins/detect.server.ts index 4494101..364ef62 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -4,17 +4,16 @@ import { detect, detectOS, parseUserAgent, - serverResponseHeadersForUserAgentHints, } from 'detect-browser-es' import { appendHeader } from 'h3' import type { ResolvedHttpClientHintsOptions, UserAgentHints } from '../shared-types/types' +import { extractBrowser } from '../utils/detect' import { useHttpClientHintsState } from './state' import { defineNuxtPlugin, useNuxtApp, useRequestEvent, - useRequestHeaders, - useRuntimeConfig, + useRequestHeaders, useRuntimeConfig, } from '#imports' import type { Plugin } from '#app' @@ -31,16 +30,11 @@ const plugin: Plugin = defineNuxtPlugin({ 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 extractBrowser( + 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..87a8613 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -1,31 +1,10 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { - DeviceInfo, - DeviceHints, - ResolvedHttpClientHintsOptions, -} from '../shared-types/types' +import type { parseUserAgent } from 'detect-browser-es' +import { extractDeviceHints, HttpRequestHeaders } from '../utils/device' import { useHttpClientHintsState } from './state' -import { type GetHeaderType, lookupHeader, writeClientHintHeaders, writeHeaders } from './headers' -import { browserFeatureAvailable } from './features' +import { writeHeaders } from './headers' import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } 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') +import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:device-server:plugin', @@ -35,95 +14,11 @@ const plugin: Plugin = defineNuxtPlugin({ dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { const state = useHttpClientHintsState() + const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType 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 + 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/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/network.server.ts b/src/runtime/plugins/network.server.ts index 503b6da..be7baa2 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -1,34 +1,10 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { NetworkInfo, NetworkHints, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import type { parseUserAgent } from 'detect-browser-es' +import { extractNetworkHints, NetworkHintsHeaders } from '../utils/network' import { useHttpClientHintsState } from './state' -import type { GetHeaderType } from './headers' -import { lookupHeader, writeClientHintHeaders, writeHeaders } from './headers' -import { browserFeatureAvailable } from './features' +import { writeHeaders } from './headers' import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } 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') +import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:network-server:plugin', @@ -38,111 +14,11 @@ const plugin: Plugin = defineNuxtPlugin({ dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { 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 httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + 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/server/index.ts b/src/runtime/server/index.ts new file mode 100644 index 0000000..243a2e7 --- /dev/null +++ b/src/runtime/server/index.ts @@ -0,0 +1,56 @@ +import { eventHandler } from 'h3' +import { useNitro } from '@nuxt/kit' +import { parseUserAgent } from 'detect-browser-es' +import type { HttpClientHintsState, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import { extractBrowser } from '../utils/detect' +import { extractCriticalHints } from '../utils/critical' +import { extractDeviceHints } from '../utils/device' +import { extractNetworkHints } from '../utils/network' + +export default eventHandler(async (event) => { + // expose the client hints in the context + const url = event.path + console.log(url) + try { + const nitro = useNitro() + const options = nitro.options.runtimeConfig.public.httpClientHints as ResolvedHttpClientHintsOptions + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + if (!critical && !device && !network && !detect) { + return undefined + } + + // expose the client hints in the context + // const url = event.path + console.log(url) + if (options.serverImages?.some(r => r.test(url))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + event.context.httpClientHints = clientHints + } + } + catch (err) { + console.error(err) + } +}) diff --git a/src/runtime/shared-types/types.ts b/src/runtime/shared-types/types.ts index e8b0a87..d69de4e 100644 --- a/src/runtime/shared-types/types.ts +++ b/src/runtime/shared-types/types.ts @@ -69,7 +69,6 @@ export interface HttpClientHintsState { browser?: BrowserInfo device?: DeviceInfo network?: NetworkInfo - userAgent?: UserAgentDataInfo critical?: CriticalInfo } @@ -179,4 +178,5 @@ export interface ResolvedHttpClientHintsOptions { * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints#critical_client_hints */ critical?: CriticalClientHintsConfiguration + serverImages?: RegExp[] } diff --git a/src/runtime/utils/critical.ts b/src/runtime/utils/critical.ts new file mode 100644 index 0000000..eea7a35 --- /dev/null +++ b/src/runtime/utils/critical.ts @@ -0,0 +1,331 @@ +import type { Browser, parseUserAgent } from 'detect-browser-es' +import type { + CriticalClientHintsConfiguration, + CriticalInfo, + ResolvedHttpClientHintsOptions, +} from '../shared-types/types' +import { lookupHeader, writeClientHintHeaders } from './headers' +import { browserFeatureAvailable } from './features' + +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 +export const CriticalHintsHeaders = Array.from(Object.values(AcceptClientHintsRequestHeaders)).concat('user-agent', 'cookie', SecChUaMobile) + +export function extractCriticalHints( + httpClientHints: ResolvedHttpClientHintsOptions, + requestHeaders: { [key in Lowercase]?: string }, + userAgent: ReturnType, + writeHeaders?: (headers: Record) => void, + writeCookie?: (cookieName: string, path: string, expires: Date, themeName: string) => void, +): CriticalInfo { + // 1. prepare client hints request + const clientHintsRequest = collectClientHints(userAgent, httpClientHints.critical!, requestHeaders) + // 2. write client hints response headers + if (writeHeaders) { + writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.critical!, writeHeaders) + } + // 3. send the theme cookie to the client when required + clientHintsRequest.colorSchemeCookie = writeThemeCookie( + clientHintsRequest, + httpClientHints.critical!, + writeCookie, + ) + + return clientHintsRequest +} + +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, + writeHeaders: (headers: Record) => void, +) { + // 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, + writeCookie?: (cookieName: string, path: string, expires: Date, themeName: string) => void, +) { + 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 (writeCookie && (!criticalInfo.firstRequest || !criticalClientHintsConfiguration.reloadOnFirstRequest)) { + writeCookie(cookieName, path, expires, themeName) + } + + return `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=Lax` +} diff --git a/src/runtime/utils/detect.ts b/src/runtime/utils/detect.ts new file mode 100644 index 0000000..8cb61cb --- /dev/null +++ b/src/runtime/utils/detect.ts @@ -0,0 +1,42 @@ +import type { BrowserInfo } from 'detect-browser-es' +import { asyncDetect, detect, serverResponseHeadersForUserAgentHints } from 'detect-browser-es' +import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' + +export async function extractBrowser( + httpClientHints: ResolvedHttpClientHintsOptions, + requestHeaders: Record, + userAgentHeader?: string, + writeHeaders?: (headers: Record) => void, +): Promise { + 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 + if (typeof writeHeaders === 'function') { + const headers = serverResponseHeadersForUserAgentHints(hints) + if (headers) { + const useHeader: Record = {} + for (const [n, value] of Object.entries(headers)) { + if (value) { + useHeader[n] = [value] + } + } + writeHeaders(useHeader) + } + } + // detect browser info + return (await asyncDetect({ + hints, + httpHeaders: requestHeaders, + })) as BrowserInfo + } + else if (userAgentHeader) { + return detect(userAgentHeader) as BrowserInfo + } + + return undefined +} diff --git a/src/runtime/utils/device.ts b/src/runtime/utils/device.ts new file mode 100644 index 0000000..18a2c95 --- /dev/null +++ b/src/runtime/utils/device.ts @@ -0,0 +1,117 @@ +import type { Browser, parseUserAgent } from 'detect-browser-es' +import type { DeviceHints, DeviceInfo, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import type { GetHeaderType } from './headers' +import { lookupHeader, writeClientHintHeaders } from './headers' +import { browserFeatureAvailable } from './features' + +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>) + +export const HttpRequestHeaders = Array.from(Object.values(DeviceClientHintsHeaders)).concat('user-agent') + +export function extractDeviceHints( + httpClientHints: ResolvedHttpClientHintsOptions, + requestHeaders: { [key in Lowercase]?: string }, + userAgent: ReturnType, + writeHeaders?: (headers: Record) => void, +): DeviceInfo { + // 1. prepare client hints request + const clientHintsRequest = collectClientHints(userAgent, httpClientHints.device!, requestHeaders) + // 2. write client hints response headers + if (writeHeaders) { + writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.device!, writeHeaders) + } + + return clientHintsRequest +} + +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[], + writeHeaders: (headers: Record) => void, +) { + 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/utils/features.ts similarity index 100% rename from src/runtime/plugins/features.ts rename to src/runtime/utils/features.ts diff --git a/src/runtime/utils/headers.ts b/src/runtime/utils/headers.ts new file mode 100644 index 0000000..c461183 --- /dev/null +++ b/src/runtime/utils/headers.ts @@ -0,0 +1,50 @@ +export function writeClientHintHeaders(headerNames: string[], key: string, headers: Record) { + headerNames.forEach((header) => { + headers[header] = (headers[header] ? headers[header] : []).concat(key) + }) +} + +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/utils/network.ts b/src/runtime/utils/network.ts new file mode 100644 index 0000000..6d37591 --- /dev/null +++ b/src/runtime/utils/network.ts @@ -0,0 +1,139 @@ +import type { Browser, parseUserAgent } from 'detect-browser-es' +import type { NetworkHints, NetworkInfo, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import type { GetHeaderType } from './headers' +import { lookupHeader, writeClientHintHeaders } from './headers' +import { browserFeatureAvailable } from './features' + +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>) + +export const NetworkHintsHeaders = Array.from(Object.values(NetworkClientHintsHeaders)).concat('user-agent') + +export function extractNetworkHints( + httpClientHints: ResolvedHttpClientHintsOptions, + requestHeaders: { [key in Lowercase]?: string }, + userAgent: ReturnType, + writeHeaders?: (headers: Record) => void, +): NetworkInfo { + // 1. prepare client hints request + const clientHintsRequest = collectClientHints(userAgent, httpClientHints.network!, requestHeaders) + // 2. write client hints response headers + if (writeHeaders) { + writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.network!, writeHeaders) + } + + return clientHintsRequest +} + +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[], + writeHeaders: (headers: Record) => void, +) { + const headers: Record = {} + + for (const hint of networkHints) { + if (networkInfo[`${hint}Available`]) { + writeClientHintHeaders(ClientHeaders, NetworkClientHintsHeaders[hint], headers) + } + } + + writeHeaders(headers) +} diff --git a/src/types.ts b/src/types.ts index 4f16ba5..8336ad5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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..6055601 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,6 +1,8 @@ import type { Nuxt } from '@nuxt/schema' -import type { Resolver } from '@nuxt/kit' +import { addDevServerHandler, addServerHandler, addServerImportsDir, type Resolver } from '@nuxt/kit' import { addPlugin, addPluginTemplate } from '@nuxt/kit' +import defu from 'defu' +import { defineEventHandler } from 'h3' import type { HttpClientHintsOptions } from '../types' import type { ResolvedHttpClientHintsOptions } from '../runtime/shared-types/types' @@ -32,6 +34,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { network, device, critical, + serverImages, } = options if (userAgent) { if (userAgent === true) { @@ -139,6 +142,77 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { addPlugin(resolver.resolve(runtimeDir, 'plugins/critical.server')) } + // Add utils to nitro config + nuxt.hook('nitro:config', (nitroConfig) => { + nitroConfig.alias = nitroConfig.alias || {} + + // Inline module runtime in Nitro bundle + nitroConfig.externals = defu( + typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, + { + inline: [resolver.resolve('./runtime/server/utils/index.js')], + }, + ) + }) + + resolvedOptions.serverImages = serverImages + ? serverImages === true + ? [/\.(png|jpeg|jpg|webp|avi)$/] + : Array.isArray(serverImages) + ? serverImages + : [serverImages] + : undefined + + if (resolvedOptions.serverImages?.length) { + // Add utils to server imports + addServerImportsDir(resolver.resolve('./runtime/utils')) + // addServerImportsDir(resolver.resolve('./runtime/server')) + if (nuxt.options.dev) { + addDevServerHandler({ + method: 'get', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) + /* nuxt.hook('nitro:init', (nitro) => { + nitro.options.devHandlers.unshift({ + route: '', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) + }) */ + /* nuxt.options.devServerHandlers.push({ + route: '', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) + addDevServerHandler({ + route: '', + handler: defineEventHandler(async (event) => { + + }), + }) */ + } + else { + addServerHandler({ + method: 'get', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) + /* nuxt.hook('nitro:init', (nitro) => { + nitro.options.handlers.unshift({ + route: '', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) + }) */ + /* nuxt.options.serverHandlers.push({ + route: '', + handler: resolver.resolve(runtimeDir, 'server/index'), + }) */ + /* addServerHandler({ + route: '', + handler: defineEventHandler(async (event) => { + + }), + }) */ + } + } + if (clientDependsOn.length) { // @ts-expect-error missing at build time addClientHintsPlugin('client', clientDependsOn.map(p => `http-client-hints:${p}-client:plugin`)) From d169785944106ae042496492b824f27687f0ca99 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 13 Oct 2024 21:32:20 +0200 Subject: [PATCH 02/18] chore: fix lint (imports) --- src/utils/configuration.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 6055601..27ff06a 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,8 +1,13 @@ import type { Nuxt } from '@nuxt/schema' -import { addDevServerHandler, addServerHandler, addServerImportsDir, type Resolver } from '@nuxt/kit' -import { addPlugin, addPluginTemplate } from '@nuxt/kit' +import type { Resolver } from '@nuxt/kit' +import { + addDevServerHandler, + addServerHandler, + addServerImportsDir, + addPlugin, + addPluginTemplate, +} from '@nuxt/kit' import defu from 'defu' -import { defineEventHandler } from 'h3' import type { HttpClientHintsOptions } from '../types' import type { ResolvedHttpClientHintsOptions } from '../runtime/shared-types/types' From 256ed1a406af6ac961a19524dfba5ad3b73e6a76 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 13 Oct 2024 21:34:35 +0200 Subject: [PATCH 03/18] chore: remove server auto imports --- src/utils/configuration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 27ff06a..00011d3 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -3,7 +3,7 @@ import type { Resolver } from '@nuxt/kit' import { addDevServerHandler, addServerHandler, - addServerImportsDir, + // addServerImportsDir, addPlugin, addPluginTemplate, } from '@nuxt/kit' @@ -170,7 +170,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { if (resolvedOptions.serverImages?.length) { // Add utils to server imports - addServerImportsDir(resolver.resolve('./runtime/utils')) + // addServerImportsDir(resolver.resolve('./runtime/utils')) // addServerImportsDir(resolver.resolve('./runtime/server')) if (nuxt.options.dev) { addDevServerHandler({ From 84576b4523de1d607d7ca0ee817dc0b5b5018dc2 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 13 Oct 2024 23:12:45 +0200 Subject: [PATCH 04/18] chore: add plugin + fix nitro configuration --- src/module.ts | 4 +-- src/runtime/server/index.ts | 7 ++-- src/runtime/server/plugin.ts | 67 ++++++++++++++++++++++++++++++++++++ src/utils/configuration.ts | 52 +++++++++++++++++++--------- 4 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 src/runtime/server/plugin.ts diff --git a/src/module.ts b/src/module.ts index 6f647c8..ec86830 100644 --- a/src/module.ts +++ b/src/module.ts @@ -33,8 +33,8 @@ export default defineNuxtModule({ logger: useLogger(`nuxt:${NAME}`), options, resolvedOptions: { - detectBrowser: false, - detectOS: false, + detectBrowser: options.detectBrowser ?? false, + detectOS: options.detectOS ?? false, userAgent: [], network: [], device: [], diff --git a/src/runtime/server/index.ts b/src/runtime/server/index.ts index 243a2e7..2b985fc 100644 --- a/src/runtime/server/index.ts +++ b/src/runtime/server/index.ts @@ -1,5 +1,5 @@ import { eventHandler } from 'h3' -import { useNitro } from '@nuxt/kit' +import { useRuntimeConfig } from 'nitropack/runtime' import { parseUserAgent } from 'detect-browser-es' import type { HttpClientHintsState, ResolvedHttpClientHintsOptions } from '../shared-types/types' import { extractBrowser } from '../utils/detect' @@ -12,8 +12,9 @@ export default eventHandler(async (event) => { const url = event.path console.log(url) try { - const nitro = useNitro() - const options = nitro.options.runtimeConfig.public.httpClientHints as ResolvedHttpClientHintsOptions + const runtimeConfig = useRuntimeConfig(event) + console.log(runtimeConfig) + const options = runtimeConfig.public.httpClientHints as ResolvedHttpClientHintsOptions const critical = !!options.critical const device = options.device.length > 0 const network = options.network.length > 0 diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts new file mode 100644 index 0000000..d4d0738 --- /dev/null +++ b/src/runtime/server/plugin.ts @@ -0,0 +1,67 @@ +import { defineNitroPlugin, useAppConfig } from 'nitropack/runtime' +import { parseUserAgent } from 'detect-browser-es' +import type { HttpClientHintsState, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import { extractBrowser } from '../utils/detect' +import { extractCriticalHints } from '../utils/critical' +import { extractDeviceHints } from '../utils/device' +import { extractNetworkHints } from '../utils/network' + +interface ServerRuntimeConfig extends Omit { + serverImages: string[] +} + +export default defineNitroPlugin((nitroApp) => { + const { serverImages, ...rest } = useAppConfig().httpClientHints as ServerRuntimeConfig + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + console.log(options) + nitroApp.hooks.hook('afterResponse', async (event) => { + // we should add the vary header to the response + }) + nitroApp.hooks.hook('request', async (event) => { + // expose the client hints in the context + const url = event.path + console.log(url) + try { + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + if (!critical && !device && !network && !detect) { + return undefined + } + + // expose the client hints in the context + // const url = event.path + if (options.serverImages?.some(r => url.match(r))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + event.context.httpClientHints = clientHints + } + } + catch (err) { + console.error(err) + } + }) +}) diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 00011d3..dd9a2a7 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,13 +1,14 @@ import type { Nuxt } from '@nuxt/schema' import type { Resolver } from '@nuxt/kit' import { - addDevServerHandler, - addServerHandler, + // addDevServerHandler, + // addServerHandler, // addServerImportsDir, addPlugin, addPluginTemplate, + addServerPlugin, } from '@nuxt/kit' -import defu from 'defu' +// import defu from 'defu' import type { HttpClientHintsOptions } from '../types' import type { ResolvedHttpClientHintsOptions } from '../runtime/shared-types/types' @@ -62,7 +63,7 @@ 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') }) @@ -128,7 +129,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { addPlugin(resolver.resolve(runtimeDir, 'plugins/init.server')) - if (options.detectBrowser || options.detectOS || resolvedOptions.userAgent.length) { + if (resolvedOptions.detectBrowser || resolvedOptions.detectOS || resolvedOptions.userAgent.length) { clientDependsOn.push('detect') serverDependsOn.push('detect') addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.client')) @@ -148,17 +149,17 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { } // Add utils to nitro config - nuxt.hook('nitro:config', (nitroConfig) => { + /* nuxt.hook('nitro:config', (nitroConfig) => { nitroConfig.alias = nitroConfig.alias || {} // Inline module runtime in Nitro bundle nitroConfig.externals = defu( typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, { - inline: [resolver.resolve('./runtime/server/utils/index.js')], + inline: [resolver.resolve('./runtime/server/utils/index')], }, ) - }) + }) */ resolvedOptions.serverImages = serverImages ? serverImages === true @@ -169,24 +170,41 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { : undefined if (resolvedOptions.serverImages?.length) { + addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) + const { serverImages, ...rest } = resolvedOptions + nuxt.options.appConfig.httpClientHints = { + ...rest, + serverImages: serverImages.map(r => r.source), + } + /* nuxt.hook('nitro:init', (nitro) => { + nitro.options.appConfig.public ??= {} + nitro.options.appConfig.public.httpClientHints = resolvedOptions + }) + nuxt.hook('nitro:config', (nitroConfig) => { + nitroConfig.runtimeConfig ??= {} + nitroConfig.runtimeConfig.public ??= {} + nitroConfig.runtimeConfig.public.httpClientHints = resolvedOptions + }) */ // Add utils to server imports // addServerImportsDir(resolver.resolve('./runtime/utils')) // addServerImportsDir(resolver.resolve('./runtime/server')) if (nuxt.options.dev) { - addDevServerHandler({ + /* addDevServerHandler({ method: 'get', handler: resolver.resolve(runtimeDir, 'server/index'), - }) + }) */ /* nuxt.hook('nitro:init', (nitro) => { nitro.options.devHandlers.unshift({ route: '', handler: resolver.resolve(runtimeDir, 'server/index'), }) }) */ - /* nuxt.options.devServerHandlers.push({ - route: '', + /* nuxt.options.devServerHandlers.unshift({ + // route: '', + method: 'get', handler: resolver.resolve(runtimeDir, 'server/index'), - }) + }) */ + /* addDevServerHandler({ route: '', handler: defineEventHandler(async (event) => { @@ -195,18 +213,18 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { }) */ } else { - addServerHandler({ + /* addServerHandler({ method: 'get', handler: resolver.resolve(runtimeDir, 'server/index'), - }) + }) */ /* nuxt.hook('nitro:init', (nitro) => { nitro.options.handlers.unshift({ route: '', handler: resolver.resolve(runtimeDir, 'server/index'), }) }) */ - /* nuxt.options.serverHandlers.push({ - route: '', + /* nuxt.options.serverHandlers.unshift({ + // route: '', handler: resolver.resolve(runtimeDir, 'server/index'), }) */ /* addServerHandler({ From bf3d909f17f5448be3f952f93ba271f386f1d249 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 13 Oct 2024 23:36:18 +0200 Subject: [PATCH 05/18] chore: replace runtime caching with app config --- src/runtime/plugins/critical.server.ts | 7 +++---- src/runtime/plugins/detect.server.ts | 8 ++++---- src/runtime/plugins/device.server.ts | 9 ++++----- src/runtime/plugins/init.server.ts | 2 +- src/runtime/plugins/network.server.ts | 7 +++---- src/runtime/plugins/state.ts | 6 ------ src/runtime/plugins/utils.ts | 18 ++++++++++++++++++ src/runtime/server/plugin.ts | 23 +++++++++++------------ src/runtime/shared-types/types.ts | 4 ++++ src/utils/configuration.ts | 15 +++++++-------- 10 files changed, 55 insertions(+), 44 deletions(-) delete mode 100644 src/runtime/plugins/state.ts create mode 100644 src/runtime/plugins/utils.ts diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 86664bc..9761510 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -1,9 +1,8 @@ import type { parseUserAgent } from 'detect-browser-es' import { CriticalHintsHeaders, extractCriticalHints } from '../utils/critical' -import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' -import { useHttpClientHintsState } from './state' import { writeHeaders } from './headers' -import { defineNuxtPlugin, useCookie, useRequestHeaders, useRuntimeConfig } from '#imports' +import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' +import { defineNuxtPlugin, useCookie, useRequestHeaders } from '#imports' import type { Plugin } from '#app' const plugin: Plugin = defineNuxtPlugin({ @@ -14,7 +13,7 @@ const plugin: Plugin = defineNuxtPlugin({ dependsOn: ['http-client-hints:init-server:plugin'], async setup(nuxtApp) { const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(CriticalHintsHeaders) const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType state.value.critical = extractCriticalHints( diff --git a/src/runtime/plugins/detect.server.ts b/src/runtime/plugins/detect.server.ts index 364ef62..d3e6282 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -6,14 +6,14 @@ import { parseUserAgent, } from 'detect-browser-es' import { appendHeader } from 'h3' -import type { ResolvedHttpClientHintsOptions, UserAgentHints } from '../shared-types/types' +import type { UserAgentHints } from '../shared-types/types' import { extractBrowser } from '../utils/detect' -import { useHttpClientHintsState } from './state' +import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useNuxtApp, useRequestEvent, - useRequestHeaders, useRuntimeConfig, + useRequestHeaders, } from '#imports' import type { Plugin } from '#app' @@ -25,7 +25,7 @@ const plugin: Plugin = defineNuxtPlugin({ dependsOn: ['http-client-hints:init-server:plugin'], async setup() { const state = useHttpClientHintsState() - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders() const userAgentHeader = requestHeaders['user-agent'] diff --git a/src/runtime/plugins/device.server.ts b/src/runtime/plugins/device.server.ts index 87a8613..5308a85 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -1,10 +1,9 @@ import type { parseUserAgent } from 'detect-browser-es' import { extractDeviceHints, HttpRequestHeaders } from '../utils/device' -import { useHttpClientHintsState } from './state' +import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' import { writeHeaders } from './headers' -import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#imports' +import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:device-server:plugin', @@ -13,9 +12,9 @@ const plugin: Plugin = defineNuxtPlugin({ // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { - const state = useHttpClientHintsState() const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + const state = useHttpClientHintsState() + const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(HttpRequestHeaders) state.value.device = extractDeviceHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, diff --git a/src/runtime/plugins/init.server.ts b/src/runtime/plugins/init.server.ts index 6664e42..d296732 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 { useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' diff --git a/src/runtime/plugins/network.server.ts b/src/runtime/plugins/network.server.ts index be7baa2..426e7b5 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -1,10 +1,9 @@ import type { parseUserAgent } from 'detect-browser-es' import { extractNetworkHints, NetworkHintsHeaders } from '../utils/network' -import { useHttpClientHintsState } from './state' +import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' import { writeHeaders } from './headers' -import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#imports' +import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:network-server:plugin', @@ -15,7 +14,7 @@ const plugin: Plugin = defineNuxtPlugin({ setup(nuxtApp) { const state = useHttpClientHintsState() const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - const httpClientHints = useRuntimeConfig().public.httpClientHints as ResolvedHttpClientHintsOptions + const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(NetworkHintsHeaders) state.value.network = extractNetworkHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, 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/utils.ts b/src/runtime/plugins/utils.ts new file mode 100644 index 0000000..5b13a78 --- /dev/null +++ b/src/runtime/plugins/utils.ts @@ -0,0 +1,18 @@ +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../shared-types/types' +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/server/plugin.ts b/src/runtime/server/plugin.ts index d4d0738..c097e5d 100644 --- a/src/runtime/server/plugin.ts +++ b/src/runtime/server/plugin.ts @@ -1,24 +1,23 @@ import { defineNitroPlugin, useAppConfig } from 'nitropack/runtime' import { parseUserAgent } from 'detect-browser-es' -import type { HttpClientHintsState, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../shared-types/types' import { extractBrowser } from '../utils/detect' import { extractCriticalHints } from '../utils/critical' import { extractDeviceHints } from '../utils/device' import { extractNetworkHints } from '../utils/network' -interface ServerRuntimeConfig extends Omit { - serverImages: string[] -} - export default defineNitroPlugin((nitroApp) => { - const { serverImages, ...rest } = useAppConfig().httpClientHints as ServerRuntimeConfig + const { serverImages, ...rest } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions const options: ResolvedHttpClientHintsOptions = { ...rest, serverImages: serverImages.map(r => new RegExp(r)), } - console.log(options) - nitroApp.hooks.hook('afterResponse', async (event) => { - // we should add the vary header to the response + nitroApp.hooks.hook('afterResponse', async (_event) => { + // we should add the Vary header to the response: is there a way to check if the response has been committed? }) nitroApp.hooks.hook('request', async (event) => { // expose the client hints in the context @@ -48,15 +47,15 @@ export default defineNitroPlugin((nitroApp) => { if (detect) { clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } if (device) { clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) } if (network) { clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } event.context.httpClientHints = clientHints } } diff --git a/src/runtime/shared-types/types.ts b/src/runtime/shared-types/types.ts index d69de4e..dc71466 100644 --- a/src/runtime/shared-types/types.ts +++ b/src/runtime/shared-types/types.ts @@ -180,3 +180,7 @@ export interface ResolvedHttpClientHintsOptions { critical?: CriticalClientHintsConfiguration serverImages?: RegExp[] } + +export interface ServerHttpClientHintsOptions extends Omit { + serverImages: string[] +} diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index dd9a2a7..f1016c7 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -125,8 +125,6 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { resolvedOptions.detectOS = options.detectOS } - nuxt.options.runtimeConfig.public.httpClientHints = resolvedOptions - addPlugin(resolver.resolve(runtimeDir, 'plugins/init.server')) if (resolvedOptions.detectBrowser || resolvedOptions.detectOS || resolvedOptions.userAgent.length) { @@ -161,7 +159,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { ) }) */ - resolvedOptions.serverImages = serverImages + const useServerImages = serverImages ? serverImages === true ? [/\.(png|jpeg|jpg|webp|avi)$/] : Array.isArray(serverImages) @@ -169,13 +167,14 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { : [serverImages] : undefined + const { serverImages: _, ...rest } = resolvedOptions + nuxt.options.appConfig.httpClientHints = { + ...rest, + serverImages: useServerImages ? useServerImages.map(r => r.source) : undefined, + } + if (resolvedOptions.serverImages?.length) { addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) - const { serverImages, ...rest } = resolvedOptions - nuxt.options.appConfig.httpClientHints = { - ...rest, - serverImages: serverImages.map(r => r.source), - } /* nuxt.hook('nitro:init', (nitro) => { nitro.options.appConfig.public ??= {} nitro.options.appConfig.public.httpClientHints = resolvedOptions From eecc8b3ec305275349d35a8f1d68d7f3b8beb205 Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 14 Oct 2024 00:15:22 +0200 Subject: [PATCH 06/18] chore: add app config to ssrContext --- src/runtime/plugins/critical.server.ts | 7 +++++-- src/runtime/plugins/detect.server.ts | 8 ++++---- src/runtime/plugins/device.server.ts | 8 +++++--- src/runtime/plugins/init.server.ts | 3 ++- src/runtime/plugins/network.server.ts | 8 +++++--- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 9761510..7759069 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -4,6 +4,8 @@ import { writeHeaders } from './headers' import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useCookie, useRequestHeaders } from '#imports' import type { Plugin } from '#app' +import type { HttpClientHintsOptions } from '~/src/types' +import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:critical-server:plugin', @@ -12,10 +14,11 @@ const plugin: Plugin = defineNuxtPlugin({ // @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 = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(CriticalHintsHeaders) - const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType state.value.critical = extractCriticalHints( httpClientHints, requestHeaders, diff --git a/src/runtime/plugins/detect.server.ts b/src/runtime/plugins/detect.server.ts index d3e6282..f0be038 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -6,9 +6,9 @@ import { parseUserAgent, } from 'detect-browser-es' import { appendHeader } from 'h3' -import type { UserAgentHints } from '../shared-types/types' +import type { ResolvedHttpClientHintsOptions, UserAgentHints } from '../shared-types/types' import { extractBrowser } from '../utils/detect' -import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' +import { useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useNuxtApp, @@ -23,9 +23,9 @@ const plugin: Plugin = defineNuxtPlugin({ 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 = useHttpClientHintsOptions() + const httpClientHints = nuxtApp.ssrContext!._httpClientHintsOptions as ResolvedHttpClientHintsOptions const requestHeaders = useRequestHeaders() const userAgentHeader = requestHeaders['user-agent'] diff --git a/src/runtime/plugins/device.server.ts b/src/runtime/plugins/device.server.ts index 5308a85..230c9a7 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -1,6 +1,7 @@ import type { parseUserAgent } from 'detect-browser-es' import { extractDeviceHints, HttpRequestHeaders } from '../utils/device' -import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' +import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' +import { useHttpClientHintsState } from './utils' import { writeHeaders } from './headers' import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' @@ -12,9 +13,10 @@ const plugin: Plugin = defineNuxtPlugin({ // @ts-expect-error missing at build time dependsOn: ['http-client-hints:init-server:plugin'], setup(nuxtApp) { - const userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType + const ssrContext = nuxtApp.ssrContext! + const httpClientHints = ssrContext._httpClientHintsOptions as ResolvedHttpClientHintsOptions + const userAgent = ssrContext._httpClientHintsUserAgent as ReturnType const state = useHttpClientHintsState() - const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(HttpRequestHeaders) state.value.device = extractDeviceHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, diff --git a/src/runtime/plugins/init.server.ts b/src/runtime/plugins/init.server.ts index d296732..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 './utils' +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 426e7b5..837c255 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -1,6 +1,7 @@ import type { parseUserAgent } from 'detect-browser-es' import { extractNetworkHints, NetworkHintsHeaders } from '../utils/network' -import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' +import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' +import { useHttpClientHintsState } from './utils' import { writeHeaders } from './headers' import { defineNuxtPlugin, useRequestHeaders } from '#imports' import type { Plugin } from '#app' @@ -12,9 +13,10 @@ const plugin: Plugin = defineNuxtPlugin({ // @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 userAgent = nuxtApp.ssrContext?._httpClientHintsUserAgent as ReturnType - const httpClientHints = useHttpClientHintsOptions() const requestHeaders = useRequestHeaders(NetworkHintsHeaders) state.value.network = extractNetworkHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, From a1799a740cf301e870b004fc7c18519b4a50300f Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 14 Oct 2024 00:15:38 +0200 Subject: [PATCH 07/18] chore: cleanup nitro plugin --- src/runtime/server/plugin.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts index c097e5d..3b4793e 100644 --- a/src/runtime/server/plugin.ts +++ b/src/runtime/server/plugin.ts @@ -11,7 +11,10 @@ import { extractDeviceHints } from '../utils/device' import { extractNetworkHints } from '../utils/network' export default defineNitroPlugin((nitroApp) => { - const { serverImages, ...rest } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions + const { + serverImages, + ...rest + } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions const options: ResolvedHttpClientHintsOptions = { ...rest, serverImages: serverImages.map(r => new RegExp(r)), @@ -20,7 +23,7 @@ export default defineNitroPlugin((nitroApp) => { // we should add the Vary header to the response: is there a way to check if the response has been committed? }) nitroApp.hooks.hook('request', async (event) => { - // expose the client hints in the context + // expose the client hints in the context const url = event.path console.log(url) try { From a5a9195e6a1019756090a81cb314df6a28d074dc Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 14 Oct 2024 00:17:00 +0200 Subject: [PATCH 08/18] chore: remove defu from unbuild externals --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index 02dea82..b4b5527 100644 --- a/package.json +++ b/package.json @@ -62,11 +62,6 @@ "vitest": "^2.1.1", "vue-tsc": "^2.1.6" }, - "build": { - "externals": [ - "defu" - ] - }, "stackblitz": { "installDependencies": false, "startCommand": "pnpm install && pnpm dev:prepare && pnpm dev" From 29953024eee71e9cafcc2dc9e887cd79520390cf Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 14 Oct 2024 00:18:21 +0200 Subject: [PATCH 09/18] chore: fix lint --- src/runtime/plugins/critical.server.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 7759069..25c55b8 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -1,11 +1,10 @@ import type { parseUserAgent } from 'detect-browser-es' import { CriticalHintsHeaders, extractCriticalHints } from '../utils/critical' +import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' import { writeHeaders } from './headers' -import { useHttpClientHintsOptions, useHttpClientHintsState } from './utils' +import { useHttpClientHintsState } from './utils' import { defineNuxtPlugin, useCookie, useRequestHeaders } from '#imports' import type { Plugin } from '#app' -import type { HttpClientHintsOptions } from '~/src/types' -import type { ResolvedHttpClientHintsOptions } from '~/src/runtime/shared-types/types' const plugin: Plugin = defineNuxtPlugin({ name: 'http-client-hints:critical-server:plugin', From 524d4729bcdb0ea9cadd15f672c4ef7434d37c41 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 18 Oct 2024 23:41:25 +0200 Subject: [PATCH 10/18] chore: update plugins chore: include pnpm workspace --- package.json | 2 +- playground/app.vue | 2 +- playground/nuxt.config.ts | 15 + playground/package.json | 7 +- playground/plugins/hints.client.ts | 7 + .../{plugin.server.ts => hints.server.ts} | 2 +- playground/server/dev-image.ts | 7 + playground/server/image.ts | 5 + pnpm-lock.yaml | 546 +++++++++++++++--- pnpm-workspace.yaml | 2 + src/runtime/plugins/critical.server.ts | 2 +- src/runtime/plugins/detect.server.ts | 2 +- src/runtime/plugins/device.server.ts | 2 +- src/runtime/plugins/network.server.ts | 2 +- src/runtime/server/index.ts | 8 +- src/runtime/server/plugin.ts | 30 +- src/utils/configuration.ts | 108 +--- 17 files changed, 570 insertions(+), 179 deletions(-) create mode 100644 playground/plugins/hints.client.ts rename playground/plugins/{plugin.server.ts => hints.server.ts} (65%) create mode 100644 playground/server/dev-image.ts create mode 100644 playground/server/image.ts create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index b4b5527..41c1533 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "Device Client Hints", "Network Client Hints", "Browser detection", - "nuxt module" + "Nuxt module" ], "sideEffects": false, "exports": { diff --git a/playground/app.vue b/playground/app.vue index c7774fe..ad92a53 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -8,7 +8,7 @@ - diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 59ec2a7..6f65bc2 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,3 +1,5 @@ +import DevImage from './server/dev-image' + export default defineNuxtConfig({ compatibilityDate: '2024-10-11', devtools: { enabled: true }, @@ -17,4 +19,17 @@ export default defineNuxtConfig({ serverImages: true, }, + nitro: { + handlers: [ + { + middleware: true, + handler: '~/server/image', + }, + ], + devHandlers: [{ + route: '', + handler: DevImage, + }], + }, + }) diff --git a/playground/package.json b/playground/package.json index d86949b..ec867f3 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", + "nuxt-http-client-hints": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.6.3", + "vue-tsc": "^2.1.6" } } diff --git a/playground/plugins/hints.client.ts b/playground/plugins/hints.client.ts new file mode 100644 index 0000000..1e41ec0 --- /dev/null +++ b/playground/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/plugins/hints.server.ts similarity index 65% rename from playground/plugins/plugin.server.ts rename to playground/plugins/hints.server.ts index f4f8eff..8e289fc 100644 --- a/playground/plugins/plugin.server.ts +++ b/playground/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/server/dev-image.ts b/playground/server/dev-image.ts new file mode 100644 index 0000000..16ff5f5 --- /dev/null +++ b/playground/server/dev-image.ts @@ -0,0 +1,7 @@ +import { eventHandler } from 'h3' + +export default eventHandler(async (event) => { + console.log('eventHandler:', event.path) + console.log('eventHandler:', event.context.httpClientHintsOptions) + console.log('eventHandler:', event.context.httpClientHints) +}) diff --git a/playground/server/image.ts b/playground/server/image.ts new file mode 100644 index 0000000..22d8209 --- /dev/null +++ b/playground/server/image.ts @@ -0,0 +1,5 @@ +export default eventHandler(async (event) => { + console.log('eventHandler:', event.path) + console.log('eventHandler:', event.context.httpClientHintsOptions) + console.log('eventHandler:', event.context.httpClientHints) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb59867..d6a2d16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,29 +10,29 @@ importers: dependencies: '@nuxt/kit': specifier: ^3.13.2 - version: 3.13.2(magicast@0.3.5)(rollup@4.24.0) + version: 3.13.2(magicast@0.3.5)(rollup@3.29.5) detect-browser-es: specifier: ^0.1.1 version: 0.1.1 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/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 +44,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.7.6)(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.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)) + nuxt-http-client-hints: + specifier: workspace:* + version: link:.. + devDependencies: + typescript: + specifier: ^5.6.3 + version: 5.6.3 vue-tsc: specifier: ^2.1.6 version: 2.1.6(typescript@5.6.3) @@ -1417,8 +1433,8 @@ 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/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -5148,12 +5164,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.7.6)(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.7.6)(terser@5.34.1) transitivePeerDependencies: - magicast - rollup @@ -5173,13 +5201,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.9 + 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.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@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.7.6)(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.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 @@ -5208,9 +5284,9 @@ snapshots: sirv: 2.0.4 tinyglobby: 0.2.9 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.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@4.24.0)(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: @@ -5253,6 +5329,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 +5385,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 +5405,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,6 +5443,32 @@ snapshots: - supports-color - webpack-sources + '@nuxt/telemetry@2.6.0(magicast@0.3.5)(rollup@3.29.5)': + dependencies: + '@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 + defu: 6.1.4 + destr: 2.0.3 + 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 + rc9: 2.1.2 + std-env: 3.7.0 + transitivePeerDependencies: + - magicast + - rollup + - supports-color + - webpack-sources + '@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) @@ -5346,10 +5495,10 @@ 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/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@4.24.0) - '@nuxt/schema': 3.13.2(rollup@4.24.0) + '@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 @@ -5372,24 +5521,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.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.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@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.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 @@ -5415,9 +5623,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.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: @@ -5692,11 +5900,11 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 '@types/json-schema@7.0.15': {} - '@types/node@22.7.5': + '@types/node@22.7.6': dependencies: undici-types: 6.19.8 @@ -5831,19 +6039,19 @@ 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@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.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) '@vitest/expect@2.1.2': @@ -5853,13 +6061,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 +6106,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 +6199,14 @@ 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 @@ -7275,6 +7496,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) @@ -7780,14 +8012,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@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)) + '@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.7.6)(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.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@4.24.0)(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@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.7.6)(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 +8193,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.7.6 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -8348,6 +8693,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 @@ -8782,6 +9136,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 +9176,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 +9292,16 @@ 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-node@2.1.2(@types/node@22.7.5)(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.5)(terser@5.34.1) + vite: 5.4.8(@types/node@22.7.6)(terser@5.34.1) transitivePeerDependencies: - '@types/node' - less @@ -8917,7 +9313,7 @@ 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 @@ -8929,7 +9325,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.7.6)(terser@5.34.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -8940,7 +9336,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.7.6)(terser@5.34.1)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.2(rollup@4.24.0) @@ -8951,14 +9365,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.7.6)(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 +9383,23 @@ 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@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.5 + '@types/node': 22.7.6 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 +9421,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 +9439,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/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index 25c55b8..a1f6116 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -8,7 +8,7 @@ import type { Plugin } from '#app' 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'], diff --git a/src/runtime/plugins/detect.server.ts b/src/runtime/plugins/detect.server.ts index f0be038..a5df457 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -19,7 +19,7 @@ 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'], diff --git a/src/runtime/plugins/device.server.ts b/src/runtime/plugins/device.server.ts index 230c9a7..b4bec71 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -8,7 +8,7 @@ import type { Plugin } from '#app' 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'], diff --git a/src/runtime/plugins/network.server.ts b/src/runtime/plugins/network.server.ts index 837c255..582f740 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -8,7 +8,7 @@ import type { Plugin } from '#app' 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'], diff --git a/src/runtime/server/index.ts b/src/runtime/server/index.ts index 2b985fc..ace8112 100644 --- a/src/runtime/server/index.ts +++ b/src/runtime/server/index.ts @@ -10,10 +10,10 @@ import { extractNetworkHints } from '../utils/network' export default eventHandler(async (event) => { // expose the client hints in the context const url = event.path - console.log(url) + // console.log(`eventHandler: ${url}`) try { - const runtimeConfig = useRuntimeConfig(event) - console.log(runtimeConfig) + const runtimeConfig = useRuntimeConfig() + // console.log(runtimeConfig) const options = runtimeConfig.public.httpClientHints as ResolvedHttpClientHintsOptions const critical = !!options.critical const device = options.device.length > 0 @@ -25,7 +25,7 @@ export default eventHandler(async (event) => { // expose the client hints in the context // const url = event.path - console.log(url) + // console.log(url) if (options.serverImages?.some(r => r.test(url))) { const userAgentHeader = event.headers.get('user-agent') const requestHeaders: { [key in Lowercase]?: string } = {} diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts index 3b4793e..cfe59f7 100644 --- a/src/runtime/server/plugin.ts +++ b/src/runtime/server/plugin.ts @@ -19,24 +19,25 @@ export default defineNitroPlugin((nitroApp) => { ...rest, serverImages: serverImages.map(r => new RegExp(r)), } - nitroApp.hooks.hook('afterResponse', async (_event) => { - // we should add the Vary header to the response: is there a way to check if the response has been committed? + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + + // todo: remove this just for testing purposes + nitroApp.hooks.hook('afterResponse', async (event) => { + // We should add the Vary header to the response: is there a way to check if the response has been committed?. + // I guess the vary header whould be added by the consule, there are a lot of header here to handle. + const receivedOptions = event.context.httpClientHintsOptions + if (receivedOptions) { + console.log(`Client Hints for ${event.path}`, event.context.httpClientHints) + } }) + nitroApp.hooks.hook('request', async (event) => { - // expose the client hints in the context - const url = event.path - console.log(url) try { - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - if (!critical && !device && !network && !detect) { - return undefined - } - // expose the client hints in the context - // const url = event.path + const url = event.path if (options.serverImages?.some(r => url.match(r))) { const userAgentHeader = event.headers.get('user-agent') const requestHeaders: { [key in Lowercase]?: string } = {} @@ -59,6 +60,7 @@ export default defineNitroPlugin((nitroApp) => { if (critical) { clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) } + event.context.httpClientHintsOptions = options event.context.httpClientHints = clientHints } } diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index f1016c7..270f55a 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,6 +1,7 @@ import type { Nuxt } from '@nuxt/schema' import type { Resolver } from '@nuxt/kit' import { + // addDevServerHandler, // addDevServerHandler, // addServerHandler, // addServerImportsDir, @@ -19,7 +20,6 @@ export interface HttpClientHintsContext { logger: ReturnType options: HttpClientHintsOptions resolvedOptions: ResolvedHttpClientHintsOptions - clientDependsOn: PluginType[] serverDependsOn: PluginType[] } @@ -29,7 +29,6 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { resolvedOptions, resolver, logger, - clientDependsOn, serverDependsOn, } = ctx @@ -40,7 +39,6 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { network, device, critical, - serverImages, } = options if (userAgent) { if (userAgent === true) { @@ -66,7 +64,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { 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 @@ -78,8 +76,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') @@ -118,17 +114,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 } + // 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 (resolvedOptions.detectBrowser || resolvedOptions.detectOS || resolvedOptions.userAgent.length) { - clientDependsOn.push('detect') serverDependsOn.push('detect') addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.client')) addPlugin(resolver.resolve(runtimeDir, 'plugins/detect.server')) @@ -146,18 +146,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { addPlugin(resolver.resolve(runtimeDir, 'plugins/critical.server')) } - // Add utils to nitro config - /* nuxt.hook('nitro:config', (nitroConfig) => { - nitroConfig.alias = nitroConfig.alias || {} - - // Inline module runtime in Nitro bundle - nitroConfig.externals = defu( - typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, - { - inline: [resolver.resolve('./runtime/server/utils/index')], - }, - ) - }) */ + const serverImages = options.serverImages const useServerImages = serverImages ? serverImages === true @@ -173,79 +162,21 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { serverImages: useServerImages ? useServerImages.map(r => r.source) : undefined, } - if (resolvedOptions.serverImages?.length) { + if (useServerImages?.length) { addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) - /* nuxt.hook('nitro:init', (nitro) => { - nitro.options.appConfig.public ??= {} - nitro.options.appConfig.public.httpClientHints = resolvedOptions - }) - nuxt.hook('nitro:config', (nitroConfig) => { - nitroConfig.runtimeConfig ??= {} - nitroConfig.runtimeConfig.public ??= {} - nitroConfig.runtimeConfig.public.httpClientHints = resolvedOptions - }) */ - // Add utils to server imports - // addServerImportsDir(resolver.resolve('./runtime/utils')) - // addServerImportsDir(resolver.resolve('./runtime/server')) - if (nuxt.options.dev) { - /* addDevServerHandler({ - method: 'get', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) */ - /* nuxt.hook('nitro:init', (nitro) => { - nitro.options.devHandlers.unshift({ - route: '', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) - }) */ - /* nuxt.options.devServerHandlers.unshift({ - // route: '', - method: 'get', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) */ - /* - addDevServerHandler({ - route: '', - handler: defineEventHandler(async (event) => { - - }), - }) */ - } - else { - /* addServerHandler({ - method: 'get', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) */ - /* nuxt.hook('nitro:init', (nitro) => { - nitro.options.handlers.unshift({ - route: '', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) - }) */ - /* nuxt.options.serverHandlers.unshift({ - // route: '', - handler: resolver.resolve(runtimeDir, 'server/index'), - }) */ - /* addServerHandler({ - route: '', - handler: defineEventHandler(async (event) => { - - }), - }) */ - } + // todo: check dev handlers and event handler in build + node ... + // there is no way to have the plugin working in dev mode: the dev handler called for jpg images + // running build + node ... the plugin is registered but the image event handler is not called for jpg images } - if (clientDependsOn.length) { - // @ts-expect-error missing at build time - addClientHintsPlugin('client', clientDependsOn.map(p => `http-client-hints:${p}-client:plugin`)) - } + 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({ @@ -254,12 +185,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, + order: 'post', + parallel: false,${dependsOnString} async setup(nuxtApp) { const clientHints = useState('http-client-hints:state') await nuxtApp.hooks.callHook( From 43c7429c4a10e9750861e2b2c0e8f5d54d634633 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 19 Oct 2024 00:06:16 +0200 Subject: [PATCH 11/18] chore: update Vary header comment --- src/runtime/server/plugin.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts index cfe59f7..fc8e7cc 100644 --- a/src/runtime/server/plugin.ts +++ b/src/runtime/server/plugin.ts @@ -26,8 +26,7 @@ export default defineNitroPlugin((nitroApp) => { // todo: remove this just for testing purposes nitroApp.hooks.hook('afterResponse', async (event) => { - // We should add the Vary header to the response: is there a way to check if the response has been committed?. - // I guess the vary header whould be added by the consule, there are a lot of header here to handle. + // I guess the consumer should add the Vary header; there are a lot of headers here to handle. const receivedOptions = event.context.httpClientHintsOptions if (receivedOptions) { console.log(`Client Hints for ${event.path}`, event.context.httpClientHints) From 63f23d80533eda129d560bcd65cddd213e1e8cd8 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 19 Oct 2024 00:21:00 +0200 Subject: [PATCH 12/18] chore: fix hints plugins --- src/utils/configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 270f55a..63e23cb 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -192,7 +192,7 @@ function addClientHintsPlugin( return `import { defineNuxtPlugin, readonly, useState } from '#imports' export default defineNuxtPlugin({ name: '${name}', - order: 'post', + enforce: 'post', parallel: false,${dependsOnString} async setup(nuxtApp) { const clientHints = useState('http-client-hints:state') From af00fe9650e4d17437113e183a22ee9ff4192395 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 19 Oct 2024 00:27:26 +0200 Subject: [PATCH 13/18] chore: cleanup module --- src/module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/module.ts b/src/module.ts index ec86830..a82bb9c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -39,7 +39,6 @@ export default defineNuxtModule({ network: [], device: [], }, - clientDependsOn: [], serverDependsOn: [], }, nuxt, From c6ca138c82db6dfff4c03a1a5304bfa5474c15c4 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 26 Oct 2024 19:40:28 +0200 Subject: [PATCH 14/18] chore: use logic inside the playground --- playground/nuxt.config.ts | 1 + playground/package.json | 3 +- playground/server/dev-image.ts | 41 ++++- playground/server/image.ts | 64 +++++++- playground/server/utils/image.ts | 62 ++++++++ pnpm-lock.yaml | 250 +++++++++++++++++++++++++++++++ src/runtime/server/index.ts | 100 +++++++------ src/runtime/server/plugin.ts | 1 + src/runtime/utils/critical.ts | 4 + src/utils/configuration.ts | 23 ++- 10 files changed, 491 insertions(+), 58 deletions(-) create mode 100644 playground/server/utils/image.ts diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 6f65bc2..c222019 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -23,6 +23,7 @@ export default defineNuxtConfig({ handlers: [ { middleware: true, + // route: '', handler: '~/server/image', }, ], diff --git a/playground/package.json b/playground/package.json index ec867f3..359ab60 100644 --- a/playground/package.json +++ b/playground/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "nuxt": "^3.13.2", - "nuxt-http-client-hints": "workspace:*" + "nuxt-http-client-hints": "workspace:*", + "sharp": "^0.33.5" }, "devDependencies": { "typescript": "^5.6.3", diff --git a/playground/server/dev-image.ts b/playground/server/dev-image.ts index 16ff5f5..5923ee6 100644 --- a/playground/server/dev-image.ts +++ b/playground/server/dev-image.ts @@ -1,7 +1,42 @@ +import { lstat, readFile } from 'node:fs/promises' +import { resolve } from 'node:path' import { eventHandler } from 'h3' +import { useNitro } from '@nuxt/kit' +import sharp from 'sharp' +import type { HttpClientHintsState } from '../../src/runtime/shared-types/types' +import { extractHTTPClientHints } from './utils/image' export default eventHandler(async (event) => { - console.log('eventHandler:', event.path) - console.log('eventHandler:', event.context.httpClientHintsOptions) - console.log('eventHandler:', event.context.httpClientHints) + console.log('dev-image:', event.path) + await extractHTTPClientHints(event) + const httpClientHintsState: HttpClientHintsState | undefined = event.context.httpClientHints + const { widthAvailable = false, width = -1 } = httpClientHintsState?.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') + event.node.res.end(image) + } + } }) + +async function convertImage(path: string, width: number) { + if (path.startsWith('/')) { + path = path.slice(1) + } + const nitro = useNitro() + const folders = nitro.options.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 sharp(await readFile(image)).resize({ width }).toBuffer() + } + } + catch (_) {} + } +} diff --git a/playground/server/image.ts b/playground/server/image.ts index 22d8209..4f3a52f 100644 --- a/playground/server/image.ts +++ b/playground/server/image.ts @@ -1,5 +1,61 @@ -export default eventHandler(async (event) => { - console.log('eventHandler:', event.path) - console.log('eventHandler:', event.context.httpClientHintsOptions) - console.log('eventHandler:', event.context.httpClientHints) +import { useAppConfig } from 'nitropack/runtime' +import { parseUserAgent } from 'detect-browser-es' +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../../src/runtime/shared-types/types' +import { extractBrowser } from '../../src/runtime/utils/detect' +import { extractDeviceHints } from '../../src/runtime/utils/device' +import { extractNetworkHints } from '../../src/runtime/utils/network' +import { extractCriticalHints } from '../../src/runtime/utils/critical' + +export default defineEventHandler(async (event) => { + console.log('request', useAppConfig().httpClientHints) + const { + serverImages, + ...rest + } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + + try { + // expose the client hints in the context + const url = event.path + console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) + if (options.serverImages?.some(r => url.match(r))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + event.context.httpClientHintsOptions = options + event.context.httpClientHints = clientHints + } + } + catch (err) { + console.error(err) + } }) diff --git a/playground/server/utils/image.ts b/playground/server/utils/image.ts new file mode 100644 index 0000000..203173e --- /dev/null +++ b/playground/server/utils/image.ts @@ -0,0 +1,62 @@ +import { useAppConfig } from 'nitropack/runtime' +import type { H3Event } from 'h3' +import { parseUserAgent } from 'detect-browser-es' +import { useNitro } from '@nuxt/kit' +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../../../src/runtime/shared-types/types' +import { extractBrowser } from '../../../src/runtime/utils/detect' +import { extractDeviceHints } from '../../../src/runtime/utils/device' +import { extractNetworkHints } from '../../../src/runtime/utils/network' +import { extractCriticalHints } from '../../../src/runtime/utils/critical' + +export async function extractHTTPClientHints(event: H3Event) { + const { + serverImages, + ...rest + } = useNitro().options.appConfig.httpClientHints as ServerHttpClientHintsOptions + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + + try { + // expose the client hints in the context + const url = event.path + console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) + if (options.serverImages?.some(r => url.match(r))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + event.context.httpClientHintsOptions = options + event.context.httpClientHints = clientHints + } + } + catch (err) { + console.error(err) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6a2d16..f0bbaf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: nuxt-http-client-hints: specifier: workspace:* version: link:.. + sharp: + specifier: ^0.33.5 + version: 0.33.5 devDependencies: typescript: specifier: ^5.6.3 @@ -232,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'} @@ -1001,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==} @@ -1933,10 +2044,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==} @@ -2738,6 +2856,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'} @@ -3736,6 +3857,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'} @@ -3760,6 +3885,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'} @@ -4663,6 +4791,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 @@ -5076,6 +5209,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': @@ -6588,8 +6796,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: {} @@ -7551,6 +7769,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -8800,6 +9020,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 @@ -8822,6 +9068,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 diff --git a/src/runtime/server/index.ts b/src/runtime/server/index.ts index ace8112..a954591 100644 --- a/src/runtime/server/index.ts +++ b/src/runtime/server/index.ts @@ -1,57 +1,63 @@ -import { eventHandler } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' +import { eventHandler, lazyEventHandler } from 'h3' import { parseUserAgent } from 'detect-browser-es' -import type { HttpClientHintsState, ResolvedHttpClientHintsOptions } from '../shared-types/types' +import { useNitro } from '@nuxt/kit' +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../shared-types/types' import { extractBrowser } from '../utils/detect' import { extractCriticalHints } from '../utils/critical' import { extractDeviceHints } from '../utils/device' import { extractNetworkHints } from '../utils/network' -export default eventHandler(async (event) => { - // expose the client hints in the context - const url = event.path - // console.log(`eventHandler: ${url}`) - try { - const runtimeConfig = useRuntimeConfig() - // console.log(runtimeConfig) - const options = runtimeConfig.public.httpClientHints as ResolvedHttpClientHintsOptions - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - if (!critical && !device && !network && !detect) { - return undefined - } +export default lazyEventHandler(() => { + const { + serverImages, + ...rest + } = useNitro().options.appConfig.httpClientHints as ServerHttpClientHintsOptions + const options: ResolvedHttpClientHintsOptions = { + ...rest, + serverImages: serverImages.map(r => new RegExp(r)), + } + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - // expose the client hints in the context - // const url = event.path - // console.log(url) - if (options.serverImages?.some(r => r.test(url))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + return eventHandler(async (event) => { + try { + // expose the client hints in the context + const url = event.path + console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) + if (options.serverImages?.some(r => url.match(r))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + event.context.httpClientHintsOptions = options + event.context.httpClientHints = clientHints } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) - } - event.context.httpClientHints = clientHints } - } - catch (err) { - console.error(err) - } + catch (err) { + console.error(err) + } + }) }) diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts index fc8e7cc..a05407a 100644 --- a/src/runtime/server/plugin.ts +++ b/src/runtime/server/plugin.ts @@ -37,6 +37,7 @@ export default defineNitroPlugin((nitroApp) => { try { // expose the client hints in the context const url = event.path + console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) if (options.serverImages?.some(r => url.match(r))) { const userAgentHeader = event.headers.get('user-agent') const requestHeaders: { [key in Lowercase]?: string } = {} diff --git a/src/runtime/utils/critical.ts b/src/runtime/utils/critical.ts index eea7a35..be04821 100644 --- a/src/runtime/utils/critical.ts +++ b/src/runtime/utils/critical.ts @@ -7,6 +7,10 @@ import type { import { lookupHeader, writeClientHintHeaders } from './headers' import { browserFeatureAvailable } from './features' +// TODO: add `Sec-CH-Prefers-Contrast` and `Sec-CH-Forced-Colors` headers +// - https://github.com/WICG/user-preference-media-features-headers +// - https://browserleaks.com/client-hints#:~:text=Sec%2DCH%2DWidth%20gives%20a,user%2Dagent's%20current%20viewport%20height. + const AcceptClientHintsHeaders = { prefersColorScheme: 'Sec-CH-Prefers-Color-Scheme', prefersReducedMotion: 'Sec-CH-Prefers-Reduced-Motion', diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 63e23cb..98e4aee 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,12 +1,12 @@ import type { Nuxt } from '@nuxt/schema' -import type { Resolver } from '@nuxt/kit' +import { addDevServerHandler, type Resolver } from '@nuxt/kit' import { // addDevServerHandler, // addDevServerHandler, // addServerHandler, // addServerImportsDir, addPlugin, - addPluginTemplate, + addPluginTemplate, addServerHandler, addServerPlugin, } from '@nuxt/kit' // import defu from 'defu' @@ -163,7 +163,24 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { } if (useServerImages?.length) { - addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) + /* addServerHandler({ + handler: resolver.resolve(runtimeDir, 'server/index'), + route: '', + middleware: true, + lazy: true, + }) */ + // addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) + /* addServerHandler({ + handler: resolver.resolve(runtimeDir, 'server/index'), + route: '', + middleware: true, + lazy: true, + }) */ + /* addDevServerHandler({ + // @ts-expect-error ignore types + handler: resolver.resolve(runtimeDir, 'server/index'), + route: '', + }) */ // todo: check dev handlers and event handler in build + node ... // there is no way to have the plugin working in dev mode: the dev handler called for jpg images // running build + node ... the plugin is registered but the image event handler is not called for jpg images From 7d4f3ea19d9b0390dde351636596c9d04f15ed80 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 27 Oct 2024 20:34:12 +0100 Subject: [PATCH 15/18] chore: use v4 compat. in playground chore: cleanup playground chore: export server utils subpackage instead using nitro imports --- package.json | 14 +- playground/{ => app}/app.vue | 0 playground/{ => app}/plugins/hints.client.ts | 0 playground/{ => app}/plugins/hints.server.ts | 0 playground/modules/image.ts | 21 +++ playground/nuxt.config.ts | 16 +- playground/package.json | 1 - playground/server/dev-image.ts | 95 ++++++---- playground/server/image.ts | 114 +++++++----- playground/server/utils/image.ts | 62 ------- pnpm-lock.yaml | 185 +++++++++++++++---- src/runtime/server/index.ts | 63 ------- src/runtime/server/plugin.ts | 71 ------- src/runtime/server/utils.ts | 55 ++++++ src/utils/configuration.ts | 41 +--- 15 files changed, 382 insertions(+), 356 deletions(-) rename playground/{ => app}/app.vue (100%) rename playground/{ => app}/plugins/hints.client.ts (100%) rename playground/{ => app}/plugins/hints.server.ts (100%) create mode 100644 playground/modules/image.ts delete mode 100644 playground/server/utils/image.ts delete mode 100644 src/runtime/server/index.ts delete mode 100644 src/runtime/server/plugin.ts create mode 100644 src/runtime/server/utils.ts diff --git a/package.json b/package.json index 41c1533..7c5e1c1 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,19 @@ "types": "./dist/types.d.mts", "default": "./dist/module.mjs" }, + "./server-utils": { + "types": "./dist/runtime/server/utils.d.ts", + "default": "./dist/runtime/server/utils.js" + }, "./package.json": "./package.json" }, "main": "./dist/module.mjs", "types": "./dist/types.d.ts", + "typesVersions": { + "*": { + "server-utils": ["./dist/runtime/server/utils.d.ts"] + } + }, "files": [ "dist" ], @@ -44,10 +53,10 @@ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit" }, "dependencies": { - "@nuxt/kit": "^3.13.2", "detect-browser-es": "^0.1.1" }, "devDependencies": { + "@nuxt/kit": "^3.13.2", "@nuxt/devtools": "^1.5.0", "@nuxt/eslint-config": "^0.5.7", "@nuxt/module-builder": "^0.8.4", @@ -62,6 +71,9 @@ "vitest": "^2.1.1", "vue-tsc": "^2.1.6" }, + "build": { + "externals": ["h3"] + }, "stackblitz": { "installDependencies": false, "startCommand": "pnpm install && pnpm dev:prepare && pnpm dev" diff --git a/playground/app.vue b/playground/app/app.vue similarity index 100% rename from playground/app.vue rename to playground/app/app.vue diff --git a/playground/plugins/hints.client.ts b/playground/app/plugins/hints.client.ts similarity index 100% rename from playground/plugins/hints.client.ts rename to playground/app/plugins/hints.client.ts diff --git a/playground/plugins/hints.server.ts b/playground/app/plugins/hints.server.ts similarity index 100% rename from playground/plugins/hints.server.ts rename to playground/app/plugins/hints.server.ts 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 c222019..b70a784 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,10 +1,12 @@ -import DevImage from './server/dev-image' - export default defineNuxtConfig({ compatibilityDate: '2024-10-11', devtools: { enabled: true }, modules: ['../src/module'], + future: { + compatibilityVersion: 4, + }, + httpClientHints: { detectBrowser: true, detectOS: 'windows-11', @@ -20,17 +22,17 @@ export default defineNuxtConfig({ }, nitro: { - handlers: [ + /* handlers: [ { middleware: true, // route: '', - handler: '~/server/image', + handler: './server/image', }, - ], - devHandlers: [{ + ], */ + /* devHandlers: [{ route: '', handler: DevImage, - }], + }], */ }, }) diff --git a/playground/package.json b/playground/package.json index 359ab60..d176752 100644 --- a/playground/package.json +++ b/playground/package.json @@ -9,7 +9,6 @@ }, "dependencies": { "nuxt": "^3.13.2", - "nuxt-http-client-hints": "workspace:*", "sharp": "^0.33.5" }, "devDependencies": { diff --git a/playground/server/dev-image.ts b/playground/server/dev-image.ts index 5923ee6..b3ea4a1 100644 --- a/playground/server/dev-image.ts +++ b/playground/server/dev-image.ts @@ -1,42 +1,71 @@ import { lstat, readFile } from 'node:fs/promises' import { resolve } from 'node:path' -import { eventHandler } from 'h3' -import { useNitro } from '@nuxt/kit' +import { Readable } from 'node:stream' +import { lazyEventHandler, eventHandler, sendStream } from 'h3' import sharp from 'sharp' -import type { HttpClientHintsState } from '../../src/runtime/shared-types/types' -import { extractHTTPClientHints } from './utils/image' +import { useNitro } from '@nuxt/kit' +import { extractImageClientHints } from '../../src/runtime/server/utils' +import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from '../../src/runtime/server/utils' -export default eventHandler(async (event) => { - console.log('dev-image:', event.path) - await extractHTTPClientHints(event) - const httpClientHintsState: HttpClientHintsState | undefined = event.context.httpClientHints - const { widthAvailable = false, width = -1 } = httpClientHintsState?.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') - event.node.res.end(image) - } +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)), } -}) -async function convertImage(path: string, width: number) { - if (path.startsWith('/')) { - path = path.slice(1) - } - const nitro = useNitro() - const folders = nitro.options.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 sharp(await readFile(image)).resize({ width }).toBuffer() + 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 } } - catch (_) {} } -} +}) diff --git a/playground/server/image.ts b/playground/server/image.ts index 4f3a52f..9e44463 100644 --- a/playground/server/image.ts +++ b/playground/server/image.ts @@ -1,61 +1,83 @@ -import { useAppConfig } from 'nitropack/runtime' -import { parseUserAgent } from 'detect-browser-es' -import type { - HttpClientHintsState, - ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../../src/runtime/shared-types/types' -import { extractBrowser } from '../../src/runtime/utils/detect' -import { extractDeviceHints } from '../../src/runtime/utils/device' -import { extractNetworkHints } from '../../src/runtime/utils/network' -import { extractCriticalHints } from '../../src/runtime/utils/critical' +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 '../../src/runtime/server/utils' +import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from '../../src/runtime/server/utils' +// import { readAsset } from '#internal/nitro/virtual/public-assets-data' -export default defineEventHandler(async (event) => { - console.log('request', useAppConfig().httpClientHints) +export default lazyEventHandler(() => { + const appConfig = useAppConfig() + const nitroApp = useNitroApp() const { serverImages, ...rest - } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions + } = appConfig.httpClientHints as ServerHttpClientHintsOptions const options: ResolvedHttpClientHintsOptions = { ...rest, serverImages: serverImages.map(r => new RegExp(r)), } - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - try { - // expose the client hints in the context - const url = event.path - console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) - if (options.serverImages?.some(r => url.match(r))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + 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)) + } } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + }) + + async function convertImage(path: string, width: number) { + /* try { + const image = await readAsset(path) + if (image) { + return await sharp(image).resize({ width }).toBuffer() } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + 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() } - event.context.httpClientHintsOptions = options - event.context.httpClientHints = clientHints + } + catch { + // just ignore } } - catch (err) { - console.error(err) - } + // } + + nitroApp.h3App.stack.unshift({ + route: '', + handler, + }) + + return eventHandler(() => {}) }) diff --git a/playground/server/utils/image.ts b/playground/server/utils/image.ts deleted file mode 100644 index 203173e..0000000 --- a/playground/server/utils/image.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useAppConfig } from 'nitropack/runtime' -import type { H3Event } from 'h3' -import { parseUserAgent } from 'detect-browser-es' -import { useNitro } from '@nuxt/kit' -import type { - HttpClientHintsState, - ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../../../src/runtime/shared-types/types' -import { extractBrowser } from '../../../src/runtime/utils/detect' -import { extractDeviceHints } from '../../../src/runtime/utils/device' -import { extractNetworkHints } from '../../../src/runtime/utils/network' -import { extractCriticalHints } from '../../../src/runtime/utils/critical' - -export async function extractHTTPClientHints(event: H3Event) { - const { - serverImages, - ...rest - } = useNitro().options.appConfig.httpClientHints as ServerHttpClientHintsOptions - const options: ResolvedHttpClientHintsOptions = { - ...rest, - serverImages: serverImages.map(r => new RegExp(r)), - } - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - - try { - // expose the client hints in the context - const url = event.path - console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) - if (options.serverImages?.some(r => url.match(r))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) - } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) - } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } - event.context.httpClientHintsOptions = options - event.context.httpClientHints = clientHints - } - } - catch (err) { - console.error(err) - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0bbaf3..9c2700d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@nuxt/kit': - specifier: ^3.13.2 - version: 3.13.2(magicast@0.3.5)(rollup@3.29.5) detect-browser-es: specifier: ^0.1.1 version: 0.1.1 @@ -21,6 +18,9 @@ importers: '@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@3.29.5))(nuxi@3.14.0)(typescript@5.6.3)(vue-tsc@2.1.6(typescript@5.6.3)) @@ -59,7 +59,7 @@ importers: dependencies: nuxt: specifier: ^3.13.2 - 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@4.24.0)(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)) + 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)) nuxt-http-client-hints: specifier: workspace:* version: link:.. @@ -1547,6 +1547,9 @@ packages: '@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==} @@ -2589,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'} @@ -4076,12 +4087,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: @@ -5384,12 +5395,12 @@ snapshots: - 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.7.6)(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))': 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.6)(terser@5.34.1) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) transitivePeerDependencies: - magicast - rollup @@ -5442,7 +5453,7 @@ 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@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)) @@ -5457,13 +5468,13 @@ snapshots: - vue - webpack-sources - '@nuxt/devtools@1.5.2(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.6)(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))': 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.6)(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.6)(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 @@ -5490,11 +5501,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.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@4.24.0)(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)) + 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: @@ -5800,12 +5811,12 @@ snapshots: - vue-tsc - webpack-sources - '@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@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))': 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.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)) + '@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 @@ -5831,9 +5842,9 @@ snapshots: 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)) + 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: @@ -6108,7 +6119,7 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.7.6 + '@types/node': 22.8.1 '@types/json-schema@7.0.15': {} @@ -6116,6 +6127,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.8.1': + dependencies: + undici-types: 6.19.8 + '@types/normalize-package-data@2.4.4': {} '@types/resolve@1.20.2': {} @@ -6257,11 +6272,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@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: + '@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': dependencies: '@vitest/spy': 2.1.2 @@ -6419,6 +6449,18 @@ snapshots: 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 + '@vue/devtools-kit@7.4.4': dependencies: '@vue/devtools-shared': 7.4.6 @@ -7471,6 +7513,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 @@ -8062,7 +8108,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) @@ -8345,14 +8391,14 @@ snapshots: - webpack-sources - xml2js - 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@4.24.0)(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)): + 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.6)(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.6)(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 @@ -8413,7 +8459,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.6 + '@types/node': 22.8.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9257,12 +9303,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 @@ -9546,6 +9592,10 @@ snapshots: dependencies: 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 @@ -9563,6 +9613,23 @@ snapshots: - supports-color - terser + 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.8.1)(terser@5.34.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - 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.6)(terser@5.34.1))(vue-tsc@2.1.6(typescript@5.6.3)): dependencies: '@babel/code-frame': 7.25.7 @@ -9586,6 +9653,29 @@ snapshots: 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 + 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.8.1)(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-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 @@ -9604,7 +9694,7 @@ snapshots: - 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.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@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) @@ -9615,7 +9705,7 @@ snapshots: 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) + vite: 5.4.8(@types/node@22.8.1)(terser@5.34.1) optionalDependencies: '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@3.29.5) transitivePeerDependencies: @@ -9637,6 +9727,21 @@ snapshots: transitivePeerDependencies: - supports-color + 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 @@ -9647,6 +9752,16 @@ snapshots: 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.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@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@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)) diff --git a/src/runtime/server/index.ts b/src/runtime/server/index.ts deleted file mode 100644 index a954591..0000000 --- a/src/runtime/server/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { eventHandler, lazyEventHandler } from 'h3' -import { parseUserAgent } from 'detect-browser-es' -import { useNitro } from '@nuxt/kit' -import type { - HttpClientHintsState, - ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../shared-types/types' -import { extractBrowser } from '../utils/detect' -import { extractCriticalHints } from '../utils/critical' -import { extractDeviceHints } from '../utils/device' -import { extractNetworkHints } from '../utils/network' - -export default lazyEventHandler(() => { - const { - serverImages, - ...rest - } = useNitro().options.appConfig.httpClientHints as ServerHttpClientHintsOptions - const options: ResolvedHttpClientHintsOptions = { - ...rest, - serverImages: serverImages.map(r => new RegExp(r)), - } - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - - return eventHandler(async (event) => { - try { - // expose the client hints in the context - const url = event.path - console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) - if (options.serverImages?.some(r => url.match(r))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) - } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) - } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } - event.context.httpClientHintsOptions = options - event.context.httpClientHints = clientHints - } - } - catch (err) { - console.error(err) - } - }) -}) diff --git a/src/runtime/server/plugin.ts b/src/runtime/server/plugin.ts deleted file mode 100644 index a05407a..0000000 --- a/src/runtime/server/plugin.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { defineNitroPlugin, useAppConfig } from 'nitropack/runtime' -import { parseUserAgent } from 'detect-browser-es' -import type { - HttpClientHintsState, - ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../shared-types/types' -import { extractBrowser } from '../utils/detect' -import { extractCriticalHints } from '../utils/critical' -import { extractDeviceHints } from '../utils/device' -import { extractNetworkHints } from '../utils/network' - -export default defineNitroPlugin((nitroApp) => { - const { - serverImages, - ...rest - } = useAppConfig().httpClientHints as ServerHttpClientHintsOptions - const options: ResolvedHttpClientHintsOptions = { - ...rest, - serverImages: serverImages.map(r => new RegExp(r)), - } - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - - // todo: remove this just for testing purposes - nitroApp.hooks.hook('afterResponse', async (event) => { - // I guess the consumer should add the Vary header; there are a lot of headers here to handle. - const receivedOptions = event.context.httpClientHintsOptions - if (receivedOptions) { - console.log(`Client Hints for ${event.path}`, event.context.httpClientHints) - } - }) - - nitroApp.hooks.hook('request', async (event) => { - try { - // expose the client hints in the context - const url = event.path - console.log('request', { url, match: options.serverImages?.some(r => url.match(r)) }) - if (options.serverImages?.some(r => url.match(r))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) - } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) - } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } - event.context.httpClientHintsOptions = options - event.context.httpClientHints = clientHints - } - } - catch (err) { - console.error(err) - } - }) -}) diff --git a/src/runtime/server/utils.ts b/src/runtime/server/utils.ts new file mode 100644 index 0000000..0e386b9 --- /dev/null +++ b/src/runtime/server/utils.ts @@ -0,0 +1,55 @@ +import type { H3Event } from 'h3' +import { parseUserAgent } from 'detect-browser-es' +import type { + HttpClientHintsState, + ResolvedHttpClientHintsOptions, + ServerHttpClientHintsOptions, +} from '../shared-types/types' +import { extractBrowser } from '../utils/detect' +import { extractDeviceHints } from '../utils/device' +import { extractNetworkHints } from '../utils/network' +import { extractCriticalHints } from '../utils/critical' + +export type { HttpClientHintsState, ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } + +export interface ServerImageClientHints { + httpClientHintsOptions: ResolvedHttpClientHintsOptions + httpClientHints: HttpClientHintsState +} + +export async function extractImageClientHints(event: H3Event, options: ResolvedHttpClientHintsOptions): Promise { + const critical = !!options.critical + const device = options.device.length > 0 + const network = options.network.length > 0 + const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 + + // expose the client hints in the context + const url = event.path + if (options.serverImages?.some(r => url.match(r))) { + const userAgentHeader = event.headers.get('user-agent') + const requestHeaders: { [key in Lowercase]?: string } = {} + for (const [key, value] of event.headers.entries()) { + requestHeaders[key.toLowerCase() as Lowercase] = value + } + const userAgent = userAgentHeader + ? parseUserAgent(userAgentHeader) + : null + const clientHints: HttpClientHintsState = {} + if (detect) { + clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) + } + if (device) { + clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) + } + if (network) { + clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) + } + if (critical) { + clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) + } + return { + httpClientHintsOptions: options, + httpClientHints: clientHints, + } satisfies ServerImageClientHints + } +} diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 98e4aee..7c98595 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -1,15 +1,6 @@ import type { Nuxt } from '@nuxt/schema' -import { addDevServerHandler, type Resolver } from '@nuxt/kit' -import { - // addDevServerHandler, - // addDevServerHandler, - // addServerHandler, - // addServerImportsDir, - addPlugin, - addPluginTemplate, addServerHandler, - addServerPlugin, -} from '@nuxt/kit' -// import defu from 'defu' +import type { Resolver } from '@nuxt/kit' +import { addPlugin, addPluginTemplate } from '@nuxt/kit' import type { HttpClientHintsOptions } from '../types' import type { ResolvedHttpClientHintsOptions } from '../runtime/shared-types/types' @@ -150,7 +141,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { const useServerImages = serverImages ? serverImages === true - ? [/\.(png|jpeg|jpg|webp|avi)$/] + ? [/\.(png|jpeg|jpg|webp|avif|tiff|gif)$/] : Array.isArray(serverImages) ? serverImages : [serverImages] @@ -159,31 +150,7 @@ export function configure(ctx: HttpClientHintsContext, nuxt: Nuxt) { const { serverImages: _, ...rest } = resolvedOptions nuxt.options.appConfig.httpClientHints = { ...rest, - serverImages: useServerImages ? useServerImages.map(r => r.source) : undefined, - } - - if (useServerImages?.length) { - /* addServerHandler({ - handler: resolver.resolve(runtimeDir, 'server/index'), - route: '', - middleware: true, - lazy: true, - }) */ - // addServerPlugin(resolver.resolve(runtimeDir, 'server/plugin')) - /* addServerHandler({ - handler: resolver.resolve(runtimeDir, 'server/index'), - route: '', - middleware: true, - lazy: true, - }) */ - /* addDevServerHandler({ - // @ts-expect-error ignore types - handler: resolver.resolve(runtimeDir, 'server/index'), - route: '', - }) */ - // todo: check dev handlers and event handler in build + node ... - // there is no way to have the plugin working in dev mode: the dev handler called for jpg images - // running build + node ... the plugin is registered but the image event handler is not called for jpg images + serverImages: useServerImages ? useServerImages.map(r => r.source) : [], } addClientHintsPlugin('client') From acde42f69babc1e0139dd76228506a3e99d164be Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 27 Oct 2024 20:58:32 +0100 Subject: [PATCH 16/18] chore: update lock file --- package.json | 3 --- pnpm-lock.yaml | 3 --- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index 7c5e1c1..c03a597 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,6 @@ "vitest": "^2.1.1", "vue-tsc": "^2.1.6" }, - "build": { - "externals": ["h3"] - }, "stackblitz": { "installDependencies": false, "startCommand": "pnpm install && pnpm dev:prepare && pnpm dev" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c2700d..189637e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,9 +60,6 @@ importers: 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)) - nuxt-http-client-hints: - specifier: workspace:* - version: link:.. sharp: specifier: ^0.33.5 version: 0.33.5 From 32965d5b37a94e0cbd8847c355292e6b0b2964e1 Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 28 Oct 2024 14:47:03 +0100 Subject: [PATCH 17/18] chore: use `http-client-hints` --- README.md | 2 + eslint.config.mjs | 4 +- package.json | 14 +- playground/server/dev-image.ts | 4 +- playground/server/image.ts | 4 +- pnpm-lock.yaml | 17 ++ src/module.ts | 2 +- src/runtime/plugins/critical.server.ts | 4 +- src/runtime/plugins/detect.client.ts | 2 +- src/runtime/plugins/detect.server.ts | 6 +- src/runtime/plugins/device.server.ts | 6 +- src/runtime/plugins/network.server.ts | 4 +- src/runtime/plugins/types.d.ts | 2 +- src/runtime/plugins/utils.ts | 4 +- src/runtime/server/utils.ts | 55 ---- src/runtime/shared-types/types.ts | 186 -------------- src/runtime/utils/critical.ts | 335 ------------------------- src/runtime/utils/detect.ts | 42 ---- src/runtime/utils/device.ts | 117 --------- src/runtime/utils/features.ts | 30 --- src/runtime/utils/headers.ts | 50 ---- src/runtime/utils/network.ts | 139 ---------- src/types.ts | 2 +- src/utils/configuration.ts | 2 +- 24 files changed, 46 insertions(+), 987 deletions(-) delete mode 100644 src/runtime/server/utils.ts delete mode 100644 src/runtime/shared-types/types.ts delete mode 100644 src/runtime/utils/critical.ts delete mode 100644 src/runtime/utils/detect.ts delete mode 100644 src/runtime/utils/device.ts delete mode 100644 src/runtime/utils/features.ts delete mode 100644 src/runtime/utils/headers.ts delete mode 100644 src/runtime/utils/network.ts 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/eslint.config.mjs b/eslint.config.mjs index 27b2e71..0d65d4d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,8 @@ export default createConfigForNuxt({ ], }, }) - .append( + .append([{ + ignores: ['server-utils.d.ts'], + }], // your custom flat config here... ) diff --git a/package.json b/package.json index c03a597..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", @@ -25,19 +25,10 @@ "types": "./dist/types.d.mts", "default": "./dist/module.mjs" }, - "./server-utils": { - "types": "./dist/runtime/server/utils.d.ts", - "default": "./dist/runtime/server/utils.js" - }, "./package.json": "./package.json" }, "main": "./dist/module.mjs", "types": "./dist/types.d.ts", - "typesVersions": { - "*": { - "server-utils": ["./dist/runtime/server/utils.d.ts"] - } - }, "files": [ "dist" ], @@ -53,7 +44,8 @@ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit" }, "dependencies": { - "detect-browser-es": "^0.1.1" + "detect-browser-es": "^0.1.1", + "http-client-hints": "^0.0.1" }, "devDependencies": { "@nuxt/kit": "^3.13.2", diff --git a/playground/server/dev-image.ts b/playground/server/dev-image.ts index b3ea4a1..fe97982 100644 --- a/playground/server/dev-image.ts +++ b/playground/server/dev-image.ts @@ -4,8 +4,8 @@ import { Readable } from 'node:stream' import { lazyEventHandler, eventHandler, sendStream } from 'h3' import sharp from 'sharp' import { useNitro } from '@nuxt/kit' -import { extractImageClientHints } from '../../src/runtime/server/utils' -import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from '../../src/runtime/server/utils' +import { extractImageClientHints } from 'http-client-hints/h3' +import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from 'http-client-hints/h3' export default lazyEventHandler(async () => { const nitroOptions = useNitro().options diff --git a/playground/server/image.ts b/playground/server/image.ts index 9e44463..6080ef3 100644 --- a/playground/server/image.ts +++ b/playground/server/image.ts @@ -4,8 +4,8 @@ import { Readable } from 'node:stream' import { fileURLToPath } from 'node:url' import { lazyEventHandler, eventHandler, sendStream } from 'h3' import sharp from 'sharp' -import { extractImageClientHints } from '../../src/runtime/server/utils' -import type { ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } from '../../src/runtime/server/utils' +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(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 189637e..e87b74c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: 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 @@ -2792,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'} @@ -7723,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 diff --git a/src/module.ts b/src/module.ts index a82bb9c..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 } diff --git a/src/runtime/plugins/critical.server.ts b/src/runtime/plugins/critical.server.ts index a1f6116..4d7b6e2 100644 --- a/src/runtime/plugins/critical.server.ts +++ b/src/runtime/plugins/critical.server.ts @@ -1,6 +1,6 @@ import type { parseUserAgent } from 'detect-browser-es' -import { CriticalHintsHeaders, extractCriticalHints } from '../utils/critical' -import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' +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' 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 a5df457..d60f1db 100644 --- a/src/runtime/plugins/detect.server.ts +++ b/src/runtime/plugins/detect.server.ts @@ -6,8 +6,8 @@ import { parseUserAgent, } from 'detect-browser-es' import { appendHeader } from 'h3' -import type { ResolvedHttpClientHintsOptions, UserAgentHints } from '../shared-types/types' -import { extractBrowser } from '../utils/detect' +import type { ResolvedHttpClientHintsOptions, UserAgentHints } from 'http-client-hints' +import { extractBrowserHints } from 'http-client-hints/detect' import { useHttpClientHintsState } from './utils' import { defineNuxtPlugin, @@ -30,7 +30,7 @@ const plugin: Plugin = defineNuxtPlugin({ const userAgentHeader = requestHeaders['user-agent'] - const browser = await extractBrowser( + const browser = await extractBrowserHints( httpClientHints, requestHeaders, userAgentHeader, diff --git a/src/runtime/plugins/device.server.ts b/src/runtime/plugins/device.server.ts index b4bec71..a28cdd7 100644 --- a/src/runtime/plugins/device.server.ts +++ b/src/runtime/plugins/device.server.ts @@ -1,6 +1,6 @@ import type { parseUserAgent } from 'detect-browser-es' -import { extractDeviceHints, HttpRequestHeaders } from '../utils/device' -import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' +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' @@ -17,7 +17,7 @@ const plugin: Plugin = defineNuxtPlugin({ const httpClientHints = ssrContext._httpClientHintsOptions as ResolvedHttpClientHintsOptions const userAgent = ssrContext._httpClientHintsUserAgent as ReturnType const state = useHttpClientHintsState() - const requestHeaders = useRequestHeaders(HttpRequestHeaders) + const requestHeaders = useRequestHeaders(DeviceHintsHeaders) state.value.device = extractDeviceHints(httpClientHints, requestHeaders, userAgent, writeHeaders) }, }) diff --git a/src/runtime/plugins/network.server.ts b/src/runtime/plugins/network.server.ts index 582f740..4fa20ba 100644 --- a/src/runtime/plugins/network.server.ts +++ b/src/runtime/plugins/network.server.ts @@ -1,6 +1,6 @@ import type { parseUserAgent } from 'detect-browser-es' -import { extractNetworkHints, NetworkHintsHeaders } from '../utils/network' -import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' +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' 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 index 5b13a78..11973e3 100644 --- a/src/runtime/plugins/utils.ts +++ b/src/runtime/plugins/utils.ts @@ -1,8 +1,8 @@ import type { HttpClientHintsState, ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../shared-types/types' +} from 'http-client-hints' +import type { ServerHttpClientHintsOptions } from 'http-client-hints/h3' import { useAppConfig, useState } from '#imports' export function useHttpClientHintsState() { diff --git a/src/runtime/server/utils.ts b/src/runtime/server/utils.ts deleted file mode 100644 index 0e386b9..0000000 --- a/src/runtime/server/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { H3Event } from 'h3' -import { parseUserAgent } from 'detect-browser-es' -import type { - HttpClientHintsState, - ResolvedHttpClientHintsOptions, - ServerHttpClientHintsOptions, -} from '../shared-types/types' -import { extractBrowser } from '../utils/detect' -import { extractDeviceHints } from '../utils/device' -import { extractNetworkHints } from '../utils/network' -import { extractCriticalHints } from '../utils/critical' - -export type { HttpClientHintsState, ResolvedHttpClientHintsOptions, ServerHttpClientHintsOptions } - -export interface ServerImageClientHints { - httpClientHintsOptions: ResolvedHttpClientHintsOptions - httpClientHints: HttpClientHintsState -} - -export async function extractImageClientHints(event: H3Event, options: ResolvedHttpClientHintsOptions): Promise { - const critical = !!options.critical - const device = options.device.length > 0 - const network = options.network.length > 0 - const detect = options.detectOS || options.detectBrowser || options.userAgent.length > 0 - - // expose the client hints in the context - const url = event.path - if (options.serverImages?.some(r => url.match(r))) { - const userAgentHeader = event.headers.get('user-agent') - const requestHeaders: { [key in Lowercase]?: string } = {} - for (const [key, value] of event.headers.entries()) { - requestHeaders[key.toLowerCase() as Lowercase] = value - } - const userAgent = userAgentHeader - ? parseUserAgent(userAgentHeader) - : null - const clientHints: HttpClientHintsState = {} - if (detect) { - clientHints.browser = await extractBrowser(options, requestHeaders as Record, userAgentHeader ?? undefined) - } - if (device) { - clientHints.device = extractDeviceHints(options, requestHeaders, userAgent) - } - if (network) { - clientHints.network = extractNetworkHints(options, requestHeaders, userAgent) - } - if (critical) { - clientHints.critical = extractCriticalHints(options, requestHeaders, userAgent) - } - return { - httpClientHintsOptions: options, - httpClientHints: clientHints, - } satisfies ServerImageClientHints - } -} diff --git a/src/runtime/shared-types/types.ts b/src/runtime/shared-types/types.ts deleted file mode 100644 index dc71466..0000000 --- a/src/runtime/shared-types/types.ts +++ /dev/null @@ -1,186 +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 - 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 - serverImages?: RegExp[] -} - -export interface ServerHttpClientHintsOptions extends Omit { - serverImages: string[] -} diff --git a/src/runtime/utils/critical.ts b/src/runtime/utils/critical.ts deleted file mode 100644 index be04821..0000000 --- a/src/runtime/utils/critical.ts +++ /dev/null @@ -1,335 +0,0 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { - CriticalClientHintsConfiguration, - CriticalInfo, - ResolvedHttpClientHintsOptions, -} from '../shared-types/types' -import { lookupHeader, writeClientHintHeaders } from './headers' -import { browserFeatureAvailable } from './features' - -// TODO: add `Sec-CH-Prefers-Contrast` and `Sec-CH-Forced-Colors` headers -// - https://github.com/WICG/user-preference-media-features-headers -// - https://browserleaks.com/client-hints#:~:text=Sec%2DCH%2DWidth%20gives%20a,user%2Dagent's%20current%20viewport%20height. - -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 -export const CriticalHintsHeaders = Array.from(Object.values(AcceptClientHintsRequestHeaders)).concat('user-agent', 'cookie', SecChUaMobile) - -export function extractCriticalHints( - httpClientHints: ResolvedHttpClientHintsOptions, - requestHeaders: { [key in Lowercase]?: string }, - userAgent: ReturnType, - writeHeaders?: (headers: Record) => void, - writeCookie?: (cookieName: string, path: string, expires: Date, themeName: string) => void, -): CriticalInfo { - // 1. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.critical!, requestHeaders) - // 2. write client hints response headers - if (writeHeaders) { - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.critical!, writeHeaders) - } - // 3. send the theme cookie to the client when required - clientHintsRequest.colorSchemeCookie = writeThemeCookie( - clientHintsRequest, - httpClientHints.critical!, - writeCookie, - ) - - return clientHintsRequest -} - -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, - writeHeaders: (headers: Record) => void, -) { - // 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, - writeCookie?: (cookieName: string, path: string, expires: Date, themeName: string) => void, -) { - 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 (writeCookie && (!criticalInfo.firstRequest || !criticalClientHintsConfiguration.reloadOnFirstRequest)) { - writeCookie(cookieName, path, expires, themeName) - } - - return `${cookieName}=${themeName}; Path=${path}; Expires=${expires.toUTCString()}; SameSite=Lax` -} diff --git a/src/runtime/utils/detect.ts b/src/runtime/utils/detect.ts deleted file mode 100644 index 8cb61cb..0000000 --- a/src/runtime/utils/detect.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { BrowserInfo } from 'detect-browser-es' -import { asyncDetect, detect, serverResponseHeadersForUserAgentHints } from 'detect-browser-es' -import type { ResolvedHttpClientHintsOptions } from '../shared-types/types' - -export async function extractBrowser( - httpClientHints: ResolvedHttpClientHintsOptions, - requestHeaders: Record, - userAgentHeader?: string, - writeHeaders?: (headers: Record) => void, -): Promise { - 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 - if (typeof writeHeaders === 'function') { - const headers = serverResponseHeadersForUserAgentHints(hints) - if (headers) { - const useHeader: Record = {} - for (const [n, value] of Object.entries(headers)) { - if (value) { - useHeader[n] = [value] - } - } - writeHeaders(useHeader) - } - } - // detect browser info - return (await asyncDetect({ - hints, - httpHeaders: requestHeaders, - })) as BrowserInfo - } - else if (userAgentHeader) { - return detect(userAgentHeader) as BrowserInfo - } - - return undefined -} diff --git a/src/runtime/utils/device.ts b/src/runtime/utils/device.ts deleted file mode 100644 index 18a2c95..0000000 --- a/src/runtime/utils/device.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { DeviceHints, DeviceInfo, ResolvedHttpClientHintsOptions } from '../shared-types/types' -import type { GetHeaderType } from './headers' -import { lookupHeader, writeClientHintHeaders } from './headers' -import { browserFeatureAvailable } from './features' - -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>) - -export const HttpRequestHeaders = Array.from(Object.values(DeviceClientHintsHeaders)).concat('user-agent') - -export function extractDeviceHints( - httpClientHints: ResolvedHttpClientHintsOptions, - requestHeaders: { [key in Lowercase]?: string }, - userAgent: ReturnType, - writeHeaders?: (headers: Record) => void, -): DeviceInfo { - // 1. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.device!, requestHeaders) - // 2. write client hints response headers - if (writeHeaders) { - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.device!, writeHeaders) - } - - return clientHintsRequest -} - -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[], - writeHeaders: (headers: Record) => void, -) { - const headers: Record = {} - - for (const hint of deviceHints) { - if (deviceInfo[`${hint}Available`]) { - writeClientHintHeaders(ClientHeaders, DeviceClientHintsHeaders[hint], headers) - } - } - - writeHeaders(headers) -} diff --git a/src/runtime/utils/features.ts b/src/runtime/utils/features.ts deleted file mode 100644 index a3c8834..0000000 --- a/src/runtime/utils/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/utils/headers.ts b/src/runtime/utils/headers.ts deleted file mode 100644 index c461183..0000000 --- a/src/runtime/utils/headers.ts +++ /dev/null @@ -1,50 +0,0 @@ -export function writeClientHintHeaders(headerNames: string[], key: string, headers: Record) { - headerNames.forEach((header) => { - headers[header] = (headers[header] ? headers[header] : []).concat(key) - }) -} - -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/utils/network.ts b/src/runtime/utils/network.ts deleted file mode 100644 index 6d37591..0000000 --- a/src/runtime/utils/network.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { Browser, parseUserAgent } from 'detect-browser-es' -import type { NetworkHints, NetworkInfo, ResolvedHttpClientHintsOptions } from '../shared-types/types' -import type { GetHeaderType } from './headers' -import { lookupHeader, writeClientHintHeaders } from './headers' -import { browserFeatureAvailable } from './features' - -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>) - -export const NetworkHintsHeaders = Array.from(Object.values(NetworkClientHintsHeaders)).concat('user-agent') - -export function extractNetworkHints( - httpClientHints: ResolvedHttpClientHintsOptions, - requestHeaders: { [key in Lowercase]?: string }, - userAgent: ReturnType, - writeHeaders?: (headers: Record) => void, -): NetworkInfo { - // 1. prepare client hints request - const clientHintsRequest = collectClientHints(userAgent, httpClientHints.network!, requestHeaders) - // 2. write client hints response headers - if (writeHeaders) { - writeClientHintsResponseHeaders(clientHintsRequest, httpClientHints.network!, writeHeaders) - } - - return clientHintsRequest -} - -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[], - writeHeaders: (headers: Record) => void, -) { - const headers: Record = {} - - for (const hint of networkHints) { - if (networkInfo[`${hint}Available`]) { - writeClientHintHeaders(ClientHeaders, NetworkClientHintsHeaders[hint], headers) - } - } - - writeHeaders(headers) -} diff --git a/src/types.ts b/src/types.ts index 8336ad5..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 } diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 7c98595..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' From 7f9fffb634f538b9aede6d2c5c5fc64934caf02d Mon Sep 17 00:00:00 2001 From: userquin Date: Mon, 28 Oct 2024 14:51:49 +0100 Subject: [PATCH 18/18] chore: cleanup eslint config file --- eslint.config.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 0d65d4d..27b2e71 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,8 +15,6 @@ export default createConfigForNuxt({ ], }, }) - .append([{ - ignores: ['server-utils.d.ts'], - }], + .append( // your custom flat config here... )