UNPKG

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