feat: add env editor utility to edit update .env files (#8741)

This commit is contained in:
Harminder Virk
2024-08-25 19:31:56 +05:30
committed by GitHub
parent 57e7653adf
commit 9a508056f6
3 changed files with 300 additions and 0 deletions
@@ -0,0 +1,195 @@
import { join } from "path"
import { FileSystem } from "../file-system"
import { EnvEditor } from "../env-editor"
const BASE_PATH = join(__dirname, "env-editor-tests")
const fs = new FileSystem(BASE_PATH)
describe("Env editor", () => {
beforeEach(async () => {
await fs.cleanup()
})
afterAll(async () => {
await fs.cleanup()
})
test("add key-value pair to dot-env files", async () => {
await fs.create(".env", "")
await fs.create(".env.template", "")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
expect(editor.toJSON()).toEqual([
{
exists: true,
filePath: join(fs.basePath, ".env"),
contents: ["", "PORT=3000"],
},
{
exists: true,
filePath: join(fs.basePath, ".env.template"),
contents: ["", "PORT=3000"],
},
])
})
test("modify non-existing files as well", async () => {
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
expect(editor.toJSON()).toEqual([
{
exists: false,
filePath: join(fs.basePath, ".env"),
contents: ["PORT=3000"],
},
{
exists: false,
filePath: join(fs.basePath, ".env.template"),
contents: ["PORT=3000"],
},
])
})
test("update existing key value pair", async () => {
await fs.create(".env", "PORT=3333")
await fs.create(".env.template", "PORT=4000")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
expect(editor.toJSON()).toEqual([
{
exists: true,
filePath: join(fs.basePath, ".env"),
contents: ["PORT=3000"],
},
{
exists: true,
filePath: join(fs.basePath, ".env.template"),
contents: ["PORT=3000"],
},
])
})
test("update in one file and add in another file", async () => {
await fs.create(".env", ["PORT=3333", "", "HOST=localhost"].join("\n"))
await fs.create(".env.template", "")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
expect(editor.toJSON()).toEqual([
{
exists: true,
filePath: join(fs.basePath, ".env"),
contents: ["PORT=3000", "", "HOST=localhost"],
},
{
exists: true,
filePath: join(fs.basePath, ".env.template"),
contents: ["", "PORT=3000"],
},
])
})
test("persist changes on disk", async () => {
await fs.create(".env", ["PORT=3333", "", "HOST=localhost"].join("\n"))
await fs.create(".env.template", "")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
await editor.save()
const envFile = await fs.contents(".env")
expect(envFile.split("\n")).toEqual(["PORT=3000", "", "HOST=localhost"])
const envTemplateFile = await fs.contents(".env.template")
expect(envTemplateFile.split("\n")).toEqual(["", "PORT=3000"])
})
test("multiple times persist changes on disk", async () => {
await fs.create(".env", ["PORT=3333", "", "HOST=localhost"].join("\n"))
await fs.create(".env.template", "")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000)
await editor.save()
let envFile = await fs.contents(".env")
expect(envFile.split("\n")).toEqual(["PORT=3000", "", "HOST=localhost"])
let envTemplateFile = await fs.contents(".env.template")
expect(envTemplateFile.split("\n")).toEqual(["", "PORT=3000"])
editor.set("HOST", "127.0.0.1")
await editor.save()
envFile = await fs.contents(".env")
expect(envFile.split("\n")).toEqual(["PORT=3000", "", "HOST=127.0.0.1"])
envTemplateFile = await fs.contents(".env.template")
expect(envTemplateFile.split("\n")).toEqual([
"",
"PORT=3000",
"HOST=127.0.0.1",
])
})
test("add empty template value", async () => {
await fs.create(".env", "")
await fs.create(".env.template", "")
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000, { withEmptyTemplateValue: true })
expect(editor.toJSON()).toEqual([
{
exists: true,
filePath: join(fs.basePath, ".env"),
contents: ["", "PORT=3000"],
},
{
exists: true,
filePath: join(fs.basePath, ".env.template"),
contents: ["", "PORT="],
},
])
})
test("do not replace existing template value with empty value", async () => {
await fs.create(".env", ["PORT=3333", "", "HOST=localhost"].join("\n"))
await fs.create(
".env.template",
["PORT=3333", "", "HOST=localhost"].join("\n")
)
const editor = new EnvEditor(BASE_PATH)
await editor.load()
editor.set("PORT", 3000, { withEmptyTemplateValue: true })
expect(editor.toJSON()).toEqual([
{
exists: true,
filePath: join(fs.basePath, ".env"),
contents: ["PORT=3000", "", "HOST=localhost"],
},
{
exists: true,
filePath: join(fs.basePath, ".env.template"),
contents: ["PORT=3333", "", "HOST=localhost"],
},
])
})
})
@@ -0,0 +1,104 @@
import { join } from "path"
import { writeFile, readFile } from "fs/promises"
/**
* Exposes the API to edit Env files
*/
export class EnvEditor {
#appRoot: string
#files: {
exists: boolean
contents: string[]
filePath: string
}[] = []
constructor(appRoot: string) {
this.#appRoot = appRoot
}
/**
* Reads a file and returns with contents. Ignores error
* when file is missing
*/
async #readFile(filePath: string) {
try {
const contents = await readFile(filePath, "utf-8")
return {
exists: true,
contents: contents.split(/\r?\n/),
filePath,
}
} catch (error) {
if (error.code === "ENOENT") {
return {
exists: false,
contents: [],
filePath,
}
}
throw error
}
}
/**
* Loads .env and .env.template files for editing.
*/
async load() {
this.#files = await Promise.all(
[join(this.#appRoot, ".env"), join(this.#appRoot, ".env.template")].map(
(filePath) => this.#readFile(filePath)
)
)
}
/**
* Set key-value pair to the dot-env files.
*
* If `withEmptyTemplateValue` is true then the key will be added with an empty value
* to the `.env.template` file.
*/
set(
key: string,
value: string | number | boolean,
options?: {
withEmptyTemplateValue: boolean
}
) {
const withEmptyTemplateValue = options?.withEmptyTemplateValue ?? false
this.#files.forEach((file) => {
let entryIndex = file.contents.findIndex((line) =>
line.startsWith(`${key}=`)
)
const writeIndex = entryIndex === -1 ? file.contents.length : entryIndex
if (withEmptyTemplateValue && file.filePath.endsWith(".env.template")) {
/**
* Do not remove existing template value (if any)
*/
if (entryIndex === -1) {
file.contents[writeIndex] = `${key}=`
}
} else {
file.contents[writeIndex] = `${key}=${value}`
}
})
}
/**
* Get files and their contents as JSON
*/
toJSON() {
return this.#files
}
/**
* Save changes back to the disk
*/
async save() {
await Promise.all(
this.#files.map((file) => {
return writeFile(file.filePath, file.contents.join("\n"))
})
)
}
}
+1
View File
@@ -69,4 +69,5 @@ export * from "./upper-case-first"
export * from "./validate-handle"
export * from "./wrap-handler"
export * from "./define-config"
export * from "./env-editor"
export * from "./normalize-import-path-with-source"