UNPKG

code-workshop-kit

Version:
435 lines 17.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = void 0; const dev_server_1 = require("@web/dev-server"); const chalk_1 = __importDefault(require("chalk")); const chokidar_1 = __importDefault(require("chokidar")); const command_line_args_1 = __importDefault(require("command-line-args")); const esm_1 = __importDefault(require("esm")); const glob_1 = __importDefault(require("glob")); const path_1 = __importDefault(require("path")); const portfinder_1 = __importDefault(require("portfinder")); const middleware_1 = require("./middleware/middleware"); const plugins_1 = require("./plugins/plugins"); const CwkStateSingleton_1 = require("./utils/CwkStateSingleton"); const runScript_1 = require("./runScript"); const logger = (str, opts) => { if (opts.logStartup !== false) { // eslint-disable-next-line no-console console.log(str); } }; const getAdminUIDefaults = () => ({ enableCaching: false, followMode: false, }); const sendAdminConfig = (ws) => { ws.send(JSON.stringify({ type: 'config-init', config: CwkStateSingleton_1.cwkState.state.adminConfig, })); }; const setDefaultAdminConfig = () => { CwkStateSingleton_1.cwkState.state = { adminConfig: getAdminUIDefaults() }; }; const getWsConnection = (participantName, feature, all = false) => { let connection; if (!CwkStateSingleton_1.cwkState.state.wsConnections || !CwkStateSingleton_1.cwkState.state.wsConnections[feature]) { return null; } if (all) { return Array.from(CwkStateSingleton_1.cwkState.state.wsConnections[feature].entries()) .filter((entry) => entry[0].endsWith(participantName)) .map((entry) => entry[1]); } if (CwkStateSingleton_1.cwkState.state.wsConnections && CwkStateSingleton_1.cwkState.state.wsConnections[feature]) { connection = CwkStateSingleton_1.cwkState.state.wsConnections[feature].get(`${participantName}-${participantName}`); } return connection; }; const runScriptForParticipant = async (participantName, cfg, opts) => { const { state } = CwkStateSingleton_1.cwkState; if (state.terminalScripts) { const oldScript = state.terminalScripts.get(participantName); if (oldScript && oldScript.script && oldScript.script.pid) { try { process.kill(-oldScript.script.pid); await oldScript.hasClosed; } catch (e) { logger(` Error: problem killing a participant terminal script, this could be related to a bug on Windows where the wrong PID is given. CWK is working on a fix or workaround.. In the meantime, we advise using WSL for windows users hosting CWK workshops, or only doing participant scripts that are self-terminating. `, opts); } } } if (!cfg || !cfg.targetOptions || !cfg.targetOptions.cmd) { return; } const { processEmitter, script } = runScript_1.runScript({ cmd: cfg.targetOptions.cmd, participant: participantName, participantIndex: cfg.participants.indexOf(participantName), dir: cfg.absoluteDir, }); if (!state.terminalScripts) { state.terminalScripts = new Map(); } // eslint-disable-next-line @typescript-eslint/no-empty-function let closeResolve = () => { }; const hasClosed = new Promise((resolve) => { if (!state.terminalScripts) { return; } closeResolve = resolve; }); // Save the current running script for participant state.terminalScripts.set(participantName, { script, closeResolve, hasClosed }); // We will set a close listener for each participant, which can easily exceed 10. script.setMaxListeners(0); script.on('close', () => { if (!state.terminalScripts) { return; } const scriptData = state.terminalScripts.get(participantName); if (scriptData) { scriptData.closeResolve(); state.terminalScripts.delete(participantName); } }); // Open terminal input on the frontend const connections = getWsConnection(participantName, 'terminal-process', true); if (Array.isArray(connections)) { connections.forEach((connection) => { if (connection) { connection.send(JSON.stringify({ type: 'terminal-input-enable' })); } // Close terminal input on the frontend script.on('close', () => { if (connection) { connection.send(JSON.stringify({ type: 'terminal-input-disable' })); } }); }); } const sendData = (data, type) => { // Check connections again, as they may have changed since checking at the start of running the script const _connections = getWsConnection(participantName, 'terminal-process', true); if (Array.isArray(_connections)) { _connections.forEach((connection) => connection.send(JSON.stringify({ type: `terminal-process-${type}`, data }))); } }; processEmitter.on('out', (data) => { sendData(data, 'output'); }); processEmitter.on('err', (data) => { sendData(data, 'error'); }); CwkStateSingleton_1.cwkState.state = state; }; const handleWsMessage = (message, ws, opts) => { const parsedMessage = JSON.parse(message); const { type } = parsedMessage; switch (type) { case 'config-init': { sendAdminConfig(ws); break; } case 'reset-state': { setDefaultAdminConfig(); break; } case 'clear-state': { CwkStateSingleton_1.cwkState.clear(); break; } case 'config-updated': { const { config, key, byAdmin } = parsedMessage; CwkStateSingleton_1.cwkState.state = { adminConfig: config }; if (key === 'followMode') { CwkStateSingleton_1.cwkState.state = { followModeInitiatedBy: byAdmin }; } ws.send(JSON.stringify({ type: 'config-update-completed', config: CwkStateSingleton_1.cwkState.state.adminConfig, })); break; } case 'terminal-process-input': { const { input, participantName } = parsedMessage; if (CwkStateSingleton_1.cwkState.state.terminalScripts) { const scriptData = CwkStateSingleton_1.cwkState.state.terminalScripts.get(participantName); if (scriptData && scriptData.script) { scriptData.script.stdin.write(`${input}\n`, (err) => { if (err) { throw new Error(`stdin error: ${err}`); } }); } } break; } case 'terminal-rerun': { const { participantName } = parsedMessage; runScriptForParticipant(participantName, CwkStateSingleton_1.cwkState.state.cwkConfig, opts); break; } case 'authenticate': { const { username, feature, participant } = parsedMessage; const { state } = CwkStateSingleton_1.cwkState; if (username) { if (!state.wsConnections) { state.wsConnections = {}; } if (!state.wsConnections[feature]) { state.wsConnections[feature] = new Map(); } // Store the websocket connection for this user state.wsConnections[feature].set(`${participant}-${username}`, ws); CwkStateSingleton_1.cwkState.state = state; ws.send(JSON.stringify({ type: 'authenticate-completed', user: username, })); } } // no default } }; const addPluginsAndMiddleware = (wdsConfig, cwkConfig) => { const newWdsConfig = wdsConfig; newWdsConfig.middleware = [ ...(wdsConfig.middleware || []), middleware_1.changeParticipantUrlMiddleware(cwkConfig.absoluteDir), middleware_1.jwtMiddleware(cwkConfig.absoluteDir), middleware_1.noCacheMiddleware, ]; newWdsConfig.plugins = [ ...(wdsConfig.plugins || []), plugins_1.queryTimestampModulesPlugin(cwkConfig.absoluteDir), plugins_1.missingIndexHtmlPlugin(cwkConfig), plugins_1.wsPortPlugin(wdsConfig.port), plugins_1.componentReplacersPlugin(cwkConfig), plugins_1.followModePlugin(wdsConfig.port), plugins_1.appShellPlugin(cwkConfig), plugins_1.adminUIPlugin(cwkConfig.absoluteDir), ]; return newWdsConfig; }; const getCwkConfig = (opts) => { var _a; // cwk defaults let cwkConfig = { participants: [], admins: [], adminPassword: '', appKey: '', absoluteDir: '/', dir: '/', title: '', logStartup: true, target: 'frontend', argv: [], _unknown: [], ...opts, targetOptions: { // terminal cmd: '', autoReload: true, fromParticipantFolder: true, excludeFromWatch: [], // frontend mode: 'iframe', ...(opts && opts.targetOptions), }, templateData: { ...(opts && opts.templateData), }, }; // If cli was used, read flags, both for cwk and wds flags if (opts.argv) { const cwkServerDefinitions = [ { name: 'dir', type: String, description: 'The directory to read the cwk.config.js from, the index.html for the app shell and the template folder for scaffolding', }, ]; cwkConfig = { ...cwkConfig, ...command_line_args_1.default(cwkServerDefinitions, { argv: opts.argv, partial: true }), }; } if ((_a = cwkConfig.dir) === null || _a === void 0 ? void 0 : _a.startsWith('/')) { // eslint-disable-next-line no-param-reassign cwkConfig.dir = `.${cwkConfig.dir}`; } cwkConfig.absoluteDir = path_1.default.resolve(process.cwd(), cwkConfig.dir); const esmRequire = esm_1.default(module); const workshop = esmRequire(`${cwkConfig.absoluteDir}/cwk.config.js`).default; // TODO: use deepmerge cwkConfig = { ...cwkConfig, ...workshop, targetOptions: { ...(cwkConfig && cwkConfig.targetOptions), ...(workshop && workshop.targetOptions), }, }; return cwkConfig; }; const getWdsConfig = (opts, cwkConfig, port) => { // wds defaults & middleware let wdsConfig = { open: false, nodeResolve: true, plugins: [], middleware: [], ...opts, }; wdsConfig.port = port; const wdsConfigWithPort = wdsConfig; wdsConfig = addPluginsAndMiddleware(wdsConfigWithPort, cwkConfig); return wdsConfig; }; const setupParticipantWatcher = (absoluteDir) => chokidar_1.default.watch(path_1.default.resolve(absoluteDir, 'participants')); const setupWatcherForTerminalProcess = (watcher, cfg, opts) => { if (cfg.targetOptions && cfg.targetOptions.autoReload) { watcher.on('change', (filePath) => { var _a, _b; // Get participant name from file path // Cancel out the participant folder from the filepath (Bob/index.js or Bob/nested/style.css), // and get the top most dir name const participantFolder = path_1.default.join(cfg.absoluteDir, 'participants/'); const participantName = filePath.split(participantFolder)[1].split(path_1.default.sep).shift(); if (participantName && participantName !== '_cwk_disconnected') { const excludeFilesArr = [ ...new Set((_b = (_a = cfg.targetOptions) === null || _a === void 0 ? void 0 : _a.excludeFromWatch) === null || _b === void 0 ? void 0 : _b.flatMap((pattern) => glob_1.default.sync(pattern, { cwd: path_1.default.resolve(cfg.absoluteDir, 'participants', participantName), dot: true, })).map((file) => path_1.default.resolve(cfg.absoluteDir, 'participants', participantName, file))), ]; // If the file that was changed is not within exclude-list if (!excludeFilesArr.includes(filePath)) { runScriptForParticipant(participantName, cfg, opts); } } }); } }; const setupHMR = (watcher, absoluteDir) => { watcher.on('change', (filePath) => { // Get participant name from file path // Cancel out the participant folder from the filepath (Bob/index.js or Bob/nested/style.css), // and get the top most dir name const participantFolder = path_1.default.join(absoluteDir, 'participants/'); const participantName = filePath.split(participantFolder)[1].split(path_1.default.sep).shift(); if (participantName && participantName !== '_cwk_disconnected') { const connections = getWsConnection(participantName, 'reload-module', true); const queryTimestamp = Date.now(); const { state } = CwkStateSingleton_1.cwkState; if (!state.queryTimestamps) { state.queryTimestamps = {}; } state.queryTimestamps[participantName] = queryTimestamp; CwkStateSingleton_1.cwkState.state = state; if (Array.isArray(connections)) { connections.forEach((connection) => { if (connection) { // Send module-changed message to all participants for the folder that changed connection.send(JSON.stringify({ type: 'reload-module', name: participantName, timestamp: queryTimestamp, })); } }); } } }); }; const getPort = async (opts) => { /** * Pre-determine the port, since we have to pass it to our middleware and plugins before * the dev server is instantiated. We either take the CLI flag port, the Node API's port * property, or as a fallback, portfinder's default first found port. */ const defaultPort = await portfinder_1.default.getPortPromise(); const port = command_line_args_1.default([ { alias: 'p', name: 'port', type: Number, }, ], { argv: opts.argv, partial: true }).port || opts.port || defaultPort; return port; }; const startServer = async (opts) => { const port = await getPort(opts); const cwkConfig = getCwkConfig(opts); const wdsConfig = getWdsConfig(opts, cwkConfig, port); const watcher = setupParticipantWatcher(cwkConfig.absoluteDir); if (cwkConfig.target === 'frontend' && cwkConfig.targetOptions.mode === 'module') { setupHMR(watcher, cwkConfig.absoluteDir); } else if (cwkConfig.target === 'terminal') { setupWatcherForTerminalProcess(watcher, cwkConfig, opts); } const server = await dev_server_1.startDevServer({ config: { ...wdsConfig, watch: false, clearTerminalOnReload: false, }, logStartMessage: false, argv: cwkConfig._unknown, // pass those options that were unknown to CWK definitions }); const wss = server.webSockets.webSocketServer; wss.on('connection', (ws) => { ws.on('message', (message) => handleWsMessage(message, ws, opts)); }); CwkStateSingleton_1.cwkState.state = { wss, cwkConfig }; setDefaultAdminConfig(); if (cwkConfig.logStartup !== false) { logger(chalk_1.default.bold('code-workshop-kit server started...'), opts); logger('', opts); let url = `http://localhost:${server.config.port}/${path_1.default.relative(process.cwd(), cwkConfig.absoluteDir)}`; if (!url.endsWith('/')) { url += '/'; } logger(`${chalk_1.default.white('Visit:')} ${chalk_1.default.cyanBright(url)}`, opts); logger('', opts); } ['exit', 'SIGINT'].forEach((event) => { process.on(event, () => { // ensure that when we close CWK, terminal script subchildren are killed off too if (CwkStateSingleton_1.cwkState.state.terminalScripts) { CwkStateSingleton_1.cwkState.state.terminalScripts.forEach((script) => { if (script && script.script && script.script.pid) { try { process.kill(-script.script.pid); } catch (e) { logger(` Error: problem killing a participant terminal script, this could be related to a bug on Windows where the wrong PID is given. CWK is working on a fix or workaround.. In the meantime, we advise using WSL for windows users hosting CWK workshops, or only doing participant scripts that are self-terminating. `, opts); } } }); } watcher.close(); }); }); return { server, wdsConfig, cwkConfig, wss, watcher }; }; exports.startServer = startServer; //# sourceMappingURL=start-server.js.map