Source code
Revision control
Copy as Markdown
Other Tools
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as babel from '@babel/core';
import * as chokidar from 'chokidar';
import * as express from 'express';
import * as morgan from 'morgan';
import * as portfinder from 'portfinder';
import * as serveIndex from 'serve-index';
import { makeListing } from './crawl.js';
// Make sure that makeListing doesn't cache imported spec files. See crawl().
process.env.STANDALONE_DEV_SERVER = '1';
function usage(rc: number): void {
console.error(`\
Usage:
tools/dev_server
tools/dev_server 0.0.0.0
npm start
npm start 0.0.0.0
By default, serves on localhost only. If the argument 0.0.0.0 is passed, serves on all interfaces.
`);
process.exit(rc);
}
const srcDir = path.resolve(__dirname, '../../');
// Import the project's babel.config.js. We'll use the same config for the runtime compiler.
const babelConfig = {
...require(path.resolve(srcDir, '../babel.config.js'))({
cache: () => {
/* not used */
},
}),
sourceMaps: 'inline',
};
// Caches for the generated listing file and compiled TS sources to speed up reloads.
// Keyed by suite name
const listingCache = new Map<string, string>();
// Keyed by the path to the .ts file, without src/
const compileCache = new Map<string, string>();
console.log('Watching changes in', srcDir);
const watcher = chokidar.watch(srcDir, {
persistent: true,
});
/**
* Handler to dirty the compile cache for changed .ts files.
*/
function dirtyCompileCache(absPath: string, stats?: fs.Stats) {
const relPath = path.relative(srcDir, absPath);
if ((stats === undefined || stats.isFile()) && relPath.endsWith('.ts')) {
const tsUrl = relPath;
if (compileCache.has(tsUrl)) {
console.debug('Dirtying compile cache', tsUrl);
}
compileCache.delete(tsUrl);
}
}
/**
* Handler to dirty the listing cache for:
* - Directory changes
* - .spec.ts changes
* - README.txt changes
* Also dirties the compile cache for changed files.
*/
function dirtyListingAndCompileCache(absPath: string, stats?: fs.Stats) {
const relPath = path.relative(srcDir, absPath);
const segments = relPath.split(path.sep);
// The listing changes if the directories change, or if a .spec.ts file is added/removed.
const listingChange =
// A directory or a file with no extension that we can't stat.
// (stat doesn't work for deletions)
((path.extname(relPath) === '' && (stats === undefined || !stats.isFile())) ||
// A spec file
relPath.endsWith('.spec.ts') ||
// A README.txt
path.basename(relPath, 'txt') === 'README') &&
segments.length > 0;
if (listingChange) {
const suite = segments[0];
if (listingCache.has(suite)) {
console.debug('Dirtying listing cache', suite);
}
listingCache.delete(suite);
}
dirtyCompileCache(absPath, stats);
}
watcher.on('add', dirtyListingAndCompileCache);
watcher.on('unlink', dirtyListingAndCompileCache);
watcher.on('addDir', dirtyListingAndCompileCache);
watcher.on('unlinkDir', dirtyListingAndCompileCache);
watcher.on('change', dirtyCompileCache);
const app = express();
// Send Chrome Origin Trial tokens
app.use((_req, res, next) => {
next();
});
// Set up logging
app.use(morgan('dev'));
// Serve the standalone runner directory
app.use('/standalone', express.static(path.resolve(srcDir, '../standalone')));
// Add out-wpt/ build dir for convenience
app.use('/out-wpt', express.static(path.resolve(srcDir, '../out-wpt')));
app.use('/docs/tsdoc', express.static(path.resolve(srcDir, '../docs/tsdoc')));
// Serve a suite's listing.js file by crawling the filesystem for all tests.
app.get('/out/:suite([a-zA-Z0-9_-]+)/listing.js', async (req, res, next) => {
const suite = req.params['suite'];
if (listingCache.has(suite)) {
res.setHeader('Content-Type', 'application/javascript');
res.send(listingCache.get(suite));
return;
}
try {
const listing = await makeListing(path.resolve(srcDir, suite, 'listing.ts'));
const result = `export const listing = ${JSON.stringify(listing, undefined, 2)}`;
listingCache.set(suite, result);
res.setHeader('Content-Type', 'application/javascript');
res.send(result);
} catch (err) {
next(err);
}
});
// Serve .as_worker.js files by generating the necessary wrapper.
app.get('/out/:suite([a-zA-Z0-9_-]+)/webworker/:filepath(*).as_worker.js', (req, res, next) => {
const { suite, filepath } = req.params;
const result = `\
import { g } from '/out/${suite}/${filepath}.spec.js';
import { wrapTestGroupForWorker } from '/out/common/runtime/helper/wrap_for_worker.js';
wrapTestGroupForWorker(g);
`;
res.setHeader('Content-Type', 'application/javascript');
res.send(result);
});
// Serve all other .js files by fetching the source .ts file and compiling it.
app.get('/out/**/*.js', async (req, res, next) => {
const jsUrl = path.relative('/out', req.url);
const tsUrl = jsUrl.replace(/\.js$/, '.ts');
if (compileCache.has(tsUrl)) {
res.setHeader('Content-Type', 'application/javascript');
res.send(compileCache.get(tsUrl));
return;
}
let absPath = path.join(srcDir, tsUrl);
if (!fs.existsSync(absPath)) {
// The .ts file doesn't exist. Try .js file in case this is a .js/.d.ts pair.
absPath = path.join(srcDir, jsUrl);
}
try {
const result = await babel.transformFileAsync(absPath, babelConfig);
if (result && result.code) {
compileCache.set(tsUrl, result.code);
res.setHeader('Content-Type', 'application/javascript');
res.send(result.code);
} else {
throw new Error(`Failed compile ${tsUrl}.`);
}
} catch (err) {
next(err);
}
});
// Serve everything else (not .js) as static, and directories as directory listings.
app.use('/out', serveIndex(path.resolve(srcDir, '../src')));
app.use('/out', express.static(path.resolve(srcDir, '../src')));
void (async () => {
let host = '127.0.0.1';
if (process.argv.length >= 3) {
if (process.argv.length !== 3) usage(1);
if (process.argv[2] === '0.0.0.0') {
host = '0.0.0.0';
} else {
usage(1);
}
}
console.log(`Finding an available port on ${host}...`);
const kPortFinderStart = 8080;
const port = await portfinder.getPortPromise({ host, port: kPortFinderStart });
watcher.on('ready', () => {
// Listen on the available port.
app.listen(port, host, () => {
console.log('Standalone test runner running at:');
if (host === '0.0.0.0') {
for (const iface of Object.values(os.networkInterfaces())) {
for (const details of iface || []) {
if (details.family === 'IPv4') {
}
}
}
} else {
}
});
});
})();