diff --git a/examples/module/test/basic.test.ts b/examples/module/test/basic.test.ts
index 2e3df6728..c9d9413e9 100644
--- a/examples/module/test/basic.test.ts
+++ b/examples/module/test/basic.test.ts
@@ -1,15 +1,49 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
-import { $fetch, setup } from '@nuxt/test-utils/e2e'
+import { $fetch, getBrowser, setRuntimeConfig, setup, url } from '@nuxt/test-utils/e2e'
describe('ssr', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
+ browser: true,
})
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
- expect(html).toContain('
basic
')
+ expect(html).toContain('original value')
+ })
+
+ it('changes runtime config client-side', async () => {
+ const browser = await getBrowser()
+ const page = await browser.newPage()
+ await page.goto(url('/'))
+
+ const el = page.locator('#runtime')
+ expect(await el.innerText()).to.equal('original value')
+
+ await page.evaluate(() => {
+ window.__NUXT_TEST_RUNTIME_CONFIG_SETTER__({ public: { myValue: 'overwritten by test!' } })
+ })
+
+ expect(await el.innerText()).to.equal('overwritten by test!')
+ })
+
+ it('changes runtime config in server route', async () => {
+ const originalConfig = await $fetch('/api/config')
+ expect(originalConfig.public.myValue).to.equal('original value')
+
+ await setRuntimeConfig({ public: { myValue: 'overwritten by test!' } })
+
+ const newConfig = await $fetch('/api/config')
+ expect(newConfig.public.myValue).to.equal('overwritten by test!')
+ })
+
+ it('changes runtime config', async () => {
+ expect(await $fetch('/')).toContain('original value')
+
+ await setRuntimeConfig({ public: { myValue: 'overwritten by test!' } }, { restart: true })
+
+ expect(await $fetch('/')).toContain('overwritten by test!')
})
})
diff --git a/examples/module/test/fixtures/basic/app.vue b/examples/module/test/fixtures/basic/app.vue
index 29a9c81fa..22f0dbf4a 100644
--- a/examples/module/test/fixtures/basic/app.vue
+++ b/examples/module/test/fixtures/basic/app.vue
@@ -1,6 +1,9 @@
- basic
+
+ basic {{ config.public.myValue }}
+
diff --git a/examples/module/test/fixtures/basic/nuxt.config.ts b/examples/module/test/fixtures/basic/nuxt.config.ts
index 1bc2f7ccd..5b1ef6ef3 100644
--- a/examples/module/test/fixtures/basic/nuxt.config.ts
+++ b/examples/module/test/fixtures/basic/nuxt.config.ts
@@ -1,6 +1,11 @@
import MyModule from '../../../src/module'
export default defineNuxtConfig({
+ runtimeConfig: {
+ public: {
+ myValue: 'original value',
+ },
+ },
modules: [
MyModule
]
diff --git a/examples/module/test/fixtures/basic/plugins/ipc-listener.ts b/examples/module/test/fixtures/basic/plugins/ipc-listener.ts
new file mode 100644
index 000000000..b1bcd800b
--- /dev/null
+++ b/examples/module/test/fixtures/basic/plugins/ipc-listener.ts
@@ -0,0 +1,30 @@
+import defu from 'defu'
+import { defineNuxtPlugin } from 'nuxt/app'
+
+declare global {
+ interface Window {
+ __NUXT_TEST_RUNTIME_CONFIG_SETTER__: (env: { public: Record }) => void
+ }
+}
+
+export default defineNuxtPlugin(() => {
+ const config = useRuntimeConfig()
+
+ if (process.client) {
+ window.__NUXT_TEST_RUNTIME_CONFIG_SETTER__ ??= (env: { public: Record }) => {
+ config.public = defu(env.public, config.public)
+ }
+ }
+
+ if (process.server) {
+ process.on('message', (msg: { type: string; value: Record }) => {
+ if (msg.type === 'update:runtime-config') {
+ for (const [key, value] of Object.entries(msg.value)) {
+ process.env[key] = value
+ }
+
+ process!.send!({ type: 'confirm:runtime-config' })
+ }
+ })
+ }
+})
diff --git a/examples/module/test/fixtures/basic/server/api/config.ts b/examples/module/test/fixtures/basic/server/api/config.ts
new file mode 100644
index 000000000..f07457e24
--- /dev/null
+++ b/examples/module/test/fixtures/basic/server/api/config.ts
@@ -0,0 +1,4 @@
+export default defineEventHandler(async (event) => {
+ const config = useRuntimeConfig(event)
+ return config
+})
diff --git a/package.json b/package.json
index 597f7ed35..e50d35672 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"pathe": "^1.1.1",
"perfect-debounce": "^1.0.0",
"radix3": "^1.1.0",
+ "scule": "^1.1.1",
"std-env": "^3.6.0",
"ufo": "^1.3.2",
"unenv": "^1.8.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ccb0a59f0..547084fb0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -64,6 +64,9 @@ importers:
radix3:
specifier: ^1.1.0
version: 1.1.0
+ scule:
+ specifier: ^1.1.1
+ version: 1.1.1
std-env:
specifier: ^3.6.0
version: 3.6.0
diff --git a/src/core/runtime-config.ts b/src/core/runtime-config.ts
new file mode 100644
index 000000000..b4edcbf17
--- /dev/null
+++ b/src/core/runtime-config.ts
@@ -0,0 +1,67 @@
+import { snakeCase } from 'scule'
+import { useTestContext } from './context'
+import { startServer } from './server'
+
+export function flattenObject(obj: Record = {}) {
+ const flattened: Record = {}
+
+ for (const key in obj) {
+ if (!(key in obj)) continue
+
+ const entry = obj[key]
+ if (typeof entry !== 'object' || entry == null) {
+ flattened[key] = obj[key]
+ continue
+ }
+ const flatObject = flattenObject(entry as Record)
+
+ for (const x in flatObject) {
+ if (!(x in flatObject)) continue
+
+ flattened[key + '_' + x] = flatObject[x]
+ }
+ }
+
+ return flattened
+}
+
+export function convertObjectToConfig(obj: Record, envPrefix: string) {
+ const makeEnvKey = (str: string) => `${envPrefix}${snakeCase(str).toUpperCase()}`
+
+ const env: Record = {}
+ const flattened = flattenObject(obj)
+ for (const key in flattened) {
+ env[makeEnvKey(key)] = flattened[key]
+ }
+
+ return env
+}
+
+type SetRuntimeConfigOptions = { envPrefix?: string; restart?: boolean }
+export async function setRuntimeConfig(config: Record, options: SetRuntimeConfigOptions = {}) {
+ const env = convertObjectToConfig(config, options.envPrefix ?? 'NUXT_')
+ const ctx = useTestContext()
+
+ if (options.restart) {
+ return await startServer({ env })
+ }
+
+ let updatedConfig = false
+ ctx.serverProcess?.once('message', (msg: { type: string }) => {
+ if (msg.type === 'confirm:runtime-config') {
+ updatedConfig = true
+ }
+ })
+
+ ctx.serverProcess?.send({ type: 'update:runtime-config', value: env })
+
+ // Wait for confirmation to ensure
+ for (let i = 0; i < 10; i++) {
+ if (updatedConfig) break
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+
+ if (!updatedConfig) {
+ throw new Error('Missing confirmation of runtime config update!')
+ }
+}
diff --git a/src/core/server.ts b/src/core/server.ts
index 1305f7b5e..e0080d619 100644
--- a/src/core/server.ts
+++ b/src/core/server.ts
@@ -1,4 +1,4 @@
-import { execa } from 'execa'
+import { execa, execaNode } from 'execa'
import { getRandomPort, waitForPort } from 'get-port-please'
import type { FetchOptions } from 'ofetch'
import { $fetch as _$fetch, fetch as _fetch } from 'ofetch'
@@ -10,7 +10,11 @@ import { useTestContext } from './context'
// eslint-disable-next-line
const kit: typeof _kit = _kit.default || _kit
-export async function startServer () {
+export interface StartServerOptions {
+ env?: Record
+}
+
+export async function startServer (options: StartServerOptions = {}) {
const ctx = useTestContext()
await stopServer()
const host = '127.0.0.1'
@@ -26,7 +30,8 @@ export async function startServer () {
_PORT: String(port), // Used by internal _dev command
PORT: String(port),
HOST: host,
- NODE_ENV: 'development'
+ NODE_ENV: 'development',
+ ...options.env
}
})
await waitForPort(port, { retries: 32, host }).catch(() => {})
@@ -45,16 +50,15 @@ export async function startServer () {
ctx.serverProcess.kill()
throw lastError || new Error('Timeout waiting for dev server!')
} else {
- ctx.serverProcess = execa('node', [
- resolve(ctx.nuxt!.options.nitro.output!.dir!, 'server/index.mjs')
- ], {
+ ctx.serverProcess = execaNode(resolve(ctx.nuxt!.options.nitro.output!.dir!, 'server/index.mjs'), {
stdio: 'inherit',
env: {
...process.env,
PORT: String(port),
HOST: host,
- NODE_ENV: 'test'
- }
+ NODE_ENV: 'test',
+ ...options.env,
+ },
})
await waitForPort(port, { retries: 20, host })
}
diff --git a/src/e2e.ts b/src/e2e.ts
index f8604f58d..7ed2bb1d2 100644
--- a/src/e2e.ts
+++ b/src/e2e.ts
@@ -5,4 +5,5 @@ export * from './core/nuxt'
export * from './core/server'
export * from './core/setup/index'
export * from './core/run'
+export { setRuntimeConfig } from './core/runtime-config'
export * from './core/types'