From 5886d49accfc6d766f2f9e1652b55afbff4b0549 Mon Sep 17 00:00:00 2001 From: onmax Date: Fri, 24 Jul 2026 15:17:31 +0700 Subject: [PATCH 1/5] fix: support Nitro 2 and 3 server runtimes --- .github/workflows/ci.yml | 35 +++++++++++++++++++ packages/script/src/module.ts | 16 ++++++++- .../src/runtime/server/bluesky-embed.ts | 3 +- .../server/google-maps-geocode-proxy.ts | 1 - .../server/google-static-maps-proxy.ts | 1 - .../src/runtime/server/gravatar-proxy.ts | 1 - .../src/runtime/server/instagram-embed.ts | 3 +- .../src/runtime/server/proxy-handler.ts | 1 - .../runtime/server/utils/cached-upstream.ts | 1 - .../src/runtime/server/utils/withSigning.ts | 3 +- packages/script/src/runtime/server/x-embed.ts | 3 +- test/e2e/proxy-alias.test.ts | 1 + test/fixtures/proxy-alias/nuxt.config.ts | 1 + test/unit/__mocks__/stub-nitro-runtime.ts | 6 ++++ test/unit/cached-upstream.test.ts | 5 +-- test/unit/proxy-handler-alias.test.ts | 7 ++-- test/unit/proxy-handler-body.test.ts | 7 ++-- test/unit/proxy-handler-hop-by-hop.test.ts | 7 ++-- test/unit/with-signing.test.ts | 7 ++-- 19 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 test/unit/__mocks__/stub-nitro-runtime.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 728690e86..edd21d2d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,3 +137,38 @@ jobs: - name: Run tests run: pnpm vitest run --project typecheck --project unit --project e2e --project nuxt-runtime + + nitro-3: + runs-on: ubuntu-24.04-arm + name: Nitro 3 compatibility + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: lts/* + cache: pnpm + + - name: Install dependencies + run: pnpm i + + - name: Install Nuxt 5 + run: | + pnpm add --workspace-root --save-dev "nuxt@npm:nuxt-nightly@5.0.0-29745766.482f3357" "@nuxt/kit@npm:@nuxt/kit-nightly@5.0.0-29745766.482f3357" + pnpm --filter @nuxt/scripts add --save-dev "@nuxt/kit@npm:@nuxt/kit-nightly@5.0.0-29745766.482f3357" + + - name: Dev prepare + run: pnpm --filter @nuxt/scripts dev:prepare + + - name: Nuxt prepare + run: pnpm exec nuxt prepare + + - name: Run Nitro 3 compatibility tests + run: pnpm vitest run --project e2e test/e2e/proxy-alias.test.ts -t 'auto-injects|resolves' diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index cdc99f7c5..b7dd847a4 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -16,6 +16,7 @@ import type { } from './runtime/types' import { randomBytes } from 'node:crypto' import { appendFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { pathToFileURL } from 'node:url' import { addBuildPlugin, addComponentsDir, @@ -23,13 +24,16 @@ import { addPlugin, addPluginTemplate, addServerHandler, + addServerImports, addTemplate, createResolver, defineNuxtModule, + getNuxtVersion, hasNuxtModule, + resolvePath as resolveNuxtPath, } from '@nuxt/kit' import { defu } from 'defu' -import { resolve as resolvePath_ } from 'pathe' +import { dirname, resolve as resolvePath_ } from 'pathe' import { readPackageJSON } from 'pkg-types' import { setupPublicAssetStrategy } from './assets' import { buildDevtoolsData, buildDevtoolsEntry, setupDevtools } from './devtools' @@ -514,6 +518,16 @@ export default defineNuxtModule({ logger.debug('The module is disabled, skipping setup.') return } + if (Number.parseInt(getNuxtVersion(nuxt), 10) >= 5) { + const nuxtDir = dirname(await resolveNuxtPath('nuxt/package.json')) + const nitroDir = dirname(await resolveNuxtPath('@nuxt/nitro-server/package.json', { cwd: nuxtDir })) + const resolveNitroImport = async (id: string) => pathToFileURL(await resolveNuxtPath(id, { cwd: nitroDir })).href + addServerImports([ + { name: 'useNitroApp', from: await resolveNitroImport('nitro/app') }, + { name: 'defineCachedFunction', from: await resolveNitroImport('nitro/cache') }, + { name: 'useRuntimeConfig', from: await resolveNitroImport('nitro/runtime-config') }, + ]) + } if (nuxt.options.dev) { setupDevtools(nuxt, { standalone: config._standaloneDevtools }) if (config._standaloneDevtools) { diff --git a/packages/script/src/runtime/server/bluesky-embed.ts b/packages/script/src/runtime/server/bluesky-embed.ts index 45090faa1..33a6bfb95 100644 --- a/packages/script/src/runtime/server/bluesky-embed.ts +++ b/packages/script/src/runtime/server/bluesky-embed.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteBlueskyPostImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' @@ -115,7 +114,7 @@ export default withSigning(defineEventHandler(async (event) => { const handlerPath = event.path?.split('?')[0] || '' const prefix = handlerPath.replace(EMBED_BSKY_SUFFIX_RE, '') || '/_scripts' const imagePath = `${prefix}/embed/bluesky-image` - const secret = (useRuntimeConfig(event)['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret + const secret = (useRuntimeConfig()['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret rewriteBlueskyPostImages(post, imagePath, secret) // Cache for 10 minutes diff --git a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts index 79df25499..2c10c7d12 100644 --- a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts +++ b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { withQuery } from 'ufo' import { createCachedJsonFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/google-static-maps-proxy.ts b/packages/script/src/runtime/server/google-static-maps-proxy.ts index e79486f99..8e27ec1f7 100644 --- a/packages/script/src/runtime/server/google-static-maps-proxy.ts +++ b/packages/script/src/runtime/server/google-static-maps-proxy.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { withQuery } from 'ufo' import { createCachedBinaryFetch } from './utils/cached-upstream' import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_PARAM } from './utils/sign-constants' diff --git a/packages/script/src/runtime/server/gravatar-proxy.ts b/packages/script/src/runtime/server/gravatar-proxy.ts index 693ae3a59..b4dcdfb17 100644 --- a/packages/script/src/runtime/server/gravatar-proxy.ts +++ b/packages/script/src/runtime/server/gravatar-proxy.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { withQuery } from 'ufo' import { createCachedBinaryFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/instagram-embed.ts b/packages/script/src/runtime/server/instagram-embed.ts index 56d207bda..0bc22639f 100644 --- a/packages/script/src/runtime/server/instagram-embed.ts +++ b/packages/script/src/runtime/server/instagram-embed.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { defineCachedFunction, useRuntimeConfig } from 'nitropack/runtime' import { $fetch } from 'ofetch' import { hash } from 'ohash' import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from 'ultrahtml' @@ -70,7 +69,7 @@ export default withSigning(defineEventHandler(async (event) => { // The route is registered as `/embed/instagram`, so strip `/embed/instagram`. const handlerPath = event.path?.split('?')[0] || '' const prefix = handlerPath.replace(EMBED_INSTAGRAM_SUFFIX_RE, '') || '/_scripts' - const secret = (useRuntimeConfig(event)['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret + const secret = (useRuntimeConfig()['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret const query = getQuery(event) const postUrl = query.url as string diff --git a/packages/script/src/runtime/server/proxy-handler.ts b/packages/script/src/runtime/server/proxy-handler.ts index 5c272a30b..4730939a9 100644 --- a/packages/script/src/runtime/server/proxy-handler.ts +++ b/packages/script/src/runtime/server/proxy-handler.ts @@ -1,6 +1,5 @@ import type { ProxyPrivacyInput, ResolvedProxyPrivacy } from './utils/privacy' import { createError, defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, readRawBody, setResponseHeader, setResponseStatus } from 'h3' -import { useNitroApp, useRuntimeConfig } from 'nitropack/runtime' import { matchDomain } from './utils/match-domain' import { anonymizeIP, diff --git a/packages/script/src/runtime/server/utils/cached-upstream.ts b/packages/script/src/runtime/server/utils/cached-upstream.ts index 931d66f23..ee027d839 100644 --- a/packages/script/src/runtime/server/utils/cached-upstream.ts +++ b/packages/script/src/runtime/server/utils/cached-upstream.ts @@ -1,5 +1,4 @@ import { Buffer } from 'node:buffer' -import { defineCachedFunction } from 'nitropack/runtime' import { $fetch } from 'ofetch' import { hash } from 'ohash' diff --git a/packages/script/src/runtime/server/utils/withSigning.ts b/packages/script/src/runtime/server/utils/withSigning.ts index 495362226..3dc0f99a4 100644 --- a/packages/script/src/runtime/server/utils/withSigning.ts +++ b/packages/script/src/runtime/server/utils/withSigning.ts @@ -22,14 +22,13 @@ import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3' import { createError, defineEventHandler } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { verifyProxyRequest } from './sign' export function withSigning( handler: EventHandler, ) { return defineEventHandler(async (event) => { - const runtimeConfig = useRuntimeConfig(event) + const runtimeConfig = useRuntimeConfig() const scriptsConfig = runtimeConfig['nuxt-scripts'] as { proxySecret?: string, pageTokenMaxAge?: number } | undefined const secret = scriptsConfig?.proxySecret diff --git a/packages/script/src/runtime/server/x-embed.ts b/packages/script/src/runtime/server/x-embed.ts index bca7c8bd4..821e972d5 100644 --- a/packages/script/src/runtime/server/x-embed.ts +++ b/packages/script/src/runtime/server/x-embed.ts @@ -1,5 +1,4 @@ import { createError, defineEventHandler, getQuery, setHeader } from 'h3' -import { useRuntimeConfig } from 'nitropack/runtime' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteTweetImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' @@ -104,7 +103,7 @@ export default withSigning(defineEventHandler(async (event) => { const handlerPath = event.path?.split('?')[0] || '' const prefix = handlerPath.replace(EMBED_X_SUFFIX_RE, '') || '/_scripts' const imagePath = `${prefix}/embed/x-image` - const secret = (useRuntimeConfig(event)['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret + const secret = (useRuntimeConfig()['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret rewriteTweetImages(tweetData, imagePath, secret) // Cache for 10 minutes diff --git a/test/e2e/proxy-alias.test.ts b/test/e2e/proxy-alias.test.ts index 11e2351b4..b2ced7cae 100644 --- a/test/e2e/proxy-alias.test.ts +++ b/test/e2e/proxy-alias.test.ts @@ -38,5 +38,6 @@ describe('proxy path aliases', () => { // genuine test failure; a resolved alias always yields an HTTP response. expect(res).not.toBeNull() expect(res!.status).not.toBe(403) + expect(res!.status).not.toBe(500) }, 30000) }) diff --git a/test/fixtures/proxy-alias/nuxt.config.ts b/test/fixtures/proxy-alias/nuxt.config.ts index 3649889b6..f4f3f4091 100644 --- a/test/fixtures/proxy-alias/nuxt.config.ts +++ b/test/fixtures/proxy-alias/nuxt.config.ts @@ -20,6 +20,7 @@ export default defineNuxtConfig({ scripts: { registry: { + instagramEmbed: {}, plausibleAnalytics: { domain: 'example.com' }, }, proxy: { diff --git a/test/unit/__mocks__/stub-nitro-runtime.ts b/test/unit/__mocks__/stub-nitro-runtime.ts new file mode 100644 index 000000000..ddeef2854 --- /dev/null +++ b/test/unit/__mocks__/stub-nitro-runtime.ts @@ -0,0 +1,6 @@ +import { vi } from 'vitest' + +export function stubNitroRuntime(stubs: Record) { + for (const [name, stub] of Object.entries(stubs)) + vi.stubGlobal(name, stub) +} diff --git a/test/unit/cached-upstream.test.ts b/test/unit/cached-upstream.test.ts index b69ccebf6..289b1d95c 100644 --- a/test/unit/cached-upstream.test.ts +++ b/test/unit/cached-upstream.test.ts @@ -1,16 +1,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' const { cacheDefinitions, hashMock } = vi.hoisted(() => ({ cacheDefinitions: [] as Array<{ getKey?: (...args: any[]) => string }>, hashMock: vi.fn((value: unknown) => `hashed:${JSON.stringify(value)}`), })) -vi.mock('nitropack/runtime', () => ({ +stubNitroRuntime({ defineCachedFunction: vi.fn((handler, options) => { cacheDefinitions.push(options) return handler }), -})) +}) vi.mock('ohash', () => ({ hash: hashMock, diff --git a/test/unit/proxy-handler-alias.test.ts b/test/unit/proxy-handler-alias.test.ts index 4b5be523b..c718efafa 100644 --- a/test/unit/proxy-handler-alias.test.ts +++ b/test/unit/proxy-handler-alias.test.ts @@ -1,7 +1,8 @@ import type { Server } from 'node:http' import { createServer } from 'node:http' import { createApp, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' /** * Issue #814: proxy paths may use opaque/custom aliases instead of the verbatim @@ -9,7 +10,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites * validating the allowlist and forwarding upstream. */ -vi.mock('nitropack/runtime', () => ({ +stubNitroRuntime({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -26,7 +27,7 @@ vi.mock('nitropack/runtime', () => ({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -})) +}) describe('proxy handler - path aliases (#814)', () => { let proxyServer: Server diff --git a/test/unit/proxy-handler-body.test.ts b/test/unit/proxy-handler-body.test.ts index f1932946f..079720810 100644 --- a/test/unit/proxy-handler-body.test.ts +++ b/test/unit/proxy-handler-body.test.ts @@ -2,10 +2,11 @@ import type { Server } from 'node:http' import { createServer } from 'node:http' import { gzipSync } from 'node:zlib' import { createApp, defineEventHandler, readRawBody, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' import proxyHandler from '../../packages/script/src/runtime/server/proxy-handler' +import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' -vi.mock('nitropack/runtime', () => ({ +stubNitroRuntime({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -18,7 +19,7 @@ vi.mock('nitropack/runtime', () => ({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -})) +}) describe('proxy handler request bodies (#836)', () => { let upstreamServer: Server diff --git a/test/unit/proxy-handler-hop-by-hop.test.ts b/test/unit/proxy-handler-hop-by-hop.test.ts index 7ddaf12b1..f0a48f5bd 100644 --- a/test/unit/proxy-handler-hop-by-hop.test.ts +++ b/test/unit/proxy-handler-hop-by-hop.test.ts @@ -1,7 +1,8 @@ import type { Server } from 'node:http' import { createServer, request as httpRequest } from 'node:http' import { createApp, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' /** * Tests for #791: proxy handler must strip hop-by-hop request headers per RFC 7230 §6.1. @@ -13,7 +14,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites * Additionally, any header named in the `Connection` header value must also be stripped. */ -vi.mock('nitropack/runtime', () => ({ +stubNitroRuntime({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -27,7 +28,7 @@ vi.mock('nitropack/runtime', () => ({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -})) +}) describe('proxy handler - hop-by-hop request headers (#791)', () => { let proxyServer: Server diff --git a/test/unit/with-signing.test.ts b/test/unit/with-signing.test.ts index 151144d4b..9036d9536 100644 --- a/test/unit/with-signing.test.ts +++ b/test/unit/with-signing.test.ts @@ -8,6 +8,7 @@ import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, } from '../../packages/script/src/runtime/server/utils/sign' +import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' // Hoisted runtime config mock — swapped between tests via `runtimeConfigMock`. const { runtimeConfigMock } = vi.hoisted(() => ({ @@ -16,11 +17,11 @@ const { runtimeConfigMock } = vi.hoisted(() => ({ }, })) -vi.mock('nitropack/runtime', () => ({ +stubNitroRuntime({ useRuntimeConfig: () => runtimeConfigMock.current, -})) +}) -// Import AFTER vi.mock so withSigning resolves against the mocked module. +// Import after installing the runtime config stub. const { withSigning } = await import('../../packages/script/src/runtime/server/utils/withSigning') const SECRET = 'with-signing-test-secret' From 516ce05c39d3f0cbbbc716366dc10e7055e054e4 Mon Sep 17 00:00:00 2001 From: onmax Date: Mon, 3 Aug 2026 17:02:05 +0200 Subject: [PATCH 2/5] fix: preserve Nitro error responses across runtimes --- .github/workflows/ci.yml | 2 +- packages/script/src/module.ts | 8 +++++++- packages/script/src/runtime/server/bluesky-embed.ts | 2 +- .../src/runtime/server/google-maps-geocode-proxy.ts | 2 +- .../src/runtime/server/google-static-maps-proxy.ts | 2 +- packages/script/src/runtime/server/gravatar-proxy.ts | 2 +- packages/script/src/runtime/server/instagram-embed.ts | 2 +- packages/script/src/runtime/server/proxy-handler.ts | 2 +- packages/script/src/runtime/server/utils/image-proxy.ts | 2 +- packages/script/src/runtime/server/utils/withSigning.ts | 2 +- packages/script/src/runtime/server/x-embed.ts | 2 +- test/e2e/proxy-alias.test.ts | 6 +++++- .../fixtures/proxy-alias/server/api/nitro-runtime.get.ts | 9 +++++++++ test/unit/__mocks__/stub-nitro-runtime.ts | 3 ++- 14 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edd21d2d0..f367b58e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,4 +171,4 @@ jobs: run: pnpm exec nuxt prepare - name: Run Nitro 3 compatibility tests - run: pnpm vitest run --project e2e test/e2e/proxy-alias.test.ts -t 'auto-injects|resolves' + run: pnpm vitest run --project e2e test/e2e/proxy-alias.test.ts diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index b7dd847a4..f3c0e56e0 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -521,8 +521,14 @@ export default defineNuxtModule({ if (Number.parseInt(getNuxtVersion(nuxt), 10) >= 5) { const nuxtDir = dirname(await resolveNuxtPath('nuxt/package.json')) const nitroDir = dirname(await resolveNuxtPath('@nuxt/nitro-server/package.json', { cwd: nuxtDir })) - const resolveNitroImport = async (id: string) => pathToFileURL(await resolveNuxtPath(id, { cwd: nitroDir })).href + const resolveNitroImport = async (id: string) => { + const resolved = await resolveNuxtPath(id, { cwd: nitroDir }) + if (!existsSync(resolved)) + throw new Error(`[nuxt-scripts] Could not resolve Nitro runtime helper "${id}" from "${nitroDir}".`) + return pathToFileURL(resolved).href + } addServerImports([ + { name: 'createError', from: await resolveNitroImport('nitro/h3') }, { name: 'useNitroApp', from: await resolveNitroImport('nitro/app') }, { name: 'defineCachedFunction', from: await resolveNitroImport('nitro/cache') }, { name: 'useRuntimeConfig', from: await resolveNitroImport('nitro/runtime-config') }, diff --git a/packages/script/src/runtime/server/bluesky-embed.ts b/packages/script/src/runtime/server/bluesky-embed.ts index 33a6bfb95..ec4ee7a76 100644 --- a/packages/script/src/runtime/server/bluesky-embed.ts +++ b/packages/script/src/runtime/server/bluesky-embed.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteBlueskyPostImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts index 2c10c7d12..5f49b32b1 100644 --- a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts +++ b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' import { createCachedJsonFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/google-static-maps-proxy.ts b/packages/script/src/runtime/server/google-static-maps-proxy.ts index 8e27ec1f7..ca19ed5d0 100644 --- a/packages/script/src/runtime/server/google-static-maps-proxy.ts +++ b/packages/script/src/runtime/server/google-static-maps-proxy.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' import { createCachedBinaryFetch } from './utils/cached-upstream' import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_PARAM } from './utils/sign-constants' diff --git a/packages/script/src/runtime/server/gravatar-proxy.ts b/packages/script/src/runtime/server/gravatar-proxy.ts index b4dcdfb17..54021c403 100644 --- a/packages/script/src/runtime/server/gravatar-proxy.ts +++ b/packages/script/src/runtime/server/gravatar-proxy.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' import { createCachedBinaryFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/instagram-embed.ts b/packages/script/src/runtime/server/instagram-embed.ts index 0bc22639f..a3e5e40d1 100644 --- a/packages/script/src/runtime/server/instagram-embed.ts +++ b/packages/script/src/runtime/server/instagram-embed.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { $fetch } from 'ofetch' import { hash } from 'ohash' import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from 'ultrahtml' diff --git a/packages/script/src/runtime/server/proxy-handler.ts b/packages/script/src/runtime/server/proxy-handler.ts index 4730939a9..2c4c8eaac 100644 --- a/packages/script/src/runtime/server/proxy-handler.ts +++ b/packages/script/src/runtime/server/proxy-handler.ts @@ -1,5 +1,5 @@ import type { ProxyPrivacyInput, ResolvedProxyPrivacy } from './utils/privacy' -import { createError, defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, readRawBody, setResponseHeader, setResponseStatus } from 'h3' +import { defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, readRawBody, setResponseHeader, setResponseStatus } from 'h3' import { matchDomain } from './utils/match-domain' import { anonymizeIP, diff --git a/packages/script/src/runtime/server/utils/image-proxy.ts b/packages/script/src/runtime/server/utils/image-proxy.ts index bb90844b8..519ab33e3 100644 --- a/packages/script/src/runtime/server/utils/image-proxy.ts +++ b/packages/script/src/runtime/server/utils/image-proxy.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { createCachedBinaryFetch } from './cached-upstream' import { withSigning } from './withSigning' diff --git a/packages/script/src/runtime/server/utils/withSigning.ts b/packages/script/src/runtime/server/utils/withSigning.ts index 3dc0f99a4..390ac7df0 100644 --- a/packages/script/src/runtime/server/utils/withSigning.ts +++ b/packages/script/src/runtime/server/utils/withSigning.ts @@ -21,7 +21,7 @@ */ import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3' -import { createError, defineEventHandler } from 'h3' +import { defineEventHandler } from 'h3' import { verifyProxyRequest } from './sign' export function withSigning( diff --git a/packages/script/src/runtime/server/x-embed.ts b/packages/script/src/runtime/server/x-embed.ts index 821e972d5..a4d9dc956 100644 --- a/packages/script/src/runtime/server/x-embed.ts +++ b/packages/script/src/runtime/server/x-embed.ts @@ -1,4 +1,4 @@ -import { createError, defineEventHandler, getQuery, setHeader } from 'h3' +import { defineEventHandler, getQuery, setHeader } from 'h3' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteTweetImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' diff --git a/test/e2e/proxy-alias.test.ts b/test/e2e/proxy-alias.test.ts index b2ced7cae..c6117d2e2 100644 --- a/test/e2e/proxy-alias.test.ts +++ b/test/e2e/proxy-alias.test.ts @@ -26,6 +26,11 @@ describe('proxy path aliases', () => { expect(res.status).toBe(403) }) + it('runs Nitro runtime helpers locally', async () => { + const result = await $fetch('/api/nitro-runtime') + expect(result).toEqual({ app: true, cached: 'ok', config: true }) + }) + it('resolves the alias back to the real domain instead of 403ing', async () => { // `pl` resolves to plausible.io and is proxied upstream. Whatever the upstream // returns (or a 502 if unreachable), it must not be our allowlist 403. @@ -38,6 +43,5 @@ describe('proxy path aliases', () => { // genuine test failure; a resolved alias always yields an HTTP response. expect(res).not.toBeNull() expect(res!.status).not.toBe(403) - expect(res!.status).not.toBe(500) }, 30000) }) diff --git a/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts b/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts new file mode 100644 index 000000000..ddbdb9700 --- /dev/null +++ b/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts @@ -0,0 +1,9 @@ +import { defineEventHandler } from 'h3' + +const getCachedValue = defineCachedFunction(() => 'ok') + +export default defineEventHandler(async () => ({ + app: Boolean(useNitroApp()), + cached: await getCachedValue(), + config: Boolean(useRuntimeConfig()), +})) diff --git a/test/unit/__mocks__/stub-nitro-runtime.ts b/test/unit/__mocks__/stub-nitro-runtime.ts index ddeef2854..5daac24b2 100644 --- a/test/unit/__mocks__/stub-nitro-runtime.ts +++ b/test/unit/__mocks__/stub-nitro-runtime.ts @@ -1,6 +1,7 @@ +import { createError } from 'h3' import { vi } from 'vitest' export function stubNitroRuntime(stubs: Record) { - for (const [name, stub] of Object.entries(stubs)) + for (const [name, stub] of Object.entries({ createError, ...stubs })) vi.stubGlobal(name, stub) } From db1b1bf88dbf9f85e837a5ee629a54a4a04f191d Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 4 Aug 2026 11:39:25 +1000 Subject: [PATCH 3/5] fix: isolate Nitro runtime compatibility --- .github/workflows/ci.yml | 7 +- packages/script/src/module.ts | 24 +--- packages/script/src/nitro-compatibility.ts | 117 ++++++++++++++++++ .../src/runtime/server/bluesky-embed.ts | 3 +- .../server/google-maps-geocode-proxy.ts | 3 +- .../server/google-static-maps-proxy.ts | 3 +- .../src/runtime/server/gravatar-proxy.ts | 3 +- .../src/runtime/server/instagram-embed.ts | 3 +- .../src/runtime/server/proxy-handler.ts | 5 +- .../runtime/server/utils/cached-upstream.ts | 1 + .../src/runtime/server/utils/image-proxy.ts | 2 +- .../script/src/runtime/server/utils/sign.ts | 4 +- .../src/runtime/server/utils/withSigning.ts | 5 +- .../runtime/server/vercel-insights-sink.ts | 2 +- packages/script/src/runtime/server/x-embed.ts | 3 +- .../server/api/nitro-runtime.get.ts | 3 +- test/unit/__mocks__/stub-nitro-runtime.ts | 7 -- test/unit/cached-upstream.test.ts | 6 +- test/unit/nitro-compatibility.test.ts | 80 ++++++++++++ test/unit/proxy-handler-alias.test.ts | 7 +- test/unit/proxy-handler-body.test.ts | 7 +- test/unit/proxy-handler-hop-by-hop.test.ts | 7 +- test/unit/with-signing.test.ts | 7 +- vitest.config.ts | 2 + 24 files changed, 248 insertions(+), 63 deletions(-) create mode 100644 packages/script/src/nitro-compatibility.ts delete mode 100644 test/unit/__mocks__/stub-nitro-runtime.ts create mode 100644 test/unit/nitro-compatibility.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f367b58e6..e215dce80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,7 +168,12 @@ jobs: run: pnpm --filter @nuxt/scripts dev:prepare - name: Nuxt prepare - run: pnpm exec nuxt prepare + run: | + pnpm exec nuxt prepare + pnpm exec nuxt prepare test/fixtures/proxy-alias + + - name: Typecheck Nitro 3 compatibility + run: pnpm exec vue-tsc --noEmit -p test/fixtures/proxy-alias/.nuxt/tsconfig.server.json - name: Run Nitro 3 compatibility tests run: pnpm vitest run --project e2e test/e2e/proxy-alias.test.ts diff --git a/packages/script/src/module.ts b/packages/script/src/module.ts index f3c0e56e0..56be2ac1b 100644 --- a/packages/script/src/module.ts +++ b/packages/script/src/module.ts @@ -16,7 +16,6 @@ import type { } from './runtime/types' import { randomBytes } from 'node:crypto' import { appendFileSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { pathToFileURL } from 'node:url' import { addBuildPlugin, addComponentsDir, @@ -24,21 +23,19 @@ import { addPlugin, addPluginTemplate, addServerHandler, - addServerImports, addTemplate, createResolver, defineNuxtModule, - getNuxtVersion, hasNuxtModule, - resolvePath as resolveNuxtPath, } from '@nuxt/kit' import { defu } from 'defu' -import { dirname, resolve as resolvePath_ } from 'pathe' +import { resolve as resolvePath_ } from 'pathe' import { readPackageJSON } from 'pkg-types' import { setupPublicAssetStrategy } from './assets' import { buildDevtoolsData, buildDevtoolsEntry, setupDevtools } from './devtools' import { installNuxtModule } from './kit' import { logger } from './logger' +import { setupNitroRuntimeCompatibility } from './nitro-compatibility' import { extractRequiredFields, migrateDeprecatedRegistryKeys, normalizeRegistryConfig } from './normalize' import { NuxtScriptsCheckScripts } from './plugins/check-scripts' import { generateInterceptPluginContents } from './plugins/intercept' @@ -518,22 +515,7 @@ export default defineNuxtModule({ logger.debug('The module is disabled, skipping setup.') return } - if (Number.parseInt(getNuxtVersion(nuxt), 10) >= 5) { - const nuxtDir = dirname(await resolveNuxtPath('nuxt/package.json')) - const nitroDir = dirname(await resolveNuxtPath('@nuxt/nitro-server/package.json', { cwd: nuxtDir })) - const resolveNitroImport = async (id: string) => { - const resolved = await resolveNuxtPath(id, { cwd: nitroDir }) - if (!existsSync(resolved)) - throw new Error(`[nuxt-scripts] Could not resolve Nitro runtime helper "${id}" from "${nitroDir}".`) - return pathToFileURL(resolved).href - } - addServerImports([ - { name: 'createError', from: await resolveNitroImport('nitro/h3') }, - { name: 'useNitroApp', from: await resolveNitroImport('nitro/app') }, - { name: 'defineCachedFunction', from: await resolveNitroImport('nitro/cache') }, - { name: 'useRuntimeConfig', from: await resolveNitroImport('nitro/runtime-config') }, - ]) - } + await setupNitroRuntimeCompatibility(nuxt) if (nuxt.options.dev) { setupDevtools(nuxt, { standalone: config._standaloneDevtools }) if (config._standaloneDevtools) { diff --git a/packages/script/src/nitro-compatibility.ts b/packages/script/src/nitro-compatibility.ts new file mode 100644 index 000000000..1a88801f2 --- /dev/null +++ b/packages/script/src/nitro-compatibility.ts @@ -0,0 +1,117 @@ +import type { Nuxt } from '@nuxt/schema' +import { existsSync } from 'node:fs' +import { pathToFileURL } from 'node:url' +import { addTypeTemplate, getNuxtVersion, resolvePath as resolveNuxtPath } from '@nuxt/kit' +import { dirname } from 'pathe' + +type NitroRuntimeCompatibility + = | { _tag: 'nitro-v2' } + | { + _tag: 'nitro-v3' + app: string + cache: string + h3: string + runtimeConfig: string + } + +type ResolveNitroImport = (id: string) => Promise + +interface NitroCompatibilityOptions { + alias?: Record + virtual?: Record +} + +const NITRO_RUNTIME_MODULE = '#nuxt-scripts/nitro' +const H3_RUNTIME_MODULE = '#nuxt-scripts/h3' +const TYPE_TEMPLATE_FILENAME = 'types/nuxt-scripts-nitro.d.ts' + +const nitroV2Runtime = `export { + defineCachedFunction, + useNitroApp, + useRuntimeConfig, +} from 'nitropack/runtime' +` + +const nitroV3RuntimeTypes = `export { useNitroApp } from 'nitro/app' +export { defineCachedFunction } from 'nitro/cache' +export function useRuntimeConfig(event?: import('nitro/h3').H3Event): ReturnType +` + +function indent(value: string, spaces: number): string { + const padding = ' '.repeat(spaces) + return value.split('\n').map(line => `${padding}${line}`).join('\n') +} + +function renderRuntimeDeclarations(compatibility: NitroRuntimeCompatibility): string { + const nitroRuntime = compatibility._tag === 'nitro-v3' ? nitroV3RuntimeTypes : nitroV2Runtime + const h3Runtime = compatibility._tag === 'nitro-v3' + ? `export * from 'nitro/h3'\n` + : `export * from 'h3'\n` + + return `declare module '${NITRO_RUNTIME_MODULE}' { +${indent(nitroRuntime.trim(), 2)} +} + +declare module '${H3_RUNTIME_MODULE}' { +${indent(h3Runtime.trim(), 2)} +} +` +} + +function renderNitroV3Runtime(compatibility: Extract): string { + return `export { useNitroApp } from ${JSON.stringify(compatibility.app)} +export { defineCachedFunction } from ${JSON.stringify(compatibility.cache)} +import { useRuntimeConfig as _useRuntimeConfig } from ${JSON.stringify(compatibility.runtimeConfig)} +export function useRuntimeConfig(_event) { return _useRuntimeConfig() } +` +} + +function applyNitroRuntimeCompatibility(nuxt: Nuxt, compatibility: NitroRuntimeCompatibility): void { + const nuxtOptions = nuxt.options as Nuxt['options'] & { nitro?: NitroCompatibilityOptions } + const nitroOptions = nuxtOptions.nitro ||= {} + nitroOptions.alias ||= {} + nitroOptions.virtual ||= {} + nitroOptions.alias[H3_RUNTIME_MODULE] = compatibility._tag === 'nitro-v3' ? compatibility.h3 : 'h3' + nitroOptions.virtual[NITRO_RUNTIME_MODULE] = compatibility._tag === 'nitro-v3' + ? renderNitroV3Runtime(compatibility) + : nitroV2Runtime +} + +async function createNuxtNitroImportResolver(): Promise { + const nuxtDir = dirname(await resolveNuxtPath('nuxt/package.json')) + const nitroDir = dirname(await resolveNuxtPath('@nuxt/nitro-server/package.json', { cwd: nuxtDir })) + + return async (id: string) => { + const resolved = await resolveNuxtPath(id, { cwd: nitroDir }) + if (!existsSync(resolved)) + throw new Error(`[nuxt-scripts] Could not resolve Nitro runtime helper "${id}" from "${nitroDir}".`) + return pathToFileURL(resolved).href + } +} + +async function resolveNitroV3Compatibility(resolveNitroImport: ResolveNitroImport): Promise { + const [app, cache, h3, runtimeConfig] = await Promise.all([ + resolveNitroImport('nitro/app'), + resolveNitroImport('nitro/cache'), + resolveNitroImport('nitro/h3'), + resolveNitroImport('nitro/runtime-config'), + ]) + + return { _tag: 'nitro-v3', app, cache, h3, runtimeConfig } +} + +export async function setupNitroRuntimeCompatibility( + nuxt: Nuxt, + resolveNitroImport?: ResolveNitroImport, +): Promise { + const compatibility: NitroRuntimeCompatibility = Number.parseInt(getNuxtVersion(nuxt), 10) >= 5 + ? await resolveNitroV3Compatibility(resolveNitroImport || await createNuxtNitroImportResolver()) + : { _tag: 'nitro-v2' } + + applyNitroRuntimeCompatibility(nuxt, compatibility) + nuxt.hooks.hookOnce('modules:done', () => applyNitroRuntimeCompatibility(nuxt, compatibility)) + addTypeTemplate({ + filename: TYPE_TEMPLATE_FILENAME, + getContents: async () => renderRuntimeDeclarations(compatibility), + }, { nitro: true, node: true, nuxt: true }) +} diff --git a/packages/script/src/runtime/server/bluesky-embed.ts b/packages/script/src/runtime/server/bluesky-embed.ts index ec4ee7a76..a3ce243e1 100644 --- a/packages/script/src/runtime/server/bluesky-embed.ts +++ b/packages/script/src/runtime/server/bluesky-embed.ts @@ -1,4 +1,5 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteBlueskyPostImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts index 5f49b32b1..dde0e62aa 100644 --- a/packages/script/src/runtime/server/google-maps-geocode-proxy.ts +++ b/packages/script/src/runtime/server/google-maps-geocode-proxy.ts @@ -1,5 +1,6 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedJsonFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/google-static-maps-proxy.ts b/packages/script/src/runtime/server/google-static-maps-proxy.ts index ca19ed5d0..51db1f51d 100644 --- a/packages/script/src/runtime/server/google-static-maps-proxy.ts +++ b/packages/script/src/runtime/server/google-static-maps-proxy.ts @@ -1,5 +1,6 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedBinaryFetch } from './utils/cached-upstream' import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_PARAM } from './utils/sign-constants' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/gravatar-proxy.ts b/packages/script/src/runtime/server/gravatar-proxy.ts index 54021c403..9467f65b1 100644 --- a/packages/script/src/runtime/server/gravatar-proxy.ts +++ b/packages/script/src/runtime/server/gravatar-proxy.ts @@ -1,5 +1,6 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' import { withQuery } from 'ufo' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedBinaryFetch } from './utils/cached-upstream' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/instagram-embed.ts b/packages/script/src/runtime/server/instagram-embed.ts index a3e5e40d1..5ae052f69 100644 --- a/packages/script/src/runtime/server/instagram-embed.ts +++ b/packages/script/src/runtime/server/instagram-embed.ts @@ -1,7 +1,8 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' import { $fetch } from 'ofetch' import { hash } from 'ohash' import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from 'ultrahtml' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { defineCachedFunction, useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedJsonFetch } from './utils/cached-upstream' import { isEmbedShell, proxyAssetUrl, rewriteUrl, rewriteUrlsInText, RSRC_RE, scopeCss } from './utils/instagram-embed' import { withSigning } from './utils/withSigning' diff --git a/packages/script/src/runtime/server/proxy-handler.ts b/packages/script/src/runtime/server/proxy-handler.ts index 2c4c8eaac..0619e9866 100644 --- a/packages/script/src/runtime/server/proxy-handler.ts +++ b/packages/script/src/runtime/server/proxy-handler.ts @@ -1,5 +1,6 @@ import type { ProxyPrivacyInput, ResolvedProxyPrivacy } from './utils/privacy' -import { defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, readRawBody, setResponseHeader, setResponseStatus } from 'h3' +import { createError, defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, readRawBody, setResponseHeader, setResponseStatus } from '#nuxt-scripts/h3' +import { useNitroApp, useRuntimeConfig } from '#nuxt-scripts/nitro' import { matchDomain } from './utils/match-domain' import { anonymizeIP, @@ -342,7 +343,7 @@ export default defineEventHandler(async (event) => { // Emit hook for E2E testing — allows capturing before/after data const nitro = useNitroApp() - await (nitro.hooks.callHook as (name: string, ctx: any) => Promise)('nuxt-scripts:proxy', { + await (nitro.hooks?.callHook as ((name: string, ctx: any) => Promise) | undefined)?.('nuxt-scripts:proxy', { timestamp: Date.now(), path: event.path, targetUrl, diff --git a/packages/script/src/runtime/server/utils/cached-upstream.ts b/packages/script/src/runtime/server/utils/cached-upstream.ts index ee027d839..eda9f657f 100644 --- a/packages/script/src/runtime/server/utils/cached-upstream.ts +++ b/packages/script/src/runtime/server/utils/cached-upstream.ts @@ -1,6 +1,7 @@ import { Buffer } from 'node:buffer' import { $fetch } from 'ofetch' import { hash } from 'ohash' +import { defineCachedFunction } from '#nuxt-scripts/nitro' /** * Server-side caches for upstream proxy fetches. diff --git a/packages/script/src/runtime/server/utils/image-proxy.ts b/packages/script/src/runtime/server/utils/image-proxy.ts index 519ab33e3..9c45e827c 100644 --- a/packages/script/src/runtime/server/utils/image-proxy.ts +++ b/packages/script/src/runtime/server/utils/image-proxy.ts @@ -1,4 +1,4 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' import { createCachedBinaryFetch } from './cached-upstream' import { withSigning } from './withSigning' diff --git a/packages/script/src/runtime/server/utils/sign.ts b/packages/script/src/runtime/server/utils/sign.ts index ab40d0c9a..13cd2d5a6 100644 --- a/packages/script/src/runtime/server/utils/sign.ts +++ b/packages/script/src/runtime/server/utils/sign.ts @@ -22,9 +22,9 @@ * prerendered HTML for no practical gain. */ -import type { H3Event } from 'h3' +import type { H3Event } from '#nuxt-scripts/h3' import { createHmac } from 'node:crypto' -import { getQuery } from 'h3' +import { getQuery } from '#nuxt-scripts/h3' import { PAGE_TOKEN_MAX_AGE, PAGE_TOKEN_PARAM, diff --git a/packages/script/src/runtime/server/utils/withSigning.ts b/packages/script/src/runtime/server/utils/withSigning.ts index 390ac7df0..604d494c7 100644 --- a/packages/script/src/runtime/server/utils/withSigning.ts +++ b/packages/script/src/runtime/server/utils/withSigning.ts @@ -20,8 +20,9 @@ * never reach the upstream fetch and cannot consume API quota. */ -import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3' -import { defineEventHandler } from 'h3' +import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from '#nuxt-scripts/h3' +import { createError, defineEventHandler } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { verifyProxyRequest } from './sign' export function withSigning( diff --git a/packages/script/src/runtime/server/vercel-insights-sink.ts b/packages/script/src/runtime/server/vercel-insights-sink.ts index ad276bda0..80fb6eee8 100644 --- a/packages/script/src/runtime/server/vercel-insights-sink.ts +++ b/packages/script/src/runtime/server/vercel-insights-sink.ts @@ -1,4 +1,4 @@ -import { defineEventHandler, setResponseStatus } from 'h3' +import { defineEventHandler, setResponseStatus } from '#nuxt-scripts/h3' export default defineEventHandler((event) => { setResponseStatus(event, 204) diff --git a/packages/script/src/runtime/server/x-embed.ts b/packages/script/src/runtime/server/x-embed.ts index a4d9dc956..9af2eda69 100644 --- a/packages/script/src/runtime/server/x-embed.ts +++ b/packages/script/src/runtime/server/x-embed.ts @@ -1,4 +1,5 @@ -import { defineEventHandler, getQuery, setHeader } from 'h3' +import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3' +import { useRuntimeConfig } from '#nuxt-scripts/nitro' import { createCachedJsonFetch } from './utils/cached-upstream' import { rewriteTweetImages } from './utils/embed-rewriters' import { withSigning } from './utils/withSigning' diff --git a/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts b/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts index ddbdb9700..cd1aab5d2 100644 --- a/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts +++ b/test/fixtures/proxy-alias/server/api/nitro-runtime.get.ts @@ -1,4 +1,5 @@ -import { defineEventHandler } from 'h3' +import { defineEventHandler } from '#nuxt-scripts/h3' +import { defineCachedFunction, useNitroApp, useRuntimeConfig } from '#nuxt-scripts/nitro' const getCachedValue = defineCachedFunction(() => 'ok') diff --git a/test/unit/__mocks__/stub-nitro-runtime.ts b/test/unit/__mocks__/stub-nitro-runtime.ts deleted file mode 100644 index 5daac24b2..000000000 --- a/test/unit/__mocks__/stub-nitro-runtime.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createError } from 'h3' -import { vi } from 'vitest' - -export function stubNitroRuntime(stubs: Record) { - for (const [name, stub] of Object.entries({ createError, ...stubs })) - vi.stubGlobal(name, stub) -} diff --git a/test/unit/cached-upstream.test.ts b/test/unit/cached-upstream.test.ts index 289b1d95c..cda7fe26f 100644 --- a/test/unit/cached-upstream.test.ts +++ b/test/unit/cached-upstream.test.ts @@ -1,17 +1,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' const { cacheDefinitions, hashMock } = vi.hoisted(() => ({ cacheDefinitions: [] as Array<{ getKey?: (...args: any[]) => string }>, hashMock: vi.fn((value: unknown) => `hashed:${JSON.stringify(value)}`), })) -stubNitroRuntime({ +vi.mock('#nuxt-scripts/nitro', () => ({ defineCachedFunction: vi.fn((handler, options) => { cacheDefinitions.push(options) return handler }), -}) + useRuntimeConfig: () => ({}), +})) vi.mock('ohash', () => ({ hash: hashMock, diff --git a/test/unit/nitro-compatibility.test.ts b/test/unit/nitro-compatibility.test.ts new file mode 100644 index 000000000..d331627d2 --- /dev/null +++ b/test/unit/nitro-compatibility.test.ts @@ -0,0 +1,80 @@ +import type { Nuxt } from '@nuxt/schema' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { setupNitroRuntimeCompatibility } from '../../packages/script/src/nitro-compatibility' + +const { addTypeTemplateMock, getNuxtVersionMock, hookOnceMock } = vi.hoisted(() => ({ + addTypeTemplateMock: vi.fn(), + getNuxtVersionMock: vi.fn(), + hookOnceMock: vi.fn(), +})) + +vi.mock('@nuxt/kit', async importOriginal => ({ + ...await importOriginal(), + addTypeTemplate: addTypeTemplateMock, + getNuxtVersion: getNuxtVersionMock, +})) + +function createNuxt(): Nuxt { + return { + hooks: { + hookOnce: hookOnceMock, + }, + options: { + nitro: {}, + }, + } as Nuxt +} + +describe('setupNitroRuntimeCompatibility', () => { + beforeEach(() => { + addTypeTemplateMock.mockReset() + getNuxtVersionMock.mockReset() + hookOnceMock.mockReset() + }) + + it('registers explicit Nitro 2 runtime modules', async () => { + getNuxtVersionMock.mockReturnValue('4.5.0') + const nuxt = createNuxt() + + await setupNitroRuntimeCompatibility(nuxt) + + expect(nuxt.options.nitro.alias?.['#nuxt-scripts/h3']).toBe('h3') + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).toContain('from \'nitropack/runtime\'') + expect(addTypeTemplateMock).toHaveBeenCalledWith(expect.any(Object), { nitro: true, node: true, nuxt: true }) + + const template = addTypeTemplateMock.mock.calls[0]![0] + await expect(template.getContents()).resolves.toContain('declare module \'#nuxt-scripts/nitro\'') + await expect(template.getContents()).resolves.toContain('export * from \'h3\'') + }) + + it('normalizes resolved Nitro 3 runtime modules without package dependencies', async () => { + getNuxtVersionMock.mockReturnValue('5.0.0') + const nuxt = createNuxt() + const resolveNitroImport = vi.fn(async (id: string) => `file:///nuxt-nitro/${id.replace('/', '-')}.mjs`) + + await setupNitroRuntimeCompatibility(nuxt, resolveNitroImport) + + expect(resolveNitroImport).toHaveBeenCalledTimes(4) + expect(nuxt.options.nitro.alias?.['#nuxt-scripts/h3']).toBe('file:///nuxt-nitro/nitro-h3.mjs') + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).toContain('file:///nuxt-nitro/nitro-app.mjs') + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).toContain('file:///nuxt-nitro/nitro-cache.mjs') + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).not.toContain('from \'nitro/') + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).toContain('useRuntimeConfig(_event)') + + const template = addTypeTemplateMock.mock.calls[0]![0] + await expect(template.getContents()).resolves.toContain('export * from \'nitro/h3\'') + await expect(template.getContents()).resolves.toContain('useRuntimeConfig(event?:') + }) + + it('reasserts compatibility after other modules finish setup', async () => { + getNuxtVersionMock.mockReturnValue('5.0.0') + const nuxt = createNuxt() + const resolveNitroImport = async (id: string) => `file:///nuxt-nitro/${id.replace('/', '-')}.mjs` + + await setupNitroRuntimeCompatibility(nuxt, resolveNitroImport) + nuxt.options.nitro.virtual!['#nuxt-scripts/nitro'] = 'stale' + hookOnceMock.mock.calls[0]![1]() + + expect(nuxt.options.nitro.virtual?.['#nuxt-scripts/nitro']).toContain('file:///nuxt-nitro/nitro-app.mjs') + }) +}) diff --git a/test/unit/proxy-handler-alias.test.ts b/test/unit/proxy-handler-alias.test.ts index c718efafa..307887bce 100644 --- a/test/unit/proxy-handler-alias.test.ts +++ b/test/unit/proxy-handler-alias.test.ts @@ -1,8 +1,7 @@ import type { Server } from 'node:http' import { createServer } from 'node:http' import { createApp, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' -import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' /** * Issue #814: proxy paths may use opaque/custom aliases instead of the verbatim @@ -10,7 +9,7 @@ import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' * validating the allowlist and forwarding upstream. */ -stubNitroRuntime({ +vi.mock('#nuxt-scripts/nitro', () => ({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -27,7 +26,7 @@ stubNitroRuntime({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -}) +})) describe('proxy handler - path aliases (#814)', () => { let proxyServer: Server diff --git a/test/unit/proxy-handler-body.test.ts b/test/unit/proxy-handler-body.test.ts index 079720810..65168dba5 100644 --- a/test/unit/proxy-handler-body.test.ts +++ b/test/unit/proxy-handler-body.test.ts @@ -2,11 +2,10 @@ import type { Server } from 'node:http' import { createServer } from 'node:http' import { gzipSync } from 'node:zlib' import { createApp, defineEventHandler, readRawBody, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import proxyHandler from '../../packages/script/src/runtime/server/proxy-handler' -import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' -stubNitroRuntime({ +vi.mock('#nuxt-scripts/nitro', () => ({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -19,7 +18,7 @@ stubNitroRuntime({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -}) +})) describe('proxy handler request bodies (#836)', () => { let upstreamServer: Server diff --git a/test/unit/proxy-handler-hop-by-hop.test.ts b/test/unit/proxy-handler-hop-by-hop.test.ts index f0a48f5bd..a8554070b 100644 --- a/test/unit/proxy-handler-hop-by-hop.test.ts +++ b/test/unit/proxy-handler-hop-by-hop.test.ts @@ -1,8 +1,7 @@ import type { Server } from 'node:http' import { createServer, request as httpRequest } from 'node:http' import { createApp, toNodeListener } from 'h3' -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' -import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' /** * Tests for #791: proxy handler must strip hop-by-hop request headers per RFC 7230 §6.1. @@ -14,7 +13,7 @@ import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' * Additionally, any header named in the `Connection` header value must also be stripped. */ -stubNitroRuntime({ +vi.mock('#nuxt-scripts/nitro', () => ({ useRuntimeConfig: () => ({ 'nuxt-scripts-proxy': { proxyPrefix: '/_scripts/p', @@ -28,7 +27,7 @@ stubNitroRuntime({ useNitroApp: () => ({ hooks: { callHook: async () => {} }, }), -}) +})) describe('proxy handler - hop-by-hop request headers (#791)', () => { let proxyServer: Server diff --git a/test/unit/with-signing.test.ts b/test/unit/with-signing.test.ts index 9036d9536..319836ba9 100644 --- a/test/unit/with-signing.test.ts +++ b/test/unit/with-signing.test.ts @@ -8,7 +8,6 @@ import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, } from '../../packages/script/src/runtime/server/utils/sign' -import { stubNitroRuntime } from './__mocks__/stub-nitro-runtime' // Hoisted runtime config mock — swapped between tests via `runtimeConfigMock`. const { runtimeConfigMock } = vi.hoisted(() => ({ @@ -17,11 +16,11 @@ const { runtimeConfigMock } = vi.hoisted(() => ({ }, })) -stubNitroRuntime({ +vi.mock('#nuxt-scripts/nitro', () => ({ useRuntimeConfig: () => runtimeConfigMock.current, -}) +})) -// Import after installing the runtime config stub. +// Import after installing the runtime config mock. const { withSigning } = await import('../../packages/script/src/runtime/server/utils/withSigning') const SECRET = 'with-signing-test-secret' diff --git a/vitest.config.ts b/vitest.config.ts index 35b86e87a..9b08c28fb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,6 +23,8 @@ export default defineConfig({ defineProject({ resolve: { alias: { + '#nuxt-scripts/h3': 'h3', + '#nuxt-scripts/nitro': new URL('./test/unit/__mocks__/empty.ts', import.meta.url).pathname, // Virtual emitted by the Nuxt module at build time; unit tests // mock it via `vi.mock('#build/nuxt-scripts-snippets')`, but the // import must first resolve to *something* the bundler accepts. From 5d575c4b6894b175d803980f83457be8676bf711 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 4 Aug 2026 11:51:41 +1000 Subject: [PATCH 4/5] chore: satisfy updated lint rules --- docs/content/scripts/google-recaptcha.md | 5 ++++- packages/devtools-app/components/DevtoolsTooltip.vue | 4 ++-- packages/devtools-app/components/UiTooltip.vue | 4 ++-- packages/script/src/kit.ts | 6 +++++- test/e2e/_usercentrics-suite.ts | 5 ++++- test/e2e/basic.test.ts | 5 ++++- test/e2e/proxy-alias.test.ts | 7 ++----- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/content/scripts/google-recaptcha.md b/docs/content/scripts/google-recaptcha.md index f9000e7d4..b2f64d073 100644 --- a/docs/content/scripts/google-recaptcha.md +++ b/docs/content/scripts/google-recaptcha.md @@ -201,7 +201,10 @@ function onSubmit() { email: email.value, message: message.value } - }).catch(() => null) + }).catch((error) => { + console.error('Failed to submit contact form', error) + return null + }) status.value = result ? 'success' : 'error' }) diff --git a/packages/devtools-app/components/DevtoolsTooltip.vue b/packages/devtools-app/components/DevtoolsTooltip.vue index a6b742f13..e830a6dc1 100644 --- a/packages/devtools-app/components/DevtoolsTooltip.vue +++ b/packages/devtools-app/components/DevtoolsTooltip.vue @@ -35,7 +35,7 @@ export const sizes = {