UNPKG

10.3 kBJavaScriptView Raw
1import fs from 'fs-extra';
2import { getContentType, SandboxType, } from '@fab/core';
3import { _log, InvalidConfigError, FabServerError, JSON5Config } from '@fab/cli';
4import { readFilesFromZip } from './utils';
5import v8_sandbox from './sandboxes/v8-isolate';
6import { Cache } from './cache';
7import node_vm_sandbox from '@fab/sandbox-node-vm';
8import url from 'url';
9import http from 'http';
10import express from 'express';
11import concat from 'concat-stream';
12import fetch, { Request as NodeFetchRequest } from 'cross-fetch';
13import { pathToSHA512 } from 'file-to-sha512';
14import Stream from 'stream';
15import { watcher } from '@fab/cli';
16import httpProxy from 'http-proxy';
17// @ts-ignore
18import nodeToWebStream from 'readable-stream-node-to-web';
19function isRequest(fetch_res) {
20 var _a;
21 return (fetch_res instanceof NodeFetchRequest || ((_a = fetch_res.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'Request');
22}
23const log = _log(`Server`);
24async function streamResponse(fetch_res, res) {
25 res.status(fetch_res.status);
26 // This is a NodeFetch response, which has this method, but
27 // the @fab/core types are from dom.ts, which doesn't. This
28 // was the easiest workaround for now.
29 // @ts-ignore
30 const response_headers = fetch_res.headers.raw();
31 delete response_headers['content-encoding'];
32 Object.keys(response_headers).forEach((header) => {
33 const values = response_headers[header];
34 res.set(header, values.length === 1 ? values[0] : values);
35 });
36 const shouldSetChunkedTransferEncoding = !response_headers['content-length'] && !response_headers['transfer-encoding'];
37 const body = fetch_res.body;
38 if (body) {
39 if (typeof body.getReader === 'function') {
40 if (shouldSetChunkedTransferEncoding)
41 res.set('transfer-encoding', 'chunked');
42 const reader = body.getReader();
43 let x;
44 while ((x = await reader.read())) {
45 const { done, value } = x;
46 if (done)
47 break;
48 if (value) {
49 if (typeof value === 'string') {
50 res.write(value);
51 }
52 else {
53 res.write(Buffer.from(value));
54 }
55 }
56 }
57 res.end();
58 }
59 else if (body instanceof Stream) {
60 if (!response_headers['transfer-encoding'])
61 res.set('transfer-encoding', 'chunked');
62 await new Promise((resolve, reject) => {
63 body.on('data', (chunk) => res.write(chunk));
64 body.on('error', reject);
65 body.on('end', resolve);
66 });
67 res.end();
68 }
69 else {
70 const blob = await fetch_res.arrayBuffer();
71 res.send(Buffer.from(blob));
72 }
73 }
74 else {
75 res.end();
76 }
77}
78function createEnhancedFetch(port) {
79 return async function enchanched_fetch(url, init) {
80 const request_url = typeof url === 'string' ? url : url.url;
81 const fetch_url = request_url.startsWith('/')
82 ? // Need a smarter way to re-enter the FAB, eventually...
83 `http://localhost:${port}${request_url}`
84 : url;
85 const response = await fetch(fetch_url, init);
86 return Object.create(response, {
87 body: {
88 value: Object.create(response.body, {
89 getReader: {
90 get() {
91 const webStream = nodeToWebStream(response.body);
92 return webStream.getReader.bind(webStream);
93 },
94 },
95 }),
96 },
97 });
98 };
99}
100class Server {
101 constructor(filename, args) {
102 this.filename = filename;
103 this.port = parseInt(args.port);
104 // TODO: cert stuff
105 if (isNaN(this.port)) {
106 throw new InvalidConfigError(`Invalid port, expected a number, got '${args.port}'`);
107 }
108 this.config = args.config;
109 this.env = args.env;
110 this.enchanched_fetch = createEnhancedFetch(this.port);
111 }
112 async createRenderer(src, runtimeType) {
113 const renderer = (await runtimeType) === SandboxType.v8isolate
114 ? await v8_sandbox(src)
115 : await node_vm_sandbox(src, this.enchanched_fetch);
116 const bundle_id = (await pathToSHA512(this.filename)).slice(0, 32);
117 const cache = new Cache();
118 // Support pre v0.2 FABs
119 if (typeof renderer.initialize === 'function') {
120 renderer.initialize({ bundle_id, cache });
121 }
122 return renderer;
123 }
124 async renderReq(renderer, req, settings_overrides) {
125 var _a;
126 const method = req.method;
127 const headers = req.headers;
128 const url = `${req.protocol}://${req.headers.host}${req.url}`;
129 const fetch_req = new NodeFetchRequest(url, {
130 method,
131 headers,
132 ...((method === 'POST' || method === 'PUT' || method === 'PATCH') ? { body: req.body } : {}),
133 });
134 const production_settings = (_a = renderer.metadata) === null || _a === void 0 ? void 0 : _a.production_settings;
135 let fetch_res;
136 try {
137 fetch_res = await renderer.render(
138 // @ts-ignore
139 fetch_req, Object.assign({}, production_settings, settings_overrides));
140 }
141 catch (err) {
142 const msg = `An error occurred calling the render method on the FAB: \nError: \n${err}`;
143 throw new Error(msg);
144 }
145 try {
146 if (fetch_res && isRequest(fetch_res)) {
147 fetch_res = await this.enchanched_fetch(fetch_res);
148 }
149 }
150 catch (err) {
151 const msg = `An error occurred proxying a request returned from the FAB: \nError:\n${err}\nRequest:\n${fetch_res}`;
152 throw new Error(msg);
153 }
154 if (!fetch_res) {
155 const msg = `Nothing was returned from the FAB renderer.`;
156 throw new Error(msg);
157 }
158 return fetch_res;
159 }
160 setupExpress(renderer, settings_overrides, files) {
161 const app = express();
162 app.use((req, res, next) => {
163 try {
164 next();
165 }
166 catch (err) {
167 log(`ERROR serving: ${req.url}`);
168 log(err);
169 if (!res.headersSent) {
170 res.writeHead(500, `Internal Error:\n${err}`);
171 }
172 res.end();
173 }
174 });
175 app.use((req, _res, next) => {
176 req.pipe(concat((data) => {
177 req.body = data.toString();
178 next();
179 }));
180 });
181 app.use((req, _res, next) => {
182 log(`🖤${req.url}🖤`);
183 next();
184 });
185 app.get('/_assets/*', (req, res) => {
186 const pathname = url.parse(req.url).pathname;
187 res.setHeader('Content-Type', getContentType(pathname));
188 res.setHeader('Cache-Control', 'immutable');
189 res.end(files[pathname]);
190 });
191 app.all('*', async (req, res) => {
192 const fetch_res = await this.renderReq(renderer, req, settings_overrides);
193 streamResponse(fetch_res, res);
194 });
195 return app;
196 }
197 async createHandler(runtimeType) {
198 log(`Reading 💛${this.filename}💛...`);
199 const files = await readFilesFromZip(this.filename);
200 const src_buffer = files['/server.js'];
201 if (!src_buffer) {
202 throw new FabServerError('Malformed FAB. Missing /server.js');
203 }
204 const src = src_buffer.toString('utf8');
205 log.tick(`Done. Booting VM...`);
206 const settings_overrides = await this.getSettingsOverrides();
207 const renderer = await this.createRenderer(src, runtimeType);
208 log.tick(`Done. Booting FAB server...`);
209 return this.setupExpress(renderer, settings_overrides, files);
210 }
211 async serve(runtimeType, watching = false, proxyWs) {
212 if (!(await fs.pathExists(this.filename))) {
213 throw new FabServerError(`Could not find file '${this.filename}'`);
214 }
215 let app;
216 let proxy;
217 let server;
218 const bootServer = async () => {
219 app = await this.createHandler(runtimeType);
220 await new Promise((resolve, _reject) => {
221 if (!server) {
222 server = http.createServer((req, res) => app(req, res));
223 if (proxyWs) {
224 if (!proxy) {
225 proxy = httpProxy.createProxyServer({
226 target: `ws://localhost:${proxyWs}`,
227 ws: true,
228 });
229 }
230 // ? https.createServer({ key: this.key, cert: this.cert }, app)
231 server.on('upgrade', (req, socket, head) => {
232 proxy.ws(req, socket, head);
233 });
234 }
235 server.listen(this.port, resolve);
236 }
237 else {
238 resolve();
239 }
240 });
241 };
242 if (watching) {
243 log.note(`Watching 💛${this.filename}💛 for changes...`);
244 await watcher([this.filename], bootServer, {
245 awaitWriteFinish: {
246 stabilityThreshold: 200,
247 pollInterval: 50,
248 },
249 });
250 }
251 else {
252 await bootServer();
253 }
254 }
255 async getSettingsOverrides() {
256 var _a;
257 if (!this.env)
258 return {};
259 const config = await JSON5Config.readFrom(this.config);
260 const overrides = (_a = config.data.settings) === null || _a === void 0 ? void 0 : _a[this.env];
261 if (!overrides) {
262 throw new InvalidConfigError(`No environment '${this.env}' found in ${this.config}!`);
263 }
264 return overrides;
265 }
266}
267const createServer = (filename, args) => new Server(filename, args);
268const serverExports = { createServer };
269export default serverExports;
270//# sourceMappingURL=index.js.map
\No newline at end of file