e117c000e4
The current logging styles has been existed for a while, this PR gives a fresh impression for the logging output from Next.js. We want to achieve few new goals that makes the output clean, modernized, sweet 🍫 . Few goals are addressed with this redesign: ## Refresh Impression & Simplification The new design of logging is much more information centralized and streamlined. * Given a `ready` message at the begining when compilers are bootstrapped. * Only show `compiled` event with green check mark indicating succesful compilation, this will merge the unclear `compiling` event which shows `(client and server)` before, now tell you the route compilation info in one line. hello world app ### `next dev` #### After vs Before <img src="https://github.com/vercel/next.js/assets/4800338/9649b340-8241-4756-a2b3-a989f0b74003" height="120"> <img src="https://github.com/vercel/next.js/assets/4800338/ee181263-3dd4-40d0-9ffc-819a56b45900" height="120"> ### `next build` #### After vs Before <img src="https://github.com/vercel/next.js/assets/4800338/5db9829a-9ffc-49f0-b030-93ee92f5c248" width="360"> <img src="https://github.com/vercel/next.js/assets/4800338/b9527b83-27c8-4426-9c0d-c0d4072b7d58" width="360"> ### error status #### After vs Before <img src="https://github.com/vercel/next.js/assets/4800338/00455226-ace7-468b-8d90-0d36bf038489" height="120"> <img src="https://github.com/vercel/next.js/assets/4800338/1be8c451-d3f0-465c-9ef7-6b0dde7cff85" height="120"> ## Streamlization If you have customized envs and experiments Next.js will give the brief in the early summary about your network information, env vars, and enabled experimental features <img src="https://github.com/vercel/next.js/assets/4800338/ca1a7409-1532-46cb-850f-687e61e587b2" width="400"> ## Polish ### fetching logging structure #### After vs Before <img src="https://github.com/vercel/next.js/assets/4800338/97526397-dffe-4736-88ed-e5cbe5e945bd" width="400"> <img src="https://github.com/vercel/next.js/assets/4800338/ab77c907-5ab5-48bb-8347-6146d2e60932" width="400"> ### Dedupe Duplicates The logging is moved from `@next/env` to `next` itself, `@next/env` will only notify the invoker that the env is reloaded. Then the duplicated logs for the env reloading cases can be avoid. #### After vs Before <img src="https://github.com/vercel/next.js/assets/4800338/04799295-e739-4035-87aa-61cec962fc39" width="400"> <img src="https://github.com/vercel/next.js/assets/4800338/e29020c9-0031-4bf3-a21b-8b64633f43a2" width="400"> ### Different indicators Use unicode text icons for different situation: * passed -> check mark * warning -> warning * error -> red cross * loading -> circle <img src="https://github.com/vercel/next.js/assets/4800338/715c34bd-298f-4990-a5d7-e12e455ead44" width="400"> Co-authored-by: Tim Neutkens <6324199+timneutkens@users.noreply.github.com>
173 lines
4.6 KiB
TypeScript
173 lines
4.6 KiB
TypeScript
/* eslint-disable import/no-extraneous-dependencies */
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import * as dotenv from 'dotenv'
|
|
import { expand as dotenvExpand } from 'dotenv-expand'
|
|
|
|
export type Env = { [key: string]: string | undefined }
|
|
export type LoadedEnvFiles = Array<{
|
|
path: string
|
|
contents: string
|
|
}>
|
|
|
|
export let initialEnv: Env | undefined = undefined
|
|
let combinedEnv: Env | undefined = undefined
|
|
let cachedLoadedEnvFiles: LoadedEnvFiles = []
|
|
let previousLoadedEnvFiles: LoadedEnvFiles = []
|
|
|
|
export function updateInitialEnv(newEnv: Env) {
|
|
Object.assign(initialEnv || {}, newEnv)
|
|
}
|
|
|
|
type Log = {
|
|
info: (...args: any[]) => void
|
|
error: (...args: any[]) => void
|
|
}
|
|
|
|
function replaceProcessEnv(sourceEnv: Env) {
|
|
Object.keys(process.env).forEach((key) => {
|
|
// Allow mutating internal Next.js env variables after the server has initiated.
|
|
// This is necessary for dynamic things like the IPC server port.
|
|
if (!key.startsWith('__NEXT_PRIVATE')) {
|
|
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
|
|
delete process.env[key]
|
|
}
|
|
}
|
|
})
|
|
|
|
Object.entries(sourceEnv).forEach(([key, value]) => {
|
|
process.env[key] = value
|
|
})
|
|
}
|
|
|
|
export function processEnv(
|
|
loadedEnvFiles: LoadedEnvFiles,
|
|
dir?: string,
|
|
log: Log = console,
|
|
forceReload = false,
|
|
onReload?: (envFilePath: string) => void
|
|
) {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (
|
|
!forceReload &&
|
|
(process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0)
|
|
) {
|
|
return process.env as Env
|
|
}
|
|
// flag that we processed the environment values already.
|
|
process.env.__NEXT_PROCESSED_ENV = 'true'
|
|
|
|
const origEnv = Object.assign({}, initialEnv)
|
|
const parsed: dotenv.DotenvParseOutput = {}
|
|
|
|
for (const envFile of loadedEnvFiles) {
|
|
try {
|
|
let result: dotenv.DotenvConfigOutput = {}
|
|
result.parsed = dotenv.parse(envFile.contents)
|
|
|
|
result = dotenvExpand(result)
|
|
|
|
if (
|
|
result.parsed &&
|
|
!previousLoadedEnvFiles.some(
|
|
(item) =>
|
|
item.contents === envFile.contents && item.path === envFile.path
|
|
)
|
|
) {
|
|
onReload?.(envFile.path)
|
|
}
|
|
|
|
for (const key of Object.keys(result.parsed || {})) {
|
|
if (
|
|
typeof parsed[key] === 'undefined' &&
|
|
typeof origEnv[key] === 'undefined'
|
|
) {
|
|
// We're being imprecise in the type system - assume parsed[key] can be undefined
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
parsed[key] = result.parsed?.[key]!
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log.error(
|
|
`Failed to load env from ${path.join(dir || '', envFile.path)}`,
|
|
err
|
|
)
|
|
}
|
|
}
|
|
return Object.assign(process.env, parsed)
|
|
}
|
|
|
|
export function resetEnv() {
|
|
if (initialEnv) {
|
|
replaceProcessEnv(initialEnv)
|
|
}
|
|
}
|
|
|
|
export function loadEnvConfig(
|
|
dir: string,
|
|
dev?: boolean,
|
|
log: Log = console,
|
|
forceReload = false,
|
|
onReload?: (envFilePath: string) => void
|
|
): {
|
|
combinedEnv: Env
|
|
loadedEnvFiles: LoadedEnvFiles
|
|
} {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (combinedEnv && !forceReload) {
|
|
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|
|
replaceProcessEnv(initialEnv)
|
|
previousLoadedEnvFiles = cachedLoadedEnvFiles
|
|
cachedLoadedEnvFiles = []
|
|
|
|
const isTest = process.env.NODE_ENV === 'test'
|
|
const mode = isTest ? 'test' : dev ? 'development' : 'production'
|
|
const dotenvFiles = [
|
|
`.env.${mode}.local`,
|
|
// Don't include `.env.local` for `test` environment
|
|
// since normally you expect tests to produce the same
|
|
// results for everyone
|
|
mode !== 'test' && `.env.local`,
|
|
`.env.${mode}`,
|
|
'.env',
|
|
].filter(Boolean) as string[]
|
|
|
|
for (const envFile of dotenvFiles) {
|
|
// only load .env if the user provided has an env config file
|
|
const dotEnvPath = path.join(dir, envFile)
|
|
|
|
try {
|
|
const stats = fs.statSync(dotEnvPath)
|
|
|
|
// make sure to only attempt to read files
|
|
if (!stats.isFile()) {
|
|
continue
|
|
}
|
|
|
|
const contents = fs.readFileSync(dotEnvPath, 'utf8')
|
|
cachedLoadedEnvFiles.push({
|
|
path: envFile,
|
|
contents,
|
|
})
|
|
} catch (err: any) {
|
|
if (err.code !== 'ENOENT') {
|
|
log.error(`Failed to load env from ${envFile}`, err)
|
|
}
|
|
}
|
|
}
|
|
combinedEnv = processEnv(
|
|
cachedLoadedEnvFiles,
|
|
dir,
|
|
log,
|
|
forceReload,
|
|
onReload
|
|
)
|
|
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|