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

feature: Adds utility functions to add envars and update Redwood toml for plugin packages to cli helpers for use in simplifying CLI setup commands #9324

Merged
3 changes: 2 additions & 1 deletion packages/cli-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"pascalcase": "1.0.0",
"prettier": "2.8.8",
"prompts": "2.4.2",
"terminal-link": "2.1.1"
"terminal-link": "2.1.1",
"toml": "3.0.0"
},
"devDependencies": {
"@babel/cli": "7.23.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should add a new environment variable when it does not exist (overwrite: false) 1`] = `
"EXISTING_VAR=value
# CommentedVar=123

# New Variable Comment
NEW_VAR=new_value
"
`;

exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should not update existing environment variable if exists and overwrite is default 1`] = `
"EXISTING_VAR=value
# CommentedVar=123
"
`;

exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should not update existing environment variable if exists and overwrite is false 1`] = `
"EXISTING_VAR=value
# CommentedVar=123
"
`;

exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should updates an existing environment variable when it exists and overwrite chosen 1`] = `
"# Updated existing variable Comment
EXISTING_VAR=new_value
# CommentedVar=123
"
`;

exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli has some plugins configured 1`] = `
"
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@existing-example/some-package-name"
package = "@example/test-package-name""
`;

exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli is not configured 1`] = `
"
[web]
title = "Redwood App"
port = 8910
apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
includeEnvironmentVariables = [
# Add any ENV vars that should be available to the web side to this array
# See https://redwoodjs.com/docs/environment-variables#web
]
[api]
port = 8911
[browser]
open = false
[notifications]
versionUpdates = ["latest"]
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@example/test-package-name""
`;

exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli is setup but has no plugins configured 1`] = `
"
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@example/test-package-name""
`;

exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin does not add duplicate place when experimental cli has that plugin configured 1`] = `
"
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@existing-example/some-package-name"
"
`;
171 changes: 171 additions & 0 deletions packages/cli-helpers/src/lib/__tests__/project.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import fs from 'fs'

import { updateTomlConfig, addEnvVar } from '../project' // Replace with the correct path to your module

jest.mock('fs')

jest.mock('@redwoodjs/project-config', () => {
return {
getPaths: () => {
return {
generated: {
base: '.redwood',
},
base: '',
}
},
getConfigPath: () => {
return '.redwood.toml'
},
getConfig: jest.fn(),
}
})

describe('addEnvVar', () => {
let envFileContent = ''

describe('addEnvVar adds environment variables as part of a setup task', () => {
beforeEach(() => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => {
return true
})

jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
return envFileContent
})

jest.spyOn(fs, 'writeFileSync').mockImplementation((envPath, envFile) => {
expect(envPath).toContain('.env')
return envFile
})
})

afterEach(() => {
jest.restoreAllMocks()
envFileContent = ''
})

it('should add a new environment variable when it does not exist (overwrite: false)', () => {
envFileContent = 'EXISTING_VAR=value\n# CommentedVar=123\n'
const file = addEnvVar(
'NEW_VAR',
'new_value',
'New Variable Comment',
false
)
expect(file).toMatchSnapshot()
})

it('should updates an existing environment variable when it exists and overwrite chosen', () => {
envFileContent = 'EXISTING_VAR=value\n# CommentedVar=123\n'
const file = addEnvVar(
'EXISTING_VAR',
'new_value',
'Updated existing variable Comment',
true
)
expect(file).toMatchSnapshot()
})

it('should not update existing environment variable if exists and overwrite is default', () => {
envFileContent = 'EXISTING_VAR=value\n# CommentedVar=123\n'
const file = addEnvVar(
'EXISTING_VAR',
'new_value',
'Updated existing variable Comment'
)
expect(file).toMatchSnapshot()
})

it('should not update existing environment variable if exists and overwrite is false', () => {
envFileContent = 'EXISTING_VAR=value\n# CommentedVar=123\n'
const file = addEnvVar(
'EXISTING_VAR',
'new_value',
'Updated existing variable Comment',
false
)
expect(file).toMatchSnapshot()
})
})
})

describe('updateTomlConfig', () => {
let tomlFileContent = `
[web]
title = "Redwood App"
port = 8910
apiUrl = "/.redwood/functions" # You can customize graphql and dbauth urls individually too: see https://redwoodjs.com/docs/app-configuration-redwood-toml#api-paths
includeEnvironmentVariables = [
# Add any ENV vars that should be available to the web side to this array
# See https://redwoodjs.com/docs/environment-variables#web
]
[api]
port = 8911
[browser]
open = false
[notifications]
versionUpdates = ["latest"]
`

describe('updateTomlConfig configures a new CLI plugin', () => {
beforeEach(() => {
jest.spyOn(fs, 'existsSync').mockImplementation(() => {
return true
})

jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
return tomlFileContent
})

jest
.spyOn(fs, 'writeFileSync')
.mockImplementation((tomlPath, tomlFile) => {
expect(tomlPath).toContain('redwood.toml')
return tomlFile
})
})

afterEach(() => {
jest.restoreAllMocks()
tomlFileContent = ''
})

it('adds when experimental cli is not configured', () => {
tomlFileContent += ''
const file = updateTomlConfig('@example/test-package-name')
expect(file).toMatchSnapshot()
})

it('adds when experimental cli has some plugins configured', () => {
tomlFileContent += `
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@existing-example/some-package-name"
`
const file = updateTomlConfig('@example/test-package-name')
expect(file).toMatchSnapshot()
})

it('adds when experimental cli is setup but has no plugins configured', () => {
tomlFileContent += `
[experimental.cli]
autoInstall = true
`
const file = updateTomlConfig('@example/test-package-name')
expect(file).toMatchSnapshot()
})

it('does not add duplicate place when experimental cli has that plugin configured', () => {
tomlFileContent += `
[experimental.cli]
autoInstall = true
[[experimental.cli.plugins]]
package = "@existing-example/some-package-name"
`
const file = updateTomlConfig('@existing-example/some-package-name')
expect(file).toMatchSnapshot()
})
})
})
113 changes: 104 additions & 9 deletions packages/cli-helpers/src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import fs from 'fs'
import path from 'path'

import * as toml from 'toml'

import { resolveFile, findUp } from '@redwoodjs/project-config'
import { getConfigPath } from '@redwoodjs/project-config'

import { colors } from './colors'
import { getPaths } from './paths'
Expand Down Expand Up @@ -33,21 +36,113 @@ export const getInstalledRedwoodVersion = () => {
}
}

export const addEnvVarTask = (name: string, value: string, comment: string) => {
/**
* Updates the project's redwood.toml file to include the specified packages plugin
*
* Uses toml parsing to determine if the plugin is already included in the file and
* only adds it if it is not.
*
* Writes the updated config to the file system by appending strings, not stringify-ing the toml.
*/
export const updateTomlConfig = (packageName: string) => {
const redwoodTomlPath = getConfigPath()

const configLines = []

const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8')
const config = toml.parse(configContent)

if (!config || !config.experimental || !config.experimental.cli) {
configLines.push('[experimental.cli]')
}

if (!config?.experimental?.cli?.autoInstall) {
configLines.push(' autoInstall = true')
}

if (!config?.experimental?.cli?.plugins) {
// If plugins array is missing, create it.
configLines.push(' [[experimental.cli.plugins]]')
}

// Check if the package is not already in the plugins array
if (
!config?.experimental?.cli?.plugins?.some(
(plugin: any) => plugin.package === packageName
)
) {
configLines.push(` package = "${packageName}"`)
}

const newConfig = configContent + configLines.join('\n')

return fs.writeFileSync(redwoodTomlPath, newConfig, 'utf-8')
}

export const updateTomlConfigTask = (packageName: string) => {
return {
title: `Updating redwood.toml to configure ${packageName} ...`,
task: () => {
updateTomlConfig(packageName)
},
}
}

export const addEnvVarTask = (
name: string,
value: string,
comment: string,
overwrite = false
) => {
return {
title: `Adding ${name} var to .env...`,
task: () => {
const envPath = path.join(getPaths().base, '.env')
const content = [comment && `# ${comment}`, `${name}=${value}`, ''].flat()
let envFile = ''
addEnvVar(name, value, comment, overwrite)
},
}
}

if (fs.existsSync(envPath)) {
envFile = fs.readFileSync(envPath).toString() + '\n'
}
export const addEnvVar = (
name: string,
value: string,
comment: string,
overwrite = false
) => {
const envPath = path.join(getPaths().base, '.env')
const content = [comment && `# ${comment}`, `${name}=${value}`, ''].flat()
let envFile = ''

fs.writeFileSync(envPath, envFile + content.join('\n'))
},
if (fs.existsSync(envPath)) {
envFile = fs.readFileSync(envPath).toString()
const lines = envFile.split('\n')

// Check if the variable already exists
const existingIndex = lines.findIndex((line) => {
const trimmedLine = line.trim()
return (
trimmedLine.startsWith(`${name}=`) ||
trimmedLine.startsWith(`#${name}=`)
)
})

if (existingIndex !== -1) {
// Variable already exists, check if overwrite is true
if (overwrite) {
// Update the existing line with the new value
const existingComment = [content[0]]
lines[existingIndex] = `${existingComment}\n${name}=${value}`
envFile = lines.join('\n')
}
// If overwrite is false, do nothing (leave the file unchanged)
} else {
// Variable doesn't exist, add it
envFile += '\n' + content.join('\n')
}
} else {
envFile = content.join('\n')
}

return fs.writeFileSync(envPath, envFile)
}

/**
Expand Down
Loading