代码生成-从环境变量生成类型定义

本文最后更新于: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

1
process.env.NODE_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> {
/**
* Can be used to change the default timezone at runtime
*/
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 中添加类型定义 => 在代码中使用环境变量,可以看到,我们基本上重复添加了两次环境变量,只是使用了不同的语法,这正是我们要解决的问题,从环境变量自动生成类型定义。

从环境变量生成类型定义.drawio.svg

实现

获取环境变量的路径(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)
// console.log('filePath: ', relative)
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 生成类型定义

代码生成-从环境变量生成类型定义
https://blog.rxliuli.com/p/d867b35e62454483ae697185d93617ab/
作者
rxliuli
发布于
2022年9月29日
许可协议