vite-plugin-typed-env 0.1.1 → 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vite-plugin-typed-env contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
@@ -191,4 +197,4 @@ Files are loaded in this order (later overrides earlier):
191
197
 
192
198
  ## License
193
199
 
194
- MIT
200
+ MIT
package/dist/index.cjs CHANGED
@@ -28,84 +28,102 @@ let node_fs = require("node:fs");
28
28
  node_fs = __toESM(node_fs);
29
29
  let node_path = require("node:path");
30
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;
31
+ //#region src/generator.ts
32
+ function generateDts(items, options) {
33
+ const lines = [];
34
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
35
+ lines.push(`// Re-run vite to regenerate this file.`);
36
+ lines.push(``);
37
+ if (options.augmentImportMeta) {
38
+ lines.push(`/// <reference types="vite/client" />`);
39
+ lines.push(``);
40
+ lines.push(`interface ImportMetaEnv {`);
41
+ for (const { entry, inferred } of items) {
42
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
43
+ const opt = inferred.isOptional ? "?" : "";
44
+ lines.push(` readonly ${entry.key}${opt}: ${inferred.tsType}`);
55
45
  }
56
- if (raw.startsWith("#")) {
57
- pendingComments.push(raw);
58
- continue;
46
+ lines.push(`}`);
47
+ lines.push(``);
48
+ lines.push(`interface ImportMeta {`);
49
+ lines.push(` readonly env: ImportMetaEnv`);
50
+ lines.push(`}`);
51
+ } else {
52
+ lines.push(`export interface Env {`);
53
+ for (const { entry, inferred } of items) {
54
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
55
+ const opt = inferred.isOptional ? "?" : "";
56
+ lines.push(` ${entry.key}${opt}: ${inferred.tsType}`);
59
57
  }
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;
58
+ lines.push(`}`);
77
59
  }
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));
60
+ return lines.join("\n");
94
61
  }
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;
62
+ function generateLoader(items, options) {
63
+ const lines = [];
64
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
65
+ lines.push(``);
66
+ if (options.schema === "zod") {
67
+ lines.push(`import { envSchema } from './env.schema'`);
68
+ lines.push(``);
69
+ lines.push(`const _parsed = envSchema.safeParse(import.meta.env)`);
70
+ lines.push(``);
71
+ lines.push(`if (!_parsed.success) {`);
72
+ lines.push(` const errors = _parsed.error.flatten().fieldErrors`);
73
+ lines.push(` const msg = Object.entries(errors)`);
74
+ lines.push(` .map(([k, v]) => \` \${k}: \${(v as string[]).join(', ')}\`)`);
75
+ lines.push(` .join('\\n')`);
76
+ lines.push(` throw new Error(\`[env-ts] Invalid environment variables:\\n\${msg}\`)`);
77
+ lines.push(`}`);
78
+ lines.push(``);
79
+ lines.push(`export const env = _parsed.data`);
80
+ lines.push(`export default env`);
81
+ } else {
82
+ const required = items.filter(({ inferred }) => !inferred.isOptional);
83
+ lines.push(`const _raw = import.meta.env`);
84
+ lines.push(``);
85
+ if (required.length > 0) {
86
+ lines.push(`const _required = [${required.map(({ entry }) => `'${entry.key}'`).join(", ")}] as const`);
87
+ lines.push(`for (const key of _required) {`);
88
+ lines.push(` if (!_raw[key]) throw new Error(\`[env-ts] Missing required env var: \${key}\`)`);
89
+ lines.push(`}`);
90
+ lines.push(``);
91
+ }
92
+ lines.push(`export const env = _raw as import('./env').Env`);
93
+ lines.push(`export default env`);
101
94
  }
95
+ return lines.join("\n");
102
96
  }
103
- function isNumberArray(v) {
104
- if (!v.includes(",")) return false;
105
- return v.split(",").every((s) => isNumber(s.trim()));
97
+ function generateZodSchema(items) {
98
+ const lines = [];
99
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
100
+ lines.push(`import { z } from 'zod'`);
101
+ lines.push(``);
102
+ lines.push(`export const envSchema = z.object({`);
103
+ for (const { entry, inferred } of items) {
104
+ if (entry.annotations.description) lines.push(` // ${entry.annotations.description}`);
105
+ lines.push(` ${entry.key}: ${inferred.zodSchema},`);
106
+ }
107
+ lines.push(`})`);
108
+ lines.push(``);
109
+ lines.push(`export type Env = z.infer<typeof envSchema>`);
110
+ return lines.join("\n");
106
111
  }
107
- function isStringArray(v) {
108
- return v.includes(",") && v.split(",").length > 1;
112
+ //#endregion
113
+ //#region src/inferrer.ts
114
+ function inferType(entry) {
115
+ const { annotations, value } = entry;
116
+ const isOptional = annotations.optional === true || value === "";
117
+ const base = (annotations.type ? inferFromAnnotation(annotations.type) : null) ?? inferFromValue(value);
118
+ let zodSchema = base.zodSchema;
119
+ if (annotations.default !== void 0) zodSchema = `${zodSchema}.default('${annotations.default}')`;
120
+ else if (isOptional) zodSchema = `${zodSchema}.optional()`;
121
+ return {
122
+ defaultValue: annotations.default,
123
+ isOptional,
124
+ tsType: base.tsType,
125
+ zodSchema
126
+ };
109
127
  }
110
128
  function inferFromAnnotation(ann) {
111
129
  const enumMatch = ann.match(/^enum\((.+)\)$/);
@@ -176,104 +194,117 @@ function inferFromValue(value) {
176
194
  zodSchema: "z.string().min(1)"
177
195
  };
178
196
  }
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
- };
197
+ function isBoolean(v) {
198
+ return [
199
+ "0",
200
+ "1",
201
+ "false",
202
+ "no",
203
+ "true",
204
+ "yes"
205
+ ].includes(v.toLowerCase());
206
+ }
207
+ function isNumber(v) {
208
+ return v !== "" && !isNaN(Number(v));
209
+ }
210
+ function isNumberArray(v) {
211
+ if (!v.includes(",")) return false;
212
+ return v.split(",").every((s) => isNumber(s.trim()));
213
+ }
214
+ function isStringArray(v) {
215
+ return v.includes(",") && v.split(",").length > 1;
216
+ }
217
+ function isUrl(v) {
218
+ try {
219
+ new URL(v);
220
+ return v.startsWith("http://") || v.startsWith("https://") || v.includes("://");
221
+ } catch {
222
+ return false;
223
+ }
192
224
  }
193
225
  //#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}`);
226
+ //#region src/parser.ts
227
+ const ANNOTATION_RE = /^#\s*@(\w+)(?::\s*(.+))?$/;
228
+ function parseEnvFile(content) {
229
+ const lines = content.split("\n");
230
+ const entries = [];
231
+ const pendingComments = [];
232
+ for (let i = 0; i < lines.length; i++) {
233
+ const raw = lines[i].trim();
234
+ if (raw === "") {
235
+ pendingComments.length = 0;
236
+ continue;
208
237
  }
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}`);
238
+ if (raw.startsWith("#")) {
239
+ pendingComments.push(raw);
240
+ continue;
220
241
  }
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},`);
242
+ const eqIdx = raw.indexOf("=");
243
+ if (eqIdx === -1) continue;
244
+ const key = raw.slice(0, eqIdx).trim();
245
+ let value = raw.slice(eqIdx + 1).trim();
246
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
247
+ const annotations = parseAnnotations(pendingComments);
248
+ const description = pendingComments.filter((l) => !ANNOTATION_RE.test(l)).map((l) => l.replace(/^#\s*/, "")).join(" ").trim();
249
+ entries.push({
250
+ annotations: {
251
+ ...annotations,
252
+ description: description || void 0
253
+ },
254
+ comment: "",
255
+ key,
256
+ value
257
+ });
258
+ pendingComments.length = 0;
234
259
  }
235
- lines.push(`})`);
236
- lines.push(``);
237
- lines.push(`export type Env = z.infer<typeof envSchema>`);
238
- return lines.join("\n");
260
+ return entries;
239
261
  }
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`);
262
+ function parseAnnotations(lines) {
263
+ const ann = {};
264
+ for (const line of lines) {
265
+ const m = line.match(ANNOTATION_RE);
266
+ if (!m) continue;
267
+ const [, key, value] = m;
268
+ if (key === "optional") ann.optional = true;
269
+ else if (key === "type" && value) ann.type = value.trim();
270
+ else if (key === "default" && value) ann.default = value.trim();
271
+ else if (key === "desc" && value) ann.description = value.trim();
272
272
  }
273
- return lines.join("\n");
273
+ return ann;
274
274
  }
275
275
  //#endregion
276
276
  //#region src/index.ts
277
+ function envTs(userOptions = {}) {
278
+ const options = {
279
+ augmentImportMeta: true,
280
+ envFiles: [],
281
+ output: "src",
282
+ schema: "zod",
283
+ strict: true,
284
+ ...userOptions
285
+ };
286
+ let config;
287
+ let outputDir;
288
+ return {
289
+ async buildStart() {
290
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
291
+ },
292
+ configResolved(resolvedConfig) {
293
+ config = resolvedConfig;
294
+ outputDir = node_path.default.resolve(config.root, options.output);
295
+ },
296
+ enforce: "pre",
297
+ async handleHotUpdate({ file, server }) {
298
+ if (!file) return;
299
+ const fileName = node_path.default.basename(file);
300
+ if (!(fileName.startsWith(".env") || options.envFiles.includes(file))) return;
301
+ console.log(`[env-ts] Detected change in ${fileName}, regenerating...`);
302
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
303
+ server.hot.send({ type: "full-reload" });
304
+ },
305
+ name: "vite-plugin-typed-env"
306
+ };
307
+ }
277
308
  async function generateTypes(envDir, outputDir, options) {
278
309
  const envFileNames = [
279
310
  ".env",
@@ -310,8 +341,8 @@ async function generateTypes(envDir, outputDir, options) {
310
341
  }
311
342
  node_fs.default.mkdirSync(outputDir, { recursive: true });
312
343
  const genOptions = {
313
- schema: options.schema,
314
- augmentImportMeta: options.augmentImportMeta
344
+ augmentImportMeta: options.augmentImportMeta,
345
+ schema: options.schema
315
346
  };
316
347
  const dtsContent = generateDts(items, genOptions);
317
348
  writeIfChanged(node_path.default.join(outputDir, "env.d.ts"), dtsContent);
@@ -329,37 +360,6 @@ function writeIfChanged(filePath, content) {
329
360
  }
330
361
  node_fs.default.writeFileSync(filePath, content, "utf-8");
331
362
  }
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
363
  //#endregion
364
364
  exports.default = envTs;
365
365
  exports.generateTypes = generateTypes;
package/dist/index.d.cts CHANGED
@@ -3,33 +3,33 @@ import { Plugin } from "vite";
3
3
  //#region src/index.d.ts
4
4
  interface EnvTsOptions {
5
5
  /**
6
- * 生成 Zod schema 文件
7
- * @default 'zod'
6
+ * 是否扩展 Vite ImportMetaEnv 类型
7
+ * 开启后 import.meta.env.YOUR_VAR 自动有类型
8
+ * @default true
8
9
  */
9
- schema?: 'zod' | false;
10
+ augmentImportMeta?: boolean;
11
+ /**
12
+ * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
13
+ */
14
+ envFiles?: string[];
10
15
  /**
11
16
  * 生成文件的输出目录(相对于项目根目录)
12
17
  * @default 'src'
13
18
  */
14
19
  output?: string;
15
20
  /**
16
- * 是否扩展 Vite ImportMetaEnv 类型
17
- * 开启后 import.meta.env.YOUR_VAR 自动有类型
18
- * @default true
21
+ * 生成 Zod schema 文件
22
+ * @default 'zod'
19
23
  */
20
- augmentImportMeta?: boolean;
24
+ schema?: 'zod' | false;
21
25
  /**
22
26
  * 缺失必填变量时是否让构建失败
23
27
  * @default true
24
28
  */
25
29
  strict?: boolean;
26
- /**
27
- * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
28
- */
29
- envFiles?: string[];
30
30
  }
31
- declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
32
31
  declare function envTs(userOptions?: EnvTsOptions): Plugin;
32
+ declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
33
33
  //#endregion
34
34
  export { EnvTsOptions, envTs as default, generateTypes };
35
35
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -3,33 +3,33 @@ import { Plugin } from "vite";
3
3
  //#region src/index.d.ts
4
4
  interface EnvTsOptions {
5
5
  /**
6
- * 生成 Zod schema 文件
7
- * @default 'zod'
6
+ * 是否扩展 Vite ImportMetaEnv 类型
7
+ * 开启后 import.meta.env.YOUR_VAR 自动有类型
8
+ * @default true
8
9
  */
9
- schema?: 'zod' | false;
10
+ augmentImportMeta?: boolean;
11
+ /**
12
+ * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
13
+ */
14
+ envFiles?: string[];
10
15
  /**
11
16
  * 生成文件的输出目录(相对于项目根目录)
12
17
  * @default 'src'
13
18
  */
14
19
  output?: string;
15
20
  /**
16
- * 是否扩展 Vite ImportMetaEnv 类型
17
- * 开启后 import.meta.env.YOUR_VAR 自动有类型
18
- * @default true
21
+ * 生成 Zod schema 文件
22
+ * @default 'zod'
19
23
  */
20
- augmentImportMeta?: boolean;
24
+ schema?: 'zod' | false;
21
25
  /**
22
26
  * 缺失必填变量时是否让构建失败
23
27
  * @default true
24
28
  */
25
29
  strict?: boolean;
26
- /**
27
- * 额外监听的 .env 文件(默认自动检测 .env, .env.local 等)
28
- */
29
- envFiles?: string[];
30
30
  }
31
- declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
32
31
  declare function envTs(userOptions?: EnvTsOptions): Plugin;
32
+ declare function generateTypes(envDir: string, outputDir: string, options: Required<EnvTsOptions>): Promise<void>;
33
33
  //#endregion
34
34
  export { EnvTsOptions, envTs as default, generateTypes };
35
35
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,83 +1,101 @@
1
1
  import fs from "node:fs";
2
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;
3
+ //#region src/generator.ts
4
+ function generateDts(items, options) {
5
+ const lines = [];
6
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
7
+ lines.push(`// Re-run vite to regenerate this file.`);
8
+ lines.push(``);
9
+ if (options.augmentImportMeta) {
10
+ lines.push(`/// <reference types="vite/client" />`);
11
+ lines.push(``);
12
+ lines.push(`interface ImportMetaEnv {`);
13
+ for (const { entry, inferred } of items) {
14
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
15
+ const opt = inferred.isOptional ? "?" : "";
16
+ lines.push(` readonly ${entry.key}${opt}: ${inferred.tsType}`);
27
17
  }
28
- if (raw.startsWith("#")) {
29
- pendingComments.push(raw);
30
- continue;
18
+ lines.push(`}`);
19
+ lines.push(``);
20
+ lines.push(`interface ImportMeta {`);
21
+ lines.push(` readonly env: ImportMetaEnv`);
22
+ lines.push(`}`);
23
+ } else {
24
+ lines.push(`export interface Env {`);
25
+ for (const { entry, inferred } of items) {
26
+ if (entry.annotations.description) lines.push(` /** ${entry.annotations.description} */`);
27
+ const opt = inferred.isOptional ? "?" : "";
28
+ lines.push(` ${entry.key}${opt}: ${inferred.tsType}`);
31
29
  }
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;
30
+ lines.push(`}`);
49
31
  }
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));
32
+ return lines.join("\n");
66
33
  }
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;
34
+ function generateLoader(items, options) {
35
+ const lines = [];
36
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
37
+ lines.push(``);
38
+ if (options.schema === "zod") {
39
+ lines.push(`import { envSchema } from './env.schema'`);
40
+ lines.push(``);
41
+ lines.push(`const _parsed = envSchema.safeParse(import.meta.env)`);
42
+ lines.push(``);
43
+ lines.push(`if (!_parsed.success) {`);
44
+ lines.push(` const errors = _parsed.error.flatten().fieldErrors`);
45
+ lines.push(` const msg = Object.entries(errors)`);
46
+ lines.push(` .map(([k, v]) => \` \${k}: \${(v as string[]).join(', ')}\`)`);
47
+ lines.push(` .join('\\n')`);
48
+ lines.push(` throw new Error(\`[env-ts] Invalid environment variables:\\n\${msg}\`)`);
49
+ lines.push(`}`);
50
+ lines.push(``);
51
+ lines.push(`export const env = _parsed.data`);
52
+ lines.push(`export default env`);
53
+ } else {
54
+ const required = items.filter(({ inferred }) => !inferred.isOptional);
55
+ lines.push(`const _raw = import.meta.env`);
56
+ lines.push(``);
57
+ if (required.length > 0) {
58
+ lines.push(`const _required = [${required.map(({ entry }) => `'${entry.key}'`).join(", ")}] as const`);
59
+ lines.push(`for (const key of _required) {`);
60
+ lines.push(` if (!_raw[key]) throw new Error(\`[env-ts] Missing required env var: \${key}\`)`);
61
+ lines.push(`}`);
62
+ lines.push(``);
63
+ }
64
+ lines.push(`export const env = _raw as import('./env').Env`);
65
+ lines.push(`export default env`);
73
66
  }
67
+ return lines.join("\n");
74
68
  }
75
- function isNumberArray(v) {
76
- if (!v.includes(",")) return false;
77
- return v.split(",").every((s) => isNumber(s.trim()));
69
+ function generateZodSchema(items) {
70
+ const lines = [];
71
+ lines.push(`// Auto-generated by vite-plugin-typed-env. DO NOT EDIT.`);
72
+ lines.push(`import { z } from 'zod'`);
73
+ lines.push(``);
74
+ lines.push(`export const envSchema = z.object({`);
75
+ for (const { entry, inferred } of items) {
76
+ if (entry.annotations.description) lines.push(` // ${entry.annotations.description}`);
77
+ lines.push(` ${entry.key}: ${inferred.zodSchema},`);
78
+ }
79
+ lines.push(`})`);
80
+ lines.push(``);
81
+ lines.push(`export type Env = z.infer<typeof envSchema>`);
82
+ return lines.join("\n");
78
83
  }
79
- function isStringArray(v) {
80
- return v.includes(",") && v.split(",").length > 1;
84
+ //#endregion
85
+ //#region src/inferrer.ts
86
+ function inferType(entry) {
87
+ const { annotations, value } = entry;
88
+ const isOptional = annotations.optional === true || value === "";
89
+ const base = (annotations.type ? inferFromAnnotation(annotations.type) : null) ?? inferFromValue(value);
90
+ let zodSchema = base.zodSchema;
91
+ if (annotations.default !== void 0) zodSchema = `${zodSchema}.default('${annotations.default}')`;
92
+ else if (isOptional) zodSchema = `${zodSchema}.optional()`;
93
+ return {
94
+ defaultValue: annotations.default,
95
+ isOptional,
96
+ tsType: base.tsType,
97
+ zodSchema
98
+ };
81
99
  }
82
100
  function inferFromAnnotation(ann) {
83
101
  const enumMatch = ann.match(/^enum\((.+)\)$/);
@@ -148,104 +166,117 @@ function inferFromValue(value) {
148
166
  zodSchema: "z.string().min(1)"
149
167
  };
150
168
  }
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
- };
169
+ function isBoolean(v) {
170
+ return [
171
+ "0",
172
+ "1",
173
+ "false",
174
+ "no",
175
+ "true",
176
+ "yes"
177
+ ].includes(v.toLowerCase());
178
+ }
179
+ function isNumber(v) {
180
+ return v !== "" && !isNaN(Number(v));
181
+ }
182
+ function isNumberArray(v) {
183
+ if (!v.includes(",")) return false;
184
+ return v.split(",").every((s) => isNumber(s.trim()));
185
+ }
186
+ function isStringArray(v) {
187
+ return v.includes(",") && v.split(",").length > 1;
188
+ }
189
+ function isUrl(v) {
190
+ try {
191
+ new URL(v);
192
+ return v.startsWith("http://") || v.startsWith("https://") || v.includes("://");
193
+ } catch {
194
+ return false;
195
+ }
164
196
  }
165
197
  //#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}`);
198
+ //#region src/parser.ts
199
+ const ANNOTATION_RE = /^#\s*@(\w+)(?::\s*(.+))?$/;
200
+ function parseEnvFile(content) {
201
+ const lines = content.split("\n");
202
+ const entries = [];
203
+ const pendingComments = [];
204
+ for (let i = 0; i < lines.length; i++) {
205
+ const raw = lines[i].trim();
206
+ if (raw === "") {
207
+ pendingComments.length = 0;
208
+ continue;
180
209
  }
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}`);
210
+ if (raw.startsWith("#")) {
211
+ pendingComments.push(raw);
212
+ continue;
192
213
  }
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},`);
214
+ const eqIdx = raw.indexOf("=");
215
+ if (eqIdx === -1) continue;
216
+ const key = raw.slice(0, eqIdx).trim();
217
+ let value = raw.slice(eqIdx + 1).trim();
218
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
219
+ const annotations = parseAnnotations(pendingComments);
220
+ const description = pendingComments.filter((l) => !ANNOTATION_RE.test(l)).map((l) => l.replace(/^#\s*/, "")).join(" ").trim();
221
+ entries.push({
222
+ annotations: {
223
+ ...annotations,
224
+ description: description || void 0
225
+ },
226
+ comment: "",
227
+ key,
228
+ value
229
+ });
230
+ pendingComments.length = 0;
206
231
  }
207
- lines.push(`})`);
208
- lines.push(``);
209
- lines.push(`export type Env = z.infer<typeof envSchema>`);
210
- return lines.join("\n");
232
+ return entries;
211
233
  }
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`);
234
+ function parseAnnotations(lines) {
235
+ const ann = {};
236
+ for (const line of lines) {
237
+ const m = line.match(ANNOTATION_RE);
238
+ if (!m) continue;
239
+ const [, key, value] = m;
240
+ if (key === "optional") ann.optional = true;
241
+ else if (key === "type" && value) ann.type = value.trim();
242
+ else if (key === "default" && value) ann.default = value.trim();
243
+ else if (key === "desc" && value) ann.description = value.trim();
244
244
  }
245
- return lines.join("\n");
245
+ return ann;
246
246
  }
247
247
  //#endregion
248
248
  //#region src/index.ts
249
+ function envTs(userOptions = {}) {
250
+ const options = {
251
+ augmentImportMeta: true,
252
+ envFiles: [],
253
+ output: "src",
254
+ schema: "zod",
255
+ strict: true,
256
+ ...userOptions
257
+ };
258
+ let config;
259
+ let outputDir;
260
+ return {
261
+ async buildStart() {
262
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
263
+ },
264
+ configResolved(resolvedConfig) {
265
+ config = resolvedConfig;
266
+ outputDir = path.resolve(config.root, options.output);
267
+ },
268
+ enforce: "pre",
269
+ async handleHotUpdate({ file, server }) {
270
+ if (!file) return;
271
+ const fileName = path.basename(file);
272
+ if (!(fileName.startsWith(".env") || options.envFiles.includes(file))) return;
273
+ console.log(`[env-ts] Detected change in ${fileName}, regenerating...`);
274
+ await generateTypes(config.envDir === false ? config.root : config.envDir, outputDir, options);
275
+ server.hot.send({ type: "full-reload" });
276
+ },
277
+ name: "vite-plugin-typed-env"
278
+ };
279
+ }
249
280
  async function generateTypes(envDir, outputDir, options) {
250
281
  const envFileNames = [
251
282
  ".env",
@@ -282,8 +313,8 @@ async function generateTypes(envDir, outputDir, options) {
282
313
  }
283
314
  fs.mkdirSync(outputDir, { recursive: true });
284
315
  const genOptions = {
285
- schema: options.schema,
286
- augmentImportMeta: options.augmentImportMeta
316
+ augmentImportMeta: options.augmentImportMeta,
317
+ schema: options.schema
287
318
  };
288
319
  const dtsContent = generateDts(items, genOptions);
289
320
  writeIfChanged(path.join(outputDir, "env.d.ts"), dtsContent);
@@ -301,37 +332,6 @@ function writeIfChanged(filePath, content) {
301
332
  }
302
333
  fs.writeFileSync(filePath, content, "utf-8");
303
334
  }
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
335
  //#endregion
336
336
  export { envTs as default, generateTypes };
337
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-typed-env",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -23,22 +23,27 @@
23
23
  "dist/*.d.cts",
24
24
  "dist/*.d.mts"
25
25
  ],
26
- "scripts": {
27
- "build": "tsdown",
28
- "typecheck": "tsc --noEmit",
29
- "test": "vitest run",
30
- "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.ts\" \"*.cjs\" \"*.json\" \"*.md\"",
31
- "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.ts\" \"*.cjs\" \"*.json\" \"*.md\""
32
- },
33
26
  "devDependencies": {
34
27
  "@types/node": "^25.5.2",
35
- "prettier": "^3.8.2",
36
28
  "tsdown": "^0.21.7",
37
29
  "typescript": "^6.0.2",
38
30
  "vite": "^8.0.8",
39
31
  "vitest": "^4.1.4"
40
32
  },
41
33
  "peerDependencies": {
42
- "vite": ">=4.0.0"
34
+ "vite": ">=4.0.0",
35
+ "zod": ">=3.0.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "zod": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "dev": "tsdown --watch",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest"
43
48
  }
44
- }
49
+ }