1 | import fs from 'fs-extra';
|
2 | import { getContentType, SandboxType, } from '@fab/core';
|
3 | import { _log, InvalidConfigError, FabServerError, JSON5Config } from '@fab/cli';
|
4 | import { readFilesFromZip } from './utils';
|
5 | import v8_sandbox from './sandboxes/v8-isolate';
|
6 | import { Cache } from './cache';
|
7 | import node_vm_sandbox from '@fab/sandbox-node-vm';
|
8 | import url from 'url';
|
9 | import http from 'http';
|
10 | import express from 'express';
|
11 | import concat from 'concat-stream';
|
12 | import fetch, { Request as NodeFetchRequest } from 'cross-fetch';
|
13 | import { pathToSHA512 } from 'file-to-sha512';
|
14 | import Stream from 'stream';
|
15 | import { watcher } from '@fab/cli';
|
16 | import httpProxy from 'http-proxy';
|
17 |
|
18 | import nodeToWebStream from 'readable-stream-node-to-web';
|
19 | function 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 | }
|
23 | const log = _log(`Server`);
|
24 | async function streamResponse(fetch_res, res) {
|
25 | res.status(fetch_res.status);
|
26 |
|
27 |
|
28 |
|
29 |
|
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 | }
|
78 | function 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 | ?
|
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 | }
|
100 | class Server {
|
101 | constructor(filename, args) {
|
102 | this.filename = filename;
|
103 | this.port = parseInt(args.port);
|
104 |
|
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 |
|
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 |
|
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 |
|
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 | }
|
267 | const createServer = (filename, args) => new Server(filename, args);
|
268 | const serverExports = { createServer };
|
269 | export default serverExports;
|
270 |
|
\ | No newline at end of file |