@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.
- package/bin/index.js +204 -0
- 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
|
+
}
|