-
-
Notifications
You must be signed in to change notification settings - Fork 375
/
Copy pathserve.js
256 lines (223 loc) · 6.9 KB
/
serve.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
import asyncCommand from '../lib/async-command';
import path from 'path';
import fs from 'fs.promised';
import tmp from 'tmp';
import { execFile } from 'child_process';
import getSslCert from '../lib/ssl-cert';
import persistencePath from 'persist-path';
import simplehttp2server from 'simplehttp2server';
import { isDir } from '../util';
export default asyncCommand({
command: 'serve [dir]',
desc: 'Start an HTTP2 static fileserver.',
builder: {
cwd: {
description: 'A directory to use instead of $PWD.',
default: '.'
},
dir: {
description: 'Directory root to serve static files from.',
default: 'build'
},
server: {
description: 'Which server to run, or "config" to produce a firebase config.',
choices: [
'simplehttp2server',
'superstatic',
'config'
],
default: 'simplehttp2server'
},
dest: {
description: 'Directory or filename where firebase.json should be written\n (used for --server config)',
defaultDescription: '-'
},
port: {
description: 'Port to start a server on.',
defaultDescription: 'PORT || 8080',
alias: 'p'
},
cors: {
description: 'Set allowed origins',
defaultDescription: 'https://localhost:${PORT}'
}
},
async handler(argv) {
await serve(argv);
}
});
/** Spawn an HTTP2 server.
* @param {object} options
* @param {string} [options.config] Filename of a Firebase Hosting configuration, relative to cwd
* @param {string} [options.cwd] Directory to host intead of `process.cwd()`
* @param {string} [options.dir='.'] Static asset directory, relative to `options.cwd`
* @param {number|string} [options.port] Port to start the http server on
*/
async function serve(options) {
let dir = path.resolve(options.cwd, options.dir || '.');
// Allow overriding default hosting config via `--config firebase.json`:
let configFile = options.config ? options.config : path.resolve(__dirname, '../resources/static-app.json');
let config = await readJson(configFile);
// Simplehttp2server can only load certs from its CWD, so we spawn it in lib/resources where the certs are located.
// The "public" field is then set to the absolute path for our static file root.
config.public = dir;
// Load and apply Link headers from a push manifest if one exists:
let pushManifest = await readJson(path.resolve(dir, 'push-manifest.json'));
if (pushManifest) {
config.headers = [].concat(
config.headers || [],
createHeadersFromPushManifest(pushManifest)
);
}
// Switch configuration to be a temp file with the preload headers merged in:
configFile = await tmpFile({ postfix: '.json' });
await fs.writeFile(configFile, JSON.stringify(config));
let port = options.port || process.env.PORT || 8080;
await serveHttp2({
options,
config: configFile,
configObj: config,
server: options.server,
cors: options.cors || `https://localhost:${port}`,
port,
dir,
cwd: path.resolve(__dirname, '../resources')
});
}
/** Produces an Array of Link rel=preload headers from a push manifest.
* Headers are in Firebase Hosting format.
*/
function createHeadersFromPushManifest(pushManifest) {
let headers = [];
for (let source in pushManifest) {
if (pushManifest.hasOwnProperty(source)) {
let section = pushManifest[source],
links = [];
for (let file in section) {
if (section.hasOwnProperty(file)) {
links.push({
url: '/' + file.replace(/^\//g,''),
...section[file]
});
}
}
links = links.sort( (a, b) => {
let diff = b.weight - a.weight;
if (!diff) {
if (b.url.match(/bundle\.js$/)) return 1;
return b.url.match(/\.js$/) ? 1 : 0;
}
return diff;
});
headers.push({
source,
headers: [{
key: 'Link',
value: links.map( ({ url, type }) =>
`<${url}>; rel=preload; as=${type}`
).join(', ')
}]
});
}
}
return headers;
}
/** Start an HTTP2 static fileserver with push support.
* @param {object} options
* @param {string} [options.config] Server configuration file in Firebase Hosting format.
* @param {number|string} [options.port=8080] Port to run the server on.
* @param {string} [options.cwd=process.cwd()] Directory from which to serve static files.
*/
const serveHttp2 = options => Promise.resolve(options)
.then(SERVERS[options.server || 'simplehttp2server'])
.then( args => new Promise( (resolve, reject) => {
if (typeof args==='string') {
process.stdout.write(args + '\n');
return resolve();
}
let child = execFile(args[0], args.slice(1), {
cwd: options.cwd,
encoding: 'utf8'
}, (err, stdout, stderr) => {
if (err) process.stderr.write('\n server error> '+err+'\n'+stderr);
else process.stdout.write('\n server spawned> '+stdout);
if (err) return reject(err + '\n' + stderr);
else resolve();
});
function proxy(type) {
child[type].on('data', data => {
data = data.replace(/^(\s*\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})?\s*/gm, '');
if (data.match(/\bRequest for\b/gim)) return;
process[type].write(` \u001b[32m${data}\u001b[39m`);
});
}
proxy('stdout');
proxy('stderr');
process.stdin.pipe(child.stdin);
}));
const SERVERS = {
async simplehttp2server(options) {
let ssl = await getSslCert();
if (ssl) {
await fs.writeFile(path.resolve(options.cwd, 'key.pem'), ssl.key);
await fs.writeFile(path.resolve(options.cwd, 'cert.pem'), ssl.cert);
} else {
options.cwd = persistencePath('preact-cli');
process.stderr.write(`Falling back to shared directory + simplehttp2server.\n(dir: ${options.cwd})\n`);
}
return [
simplehttp2server,
'-cors', options.cors,
'-config', options.config,
'-listen', `:${options.port}`
];
},
superstatic(options) {
return [
'superstatic',
path.relative(options.cwd, options.dir),
'--gzip',
'-p', options.port,
'-c', JSON.stringify({ ...options.configObj, public: undefined })
];
},
/** Writes the firebase/superstatic/simplehttp2server configuration to stdout or a file. */
async config({ configObj, options }) {
let dir = process.cwd(),
outfile;
if (options.dest && options.dest!=='-') {
if (isDir(options.dest)) {
dir = options.dest;
outfile = 'firebase.json';
} else {
dir = path.dirname(options.dest);
outfile = path.basename(options.dest);
}
}
let config = await readJson(path.resolve(dir, outfile));
config = Object.assign({}, config, {
hosting: {
...configObj,
public: path.relative(dir, configObj.public),
}
});
config = JSON.stringify(config, null, 2);
if (outfile) {
await fs.writeFile(path.resolve(dir, outfile), config);
return `Configuration written to ${outfile}.`;
} else {
return config;
}
}
};
/** Create a temporary file. See https://npm.im/tmp */
const tmpFile = opts => new Promise((res, rej) => {
tmp.file(opts, (err, path) => err ? rej(err) : res(path));
});
/** Safely read a JSON file. Failures simply return `undefined`. */
async function readJson(filename) {
try {
return JSON.parse(await fs.readFile(filename, 'utf8'));
}
catch (e) { }
}