ide/deploy/serve.js (154 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import {statSync, readFileSync} from "fs"; import {resolve, extname} from "path"; import {parse} from "url"; import process from 'process'; /** * Helper function to find the first available port starting from 8080 * @param host * @param startPort * @returns {Promise<number>} */ async function findAvailablePort(host, startPort = 8080) { host = host || '127.0.0.1'; for (let port = startPort; port < 65535; port++) { try { const server = Bun.listen({ host: host, port: port, socket: { data(socket, data) { }, } }); server.stop(); // Close immediately if successful return port; } catch (e) { // Port is taken, try the next one } } throw new Error("No available port found."); } /** * Helper function thath convert the input stream to a Buffer * @param stream * @returns {Promise<Buffer<ArrayBuffer>>} */ async function toBuffer(stream) { const list = [] const reader = stream.getReader(); while (true) { const {value, done} = await reader.read(); if (value) list.push(value) if (done) break } return Buffer.concat(list) } // /** * Helper function to determine MIME type based on file extension * @param filePath * @returns {*|string} */ function getMimeType(filePath) { const ext = extname(filePath).toLowerCase(); const mimeTypes = { ".html": "text/html", ".css": "text/css", ".js": "application/javascript", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".txt": "text/plain", // Add more mimetypes as needed }; return mimeTypes[ext] || "application/octet-stream"; } const excludedAssets = ['favicon.ico']; /** * Entry point. This will start a web server on the first available port * starting from 8080. When the file ot serve is not local and a proxy host * is specified by the `-P flag`, the request will be sent to the proxy host * and the response returned back. * * @returns {Promise<Response>} */ async function main() { // Get command-line arguments const args = Bun.argv.slice(2); const flags = { port: 8080, host: "127.0.0.1", dir: "./", proxyHost: null, cacheTime: null, mimeType: null, }; // Parse command-line flags for (let i = 0; i < args.length; i++) { switch (args[i]) { case "-h": flags.host = args[i + 1]; i++; break; case "-d": flags.dir = args[i + 1]; i++; break; case "-P": flags.proxyHost = args[i + 1]; i++; break; case "-c": flags.cacheTime = parseInt(args[i + 1], 10) || null; i++; break; case "--mimetype": flags.mimeType = args[i + 1]; i++; break; } } const assetsDirectory = resolve(flags.dir); const serverPort = await findAvailablePort(flags.host, flags.port); Bun.serve({ port: serverPort, hostname: flags.host, async fetch(req) { const {pathname} = parse(req.url); const filePath = `${assetsDirectory}${pathname === "/" ? "/index.html" : pathname}`; // Check if file exists locally try { // console.debug(`Asset dir: ${assetsDirectory} - Filepath is ${filePath}`); const fileStats = statSync(filePath); if (fileStats.isFile()) { const mimeType = flags.mimeType || getMimeType(filePath); const headers = { "Content-Type": mimeType, }; if (flags.cacheTime) { headers["Cache-Control"] = `max-age=${flags.cacheTime}`; } console.log(`[200] - Serving file ${pathname} from filesystem`); return new Response(readFileSync(filePath), {headers}); } } catch (err) { let shouldExclude = (excludedAssets.indexOf(pathname) !== -1); // File not found, fall back to proxy if (flags.proxyHost && !shouldExclude) { const destProxyUrl = `${flags.proxyHost}${pathname}`; console.log(`[ P ] - Proxying request ${req.method} to '${destProxyUrl}'`); const newHeaders = JSON.parse(JSON.stringify(req.headers)); const excludedHeaders = [ 'host', 'origin', // 'accept-encoding', 'sec-fetch-mode', 'sec-fetch-dest', 'sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform', 'sec-fetch-site' ]; for (const header of excludedHeaders) { delete newHeaders[header]; } const init = { method: req.method, headers: newHeaders, }; if (req.method.toLowerCase() !== 'get') { let body = await toBuffer(req.body); body = body.toString('utf8'); if (body !== undefined) { init['body'] = body; } } const respP = await fetch(destProxyUrl, init); console.log(`[${respP.status}] - Sending response with status ${respP.statusText}`); return respP; } else { console.log(`[404] - File ${pathname} not found`); return new Response("File not found", {status: 404}); } } }, error(e) { console.error("Error occurred:", e); return new Response("Internal Server Error", {status: 500}); }, }); console.log(`Server running at http://${flags.host}:${serverPort}`); if (flags.proxyHost) { console.log(`Server proxying at ${flags.proxyHost}`); } } main().catch(err => { console.error(err); process.exit(1); });