Skip to content

Commit f0f522d

Browse files
committed
feat: add env:add command
1 parent 40a85f9 commit f0f522d

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

commands/env/add.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* @adonisjs/core
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { CommandOptions } from '../../types/ace.js'
11+
import { args, BaseCommand, flags } from '../../modules/ace/main.js'
12+
import stringHelpers from '../../src/helpers/string.js'
13+
14+
const ALLOWED_TYPES = ['string', 'boolean', 'number', 'enum'] as const
15+
type AllowedTypes = (typeof ALLOWED_TYPES)[number]
16+
17+
/**
18+
* The env:add command is used to add a new environment variable to the
19+
* `.env`, `.env.example` and `start/env.ts` files.
20+
*/
21+
export default class EnvAdd extends BaseCommand {
22+
static commandName = 'env:add'
23+
static description = 'Add a new environment variable'
24+
static options: CommandOptions = {
25+
allowUnknownFlags: true,
26+
}
27+
28+
@args.string({
29+
description: 'Variable name. Will be converted to screaming snake case',
30+
required: false,
31+
})
32+
declare name: string
33+
34+
@args.string({ description: 'Variable value', required: false })
35+
declare value: string
36+
37+
@flags.string({ description: 'Type of the variable' })
38+
declare type: AllowedTypes
39+
40+
@flags.array({
41+
description: 'Allowed values for the enum type in a comma-separated list',
42+
default: [''],
43+
required: false,
44+
})
45+
declare enumValues: string[]
46+
47+
/**
48+
* Validate the type flag passed by the user
49+
*/
50+
#isTypeFlagValid() {
51+
return ALLOWED_TYPES.includes(this.type)
52+
}
53+
54+
async run() {
55+
/**
56+
* Prompt for missing name
57+
*/
58+
if (!this.name) {
59+
this.name = await this.prompt.ask('Enter the variable name', {
60+
validate: (value) => !!value,
61+
format: (value) => stringHelpers.snakeCase(value).toUpperCase(),
62+
})
63+
}
64+
65+
/**
66+
* Prompt for missing value
67+
*/
68+
if (!this.value) {
69+
this.value = await this.prompt.ask('Enter the variable value')
70+
}
71+
72+
/**
73+
* Prompt for missing type
74+
*/
75+
if (!this.type) {
76+
this.type = await this.prompt.choice('Select the variable type', ALLOWED_TYPES)
77+
}
78+
79+
/**
80+
* Prompt for missing enum values if the selected env type is `enum`
81+
*/
82+
if (this.type === 'enum' && !this.enumValues) {
83+
this.enumValues = await this.prompt.ask('Enter the enum values separated by a comma', {
84+
result: (value) => value.split(',').map((one) => one.trim()),
85+
})
86+
}
87+
88+
/**
89+
* Validate inputs
90+
*/
91+
if (!this.#isTypeFlagValid()) {
92+
this.logger.error(`Invalid type "${this.type}". Must be one of ${ALLOWED_TYPES.join(', ')}`)
93+
return
94+
}
95+
96+
/**
97+
* Add the environment variable to the `.env` and `.env.example` files
98+
*/
99+
const codemods = await this.createCodemods()
100+
const transformedName = stringHelpers.snakeCase(this.name).toUpperCase()
101+
102+
await codemods.defineEnvVariables({ [transformedName]: this.value })
103+
104+
/**
105+
* Add the environment variable to the `start/env.ts` file
106+
*/
107+
const validation = {
108+
string: 'Env.schema.string()',
109+
number: 'Env.schema.number()',
110+
boolean: 'Env.schema.boolean()',
111+
enum: `Env.schema.enum(['${this.enumValues.join("','")}'] as const)`,
112+
}[this.type]
113+
114+
await codemods.defineEnvValidations({ variables: { [transformedName]: validation } })
115+
116+
this.logger.success('Environment variable added successfully')
117+
}
118+
}

tests/commands/env_add.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* @adonisjs/core
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { test } from '@japa/runner'
11+
import EnvAdd from '../../commands/env/add.js'
12+
import { AceFactory } from '../../factories/core/ace.js'
13+
14+
test.group('Env Add command', () => {
15+
test('add new env variable to the different files', async ({ assert, fs }) => {
16+
await fs.createJson('tsconfig.json', {})
17+
await fs.create('.env', '')
18+
await fs.create('.env.example', '')
19+
await fs.create(
20+
'./start/env.ts',
21+
`import { Env } from '@adonisjs/core/env'
22+
export default await Env.create(new URL('../', import.meta.url), {})`
23+
)
24+
25+
const ace = await new AceFactory().make(fs.baseUrl)
26+
await ace.app.init()
27+
ace.ui.switchMode('raw')
28+
29+
const command = await ace.create(EnvAdd, ['variable', 'value', '--type=string'])
30+
await command.exec()
31+
32+
await assert.fileContains('.env', 'VARIABLE=value')
33+
await assert.fileContains('.env.example', 'VARIABLE=value')
34+
await assert.fileContains('./start/env.ts', 'VARIABLE: Env.schema.string()')
35+
})
36+
37+
test('convert variable to screaming snake case', async ({ assert, fs }) => {
38+
await fs.createJson('tsconfig.json', {})
39+
await fs.create('.env', '')
40+
await fs.create('.env.example', '')
41+
await fs.create(
42+
'./start/env.ts',
43+
`import { Env } from '@adonisjs/core/env'
44+
export default await Env.create(new URL('../', import.meta.url), {})`
45+
)
46+
47+
const ace = await new AceFactory().make(fs.baseUrl)
48+
await ace.app.init()
49+
ace.ui.switchMode('raw')
50+
51+
const command = await ace.create(EnvAdd, ['stripe_ApiKey', 'value', '--type=string'])
52+
await command.exec()
53+
54+
await assert.fileContains('.env', 'STRIPE_API_KEY=value')
55+
await assert.fileContains('.env.example', 'STRIPE_API_KEY=value')
56+
await assert.fileContains('./start/env.ts', 'STRIPE_API_KEY: Env.schema.string()')
57+
})
58+
59+
test('enum type with allowed values', async ({ assert, fs }) => {
60+
await fs.createJson('tsconfig.json', {})
61+
await fs.create('.env', '')
62+
await fs.create('.env.example', '')
63+
await fs.create(
64+
'./start/env.ts',
65+
`import { Env } from '@adonisjs/core/env'
66+
export default await Env.create(new URL('../', import.meta.url), {})`
67+
)
68+
69+
const ace = await new AceFactory().make(fs.baseUrl)
70+
await ace.app.init()
71+
ace.ui.switchMode('raw')
72+
73+
const command = await ace.create(EnvAdd, [
74+
'variable',
75+
'bar',
76+
'--type=enum',
77+
'--enum-values=foo',
78+
'--enum-values=bar',
79+
])
80+
await command.exec()
81+
82+
await assert.fileContains('.env', 'VARIABLE=bar')
83+
await assert.fileContains('.env.example', 'VARIABLE=bar')
84+
await assert.fileContains(
85+
'./start/env.ts',
86+
"VARIABLE: Env.schema.enum(['foo', 'bar'] as const)"
87+
)
88+
})
89+
90+
test('prompt when nothing is passed to the command', async ({ assert, fs }) => {
91+
await fs.createJson('tsconfig.json', {})
92+
await fs.create('.env', '')
93+
await fs.create('.env.example', '')
94+
await fs.create(
95+
'./start/env.ts',
96+
`import { Env } from '@adonisjs/core/env'
97+
export default await Env.create(new URL('../', import.meta.url), {})`
98+
)
99+
100+
const ace = await new AceFactory().make(fs.baseUrl)
101+
await ace.app.init()
102+
ace.ui.switchMode('raw')
103+
104+
const command = await ace.create(EnvAdd, [])
105+
106+
command.prompt.trap('Enter the variable name').replyWith('my_variable_name')
107+
command.prompt.trap('Enter the variable value').replyWith('my_value')
108+
command.prompt.trap('Select the variable type').replyWith('string')
109+
110+
await command.exec()
111+
112+
await assert.fileContains('.env', 'MY_VARIABLE_NAME=my_value')
113+
await assert.fileContains('.env.example', 'MY_VARIABLE_NAME=my_value')
114+
await assert.fileContains('./start/env.ts', 'MY_VARIABLE_NAME: Env.schema.string()')
115+
})
116+
})

0 commit comments

Comments
 (0)