-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add env editor utility to edit update .env files (#8741)
- Loading branch information
1 parent
57e7653
commit 9a50805
Showing
3 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
195 changes: 195 additions & 0 deletions
195
packages/core/utils/src/common/__tests__/env-editor.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"], | ||
}, | ||
]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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")) | ||
}) | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters