@saasak/tool-env 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/index.js +204 -0
  2. package/package.json +22 -0
package/bin/index.js ADDED
@@ -0,0 +1,204 @@
1
+ import minimist from 'minimist';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { $ } from 'execa';
5
+
6
+ const args = minimist(process.argv.slice(2));
7
+ const __dirname = path.dirname(new URL(import.meta.url).pathname)
8
+ const __root = path.resolve(__dirname, '..');
9
+
10
+ // Here we fetch the target env from cli args
11
+ const DEFAULT_ENV = 'dev';
12
+ const targets = {
13
+ "development": "dev",
14
+ "dev": "dev",
15
+ "staging": "preprod",
16
+ "pp": "preprod",
17
+ "preprod": "preprod",
18
+ "prod": "production",
19
+ "production": "production",
20
+ }
21
+ const target = targets[args.target] || DEFAULT_ENV;
22
+
23
+ // Here we fetch the master env.json file
24
+ // from which we will build all the .env files
25
+ const envPath = args.env || '.env.json';
26
+ const maybeEnvFile = path.resolve(__root, envPath);
27
+ const envFile = fs.existsSync(maybeEnvFile) ? maybeEnvFile : null;
28
+
29
+ if (!envFile) {
30
+ console.error('No env file found.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const envContent = fs.readFileSync(envFile, 'utf8');
35
+ const env = JSON.parse(envContent);
36
+
37
+ // Here we fetch the root package.json file
38
+ // And all packages that live in the monorepo
39
+ // So we can extract names and directories
40
+ const rootPkgPath = path.resolve(__root, 'package.json');
41
+ const rootPkgContent = fs.readFileSync(rootPkgPath, 'utf8');
42
+ const rootPkg = JSON.parse(rootPkgContent);
43
+ const scope = rootPkg.name.split('/')[0];
44
+
45
+ const packageList = await $`pnpm ls -r --depth=-1`
46
+ const packageDirs = packageList.stdout
47
+ .split('\n')
48
+ .filter((line) => !!line)
49
+ .map((line) => line.split(' ')[1]);
50
+
51
+ const [_, ...depPkgs] = packageDirs;
52
+ const deps = depPkgs.map((pkgDir) => {
53
+ const pkgFile = fs.readFileSync(path.join(pkgDir, 'package.json'));
54
+ const pkg = JSON.parse(pkgFile);
55
+ return {
56
+ name: pkg.name.replace(`${scope}/`, ''),
57
+ dir: pkgDir,
58
+ };
59
+ }).concat({
60
+ name: 'root',
61
+ dir: __root,
62
+ });
63
+
64
+ const depsName = deps.map((dep) => dep.name);
65
+
66
+ // Here we do a check to see if target env is described
67
+ const allowedEnvs = env && env.envs && Array.isArray(env.envs) ? env.envs : [DEFAULT_ENV];
68
+ const targetEnv = allowedEnvs.find((env) => env === target);
69
+ if (!targetEnv) {
70
+ console.error(`Target env "${target}" is not allowed.`);
71
+ process.exit(1);
72
+ }
73
+
74
+ // Here we build the env object which will be used
75
+ // and which contains all the variables for all packages
76
+ const allEnvs = buildEnv(targetEnv, env, depsName);
77
+
78
+ // Here we write the .env files for each package to disk
79
+ // This should not influence the vcs state as those env files
80
+ // should be ignored by it
81
+ for await (const dep of deps) {
82
+ const pkgEnv = Object.entries(allEnvs[dep.name] || {})
83
+ .map(([key, value]) => `${key}=${value}`)
84
+ .join('\n')
85
+ console.log(`Writing env file for ${dep.name} in ${dep.dir} with target ${targetEnv}`);
86
+ await fs.writeFile(path.join(dep.dir, '.env'), pkgEnv);
87
+ }
88
+
89
+ // ========================================
90
+ // Helpers for core logic
91
+ // ========================================
92
+
93
+ function buildEnv(target, env, names) {
94
+ const variablesForAllTargets = env.variables;
95
+ const overrides = env.overrides || {};
96
+ const sections = env.sections || {};
97
+
98
+ // For each variables, extract the value for the target env
99
+ const variables = Object.entries(variablesForAllTargets).reduce((acc, entry) => {
100
+ const [key, value] = entry;
101
+ return {
102
+ ...acc,
103
+ [key]: value[target] || value['@@'] || ''
104
+ };
105
+ }, {});
106
+
107
+ // For each package, get all variables
108
+ // compute the overrides and merge them
109
+ // allVars is an object with the package name as key
110
+ // and the variables as value like so
111
+ // => { [name]: { [variable]: value } }
112
+ const allVars = names.reduce((acc, name) => {
113
+ acc[name] = {
114
+ ...variables,
115
+ ...parseOverrides(target, overrides[name], variables)
116
+ };
117
+
118
+ return acc;
119
+ }, {});
120
+
121
+ // For each package, get all dependencies of variables
122
+ // For instance and for clarity, backoffice can include all vars
123
+ // from the api, and the api can include all vars from the core
124
+ const allDeps = names.reduce((acc, name) => ({
125
+ ...acc,
126
+ [name]: parseSectionDeps(name, sections).reverse()
127
+ }), {})
128
+
129
+ // For each package, get all variable names from the dependencies
130
+ // and from the package itself and merge them
131
+ const allSections = Object.entries(allDeps).reduce((acc, entry) => {
132
+ const [key, deps] = entry;
133
+
134
+ // For all deps, get the variable names
135
+ const depVars = deps.filter(Boolean).reduce((_acc, dep) => {
136
+ const vars = parseSectionVars(dep, sections)
137
+ return [..._acc, ...vars];
138
+ }, []);
139
+
140
+ // For the current section, get the variable names
141
+ const nameVars = parseSectionVars(key, sections)
142
+
143
+ // Merge all variables names, and for each variable name,
144
+ // get the value from the allVars object
145
+ return {
146
+ ...acc,
147
+ [key]: [...depVars, ...nameVars].reduce((_acc, variable) => {
148
+ if (!allVars[key] || allVars[key][variable] === null) return _acc;
149
+
150
+ return { ..._acc, [variable]: allVars[key][variable] };
151
+ }, {})
152
+ };
153
+ }, {});
154
+
155
+
156
+ return allSections;
157
+ }
158
+
159
+ function parseOverrides(target, overrides, vars) {
160
+ if (!overrides) return {};
161
+
162
+ return Object.entries(overrides).reduce((acc, entry) => {
163
+ const [key, val] = entry;
164
+ const value = val[target] || val['@@'] || '';
165
+
166
+ if (value === null) return acc;
167
+
168
+ acc[key] = Array.isArray(value)
169
+ ? value.map(v => vars[v] || v).join('')
170
+ : value
171
+ return acc;
172
+ }, {});
173
+ }
174
+
175
+ function parseSectionDeps(name, sections, collected = []) {
176
+ if (!sections || !sections[name]) return [];
177
+
178
+ const collectedWithSelf = Array.from(new Set([...collected, name]));
179
+ const deps = sections[name]
180
+ .filter((key) => key.startsWith('@@'))
181
+ .map((key) => key.split('@@')[1])
182
+ .filter((key) => !collectedWithSelf.includes(key));
183
+
184
+ if (!deps.length) return [];
185
+
186
+ const uniquelyCollected = Array.from(new Set([...collected, ...deps]));
187
+ return [
188
+ ...deps,
189
+ ...deps.map(
190
+ (dep) => parseSectionDeps(
191
+ dep,
192
+ sections,
193
+ uniquelyCollected
194
+ )
195
+ ).flat(),
196
+ ];
197
+ }
198
+
199
+ function parseSectionVars(name, sections) {
200
+ if (!sections || !sections[name]) return [];
201
+
202
+ return sections[name]
203
+ .filter((key) => !key.startsWith('@@'))
204
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@saasak/tool-env",
3
+ "license": "MIT",
4
+ "version": "0.0.1",
5
+ "author": "dev@saasak.studio",
6
+ "description": "A small util to manage environment variables for your monorepo",
7
+ "keywords": [
8
+ "tool",
9
+ "node",
10
+ "env"
11
+ ],
12
+ "type": "module",
13
+ "main": "bin/index.js",
14
+ "bin": {
15
+ "wrenv": "bin/index.js"
16
+ },
17
+ "dependencies": {
18
+ "execa": "8.0.1",
19
+ "fs-extra": "11.2.0",
20
+ "minimist": "1.2.8"
21
+ }
22
+ }