本文最后更新于:2022年10月9日 凌晨
前言
既之前的 代码生成-从 module css 生成 dts 之后,这篇来实现从环境变量生成接口,便于开发时能够有正确的类型提示。
动机
在前端日常开发中,环境变量经常用于区分不同环境的配置,最常见的像是服务端地址。但在使用时,往往不能得到提示,或是一个环境变量没有。
我们通常使用以下两种方式访问环境变量
在 vite 中使用 import.meta.env
1
| import.meta.env.NODE_ENV
|
在 vite importMeta.d.ts 中的类型定义为
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface ImportMeta { readonly env: ImportMetaEnv }
interface ImportMetaEnv { [key: string]: any BASE_URL: string MODE: string DEV: boolean PROD: boolean SSR: boolean }
|
或者在普通 nodejs 项目中使用 process.env
在 nodejs process.d.ts 中的类型定义为
1 2 3 4 5 6 7 8 9 10
| interface Dict<T> { [key: string]: T | undefined }
interface ProcessEnv extends Dict<string> {
TZ?: string }
|
但无论如何,它们都不能定义一些项目中定制的环境变量,这需要我们手动完成。
解决
这里使用 vite 举例,如果需要在代码中使用 import.meta.env
时能够提示自定义的环境变量的话,需要在 vite-env.d.ts 中添加 ImportMetaEnv
。
例如添加环境变量 VITE_PORT/VITE_AUTH_TOKEN 的类型定义
1 2 3 4
| interface ImportMetaEnv { VITE_PORT?: string VITE_AUTH_TOKEN?: string }
|
现在,使用环境变量的流程变成:在 .env 中添加环境变量 => 在 vite-env.d.ts 中添加类型定义 => 在代码中使用环境变量,可以看到,我们基本上重复添加了两次环境变量,只是使用了不同的语法,这正是我们要解决的问题,从环境变量自动生成类型定义。
实现
获取环境变量的路径(vite 的环境变量类型定义文件发生过一次变化,需要做下兼容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async function getEnvPath(cwd: string) { let envPath = path.resolve(cwd, 'src/vite-env.d.ts')
if (await pathExists(envPath)) { return envPath }
envPath = path.resolve(cwd, 'src/env.d.ts')
if (await pathExists(envPath)) { return envPath }
throw new Error('未找到环境变量配置文件') }
|
扫描所有的环境变量
1 2 3 4 5 6 7 8 9 10
| export async function scan(dir: string): Promise<string[]> { const files = await FastGlob('.env*', { cwd: path.resolve(dir), })
const configs = await Promise.all( files.map((file) => readFile(path.resolve(dir, file), 'utf-8')), ) return uniqueBy(configs.map((s) => Object.keys(parse(s))).flat()) }
|
对比环境变量与类型定义
1 2 3 4 5 6 7 8 9 10 11 12
| export function eq(a: string[], b: string[]): boolean { const f = (a: string, b: string) => a.localeCompare(b) return JSON.stringify([...a].sort(f)) === JSON.stringify([...b].sort(f)) }
export function getEnvs(ast: n.ASTNode): string[] { return CodeUtil.iterator(ast, n.TSInterfaceDeclaration) .filter((item) => (item.id as n.Identifier).name === 'ImportMetaEnv') .flatMap((ast) => CodeUtil.iterator(ast, n.TSPropertySignature)) .flatMap((ast) => CodeUtil.iterator(ast, n.Identifier)) .map((item) => item.name) }
|
修改类型定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function convert(ast: n.ASTNode, envs: string[]): n.ASTNode { let envInterface = CodeUtil.iterator(ast, n.TSInterfaceDeclaration).find( (item) => (item.id as n.Identifier).name === 'ImportMetaEnv', )
if (!envInterface) { envInterface = b.tsInterfaceDeclaration( b.identifier('ImportMetaEnv'), b.tsInterfaceBody([]), ) ;(ast as n.File).program.body.push(envInterface) }
envInterface.body.body = envs.map((name) => b.tsPropertySignature.from({ key: b.identifier(name), typeAnnotation: b.tsTypeAnnotation(b.tsStringKeyword()), readonly: true, }), )
return ast }
|
最后,将它们连接起来
1 2 3 4 5 6 7 8 9 10 11 12
| export async function gen(cwd: string): Promise<void> { const envPath = await getEnvPath(cwd) const code = await readFile(envPath, 'utf-8') const ast = CodeUtil.parse(code) const envNames = await scan(cwd)
if (eq(envNames, getEnvs(ast))) { return }
await writeFile(envPath, CodeUtil.print(convert(ast, envNames))) }
|
实现 vite 插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { Plugin } from 'vite' import { gen } from './gen' import * as path from 'path'
export function envDtsGen(): Plugin { let rootPath: string return { name: 'vite-plugin-env-dts-gen', configResolved(resolveConfig) { rootPath = resolveConfig.root }, configureServer(server) { server.watcher.add('.env*') const listener = async (filePath: string) => { const relative = path.relative(rootPath, filePath) if (relative.startsWith('.env')) { await gen(rootPath) } } server.watcher.on('change', listener) server.watcher.on('add', listener) }, async buildStart() { await gen(rootPath) }, } }
|
完整代码 ref: https://github.com/rxliuli/liuli-tools/blob/master/libs/vite-plugin-env-dts-gen
使用
1 2 3 4 5 6
| import { defineConfig } from 'vite' import { envDtsGen } from '@liuli-util/vite-plugin-env-dts-gen'
export default defineConfig({ plugins: [envDtsGen()], })
|
现在,每当修改环境变量文件时,都会自动修改对应的类型定义,编写代码时也会有提示和校验了。
结语
之后,将演示两个现有的代码生成的实例。
- 从 graphql 生成代码
- 从 open api schema 生成类型定义