Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some tweaks to CLI configuration #323

Merged
merged 3 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ Ensure the value passed into APP_ENV matches the file name of the .env file you

If no value is passed `.env.local` will be used as the default

### Additional flags

Additional env flags can be set by prepending them with "--". To pass arguments through to the underlying utility (e.g. jest) use "--" as a separator.

Examples:

```
yarn dev --NEXT_PUBLIC_DRUPAL_BASE_URL https://staging.cms.va.gov -- --port 3003
```

```
yarn test -- path/to/file
```

Available env variables and underlying utility help can be viewed by appending `-h` to the yarn script:
Examples:

```
yarn test -h
```

```
yarn build -h
```

## Local CMS endpoint

To use the local CMS as an endpoint, follow the install directions for [the CMS repo here](https://github.com/department-of-veterans-affairs/va.gov-cms/blob/main/READMES/getting-started.md).
Expand Down
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
// Run unit tests relating to modified files.
// todo: Jest should be able to ignore files that don't need tests (*.stories.*, config, etc)
'**/*.(ts|tsx|js|jsx)': (filenames) => [
`yarn lint:staged:test-modified-files '${filenames.join(' ')}'`,
`yarn test -- --findRelatedTests ${filenames.join(' ')} --passWithNoTests`,
],

// Format MarkDown and JSON
Expand Down
19 changes: 8 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,22 @@
"lint": "next lint",
"plop": "plop",
"postinstall": "husky install",
"typedoc": "typedoc",
"typedoc:serve": "http-server typedocs -p 8002",
"lint:staged:test-modified-files": "yarn build:env-loader && node ./scripts/yarn/lint:staged:test-modified-files.js",
"test": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test.js",
"test:ci": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:ci.js",
"test:coverage": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:coverage.js",
"test:playwright": "playwright test --project=main",
"test:playwright:interactive": "playwright test --ui",
"test:playwright:a11y": "playwright test --project=a11y",
"test:debug": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:debug.js",
"test:watch": "yarn test -- --watch",
"test:examples": "APP_ENV=test jest --rootDir ./example_tests --config ./example_tests/jest.config.js",
"test:example": "APP_ENV=test yarn test:examples example_tests/${1}/",
"test:debug": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:debug.js",
"test:format": "APP_ENV=test prettier --check .",
"test:playwright": "playwright test --project=main",
"test:playwright:interactive": "playwright test --ui",
"test:playwright:a11y": "playwright test --project=a11y",
"test:types": "yarn build:env-loader && APP_ENV=test tsc --noEmit",
"test:watch": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:watch.js",
"test:update-snapshots": "yarn build:env-loader && APP_ENV=test node ./scripts/yarn/test:update-snapshots.js",
"test:u": "yarn test:update-snapshots",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build -o out/storybook-static --quiet"
"storybook:build": "storybook build -o out/storybook-static --quiet",
"typedoc": "typedoc",
"typedoc:serve": "http-server typedocs -p 8002"
},
"dependencies": {
"@department-of-veterans-affairs/component-library": "^31.0.0",
Expand Down
119 changes: 73 additions & 46 deletions packages/env-loader/src/cli-options.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,71 @@
import { Command, Option } from 'commander'
import { EnvVars } from '.'

type CliOptionsAndArgs = {
options: EnvVars
args: string[]
}

type AdditionalHelp = {
heading: string
commands: string[]
}

const ADDITIONAL_HELP: { [key: string]: AdditionalHelp[] } = {
test: [
{
heading: 'Watch mode',
commands: ['yarn test:watch'],
},
{
heading: 'With coverage report',
commands: ['yarn test:coverage'],
},
{
heading: 'Update test snapshots',
commands: ['yarn test:update-snapshots', 'yarn test:u'],
},
{
heading: 'Run Playwright E2E tests',
commands: ['yarn test:playwright'],
},
],
}
const ADDITIONAL_HELP: AdditionalHelp[] = [
{
heading: 'Run unit tests in watch mode',
commands: ['yarn test -- --watch'],
},
{
heading: 'Unit test specific file',
commands: ['yarn test -- path/to/file'],
},
{
heading: 'Update unit-test snapshots',
commands: ['yarn test -- -u'],
},
{
heading: 'Get help for Next build',
commands: ['yarn build -- -h'],
},
{
heading:
'Run Next dev server on a specific port and pull data from a specific Drupal instance',
commands: [
'yarn dev --NEXT_PUBLIC_DRUPAL_BASE_URL <drupal base url> -- -p <port number>',
],
},
]

const formatHelpText = (
scriptName: string,
helpCommands: AdditionalHelp[]
): string => {
const outputLines: string[] = []
outputLines.push(`Additional \`${scriptName}\` commands:`)
helpCommands.forEach((helpCommand) => {
outputLines.push(`\n ${helpCommand.heading}:`)
const getAdditionalHelpText = (): string => {
const helpText: string[] = []
helpText.push(
'Args can be passed through to the underlying utility by adding them to the end of the command.'
)
helpText.push(
'\nUse "--" to separate the env flags from the args meant for the underlying utility.'
)
helpText.push('\nEx:')
ADDITIONAL_HELP.forEach((helpCommand) => {
helpText.push(`\n ${helpCommand.heading}:`)
helpCommand.commands.forEach((terminalCommand) => {
outputLines.push(`\n $ ${terminalCommand}`)
helpText.push(`\n $ ${terminalCommand}`)
})
})
outputLines.push('\n')

return outputLines.join('')
}

const configureAdditionalHelpText = (
program: Command,
scriptName: string
): void => {
const additionalHelp = ADDITIONAL_HELP[scriptName]
if (additionalHelp) {
program.addHelpText('beforeAll', formatHelpText(scriptName, additionalHelp))
}
helpText.push('\n')
return helpText.join('')
}

/**
* Parses CLI options from the command line into an object.
* Parses CLI options and arguments from the command line into an object.
*
* See https://www.npmjs.com/package/commander#common-option-types-boolean-and-value
*/
export const getCliOptions = (scriptName: string): EnvVars => {
export const getCliOptionsAndArgs = (): CliOptionsAndArgs => {
const program = new Command()

program
.passThroughOptions()
.option('--NEXT_IMAGE_DOMAIN <url>', 'Drupal image domain')
.option(
'--NEXT_PUBLIC_ASSETS_URL <url>',
Expand Down Expand Up @@ -95,7 +99,30 @@ export const getCliOptions = (scriptName: string): EnvVars => {
).choices(['true', 'false'])
)

configureAdditionalHelpText(program, scriptName)
const additionalHelpText = getAdditionalHelpText()
program.addHelpText('beforeAll', additionalHelpText)

program.exitOverride()
try {
const parsed = program.parse()
const options = parsed.opts()
const args = parsed.args

return program.parse().opts()
return {
options,
args,
}
} catch (err) {
if (err.code === 'commander.helpDisplayed') {
return {
options: {},
args: ['-h'],
}
}

return {
options: {},
args: [],
}
}
}
59 changes: 21 additions & 38 deletions packages/env-loader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*eslint-disable no-console*/
import { getCliOptions } from './cli-options'
import { getCliOptionsAndArgs } from './cli-options'
import { getEnvFileVars } from './env-file'
import { getCmsFeatureFlags } from './cms-feature-flags'
import { spawn } from 'child_process'
Expand All @@ -8,53 +8,36 @@ export type EnvVars = {
[key: string]: string
}

const getAllEnvVars = async (scriptName: string): Promise<EnvVars> => {
// CLI OPTIONS
const cliOptions = getCliOptions(scriptName)
export const processEnv = async (command: string): Promise<void> => {
// CLI
const { args: cliArgs, options: cliOptions } = getCliOptionsAndArgs()

// ENV FILE VARS
// ENV FILE
const envVars = getEnvFileVars(process.env.APP_ENV)

// For now, don't fetch CMS feature flags for tests.
// Will fail CI.
// CMS FEATURE FLAGS
let cmsFeatureFlags
if (process.env.APP_ENV === 'test') {
return {
...envVars,
...cliOptions,
}
// For now, don't fetch CMS feature flags for tests. Will fail CI.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker for this, just curious what failures you were seeing? is it related to fetching an env behind the VA network through GHA (gh-provided vs self-hosted runners), or something else?

cmsFeatureFlags = {}
} else {
const drupalBaseUrlProp = 'NEXT_PUBLIC_DRUPAL_BASE_URL'
const drupalBaseUrl =
cliOptions[drupalBaseUrlProp] || envVars[drupalBaseUrlProp]
cmsFeatureFlags = await getCmsFeatureFlags(drupalBaseUrl)
}

// CMS FEATURE FLAGS
const drupalBaseUrlProp = 'NEXT_PUBLIC_DRUPAL_BASE_URL'
const drupalBaseUrl =
cliOptions[drupalBaseUrlProp] || envVars[drupalBaseUrlProp]
const cmsFeatureFlags = await getCmsFeatureFlags(drupalBaseUrl)

// Return all options with proper cascading:
// 1. CLI options have highest precedence
// 2. CMS feature flags next
// 3. ENV file vars last
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment (or something to that effect) is valuable to keep even if it is technically self-documented in the code

return {
...envVars,
...cmsFeatureFlags,
...cliOptions,
}
}

const getYarnScriptName = (path: string): string => {
const match = path.match(/\/scripts\/yarn\/(.*)\.js/)
return match?.[1] || ''
}

export const processEnv = async (command: string): Promise<void> => {
const yarnScriptName = getYarnScriptName(process.argv[1])

process.env = {
...process.env,
...(await getAllEnvVars(yarnScriptName)),
...{
...envVars,
...cmsFeatureFlags,
...cliOptions,
},
}

spawn(command, {
// Pass additional arguments through to the underlying command
spawn(`${command} ${cliArgs.join(' ')}`, {
shell: true,
stdio: 'inherit',
})
Expand Down
4 changes: 0 additions & 4 deletions scripts/yarn/lint:staged:test-modified-files.js

This file was deleted.

3 changes: 0 additions & 3 deletions scripts/yarn/test:update-snapshots.js

This file was deleted.

3 changes: 0 additions & 3 deletions scripts/yarn/test:watch.js

This file was deleted.