@@ -5,7 +5,7 @@ import { homedir } from 'node:os'
55import { join , resolve } from 'node:path'
66import process from 'node:process'
77import { consola } from 'consola'
8- import { loadFile , writeFile } from 'magicast'
8+ import { builders , loadFile , writeFile } from 'magicast'
99import { getDefaultExportOptions } from 'magicast/helpers'
1010import { $fetch } from 'ofetch'
1111import { 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 */
100111function 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
183194function findNuxtConfig ( rootDir : string ) : string | null {
@@ -191,7 +202,7 @@ function findNuxtConfig(rootDir: string): string | null {
191202}
192203
193204interface 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 ( / ^ ( i m p o r t [ \s \S ] * ?(?: \n (? ! i m p o r t ) | \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 ( / ^ ( i m p o r t [ \s \S ] * ?(?: \n (? ! i m p o r t ) | \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
0 commit comments