UNPKG

@ui5/server

Version:
202 lines (178 loc) 6.62 kB
import express from "express"; import portscanner from "portscanner"; import MiddlewareManager from "./middleware/MiddlewareManager.js"; import {createReaderCollection} from "@ui5/fs/resourceFactory"; import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized"; import {getLogger} from "@ui5/logger"; const log = getLogger("server"); /** * @public * @module @ui5/server */ /** * Returns a promise resolving by starting the server. * * @param {object} app The express application object * @param {number} port Desired port to listen to * @param {boolean} changePortIfInUse If true and the port is already in use, an unused port is searched * @param {boolean} acceptRemoteConnections If true, listens to remote connections and not only to localhost connections * @returns {Promise<object>} Returns an object containing server related information like (selected port, protocol) * @private */ function _listen(app, port, changePortIfInUse, acceptRemoteConnections) { return new Promise(function(resolve, reject) { const options = {}; if (!acceptRemoteConnections) { options.host = "localhost"; } const host = options.host || "127.0.0.1"; let portMax; if (changePortIfInUse) { portMax = port + 30; } else { portMax = port; } portscanner.findAPortNotInUse(port, portMax, host, function(error, foundPort) { if (error) { reject(error); return; } if (!foundPort) { if (changePortIfInUse) { const error = new Error( `EADDRINUSE: Could not find available ports between ${port} and ${portMax}.`); error.code = "EADDRINUSE"; error.errno = "EADDRINUSE"; error.address = host; error.port = portMax; reject(error); return; } else { const error = new Error(`EADDRINUSE: Port ${port} is already in use.`); error.code = "EADDRINUSE"; error.errno = "EADDRINUSE"; error.address = host; error.port = portMax; reject(error); return; } } options.port = foundPort; const server = app.listen(options, function() { resolve({port: options.port, server}); }); server.on("error", function(err) { reject(err); }); }); }); } /** * Adds SSL support to an express application. * * @param {object} parameters * @param {object} parameters.app The original express application * @param {string} parameters.key Path to private key to be used for https * @param {string} parameters.cert Path to certificate to be used for for https * @returns {Promise<object>} The express application with SSL support * @private */ async function _addSsl({app, key, cert}) { // Using spdy as http2 server as the native http2 implementation // from Node v8.4.0 doesn't seem to work with express const {default: spdy} = await import("spdy"); return spdy.createServer({cert, key}, app); } /** * SAP target CSP middleware options * * @public * @typedef {object} module:@ui5/server.SAPTargetCSPOptions * @property {string} [defaultPolicy="sap-target-level-1"] * @property {string} [defaultPolicyIsReportOnly=true] * @property {string} [defaultPolicy2="sap-target-level-3"] * @property {string} [defaultPolicy2IsReportOnly=true] * @property {string[]} [ignorePaths=["test-resources/sap/ui/qunit/testrunner.html"]] */ /** * Start a server for the given project (sub-)tree. * * @public * @param {@ui5/project/graph/ProjectGraph} graph Project graph * @param {object} options Options * @param {number} options.port Port to listen to * @param {boolean} [options.changePortIfInUse=false] If true, change the port if it is already in use * @param {boolean} [options.h2=false] Whether HTTP/2 should be used - defaults to <code>http</code> * @param {string} [options.key] Path to private key to be used for https * @param {string} [options.cert] Path to certificate to be used for for https * @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing * @param {boolean} [options.acceptRemoteConnections=false] If true, listens to remote connections and * not only to localhost connections * @param {boolean|module:@ui5/server.SAPTargetCSPOptions} [options.sendSAPTargetCSP=false] * If set to <code>true</code> or an object, then the default (or configured) * set of security policies that SAP and UI5 aim for (AKA 'target policies'), * are send for any requested <code>*.html</code> file * @param {boolean} [options.serveCSPReports=false] Enable CSP reports serving for request url * '/.ui5/csp/csp-reports.json' * @returns {Promise<object>} Promise resolving once the server is listening. * It resolves with an object containing the <code>port</code>, * <code>h2</code>-flag and a <code>close</code> function, * which can be used to stop the server. */ export async function serve(graph, { port: requestedPort, changePortIfInUse = false, h2 = false, key, cert, acceptRemoteConnections = false, sendSAPTargetCSP = false, simpleIndex = false, serveCSPReports = false }) { const rootProject = graph.getRoot(); const readers = []; await graph.traverseBreadthFirst(async function({project: dep}) { if (dep.getName() === rootProject.getName()) { // Ignore root project return; } readers.push(dep.getReader({style: "runtime"})); }); const dependencies = createReaderCollection({ name: `Dependency reader collection for project ${rootProject.getName()}`, readers }); const rootReader = rootProject.getReader({style: "runtime"}); // TODO change to ReaderCollection once duplicates are sorted out const combo = new ReaderCollectionPrioritized({ name: "server - prioritize workspace over dependencies", readers: [rootReader, dependencies] }); const resources = { rootProject: rootReader, dependencies: dependencies, all: combo }; const middlewareManager = new MiddlewareManager({ graph, rootProject, resources, options: { sendSAPTargetCSP, serveCSPReports, simpleIndex } }); let app = express(); await middlewareManager.applyMiddleware(app); if (h2) { const nodeVersion = parseInt(process.versions.node.split(".")[0], 10); if (nodeVersion >= 24) { log.error("ERROR: With Node v24, usage of HTTP/2 is no longer supported. Please check https://github.com/UI5/cli/issues/327 for updates."); process.exit(1); } app = await _addSsl({app, key, cert}); } const {port, server} = await _listen(app, requestedPort, changePortIfInUse, acceptRemoteConnections); return { h2, port, close: function(callback) { server.close(callback); } }; }