Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
15 changes: 13 additions & 2 deletions examples/module/test/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
import { $fetch, setRuntimeConfig, setup } from '@nuxt/test-utils/e2e'

describe('ssr', async () => {
await setup({
Expand All @@ -10,6 +10,17 @@ describe('ssr', async () => {
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
expect(html).toContain('<div>basic</div>')
expect(html).toContain('<div>basic <span>original value</span></div>')
})

it('changes runtime config and restarts', async () => {
const restoreConfig = await setRuntimeConfig({ public: { myValue: 'overwritten by test!' } })

const html = await $fetch('/')
expect(html).toContain('<div>basic <span>overwritten by test!</span></div>')

await restoreConfig()
const htmlRestored = await $fetch('/')
expect(htmlRestored).toContain('<div>basic <span>original value</span></div>')
})
})
3 changes: 2 additions & 1 deletion examples/module/test/fixtures/basic/app.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div>basic</div>
<div>basic <span>{{ config.public.myValue }}</span></div>
</template>

<script setup>
const config = useRuntimeConfig();
</script>
5 changes: 5 additions & 0 deletions examples/module/test/fixtures/basic/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import MyModule from '../../../src/module'

export default defineNuxtConfig({
runtimeConfig: {
public: {
myValue: 'original value',
},
},
modules: [
MyModule
]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions src/core/runtime-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { snakeCase } from 'scule'
import { startServer } from './server'

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

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<string, unknown>)

for (const x in flatObject) {
if (!(x in flatObject)) continue

flattened[key + '_' + x] = flatObject[x]
}
}

return flattened
}

export function convertObjectToConfig(obj: Record<string, unknown>, envPrefix: string) {
const makeEnvKey = (str: string) => `${envPrefix}${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 async function setRuntimeConfig(config: Record<string, unknown>, envPrefix = 'NUXT_') {
const env = convertObjectToConfig(config, envPrefix)
await startServer({ env })

// restore
return async () => startServer()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not too much fan of this API. setRuntimeConfig sounds like doing a runtime operation without full server restart and nitro useRuntimeConfig - when used correctly in context - reflects any runtime env changes as soon as they change without need of a manual server restart. If manual server restart is desired for any incompatibility, it should be opt-in IMO with something like setRuntimeConfig({}, { restart: true } (we could also automatically do it for well known keys such as baseurl)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the issue that it sounds too similar to useRuntimeConfig while not functioning in a similar way (trigger restart, not reactive), or that the server gets restarted either way?

The reason for restarting the server is that I don't know how (or if it is possible) to update the env variables of the running server process. It would be preferable to be able to handle/apply this without a server restart, if that is not possible I don't think opt-in restart makes much sense in my opinion.

I agree that it sounds similar to useRuntimeConfig, maybe a better name could be applyRuntimeConfig, overrideRuntimeConfig or overwriteRuntimeConfig?

Something else I considered would be something like this:

await withRuntimeConfig(config, async () => { /* tests that use `config` */ }, options)

So that restoring/resetting config always happens without additional action, I think having to restore it separately can easily be forgotten.

Either way, I'm open to suggestions 😅 I'm not aware of other ways to easily test runtime config support for module authors, so I think this is essential if we want more modules to reliably support it.

Copy link
Member

@pi0 pi0 Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for restarting the server is that I don't know how (or if it is possible) to update the env variables of the running server process

An IPC signal can do it using process.send . Alternatively, we can export a dev middleware when starting nuxt in test mode to allow it. It is much cheaper than full-reload and also allows stateful testing when we need to see the effect of one even without for example losing connection. Please ping me if need any more insights on how to do this.

I agree that it sounds similar to useRuntimeConfig, maybe a better name could be applyRuntimeConfig, overrideRuntimeConfig, or overwriteRuntimeConfig?

If you are thinking this (as a set-and-restart) would be still a useful tool and faster to iterate, I would suggest to explicitly call it startServerWithRuntimeConfig as it does this exactly.

Copy link
Member Author

@BobbieGoede BobbieGoede Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An IPC signal can do it using process.send

Thanks for the hint!

I managed to communicate with the server process using process.send and am able to make changes to the process.env while keeping it running. Using a server route I can confirm that useRuntimeConfig(event) contains the updated values, unfortunately it doesn't seem to update in plugins (testing server side) before/after page reload.

Does overwriting runtime config variables with environment variables only work in server routes or should this also work elsewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm on the wrong track entirely but it seems like things get a bit more complicated.. 😅 Please let me know if this sounds right or not.

1. Server routes
Runtime config can be overwritten by setting environment variables using an IPC signal.

2. Server-side plugins and such
Runtime config is frozen, overwriting requires restarting server with environment variables.

3. Client-side
Runtime config is reactive, can be changed using page.evaluate or something along those lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does overwriting runtime config variables with environment variables only work in server routes or should this also work elsewhere?

We might need to (also) listen via a Nuxt plugin but seems so strange because last I recall Nuxt also uses direct import from Nitro utils...

Do you mind to push your latest changes? If feel too much different another (draft PR) might also be good so I can check locally somehow 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been working on this locally inside the @nuxtjs/i18n repo as I already have tests for runtime config there, also I'm not sure what the intended workflow in this repo is but I find myself having to run dev:prepare and prepack before running tests/checking if the changed test utils work.

The code is a bit of a mess as I was exploring what worked and what didn't, I'll clean it up a bit and open a draft PR.

We might need to (also) listen via a Nuxt plugin ...

My implementation uses a Nuxt plugin to start listening for messages, is that what you mean?

... but seems so strange because last I recall Nuxt also uses direct import from Nitro utils...

When an H3Event is passed to useRuntimeConfig it does pick up the changes, otherwise it keeps using the original values. I think you describe the same issue of having to restart the server/Nuxt instance here: nuxt/nuxt#14330.

I'll link the draft PR here once I have it ready!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My implementation uses a Nuxt plugin to start listening for messages, is that what you mean?

Then you probably can try swapping/adding a Nitro plugin. It runs before and also applies for server logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0
Here's the draft PR #670

}
12 changes: 9 additions & 3 deletions src/core/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
}

export async function startServer (options: StartServerOptions = {}) {
const ctx = useTestContext()
await stopServer()
const host = '127.0.0.1'
Expand All @@ -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(() => {})
Expand All @@ -53,7 +58,8 @@ export async function startServer () {
...process.env,
PORT: String(port),
HOST: host,
NODE_ENV: 'test'
NODE_ENV: 'test',
...options.env
}
})
await waitForPort(port, { retries: 20, host })
Expand Down
1 change: 1 addition & 0 deletions src/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'