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 console.log('HELLO!');
104 this.port = parseInt(args.port);
105 // TODO: cert stuff
106 if (isNaN(this.port)) {
107 throw new InvalidConfigError(`Invalid port, expected a number, got '${args.port}'`);
108 }
109 this.config = args.config;
110 this.env = args.env;
111 this.enchanched_fetch = createEnhancedFetch(this.port);
112 }
113 async createRenderer(src, runtimeType) {
114 const renderer = (await runtimeType) === SandboxType.v8isolate
115 ? await v8_sandbox(src)
116 : await node_vm_sandbox(src, this.enchanched_fetch);
117 const bundle_id = (await pathToSHA512(this.filename)).slice(0, 32);
118 const cache = new Cache();
119 // Support pre v0.2 FABs
120 if (typeof renderer.initialize === 'function') {
121 renderer.initialize({ bundle_id, cache });
122 }
123 return renderer;
124 }
125 async renderReq(renderer, req, settings_overrides) {
126 var _a;
127 const method = req.method;
128 const headers = req.headers;
129 const url = `${req.protocol}://${req.headers.host}${req.url}`;
130 const fetch_req = new NodeFetchRequest(url, {
131 method,
132 headers,
133 ...(method === 'POST' ? { body: req.body } : {}),
134 });
135 const production_settings = (_a = renderer.metadata) === null || _a === void 0 ? void 0 : _a.production_settings;
136 let fetch_res;
137 try {
138 fetch_res = await renderer.render(
139 // @ts-ignore
140 fetch_req, Object.assign({}, production_settings, settings_overrides));
141 }
142 catch (err) {
143 const msg = `An error occurred calling the render method on the FAB: \nError: \n${err}`;
144 throw new Error(msg);
145 }
146 try {
147 if (fetch_res && isRequest(fetch_res)) {
148 fetch_res = await this.enchanched_fetch(fetch_res);
149 }
150 }
151 catch (err) {
152 const msg = `An error occurred proxying a request returned from the FAB: \nError:\n${err}\nRequest:\n${fetch_res}`;
153 throw new Error(msg);
154 }
155 if (!fetch_res) {
156 const msg = `Nothing was returned from the FAB renderer.`;
157 throw new Error(msg);
158 }
159 return fetch_res;
160 }
161 setupExpress(renderer, settings_overrides, files) {
162 const app = express();
163 app.use((req, res, next) => {
164 try {
165 next();
166 }
167 catch (err) {
168 log(`ERROR serving: ${req.url}`);
169 log(err);
170 if (!res.headersSent) {
171 res.writeHead(500, `Internal Error:\n${err}`);
172 }
173 res.end();
174 }
175 });
176 app.use((req, _res, next) => {
177 req.pipe(concat((data) => {
178 req.body = data.toString();
179 next();
180 }));
181 });
182 app.use((req, _res, next) => {
183 log(`🖤${req.url}🖤`);
184 next();
185 });
186 app.get('/_assets/*', (req, res) => {
187 const pathname = url.parse(req.url).pathname;
188 res.setHeader('Content-Type', getContentType(pathname));
189 res.setHeader('Cache-Control', 'immutable');
190 res.end(files[pathname]);
191 });
192 app.all('*', async (req, res) => {
193 const fetch_res = await this.renderReq(renderer, req, settings_overrides);
194 streamResponse(fetch_res, res);
195 });
196 return app;
197 }
198 async createHandler(runtimeType) {
199 log(`Reading 💛${this.filename}💛...`);
200 const files = await readFilesFromZip(this.filename);
201 const src_buffer = files['/server.js'];
202 if (!src_buffer) {
203 throw new FabServerError('Malformed FAB. Missing /server.js');
204 }
205 const src = src_buffer.toString('utf8');
206 log.tick(`Done. Booting VM...`);
207 const settings_overrides = await this.getSettingsOverrides();
208 const renderer = await this.createRenderer(src, runtimeType);
209 log.tick(`Done. Booting FAB server...`);
210 return this.setupExpress(renderer, settings_overrides, files);
211 }
212 async serve(runtimeType, watching = false, proxyWs) {
213 if (!(await fs.pathExists(this.filename))) {
214 throw new FabServerError(`Could not find file '${this.filename}'`);
215 }
216 let app;
217 let proxy;
218 let server;
219 const bootServer = async () => {
220 app = await this.createHandler(runtimeType);
221 await new Promise((resolve, _reject) => {
222 if (!server) {
223 server = http.createServer((req, res) => app(req, res));
224 if (proxyWs) {
225 if (!proxy) {
226 proxy = httpProxy.createProxyServer({
227 target: `ws://localhost:${proxyWs}`,
228 ws: true,
229 });
230 }
231 // ? https.createServer({ key: this.key, cert: this.cert }, app)
232 server.on('upgrade', (req, socket, head) => {
233 proxy.ws(req, socket, head);
234 });
235 }
236 server.listen(this.port, resolve);
237 }
238 else {
239 resolve();
240 }
241 });
242 };
243 if (watching) {
244 log.note(`Watching 💛${this.filename}💛 for changes...`);
245 await watcher([this.filename], bootServer, {
246 awaitWriteFinish: {
247 stabilityThreshold: 200,
248 pollInterval: 50,
249 },
250 });
251 }
252 else {
253 await bootServer();
254 }
255 }
256 async getSettingsOverrides() {
257 var _a;
258 if (!this.env)
259 return {};
260 const config = await JSON5Config.readFrom(this.config);
261 const overrides = (_a = config.data.settings) === null || _a === void 0 ? void 0 : _a[this.env];
262 if (!overrides) {
263 throw new InvalidConfigError(`No environment '${this.env}' found in ${this.config}!`);
264 }
265 return overrides;
266 }
267}
268const createServer = (filename, args) => new Server(filename, args);
269const serverExports = { createServer };
270export default serverExports;
271//# sourceMappingURL=index.js.map
\No newline at end of file