-
Notifications
You must be signed in to change notification settings - Fork 1
/
construct.coffee
359 lines (327 loc) · 17.5 KB
/
construct.coffee
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# NOTE only up to 32 cloudflare workers variables are allowed, a combined count of
# secret variables and normal env variables (e.g. set via CF UI)
# each uploaded secret can only be up to 1KB in size
# deploy with environment choice? dev/live/other?
# need node and npm. If not present, this will fail
fs = require 'fs'
coffee = require 'coffeescript'
https = require 'https'
{exec} = require 'child_process'
crypto = require 'crypto'
# uglify-js (2.x, such as 2.8.29) and uglifycss are also required for client transforms
# see below in the client section where it will attempt to require them
ENV = '' # if env is provided, look in usual secrets folders for files prefixed with ENV_ and prefer them, but if not present, use whatever else is there
GROUP = '' # if group is provided look for secrets folders prefixed with GROUP_ and ONLY use those. If they're not present don't use the default ones.
# so group could be used to send workers to completely different CF accounts with separate configs, whereas env allows overwrites of certain configs
# should ENV try to deploy to a script ID prefixed with the ENV name as well? It would have to exist first, or does CF API create it on PUT?
KEEPTOKEN = true
args = process.argv.slice 2
rm = []
for a of args
arg = args[a]
console.log a, arg
if arg.toLowerCase().indexOf('env=') isnt -1
rm.push a
ENV = arg.split('=')[1].trim() + '_'
else if arg.toLowerCase().indexOf('group=') isnt -1
rm.push a
GROUP = arg.split('=')[1].trim() + '_'
else if arg.toLowerCase().indexOf('token') isnt -1
rm.push a
KEEPTOKEN = false
for r in rm
delete args[r]
args = args.filter (el) -> return el isnt null
if GROUP
console.log 'Deploying with any secrets in group secrets folders named with prefix ' + GROUP + (if ENV then ', overriding with files prefixed with ' + ENV else '')
else if ENV
console.log 'Deploying with default secrets folders, overriding with files prefixed with ' + ENV
if args.length and 'worker' not in args and 'server' not in args and ('build' in args or 'deploy' in args)
args.push 'worker' # do both unless only one is specified
args.push 'server'
if not args.length
args = ['build', 'deploy', 'worker', 'server', 'secrets', 'client']
console.log "Doing full deploy, which tries all options:"
console.log "build - build one or both of worker and server (defaults to both unless only one is specified)"
console.log "deploy - deploy one or both of worker and server (if settings configure somewhere to deploy to)"
console.log "worker - build/deploy the worker (deploys to cloudflare if suitable env settings are available)"
console.log "server - build/deploy the server (deploys to remote server if suitable env settings are available)"
console.log "secrets - deploy secrets (if any) to cloudflare worker (secrets are always included in a server build)"
console.log "client - builds the supplemental browser scripts in the client directory\n"
if not fs.existsSync('./' + GROUP + 'secrets') and not fs.existsSync('./worker/' + GROUP + 'secrets') and not fs.existsSync('./server/' + GROUP + 'secrets')
console.log "No settings or secrets available, so DEMO version being built. Read more about settings and secrets in the docs."
DEMO = true
else
DEMO = false
if KEEPTOKEN
try
SYSTOKEN = fs.readFileSync('./server/dist/server.js').toString().split('SECRETS_SETTINGS')[1].split('"system":"')[1].split('"')[0]
console.log 'keeping system token', SYSTOKEN
catch
console.log 'could not find system token to keep, creating a new one'
SYSTOKEN = crypto.randomBytes(32).toString 'hex'
else
SYSTOKEN = crypto.randomBytes(32).toString 'hex'
DATE = new Date().toString().split(' (')[0]
VERSION = '' # get read from main worker file
BG = '' # get read from worker settings, and if present is used to ping the URL to check it is up
CNS = []
if fs.existsSync './' + GROUP + 'secrets/' + ENV + 'construct.json'
CNS = JSON.parse fs.readFileSync('./' + GROUP + 'secrets/' + ENV + 'construct.json').toString()
CNS = [CNS] if not Array.isArray CNS
else
console.log "No cloudflare config loaded."
console.log "A folder called " + GROUP + "secrets should be placed at the top level directory / root of the project, and also one each in server/ and worker/"
console.log "Anything in these folders will be ignored by any future git commits, so it is safe to put secret data in them."
console.log "Deployment to cloudflare requires a ./secrets/construct.json file containing an object with keys ACCOUNT_ID, SCRIPT_ID, API_TOKEN"
_sr = (data, opts) ->
return new Promise (resolve, reject) =>
req = https.request opts, (res) =>
if res.statusCode isnt 200
console.log res.statusCode
body = ''
res.on 'data', (chunk) -> body += chunk
res.on 'end', () ->
try body = JSON.parse body
resolve body
req.on 'error', (err) =>
console.log 'SYNC PUT ERROR', err
reject err
req.write data
req.end()
_put = (data) ->
if typeof data isnt 'string'
sn = data.name
data = JSON.stringify data
ps = '/secrets'
else
sn = ''
ps = ''
for CNE in CNS
console.log 'Sending ' + (if sn then sn + ' ' else '') + 'for ' + CNE.SCRIPT_ID + (if CNE.NAME then ' on ' + CNE.NAME else '')
if CNE.ACCOUNT_ID and CNE.SCRIPT_ID and CNE.API_TOKEN
# data is either an object to send to secrets, or a file handle to stream to worker
ret = await _sr data,
hostname: 'api.cloudflare.com'
port: 443
path: '/client/v4/accounts/' + CNE.ACCOUNT_ID + '/workers/scripts/' + CNE.SCRIPT_ID + ps
method: 'PUT'
headers:
'Content-Type': if ps then 'application/json' else 'application/javascript'
#'Content-Length': data.length
'Authorization': 'Bearer ' + CNE.API_TOKEN
try console.log('ERROR', e.code, e.message) for e in ret.errors
try console.log (ret?.success ? 'false'), CNE.SCRIPT_ID, CNE.NAME, sn
console.log '------'
_exec = (cmd) ->
return new Promise (d) ->
exec cmd, (e, s) ->
if e
console.log e
return
d s
_walk = (drt, names=[]) ->
# list all files in a dir, including subdirs, sorted alpbabetically / hierarchically
dirs = []
for n in fs.readdirSync(drt).sort()
if n.indexOf('.') is -1 or n.split('.').pop() in ['js', 'coffee', 'json', 'css']
if fs.lstatSync(drt + '/' + n).isDirectory()
dirs.push drt + '/' + n
else
names.push drt + '/' + n
names = _walk(d, names) for d in dirs
return names
_w = () ->
# add checks for things that need to be installed? could be handy
#console.log await _exec 'which google-chrome'
wfl = ''
sfl = ''
if 'build' in args
if 'worker' in args or 'server' in args
console.log "Building worker" + (if 'worker' in args then '' else ' (necessary for compilation into server)')
if fs.existsSync './worker/dist'
try fs.unlinkSync './worker/dist/worker.js'
else
fs.mkdirSync './worker/dist'
console.log await _exec 'cd ./worker && npm install'
for fl in await _walk './worker/src'
console.log fl
if fl.endsWith '.coffee'
wfl += coffee.compile fs.readFileSync(fl).toString(), bare: true
else if fl.endsWith '.js'
wfl += fs.readFileSync(fl).toString()
wfl += '\n'
wfl += '\nS.built = \"' + DATE + '\";'
wfl += '\nS.demo = true;' if DEMO
VERSION = wfl.split('S.version = ')[1].split('\n')[0].split('//')[0].replace(/"/g, '').replace(/'/g, '').replace(';','').trim()
if VERSION
if fs.existsSync './worker/package.json'
wp = JSON.parse fs.readFileSync('./worker/package.json').toString()
wp.version = VERSION
fs.writeFileSync './worker/package.json', JSON.stringify wp, '', 2
if fs.existsSync './server/package.json'
sp = JSON.parse fs.readFileSync('./server/package.json').toString()
sp.version = VERSION
fs.writeFileSync './server/package.json', JSON.stringify sp, '', 2
if 'server' in args
console.log "Building server"
if not wfl.length
if fs.existsSync './worker/dist/worker.js'
wfl = fs.readFileSync('./worker/dist/worker.js').toString()
else
console.log 'Server build cannot complete until worker build has run at least once, making ./worker/dist/worker.js available for incorporation'
process.exit()
sfl = wfl
if fs.existsSync './server/dist'
try fs.unlinkSync './server/dist/server.js'
else
fs.mkdirSync './server/dist'
console.log await _exec 'cd server && npm install'
for fl in await _walk './server/src'
console.log fl
if fl.endsWith '.coffee'
sfl += coffee.compile fs.readFileSync(fl).toString(), bare: true
else if fl.endsWith '.js'
sfl += fs.readFileSync(fl).toString()
sfl += '\n'
if wfl
adds = []
for line in sfl.split '\n'
if line.startsWith('P.') and (line.indexOf('function') isnt -1 or line.replace(/\s/g, '').indexOf('={') isnt -1 or line.indexOf('._') isnt -1) and line.indexOf('->') is -1 # avoid commented out coffeescript definitions, by the time they're converted to js these would not be defined with -> functions
bgp = line.split('=')[0].split('._')[0].replace(/\s/g, '')
if wfl.indexOf('\n' + bgp) is -1 or bgp in adds
adds.push(bgp) if bgp not in adds
if line.indexOf('function') is -1
console.log 'adding ' + line + ' to worker stub'
wfl += '\n' + line + '// added by constructor\n'
else
console.log 'adding ' + bgp + ' bg stub to worker'
wfl += '\n' + bgp + ' = {_bg: true}' + '// added by constructor\n'
if 'client' in args
# TODO add a browser build of the main app too, at least all parts that can run browser-side
console.log "Building client files"
for fl in await _walk './client'
if fl.endsWith '.coffee'
console.log fl
cpl = coffee.compile fs.readFileSync(fl).toString(), bare: true
fs.writeFileSync fl.replace('.coffee', '.js'), cpl
try
uglifyjs = require 'uglify-js'
uglyjs = uglifyjs.minify flj: cpl
fs.writeFileSync fl.replace('.coffee', '.min.js'), uglyjs.code
# this can also be used to build bundles, given an object of filenames and their contents
# secrets/construct.json could be extended to define bundle lists, if that becomes useful in future
#jshashname = 'pradm_' + crypto.createHash('md5').update(uglyjs.code).digest('hex')
catch
console.log 'Could not minify - maybe uglify-js needs to be installed'
else if fl.endsWith('.js') and not fl.endsWith('.min.js') and not fs.existsSync fl.replace '.js', '.coffee'
try
console.log fl
uglifyjs = require 'uglify-js'
uglyjs = uglifyjs.minify fl: cpl
fs.writeFileSync fl.replace('.js', '.min.js'), uglyjs.code
catch
console.log 'Could not minify - maybe uglify-js needs to be installed'
else if fl.endsWith('.css') and not fl.endsWith '.min.css'
try
console.log fl
uglifycss = require 'uglifycss'
uglycss = uglifycss.processFiles [fl] # this can also be used to bundle a list of filenames
#csshashname = 'pradm_' + crypto.createHash('md5').update(uglycss).digest('hex')
fs.writeFileSync fl.replace('.css', '.min.css'), uglycss
catch
console.log 'Could not minify - maybe uglifycss needs to be installed'
if 'server' in args
if fs.existsSync './server/' + GROUP + 'secrets'
fls = fs.readdirSync './server/' + GROUP + 'secrets'
if fls.length
for F in fls
# only use files prefixed with env if it is present, or the default files if no file for the env is available
# don't use any that are prefixed for some other env - NOTE this means settings files MUST NOT have underscores in them except for when separating the env
if ENV is '' or F.indexOf(ENV) is 0 or (F.indexOf('_') is -1 and not fs.existsSync './server/' + GROUP + 'secrets/' + ENV + F)
SECRETS_DATA = JSON.parse fs.readFileSync('./server/' + GROUP + 'secrets/' + F).toString()
SECRETS_NAME = 'SECRETS_' + F.split('.')[0].toUpperCase().replace ENV + '_', ''
console.log 'Saving server ' + SECRETS_NAME + ' to server file'
# these are written to file as strings to be interpreted in the file, because that is how they'd
# have to be interpreted from CF workers secrets anyway - so no point handling strings sometimes
# and objects sometimes in the main code - just deliver these all as strings for parsing in the main.
sfl = "var " + SECRETS_NAME + " = '" + JSON.stringify(SECRETS_DATA) + "';\n" + sfl
else
console.log "No server secrets json files present, so no extra server secrets built into server script\n"
else
console.log "No server secrets folder present so no extra server secrets built into server script\n"
if 'worker' in args or 'server' in args
if fs.existsSync './worker/' + GROUP + 'secrets'
wfls = fs.readdirSync './worker/' + GROUP + 'secrets'
if wfls.length
# TODO find necessary KV namespaces from the code / config and create them via cloudflare API?
for WF in wfls
if ENV is '' or WF.indexOf(ENV) is 0 or (WF.indexOf('_') is -1 and not fs.existsSync './worker/' + GROUP + 'secrets/' + ENV + WF)
SECRETS_DATA = JSON.parse fs.readFileSync('./worker/' + GROUP + 'secrets/' + WF).toString()
try BG = SECRETS_DATA.bg if not BG
SECRETS_NAME = 'SECRETS_' + WF.split('.')[0].toUpperCase().replace ENV + '_', ''
if (if WF.includes('_') then WF.split('_').pop() else WF).split('.')[0].toLowerCase() is 'settings' and not SECRETS_DATA.system
console.log 'Adding system token', SYSTOKEN
SECRETS_DATA.system = SYSTOKEN
if 'worker' in args
if 'secrets' in args
if not CNS.length
console.log "To push secrets to cloudflare, cloudflare account ID, API token, and script ID must be set to keys ACCOUNT_ID, API_TOKEN, SCRIPT_ID, in ./secrets/construct.json"
else
console.log 'Sending worker ' + SECRETS_NAME + ' secrets to cloudflare'
await _put {name: SECRETS_NAME, text: JSON.stringify(SECRETS_DATA), type: 'secret_text'}
if 'server' in args
console.log 'Saving worker ' + SECRETS_NAME + ' to server file'
sfl = "var " + SECRETS_NAME + " = '" + JSON.stringify(SECRETS_DATA) + "';\n" + sfl
else
console.log "No worker secrets json files present, so no worker secrets imported to cloudlfare or built into server script\n"
else
console.log "No worker secrets folder present, so no worker secrets imported to cloudflare or built into server script\n"
if 'build' in args
if 'worker' in args or 'server' in args # server needs worker to be built as well anyway
fs.writeFileSync './worker/dist/worker.js', wfl
await _exec 'cd ./worker && npm run build'
try fs.unlinkSync './worker/dist/worker.min.js.LICENSE.txt' #webpack adds this file, but it's not needed
console.log 'Worker file size ' + (fs.statSync('./worker/dist/worker.min.js').size)/1024 + 'K'
if 'server' in args
fs.writeFileSync './server/dist/server.js', sfl
try fs.unlinkSync './server/dist/server.min.js.LICENSE.txt' #webpack adds this file, but it's not needed
await _exec 'cd ./server && npm run build'
if 'deploy' in args
if 'worker' in args
if fs.existsSync './worker/dist/worker.min.js'
if not CNS.length
console.log "To deploy worker to cloudflare, cloudflare account ID, API token, and script ID must be set to keys ACCOUNT_ID, API_TOKEN, SCRIPT_ID, in secrets/construct.json"
else
console.log "Deploying worker to cloudflare"
await _put fs.readFileSync('./worker/dist/worker.min.js').toString()
else
console.log "No worker file available to deploy to cloudflare at worker/dist/worker.min.js\n"
if 'server' in args
for CNE in CNS
if CNE.SCP
if fs.existsSync './server/dist/server.min.js'
console.log "Deploying server to " + CNE.SCP + (if CNE.NAME then ' for ' + CNE.NAME else '')
console.log await _exec 'scp ./server/dist/server.min.js ' + CNE.SCP
else
console.log "No server file available to deploy at server/dist/server.min.js\n"
break
if 'construct' in args
# not done by default, and not mentioned. Unnecessary, because even if a js is constructed, it still needs coffee for the translation stage
try fs.writeFileSync './construct.js', coffee.compile fs.readFileSync('./construct.coffee').toString(), bare: true
if VERSION
console.log 'v' + VERSION + ' built at ' + DATE
if BG.startsWith 'http' # this will confirm version deployment to BG if available, and also causes any scheduled tasks to be loaded on restart
setTimeout () ->
req = https.request {hostname: BG.split('://')[1], port: if BG.startsWith('https') then 443 else 80}, (res) ->
body = ''
res.on 'data', (chunk) -> body += chunk
res.on 'end', () ->
try
body = JSON.parse body
console.log 'Ping', BG, if VERSION.includes(body.version) then 'confirms ' + body.version else 'FAILED TO FIND expected version ' + VERSION + ', got ' + body.version
catch
console.log 'Ping ERROR'
req.end()
, 1000
_w()