UNPKG

cypress-ct-custom-devserver

Version:

A helper to simplify the api for creating a custom dev-server for cypress.

171 lines (170 loc) 8.66 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createCustomDevServer = void 0; const express_1 = __importDefault(require("express")); const path_1 = __importDefault(require("path")); const promises_1 = require("fs/promises"); const minimatch_1 = require("minimatch"); const util_1 = require("./util"); const pathToSpec = (relativePath, root) => { const baseName = relativePath.split(path_1.default.sep).slice(-1)[0]; return { absolute: path_1.default.join(root, relativePath), relative: relativePath, name: baseName, specType: 'component', baseName, fileName: baseName.split('.')[0], specFileExtension: '.' + baseName.split('.').slice(1).join('.'), fileExtension: '.' + baseName.split('.').slice(-1)[0], }; }; const hasStringArrayContentChanged = (oldList, newList) => { return oldList.length !== newList.length || new Set([].concat(oldList, newList)).size !== oldList.length; }; function createCustomDevServer(initBuildCallback) { return ({ cypressConfig, specs, devServerEvents }) => __awaiter(this, void 0, void 0, function* () { const specPatterns = (Array.isArray(cypressConfig.specPattern) ? cypressConfig.specPattern : [cypressConfig.specPattern]) .map(pattern => pattern.replace(/^.\//, '')); let started; let isBuilding = false; let done = Function.prototype; let log = Function.prototype; const app = (0, express_1.default)(); // wait for build to be finished before serving files app.use((_req, _res, next) => __awaiter(this, void 0, void 0, function* () { if (isBuilding) { log(6, 'Stalling request, waiting for rebuild to finish...'); yield isBuilding; log(6, 'Rebuild finished, continue request.'); } next(); })); const staticMappings = []; const { onSpecChange, loadTest, devServerPort, onClose, logFunction } = yield initBuildCallback({ cypressConfig, specs, supportFile: cypressConfig.supportFile && { absolute: cypressConfig.supportFile, relative: cypressConfig.supportFile.replace(cypressConfig.projectRoot, ''), name: path_1.default.basename(cypressConfig.supportFile), fileExtension: path_1.default.extname(cypressConfig.supportFile) }, onBuildComplete: () => { devServerEvents.emit('dev-server:compile:success'); done(); }, onBuildStart: () => { if (isBuilding === false) { isBuilding = new Promise((resolve) => { log(5, 'Devserver signaled start of build. Stalling all requests.'); done = () => { log(5, 'Devserver signaled end of build. Resuming all requests.'); resolve(); isBuilding = false; done = Function.prototype; }; }); } }, serveStatic: (folder, path = '/') => { staticMappings.push([folder, path]); } }); log = (logLevel, ...messages) => typeof logFunction === 'function' && logFunction(logLevel, ...messages); const logger = (req, _res, next) => { log(7, `Checking request for static ressource: '${req.url}'`); next(); }; staticMappings.forEach(([folder, publicPath]) => { const setHeaders = (_response, file_path) => { log(6, `Serving static file: '${path_1.default.relative(folder, file_path)}'.`); }; log(6, `Adding static route from '${path_1.default}' to folder '${folder}'.`); const staticRouter = express_1.default.static(folder, { setHeaders }); const cypressSrcPath = `${cypressConfig.devServerPublicPathRoute}/${publicPath}`.replaceAll('//', '/'); app.use(publicPath, logger, staticRouter); app.use(cypressSrcPath, logger, staticRouter); }); let lastSpecs = specs.map(spec => spec.relative); devServerEvents.on('dev-server:specs:changed', (eventData) => { // Handle both Cypress < 14 (eventData is specs array) and >= 14 (eventData.specs is specs array) const specs = Array.isArray(eventData) ? eventData : eventData.specs; const currentSpecPaths = specs.map(spec => spec.relative); if (hasStringArrayContentChanged(lastSpecs, currentSpecPaths)) { lastSpecs = currentSpecPaths; if (typeof onSpecChange === 'function') { log(5, 'Test files changed. Rebuilding...'); onSpecChange(specs); } else { log(3, 'Test files changed. Please restart the server.'); } } }); let lastTestBasePath; app.get(cypressConfig.devServerPublicPathRoute + '/index.html', (req, res) => __awaiter(this, void 0, void 0, function* () { const testPath = path_1.default.relative(cypressConfig.projectRoot, req.headers.__cypress_spec_path); const isTest = specPatterns.some((pattern) => (0, minimatch_1.minimatch)(testPath, pattern)); if (!isTest && (!lastTestBasePath || !testPath.includes(lastTestBasePath))) { log(4, `Non-testfile requested with relative url: "${testPath}" but could not be matched.`); return res.send(''); } else if (!isTest) { log(6, `Non-testfile requested with relative url: "${testPath}" and redirected.`); const relativePath = testPath.replace(lastTestBasePath, '').split(path_1.default.sep).join('/'); return res.redirect(`${cypressConfig.devServerPublicPathRoute}/${relativePath}`.replaceAll('//', '/')); } else { lastTestBasePath = testPath.split(path_1.default.sep).slice(0, -1).join(path_1.default.sep); } log(4, `Index.html requested for test ${testPath}`); let html = ''; try { html = yield (0, promises_1.readFile)(path_1.default.join(cypressConfig.projectRoot, cypressConfig.indexHtmlFile), 'utf8'); } catch (e) { log(3, 'Index.html missing.'); } const utils = (0, util_1.createLoadTestUtils)(cypressConfig.devServerPublicPathRoute, log); yield loadTest(pathToSpec(testPath, cypressConfig.projectRoot), utils); res.status(200).send(utils.transformHTML(html)); })); app.use('*', (req, res) => { log(4, 'Could not match request to url: ', req.originalUrl); res.status(404).send(); }); const server = app.listen(devServerPort !== null && devServerPort !== void 0 ? devServerPort : 0, () => { const { port } = server.address(); log(2, 'Dev server started on port: ', port); started(port); }); return new Promise(resolve => { started = (port) => resolve({ port, close: (done) => __awaiter(this, void 0, void 0, function* () { if (typeof onClose === 'function') { yield onClose(); } server.close(() => { log(2, 'Devserver shut down.'); done(); }); }) }); }); }); } exports.createCustomDevServer = createCustomDevServer;