From cebc89186e30a5e5faea2e1823cd07d5bfcf1488 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 9 May 2024 18:49:35 +0100 Subject: [PATCH] feat(kit): add `useRuntimeConfig` and `updateRuntimeConfig` utils (#27117) --- docs/3.api/5.kit/10.runtime-config.md | 27 +++++++ packages/kit/package.json | 2 + packages/kit/src/index.ts | 1 + packages/kit/src/runtime-config.ts | 103 ++++++++++++++++++++++++++ packages/nuxt/src/core/nitro.ts | 22 +----- packages/nuxt/src/core/nuxt.ts | 5 +- packages/schema/src/config/nitro.ts | 19 +++++ pnpm-lock.yaml | 6 ++ 8 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 docs/3.api/5.kit/10.runtime-config.md create mode 100644 packages/kit/src/runtime-config.ts diff --git a/docs/3.api/5.kit/10.runtime-config.md b/docs/3.api/5.kit/10.runtime-config.md new file mode 100644 index 0000000000..49f5348703 --- /dev/null +++ b/docs/3.api/5.kit/10.runtime-config.md @@ -0,0 +1,27 @@ +--- +title: Runtime Config +description: Nuxt Kit provides a set of utilities to help you access and modify Nuxt runtime configuration. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/nuxt/blob/main/packages/kit/src/runtime-config.ts + size: xs +--- + +## `useRuntimeConfig` + +At build-time, it is possible to access the resolved Nuxt [runtime config](/docs/guide/going-further/runtime-config). + +### Type + +```ts +function useRuntimeConfig (): Record +``` + +## `updateRuntimeConfig` + +It is also possible to update runtime configuration. This will be merged with the existing runtime configuration, and if Nitro has already been initialized it will trigger an HMR event to reload the Nitro runtime config. + +```ts +function updateRuntimeConfig (config: Record): void | Promise +``` diff --git a/packages/kit/package.json b/packages/kit/package.json index 15ca2e4001..5dd4e55693 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -30,10 +30,12 @@ "c12": "^1.10.0", "consola": "^3.2.3", "defu": "^6.1.4", + "destr": "^2.0.3", "globby": "^14.0.1", "hash-sum": "^2.0.0", "ignore": "^5.3.1", "jiti": "^1.21.0", + "klona": "^2.0.6", "knitwork": "^1.1.0", "mlly": "^1.7.0", "pathe": "^1.1.2", diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index fb2350660f..405de8606a 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -10,6 +10,7 @@ export * from './loader/nuxt' // Utils export * from './imports' +export { updateRuntimeConfig, useRuntimeConfig } from './runtime-config' export * from './build' export * from './compatibility' export * from './components' diff --git a/packages/kit/src/runtime-config.ts b/packages/kit/src/runtime-config.ts new file mode 100644 index 0000000000..a83358f11c --- /dev/null +++ b/packages/kit/src/runtime-config.ts @@ -0,0 +1,103 @@ +import process from 'node:process' +import destr from 'destr' +import { snakeCase } from 'scule' +import { klona } from 'klona' + +import defu from 'defu' +import { useNuxt } from './context' +import { useNitro } from './nitro' + +/** + * Access 'resolved' Nuxt runtime configuration, with values updated from environment. + * + * This mirrors the runtime behavior of Nitro. + */ +export function useRuntimeConfig () { + const nuxt = useNuxt() + return applyEnv(klona(nuxt.options.nitro.runtimeConfig!), { + prefix: 'NITRO_', + altPrefix: 'NUXT_', + envExpansion: nuxt.options.nitro.experimental?.envExpansion ?? !!process.env.NITRO_ENV_EXPANSION, + }) +} + +/** + * Update Nuxt runtime configuration. + */ +export function updateRuntimeConfig (runtimeConfig: Record) { + const nuxt = useNuxt() + Object.assign(nuxt.options.nitro.runtimeConfig as Record, defu(runtimeConfig, nuxt.options.nitro.runtimeConfig)) + + try { + return useNitro().updateConfig({ runtimeConfig }) + } catch { + // Nitro is not yet initialised - we can safely ignore this error + } +} + +/** + * @internal + * + * https://github.com/unjs/nitro/blob/main/src/runtime/utils.env.ts. + * + * These utils will be replaced by util exposed from nitropack. See https://github.com/unjs/nitro/pull/2404 + * for more context and future plans.) + */ + +type EnvOptions = { + prefix?: string + altPrefix?: string + envExpansion?: boolean +} + +function getEnv (key: string, opts: EnvOptions, env = process.env) { + const envKey = snakeCase(key).toUpperCase() + return destr( + env[opts.prefix + envKey] ?? env[opts.altPrefix + envKey], + ) +} + +function _isObject (input: unknown) { + return typeof input === 'object' && !Array.isArray(input) +} + +function applyEnv ( + obj: Record, + opts: EnvOptions, + parentKey = '', +) { + for (const key in obj) { + const subKey = parentKey ? `${parentKey}_${key}` : key + const envValue = getEnv(subKey, opts) + if (_isObject(obj[key])) { + // Same as before + if (_isObject(envValue)) { + obj[key] = { ...(obj[key] as any), ...(envValue as any) } + applyEnv(obj[key], opts, subKey) + } else if (envValue === undefined) { + // If envValue is undefined + // Then proceed to nested properties + applyEnv(obj[key], opts, subKey) + } else { + // If envValue is a primitive other than undefined + // Then set objValue and ignore the nested properties + obj[key] = envValue ?? obj[key] + } + } else { + obj[key] = envValue ?? obj[key] + } + // Experimental env expansion + if (opts.envExpansion && typeof obj[key] === 'string') { + obj[key] = _expandFromEnv(obj[key]) + } + } + return obj +} + +const envExpandRx = /{{(.*?)}}/g + +function _expandFromEnv (value: string, env: Record = process.env) { + return value.replace(envExpandRx, (match, key) => { + return env[key] || match + }) +} diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index d908f28704..1a5ac75fb2 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -12,7 +12,7 @@ import { defu } from 'defu' import fsExtra from 'fs-extra' import { dynamicEventHandler } from 'h3' import { isWindows } from 'std-env' -import type { Nuxt, NuxtOptions, RuntimeConfig } from 'nuxt/schema' +import type { Nuxt, NuxtOptions } from 'nuxt/schema' import { version as nuxtVersion } from '../../package.json' import { distDir } from '../dirs' import { toArray } from '../utils' @@ -27,8 +27,6 @@ const logLevelMapReverse = { export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { // Resolve config - const _nitroConfig = ((nuxt.options as any).nitro || {}) as NitroConfig - const excludePaths = nuxt.options._layers .flatMap(l => [ l.cwd.match(/(?<=\/)node_modules\/(.+)$/)?.[1], @@ -48,7 +46,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { .map(m => m.entryPath), ) - const nitroConfig: NitroConfig = defu(_nitroConfig, { + const nitroConfig: NitroConfig = defu(nuxt.options.nitro, { debug: nuxt.options.debug, rootDir: nuxt.options.rootDir, workspaceDir: nuxt.options.workspaceDir, @@ -57,7 +55,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { buildDir: nuxt.options.buildDir, experimental: { asyncContext: nuxt.options.experimental.asyncContext, - typescriptBundlerResolution: nuxt.options.future.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || _nitroConfig.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler', + typescriptBundlerResolution: nuxt.options.future.typescriptBundlerResolution || nuxt.options.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler' || nuxt.options.nitro.typescript?.tsConfig?.compilerOptions?.moduleResolution?.toLowerCase() === 'bundler', }, framework: { name: 'nuxt', @@ -110,20 +108,6 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) { routeRules: { '/__nuxt_error': { cache: false }, }, - runtimeConfig: { - ...nuxt.options.runtimeConfig, - app: { - ...nuxt.options.runtimeConfig.app, - baseURL: nuxt.options.runtimeConfig.app.baseURL.startsWith('./') - ? nuxt.options.runtimeConfig.app.baseURL.slice(1) - : nuxt.options.runtimeConfig.app.baseURL, - }, - nitro: { - envPrefix: 'NUXT_', - // TODO: address upstream issue with defu types...? - ...nuxt.options.runtimeConfig.nitro satisfies RuntimeConfig['nitro'] as any, - }, - }, appConfig: nuxt.options.appConfig, appConfigFiles: nuxt.options._layers.map( layer => resolve(layer.config.srcDir, 'app.config'), diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index fe2c189081..113d4aea73 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -4,7 +4,7 @@ import ignore from 'ignore' import type { LoadNuxtOptions } from '@nuxt/kit' import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit' import { resolvePath as _resolvePath } from 'mlly' -import type { Nuxt, NuxtHooks, NuxtOptions } from 'nuxt/schema' +import type { Nuxt, NuxtHooks, NuxtOptions, RuntimeConfig } from 'nuxt/schema' import type { PackageJson } from 'pkg-types' import { readPackageJSON, resolvePackageJSON } from 'pkg-types' @@ -600,6 +600,9 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise { options._modules.push('@nuxt/telemetry') } + // Ensure we share runtime config between Nuxt and Nitro + options.runtimeConfig = options.nitro.runtimeConfig as RuntimeConfig + const nuxt = createNuxt(options) // We register hooks layer-by-layer so any overrides need to be registered separately diff --git a/packages/schema/src/config/nitro.ts b/packages/schema/src/config/nitro.ts index d6ca241c17..5c72ba750f 100644 --- a/packages/schema/src/config/nitro.ts +++ b/packages/schema/src/config/nitro.ts @@ -1,4 +1,5 @@ import { defineUntypedSchema } from 'untyped' +import type { RuntimeConfig } from '../types/config' export default defineUntypedSchema({ /** @@ -7,6 +8,24 @@ export default defineUntypedSchema({ * @type {typeof import('nitropack')['NitroConfig']} */ nitro: { + runtimeConfig: { + $resolve: async (val: Record | undefined, get) => { + const runtimeConfig = await get('runtimeConfig') as RuntimeConfig + return { + ...runtimeConfig, + app: { + ...runtimeConfig.app, + baseURL: runtimeConfig.app.baseURL.startsWith('./') + ? runtimeConfig.app.baseURL.slice(1) + : runtimeConfig.app.baseURL, + }, + nitro: { + envPrefix: 'NUXT_', + ...runtimeConfig.nitro, + }, + } + }, + }, routeRules: { $resolve: async (val: Record | undefined, get) => ({ ...await get('routeRules') as Record, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7eba88cbd5..8700deebe9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: defu: specifier: ^6.1.4 version: 6.1.4 + destr: + specifier: ^2.0.3 + version: 2.0.3 globby: specifier: ^14.0.1 version: 14.0.1 @@ -179,6 +182,9 @@ importers: jiti: specifier: ^1.21.0 version: 1.21.0 + klona: + specifier: ^2.0.6 + version: 2.0.6 knitwork: specifier: ^1.1.0 version: 1.1.0