Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,3 +137,43 @@ 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
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
5 changes: 4 additions & 1 deletion docs/content/scripts/google-recaptcha.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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'
})
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools-app/components/DevtoolsTooltip.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ export const sizes = {
<UIcon name="i-heroicons-question-mark-circle" class="size-3 text-(--color-text-subtle) hover:text-(--color-text-muted) transition-colors" />
<template #content>
<div class="devtools-tooltip-panel">
<div :class="`w-max ${sizes[size || 'md']}`">
<div class="w-max" :class="sizes[size || 'md']">
<template v-if="title">
<div class="font-semibold">{{ title }}</div>
<div v-if="description" class="text-(--color-text-muted) text-xs">{{ description }}</div>
Expand All@@ -56,7 +56,7 @@ export const sizes = {
<UIcon v-else name="i-heroicons-question-mark-circle" color="primary" :size="iconSize || 'md'" />
<template #content>
<div class="devtools-tooltip-panel">
<div :class="`w-max ${sizes[size || 'md']}`">
<div class="w-max" :class="sizes[size || 'md']">
<slot v-if="$slots.text" name="text" />
<template v-else-if="title">
<div class="font-semibold">
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools-app/components/UiTooltip.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ export const sizes = {
<UIcon name="i-carbon-help" class="size-3 text-(--color-text-subtle) hover:text-(--color-text-muted) transition-colors" />
<template #content>
<div class="ui-tooltip-panel">
<div :class="`w-max ${sizes[size || 'md']}`">
<div class="w-max" :class="sizes[size || 'md']">
<template v-if="title">
<div class="font-semibold">
{{ title }}
Expand DownExpand Up@@ -66,7 +66,7 @@ export const sizes = {
<UIcon v-else name="i-carbon-help" color="primary" :size="iconSize || 'md'" />
<template #content>
<div class="ui-tooltip-panel">
<div :class="`w-max ${sizes[size || 'md']}`">
<div class="w-max" :class="sizes[size || 'md']">
<slot v-if="$slots.text" name="text" />
<template v-else-if="title">
<div class="font-semibold">
Expand Down
6 changes: 5 additions & 1 deletion packages/script/src/kit.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,8 +11,12 @@ interface EnsurePackageInstalledOptions {
}

async function promptToInstall(name: string, installCommand: () => Promise<void>, options: EnsurePackageInstalledOptions) {
if (await resolvePackageJSON(name).catch(() => null))
if (await resolvePackageJSON(name).catch(() => {
// A missing package is expected here; the install prompt handles it below.
return null
})) {
return true
}

logger.info(`Package ${name} is missing`)
if (isCI)
Expand Down
2 changes: 2 additions & 0 deletions packages/script/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,7 @@ 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'
Expand DownExpand Up@@ -514,6 +515,7 @@ export default defineNuxtModule<ModuleOptions>({
logger.debug('The module is disabled, skipping setup.')
return
}
await setupNitroRuntimeCompatibility(nuxt)
if (nuxt.options.dev) {
setupDevtools(nuxt, { standalone: config._standaloneDevtools })
if (config._standaloneDevtools) {
Expand Down
127 changes: 127 additions & 0 deletions packages/script/src/nitro-compatibility.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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<string>

interface NitroCompatibilityOptions {
alias?: Record<string, string>
virtual?: Record<string, string>
}

interface NitroCompatibilityDependencies {
addTypeTemplate: typeof addTypeTemplate
getNuxtVersion: typeof getNuxtVersion
resolveNitroImport?: ResolveNitroImport
}

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 defaultDependencies: NitroCompatibilityDependencies = {
addTypeTemplate,
getNuxtVersion,
}

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<typeof import('nitro/runtime-config').useRuntimeConfig>
`

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<NitroRuntimeCompatibility, { _tag: 'nitro-v3' }>): 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<ResolveNitroImport> {
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<NitroRuntimeCompatibility> {
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,
dependencies: NitroCompatibilityDependencies = defaultDependencies,
): Promise<void> {
const compatibility: NitroRuntimeCompatibility = Number.parseInt(dependencies.getNuxtVersion(nuxt), 10) >= 5
? await resolveNitroV3Compatibility(dependencies.resolveNitroImport || await createNuxtNitroImportResolver())
: { _tag: 'nitro-v2' }

applyNitroRuntimeCompatibility(nuxt, compatibility)
nuxt.hooks.hookOnce('modules:done', () => applyNitroRuntimeCompatibility(nuxt, compatibility))
dependencies.addTypeTemplate({
filename: TYPE_TEMPLATE_FILENAME,
getContents: async () => renderRuntimeDeclarations(compatibility),
}, { nitro: true, node: true, nuxt: true })
}
6 changes: 3 additions & 3 deletions packages/script/src/runtime/server/bluesky-embed.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
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'
Expand DownExpand Up@@ -115,7 +115,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
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
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'

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
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'
Expand Down
4 changes: 2 additions & 2 deletions packages/script/src/runtime/server/gravatar-proxy.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
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'

Expand Down
6 changes: 3 additions & 3 deletions packages/script/src/runtime/server/instagram-embed.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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'
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'
Expand DownExpand Up@@ -70,7 +70,7 @@ export default withSigning(defineEventHandler(async (event) => {
// The route is registered as `<prefix>/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
Expand Down
6 changes: 3 additions & 3 deletions packages/script/src/runtime/server/proxy-handler.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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 { 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,
Expand DownExpand Up@@ -343,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<void>)('nuxt-scripts:proxy', {
await (nitro.hooks?.callHook as ((name: string, ctx: any) => Promise<void>) | undefined)?.('nuxt-scripts:proxy', {
timestamp: Date.now(),
path: event.path,
targetUrl,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
import { Buffer } from 'node:buffer'
import { defineCachedFunction } from 'nitropack/runtime'
import { $fetch } from 'ofetch'
import { hash } from 'ohash'
import { defineCachedFunction } from '#nuxt-scripts/nitro'

/**
* Server-side caches for upstream proxy fetches.
Expand Down
2 changes: 1 addition & 1 deletion packages/script/src/runtime/server/utils/image-proxy.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3'
import { createCachedBinaryFetch } from './cached-upstream'
import { withSigning } from './withSigning'

Expand Down
4 changes: 2 additions & 2 deletions packages/script/src/runtime/server/utils/sign.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand Down
8 changes: 4 additions & 4 deletions packages/script/src/runtime/server/utils/withSigning.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,16 +20,16 @@
* never reach the upstream fetch and cannot consume API quota.
*/

import type { EventHandler, EventHandlerRequest, EventHandlerResponse } from 'h3'
import { createError, defineEventHandler } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
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<Req extends EventHandlerRequest = EventHandlerRequest, Res extends EventHandlerResponse = EventHandlerResponse>(
handler: EventHandler<Req, Res>,
) {
return defineEventHandler<Req>(async (event) => {
const runtimeConfig = useRuntimeConfig(event)
const runtimeConfig = useRuntimeConfig()
const scriptsConfig = runtimeConfig['nuxt-scripts'] as { proxySecret?: string, pageTokenMaxAge?: number } | undefined
const secret = scriptsConfig?.proxySecret

Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { defineEventHandler, setResponseStatus } from 'h3'
import { defineEventHandler, setResponseStatus } from '#nuxt-scripts/h3'

export default defineEventHandler((event) => {
setResponseStatus(event, 204)
Expand Down
Loading
Loading