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/README.md CHANGED
@@ -18,6 +18,12 @@ A Vite plugin that automatically generates TypeScript types and Zod schemas from
18
18
  npm install vite-plugin-typed-env -D
19
19
  ```
20
20
 
21
+ If using Zod validation (default), also install zod:
22
+
23
+ ```bash
24
+ npm install zod
25
+ ```
26
+
21
27
  ## Usage
22
28
 
23
29
  ### 1. Add to Vite config
package/dist/index.cjs ADDED
@@ -0,0 +1,367 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
27
+ let node_fs = require("node:fs");
28
+ node_fs = __toESM(node_fs);
29
+ let node_path = require("node:path");
30
+ node_path = __toESM(node_path);
31
+ //#region src/parser.ts
32
+ const ANNOTATION_RE = /^#\s*@(\w+)(?::\s*(.+))?$/;
33
+ function parseAnnotations(lines) {
34
+ const ann = {};
35
+ for (const line of lines) {
36
+ const m = line.match(ANNOTATION_RE);
37
+ if (!m) continue;
38
+ const [, key, value] = m;
39
+ if (key === "optional") ann.optional = true;
40
+ else if (key === "type" && value) ann.type = value.trim();
41
+ else if (key === "default" && value) ann.default = value.trim();
42
+ else if (key === "desc" && value) ann.description = value.trim();
43
+ }
44
+ return ann;
45
+ }
46
+ function parseEnvFile(content) {
47
+ const lines = content.split("\n");
48
+ const entries = [];
49
+ const pendingComments = [];
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const raw = lines[i].trim();
52
+ if (raw === "") {
53
+ pendingComments.length = 0;
54
+ continue;
55
+ }
56
+ if (raw.startsWith("#")) {
57
+ pendingComments.push(raw);
58
+ continue;
59
+ }
60
+ const eqIdx = raw.indexOf("=");
61
+ if (eqIdx === -1) continue;
62
+ const key = raw.slice(0, eqIdx).trim();
63
+ let value = raw.slice(eqIdx + 1).trim();
64
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
65
+ const annotations = parseAnnotations(pendingComments);
66
+ const description = pendingComments.filter((l) => !ANNOTATION_RE.test(l)).map((l) => l.replace(/^#\s*/, "")).join(" ").trim();
67
+ entries.push({
68
+ key,
69
+ value,
70
+ annotations: {
71
+ ...annotations,
72
+ description: description || void 0
73
+ },
74
+ comment: ""
75
+ });
76
+ pendingComments.length = 0;
77
+ }
78
+ return entries;
79
+ }
80
+ //#endregion
81
+ //#region src/inferrer.ts
82
+ function isBoolean(v) {
83
+ return [
84
+ "true",
85
+ "false",
86
+ "1",
87
+ "0",
88
+ "yes",
89
+ "no"
90
+ ].includes(v.toLowerCase());
91
+ }
92
+ function isNumber(v) {
93
+ return v !== "" && !isNaN(Number(v));
94
+ }
95
+ function isUrl(v) {
96
+ try {
97
+ new URL(v);
98
+ return v.startsWith("http://") || v.startsWith("https://") || v.includes("://");
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function isNumberArray(v) {
104
+ if (!v.includes(",")) return false;
105
+ return v.split(",").every((s) => isNumber(s.trim()));
106
+ }
107
+ function isStringArray(v) {
108
+ return v.includes(",") && v.split(",").length > 1;
109
+ }
110
+ function inferFromAnnotation(ann) {
111
+ const enumMatch = ann.match(/^enum\((.+)\)$/);
112
+ if (enumMatch) {
113
+ const values = enumMatch[1].split(",").map((s) => s.trim());
114
+ return {
115
+ tsType: values.map((v) => `'${v}'`).join(" | "),
116
+ zodSchema: `z.enum([${values.map((v) => `'${v}'`).join(", ")}])`
117
+ };
118
+ }
119
+ if (ann === "number[]") return {
120
+ tsType: "number[]",
121
+ zodSchema: `z.string().transform(v => v.split(',').map(Number))`
122
+ };
123
+ if (ann === "string[]") return {
124
+ tsType: "string[]",
125
+ zodSchema: `z.string().transform(v => v.split(','))`
126
+ };
127
+ if (ann === "url") return {
128
+ tsType: "string",
129
+ zodSchema: `z.string().url()`
130
+ };
131
+ if (ann === "number") return {
132
+ tsType: "number",
133
+ zodSchema: `z.coerce.number()`
134
+ };
135
+ if (ann === "boolean") return {
136
+ tsType: "boolean",
137
+ zodSchema: `z.enum(['true','false','1','0']).transform(v => v === 'true' || v === '1')`
138
+ };
139
+ if (ann === "port") return {
140
+ tsType: "number",
141
+ zodSchema: `z.coerce.number().int().min(1).max(65535)`
142
+ };
143
+ if (ann === "email") return {
144
+ tsType: "string",
145
+ zodSchema: `z.string().email()`
146
+ };
147
+ return null;
148
+ }
149
+ function inferFromValue(value) {
150
+ if (value === "") return {
151
+ tsType: "string",
152
+ zodSchema: "z.string()"
153
+ };
154
+ if (isBoolean(value)) return {
155
+ tsType: "boolean",
156
+ zodSchema: `z.enum(['true','false','1','0','yes','no']).transform(v => ['true','1','yes'].includes(v.toLowerCase()))`
157
+ };
158
+ if (isNumber(value)) return {
159
+ tsType: "number",
160
+ zodSchema: Number.isInteger(Number(value)) ? "z.coerce.number().int()" : "z.coerce.number()"
161
+ };
162
+ if (isNumberArray(value)) return {
163
+ tsType: "number[]",
164
+ zodSchema: `z.string().transform(v => v.split(',').map(Number))`
165
+ };
166
+ if (isUrl(value)) return {
167
+ tsType: "string",
168
+ zodSchema: "z.string().url()"
169
+ };
170
+ if (isStringArray(value)) return {
171
+ tsType: "string[]",
172
+ zodSchema: `z.string().transform(v => v.split(',').map(s => s.trim()))`
173
+ };
174
+ return {
175
+ tsType: "string",
176
+ zodSchema: "z.string().min(1)"
177
+ };
178
+ }
179
+ function inferType(entry) {
180
+ const { value, annotations } = entry;
181
+ const isOptional = annotations.optional === true || value === "";
182
+ const base = (annotations.type ? inferFromAnnotation(annotations.type) : null) ?? inferFromValue(value);
183
+ let zodSchema = base.zodSchema;
184
+ if (annotations.default !== void 0) zodSchema = `${zodSchema}.default('${annotations.default}')`;
185
+ else if (isOptional) zodSchema = `${zodSchema}.optional()`;
186
+ return {
187
+ tsType: base.tsType,
188
+ zodSchema,
189
+ isOptional,
190
+ defaultValue: annotations.default
191
+ };
192
+ }
193
+ //#endregion
194
+ //#region src/generator.ts
195
+ function generateDts(items, options) {
196
+ const lines = [];
197
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
198
+ lines.push(`// Re-run vite to regenerate this file.`);
199
+ lines.push(``);
200
+ if (options.augmentImportMeta) {
201
+ lines.push(`/// <reference types="vite/client" />`);
202
+ lines.push(``);
203
+ lines.push(`interface ImportMetaEnv {`);
204
+ for (const { entry, inferred } of items) {
205
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
206
+ const opt = inferred.isOptional ? "?" : "";
207
+ lines.push(` readonly ${entry.key}${opt}: ${inferred.tsType}`);
208
+ }
209
+ lines.push(`}`);
210
+ lines.push(``);
211
+ lines.push(`interface ImportMeta {`);
212
+ lines.push(` readonly env: ImportMetaEnv`);
213
+ lines.push(`}`);
214
+ } else {
215
+ lines.push(`export interface Env {`);
216
+ for (const { entry, inferred } of items) {
217
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
218
+ const opt = inferred.isOptional ? "?" : "";
219
+ lines.push(` ${entry.key}${opt}: ${inferred.tsType}`);
220
+ }
221
+ lines.push(`}`);
222
+ }
223
+ return lines.join("\n");
224
+ }
225
+ function generateZodSchema(items) {
226
+ const lines = [];
227
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
228
+ lines.push(`import { z } from 'zod'`);
229
+ lines.push(``);
230
+ lines.push(`export const envSchema = z.object({`);
231
+ for (const { entry, inferred } of items) {
232
+ if (entry.annotations.description) lines.push(` // ${entry.annotations.description}`);
233
+ lines.push(` ${entry.key}: ${inferred.zodSchema},`);
234
+ }
235
+ lines.push(`})`);
236
+ lines.push(``);
237
+ lines.push(`export type Env = z.infer<typeof envSchema>`);
238
+ return lines.join("\n");
239
+ }
240
+ function generateLoader(items, options) {
241
+ const lines = [];
242
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
243
+ lines.push(``);
244
+ if (options.schema === "zod") {
245
+ lines.push(`import { envSchema } from './env.schema'`);
246
+ lines.push(``);
247
+ lines.push(`const _parsed = envSchema.safeParse(import.meta.env)`);
248
+ lines.push(``);
249
+ lines.push(`if (!_parsed.success) {`);
250
+ lines.push(` const errors = _parsed.error.flatten().fieldErrors`);
251
+ lines.push(` const msg = Object.entries(errors)`);
252
+ lines.push(` .map(([k, v]) => \` \${k}: \${(v as string[]).join(', ')}\`)`);
253
+ lines.push(` .join('\\n')`);
254
+ lines.push(` throw new Error(\`[env-ts] Invalid environment variables:\\n\${msg}\`)`);
255
+ lines.push(`}`);
256
+ lines.push(``);
257
+ lines.push(`export const env = _parsed.data`);
258
+ lines.push(`export default env`);
259
+ } else {
260
+ const required = items.filter(({ inferred }) => !inferred.isOptional);
261
+ lines.push(`const _raw = import.meta.env`);
262
+ lines.push(``);
263
+ if (required.length > 0) {
264
+ lines.push(`const _required = [${required.map(({ entry }) => `'${entry.key}'`).join(", ")}] as const`);
265
+ lines.push(`for (const key of _required) {`);
266
+ lines.push(` if (!_raw[key]) throw new Error(\`[env-ts] Missing required env var: \${key}\`)`);
267
+ lines.push(`}`);
268
+ lines.push(``);
269
+ }
270
+ lines.push(`export const env = _raw as import('./env').Env`);
271
+ lines.push(`export default env`);
272
+ }
273
+ return lines.join("\n");
274
+ }
275
+ //#endregion
276
+ //#region src/index.ts
277
+ async function generateTypes(envDir, outputDir, options) {
278
+ const envFileNames = [
279
+ ".env",
280
+ ".env.local",
281
+ `.env.${process.env.NODE_ENV ?? "development"}`,
282
+ `.env.${process.env.NODE_ENV ?? "development"}.local`,
283
+ ...options.envFiles
284
+ ];
285
+ const entries = /* @__PURE__ */ new Map();
286
+ for (const fileName of envFileNames) {
287
+ const filePath = node_path.default.join(envDir, fileName);
288
+ if (!node_fs.default.existsSync(filePath)) continue;
289
+ const parsed = parseEnvFile(node_fs.default.readFileSync(filePath, "utf-8"));
290
+ for (const entry of parsed) entries.set(entry.key, entry);
291
+ }
292
+ if (entries.size === 0) {
293
+ console.warn("[env-ts] No .env files found or all are empty, skipping generation.");
294
+ return;
295
+ }
296
+ const items = Array.from(entries.values()).map((entry) => ({
297
+ entry,
298
+ inferred: inferType(entry)
299
+ }));
300
+ if (options.strict) {
301
+ const missing = items.filter(({ inferred }) => !inferred.isOptional && inferred.defaultValue === void 0).filter(({ entry }) => {
302
+ const v = process.env[entry.key];
303
+ return v === void 0 || v === "";
304
+ }).map(({ entry }) => entry.key);
305
+ if (missing.length > 0) {
306
+ const msg = `[env-ts] Missing required env variables: ${missing.join(", ")}`;
307
+ if (process.env.NODE_ENV === "production") throw new Error(msg);
308
+ else console.warn(msg);
309
+ }
310
+ }
311
+ node_fs.default.mkdirSync(outputDir, { recursive: true });
312
+ const genOptions = {
313
+ schema: options.schema,
314
+ augmentImportMeta: options.augmentImportMeta
315
+ };
316
+ const dtsContent = generateDts(items, genOptions);
317
+ writeIfChanged(node_path.default.join(outputDir, "env.d.ts"), dtsContent);
318
+ if (options.schema === "zod") {
319
+ const schemaContent = generateZodSchema(items);
320
+ writeIfChanged(node_path.default.join(outputDir, "env.schema.ts"), schemaContent);
321
+ }
322
+ const loaderContent = generateLoader(items, genOptions);
323
+ writeIfChanged(node_path.default.join(outputDir, "env.ts"), loaderContent);
324
+ console.log(`[env-ts] Generated ${items.length} env types → ${node_path.default.relative(process.cwd(), outputDir)}/`);
325
+ }
326
+ function writeIfChanged(filePath, content) {
327
+ if (node_fs.default.existsSync(filePath)) {
328
+ if (node_fs.default.readFileSync(filePath, "utf-8") === content) return;
329
+ }
330
+ node_fs.default.writeFileSync(filePath, content, "utf-8");
331
+ }
332
+ function envTs(userOptions = {}) {
333
+ const options = {
334
+ schema: "zod",
335
+ output: "src",
336
+ augmentImportMeta: true,
337
+ strict: true,
338
+ envFiles: [],
339
+ ...userOptions
340
+ };
341
+ let config;
342
+ let outputDir;
343
+ return {
344
+ name: "vite-plugin-typed-env",
345
+ enforce: "pre",
346
+ configResolved(resolvedConfig) {
347
+ config = resolvedConfig;
348
+ outputDir = node_path.default.resolve(config.root, options.output);
349
+ },
350
+ async buildStart() {
351
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
352
+ },
353
+ async handleHotUpdate({ file, server }) {
354
+ if (!file) return;
355
+ const fileName = node_path.default.basename(file);
356
+ if (!(fileName.startsWith(".env") || options.envFiles.includes(file))) return;
357
+ console.log(`[env-ts] Detected change in ${fileName}, regenerating...`);
358
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
359
+ server.hot.send({ type: "full-reload" });
360
+ }
361
+ };
362
+ }
363
+ //#endregion
364
+ exports.default = envTs;
365
+ exports.generateTypes = generateTypes;
366
+
367
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1,35 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/index.d.ts
4
+ interface EnvTsOptions {
5
+ /**
6
+ * 生成 Zod schema 文件
7
+ * @default 'zod'
8
+ */
9
+ schema?: 'zod' | false;
10
+ /**
11
+ * 生成文件的输出目录(相对于项目根目录)
12
+ * @default 'src'
13
+ */
14
+ output?: string;
15
+ /**
16
+ * 是否扩展 Vite 的 ImportMetaEnv 类型
17
+ * 开启后 import.meta.env.YOUR_VAR 自动有类型
18
+ * @default true
19
+ */
20
+ augmentImportMeta?: boolean;
21
+ /**
22
+ * 缺失必填变量时是否让构建失败
23
+ * @default true
24
+ */
25
+ strict?: boolean;
26
+ /**
27
+ * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
28
+ */
29
+ envFiles?: string[];
30
+ }
31
+ declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
32
+ declare function envTs(userOptions?: EnvTsOptions): Plugin;
33
+ //#endregion
34
+ export { EnvTsOptions, envTs as default, generateTypes };
35
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,35 @@
1
+ import { Plugin } from "vite";
2
+
3
+ //#region src/index.d.ts
4
+ interface EnvTsOptions {
5
+ /**
6
+ * 生成 Zod schema 文件
7
+ * @default 'zod'
8
+ */
9
+ schema?: 'zod' | false;
10
+ /**
11
+ * 生成文件的输出目录(相对于项目根目录)
12
+ * @default 'src'
13
+ */
14
+ output?: string;
15
+ /**
16
+ * 是否扩展 Vite 的 ImportMetaEnv 类型
17
+ * 开启后 import.meta.env.YOUR_VAR 自动有类型
18
+ * @default true
19
+ */
20
+ augmentImportMeta?: boolean;
21
+ /**
22
+ * 缺失必填变量时是否让构建失败
23
+ * @default true
24
+ */
25
+ strict?: boolean;
26
+ /**
27
+ * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
28
+ */
29
+ envFiles?: string[];
30
+ }
31
+ declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
32
+ declare function envTs(userOptions?: EnvTsOptions): Plugin;
33
+ //#endregion
34
+ export { EnvTsOptions, envTs as default, generateTypes };
35
+ //# sourceMappingURL=index.d.mts.map