Skip to content

Commit

Permalink
feat: add env:add command
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-R44 committed Apr 20, 2024
1 parent 40a85f9 commit f0f522d
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
118 changes: 118 additions & 0 deletions commands/env/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* @adonisjs/core
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { CommandOptions } from '../../types/ace.js'
import { args, BaseCommand, flags } from '../../modules/ace/main.js'
import stringHelpers from '../../src/helpers/string.js'

const ALLOWED_TYPES = ['string', 'boolean', 'number', 'enum'] as const
type AllowedTypes = (typeof ALLOWED_TYPES)[number]

/**
* The env:add command is used to add a new environment variable to the
* `.env`, `.env.example` and `start/env.ts` files.
*/
export default class EnvAdd extends BaseCommand {
static commandName = 'env:add'
static description = 'Add a new environment variable'
static options: CommandOptions = {
allowUnknownFlags: true,
}

@args.string({
description: 'Variable name. Will be converted to screaming snake case',
required: false,
})
declare name: string

@args.string({ description: 'Variable value', required: false })
declare value: string

@flags.string({ description: 'Type of the variable' })
declare type: AllowedTypes

@flags.array({
description: 'Allowed values for the enum type in a comma-separated list',
default: [''],
required: false,
})
declare enumValues: string[]

/**
* Validate the type flag passed by the user
*/
#isTypeFlagValid() {
return ALLOWED_TYPES.includes(this.type)
}

async run() {
/**
* Prompt for missing name
*/
if (!this.name) {
this.name = await this.prompt.ask('Enter the variable name', {
validate: (value) => !!value,
format: (value) => stringHelpers.snakeCase(value).toUpperCase(),
})
}

/**
* Prompt for missing value
*/
if (!this.value) {
this.value = await this.prompt.ask('Enter the variable value')
}

/**
* Prompt for missing type
*/
if (!this.type) {
this.type = await this.prompt.choice('Select the variable type', ALLOWED_TYPES)
}

/**
* Prompt for missing enum values if the selected env type is `enum`
*/
if (this.type === 'enum' && !this.enumValues) {
this.enumValues = await this.prompt.ask('Enter the enum values separated by a comma', {
result: (value) => value.split(',').map((one) => one.trim()),
})
}

/**
* Validate inputs
*/
if (!this.#isTypeFlagValid()) {
this.logger.error(`Invalid type "${this.type}". Must be one of ${ALLOWED_TYPES.join(', ')}`)
return
}

/**
* Add the environment variable to the `.env` and `.env.example` files
*/
const codemods = await this.createCodemods()
const transformedName = stringHelpers.snakeCase(this.name).toUpperCase()

await codemods.defineEnvVariables({ [transformedName]: this.value })

/**
* Add the environment variable to the `start/env.ts` file
*/
const validation = {
string: 'Env.schema.string()',
number: 'Env.schema.number()',
boolean: 'Env.schema.boolean()',
enum: `Env.schema.enum(['${this.enumValues.join("','")}'] as const)`,
}[this.type]

await codemods.defineEnvValidations({ variables: { [transformedName]: validation } })

this.logger.success('Environment variable added successfully')
}
}
116 changes: 116 additions & 0 deletions tests/commands/env_add.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* @adonisjs/core
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { test } from '@japa/runner'
import EnvAdd from '../../commands/env/add.js'
import { AceFactory } from '../../factories/core/ace.js'

test.group('Env Add command', () => {
test('add new env variable to the different files', async ({ assert, fs }) => {
await fs.createJson('tsconfig.json', {})
await fs.create('.env', '')
await fs.create('.env.example', '')
await fs.create(
'./start/env.ts',
`import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {})`
)

const ace = await new AceFactory().make(fs.baseUrl)
await ace.app.init()
ace.ui.switchMode('raw')

const command = await ace.create(EnvAdd, ['variable', 'value', '--type=string'])
await command.exec()

await assert.fileContains('.env', 'VARIABLE=value')
await assert.fileContains('.env.example', 'VARIABLE=value')
await assert.fileContains('./start/env.ts', 'VARIABLE: Env.schema.string()')
})

test('convert variable to screaming snake case', async ({ assert, fs }) => {
await fs.createJson('tsconfig.json', {})
await fs.create('.env', '')
await fs.create('.env.example', '')
await fs.create(
'./start/env.ts',
`import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {})`
)

const ace = await new AceFactory().make(fs.baseUrl)
await ace.app.init()
ace.ui.switchMode('raw')

const command = await ace.create(EnvAdd, ['stripe_ApiKey', 'value', '--type=string'])
await command.exec()

await assert.fileContains('.env', 'STRIPE_API_KEY=value')
await assert.fileContains('.env.example', 'STRIPE_API_KEY=value')
await assert.fileContains('./start/env.ts', 'STRIPE_API_KEY: Env.schema.string()')
})

test('enum type with allowed values', async ({ assert, fs }) => {
await fs.createJson('tsconfig.json', {})
await fs.create('.env', '')
await fs.create('.env.example', '')
await fs.create(
'./start/env.ts',
`import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {})`
)

const ace = await new AceFactory().make(fs.baseUrl)
await ace.app.init()
ace.ui.switchMode('raw')

const command = await ace.create(EnvAdd, [
'variable',
'bar',
'--type=enum',
'--enum-values=foo',
'--enum-values=bar',
])
await command.exec()

await assert.fileContains('.env', 'VARIABLE=bar')
await assert.fileContains('.env.example', 'VARIABLE=bar')
await assert.fileContains(
'./start/env.ts',
"VARIABLE: Env.schema.enum(['foo', 'bar'] as const)"
)
})

test('prompt when nothing is passed to the command', async ({ assert, fs }) => {
await fs.createJson('tsconfig.json', {})
await fs.create('.env', '')
await fs.create('.env.example', '')
await fs.create(
'./start/env.ts',
`import { Env } from '@adonisjs/core/env'
export default await Env.create(new URL('../', import.meta.url), {})`
)

const ace = await new AceFactory().make(fs.baseUrl)
await ace.app.init()
ace.ui.switchMode('raw')

const command = await ace.create(EnvAdd, [])

command.prompt.trap('Enter the variable name').replyWith('my_variable_name')
command.prompt.trap('Enter the variable value').replyWith('my_value')
command.prompt.trap('Select the variable type').replyWith('string')

await command.exec()

await assert.fileContains('.env', 'MY_VARIABLE_NAME=my_value')
await assert.fileContains('.env.example', 'MY_VARIABLE_NAME=my_value')
await assert.fileContains('./start/env.ts', 'MY_VARIABLE_NAME: Env.schema.string()')
})
})

0 comments on commit f0f522d

Please sign in to comment.