vite-plugin-typed-env 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,338 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/parser.ts
4
+ const ANNOTATION_RE = /^#\s*@(\w+)(?::\s*(.+))?$/;
5
+ function parseAnnotations(lines) {
6
+ const ann = {};
7
+ for (const line of lines) {
8
+ const m = line.match(ANNOTATION_RE);
9
+ if (!m) continue;
10
+ const [, key, value] = m;
11
+ if (key === "optional") ann.optional = true;
12
+ else if (key === "type" && value) ann.type = value.trim();
13
+ else if (key === "default" && value) ann.default = value.trim();
14
+ else if (key === "desc" && value) ann.description = value.trim();
15
+ }
16
+ return ann;
17
+ }
18
+ function parseEnvFile(content) {
19
+ const lines = content.split("\n");
20
+ const entries = [];
21
+ const pendingComments = [];
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const raw = lines[i].trim();
24
+ if (raw === "") {
25
+ pendingComments.length = 0;
26
+ continue;
27
+ }
28
+ if (raw.startsWith("#")) {
29
+ pendingComments.push(raw);
30
+ continue;
31
+ }
32
+ const eqIdx = raw.indexOf("=");
33
+ if (eqIdx === -1) continue;
34
+ const key = raw.slice(0, eqIdx).trim();
35
+ let value = raw.slice(eqIdx + 1).trim();
36
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
37
+ const annotations = parseAnnotations(pendingComments);
38
+ const description = pendingComments.filter((l) => !ANNOTATION_RE.test(l)).map((l) => l.replace(/^#\s*/, "")).join(" ").trim();
39
+ entries.push({
40
+ key,
41
+ value,
42
+ annotations: {
43
+ ...annotations,
44
+ description: description || void 0
45
+ },
46
+ comment: ""
47
+ });
48
+ pendingComments.length = 0;
49
+ }
50
+ return entries;
51
+ }
52
+ //#endregion
53
+ //#region src/inferrer.ts
54
+ function isBoolean(v) {
55
+ return [
56
+ "true",
57
+ "false",
58
+ "1",
59
+ "0",
60
+ "yes",
61
+ "no"
62
+ ].includes(v.toLowerCase());
63
+ }
64
+ function isNumber(v) {
65
+ return v !== "" && !isNaN(Number(v));
66
+ }
67
+ function isUrl(v) {
68
+ try {
69
+ new URL(v);
70
+ return v.startsWith("http://") || v.startsWith("https://") || v.includes("://");
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+ function isNumberArray(v) {
76
+ if (!v.includes(",")) return false;
77
+ return v.split(",").every((s) => isNumber(s.trim()));
78
+ }
79
+ function isStringArray(v) {
80
+ return v.includes(",") && v.split(",").length > 1;
81
+ }
82
+ function inferFromAnnotation(ann) {
83
+ const enumMatch = ann.match(/^enum\((.+)\)$/);
84
+ if (enumMatch) {
85
+ const values = enumMatch[1].split(",").map((s) => s.trim());
86
+ return {
87
+ tsType: values.map((v) => `'${v}'`).join(" | "),
88
+ zodSchema: `z.enum([${values.map((v) => `'${v}'`).join(", ")}])`
89
+ };
90
+ }
91
+ if (ann === "number[]") return {
92
+ tsType: "number[]",
93
+ zodSchema: `z.string().transform(v => v.split(',').map(Number))`
94
+ };
95
+ if (ann === "string[]") return {
96
+ tsType: "string[]",
97
+ zodSchema: `z.string().transform(v => v.split(','))`
98
+ };
99
+ if (ann === "url") return {
100
+ tsType: "string",
101
+ zodSchema: `z.string().url()`
102
+ };
103
+ if (ann === "number") return {
104
+ tsType: "number",
105
+ zodSchema: `z.coerce.number()`
106
+ };
107
+ if (ann === "boolean") return {
108
+ tsType: "boolean",
109
+ zodSchema: `z.enum(['true','false','1','0']).transform(v => v === 'true' || v === '1')`
110
+ };
111
+ if (ann === "port") return {
112
+ tsType: "number",
113
+ zodSchema: `z.coerce.number().int().min(1).max(65535)`
114
+ };
115
+ if (ann === "email") return {
116
+ tsType: "string",
117
+ zodSchema: `z.string().email()`
118
+ };
119
+ return null;
120
+ }
121
+ function inferFromValue(value) {
122
+ if (value === "") return {
123
+ tsType: "string",
124
+ zodSchema: "z.string()"
125
+ };
126
+ if (isBoolean(value)) return {
127
+ tsType: "boolean",
128
+ zodSchema: `z.enum(['true','false','1','0','yes','no']).transform(v => ['true','1','yes'].includes(v.toLowerCase()))`
129
+ };
130
+ if (isNumber(value)) return {
131
+ tsType: "number",
132
+ zodSchema: Number.isInteger(Number(value)) ? "z.coerce.number().int()" : "z.coerce.number()"
133
+ };
134
+ if (isNumberArray(value)) return {
135
+ tsType: "number[]",
136
+ zodSchema: `z.string().transform(v => v.split(',').map(Number))`
137
+ };
138
+ if (isUrl(value)) return {
139
+ tsType: "string",
140
+ zodSchema: "z.string().url()"
141
+ };
142
+ if (isStringArray(value)) return {
143
+ tsType: "string[]",
144
+ zodSchema: `z.string().transform(v => v.split(',').map(s => s.trim()))`
145
+ };
146
+ return {
147
+ tsType: "string",
148
+ zodSchema: "z.string().min(1)"
149
+ };
150
+ }
151
+ function inferType(entry) {
152
+ const { value, annotations } = entry;
153
+ const isOptional = annotations.optional === true || value === "";
154
+ const base = (annotations.type ? inferFromAnnotation(annotations.type) : null) ?? inferFromValue(value);
155
+ let zodSchema = base.zodSchema;
156
+ if (annotations.default !== void 0) zodSchema = `${zodSchema}.default('${annotations.default}')`;
157
+ else if (isOptional) zodSchema = `${zodSchema}.optional()`;
158
+ return {
159
+ tsType: base.tsType,
160
+ zodSchema,
161
+ isOptional,
162
+ defaultValue: annotations.default
163
+ };
164
+ }
165
+ //#endregion
166
+ //#region src/generator.ts
167
+ function generateDts(items, options) {
168
+ const lines = [];
169
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
170
+ lines.push(`// Re-run vite to regenerate this file.`);
171
+ lines.push(``);
172
+ if (options.augmentImportMeta) {
173
+ lines.push(`/// <reference types="vite/client" />`);
174
+ lines.push(``);
175
+ lines.push(`interface ImportMetaEnv {`);
176
+ for (const { entry, inferred } of items) {
177
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
178
+ const opt = inferred.isOptional ? "?" : "";
179
+ lines.push(` readonly ${entry.key}${opt}: ${inferred.tsType}`);
180
+ }
181
+ lines.push(`}`);
182
+ lines.push(``);
183
+ lines.push(`interface ImportMeta {`);
184
+ lines.push(` readonly env: ImportMetaEnv`);
185
+ lines.push(`}`);
186
+ } else {
187
+ lines.push(`export interface Env {`);
188
+ for (const { entry, inferred } of items) {
189
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
190
+ const opt = inferred.isOptional ? "?" : "";
191
+ lines.push(` ${entry.key}${opt}: ${inferred.tsType}`);
192
+ }
193
+ lines.push(`}`);
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+ function generateZodSchema(items) {
198
+ const lines = [];
199
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
200
+ lines.push(`import { z } from 'zod'`);
201
+ lines.push(``);
202
+ lines.push(`export const envSchema = z.object({`);
203
+ for (const { entry, inferred } of items) {
204
+ if (entry.annotations.description) lines.push(` // ${entry.annotations.description}`);
205
+ lines.push(` ${entry.key}: ${inferred.zodSchema},`);
206
+ }
207
+ lines.push(`})`);
208
+ lines.push(``);
209
+ lines.push(`export type Env = z.infer<typeof envSchema>`);
210
+ return lines.join("\n");
211
+ }
212
+ function generateLoader(items, options) {
213
+ const lines = [];
214
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
215
+ lines.push(``);
216
+ if (options.schema === "zod") {
217
+ lines.push(`import { envSchema } from './env.schema'`);
218
+ lines.push(``);
219
+ lines.push(`const _parsed = envSchema.safeParse(import.meta.env)`);
220
+ lines.push(``);
221
+ lines.push(`if (!_parsed.success) {`);
222
+ lines.push(` const errors = _parsed.error.flatten().fieldErrors`);
223
+ lines.push(` const msg = Object.entries(errors)`);
224
+ lines.push(` .map(([k, v]) => \` \${k}: \${(v as string[]).join(', ')}\`)`);
225
+ lines.push(` .join('\\n')`);
226
+ lines.push(` throw new Error(\`[env-ts] Invalid environment variables:\\n\${msg}\`)`);
227
+ lines.push(`}`);
228
+ lines.push(``);
229
+ lines.push(`export const env = _parsed.data`);
230
+ lines.push(`export default env`);
231
+ } else {
232
+ const required = items.filter(({ inferred }) => !inferred.isOptional);
233
+ lines.push(`const _raw = import.meta.env`);
234
+ lines.push(``);
235
+ if (required.length > 0) {
236
+ lines.push(`const _required = [${required.map(({ entry }) => `'${entry.key}'`).join(", ")}] as const`);
237
+ lines.push(`for (const key of _required) {`);
238
+ lines.push(` if (!_raw[key]) throw new Error(\`[env-ts] Missing required env var: \${key}\`)`);
239
+ lines.push(`}`);
240
+ lines.push(``);
241
+ }
242
+ lines.push(`export const env = _raw as import('./env').Env`);
243
+ lines.push(`export default env`);
244
+ }
245
+ return lines.join("\n");
246
+ }
247
+ //#endregion
248
+ //#region src/index.ts
249
+ async function generateTypes(envDir, outputDir, options) {
250
+ const envFileNames = [
251
+ ".env",
252
+ ".env.local",
253
+ `.env.${process.env.NODE_ENV ?? "development"}`,
254
+ `.env.${process.env.NODE_ENV ?? "development"}.local`,
255
+ ...options.envFiles
256
+ ];
257
+ const entries = /* @__PURE__ */ new Map();
258
+ for (const fileName of envFileNames) {
259
+ const filePath = path.join(envDir, fileName);
260
+ if (!fs.existsSync(filePath)) continue;
261
+ const parsed = parseEnvFile(fs.readFileSync(filePath, "utf-8"));
262
+ for (const entry of parsed) entries.set(entry.key, entry);
263
+ }
264
+ if (entries.size === 0) {
265
+ console.warn("[env-ts] No .env files found or all are empty, skipping generation.");
266
+ return;
267
+ }
268
+ const items = Array.from(entries.values()).map((entry) => ({
269
+ entry,
270
+ inferred: inferType(entry)
271
+ }));
272
+ if (options.strict) {
273
+ const missing = items.filter(({ inferred }) => !inferred.isOptional && inferred.defaultValue === void 0).filter(({ entry }) => {
274
+ const v = process.env[entry.key];
275
+ return v === void 0 || v === "";
276
+ }).map(({ entry }) => entry.key);
277
+ if (missing.length > 0) {
278
+ const msg = `[env-ts] Missing required env variables: ${missing.join(", ")}`;
279
+ if (process.env.NODE_ENV === "production") throw new Error(msg);
280
+ else console.warn(msg);
281
+ }
282
+ }
283
+ fs.mkdirSync(outputDir, { recursive: true });
284
+ const genOptions = {
285
+ schema: options.schema,
286
+ augmentImportMeta: options.augmentImportMeta
287
+ };
288
+ const dtsContent = generateDts(items, genOptions);
289
+ writeIfChanged(path.join(outputDir, "env.d.ts"), dtsContent);
290
+ if (options.schema === "zod") {
291
+ const schemaContent = generateZodSchema(items);
292
+ writeIfChanged(path.join(outputDir, "env.schema.ts"), schemaContent);
293
+ }
294
+ const loaderContent = generateLoader(items, genOptions);
295
+ writeIfChanged(path.join(outputDir, "env.ts"), loaderContent);
296
+ console.log(`[env-ts] Generated ${items.length} env types → ${path.relative(process.cwd(), outputDir)}/`);
297
+ }
298
+ function writeIfChanged(filePath, content) {
299
+ if (fs.existsSync(filePath)) {
300
+ if (fs.readFileSync(filePath, "utf-8") === content) return;
301
+ }
302
+ fs.writeFileSync(filePath, content, "utf-8");
303
+ }
304
+ function envTs(userOptions = {}) {
305
+ const options = {
306
+ schema: "zod",
307
+ output: "src",
308
+ augmentImportMeta: true,
309
+ strict: true,
310
+ envFiles: [],
311
+ ...userOptions
312
+ };
313
+ let config;
314
+ let outputDir;
315
+ return {
316
+ name: "vite-plugin-typed-env",
317
+ enforce: "pre",
318
+ configResolved(resolvedConfig) {
319
+ config = resolvedConfig;
320
+ outputDir = path.resolve(config.root, options.output);
321
+ },
322
+ async buildStart() {
323
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
324
+ },
325
+ async handleHotUpdate({ file, server }) {
326
+ if (!file) return;
327
+ const fileName = path.basename(file);
328
+ if (!(fileName.startsWith(".env") || options.envFiles.includes(file))) return;
329
+ console.log(`[env-ts] Detected change in ${fileName}, regenerating...`);
330
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
331
+ server.hot.send({ type: "full-reload" });
332
+ }
333
+ };
334
+ }
335
+ //#endregion
336
+ export { envTs as default, generateTypes };
337
+
338
+ //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "vite-plugin-typed-env",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
7
+ "types": "./dist/index.d.cts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "import": {
@@ -17,6 +17,12 @@
17
17
  }
18
18
  }
19
19
  },
20
+ "files": [
21
+ "dist/*.cjs",
22
+ "dist/*.mjs",
23
+ "dist/*.d.cts",
24
+ "dist/*.d.mts"
25
+ ],
20
26
  "scripts": {
21
27
  "build": "tsdown",
22
28
  "typecheck": "tsc --noEmit",
@@ -33,6 +39,12 @@
33
39
  "vitest": "^4.1.4"
34
40
  },
35
41
  "peerDependencies": {
36
- "vite": ">=4.0.0"
42
+ "vite": ">=4.0.0",
43
+ "zod": ">=3.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "zod": {
47
+ "optional": true
48
+ }
37
49
  }
38
50
  }
@@ -1,8 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm view:*)",
5
- "WebFetch(domain:github.com)"
6
- ]
7
- }
8
- }
package/.prettierrc.cjs DELETED
@@ -1,14 +0,0 @@
1
- module.exports = {
2
- printWidth: 120,
3
- tabWidth: 2,
4
- useTabs: false,
5
- semi: false,
6
- vueIndentScriptAndStyle: true,
7
- trailingComma: 'none',
8
- singleQuote: true,
9
- bracketSpacing: true,
10
- bracketSameLine: true,
11
- arrowParens: 'always',
12
- requirePragma: false,
13
- insertPragma: false
14
- }
package/src/generator.ts DELETED
@@ -1,119 +0,0 @@
1
- import type { EnvEntry } from './parser'
2
- import type { InferredType } from './inferrer'
3
-
4
- export interface GenerateOptions {
5
- schema: 'zod' | 'valibot' | false
6
- /** 是否扩展 Vite 的 ImportMetaEnv */
7
- augmentImportMeta: boolean
8
- }
9
-
10
- interface EntryWithType {
11
- entry: EnvEntry
12
- inferred: InferredType
13
- }
14
-
15
- // ─── env.d.ts ────────────────────────────────────────────────────────────────
16
-
17
- export function generateDts(items: EntryWithType[], options: GenerateOptions): string {
18
- const lines: string[] = []
19
-
20
- lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`)
21
- lines.push(`// Re-run vite to regenerate this file.`)
22
- lines.push(``)
23
-
24
- if (options.augmentImportMeta) {
25
- lines.push(`/// <reference types="vite/client" />`)
26
- lines.push(``)
27
- lines.push(`interface ImportMetaEnv {`)
28
- for (const { entry, inferred } of items) {
29
- if (entry.annotations.description) {
30
- lines.push(` /** ${entry.annotations.description} */`)
31
- }
32
- const opt = inferred.isOptional ? '?' : ''
33
- lines.push(` readonly ${entry.key}${opt}: ${inferred.tsType}`)
34
- }
35
- lines.push(`}`)
36
- lines.push(``)
37
- lines.push(`interface ImportMeta {`)
38
- lines.push(` readonly env: ImportMetaEnv`)
39
- lines.push(`}`)
40
- } else {
41
- lines.push(`export interface Env {`)
42
- for (const { entry, inferred } of items) {
43
- if (entry.annotations.description) {
44
- lines.push(` /** ${entry.annotations.description} */`)
45
- }
46
- const opt = inferred.isOptional ? '?' : ''
47
- lines.push(` ${entry.key}${opt}: ${inferred.tsType}`)
48
- }
49
- lines.push(`}`)
50
- }
51
-
52
- return lines.join('\n')
53
- }
54
-
55
- // ─── env.schema.ts ───────────────────────────────────────────────────────────
56
-
57
- export function generateZodSchema(items: EntryWithType[]): string {
58
- const lines: string[] = []
59
-
60
- lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`)
61
- lines.push(`import { z } from 'zod'`)
62
- lines.push(``)
63
- lines.push(`export const envSchema = z.object({`)
64
-
65
- for (const { entry, inferred } of items) {
66
- if (entry.annotations.description) {
67
- lines.push(` // ${entry.annotations.description}`)
68
- }
69
- lines.push(` ${entry.key}: ${inferred.zodSchema},`)
70
- }
71
-
72
- lines.push(`})`)
73
- lines.push(``)
74
- lines.push(`export type Env = z.infer<typeof envSchema>`)
75
-
76
- return lines.join('\n')
77
- }
78
-
79
- // ─── env.ts (runtime loader) ─────────────────────────────────────────────────
80
-
81
- export function generateLoader(items: EntryWithType[], options: GenerateOptions): string {
82
- const lines: string[] = []
83
-
84
- lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`)
85
- lines.push(``)
86
-
87
- if (options.schema === 'zod') {
88
- lines.push(`import { envSchema } from './env.schema'`)
89
- lines.push(``)
90
- lines.push(`const _parsed = envSchema.safeParse(import.meta.env)`)
91
- lines.push(``)
92
- lines.push(`if (!_parsed.success) {`)
93
- lines.push(` const errors = _parsed.error.flatten().fieldErrors`)
94
- lines.push(` const msg = Object.entries(errors)`)
95
- lines.push(` .map(([k, v]) => \` \${k}: \${(v as string[]).join(', ')}\`)`)
96
- lines.push(` .join('\\n')`)
97
- lines.push(` throw new Error(\`[env-ts] Invalid environment variables:\\n\${msg}\`)`)
98
- lines.push(`}`)
99
- lines.push(``)
100
- lines.push(`export const env = _parsed.data`)
101
- lines.push(`export default env`)
102
- } else {
103
- // 无 schema 时,直接做简单的必填检查
104
- const required = items.filter(({ inferred }) => !inferred.isOptional)
105
- lines.push(`const _raw = import.meta.env`)
106
- lines.push(``)
107
- if (required.length > 0) {
108
- lines.push(`const _required = [${required.map(({ entry }) => `'${entry.key}'`).join(', ')}] as const`)
109
- lines.push(`for (const key of _required) {`)
110
- lines.push(` if (!_raw[key]) throw new Error(\`[env-ts] Missing required env var: \${key}\`)`)
111
- lines.push(`}`)
112
- lines.push(``)
113
- }
114
- lines.push(`export const env = _raw as import('./env').Env`)
115
- lines.push(`export default env`)
116
- }
117
-
118
- return lines.join('\n')
119
- }
package/src/index.ts DELETED
@@ -1,174 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import type { Plugin, ResolvedConfig } from 'vite'
4
- import { parseEnvFile, type EnvEntry } from './parser'
5
- import { inferType } from './inferrer'
6
- import { generateDts, generateZodSchema, generateLoader } from './generator'
7
-
8
- export interface EnvTsOptions {
9
- /**
10
- * 生成 Zod schema 文件
11
- * @default 'zod'
12
- */
13
- schema?: 'zod' | false
14
- /**
15
- * 生成文件的输出目录(相对于项目根目录)
16
- * @default 'src'
17
- */
18
- output?: string
19
- /**
20
- * 是否扩展 Vite 的 ImportMetaEnv 类型
21
- * 开启后 import.meta.env.YOUR_VAR 自动有类型
22
- * @default true
23
- */
24
- augmentImportMeta?: boolean
25
- /**
26
- * 缺失必填变量时是否让构建失败
27
- * @default true
28
- */
29
- strict?: boolean
30
- /**
31
- * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
32
- */
33
- envFiles?: string[]
34
- }
35
-
36
- // ─── 核心函数 ─────────────────────────────────────────────────────────────────
37
-
38
- export async function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void> {
39
- // 1. 找到所有 .env 文件(按优先级顺序读取,后面的覆盖前面的)
40
- const envFileNames = [
41
- '.env',
42
- '.env.local',
43
- `.env.${process.env.NODE_ENV ?? 'development'}`,
44
- `.env.${process.env.NODE_ENV ?? 'development'}.local`,
45
- ...options.envFiles
46
- ]
47
-
48
- const entries = new Map<string, EnvEntry>()
49
-
50
- for (const fileName of envFileNames) {
51
- const filePath = path.join(envDir, fileName)
52
- if (!fs.existsSync(filePath)) continue
53
-
54
- const content = fs.readFileSync(filePath, 'utf-8')
55
- const parsed = parseEnvFile(content)
56
-
57
- // 后读的文件覆盖先读的(保留注释/annotation)
58
- for (const entry of parsed) {
59
- entries.set(entry.key, entry)
60
- }
61
- }
62
-
63
- if (entries.size === 0) {
64
- console.warn('[env-ts] No .env files found or all are empty, skipping generation.')
65
- return
66
- }
67
-
68
- // 2. 对每个变量做类型推断
69
- const items = Array.from(entries.values()).map((entry) => ({
70
- entry,
71
- inferred: inferType(entry)
72
- }))
73
-
74
- // strict 模式:检查必填变量在 process.env 里是否真实存在
75
- if (options.strict) {
76
- const missing = items
77
- .filter(({ inferred }) => !inferred.isOptional && inferred.defaultValue === undefined)
78
- .filter(({ entry }) => {
79
- const v = process.env[entry.key]
80
- return v === undefined || v === ''
81
- })
82
- .map(({ entry }) => entry.key)
83
-
84
- if (missing.length > 0) {
85
- // 开发时警告,构建时报错
86
- const msg = `[env-ts] Missing required env variables: ${missing.join(', ')}`
87
- if (process.env.NODE_ENV === 'production') {
88
- throw new Error(msg)
89
- } else {
90
- console.warn(msg)
91
- }
92
- }
93
- }
94
-
95
- // 3. 确保输出目录存在
96
- fs.mkdirSync(outputDir, { recursive: true })
97
-
98
- const genOptions = {
99
- schema: options.schema,
100
- augmentImportMeta: options.augmentImportMeta
101
- }
102
-
103
- // 4. 生成 env.d.ts
104
- const dtsContent = generateDts(items, genOptions)
105
- writeIfChanged(path.join(outputDir, 'env.d.ts'), dtsContent)
106
-
107
- // 5. 生成 env.schema.ts(可选)
108
- if (options.schema === 'zod') {
109
- const schemaContent = generateZodSchema(items)
110
- writeIfChanged(path.join(outputDir, 'env.schema.ts'), schemaContent)
111
- }
112
-
113
- // 6. 生成 env.ts(运行时 loader)
114
- const loaderContent = generateLoader(items, genOptions)
115
- writeIfChanged(path.join(outputDir, 'env.ts'), loaderContent)
116
-
117
- console.log(`[env-ts] Generated ${items.length} env types → ${path.relative(process.cwd(), outputDir)}/`)
118
- }
119
-
120
- // 只在内容变化时写文件,避免触发不必要的热更新
121
- function writeIfChanged(filePath: string, content: string): void {
122
- if (fs.existsSync(filePath)) {
123
- const existing = fs.readFileSync(filePath, 'utf-8')
124
- if (existing === content) return
125
- }
126
- fs.writeFileSync(filePath, content, 'utf-8')
127
- }
128
-
129
- // ─── Vite 插件 ────────────────────────────────────────────────────────────────
130
-
131
- export default function envTs(userOptions: EnvTsOptions = {}): Plugin {
132
- const options: Required<EnvTsOptions> = {
133
- schema: 'zod',
134
- output: 'src',
135
- augmentImportMeta: true,
136
- strict: true,
137
- envFiles: [],
138
- ...userOptions
139
- }
140
-
141
- let config: ResolvedConfig
142
- let outputDir: string
143
-
144
- return {
145
- name: 'vite-plugin-typed-env',
146
- enforce: 'pre', // 在其他插件之前运行,确保类型文件先生成
147
-
148
- configResolved(resolvedConfig) {
149
- config = resolvedConfig
150
- outputDir = path.resolve(config.root, options.output)
151
- },
152
-
153
- async buildStart() {
154
- const envDir = config.envDir === false ? config.root : config.envDir
155
- await generateTypes(envDir, outputDir, options)
156
- },
157
-
158
- async handleHotUpdate({ file, server }) {
159
- if (!file) return
160
-
161
- const fileName = path.basename(file)
162
- const isEnvFile = fileName.startsWith('.env') || options.envFiles.includes(file)
163
-
164
- if (!isEnvFile) return
165
-
166
- console.log(`[env-ts] Detected change in ${fileName}, regenerating...`)
167
- const envDir = config.envDir === false ? config.root : config.envDir
168
- await generateTypes(envDir, outputDir, options)
169
-
170
- // 通知 client 有文件更新(触发 TS 语言服务刷新)
171
- server.hot.send({ type: 'full-reload' })
172
- }
173
- }
174
- }