Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt): app.config with hmr and reactivity support #6333

Merged
merged 38 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5b8a04e
feat(nuxt): `app.config` with hmr support
pi0 Aug 3, 2022
eda4b99
Merge branch 'main' into feat/app-config
pi0 Aug 4, 2022
79ff3b1
webpack hmr support
pi0 Aug 4, 2022
c949684
update AppConfig schema
pi0 Aug 4, 2022
8316b5a
handle key removals
pi0 Aug 4, 2022
171d869
support inline config using `appConfig` in nuxt.config
pi0 Aug 4, 2022
22518d8
fix template when no appConfigs added
pi0 Aug 4, 2022
75e3be9
handle app.config add/removal
pi0 Aug 4, 2022
231e0ca
Merge branch 'main' into feat/app-config
pi0 Aug 4, 2022
efd22f0
auto generate types
pi0 Aug 4, 2022
3e0534c
add tests
pi0 Aug 4, 2022
8c2a012
Merge branch 'main' into feat/app-config
pi0 Aug 4, 2022
9cb0c74
fix test side effect
pi0 Aug 4, 2022
7d6c04a
Merge branch 'main' into feat/app-config
pi0 Aug 4, 2022
d3b9163
simplify reserved namespaces
pi0 Aug 4, 2022
9178cd8
fix: reserved are optional
pi0 Aug 4, 2022
bc269f3
Merge branch 'main' into feat/app-config
pi0 Aug 12, 2022
808bf9d
feat(nuxt): include type of resolved configs in AppConfig
danielroe Aug 16, 2022
d5011cf
refactor: write a single type declaration file
danielroe Aug 16, 2022
5d9c593
chore: upgrade defu
danielroe Aug 16, 2022
43b58f3
test: add type test
danielroe Aug 16, 2022
d152678
fix: update to use `Defu` type helper
danielroe Aug 16, 2022
59ed668
fix: use `ResolvedAppConfig` to for type inference and extract `defin…
danielroe Aug 17, 2022
c63fd89
Merge branch 'main' into feat/app-config
pi0 Aug 17, 2022
69c5b50
try removing subpath from package.json
pi0 Aug 17, 2022
15ed77c
refactor: move `defineAppConfig` to `nuxt.ts`
pi0 Aug 17, 2022
f36e0d0
Update packages/nuxt/src/app/config.ts
pi0 Aug 17, 2022
df2380d
chore: fix ts issue
pi0 Aug 17, 2022
b2c7f57
remove unused import
pi0 Aug 17, 2022
a08d987
add usage to examples
pi0 Aug 17, 2022
6a571f5
add docs
pi0 Aug 17, 2022
e54db5d
fix vite hmr
pi0 Aug 17, 2022
2cace40
update docs
pi0 Aug 17, 2022
840f245
update api guide
pi0 Aug 17, 2022
ed0b584
revert useRuntimeConfig back to nuxt.ts
pi0 Aug 17, 2022
12ec374
Merge branch 'main' into feat/app-config
pi0 Aug 17, 2022
5e7d9c6
i touched it!
pi0 Aug 17, 2022
15f5e3f
strict is not funny
pi0 Aug 17, 2022
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
55 changes: 55 additions & 0 deletions docs/content/2.guide/2.features/10.app-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# App Config

::StabilityEdge
::

Nuxt 3 provides an `app.config` config file to expose reactive configuration within your application with the ability to update it at runtime within lifecycle or using a nuxt plugin and editing it with HMR (hot-module-replacement).

::alert{type=warning}
Do not put any secret values inside `app.config` file. It is exposed to the user client bundle.
::

## Defining App Config

To expose config and environment variables to the rest of your app, you will need to define configuration in `app.config` file.

**Example:**

```ts [app.config.ts]
export default defineAppConfig({
theme: {
primaryColor: '#ababab'
}
})
```

When adding `theme` to the `app.config`, Nuxt uses Vite or Webpack to bundle the code. We can universally access `theme` in both server and browser using [useAppConfig](/api/composables/use-app-config) composable.

```js
const appConfig = useAppConfig()

console.log(appConfig.theme)
```

<!-- TODO: Document module author for extension -->

### Manually Typing App Config

Nuxt tries to automatically generate a typescript interface from provided app config.

It is also possible to type app config manually:

```ts [index.d.ts]
declare module '@nuxt/schema' {
interface AppConfig {
/** Theme configuration */
theme: {
/** Primary app color */
primaryColor: string
}
}
}

// It is always important to ensure you import/export something when augmenting a type
export {}
```
24 changes: 24 additions & 0 deletions docs/content/2.guide/3.directory-structure/16.app.config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
icon: IconFile
title: app.config.ts
head.title: Nuxt App Config
---

# Nuxt App Config

::StabilityEdge
::

You can easily provide runtime app configuration using `app.config.ts` file. It can have either of `.ts`, `.js`, or `.mjs` extensions.

```ts [app.config.ts]
export default defineAppConfig({
foo: 'bar'
})
```

::alert{type=warning}
Do not put any secret values inside `app.config` file. It is exposed to the user client bundle.
::

::ReadMore{link="/guide/features/app-config"}
16 changes: 16 additions & 0 deletions docs/content/3.api/1.composables/use-app-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# `useAppConfig`

::StabilityEdge
::

Access [app config](/guide/features/app-config):

**Usage:**

```js
const appConfig = useAppConfig()

console.log(appConfig)
```

::ReadMore{link="/guide/features/app-config"}
5 changes: 5 additions & 0 deletions examples/advanced/config-extends/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineAppConfig({
foo: 'user',
bar: 'user',
baz: 'base'
})
4 changes: 4 additions & 0 deletions examples/advanced/config-extends/base/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default defineAppConfig({
bar: 'base',
baz: 'base'
})
5 changes: 4 additions & 1 deletion examples/advanced/config-extends/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<script setup>
const themeConfig = useRuntimeConfig().theme
const appConfig = useAppConfig()
const foo = useFoo()
const bar = getBar()
</script>

<template>
<NuxtExampleLayout example="advanced/config-extends">
theme runtimeConfig
appConfig:
<pre>{{ JSON.stringify(appConfig, null, 2) }}</pre>
runtimeConfig:
<pre>{{ JSON.stringify(themeConfig, null, 2) }}</pre>
<BaseButton>Base Button</BaseButton>
<FancyButton>Fancy Button</FancyButton>
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default defineNuxtCommand({
dLoad(true, `Directory \`${relativePath}/\` ${event === 'addDir' ? 'created' : 'removed'}`)
}
} else if (isFileChange) {
if (file.match(/(app|error)\.(js|ts|mjs|jsx|tsx|vue)$/)) {
if (file.match(/(app|error|app\.config)\.(js|ts|mjs|jsx|tsx|vue)$/)) {
dLoad(true, `\`${relativePath}\` ${event === 'add' ? 'created' : 'removed'}`)
}
}
Expand Down
48 changes: 48 additions & 0 deletions packages/nuxt/src/app/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { AppConfig } from '@nuxt/schema'
import { reactive } from 'vue'
import { useNuxtApp } from './nuxt'
// @ts-ignore
import __appConfig from '#build/app.config.mjs'

// Workaround for vite HMR with virtual modules
export const _getAppConfig = () => __appConfig as AppConfig

export function useAppConfig (): AppConfig {
const nuxtApp = useNuxtApp()
if (!nuxtApp._appConfig) {
nuxtApp._appConfig = reactive(__appConfig) as AppConfig
}
return nuxtApp._appConfig
}

// HMR Support
if (process.dev) {
function applyHMR (newConfig: AppConfig) {
const appConfig = useAppConfig()
if (newConfig && appConfig) {
for (const key in newConfig) {
(appConfig as any)[key] = (newConfig as any)[key]
}
for (const key in appConfig) {
if (!(key in newConfig)) {
delete (appConfig as any)[key]
}
}
}
}

// Vite
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
const newConfig = newModule._getAppConfig()
applyHMR(newConfig)
})
}

// Webpack
if (import.meta.webpackHot) {
import.meta.webpackHot.accept('#build/app.config.mjs', () => {
applyHMR(__appConfig)
})
}
}
1 change: 1 addition & 0 deletions packages/nuxt/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export * from './nuxt'
export * from './composables'
export * from './components'
export * from './config'

// eslint-disable-next-line import/no-restricted-paths
export type { PageMeta } from '../pages/runtime'
Expand Down
6 changes: 5 additions & 1 deletion packages/nuxt/src/app/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { getCurrentInstance, reactive } from 'vue'
import type { App, onErrorCaptured, VNode } from 'vue'
import { createHooks, Hookable } from 'hookable'
import type { RuntimeConfig } from '@nuxt/schema'
import type { RuntimeConfig, AppConfigInput } from '@nuxt/schema'
import { getContext } from 'unctx'
import type { SSRContext } from 'vue-bundle-renderer/runtime'
import type { CompatibilityEvent } from 'h3'
Expand Down Expand Up @@ -281,3 +281,7 @@ export function useRuntimeConfig (): RuntimeConfig {
function defineGetter<K extends string | number | symbol, V> (obj: Record<K, V>, key: K, val: V) {
Object.defineProperty(obj, key, { get: () => val })
}

export function defineAppConfig<C extends AppConfigInput> (config: C): C {
return config
}
10 changes: 6 additions & 4 deletions packages/nuxt/src/auto-imports/presets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineUnimportPreset, Preset } from 'unimport'

export const commonPresets: Preset[] = [
const commonPresets: Preset[] = [
// #head
defineUnimportPreset({
from: '#head',
Expand All @@ -19,7 +19,7 @@ export const commonPresets: Preset[] = [
})
]

export const appPreset = defineUnimportPreset({
const appPreset = defineUnimportPreset({
from: '#app',
imports: [
'useAsyncData',
Expand Down Expand Up @@ -49,12 +49,14 @@ export const appPreset = defineUnimportPreset({
'isNuxtError',
'useError',
'createError',
'defineNuxtLink'
'defineNuxtLink',
'useAppConfig',
'defineAppConfig'
]
})

// vue
export const vuePreset = defineUnimportPreset({
const vuePreset = defineUnimportPreset({
from: 'vue',
imports: [
// <script setup>
Expand Down
9 changes: 9 additions & 0 deletions packages/nuxt/src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
app.middleware = uniqueBy(await resolvePaths(app.middleware, 'path'), 'name')
app.plugins = uniqueBy(await resolvePaths(app.plugins, 'src'), 'src')

// Resolve app.config
app.configs = []
for (const config of nuxt.options._layers.map(layer => layer.config)) {
const appConfigPath = await findPath(resolve(config.srcDir, 'app.config'))
if (appConfigPath) {
app.configs.push(appConfigPath)
}
}

// Extend app
await nuxt.callHook('app:resolve', app)

Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/core/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ async function initNuxt (nuxt: Nuxt) {
}
// Add module augmentations directly to NuxtConfig
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/schema.d.ts') })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app.config.d.ts') })
})

// Add import protection
Expand Down
38 changes: 35 additions & 3 deletions packages/nuxt/src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const schemaTemplate: NuxtTemplate<TemplateContext> = {
"declare module '@nuxt/schema' {",
' interface NuxtConfig {',
...moduleInfo.filter(Boolean).map(meta =>
` [${genString(meta.configKey)}]?: typeof ${genDynamicImport(meta.importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`
` [${genString(meta.configKey)}]?: typeof ${genDynamicImport(meta.importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`
),
' }',
generateTypes(resolveSchema(Object.fromEntries(Object.entries(nuxt.options.runtimeConfig).filter(([key]) => key !== 'public'))),
Expand Down Expand Up @@ -157,7 +157,7 @@ export const layoutTemplate: NuxtTemplate<TemplateContext> = {
}))
return [
'import { defineAsyncComponent } from \'vue\'',
`export default ${layoutsObject}`
`export default ${layoutsObject}`
].join('\n')
}
}
Expand All @@ -184,7 +184,39 @@ export const useRuntimeConfig = () => window?.__NUXT__?.config || {}
`
}

export const publicPathTemplate: NuxtTemplate<TemplateContext> = {
export const appConfigDeclarationTemplate: NuxtTemplate = {
filename: 'types/app.config.d.ts',
getContents: ({ app, nuxt }) => {
return `
import type { Defu } from 'defu'
${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id.replace(/(?<=\w)\.\w+$/g, ''))}`).join('\n')}

declare const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}
type ResolvedAppConfig = Defu<typeof inlineConfig, [${app.configs.map((_id: string, index: number) => `typeof cfg${index}`).join(', ')}]>

declare module '@nuxt/schema' {
interface AppConfig extends ResolvedAppConfig { }
}
`
}
}

export const appConfigTemplate: NuxtTemplate = {
filename: 'app.config.mjs',
write: true,
getContents: ({ app, nuxt }) => {
return `
import defu from 'defu'

const inlineConfig = ${JSON.stringify(nuxt.options.appConfig, null, 2)}

${app.configs.map((id: string, index: number) => `import ${`cfg${index}`} from ${JSON.stringify(id)}`).join('\n')}
export default defu(${app.configs.map((_id: string, index: number) => `cfg${index}`).concat(['inlineConfig']).join(', ')})
`
}
}

export const publicPathTemplate: NuxtTemplate = {
filename: 'paths.mjs',
getContents ({ nuxt }) {
return [
Expand Down
13 changes: 12 additions & 1 deletion packages/schema/src/config/_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,5 +747,16 @@ export default {
* @version 3
* @deprecated Use `runtimeConfig` option with `public` key (`runtimeConfig.public.*`).
*/
publicRuntimeConfig: {}
publicRuntimeConfig: {},

/**
* Additional app configuration
*
* For programmatic usage and type support, you can directly provide app config with this option.
* It will be merged with `app.config` file as default value.
*
* @type {typeof import('../src/types/config').AppConfig}
* @version 3
*/
appConfig: {},
}
Loading