Skip to content

Commit d67d137

Browse files
onmaxharlan-zw
andcommitted
fix: support Nitro 2 and 3 server runtimes (#844)
Co-authored-by: Harlan Wilton <harlan@harlanzw.com>
1 parent d7f1f34 commit d67d137

31 files changed

Lines changed: 326 additions & 48 deletions

.github/workflows/ci.yml

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,48 @@ jobs:
8181
run: pnpm exec playwright-core install chromium
8282
continue-on-error: true
8383

84-
- run: pnpm run --if-present prepare:fixtures
85-
- run: pnpm vitest run --project typecheck --project unit --project e2e --project nuxt-runtime
84+
- name: Prepare fixtures
85+
run: pnpm run --if-present prepare:fixtures
86+
87+
- name: Run tests
88+
run: pnpm vitest run --project typecheck --project unit --project e2e --project nuxt-runtime
89+
90+
nitro-3:
91+
runs-on: ubuntu-24.04-arm
92+
name: Nitro 3 compatibility
93+
steps:
94+
- name: Checkout
95+
uses: actions/checkout@v7
96+
with:
97+
persist-credentials: false
98+
99+
- name: Setup pnpm
100+
uses: pnpm/action-setup@v6
101+
102+
- name: Setup Node
103+
uses: actions/setup-node@v7
104+
with:
105+
node-version: lts/*
106+
cache: pnpm
107+
108+
- name: Install dependencies
109+
run: pnpm i
110+
111+
- name: Install Nuxt 5
112+
run: |
113+
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"
114+
pnpm --filter @nuxt/scripts add --save-dev "@nuxt/kit@npm:@nuxt/kit-nightly@5.0.0-29745766.482f3357"
115+
116+
- name: Dev prepare
117+
run: pnpm --filter @nuxt/scripts dev:prepare
118+
119+
- name: Nuxt prepare
120+
run: |
121+
pnpm exec nuxt prepare
122+
pnpm exec nuxt prepare test/fixtures/proxy-alias
123+
124+
- name: Typecheck Nitro 3 compatibility
125+
run: pnpm exec vue-tsc --noEmit -p test/fixtures/proxy-alias/.nuxt/tsconfig.server.json
126+
127+
- name: Run Nitro 3 compatibility tests
128+
run: pnpm vitest run --project e2e test/e2e/proxy-alias.test.ts

docs/content/scripts/google-recaptcha.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ function onSubmit() {
201201
email: email.value,
202202
message: message.value
203203
}
204-
}).catch(() => null)
204+
}).catch((error) => {
205+
console.error('Failed to submit contact form', error)
206+
return null
207+
})
205208
206209
status.value = result ? 'success' : 'error'
207210
})

packages/devtools-app/components/DevtoolsTooltip.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const sizes = {
3535
<UIcon name="i-heroicons-question-mark-circle" class="size-3 text-(--color-text-subtle) hover:text-(--color-text-muted) transition-colors" />
3636
<template #content>
3737
<div class="devtools-tooltip-panel">
38-
<div :class="`w-max ${sizes[size || 'md']}`">
38+
<div class="w-max" :class="sizes[size || 'md']">
3939
<template v-if="title">
4040
<div class="font-semibold">{{ title }}</div>
4141
<div v-if="description" class="text-(--color-text-muted) text-xs">{{ description }}</div>
@@ -56,7 +56,7 @@ export const sizes = {
5656
<UIcon v-else name="i-heroicons-question-mark-circle" color="primary" :size="iconSize || 'md'" />
5757
<template #content>
5858
<div class="devtools-tooltip-panel">
59-
<div :class="`w-max ${sizes[size || 'md']}`">
59+
<div class="w-max" :class="sizes[size || 'md']">
6060
<slot v-if="$slots.text" name="text" />
6161
<template v-else-if="title">
6262
<div class="font-semibold">

packages/devtools-app/components/UiTooltip.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const sizes = {
3737
<UIcon name="i-carbon-help" class="size-3 text-(--color-text-subtle) hover:text-(--color-text-muted) transition-colors" />
3838
<template #content>
3939
<div class="ui-tooltip-panel">
40-
<div :class="`w-max ${sizes[size || 'md']}`">
40+
<div class="w-max" :class="sizes[size || 'md']">
4141
<template v-if="title">
4242
<div class="font-semibold">
4343
{{ title }}
@@ -66,7 +66,7 @@ export const sizes = {
6666
<UIcon v-else name="i-carbon-help" color="primary" :size="iconSize || 'md'" />
6767
<template #content>
6868
<div class="ui-tooltip-panel">
69-
<div :class="`w-max ${sizes[size || 'md']}`">
69+
<div class="w-max" :class="sizes[size || 'md']">
7070
<slot v-if="$slots.text" name="text" />
7171
<template v-else-if="title">
7272
<div class="font-semibold">

packages/script/src/kit.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ interface EnsurePackageInstalledOptions {
1111
}
1212

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

1721
logger.info(`Package ${name} is missing`)
1822
if (isCI)

packages/script/src/module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { setupPublicAssetStrategy } from './assets'
3535
import { buildDevtoolsData, buildDevtoolsEntry, setupDevtools } from './devtools'
3636
import { installNuxtModule } from './kit'
3737
import { logger } from './logger'
38+
import { setupNitroRuntimeCompatibility } from './nitro-compatibility'
3839
import { extractRequiredFields, migrateDeprecatedRegistryKeys, normalizeRegistryConfig } from './normalize'
3940
import { NuxtScriptsCheckScripts } from './plugins/check-scripts'
4041
import { generateInterceptPluginContents } from './plugins/intercept'
@@ -514,6 +515,7 @@ export default defineNuxtModule<ModuleOptions>({
514515
logger.debug('The module is disabled, skipping setup.')
515516
return
516517
}
518+
await setupNitroRuntimeCompatibility(nuxt)
517519
if (nuxt.options.dev) {
518520
setupDevtools(nuxt, { standalone: config._standaloneDevtools })
519521
if (config._standaloneDevtools) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type { Nuxt } from '@nuxt/schema'
2+
import { existsSync } from 'node:fs'
3+
import { pathToFileURL } from 'node:url'
4+
import { addTypeTemplate, getNuxtVersion, resolvePath as resolveNuxtPath } from '@nuxt/kit'
5+
import { dirname } from 'pathe'
6+
7+
type NitroRuntimeCompatibility
8+
= | { _tag: 'nitro-v2' }
9+
| {
10+
_tag: 'nitro-v3'
11+
app: string
12+
cache: string
13+
h3: string
14+
runtimeConfig: string
15+
}
16+
17+
type ResolveNitroImport = (id: string) => Promise<string>
18+
19+
interface NitroCompatibilityOptions {
20+
alias?: Record<string, string>
21+
virtual?: Record<string, string>
22+
}
23+
24+
interface NitroCompatibilityDependencies {
25+
addTypeTemplate: typeof addTypeTemplate
26+
getNuxtVersion: typeof getNuxtVersion
27+
resolveNitroImport?: ResolveNitroImport
28+
}
29+
30+
const NITRO_RUNTIME_MODULE = '#nuxt-scripts/nitro'
31+
const H3_RUNTIME_MODULE = '#nuxt-scripts/h3'
32+
const TYPE_TEMPLATE_FILENAME = 'types/nuxt-scripts-nitro.d.ts'
33+
const defaultDependencies: NitroCompatibilityDependencies = {
34+
addTypeTemplate,
35+
getNuxtVersion,
36+
}
37+
38+
const nitroV2Runtime = `export {
39+
defineCachedFunction,
40+
useNitroApp,
41+
useRuntimeConfig,
42+
} from 'nitropack/runtime'
43+
`
44+
45+
const nitroV3RuntimeTypes = `export { useNitroApp } from 'nitro/app'
46+
export { defineCachedFunction } from 'nitro/cache'
47+
export function useRuntimeConfig(event?: import('nitro/h3').H3Event): ReturnType<typeof import('nitro/runtime-config').useRuntimeConfig>
48+
`
49+
50+
function indent(value: string, spaces: number): string {
51+
const padding = ' '.repeat(spaces)
52+
return value.split('\n').map(line => `${padding}${line}`).join('\n')
53+
}
54+
55+
function renderRuntimeDeclarations(compatibility: NitroRuntimeCompatibility): string {
56+
const nitroRuntime = compatibility._tag === 'nitro-v3' ? nitroV3RuntimeTypes : nitroV2Runtime
57+
const h3Runtime = compatibility._tag === 'nitro-v3'
58+
? `export * from 'nitro/h3'\n`
59+
: `export * from 'h3'\n`
60+
61+
return `declare module '${NITRO_RUNTIME_MODULE}' {
62+
${indent(nitroRuntime.trim(), 2)}
63+
}
64+
65+
declare module '${H3_RUNTIME_MODULE}' {
66+
${indent(h3Runtime.trim(), 2)}
67+
}
68+
`
69+
}
70+
71+
function renderNitroV3Runtime(compatibility: Extract<NitroRuntimeCompatibility, { _tag: 'nitro-v3' }>): string {
72+
return `export { useNitroApp } from ${JSON.stringify(compatibility.app)}
73+
export { defineCachedFunction } from ${JSON.stringify(compatibility.cache)}
74+
import { useRuntimeConfig as _useRuntimeConfig } from ${JSON.stringify(compatibility.runtimeConfig)}
75+
export function useRuntimeConfig(_event) { return _useRuntimeConfig() }
76+
`
77+
}
78+
79+
function applyNitroRuntimeCompatibility(nuxt: Nuxt, compatibility: NitroRuntimeCompatibility): void {
80+
const nuxtOptions = nuxt.options as Nuxt['options'] & { nitro?: NitroCompatibilityOptions }
81+
const nitroOptions = nuxtOptions.nitro ||= {}
82+
nitroOptions.alias ||= {}
83+
nitroOptions.virtual ||= {}
84+
nitroOptions.alias[H3_RUNTIME_MODULE] = compatibility._tag === 'nitro-v3' ? compatibility.h3 : 'h3'
85+
nitroOptions.virtual[NITRO_RUNTIME_MODULE] = compatibility._tag === 'nitro-v3'
86+
? renderNitroV3Runtime(compatibility)
87+
: nitroV2Runtime
88+
}
89+
90+
async function createNuxtNitroImportResolver(): Promise<ResolveNitroImport> {
91+
const nuxtDir = dirname(await resolveNuxtPath('nuxt/package.json'))
92+
const nitroDir = dirname(await resolveNuxtPath('@nuxt/nitro-server/package.json', { cwd: nuxtDir }))
93+
94+
return async (id: string) => {
95+
const resolved = await resolveNuxtPath(id, { cwd: nitroDir })
96+
if (!existsSync(resolved))
97+
throw new Error(`[nuxt-scripts] Could not resolve Nitro runtime helper "${id}" from "${nitroDir}".`)
98+
return pathToFileURL(resolved).href
99+
}
100+
}
101+
102+
async function resolveNitroV3Compatibility(resolveNitroImport: ResolveNitroImport): Promise<NitroRuntimeCompatibility> {
103+
const [app, cache, h3, runtimeConfig] = await Promise.all([
104+
resolveNitroImport('nitro/app'),
105+
resolveNitroImport('nitro/cache'),
106+
resolveNitroImport('nitro/h3'),
107+
resolveNitroImport('nitro/runtime-config'),
108+
])
109+
110+
return { _tag: 'nitro-v3', app, cache, h3, runtimeConfig }
111+
}
112+
113+
export async function setupNitroRuntimeCompatibility(
114+
nuxt: Nuxt,
115+
dependencies: NitroCompatibilityDependencies = defaultDependencies,
116+
): Promise<void> {
117+
const compatibility: NitroRuntimeCompatibility = Number.parseInt(dependencies.getNuxtVersion(nuxt), 10) >= 5
118+
? await resolveNitroV3Compatibility(dependencies.resolveNitroImport || await createNuxtNitroImportResolver())
119+
: { _tag: 'nitro-v2' }
120+
121+
applyNitroRuntimeCompatibility(nuxt, compatibility)
122+
nuxt.hooks.hookOnce('modules:done', () => applyNitroRuntimeCompatibility(nuxt, compatibility))
123+
dependencies.addTypeTemplate({
124+
filename: TYPE_TEMPLATE_FILENAME,
125+
getContents: async () => renderRuntimeDeclarations(compatibility),
126+
}, { nitro: true, node: true, nuxt: true })
127+
}

packages/script/src/runtime/server/bluesky-embed.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
2-
import { useRuntimeConfig } from 'nitropack/runtime'
1+
import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3'
2+
import { useRuntimeConfig } from '#nuxt-scripts/nitro'
33
import { createCachedJsonFetch } from './utils/cached-upstream'
44
import { rewriteBlueskyPostImages } from './utils/embed-rewriters'
55
import { withSigning } from './utils/withSigning'
@@ -115,7 +115,7 @@ export default withSigning(defineEventHandler(async (event) => {
115115
const handlerPath = event.path?.split('?')[0] || ''
116116
const prefix = handlerPath.replace(EMBED_BSKY_SUFFIX_RE, '') || '/_scripts'
117117
const imagePath = `${prefix}/embed/bluesky-image`
118-
const secret = (useRuntimeConfig(event)['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret
118+
const secret = (useRuntimeConfig()['nuxt-scripts'] as { proxySecret?: string } | undefined)?.proxySecret
119119
rewriteBlueskyPostImages(post, imagePath, secret)
120120

121121
// Cache for 10 minutes

packages/script/src/runtime/server/google-maps-geocode-proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
2-
import { useRuntimeConfig } from 'nitropack/runtime'
31
import { withQuery } from 'ufo'
2+
import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3'
3+
import { useRuntimeConfig } from '#nuxt-scripts/nitro'
44
import { createCachedJsonFetch } from './utils/cached-upstream'
55
import { withSigning } from './utils/withSigning'
66

packages/script/src/runtime/server/google-static-maps-proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { createError, defineEventHandler, getQuery, setHeader } from 'h3'
2-
import { useRuntimeConfig } from 'nitropack/runtime'
31
import { withQuery } from 'ufo'
2+
import { createError, defineEventHandler, getQuery, setHeader } from '#nuxt-scripts/h3'
3+
import { useRuntimeConfig } from '#nuxt-scripts/nitro'
44
import { createCachedBinaryFetch } from './utils/cached-upstream'
55
import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_PARAM } from './utils/sign-constants'
66
import { withSigning } from './utils/withSigning'

0 commit comments

Comments
 (0)