Skip to content

Commit 5205c20

Browse files
committed
feat(create): prompt and seed add-on environment variables
1 parent 6ebfce6 commit 5205c20

7 files changed

Lines changed: 139 additions & 0 deletions

File tree

packages/cli/src/command-line.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ export async function normalizeOptions(
231231

232232
;(normalized as Options & { includeExamples?: boolean }).includeExamples =
233233
includeExamples
234+
;(normalized as Options & { envVarValues?: Record<string, string> }).envVarValues =
235+
{}
234236

235237
return normalized
236238
}

packages/cli/src/options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import {
1212
getProjectName,
1313
promptForAddOnOptions,
14+
promptForEnvVars,
1415
selectAddOns,
1516
selectDeployment,
1617
selectExamples,
@@ -169,6 +170,13 @@ export async function promptForCreateOptions(
169170
options.addOnOptions = { ...defaultOptions, ...userOptions }
170171
}
171172

173+
// Prompt for env vars exposed by selected add-ons in interactive mode
174+
const envVarValues = Array.isArray(cliOptions.addOns)
175+
? {}
176+
: await promptForEnvVars(options.chosenAddOns)
177+
;(options as Required<Options> & { envVarValues?: Record<string, string> }).envVarValues =
178+
envVarValues
179+
172180
options.git = cliOptions.git ?? (await selectGit())
173181
if (cliOptions.install === false) {
174182
options.install = false

packages/cli/src/ui-prompts.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isCancel,
55
multiselect,
66
note,
7+
password,
78
select,
89
text,
910
} from '@clack/prompts'
@@ -251,6 +252,69 @@ export async function promptForAddOnOptions(
251252
return addOnOptions
252253
}
253254

255+
export async function promptForEnvVars(
256+
addOns: Array<AddOn>,
257+
): Promise<Record<string, string>> {
258+
const envVars = new Map<
259+
string,
260+
{
261+
name: string
262+
description?: string
263+
required?: boolean
264+
default?: string
265+
secret?: boolean
266+
}
267+
>()
268+
269+
for (const addOn of addOns as Array<any>) {
270+
for (const envVar of addOn.envVars || []) {
271+
if (!envVars.has(envVar.name)) {
272+
envVars.set(envVar.name, envVar)
273+
}
274+
}
275+
}
276+
277+
const result: Record<string, string> = {}
278+
279+
for (const envVar of envVars.values()) {
280+
const label = envVar.description
281+
? `${envVar.name} (${envVar.description})`
282+
: envVar.name
283+
284+
const value = envVar.secret
285+
? await password({
286+
message: `Enter ${label}`,
287+
validate: envVar.required
288+
? (v) =>
289+
v && v.trim().length > 0
290+
? undefined
291+
: `${envVar.name} is required`
292+
: undefined,
293+
})
294+
: await text({
295+
message: `Enter ${label}`,
296+
defaultValue: envVar.default,
297+
validate: envVar.required
298+
? (v) =>
299+
v && v.trim().length > 0
300+
? undefined
301+
: `${envVar.name} is required`
302+
: undefined,
303+
})
304+
305+
if (isCancel(value)) {
306+
cancel('Operation cancelled.')
307+
process.exit(0)
308+
}
309+
310+
if (value && value.trim()) {
311+
result[envVar.name] = value.trim()
312+
}
313+
}
314+
315+
return result
316+
}
317+
254318
export async function selectDeployment(
255319
framework: Framework,
256320
deployment?: string,

packages/create/src/create-app.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,32 @@ async function runCommandsAndInstallDependencies(
280280
await installShadcnComponents(environment, options.targetDir, options)
281281
}
282282

283+
async function seedEnvValues(environment: Environment, options: Options) {
284+
const envVarValues = options.envVarValues || {}
285+
const entries = Object.entries(envVarValues)
286+
if (entries.length === 0) return
287+
288+
const envLocalPath = resolve(options.targetDir, '.env.local')
289+
if (!environment.exists(envLocalPath)) {
290+
return
291+
}
292+
293+
let envContents = await environment.readFile(envLocalPath)
294+
for (const [key, value] of entries) {
295+
const escapedValue = value.replace(/\n/g, '\\n')
296+
const nextLine = `${key}=${escapedValue}`
297+
const pattern = new RegExp(`^${key}=.*$`, 'm')
298+
299+
if (pattern.test(envContents)) {
300+
envContents = envContents.replace(pattern, nextLine)
301+
} else {
302+
envContents += `${envContents.endsWith('\n') ? '' : '\n'}${nextLine}\n`
303+
}
304+
}
305+
306+
await environment.writeFile(envLocalPath, envContents)
307+
}
308+
283309
function report(environment: Environment, options: Options) {
284310
const warnings: Array<string> = []
285311
for (const addOn of options.chosenAddOns) {
@@ -331,6 +357,7 @@ export async function createApp(environment: Environment, options: Options) {
331357

332358
environment.startRun()
333359
await writeFiles(environment, effectiveOptions)
360+
await seedEnvValues(environment, effectiveOptions)
334361
await runCommandsAndInstallDependencies(environment, effectiveOptions)
335362
environment.finishRun()
336363

packages/create/src/frameworks/react/add-ons/clerk/info.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,14 @@
2828
"jsName": "ClerkProvider",
2929
"path": "src/integrations/clerk/provider.tsx"
3030
}
31+
],
32+
"envVars": [
33+
{
34+
"name": "VITE_CLERK_PUBLISHABLE_KEY",
35+
"description": "Clerk publishable key",
36+
"required": true,
37+
"secret": false,
38+
"file": ".env.local"
39+
}
3140
]
3241
}

packages/create/src/frameworks/react/add-ons/convex/info.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,21 @@
2323
"path": "src/integrations/convex/provider.tsx",
2424
"jsName": "ConvexProvider"
2525
}
26+
],
27+
"envVars": [
28+
{
29+
"name": "CONVEX_DEPLOYMENT",
30+
"description": "Convex deployment name",
31+
"required": false,
32+
"secret": false,
33+
"file": ".env.local"
34+
},
35+
{
36+
"name": "VITE_CONVEX_URL",
37+
"description": "Convex deployment URL",
38+
"required": true,
39+
"secret": false,
40+
"file": ".env.local"
41+
}
2642
]
2743
}

packages/create/src/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ export const AddOnBaseSchema = z.object({
9292
createSpecialSteps: z.array(z.string()).optional(),
9393
postInitSpecialSteps: z.array(z.string()).optional(),
9494
options: AddOnOptionsSchema.optional(),
95+
envVars: z
96+
.array(
97+
z.object({
98+
name: z.string(),
99+
description: z.string().optional(),
100+
required: z.boolean().optional(),
101+
default: z.string().optional(),
102+
secret: z.boolean().optional(),
103+
file: z.enum(['.env', '.env.local']).optional(),
104+
}),
105+
)
106+
.optional(),
95107
default: z.boolean().optional(),
96108
})
97109

@@ -208,6 +220,7 @@ export interface Options {
208220
starter?: Starter | undefined
209221
routerOnly?: boolean
210222
includeExamples?: boolean
223+
envVarValues?: Record<string, string>
211224
}
212225

213226
export type SerializedOptions = Omit<

0 commit comments

Comments
 (0)