Skip to content

Commit 885cbc8

Browse files
committed
Refactor utilities out of setup script
1 parent 6580e97 commit 885cbc8

File tree

3 files changed

+116
-89
lines changed

3 files changed

+116
-89
lines changed

scripts/lib/dotenv.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const fsp = require("fs").promises;
2+
const dotenv = require("dotenv");
3+
4+
const DOTENV_PATH = `${__dirname}/../../.env`;
5+
6+
async function readDotenv() {
7+
let buffer = null;
8+
try {
9+
buffer = await fsp.readFile(DOTENV_PATH);
10+
} catch (e) {
11+
/* noop */
12+
}
13+
const config = buffer ? dotenv.parse(buffer) : null;
14+
// also read from current env, because docker-compose already needs to know some of it
15+
// eg. $PG_DUMP, $CONFIRM
16+
return { ...config, ...process.env };
17+
}
18+
19+
function encodeDotenvValue(str) {
20+
if (typeof str !== "string") {
21+
throw new Error(`'${str}' is not a string`);
22+
}
23+
if (str.trim() !== str) {
24+
// `dotenv` would escape this with single/double quotes but that won't work in docker-compose
25+
throw new Error(
26+
"We don't support leading/trailing whitespace in config variables"
27+
);
28+
}
29+
if (str.indexOf("\n") >= 0) {
30+
// `dotenv` would escape this with single/double quotes and `\n` but that won't work in docker-compose
31+
throw new Error("We don't support newlines in config variables");
32+
}
33+
return str;
34+
}
35+
36+
async function withDotenvUpdater(overrides, callback) {
37+
let data;
38+
try {
39+
data = await fsp.readFile(DOTENV_PATH, "utf8");
40+
// Trim whitespace, and prefix with newline so we can do easier checking later
41+
data = "\n" + data.trim();
42+
} catch (e) {
43+
data = "";
44+
}
45+
46+
const config = data ? dotenv.parse(data) : null;
47+
const answers = {
48+
...config,
49+
...process.env,
50+
...overrides,
51+
};
52+
53+
function add(varName, defaultValue, comment) {
54+
const SET = `\n${varName}=`;
55+
const encodedValue = encodeDotenvValue(
56+
varName in answers ? answers[varName] : defaultValue || ""
57+
);
58+
const pos = data.indexOf(SET);
59+
if (pos >= 0) {
60+
/* Replace this value with the new value */
61+
62+
// Where's the next newline (or the end of the file if there is none)
63+
let nlpos = data.indexOf("\n", pos + 1);
64+
if (nlpos < 0) {
65+
nlpos = data.length;
66+
}
67+
68+
// Surgical editing
69+
data =
70+
data.substr(0, pos + SET.length) + encodedValue + data.substr(nlpos);
71+
} else {
72+
/* This value didn't already exist; add it to the end */
73+
74+
if (comment) {
75+
data += `\n\n${comment}`;
76+
}
77+
78+
data += `${SET}${encodedValue}`;
79+
}
80+
}
81+
82+
await callback(add);
83+
84+
data = data.trim() + "\n";
85+
86+
await fsp.writeFile(DOTENV_PATH, data);
87+
}
88+
89+
exports.readDotenv = readDotenv;
90+
exports.withDotenvUpdater = withDotenvUpdater;

scripts/lib/random.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { randomBytes } = require("crypto");
2+
3+
function safeRandomString(length) {
4+
// Roughly equivalent to shell `openssl rand -base64 30 | tr '+/' '-_'`
5+
return randomBytes(length)
6+
.toString("base64")
7+
.replace(/\+/g, "-")
8+
.replace(/\//g, "_");
9+
}
10+
11+
exports.safeRandomString = safeRandomString;

scripts/setup.js

Lines changed: 15 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ if (parseInt(process.version.split(".")[0], 10) < 10) {
44
}
55

66
const fsp = require("fs").promises;
7-
const { randomBytes } = require("crypto");
87
const { spawnSync: rawSpawnSync } = require("child_process");
98
const dotenv = require("dotenv");
109
const inquirer = require("inquirer");
1110
const pg = require("pg");
11+
const { withDotenvUpdater, readDotenv } = require("./lib/dotenv");
12+
const { safeRandomString } = require("./lib/random");
1213

1314
// fixes spawnSync not throwing ENOENT on windows
1415
const platform = require("os").platform();
@@ -61,85 +62,7 @@ const spawnSync = (cmd, args, options) => {
6162

6263
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
6364

64-
const DOTENV_PATH = `${__dirname}/../.env`;
65-
66-
function safeRandomString(length) {
67-
// Roughly equivalent to shell `openssl rand -base64 30 | tr '+/' '-_'`
68-
return randomBytes(length)
69-
.toString("base64")
70-
.replace(/\+/g, "-")
71-
.replace(/\//g, "_");
72-
}
73-
74-
async function readDotenv() {
75-
let buffer = null;
76-
try {
77-
buffer = await fsp.readFile(DOTENV_PATH);
78-
} catch (e) {
79-
/* noop */
80-
}
81-
const config = buffer ? dotenv.parse(buffer) : null;
82-
// also read from current env, because docker-compose already needs to know some of it
83-
// eg. $PG_DUMP, $CONFIRM
84-
return { ...config, ...process.env };
85-
}
86-
87-
function encodeDotenvValue(str) {
88-
if (typeof str !== "string") {
89-
throw new Error(`'${str}' is not a string`);
90-
}
91-
if (str.trim() !== str) {
92-
// `dotenv` would escape this with single/double quotes but that won't work in docker-compose
93-
throw new Error(
94-
"We don't support leading/trailing whitespace in config variables"
95-
);
96-
}
97-
if (str.indexOf("\n") >= 0) {
98-
// `dotenv` would escape this with single/double quotes and `\n` but that won't work in docker-compose
99-
throw new Error("We don't support newlines in config variables");
100-
}
101-
return str;
102-
}
103-
104-
async function updateDotenv(answers) {
105-
let data;
106-
try {
107-
data = await fsp.readFile(DOTENV_PATH, "utf8");
108-
// Trim whitespace, and prefix with newline so we can do easier checking later
109-
data = "\n" + data.trim();
110-
} catch (e) {
111-
data = "";
112-
}
113-
114-
function add(varName, defaultValue, comment) {
115-
const SET = `\n${varName}=`;
116-
const encodedValue = encodeDotenvValue(
117-
varName in answers ? answers[varName] : defaultValue || ""
118-
);
119-
const pos = data.indexOf(SET);
120-
if (pos >= 0) {
121-
/* Replace this value with the new value */
122-
123-
// Where's the next newline (or the end of the file if there is none)
124-
let nlpos = data.indexOf("\n", pos + 1);
125-
if (nlpos < 0) {
126-
nlpos = data.length;
127-
}
128-
129-
// Surgical editing
130-
data =
131-
data.substr(0, pos + SET.length) + encodedValue + data.substr(nlpos);
132-
} else {
133-
/* This value didn't already exist; add it to the end */
134-
135-
if (comment) {
136-
data += `\n\n${comment}`;
137-
}
138-
139-
data += `${SET}${encodedValue}`;
140-
}
141-
}
142-
65+
function updateDotenv(add, answers) {
14366
add(
14467
"GRAPHILE_LICENSE",
14568
null,
@@ -262,10 +185,6 @@ async function updateDotenv(answers) {
262185
# The name of the folder you cloned graphile-starter to (so we can run docker-compose inside a container):`
263186
);
264187
}
265-
266-
data = data.trim() + "\n";
267-
268-
await fsp.writeFile(DOTENV_PATH, data);
269188
}
270189

271190
async function main() {
@@ -335,10 +254,12 @@ async function main() {
335254
];
336255
const answers = await inquirer.prompt(questions);
337256

338-
await updateDotenv({
339-
...config,
340-
...answers,
341-
});
257+
await withDotenvUpdater(answers, add =>
258+
updateDotenv(add, {
259+
...config,
260+
...answers,
261+
})
262+
);
342263

343264
// And perform setup
344265
spawnSync(yarnCmd, ["server", "build"]);
@@ -397,9 +318,14 @@ async function main() {
397318
await pgPool.query('select true as "Connection test";');
398319
break;
399320
} catch (e) {
321+
if (e.code === "28P01") {
322+
throw e;
323+
}
400324
attempts++;
401325
if (attempts <= 30) {
402-
console.log(`Database is not ready yet (attempt ${attempts})`);
326+
console.log(
327+
`Database is not ready yet (attempt ${attempts}): ${e.message}`
328+
);
403329
} else {
404330
console.log(`Database never came up, aborting :(`);
405331
process.exit(1);

0 commit comments

Comments
 (0)