Skip to content

test: set runtime config using IPC signal #3572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 25, 2025
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
3 changes: 2 additions & 1 deletion knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"src/runtime/{plugins,components,composables}/**",
"src/runtime/server/**",
"src/runtime/kit/**",
"scripts/*"
"scripts/*",
"specs/utils/nitro-plugin.ts"
],
"ignoreUnresolved": [
// virtuals
Expand Down
12 changes: 3 additions & 9 deletions specs/basic-usage-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,12 @@ export function basicUsageTests() {

expect(await getText(page, '#runtime-config')).toEqual('Hello from runtime config!')

const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: { runtimeValue: 'The environment variable has changed!' }
})

await gotoPath(page, '/')
expect(await getText(page, '#runtime-config')).toEqual('The environment variable has changed!')

await restore()
})

test('layer provides locale `nl` and translation for key `hello`', async () => {
Expand Down Expand Up @@ -389,7 +387,7 @@ export function basicUsageTests() {
test('render seo tags with baseUrl', async () => {
const configDomain = 'https://runtime-config-domain.com'

const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
baseUrl: configDomain
Expand All @@ -407,8 +405,6 @@ export function basicUsageTests() {
expect(dom.querySelector('#i18n-alt-fr')?.getAttribute('href')).toEqual(
'https://runtime-config-domain.com/fr?canonical='
)

await restore()
})

test('render seo tags with `experimental.alternateLinkCanonicalQueries`', async () => {
Expand Down Expand Up @@ -534,15 +530,13 @@ export function basicUsageTests() {
})

test('(#2000) Should be able to load large vue-i18n messages', async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: { longTextTest: true }
})

const { page } = await renderPage('/nl/long-text')

expect(await getText(page, '#long-text')).toEqual('hallo,'.repeat(8 * 500))

await restore()
})

test('(#2094) vue-i18n messages are loaded from config exported as variable', async () => {
Expand Down
17 changes: 10 additions & 7 deletions specs/basic_usage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ describe('basic usage', async () => {
describe('language switching', async () => {
beforeAll(async () => {
setTestContext(ctx)
await startServerWithRuntimeConfig({
public: {
i18n: {
skipSettingLocaleOnNavigate: true,
detectBrowserLanguage: false
await startServerWithRuntimeConfig(
{
public: {
i18n: {
skipSettingLocaleOnNavigate: true,
detectBrowserLanguage: false
}
}
}
})
},
true
)
})

languageSwitchingTests()
Expand Down
17 changes: 10 additions & 7 deletions specs/basic_usage_compat_4.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@ describe('basic usage - compatibilityVersion: 4', async () => {
describe('language switching', async () => {
beforeAll(async () => {
setTestContext(ctx)
await startServerWithRuntimeConfig({
public: {
i18n: {
skipSettingLocaleOnNavigate: true,
detectBrowserLanguage: false
await startServerWithRuntimeConfig(
{
public: {
i18n: {
skipSettingLocaleOnNavigate: true,
detectBrowserLanguage: false
}
}
}
})
},
true
)
})

languageSwitchingTests()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ await setup({

describe('`detectBrowserLanguage` using strategy `prefix_except_default`', async () => {
test('(#2262) redirect using browser cookie with `alwaysRedirect: true`', async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
detectBrowserLanguage: {
Expand Down Expand Up @@ -52,8 +52,6 @@ describe('`detectBrowserLanguage` using strategy `prefix_except_default`', async
await page.locator('#nuxt-locale-link-en').click()
expect(await getText(page, '#lang-switcher-current-locale code')).toEqual('en')
expect(await ctx.cookies()).toMatchObject([{ name: 'i18n_redirected', value: 'en' }])

await restore()
})

describe('(#2255) detect browser language and redirect on root', async () => {
Expand Down
57 changes: 29 additions & 28 deletions specs/helper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// @ts-ignore
import createJITI from 'jiti'
import { JSDOM } from 'jsdom'
import { getBrowser, startServer, url, useTestContext } from './utils'
import { getBrowser, TestContext, url, useTestContext } from './utils'
import { snakeCase } from 'scule'
import { resolveAlias } from '@nuxt/kit'
import { onTestFinished } from 'vitest'

import { errors, type BrowserContextOptions, type Page } from 'playwright-core'

Expand Down Expand Up @@ -149,42 +150,42 @@ export async function waitForURL(page: Page, path: string) {
}
}

function flattenObject(obj: Record<string, unknown> = {}) {
const flattened: Record<string, unknown> = {}

for (const key of Object.keys(obj)) {
const entry = obj[key]

if (typeof entry !== 'object' || entry == null) {
flattened[key] = obj[key]
continue
async function updateProcessRuntimeConfig(ctx: TestContext, config: unknown) {
const updated = new Promise<unknown>(resolve => {
const handler = (msg: { type: string; value: unknown }) => {
if (msg.type === 'confirm:runtime-config') {
ctx.serverProcess!.process?.off('message', handler)
resolve(msg.value)
}
}
ctx.serverProcess!.process?.on('message', handler)
})

const flatObject = flattenObject(entry as Record<string, unknown>)
for (const x of Object.keys(flatObject)) {
flattened[key + '_' + x] = flatObject[x]
}
}
ctx.serverProcess!.process?.send({ type: 'update:runtime-config', value: config }, undefined, {
keepOpen: true
})

return flattened
return await updated
}

function convertObjectToConfig(obj: Record<string, unknown>) {
const makeEnvKey = (str: string) => `NUXT_${snakeCase(str).toUpperCase()}`
export async function startServerWithRuntimeConfig(env: Record<string, unknown>, skipRestore = false) {
const ctx = useTestContext()

const stored = await updateProcessRuntimeConfig(ctx, env)

const env: Record<string, unknown> = {}
const flattened = flattenObject(obj)
for (const key in flattened) {
env[makeEnvKey(key)] = flattened[key]
let restored = false
const restoreFn = async () => {
if (restored) return

restored = true
await await updateProcessRuntimeConfig(ctx, stored)
}

return env
}
if (!skipRestore) {
onTestFinished(restoreFn)
}

export async function startServerWithRuntimeConfig(env: Record<string, unknown>) {
const converted = convertObjectToConfig(env)
await startServer(converted)
return async () => startServer()
return restoreFn
}

export async function localeLoaderHelpers() {
Expand Down
13 changes: 8 additions & 5 deletions specs/routing_strategies/no_prefix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ await setup({

describe('strategy: no_prefix', async () => {
beforeAll(async () => {
await startServerWithRuntimeConfig({
public: {
i18n: { detectBrowserLanguage: false }
}
})
await startServerWithRuntimeConfig(
{
public: {
i18n: { detectBrowserLanguage: false }
}
},
true
)
})

test('cannot access with locale prefix: /ja', async () => {
Expand Down
21 changes: 10 additions & 11 deletions specs/routing_strategies/prefix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ await setup({
describe('strategy: prefix', async () => {
beforeEach(async () => {
// use original fixture `detectBrowserLanguage` value as default for tests, overwrite here needed
await startServerWithRuntimeConfig({
public: {
i18n: { detectBrowserLanguage: false }
}
})
await startServerWithRuntimeConfig(
{
public: {
i18n: { detectBrowserLanguage: false }
}
},
true
)
})

test.each([
Expand Down Expand Up @@ -120,7 +123,7 @@ describe('strategy: prefix', async () => {
})

test('(#1889) navigation to page with `defineI18nRoute(false)`', async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
detectBrowserLanguage: {
Expand Down Expand Up @@ -153,8 +156,6 @@ describe('strategy: prefix', async () => {
// does not redirect to prefixed route for routes with disabled localization
await page.goto(url('/ignore-routes/disable'))
await waitForURL(page, '/ignore-routes/disable')

await restore()
})

test('should not transform `defineI18nRoute()` inside template', async () => {
Expand All @@ -165,7 +166,7 @@ describe('strategy: prefix', async () => {
})

test("(#2132) should redirect on root url with `redirectOn: 'no prefix'`", async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
detectBrowserLanguage: {
Expand All @@ -183,8 +184,6 @@ describe('strategy: prefix', async () => {

await gotoPath(page, '/en')
expect(await getText(page, '#home-header')).toEqual('Homepage')

await restore()
})

test('(#2020) pass query parameter', async () => {
Expand Down
8 changes: 2 additions & 6 deletions specs/routing_strategies/root_redirect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ await setup({

describe('rootRedirect', async () => {
test('can redirect to rootRedirect option path', async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
rootRedirect: 'fr'
Expand All @@ -29,12 +29,10 @@ describe('rootRedirect', async () => {

const res = await fetch('/')
expect(res.url).toBe(url('/fr'))

await restore()
})

test('(#2758) `statusCode` in `rootRedirect` should work with strategy "prefix"', async () => {
const restore = await startServerWithRuntimeConfig({
await startServerWithRuntimeConfig({
public: {
i18n: {
rootRedirect: { statusCode: 418, path: 'test-route' }
Expand All @@ -45,7 +43,5 @@ describe('rootRedirect', async () => {
const res = await fetch(url('/'))
expect(res.status).toEqual(418)
expect(res.headers.get('location')).toEqual('/en/test-route')

await restore()
})
})
68 changes: 68 additions & 0 deletions specs/utils/nitro-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
import { snakeCase } from 'scule'

function flattenObject(obj: Record<string, unknown> = {}) {
const flattened: Record<string, unknown> = {}

for (const key of Object.keys(obj)) {
const entry = obj[key]

if (typeof entry !== 'object' || entry == null) {
flattened[key] = obj[key]
continue
}

const flatObject = flattenObject(entry as Record<string, unknown>)
for (const x of Object.keys(flatObject)) {
flattened[key + '_' + x] = flatObject[x]
}
}

return flattened
}

export function convertObjectToConfig(obj: Record<string, unknown>) {
const makeEnvKey = (str: string) => `NUXT_${snakeCase(str).toUpperCase()}`

const env: Record<string, unknown> = {}
const flattened = flattenObject(obj)
for (const key in flattened) {
env[makeEnvKey(key)] = flattened[key]
}

return env
}

export default defineNitroPlugin(async nitroApp => {
const config = useRuntimeConfig()
const tempKeys = new Set<string>()

const handler = (msg: { type: string; value: Record<string, string> }) => {
if (msg.type !== 'update:runtime-config') return

// cleanup temporary keys
for (const k of tempKeys) {
delete process.env[k]
}

// flatten object and use env variable keys
const envConfig = convertObjectToConfig(msg.value)
for (const [k, val] of Object.entries(envConfig)) {
// collect keys which are newly introduced to cleanup later
if (k in process.env === false) {
tempKeys.add(k)
}

// @ts-expect-error untyped
process.env[k] = val
}

process.send!({ type: 'confirm:runtime-config', value: config }, undefined, {
keepOpen: true
})
}

process.on('message', handler)

nitroApp.hooks.hook('close', () => process.off('message', handler))
})
Loading