-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathsetup-env.js
320 lines (297 loc) · 11.6 KB
/
setup-env.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/* eslint-disable no-console */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-continue */
/* eslint-disable no-multi-str */
// This isn't the actual code so it's correct to have it in devDependencies
/* eslint-disable import/no-extraneous-dependencies */
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs');
const _ = require('lodash');
// Get the sample env file
let sampleEnv;
try {
sampleEnv = fs.readFileSync('.sample-env', { encoding: 'utf8' });
} catch (e) {
console.error('\n\n');
console.error(
'ERROR: There was an error opening the .sample-env file, are you sure you \
are running this command through npm run setup or from the root directory?',
);
console.error('\n\n');
throw e;
}
const trimmedSampleEnvLines = sampleEnv.split('\n').map(x => x.trim());
// Attempt to get the current .env file if it exists
let envFile;
try {
envFile = fs.readFileSync('.env', { encoding: 'utf8' });
} catch (e) {
// ENOENT means the file didn't exist, in which case we just assume that
// the user currently has no .env file which is fine
if (e.code !== 'ENOENT') {
console.error('\n\n');
console.error(
'ERROR: There was an error opening the .env file, even though it seems to exist',
);
console.error('\n\n');
throw e;
}
}
const trimmedEnvFileLines = envFile && envFile.split('\n').map(x => x.trim());
// The different states our parser can be in
const NEUTRAL = 0;
const MAIN_DESCRIPTION = 1;
const SECTION_HEADER = 2;
const COMMENT = 3;
// We use this state if the user doesn't want to do deployment config
const WRITE_REST = 4;
// A global state of whether we should get defaults from the sample or real env
let shouldUseCurrentEnv = false;
// Tests for each type
const isSectionComment = line => /^#.*#$/.test(line);
const extractSectionCommentText = line =>
/^#+([\w\s.,'!:`#()-]*?)#+$/.exec(line)[1].trim();
const isAssignment = line => line.includes('=');
const parseAssignment = line => /^(\w+?)=(.*)/.exec(line).slice(1, 3);
const isTitle = line => /^#[^#]/.test(line);
const extractTitle = line => /^#(.+)/.exec(line)[1].trim();
const isComment = line => /^##[^#]/.test(line);
const extractComment = line => /^##(.+)/.exec(line)[1].trim();
// Helper
function throwError(err) {
console.error(err);
process.exit(1);
}
// The styles for our messages
const welcomeGoodbyeStyle = chalk.underline;
const boldCyan = chalk.bold.cyanBright;
const titleStyle = chalk.whiteBright.bold;
const WELCOME_MESSAGE = `\n\
Hi! Welcome to the setup script for The Gazelle's Engineering Team! \
The source code for this script is located in the root directory of this \
project with file name ${boldCyan('setup-env.js')}, and it uses ${boldCyan(
'.sample-env',
)}, also in the root directory, as the source for all the section headers, \
comments, and environment variables to setup. At the end of this guide the \
script will output a ${boldCyan('.env')} file to the root directory which \
our code needs in order to for example connect to the database. If you ever \
need to edit this file you can either run this script again \
(${boldCyan('node setup-env.js')}) or edit ${boldCyan('.env')} directly.
When filling out the variables, the default value if you don't input anything \
will be shown in parenthesis at the end of the prompt. If a default is \
provided it's often because it's the value you'll be using in most cases \
and unless there's something different about your setup you shouldn't \
need to change these.
If you find that there's anything that could be better about this guide, \
please do improve ${boldCyan('setup-env.js')} or ${boldCyan('.sample-env')} \
and submit a pull request so the next generation of developers can have \
an easier time!\
`;
const GOODBYE_MESSAGE = `\
You should now have your environment totally setup! Try out running \
npm run build && npm start and see if you have The Gazelle running \
successfully by checking out localhost:3000 and localhost:4000 (or \
whichever ports you set in the config) in your browser!`;
// I want to use async/await, but only works in an async function
main();
async function main() {
// We support a command here to check whether .env is outdated (is missing some variables from .sample-env)
// it makes more sense to put this command in this script than write a separate one as we reuse code and
// don't have to keep track of as many files.
// Before anything else we check whether the user wanted this command or the usual script
if (process.argv.length === 3 && process.argv[2] === '--check-outdated') {
if (!trimmedEnvFileLines) {
// It is of course outdated if there is no .env
process.exit(1);
}
const exitCode = sampleAndCurrentHaveSameVariables() ? 0 : 1;
// We notify the caller of the result through our exit code
process.exit(exitCode);
}
// Say hi
console.log(welcomeGoodbyeStyle(`${WELCOME_MESSAGE}\n`));
// Give them some space to read the message
await shouldContinue();
// newline
console.log();
// Before we start the actual loop we check whether there already exists a .env file
if (trimmedEnvFileLines) {
console.log(
welcomeGoodbyeStyle('We discovered that you already have a .env \
file\n'),
);
const answer = await inquirer.prompt({
type: 'confirm',
name: 'shouldUseCurrentEnv',
default: true,
message:
'Would you like us to use the currently set values as the defaults when possible?',
});
({ shouldUseCurrentEnv } = answer);
}
let state = NEUTRAL;
let mainDescriptionSeen = false;
let currentText = '';
// The file descriptor for the env file
const fd = fs.openSync('.env', 'w');
for (let i = 0; i < trimmedSampleEnvLines.length; i++) {
const line = trimmedSampleEnvLines[i];
let lineToWrite = line;
if (state === MAIN_DESCRIPTION || state === SECTION_HEADER) {
if (isSectionComment(line)) {
const text = extractSectionCommentText(line);
if (
text === 'DEPLOYMENT' &&
!(await checkIfShouldDoDeploymentConfig())
) {
state = WRITE_REST;
} else {
currentText += '\n';
currentText += line;
if (!text) {
if (state === SECTION_HEADER) {
console.log(boldCyan(`${currentText}\n`));
}
state = NEUTRAL;
}
}
} else {
throwError('Found a non closed section comment');
}
} else if (state === COMMENT) {
if (isComment(line)) {
currentText = `${currentText} ${extractComment(line)}`;
} else {
// Comment is done, so we print it and change the state to neutral + reset the loop
// so the current line can be properly parsed
console.log(boldCyan(currentText));
state = NEUTRAL;
i -= 1;
continue;
}
} else if (state === NEUTRAL) {
// Note that the order and the else if's are important here as otherwise
// our regexes would have to be more specific
if (isSectionComment(line)) {
currentText = line;
// We assume that the first section is the main description
// which we don't want to print as it's only for reading directly
if (!mainDescriptionSeen) {
mainDescriptionSeen = true;
state = MAIN_DESCRIPTION;
} else {
state = SECTION_HEADER;
}
} else if (isTitle(line)) {
// We assume titles are never more than one line
const title = extractTitle(line);
console.log(titleStyle(title));
console.log(titleStyle('-'.repeat(title.length)));
// newline
console.log();
} else if (isComment(line)) {
state = COMMENT;
currentText = `NOTE: ${extractComment(line)}`;
} else if (isAssignment(line)) {
const [variable] = parseAssignment(line);
const savedDefaultValue = getDefaultValue(variable);
// If it's ROOT_DIRECTORY we make the default the directory of this script if there
// isn't already another default
const defaultValue =
variable === 'ROOT_DIRECTORY'
? savedDefaultValue || __dirname
: savedDefaultValue;
let assignedValue;
const question = `What value would you like to set ${variable} to?`;
// If it's GAZELLE_ENV we want to restrict answers to 'staging' or 'production'
if (variable === 'GAZELLE_ENV') {
const answer = await inquirer.prompt({
type: 'list',
name: variable,
message: question,
choices: ['staging', 'production'],
});
assignedValue = answer[variable];
} else {
const isPassword = variable.toLowerCase().includes('password');
const answer = await inquirer.prompt({
type: isPassword ? 'password' : 'input',
mask: isPassword ? '*' : undefined,
name: variable,
default: defaultValue,
message: question,
});
assignedValue = answer[variable];
}
// We aren't handling all the escaping values or setting quotes or not
// but in most if not all cases this should work. If in the future problems
// are encountered with this then it would have to be handled with some logic though
lineToWrite = `${variable}=${assignedValue}`;
// newline
console.log();
} else {
state = NEUTRAL;
}
} else if (state === WRITE_REST) {
// Don't do anything, the user chose not to continue, we'll set defaults for the rest
// and not print anything
} else {
throwError('Parser is in an unknown state');
}
fs.writeSync(fd, Buffer.from(`${lineToWrite}\n`, 'utf-8'));
}
fs.closeSync(fd);
console.log(welcomeGoodbyeStyle(`${GOODBYE_MESSAGE}\n`));
// Give them some space to read goodbye message
await shouldContinue('Press enter to exit', 'input');
}
async function checkIfShouldDoDeploymentConfig() {
const answer = await inquirer.prompt({
type: 'confirm',
name: 'shouldContinue',
default: false,
message:
"You are done with all the necessary config for development now, would you also like to set up deployment related config? This is meant only for setting up the actual server and if you're in doubt, the answer is probably no",
});
// newline
console.log();
return answer.shouldContinue;
}
async function shouldContinue(message, type) {
const answer = await inquirer.prompt({
type: type || 'confirm',
name: 'shouldContinue',
message: message || 'Continue?',
});
if (!answer.shouldContinue) {
process.exit(0);
}
}
function sampleAndCurrentHaveSameVariables() {
// We use sets as for the deep equality we don't care about order of variables
const sampleVariables = new Set(
trimmedSampleEnvLines.filter(isAssignment).map(x => parseAssignment(x)[0]),
);
const currentVariables = new Set(
trimmedEnvFileLines.filter(isAssignment).map(x => parseAssignment(x)[0]),
);
// Perform deep equality comparison on variables
return _.isEqual(sampleVariables, currentVariables);
}
function getDefaultValue(variable) {
const sampleRelevantAssignment = trimmedSampleEnvLines.find(
x => isAssignment(x) && parseAssignment(x)[0] === variable,
);
const currentRelevantAssignment =
trimmedEnvFileLines &&
trimmedEnvFileLines.find(
x => isAssignment(x) && parseAssignment(x)[0] === variable,
);
// Only use current value if user chose it and it exists, may be that sample env changed and current one
// doesn't have that value
const relevantAssignment =
(shouldUseCurrentEnv && currentRelevantAssignment) ||
sampleRelevantAssignment;
return parseAssignment(relevantAssignment)[1];
}