Skip to content

Commit 4c02a82

Browse files
committed
feat(utils): bun and deno utilities
0 parents  commit 4c02a82

9 files changed

Lines changed: 795 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
MYENV_MYVAR=development
2+
MYENV_MYVAROUT=${MYENV_MYVAR}
3+
ASPNETCORE_ENVIRONMENT=development
4+
NODE_ENV=development
5+
PRODUCTION=false
6+
MYENV_NEWLINE_KEY='-----BEGIN ENCRYPTED PRIVATE KEY-----\r\nMIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIH70biY9xOiICAggA\r\nMBQGCCqGSIb3DQMHBAgLV06eX1LgDQSCBMjNDQ1/d/2FEDGnwdF04ieRbli2nFv0\r\nJb0QYu9HBBmGKa+0PUCwqQ7jDZt3aYsUQfDmYTJTgJ5V2YYDLn0xvVDzj4m5wHgt\r\namf+q702cY8fhGGFaO1vGijaefu6fK3A8lV8Q558i+8QBFVt3su9egAr49rI6DR/\r\nd7iY+J9Sbra3t6e/6m0YObF/HiOzrCDxqcOvzGORYizKljFrqRTra5Ikqph9Mtg2\r\nVStxeRmaOOUV2iyxq9nQZ4ncDDHN8gGDmcT2LPvyZxyExB09NQ+3Mnhj6w6IcvoF\r\nsszfhIsyxq1lk9tf9XAqEIBHvFlPDgz/uzHKXr/cytIgt3FhyHeCNX/uwajX94jK\r\n0IXEd5xAZvV84W2hLykUJ2cOvp42LYNNV2QMuqWrHbZxi1x/lkztvci891EtpcDa\r\ny+VDyNLR05aA/RdHd/t2j50En8PBx7muuwrQNKNszcEgWaftnLOFtydHDdzC/lX+\r\nJMBAeYr2u9rwZ8D3CwUQhjL67Te2Oo9+mu26qsJi5U2R8K1TaNdN4RGiJSbsIWLC\r\nxGdw/0mY8OGdusi1egvniFREuiMBjZBge+l6pPh3zcpqbS6IA6TcPFGYdkS+NSGW\r\nz76ID55Yn6EEfggBuCV1y0zhizbmTX8FvgkLwbe5txLP0bjrDVupar38EnNHUDvw\r\n/jTcYDjHavC82IXG3f3dq9JLVfk9aRwOgSWV9YuMhQxrHh9lxSbG+gcvEUziIrGG\r\nl+UgChb3xsPvO/W9gpqMGuj1T/JBqPhhGVuJTvA5I+ESlV/Dfdc4Ld+Le+8fJKRi\r\n74x37s8hyjhMmNozcUkCaINh2HFXBc91wzH95KG5ue9SIVVVGjyQ6Wc38x+Af7hd\r\ncnqwIJCgMjtwTaPPx93ayN8gWzzrtcvxyVLCEQzOlbTOQeJ7zrSkc6LarAXdR4sO\r\nkYGueJ2twv6pRPfsEOaHhUxVSUHD/KJNDzPWNp/WBFsywT83YKt/gCYZ/h/wbwwI\r\nNrsfIOLbgmQyUIIoIz8LubGZnRecd0Z8ry0LwFigpKvqlVaL9dyb07Ccl6Ehy+4a\r\n8S4vPQEJ64LM9bItz7PXK31+tLnrYIGSu+rEbA8U511bWXNWrNGbMt1g8ZbZ+yEy\r\nwmrHfjgbFmmxzQ5wGbs5kTg1iIb6lNCllYEcsriH8/mkt4y9TzL4hUyS5ib93AeQ\r\nyt0G07B5goavFOdF7coImtTDKZkJo+5PYRXkItl9KRBJeFGM5zJfwO9u0G+uL6ft\r\nY5+YPSx9I6UUGpy4El9tBNo7NTChOMzRYIorWQkTqG2IxSo0OsGL85eJAcMJCSNV\r\nlPLBUr1zt9wHTYwSlLR30SkfkbHLb+n570ozldrgEcmYOd7y7sX/et/TjjjvqwjS\r\nOZBL3GxkgWM+XkHiHYcbXW2czsRewRh3lqG3tkhCIW1osaiSchJkx3wM+OtOT7ro\r\nGi4jAR5osOiYXIuKkhUFWt50OpKQhM5wMbIGoNk7s3MNXHrAH15RfARZ8diPPD5b\r\nzH36gQkMz5y3HKrzQ9Qneokl0iDlyot05cjHzxtbSNpR5+UX/J3u5t2a346UrUwQ\r\neMLPopuekxFBXqqfgCp7Jg6W+UNZsAO+nkyEUE1sUxP8aWljnAsu2As2vV8B7tlh\r\nJyo=\r\n-----END ENCRYPTED PRIVATE KEY-----\r\n'

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
.env
3+
/bin
4+
5+
bun.lockb

Makefile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!make
2+
MAKEFLAGS += --silent --jobs=15
3+
4+
##################################################################
5+
## WARNING: assumes that .zsh_config is sourced!
6+
##################################################################
7+
8+
###
9+
### Environment
10+
###
11+
12+
environment ?= development
13+
export DOTENV_EXPAND_PREFIXES=MYENV_,ASPNETCORE_,DOTNET_,NODE_,PRODUCTION,WORKING_ENV
14+
_ := $(shell bunx dotenv-cli -e .config/environment/.$(environment).env -- bun --bun ./nx/dotenv-expand-transpiler.ts ./ .env false >&2)
15+
include .env
16+
export $(shell sed 's/=.*//' .env)
17+
18+
EnvCmd = dotenv .config/environment/.$(environment).env
19+
RebookPorts = 5000
20+
21+
# trick variables
22+
noop =
23+
comma := ,
24+
space = $(noop) $(noop)
25+
26+
###
27+
### Targets
28+
###
29+
30+
install:
31+
# deno install --unstable --no-check -qrfAn dx https://deno.land/x/deno_dx@0.3.1/cli.ts
32+
# dx ensureUtilsAreInstalled

nx.json

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"$schema": "./node_modules/nx/schemas/nx-schema.json",
3+
"affected": {
4+
"defaultBase": "main"
5+
},
6+
"namedInputs": {
7+
"default": ["{projectRoot}/**/*", "sharedGlobals"],
8+
"production": [
9+
"default",
10+
"!{projectRoot}/.eslintrc.json",
11+
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
12+
"!{projectRoot}/tsconfig.spec.json",
13+
"!{projectRoot}/jest.config.[jt]s",
14+
"!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)",
15+
"!{projectRoot}/.storybook/**/*",
16+
"!{projectRoot}/tsconfig.storybook.json",
17+
"!{projectRoot}/src/test-setup.[jt]s"
18+
],
19+
"sharedGlobals": []
20+
},
21+
"targetDefaults": {
22+
"build": {
23+
"dependsOn": ["^build", "codegen", "^codegen"],
24+
"inputs": ["production", "^production"]
25+
},
26+
"build-storybook": {
27+
"inputs": [
28+
"default",
29+
"^production",
30+
"{projectRoot}/.storybook/**/*",
31+
"{projectRoot}/tsconfig.storybook.json"
32+
]
33+
},
34+
"e2e": {
35+
"inputs": ["default", "^production"]
36+
},
37+
"lint": {
38+
"inputs": [
39+
"default",
40+
"{workspaceRoot}/.eslintrc.json",
41+
"{workspaceRoot}/.eslintignore"
42+
]
43+
},
44+
"test": {
45+
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
46+
},
47+
"versioning": {
48+
"dependsOn": ["^versioning"]
49+
}
50+
}
51+
}

nx/deno/dotenv-expander.ts

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
/**
2+
* Environment Variable Expander for Deno
3+
*
4+
* This script processes and merges multiple configuration files, expanding referenced
5+
* environment variables and writes the consolidated configuration to an output file.
6+
*
7+
* USAGE:
8+
* deno run --allow-read --allow-write --unstable script.ts [OPTIONS]
9+
*
10+
* OPTIONS:
11+
* -p, --prefixes Comma-separated prefixes to identify environment variables for expansion.
12+
* -e, --env Space-separated paths to the input configuration files.
13+
* -l, --logs Enable or disable logs (default: `false`).
14+
* -o, --output-file Path to the output configuration file (default: `.env`).
15+
*
16+
* EXAMPLE:
17+
* deno run --allow-read --allow-write --unstable script.ts -p DB,API -e ".config/.env .config/other.env" -l true -o .env.production
18+
*
19+
* INSTALLATION:
20+
* Install Deno using:
21+
* curl -fsSL https://deno.land/x/install/install.sh | sh
22+
*
23+
* PERMISSIONS:
24+
* The script requires permissions to read and write files. Utilize --allow-read and --allow-write flags.
25+
*
26+
* SECURITY NOTE:
27+
* Ensure sensitive data is handled securely, as environment variables might be written to an output file.
28+
*/
29+
30+
import { parse } from "https://deno.land/std@0.204.0/flags/mod.ts";
31+
import { existsSync } from "https://deno.land/std@0.204.0/fs/mod.ts";
32+
33+
/**
34+
* Parses command-line arguments.
35+
*/
36+
const args = parse(Deno.args, {
37+
string: ["p", "prefixes", "e", "env", "l", "logs", "o", "output-file"],
38+
alias: {
39+
p: "prefixes",
40+
e: "env",
41+
l: "logs",
42+
o: "output-file",
43+
},
44+
default: {
45+
logs: "false",
46+
"output-file": ".env",
47+
},
48+
});
49+
50+
/** Environment variable prefixes. */
51+
const prefixes = args.prefixes
52+
? args.prefixes.split(" ").map((prefix: string) => prefix.trim())
53+
: [];
54+
/** Paths to environment variable files. */
55+
const envPaths = args.env
56+
? args.env.split(" ").map((envPath: string) => envPath.trim())
57+
: [];
58+
/** Override whether to show logs. */
59+
const showLogs =
60+
Deno.env.get("DENU_ENV_EXPANDER_DEBUG")?.toLowerCase() === "1" ||
61+
Deno.env.get("DENU_ENV_EXPANDER_DEBUG")?.toLowerCase() === "true" ||
62+
args.logs === "true";
63+
/** Path to the output file. */
64+
const outputFilePath = args["output-file"];
65+
66+
/**
67+
* Reads the content of an environment file and validates its existence.
68+
* @param {string} envPath - The path to the environment file.
69+
* @returns {Promise<string>} - A promise that resolves with the file content.
70+
* @throws Will throw an error if the file does not exist.
71+
*/
72+
async function readAndValidateEnvFile(envPath: string): Promise<string> {
73+
if (!existsSync(envPath)) {
74+
throw new Error(`.env file at path ${envPath} does not exist.`);
75+
}
76+
return await Deno.readTextFile(envPath);
77+
}
78+
79+
/**
80+
* Parses and filters the content of a configuration file.
81+
* Ignores comment lines and trims unnecessary whitespace.
82+
* @param {string} rawFileContent - The raw content of the configuration file.
83+
* @returns {{ [key: string]: string }} - An object mapping of key-value pairs from the file.
84+
*/
85+
function parseAndFilterFileContent(rawFileContent: string): {
86+
[key: string]: string;
87+
} {
88+
const currentConfig: { [key: string]: string } = {};
89+
rawFileContent.split("\n").forEach((line) => {
90+
line = line.replace(/\s*=\s*/, "=").trim();
91+
if (!line || line.startsWith("#")) return;
92+
93+
const indexOfEquals = line.indexOf("=");
94+
if (indexOfEquals !== -1) {
95+
const key = line.substring(0, indexOfEquals).trim();
96+
const value = line.substring(indexOfEquals + 1).trim();
97+
currentConfig[key] = value;
98+
}
99+
});
100+
return currentConfig;
101+
}
102+
103+
/**
104+
* Expands environment variables in the provided configuration object.
105+
* @param {{ [key: string]: string }} currentConfig - The current configuration to be expanded.
106+
* @param {{ [key: string]: string }} baseConfig - The base configuration used for variable expansion.
107+
* @returns {{ [key: string]: string }} - The expanded configuration object.
108+
*/
109+
function expandEnvVariablesInConfig(
110+
currentConfig: { [key: string]: string },
111+
baseConfig: { [key: string]: string },
112+
): { [key: string]: string } {
113+
for (const key in currentConfig) {
114+
currentConfig[key] = expandValue(currentConfig[key], {
115+
...baseConfig,
116+
...currentConfig,
117+
});
118+
}
119+
return currentConfig;
120+
}
121+
122+
/**
123+
* Searches for the last occurrence of a regex pattern in a string.
124+
* @param {string} str - The string to search within.
125+
* @param {RegExp} rgx - The regex pattern to search for.
126+
* @returns {number} - The index of the last occurrence of the pattern. Returns -1 if not found.
127+
*/
128+
function searchLast(str: string, rgx: RegExp): number {
129+
const matches = Array.from(str.matchAll(rgx));
130+
if (matches.length === 0) return -1;
131+
132+
const lastIndex = matches.slice(-1)[0].index;
133+
return lastIndex !== undefined ? lastIndex : -1;
134+
}
135+
136+
/**
137+
* Interpolates the values in the provided string with corresponding values from the configuration.
138+
* @param {string} envValue - The value to be interpolated.
139+
* @param {{ [key: string]: string }} config - The configuration containing values for interpolation.
140+
* @returns {string} - The interpolated string.
141+
*/
142+
function interpolate(
143+
envValue: string,
144+
config: { [key: string]: string },
145+
): string {
146+
const lastUnescapedDollarSignIndex = searchLast(envValue, /(?<!\\)\$/g);
147+
148+
if (lastUnescapedDollarSignIndex === -1) return envValue;
149+
150+
const rightMostGroup = envValue.slice(lastUnescapedDollarSignIndex);
151+
152+
const matchGroup = /((?<!\\)\${?([\w]+)(?::-([^}\\]*))?}?)/;
153+
const match = rightMostGroup.match(matchGroup);
154+
155+
if (match != null) {
156+
const [, group, variableName, defaultValue] = match;
157+
158+
return interpolate(
159+
envValue.replace(group, config[variableName] || defaultValue || ""),
160+
config,
161+
);
162+
}
163+
164+
return envValue;
165+
}
166+
167+
/**
168+
* Resolves escape sequences in the given string. Specifically, it replaces escaped dollar signs.
169+
* @param {string} value - The string containing escape sequences.
170+
* @returns {string} - The string with resolved escape sequences.
171+
*/
172+
function resolveEscapeSequences(value: string): string {
173+
return value.replace(/\\\$/g, "$");
174+
}
175+
176+
/**
177+
* Expands the values in the provided string using values from the given configuration.
178+
* This function also resolves any escape sequences present.
179+
* @param {string} value - The value to be expanded.
180+
* @param {{ [key: string]: string }} config - The configuration to use for expansion.
181+
* @returns {string} - The expanded string.
182+
*/
183+
export function expandValue(
184+
value: string,
185+
config: { [key: string]: string },
186+
): string {
187+
return resolveEscapeSequences(interpolate(value, config));
188+
}
189+
190+
/**
191+
* Logs a message to the console if logging is enabled.
192+
* @param {string} message - The message to be logged.
193+
*/
194+
function log(message: string) {
195+
showLogs ? console.log(message) : undefined;
196+
}
197+
198+
/**
199+
* Logs a separator to the console if logging is enabled.
200+
* @param {string} message - The message to be logged.
201+
*/
202+
function logSeparator() {
203+
log("");
204+
log(
205+
"---------------------------------------------------------------------------------------",
206+
);
207+
log("");
208+
}
209+
210+
/**
211+
* Merges and expands configurations from provided file paths.
212+
* @param {string[]} envPaths - An array of file paths containing environment configurations.
213+
* @param {{ [key: string]: string }} baseConfig - (Optional) A base configuration to merge with and expand upon.
214+
* @returns {Promise<{ [key: string]: string }>} - A promise that resolves with the merged and expanded configuration.
215+
*/
216+
export async function mergeAndExpandConfigs(
217+
envPaths: string[],
218+
baseConfig: { [key: string]: string } = {},
219+
): Promise<{ [key: string]: string }> {
220+
let config = { ...baseConfig };
221+
222+
for (const envPath of envPaths) {
223+
const rawFileContent = await readAndValidateEnvFile(envPath);
224+
let currentConfig = parseAndFilterFileContent(rawFileContent);
225+
config = {
226+
...config,
227+
...expandEnvVariablesInConfig(currentConfig, config),
228+
};
229+
}
230+
231+
return config;
232+
}
233+
234+
/**
235+
* Checks the existence of a file at a given path.
236+
* @param {string} path - The path to check.
237+
* @throws Will throw an error if the file does not exist.
238+
*/
239+
export function checkFileExistence(path: string): void {
240+
if (!existsSync(path)) {
241+
throw new Error(`File at path ${path} does not exist.`);
242+
}
243+
}
244+
245+
/**
246+
* Writes a string content to a specified file path.
247+
* @param {string} filePath - The path where the content will be written.
248+
* @param {string} content - The content to write.
249+
* @returns {Promise<void>} - A promise indicating the completion of the write operation.
250+
*/
251+
export async function writeFile(
252+
filePath: string,
253+
content: string,
254+
): Promise<void> {
255+
try {
256+
await Deno.writeTextFile(filePath, content);
257+
log(`Expanded environment variables written to: ${filePath}`);
258+
} catch (error) {
259+
console.error(error);
260+
}
261+
}
262+
263+
/**
264+
* Main function orchestrating the script flow:
265+
* 1. Validates file existence.
266+
* 2. Merges and expands configurations.
267+
* 3. Writes the resulting configuration to the output file.
268+
* Handles errors gracefully and logs the operation steps if logging is enabled.
269+
*/
270+
async function main() {
271+
try {
272+
envPaths.forEach((path: string) => checkFileExistence(path));
273+
274+
let config = await mergeAndExpandConfigs(envPaths); // Don't forget to await here
275+
276+
// Filter by prefix only if there are prefixes specified
277+
if (prefixes.length) {
278+
config = Object.fromEntries(
279+
Object.entries(config).filter(([key]) =>
280+
prefixes.some((prefix: string) => key.startsWith(prefix)),
281+
),
282+
);
283+
}
284+
285+
await writeFile(
286+
outputFilePath,
287+
Object.entries(config)
288+
.map(([key, value]) => `${key}=${value}`)
289+
.join("\n"),
290+
);
291+
292+
log("Operation completed.");
293+
} catch (error) {
294+
console.error(error);
295+
Deno.exit(1);
296+
}
297+
}
298+
299+
main();

0 commit comments

Comments
 (0)