-
Notifications
You must be signed in to change notification settings - Fork 21
/
file.js
177 lines (156 loc) · 4.48 KB
/
file.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
// interface for reading/writing to .env file
'use strict'
const fs = require('fs-extra')
const cli = require('heroku-cli-util')
const HOME_DIR = require('os').homedir()
const DEFAULT_FNAME = '.env'
const header = '# this file was created automatically by heroku-config\n\n'
const objToFileFormat = (obj, flags = {}) => {
let res = `${header}`
// always write keys alphabetically
// makes file writing deterministic
let keys = Object.keys(obj).sort()
keys.forEach(key => {
if (flags.unquoted) {
res += `${key}=${obj[key]}\n`
} else {
res += `${key}="${obj[key]}"\n`
}
})
return res
}
const defaultMulti = () => {
return {
key: '',
values: []
}
}
// checks whether this is the end of a multi
const isEnding = s => {
return s[s.length - 1] === '"'
}
const isSkippable = s => {
return s[0] === '#' || s === ''
}
const unquote = s => {
return s.replace(/^"|"$/g, '')
}
const objFromFileFormat = (s, flags = {}) => {
let res = {}
let splitter
let multi = defaultMulti()
// could also use process.platform but this feels more reliable
if (s.match(/\r\n/)) {
splitter = '\r\n'
} else {
splitter = '\n'
}
const lines = s.split(splitter)
let expandedVars = ''
if (flags.expanded) {
// this is a regex string that shows non-standard values that are accepted
expandedVars = String.raw`\.-`
}
const lineRegex = new RegExp(
String.raw`^(export)?\s?([a-zA-Z_][a-zA-Z0-9_${expandedVars}]*)\s?=\s?(.*)$`
)
lines.forEach(line => {
if (isSkippable(line)) {
return
}
let maybeKVPair = line.match(lineRegex)
if (maybeKVPair) {
// regular line
let key = maybeKVPair[2]
const quotedVal = maybeKVPair[3]
if ((quotedVal[0] === '"') & !isEnding(quotedVal)) {
// start of multi
multi.key = key
multi.values.push(quotedVal)
} else {
if (res[key] && !flags.quiet) {
cli.warn(`[WARN]: "${key}" is in env file twice`)
}
res[key] = unquote(quotedVal)
}
} else if (multi.key) {
// not a regular looking line, but we're in the middle of a multi
multi.values.push(line)
if (isEnding(line)) {
res[multi.key] = unquote(multi.values.join('\n'))
multi = defaultMulti()
}
} else {
// borked
if (!flags.quiet) {
cli.warn(`[WARN]: unable to parse line: ${line}`)
}
}
})
return res
}
const question = val => {
return [
`Your config has a value called "${val}", which is usually pulled in error. Should we:`,
'[d]elete | [i]gnore | [a]lways (delete) | [n]ever (delete)',
'that key/value pair for this app?'
].join('\n\n')
}
module.exports = {
read: (fname = DEFAULT_FNAME, flags) => {
return fs
.readFile(fname, 'utf-8')
.then(data => {
return Promise.resolve(objFromFileFormat(data, flags))
})
.catch(() => {
// if it doesn't exist or we can't read, just start from scratch
return Promise.resolve({})
})
},
write: (obj, fname = DEFAULT_FNAME, flags = {}) => {
return fs
.writeFile(fname, objToFileFormat(obj, flags))
.then(() => {
if (!flags.quiet) {
cli.log(`Successfully wrote config to "${fname}"!`)
}
})
.catch(err => {
return Promise.reject(
new Error(`Error writing to file "${fname}" (${err.message})`)
)
})
},
// eslint-disable-next-line generator-star-spacing
shouldDeleteProd: function*(context, val) {
const path = require('path')
const settingsUrl = path.join(HOME_DIR, '.heroku_config_settings.json')
let settings
try {
settings = JSON.parse(fs.readFileSync(settingsUrl, 'utf-8'))
} catch (e) {
settings = {}
}
if (settings[context.app] === undefined) {
let answer = (yield cli.prompt(question(val))).toLowerCase()
if (answer === 'd' || answer === 'delete') {
return true
} else if (answer === 'i' || answer === 'ignore') {
return false
} else if (answer === 'a' || answer === 'always') {
settings[context.app] = true
fs.writeFileSync(settingsUrl, JSON.stringify(settings))
return true
} else if (answer === 'n' || answer === 'never') {
settings[context.app] = false
fs.writeFileSync(settingsUrl, JSON.stringify(settings))
return false
} else {
cli.exit(1, 'Invalid command. Use one of [d|i|a|n] instead')
}
} else {
return settings[context.app]
}
}
}