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 @@ 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'