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
JavaScript
;
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;