Skip to content

Commit

Permalink
feat: support dynamic app config and runtime config (#1154)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Apr 21, 2023
1 parent 6a4e57e commit 1d218eb
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 40 deletions.
3 changes: 2 additions & 1 deletion docs/content/1.guide/2.auto-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Nitro is using [unjs/unimport](https://github.com/unjs/unimport) to auto import
- `defineCachedFunction(fn, options)`{lang=ts} / `cachedFunction(fn, options)`{lang=ts}
- `defineCachedEventHandler(handler, options)`{lang=ts} / `cachedEventHandler(handler, options)`{lang=ts}
- `defineRenderHandler(handler)`{lang=ts}
- `useRuntimeConfig()`{lang=ts}
- `useRuntimeConfig(event?)`{lang=ts}
- `useAppConfig(event?)`{lang=ts}
- `useStorage(base?)`{lang=ts}
- `useNitroApp()`{lang=ts}
- `defineNitroPlugin(plugin)`{lang=ts}
Expand Down
6 changes: 3 additions & 3 deletions playground/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { eventHandler } from "h3";

export default eventHandler(() => "<h1>Hello Nitro!</h1>");
export default eventHandler(() => {
return "<h1>Hello Nitro!</h1>";
});
2 changes: 2 additions & 0 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ function createNitroApp(): NitroApp {
// A generic event handler give nitro acess to the requests
h3App.use(
eventHandler((event) => {
// Init nitro context
event.context.nitro = event.context.nitro || {};
// Support platform context provided by local fetch
const envContext = (event.node.req as any).__unenv__;
if (envContext) {
Expand Down
84 changes: 65 additions & 19 deletions src/runtime/config.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,103 @@
import destr from "destr";
import { snakeCase } from "scule";
import { appConfig as _appConfig } from "#internal/nitro/virtual/app-config";
import { klona } from "klona";
import { H3Event } from "h3";
import { appConfig as _inlineAppConfig } from "#internal/nitro/virtual/app-config";

// Runtime config
const _runtimeConfig = process.env.RUNTIME_CONFIG as any;
// Static runtime config inlined by nitro build
const _inlineRuntimeConfig = process.env.RUNTIME_CONFIG as any;
const ENV_PREFIX = "NITRO_";
const ENV_PREFIX_ALT =
_runtimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_";
overrideConfig(_runtimeConfig);
_inlineRuntimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_";

const runtimeConfig = deepFreeze(_runtimeConfig);
export default runtimeConfig; // TODO: Remove in next major version
export const useRuntimeConfig = () => runtimeConfig;
// Runtime config
const _sharedRuntimeConfig = _deepFreeze(
_applyEnv(klona(_inlineRuntimeConfig))
);
export function useRuntimeConfig(event?: H3Event) {
// Backwards compatibility with ambient context
if (!event) {
return _sharedRuntimeConfig;
}
// Reuse cached runtime config from event context
if (event.context.nitro.runtimeConfig) {
return event.context.nitro.runtimeConfig;
}
// Prepare runtime config for event context
const runtimeConfig = klona(_inlineRuntimeConfig);
_applyEnv(runtimeConfig);
event.context.nitro.runtimeConfig = runtimeConfig;
return runtimeConfig;
}

// App config
const appConfig = deepFreeze(_appConfig);
export const useAppConfig = () => appConfig;
const _sharedAppConfig = _deepFreeze(klona(_inlineAppConfig));
export function useAppConfig(event?: H3Event) {
// Backwards compatibility with ambient context
if (!event) {
return _sharedAppConfig;
}
// Reuse cached app config from event context
if (event.context.nitro.appConfig) {
return event.context.nitro.appConfig;
}
// Prepare app config for event context
const appConfig = klona(_inlineAppConfig);
event.context.nitro.appConfig = appConfig;
return appConfig;
}

// --- Utils ---

function getEnv(key: string) {
function _getEnv(key: string) {
const envKey = snakeCase(key).toUpperCase();
return destr(
process.env[ENV_PREFIX + envKey] ?? process.env[ENV_PREFIX_ALT + envKey]
);
}

function isObject(input: unknown) {
function _isObject(input: unknown) {
return typeof input === "object" && !Array.isArray(input);
}

function overrideConfig(obj: object, parentKey = "") {
function _applyEnv(obj: object, parentKey = "") {
for (const key in obj) {
const subKey = parentKey ? `${parentKey}_${key}` : key;
const envValue = getEnv(subKey);
if (isObject(obj[key])) {
if (isObject(envValue)) {
const envValue = _getEnv(subKey);
if (_isObject(obj[key])) {
if (_isObject(envValue)) {
obj[key] = { ...obj[key], ...envValue };
}
overrideConfig(obj[key], subKey);
_applyEnv(obj[key], subKey);
} else {
obj[key] = envValue ?? obj[key];
}
}
return obj;
}

function deepFreeze(object: Record<string, any>) {
function _deepFreeze(object: Record<string, any>) {
const propNames = Object.getOwnPropertyNames(object);
for (const name of propNames) {
const value = object[name];
if (value && typeof value === "object") {
deepFreeze(value);
_deepFreeze(value);
}
}
return Object.freeze(object);
}

// --- Deprecated default export ---
// TODO: Remove in next major version
export default new Proxy(Object.create(null), {
get: (_, prop) => {
console.warn(
"Please use `useRuntimeConfig()` instead of accessing config directly."
);
const runtimeConfig = useRuntimeConfig();
if (prop in runtimeConfig) {
return runtimeConfig[prop];
}
return undefined;
},
});
6 changes: 6 additions & 0 deletions test/fixture/middleware/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
process.env.NITRO_DYNAMIC = "from-env";

export default eventHandler((event) => {
const appConfig = useAppConfig(event);
appConfig.dynamic = "from-middleware";
});
4 changes: 4 additions & 0 deletions test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default defineNitroConfig({
],
appConfig: {
"nitro-config": true,
dynamic: "initial",
},
runtimeConfig: {
dynamic: "initial",
},
appConfigFiles: ["~/server.config.ts"],
publicAssets: [
Expand Down
7 changes: 0 additions & 7 deletions test/fixture/routes/app-config.ts

This file was deleted.

14 changes: 14 additions & 0 deletions test/fixture/routes/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const sharedAppConfig = useAppConfig();
const sharedRuntimeConfig = useRuntimeConfig();

export default eventHandler((event) => {
const appConfig = useAppConfig(event);
const runtimeConfig = useRuntimeConfig(event);

return {
sharedAppConfig,
appConfig,
runtimeConfig,
sharedRuntimeConfig,
};
});
41 changes: 31 additions & 10 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,19 +308,40 @@ export function testNitro(
expect(data.url).toBe("/api/echo?foo=bar");
});

it("app config", async () => {
it("config", async () => {
const { data } = await callHandler({
url: "/app-config",
url: "/config",
});
expect(data).toMatchInlineSnapshot(`
{
"appConfig": {
"app-config": true,
"nitro-config": true,
"server-config": true,
expect(data).toMatchObject({
appConfig: {
dynamic: "from-middleware",
"app-config": true,
"nitro-config": true,
"server-config": true,
},
runtimeConfig: {
dynamic: "from-env",
app: {
baseURL: "/",
},
},
sharedAppConfig: {
dynamic: "initial",
"app-config": true,
"nitro-config": true,
"server-config": true,
},
sharedRuntimeConfig: {
dynamic:
// TODO
ctx.preset.includes("cloudflare") || ctx.preset === "nitro-dev"
? "initial"
: "from-env",
app: {
baseURL: "/",
},
}
`);
},
});
});

if (ctx.nitro!.options.timing) {
Expand Down

0 comments on commit 1d218eb

Please sign in to comment.