diff --git a/examples/config-extends/app.vue b/examples/config-extends/app.vue new file mode 100644 index 0000000000..430bf40a54 --- /dev/null +++ b/examples/config-extends/app.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/examples/config-extends/base/nuxt.config.ts b/examples/config-extends/base/nuxt.config.ts new file mode 100644 index 0000000000..aa815ae614 --- /dev/null +++ b/examples/config-extends/base/nuxt.config.ts @@ -0,0 +1,10 @@ +import { defineNuxtConfig } from 'nuxt3' + +export default defineNuxtConfig({ + publicRuntimeConfig: { + theme: { + primaryColor: 'base_primary', + secondaryColor: 'base_secondary' + } + } +}) diff --git a/examples/config-extends/nuxt.config.ts b/examples/config-extends/nuxt.config.ts new file mode 100644 index 0000000000..a08add69b9 --- /dev/null +++ b/examples/config-extends/nuxt.config.ts @@ -0,0 +1,13 @@ +import { defineNuxtConfig } from 'nuxt3' + +export default defineNuxtConfig({ + extends: './base', + publicRuntimeConfig: { + theme: { + primaryColor: 'user_primary' + } + }, + modules: [ + '@nuxt/ui' + ] +}) diff --git a/examples/config-extends/package.json b/examples/config-extends/package.json new file mode 100644 index 0000000000..da5aa26e79 --- /dev/null +++ b/examples/config-extends/package.json @@ -0,0 +1,13 @@ +{ + "name": "example-config-extends", + "private": true, + "devDependencies": { + "@nuxt/ui": "npm:@nuxt/ui-edge@latest", + "nuxt3": "latest" + }, + "scripts": { + "dev": "nuxi dev", + "build": "nuxi build", + "start": "nuxi preview" + } +} diff --git a/examples/config-extends/tsconfig.json b/examples/config-extends/tsconfig.json new file mode 100644 index 0000000000..4b34df1571 --- /dev/null +++ b/examples/config-extends/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/packages/kit/package.json b/packages/kit/package.json index 3fb7bca788..7373382d7f 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -14,9 +14,9 @@ }, "dependencies": { "@nuxt/schema": "^3.0.0", + "c12": "^0.1.1", "consola": "^2.15.3", "defu": "^5.0.1", - "dotenv": "^15.0.0", "globby": "^13.1.0", "hash-sum": "^2.0.0", "jiti": "^1.12.15", @@ -24,7 +24,6 @@ "mlly": "^0.4.1", "pathe": "^0.2.0", "pkg-types": "^0.3.2", - "rc9": "^1.2.0", "scule": "^0.2.1", "semver": "^7.3.5", "unctx": "^1.0.2", diff --git a/packages/kit/src/internal/dotenv.ts b/packages/kit/src/internal/dotenv.ts deleted file mode 100644 index eebfdff7dd..0000000000 --- a/packages/kit/src/internal/dotenv.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { promises as fsp, existsSync } from 'fs' -import { resolve } from 'pathe' -import dotenv from 'dotenv' - -export interface DotenvOptions { - /** The project root directory (either absolute or relative to the current working directory). */ - rootDir: string - - /** - * What file to look in for environment variables (either absolute or relative - * to the current working directory). For example, `.env`. - */ - fileName: string - - /** - * Whether to interpolate variables within .env. - * - * @example - * ```env - * BASE_DIR="/test" - * # resolves to "/test/further" - * ANOTHER_DIR="${BASE_DIR}/further" - * ``` - */ - expand: boolean - - /** An object describing environment variables (key, value pairs). */ - env: NodeJS.ProcessEnv -} - -export type Env = typeof process.env - -/** - * Load and interpolate environment variables into `process.env`. - * If you need more control (or access to the values), consider using `loadDotenv` instead - * - */ -export async function setupDotenv (options: DotenvOptions): Promise { - const targetEnv = options.env ?? process.env - - // Load env - const env = await loadDotenv({ - rootDir: options.rootDir, - fileName: options.fileName ?? '.env', - env: targetEnv, - expand: options.expand ?? true - }) - - // Fill process.env - for (const key in env) { - if (!key.startsWith('_') && targetEnv[key] === undefined) { - targetEnv[key] = env[key] - } - } - - return env -} - -/** Load environment variables into an object. */ -export async function loadDotenv (opts: DotenvOptions): Promise { - const env = Object.create(null) - - const dotenvFile = resolve(opts.rootDir, opts.fileName) - - if (existsSync(dotenvFile)) { - const parsed = dotenv.parse(await fsp.readFile(dotenvFile, 'utf-8')) - Object.assign(env, parsed) - } - - // Apply process.env - if (!opts.env._applied) { - Object.assign(env, opts.env) - env._applied = true - } - - // Interpolate env - if (opts.expand) { - expand(env) - } - - return env -} - -// Based on https://github.com/motdotla/dotenv-expand -function expand (target: Record, source: Record = {}, parse = (v: any) => v) { - function getValue (key: string) { - // Source value 'wins' over target value - return source[key] !== undefined ? source[key] : target[key] - } - - function interpolate (value: unknown, parents: string[] = []) { - if (typeof value !== 'string') { - return value - } - const matches = value.match(/(.?\${?(?:[a-zA-Z0-9_:]+)?}?)/g) || [] - return parse(matches.reduce((newValue, match) => { - const parts = /(.?)\${?([a-zA-Z0-9_:]+)?}?/g.exec(match) - const prefix = parts[1] - - let value, replacePart: string - - if (prefix === '\\') { - replacePart = parts[0] - value = replacePart.replace('\\$', '$') - } else { - const key = parts[2] - replacePart = parts[0].substring(prefix.length) - - // Avoid recursion - if (parents.includes(key)) { - console.warn(`Please avoid recursive environment variables ( loop: ${parents.join(' > ')} > ${key} )`) - return '' - } - - value = getValue(key) - - // Resolve recursive interpolations - value = interpolate(value, [...parents, key]) - } - - return value !== undefined ? newValue.replace(replacePart, value) : newValue - }, value)) - } - - for (const key in target) { - target[key] = interpolate(getValue(key)) - } -} diff --git a/packages/kit/src/loader/config.ts b/packages/kit/src/loader/config.ts index 54636b30b6..548eccc0fc 100644 --- a/packages/kit/src/loader/config.ts +++ b/packages/kit/src/loader/config.ts @@ -1,12 +1,10 @@ -import { existsSync } from 'fs' import { resolve } from 'pathe' -import defu from 'defu' import { applyDefaults } from 'untyped' -import * as rc from 'rc9' +import { loadConfig, DotenvOptions } from 'c12' import type { NuxtOptions } from '@nuxt/schema' import { NuxtConfigSchema } from '@nuxt/schema' -import { tryResolveModule, requireModule, scanRequireTree } from '../internal/cjs' -import { setupDotenv, DotenvOptions } from '../internal/dotenv' +// TODO +// import { tryResolveModule, requireModule, scanRequireTree } from '../internal/cjs' export interface LoadNuxtConfigOptions { /** Your project root directory (either absolute or relative to the current working directory). */ @@ -25,39 +23,22 @@ export interface LoadNuxtConfigOptions { export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise { const rootDir = resolve(process.cwd(), opts.rootDir || '.') - if (opts.dotenv !== false) { - await setupDotenv({ rootDir, ...opts.dotenv }) - } + const { config: nuxtConfig, configFile, layers } = await loadConfig({ + cwd: rootDir, + name: 'nuxt', + configFile: 'nuxt.config', + rcFile: '.nuxtrc', + dotenv: opts.dotenv, + globalRc: true, + overrides: opts.config + }) - const nuxtConfigFile = tryResolveModule(resolve(rootDir, opts.configFile || 'nuxt.config')) + nuxtConfig.rootDir = nuxtConfig.rootDir || rootDir - let nuxtConfig: any = {} + nuxtConfig._nuxtConfigFile = configFile + nuxtConfig._nuxtConfigFiles = [configFile] - if (nuxtConfigFile && existsSync(nuxtConfigFile)) { - nuxtConfig = requireModule(nuxtConfigFile, { clearCache: true }) - - if (typeof nuxtConfig === 'function') { - nuxtConfig = await nuxtConfig(opts) - } - - nuxtConfig = { ...nuxtConfig } - nuxtConfig._nuxtConfigFile = nuxtConfigFile - nuxtConfig._nuxtConfigFiles = Array.from(scanRequireTree(nuxtConfigFile)) - } - - // Combine configs - // Priority: configOverrides > nuxtConfig > .nuxtrc > .nuxtrc (global) - nuxtConfig = defu( - opts.config, - nuxtConfig, - rc.read({ name: '.nuxtrc', dir: rootDir }), - rc.readUser('.nuxtrc') - ) - - // Set rootDir - if (!nuxtConfig.rootDir) { - nuxtConfig.rootDir = rootDir - } + nuxtConfig.layers = layers // Resolve and apply defaults return applyDefaults(NuxtConfigSchema, nuxtConfig) as NuxtOptions diff --git a/packages/schema/package.json b/packages/schema/package.json index 06b4a573bb..d97fd0b5d1 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -19,6 +19,7 @@ "unbuild": "latest" }, "dependencies": { + "c12": "^0.1.1", "create-require": "^1.1.1", "defu": "^5.0.1", "jiti": "^1.12.15", diff --git a/packages/schema/src/config/_common.ts b/packages/schema/src/config/_common.ts index cd8325d0c8..50d822f43c 100644 --- a/packages/schema/src/config/_common.ts +++ b/packages/schema/src/config/_common.ts @@ -6,6 +6,19 @@ import jiti from 'jiti' import defu from 'defu' export default { + /** + * Extend nested configurations from multiple local or remoted sources + * + * Value should be either a string or array of strings pointing to source directories or config path relative to current config. + * + * You can use `github:`, `gitlab:`, `bitbucket:` or `https://` to extend from a remote git repository. + * + * @typedef {string|string[]} + * + * @version 3 + */ + extends: null, + /** * Define the workspace directory of your application. * diff --git a/packages/schema/src/types/config.ts b/packages/schema/src/types/config.ts index 36b477f20f..d7c794b61d 100644 --- a/packages/schema/src/types/config.ts +++ b/packages/schema/src/types/config.ts @@ -1,7 +1,10 @@ import { ConfigSchema } from '../../schema/config' +import type { ResolvedConfig } from 'c12' /** Normalized Nuxt options available as `nuxt.options.*` */ -export interface NuxtOptions extends ConfigSchema { } +export interface NuxtOptions extends ConfigSchema { + layers: ResolvedConfig[] +} type DeepPartial = T extends Record ? { [P in keyof T]?: DeepPartial | T[P] } : T diff --git a/yarn.lock b/yarn.lock index d0cb77ef6f..894e43a7da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2859,9 +2859,9 @@ __metadata: "@nuxt/schema": ^3.0.0 "@types/lodash.template": ^4 "@types/semver": ^7 + c12: ^0.1.1 consola: ^2.15.3 defu: ^5.0.1 - dotenv: ^15.0.0 globby: ^13.1.0 hash-sum: ^2.0.0 jiti: ^1.12.15 @@ -2869,7 +2869,6 @@ __metadata: mlly: ^0.4.1 pathe: ^0.2.0 pkg-types: ^0.3.2 - rc9: ^1.2.0 scule: ^0.2.1 semver: ^7.3.5 unbuild: latest @@ -3021,6 +3020,7 @@ __metadata: dependencies: "@types/lodash.template": ^4 "@types/semver": ^7 + c12: ^0.1.1 create-require: ^1.1.1 defu: ^5.0.1 jiti: ^1.12.15 @@ -6744,6 +6744,21 @@ __metadata: languageName: node linkType: hard +"c12@npm:^0.1.1": + version: 0.1.1 + resolution: "c12@npm:0.1.1" + dependencies: + defu: ^5.0.1 + dotenv: ^14.3.2 + gittar: ^0.1.1 + jiti: ^1.12.14 + mlly: ^0.4.1 + pathe: ^0.2.0 + rc9: ^1.2.0 + checksum: 628bd42845926249f9dfbaf14371793de72c4b9fa764e8a635ae56f5e67c99d765f8ea09ac44e3876e334095a627ca24d7029ca5d6254829998b9fab67927460 + languageName: node + linkType: hard + "cacache@npm:^12.0.2": version: 12.0.4 resolution: "cacache@npm:12.0.4" @@ -8908,20 +8923,13 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^14.3.0": +"dotenv@npm:^14.3.0, dotenv@npm:^14.3.2": version: 14.3.2 resolution: "dotenv@npm:14.3.2" checksum: 86c06758915d6facc35275f4a7fafc16705b6f3b44befaa8abca91367991efc8ff8db5437d3cc14778231d19fb97610fe82d60f8a53ba723cdb69fe4171439aa languageName: node linkType: hard -"dotenv@npm:^15.0.0": - version: 15.0.0 - resolution: "dotenv@npm:15.0.0" - checksum: be5d852b2ad1708780e7038481c0687ce4bb9edf354d439872511e966a5f28c083f296e1d1c182256d695bfdd67e320382eb3b0f2e9d83efa3615cd1757257ab - languageName: node - linkType: hard - "dotenv@npm:^8.2.0": version: 8.6.0 resolution: "dotenv@npm:8.6.0" @@ -10081,6 +10089,15 @@ __metadata: languageName: node linkType: hard +"example-config-extends@workspace:examples/config-extends": + version: 0.0.0-use.local + resolution: "example-config-extends@workspace:examples/config-extends" + dependencies: + "@nuxt/ui": "npm:@nuxt/ui-edge@latest" + nuxt3: latest + languageName: unknown + linkType: soft + "example-hello-world@workspace:examples/hello-world": version: 0.0.0-use.local resolution: "example-hello-world@workspace:examples/hello-world" @@ -11116,6 +11133,16 @@ __metadata: languageName: node linkType: hard +"gittar@npm:^0.1.1": + version: 0.1.1 + resolution: "gittar@npm:0.1.1" + dependencies: + mkdirp: ^0.5.1 + tar: ^4.4.1 + checksum: fcc6127a9ce36116f1d2e429b896ab133c27ddd84817a48ccb88b3039caca33e1f8566fe00d14c6e7c16a0aa65cf543c94588427e5b72c52c1cd4d8d87414744 + languageName: node + linkType: hard + "glob-parent@npm:^3.1.0": version: 3.1.0 resolution: "glob-parent@npm:3.1.0" @@ -19608,7 +19635,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^4, tar@npm:^4.4.12": +"tar@npm:^4, tar@npm:^4.4.1, tar@npm:^4.4.12": version: 4.4.19 resolution: "tar@npm:4.4.19" dependencies: