Skip to content

Commit 46307d8

Browse files
committed
fix: inline schema + strip NUXT_ prefix + multiline
1 parent 194efef commit 46307d8

File tree

2 files changed

+66
-51
lines changed

2 files changed

+66
-51
lines changed

src/wizard/shelve-setup.ts

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { homedir } from 'node:os'
55
import { join, resolve } from 'node:path'
66
import process from 'node:process'
77
import { consola } from 'consola'
8-
import { loadFile, writeFile } from 'magicast'
8+
import { builders, loadFile, writeFile } from 'magicast'
99
import { getDefaultExportOptions } from 'magicast/helpers'
1010
import { $fetch } from 'ofetch'
1111
import { getPrefix, toCamelCase } from '../utils/transform'
@@ -96,41 +96,54 @@ function countPrefixes(keys: string[]): Map<string, number> {
9696
return counts
9797
}
9898

99+
/** Strip Nuxt-specific prefixes from env var keys */
100+
function stripNuxtPrefix(key: string): { key: string, isPublic: boolean } {
101+
if (key.startsWith('NUXT_PUBLIC_'))
102+
return { key: key.slice(12), isPublic: true }
103+
if (key.startsWith('PUBLIC_'))
104+
return { key: key.slice(7), isPublic: true }
105+
if (key.startsWith('NUXT_'))
106+
return { key: key.slice(5), isPublic: false }
107+
return { key, isPublic: false }
108+
}
109+
99110
/** Transform env var keys into nested runtimeConfig structure */
100111
function transformKeysToStructure(keys: string[]): Record<string, Record<string, true> | true> {
101-
const prefixCounts = countPrefixes(keys)
112+
// First, normalize keys by stripping NUXT_ prefixes
113+
const normalizedKeys = keys.map((k) => {
114+
const { key, isPublic } = stripNuxtPrefix(k)
115+
return { original: k, normalized: key, isPublic }
116+
})
117+
118+
// Count prefixes on normalized keys
119+
const prefixCounts = countPrefixes(normalizedKeys.map(k => k.normalized))
102120
const result: Record<string, Record<string, true> | true> = {}
103121

104-
for (const key of keys) {
105-
if (key.startsWith('PUBLIC_')) {
106-
result.public ??= {}
107-
assignNestedKey(result.public as Record<string, true>, key.slice(7), prefixCounts, 'PUBLIC_')
108-
continue
109-
}
110-
if (key.startsWith('NUXT_PUBLIC_')) {
122+
for (const { normalized, isPublic } of normalizedKeys) {
123+
if (isPublic) {
111124
result.public ??= {}
112-
assignNestedKey(result.public as Record<string, true>, key.slice(12), prefixCounts, 'NUXT_PUBLIC_')
125+
assignNestedKey(result.public as Record<string, true>, normalized, prefixCounts)
113126
continue
114127
}
115128

116-
const prefix = getPrefix(key)
129+
const prefix = getPrefix(normalized)
117130
if (prefix && (prefixCounts.get(prefix) || 0) >= 2) {
118131
const groupKey = toCamelCase(prefix)
119132
result[groupKey] ??= {}
120-
const nestedKey = toCamelCase(key.slice(prefix.length + 1))
133+
const nestedKey = toCamelCase(normalized.slice(prefix.length + 1))
121134
;(result[groupKey] as Record<string, true>)[nestedKey] = true
122135
}
123136
else {
124-
result[toCamelCase(key)] = true
137+
result[toCamelCase(normalized)] = true
125138
}
126139
}
127140

128141
return result
129142
}
130143

131-
function assignNestedKey(obj: Record<string, true | Record<string, true>>, key: string, prefixCounts: Map<string, number>, publicPrefix: string): void {
144+
function assignNestedKey(obj: Record<string, true | Record<string, true>>, key: string, prefixCounts: Map<string, number>): void {
132145
const prefix = getPrefix(key)
133-
if (prefix && (prefixCounts.get(`${publicPrefix}${prefix}`) || 0) >= 2) {
146+
if (prefix && (prefixCounts.get(prefix) || 0) >= 2) {
134147
const groupKey = toCamelCase(prefix)
135148
obj[groupKey] ??= {}
136149
const rest = key.slice(prefix.length + 1)
@@ -142,42 +155,40 @@ function assignNestedKey(obj: Record<string, true | Record<string, true>>, key:
142155
}
143156
}
144157

145-
function generateSchemaCode(keys: string[] | null, library: ValidationLibrary): { imports: string, schema: string } {
158+
function generateSchemaCode(keys: string[] | null, library: ValidationLibrary): { imports: string, schemaExpr: string } {
159+
const s = library === 'valibot' ? 'v.string()' : 'z.string()'
160+
const o = library === 'valibot' ? 'v.object' : 'z.object'
161+
const imports = library === 'valibot' ? `import * as v from 'valibot'` : `import { z } from 'zod'`
162+
146163
if (!keys || keys.length === 0) {
147-
// Dummy schema
148-
if (library === 'valibot') {
149-
return {
150-
imports: `import * as v from 'valibot'`,
151-
schema: `const runtimeConfigSchema = v.object({\n // Add your runtime config keys here\n // example: apiKey: v.string(),\n})`,
152-
}
153-
}
154-
return {
155-
imports: `import { z } from 'zod'`,
156-
schema: `const runtimeConfigSchema = z.object({\n // Add your runtime config keys here\n // example: apiKey: z.string(),\n})`,
157-
}
164+
// Placeholder schema
165+
return { imports, schemaExpr: `${o}({})` }
158166
}
159167

160168
const structure = transformKeysToStructure(keys)
161-
const s = library === 'valibot' ? 'v.string()' : 'z.string()'
162-
const o = library === 'valibot' ? 'v.object' : 'z.object'
163169

164170
function renderObject(obj: Record<string, true | Record<string, true>>, indent = 2): string {
165171
const entries = Object.entries(obj)
166172
if (entries.length === 0)
167173
return '{}'
168174
const pad = ' '.repeat(indent)
175+
const closePad = ' '.repeat(indent - 2)
169176
const lines = entries.map(([k, v]) => {
170177
if (v === true)
171178
return `${pad}${k}: ${s},`
172-
return `${pad}${k}: ${o}({ ${Object.keys(v).map(nk => `${nk}: ${s}`).join(', ')} }),`
179+
// Nested object - render inline if small, multiline if large
180+
const nestedKeys = Object.keys(v)
181+
if (nestedKeys.length <= 2) {
182+
return `${pad}${k}: ${o}({ ${nestedKeys.map(nk => `${nk}: ${s}`).join(', ')} }),`
183+
}
184+
const nestedPad = ' '.repeat(indent + 2)
185+
const nestedLines = nestedKeys.map(nk => `${nestedPad}${nk}: ${s},`)
186+
return `${pad}${k}: ${o}({\n${nestedLines.join('\n')}\n${pad}}),`
173187
})
174-
return `{\n${lines.join('\n')}\n${' '.repeat(indent - 2)}}`
188+
return `{\n${lines.join('\n')}\n${closePad}}`
175189
}
176190

177-
const imports = library === 'valibot' ? `import * as v from 'valibot'` : `import { z } from 'zod'`
178-
const schema = `const runtimeConfigSchema = ${o}(${renderObject(structure)})`
179-
180-
return { imports, schema }
191+
return { imports, schemaExpr: `${o}(${renderObject(structure)})` }
181192
}
182193

183194
function findNuxtConfig(rootDir: string): string | null {
@@ -191,7 +202,7 @@ function findNuxtConfig(rootDir: string): string | null {
191202
}
192203

193204
interface UpdateNuxtConfigOptions {
194-
schema: { imports: string, schema: string } | null
205+
schema: { imports: string, schemaExpr: string } | null
195206
shelve: { project: string, slug: string } | null
196207
logger: ConsolaInstance
197208
}
@@ -205,7 +216,7 @@ async function updateNuxtConfig(rootDir: string, options: UpdateNuxtConfigOption
205216
}
206217

207218
try {
208-
// If we have a schema, we need to manually edit the file to add imports and the schema variable
219+
// If we have a schema, add imports at the top of the file
209220
if (options.schema) {
210221
let content = readFileSync(configPath, 'utf-8')
211222

@@ -214,30 +225,33 @@ async function updateNuxtConfig(rootDir: string, options: UpdateNuxtConfigOption
214225
logger.info('Schema already exists in nuxt.config, skipping schema generation')
215226
}
216227
else {
217-
// Add imports at the top (after any existing imports)
218-
const importMatch = content.match(/^(import[\s\S]*?(?:\n(?!import)|\n$))/m)
219-
if (importMatch) {
220-
const lastImportEnd = importMatch.index! + importMatch[0].length
221-
content = `${content.slice(0, lastImportEnd)}\n${options.schema.imports}\n\n${options.schema.schema}\n${content.slice(lastImportEnd)}`
222-
}
223-
else {
224-
// No imports, add at the very top
225-
content = `${options.schema.imports}\n\n${options.schema.schema}\n\n${content}`
228+
// Check if import already exists
229+
const importStatement = options.schema.imports
230+
if (!content.includes(importStatement)) {
231+
// Add imports at the top (after any existing imports)
232+
const importMatch = content.match(/^(import[\s\S]*?(?:\n(?!import)|\n$))/m)
233+
if (importMatch) {
234+
const lastImportEnd = importMatch.index! + importMatch[0].length
235+
content = `${content.slice(0, lastImportEnd)}${importStatement}\n${content.slice(lastImportEnd)}`
236+
}
237+
else {
238+
// No imports, add at the very top
239+
content = `${importStatement}\n\n${content}`
240+
}
241+
writeFileSync(configPath, content)
226242
}
227-
228-
writeFileSync(configPath, content)
229243
}
230244
}
231245

232246
// Use magicast to update the config object
233247
const mod = await loadFile(configPath)
234248
const config = getDefaultExportOptions(mod)
235249

236-
// Add $schema reference if we generated a schema
250+
// Add $schema as inline expression
237251
if (options.schema && !config.safeRuntimeConfig?.$schema) {
238252
if (!config.safeRuntimeConfig)
239253
config.safeRuntimeConfig = {}
240-
;(config.safeRuntimeConfig as any).$schema = { $type: 'identifier', value: 'runtimeConfigSchema' }
254+
;(config.safeRuntimeConfig as any).$schema = builders.raw(options.schema.schemaExpr)
241255
}
242256

243257
// Set shelve config if provided

test/wizard.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ describe('shelve wizard', () => {
111111
expect(mockFetch).not.toHaveBeenCalled()
112112
const config = readFileSync(nuxtConfig, 'utf-8')
113113
expect(config).toContain('valibot') // schema import
114-
expect(config).toContain('runtimeConfigSchema')
114+
expect(config).toContain('$schema') // inline schema
115+
expect(config).toContain('v.object') // valibot schema expression
115116
expect(config).not.toContain('shelve') // no shelve config
116117
})
117118

0 commit comments

Comments
 (0)