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/src/inferrer.ts DELETED
@@ -1,165 +0,0 @@
1
- import type { EnvEntry } from './parser'
2
-
3
- export interface InferredType {
4
- tsType: string // TypeScript 类型字符串
5
- zodSchema: string // Zod schema 字符串
6
- isOptional: boolean
7
- defaultValue?: string
8
- }
9
-
10
- // 判断是否是布尔值
11
- function isBoolean(v: string): boolean {
12
- return ['true', 'false', '1', '0', 'yes', 'no'].includes(v.toLowerCase())
13
- }
14
-
15
- // 判断是否是纯数字
16
- function isNumber(v: string): boolean {
17
- return v !== '' && !isNaN(Number(v))
18
- }
19
-
20
- // 判断是否是 URL
21
- function isUrl(v: string): boolean {
22
- try {
23
- new URL(v)
24
- return v.startsWith('http://') || v.startsWith('https://') || v.includes('://') // postgres://, redis:// 等
25
- } catch {
26
- return false
27
- }
28
- }
29
-
30
- // 判断是否是逗号分隔的数字列表
31
- function isNumberArray(v: string): boolean {
32
- if (!v.includes(',')) return false
33
- return v.split(',').every((s) => isNumber(s.trim()))
34
- }
35
-
36
- // 判断是否是逗号分隔的字符串列表
37
- function isStringArray(v: string): boolean {
38
- return v.includes(',') && v.split(',').length > 1
39
- }
40
-
41
- // 解析 @type 注释指令
42
- function inferFromAnnotation(ann: string): Pick<InferredType, 'tsType' | 'zodSchema'> | null {
43
- // @type: enum(a, b, c)
44
- const enumMatch = ann.match(/^enum\((.+)\)$/)
45
- if (enumMatch) {
46
- const values = enumMatch[1].split(',').map((s) => s.trim())
47
- const tsLiterals = values.map((v) => `'${v}'`).join(' | ')
48
- const zodValues = values.map((v) => `'${v}'`).join(', ')
49
- return {
50
- tsType: tsLiterals,
51
- zodSchema: `z.enum([${zodValues}])`
52
- }
53
- }
54
-
55
- // @type: number[]
56
- if (ann === 'number[]') {
57
- return {
58
- tsType: 'number[]',
59
- zodSchema: `z.string().transform(v => v.split(',').map(Number))`
60
- }
61
- }
62
-
63
- // @type: string[]
64
- if (ann === 'string[]') {
65
- return {
66
- tsType: 'string[]',
67
- zodSchema: `z.string().transform(v => v.split(','))`
68
- }
69
- }
70
-
71
- // @type: url
72
- if (ann === 'url') {
73
- return { tsType: 'string', zodSchema: `z.string().url()` }
74
- }
75
-
76
- // @type: number
77
- if (ann === 'number') {
78
- return { tsType: 'number', zodSchema: `z.coerce.number()` }
79
- }
80
-
81
- // @type: boolean
82
- if (ann === 'boolean') {
83
- return {
84
- tsType: 'boolean',
85
- zodSchema: `z.enum(['true','false','1','0']).transform(v => v === 'true' || v === '1')`
86
- }
87
- }
88
-
89
- // @type: port
90
- if (ann === 'port') {
91
- return { tsType: 'number', zodSchema: `z.coerce.number().int().min(1).max(65535)` }
92
- }
93
-
94
- // @type: email
95
- if (ann === 'email') {
96
- return { tsType: 'string', zodSchema: `z.string().email()` }
97
- }
98
-
99
- return null
100
- }
101
-
102
- // 从值自动推断类型
103
- function inferFromValue(value: string): Pick<InferredType, 'tsType' | 'zodSchema'> {
104
- if (value === '') {
105
- return { tsType: 'string', zodSchema: 'z.string()' }
106
- }
107
-
108
- if (isBoolean(value)) {
109
- return {
110
- tsType: 'boolean',
111
- zodSchema: `z.enum(['true','false','1','0','yes','no']).transform(v => ['true','1','yes'].includes(v.toLowerCase()))`
112
- }
113
- }
114
-
115
- if (isNumber(value)) {
116
- // 区分整数和浮点
117
- const schema = Number.isInteger(Number(value)) ? 'z.coerce.number().int()' : 'z.coerce.number()'
118
- return { tsType: 'number', zodSchema: schema }
119
- }
120
-
121
- if (isNumberArray(value)) {
122
- return {
123
- tsType: 'number[]',
124
- zodSchema: `z.string().transform(v => v.split(',').map(Number))`
125
- }
126
- }
127
-
128
- if (isUrl(value)) {
129
- return { tsType: 'string', zodSchema: 'z.string().url()' }
130
- }
131
-
132
- if (isStringArray(value)) {
133
- return {
134
- tsType: 'string[]',
135
- zodSchema: `z.string().transform(v => v.split(',').map(s => s.trim()))`
136
- }
137
- }
138
-
139
- return { tsType: 'string', zodSchema: 'z.string().min(1)' }
140
- }
141
-
142
- export function inferType(entry: EnvEntry): InferredType {
143
- const { value, annotations } = entry
144
- const isOptional = annotations.optional === true || value === ''
145
-
146
- // 优先使用 @type 注释
147
- const fromAnnotation = annotations.type ? inferFromAnnotation(annotations.type) : null
148
-
149
- const base = fromAnnotation ?? inferFromValue(value)
150
-
151
- let zodSchema = base.zodSchema
152
- // 处理 optional 和 default
153
- if (annotations.default !== undefined) {
154
- zodSchema = `${zodSchema}.default('${annotations.default}')`
155
- } else if (isOptional) {
156
- zodSchema = `${zodSchema}.optional()`
157
- }
158
-
159
- return {
160
- tsType: base.tsType,
161
- zodSchema,
162
- isOptional,
163
- defaultValue: annotations.default
164
- }
165
- }
package/src/parser.ts DELETED
@@ -1,72 +0,0 @@
1
- export interface EnvEntry {
2
- key: string
3
- value: string
4
- annotations: Annotations
5
- comment: string
6
- }
7
-
8
- export interface Annotations {
9
- type?: string // @type: url | enum(a,b,c) | number[] ...
10
- optional?: boolean // @optional
11
- default?: string // @default: foo
12
- description?: string // @desc: some description
13
- }
14
-
15
- const ANNOTATION_RE = /^#\s*@(\w+)(?::\s*(.+))?$/
16
-
17
- function parseAnnotations(lines: string[]): Annotations {
18
- const ann: Annotations = {}
19
- for (const line of lines) {
20
- const m = line.match(ANNOTATION_RE)
21
- if (!m) continue
22
- const [, key, value] = m
23
- if (key === 'optional') ann.optional = true
24
- else if (key === 'type' && value) ann.type = value.trim()
25
- else if (key === 'default' && value) ann.default = value.trim()
26
- else if (key === 'desc' && value) ann.description = value.trim()
27
- }
28
- return ann
29
- }
30
-
31
- export function parseEnvFile(content: string): EnvEntry[] {
32
- const lines = content.split('\n')
33
- const entries: EnvEntry[] = []
34
- const pendingComments: string[] = []
35
-
36
- for (let i = 0; i < lines.length; i++) {
37
- const raw = lines[i].trim()
38
-
39
- if (raw === '') {
40
- pendingComments.length = 0
41
- continue
42
- }
43
-
44
- if (raw.startsWith('#')) {
45
- pendingComments.push(raw)
46
- continue
47
- }
48
-
49
- const eqIdx = raw.indexOf('=')
50
- if (eqIdx === -1) continue
51
-
52
- const key = raw.slice(0, eqIdx).trim()
53
- let value = raw.slice(eqIdx + 1).trim()
54
-
55
- // 去除引号
56
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
57
- value = value.slice(1, -1)
58
- }
59
-
60
- const annotations = parseAnnotations(pendingComments)
61
- const description = pendingComments
62
- .filter((l) => !ANNOTATION_RE.test(l))
63
- .map((l) => l.replace(/^#\s*/, ''))
64
- .join(' ')
65
- .trim()
66
-
67
- entries.push({ key, value, annotations: { ...annotations, description: description || undefined }, comment: '' })
68
- pendingComments.length = 0
69
- }
70
-
71
- return entries
72
- }
@@ -1,154 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { parseEnvFile } from '../src/parser.js'
3
- import { inferType } from '../src/inferrer.js'
4
- import { generateDts, generateZodSchema } from '../src/generator.js'
5
-
6
- // ─── Parser ──────────────────────────────────────────────────────────────────
7
-
8
- describe('parseEnvFile', () => {
9
- it('parses basic key=value', () => {
10
- const result = parseEnvFile('PORT=3000\nHOST=localhost')
11
- expect(result).toHaveLength(2)
12
- expect(result[0]).toMatchObject({ key: 'PORT', value: '3000' })
13
- expect(result[1]).toMatchObject({ key: 'HOST', value: 'localhost' })
14
- })
15
-
16
- it('strips quotes', () => {
17
- const result = parseEnvFile(`DB_URL="postgres://localhost/mydb"`)
18
- expect(result[0].value).toBe('postgres://localhost/mydb')
19
- })
20
-
21
- it('parses @type annotation', () => {
22
- const result = parseEnvFile(`# @type: enum(info, warn, error)\nLOG_LEVEL=info`)
23
- expect(result[0].annotations.type).toBe('enum(info, warn, error)')
24
- })
25
-
26
- it('parses @optional annotation', () => {
27
- const result = parseEnvFile(`# @optional\nSENTRY_DSN=`)
28
- expect(result[0].annotations.optional).toBe(true)
29
- })
30
-
31
- it('parses @default annotation', () => {
32
- const result = parseEnvFile(`# @default: 3000\nPORT=`)
33
- expect(result[0].annotations.default).toBe('3000')
34
- })
35
-
36
- it('ignores empty lines and resets comment buffer', () => {
37
- const result = parseEnvFile(`# @optional\n\nPORT=3000`)
38
- expect(result[0].annotations.optional).toBeUndefined()
39
- })
40
-
41
- it('parses description comment', () => {
42
- const result = parseEnvFile(`# Database connection string\nDATABASE_URL=postgres://localhost/db`)
43
- expect(result[0].annotations.description).toBe('Database connection string')
44
- })
45
- })
46
-
47
- // ─── Inferrer ────────────────────────────────────────────────────────────────
48
-
49
- describe('inferType', () => {
50
- const make = (value: string, annType?: string, optional?: boolean) =>
51
- inferType({
52
- key: 'TEST',
53
- value,
54
- comment: '',
55
- annotations: { type: annType, optional }
56
- })
57
-
58
- it('infers number from numeric string', () => {
59
- const r = make('3000')
60
- expect(r.tsType).toBe('number')
61
- expect(r.zodSchema).toContain('z.coerce.number()')
62
- })
63
-
64
- it('infers boolean from true/false', () => {
65
- const r = make('true')
66
- expect(r.tsType).toBe('boolean')
67
- })
68
-
69
- it('infers string for plain string', () => {
70
- const r = make('hello')
71
- expect(r.tsType).toBe('string')
72
- })
73
-
74
- it('infers url type for http URLs', () => {
75
- const r = make('https://api.example.com')
76
- expect(r.zodSchema).toContain('.url()')
77
- })
78
-
79
- it('infers number[] for comma-separated numbers', () => {
80
- const r = make('1,2,3')
81
- expect(r.tsType).toBe('number[]')
82
- })
83
-
84
- it('infers string[] for comma-separated strings', () => {
85
- const r = make('a,b,c')
86
- expect(r.tsType).toBe('string[]')
87
- })
88
-
89
- it('respects @type: enum annotation', () => {
90
- const r = make('info', 'enum(info, warn, error)')
91
- expect(r.tsType).toBe("'info' | 'warn' | 'error'")
92
- expect(r.zodSchema).toContain("z.enum(['info', 'warn', 'error'])")
93
- })
94
-
95
- it('respects @type: url annotation', () => {
96
- const r = make('not-a-url', 'url')
97
- expect(r.zodSchema).toContain('.url()')
98
- })
99
-
100
- it('marks empty value as optional', () => {
101
- const r = make('')
102
- expect(r.isOptional).toBe(true)
103
- })
104
-
105
- it('marks @optional annotated entry as optional', () => {
106
- const r = make('value', undefined, true)
107
- expect(r.isOptional).toBe(true)
108
- expect(r.zodSchema).toContain('.optional()')
109
- })
110
- })
111
-
112
- // ─── Generator ───────────────────────────────────────────────────────────────
113
-
114
- describe('generateDts', () => {
115
- const items = [
116
- {
117
- entry: { key: 'PORT', value: '3000', comment: '', annotations: { description: 'Server port' } },
118
- inferred: { tsType: 'number', zodSchema: 'z.coerce.number()', isOptional: false }
119
- },
120
- {
121
- entry: { key: 'SENTRY_DSN', value: '', comment: '', annotations: { optional: true } },
122
- inferred: { tsType: 'string', zodSchema: 'z.string().optional()', isOptional: true }
123
- }
124
- ]
125
-
126
- it('generates ImportMetaEnv augmentation', () => {
127
- const output = generateDts(items, { schema: 'zod', augmentImportMeta: true })
128
- expect(output).toContain('interface ImportMetaEnv')
129
- expect(output).toContain('readonly PORT: number')
130
- expect(output).toContain('readonly SENTRY_DSN?: string')
131
- })
132
-
133
- it('includes JSDoc from description', () => {
134
- const output = generateDts(items, { schema: 'zod', augmentImportMeta: true })
135
- expect(output).toContain('/** Server port */')
136
- })
137
- })
138
-
139
- describe('generateZodSchema', () => {
140
- const items = [
141
- {
142
- entry: { key: 'DATABASE_URL', value: 'postgres://localhost/db', comment: '', annotations: {} },
143
- inferred: { tsType: 'string', zodSchema: 'z.string().url()', isOptional: false }
144
- }
145
- ]
146
-
147
- it('generates valid zod schema', () => {
148
- const output = generateZodSchema(items)
149
- expect(output).toContain("import { z } from 'zod'")
150
- expect(output).toContain('export const envSchema = z.object({')
151
- expect(output).toContain('DATABASE_URL: z.string().url()')
152
- expect(output).toContain('export type Env = z.infer<typeof envSchema>')
153
- })
154
- })
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "esnext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "declaration": true,
8
- "outDir": "dist",
9
- "rootDir": "src"
10
- },
11
- "include": ["src"]
12
- }
package/tsdown.config.ts DELETED
@@ -1,9 +0,0 @@
1
- import { defineConfig } from 'tsdown'
2
-
3
- export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['esm', 'cjs'],
6
- dts: true,
7
- clean: true,
8
- sourcemap: true
9
- })