tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

dev_server.ts (6875B)


      1 import * as fs from 'fs';
      2 import * as os from 'os';
      3 import * as path from 'path';
      4 
      5 import * as babel from '@babel/core';
      6 import * as chokidar from 'chokidar';
      7 import * as express from 'express';
      8 import * as morgan from 'morgan';
      9 import * as portfinder from 'portfinder';
     10 import * as serveIndex from 'serve-index';
     11 
     12 import { makeListing } from './crawl.js';
     13 
     14 // Make sure that makeListing doesn't cache imported spec files. See crawl().
     15 process.env.STANDALONE_DEV_SERVER = '1';
     16 
     17 function usage(rc: number): void {
     18  console.error(`\
     19 Usage:
     20  tools/dev_server
     21  tools/dev_server 0.0.0.0
     22  npm start
     23  npm start 0.0.0.0
     24 
     25 By default, serves on localhost only. If the argument 0.0.0.0 is passed, serves on all interfaces.
     26 `);
     27  process.exit(rc);
     28 }
     29 
     30 const srcDir = path.resolve(__dirname, '../../');
     31 
     32 // Import the project's babel.config.js. We'll use the same config for the runtime compiler.
     33 const babelConfig = {
     34  ...require(path.resolve(srcDir, '../babel.config.js'))({
     35    cache: () => {
     36      /* not used */
     37    },
     38  }),
     39  sourceMaps: 'inline',
     40 };
     41 
     42 // Caches for the generated listing file and compiled TS sources to speed up reloads.
     43 // Keyed by suite name
     44 const listingCache = new Map<string, string>();
     45 // Keyed by the path to the .ts file, without src/
     46 const compileCache = new Map<string, string>();
     47 
     48 console.log('Watching changes in', srcDir);
     49 const watcher = chokidar.watch(srcDir, {
     50  persistent: true,
     51 });
     52 
     53 /**
     54 * Handler to dirty the compile cache for changed .ts files.
     55 */
     56 function dirtyCompileCache(absPath: string, stats?: fs.Stats) {
     57  const relPath = path.relative(srcDir, absPath);
     58  if ((stats === undefined || stats.isFile()) && relPath.endsWith('.ts')) {
     59    const tsUrl = relPath;
     60    if (compileCache.has(tsUrl)) {
     61      console.debug('Dirtying compile cache', tsUrl);
     62    }
     63    compileCache.delete(tsUrl);
     64  }
     65 }
     66 
     67 /**
     68 * Handler to dirty the listing cache for:
     69 *  - Directory changes
     70 *  - .spec.ts changes
     71 *  - README.txt changes
     72 * Also dirties the compile cache for changed files.
     73 */
     74 function dirtyListingAndCompileCache(absPath: string, stats?: fs.Stats) {
     75  const relPath = path.relative(srcDir, absPath);
     76 
     77  const segments = relPath.split(path.sep);
     78  // The listing changes if the directories change, or if a .spec.ts file is added/removed.
     79  const listingChange =
     80    // A directory or a file with no extension that we can't stat.
     81    // (stat doesn't work for deletions)
     82    ((path.extname(relPath) === '' && (stats === undefined || !stats.isFile())) ||
     83      // A spec file
     84      relPath.endsWith('.spec.ts') ||
     85      // A README.txt
     86      path.basename(relPath, 'txt') === 'README') &&
     87    segments.length > 0;
     88  if (listingChange) {
     89    const suite = segments[0];
     90    if (listingCache.has(suite)) {
     91      console.debug('Dirtying listing cache', suite);
     92    }
     93    listingCache.delete(suite);
     94  }
     95 
     96  dirtyCompileCache(absPath, stats);
     97 }
     98 
     99 watcher.on('add', dirtyListingAndCompileCache);
    100 watcher.on('unlink', dirtyListingAndCompileCache);
    101 watcher.on('addDir', dirtyListingAndCompileCache);
    102 watcher.on('unlinkDir', dirtyListingAndCompileCache);
    103 watcher.on('change', dirtyCompileCache);
    104 
    105 const app = express();
    106 
    107 // Send Chrome Origin Trial tokens
    108 app.use((_req, res, next) => {
    109  next();
    110 });
    111 
    112 // Set up logging
    113 app.use(morgan('dev'));
    114 
    115 // Serve the standalone runner directory
    116 app.use('/standalone', express.static(path.resolve(srcDir, '../standalone')));
    117 // Add out-wpt/ build dir for convenience
    118 app.use('/out-wpt', express.static(path.resolve(srcDir, '../out-wpt')));
    119 app.use('/docs/tsdoc', express.static(path.resolve(srcDir, '../docs/tsdoc')));
    120 
    121 // Serve a suite's listing.js file by crawling the filesystem for all tests.
    122 app.get('/out/:suite([a-zA-Z0-9_-]+)/listing.js', async (req, res, next) => {
    123  const suite = req.params['suite'];
    124 
    125  if (listingCache.has(suite)) {
    126    res.setHeader('Content-Type', 'application/javascript');
    127    res.send(listingCache.get(suite));
    128    return;
    129  }
    130 
    131  try {
    132    const listing = await makeListing(path.resolve(srcDir, suite, 'listing.ts'));
    133    const result = `export const listing = ${JSON.stringify(listing, undefined, 2)}`;
    134 
    135    listingCache.set(suite, result);
    136    res.setHeader('Content-Type', 'application/javascript');
    137    res.send(result);
    138  } catch (err) {
    139    next(err);
    140  }
    141 });
    142 
    143 // Serve .as_worker.js files by generating the necessary wrapper.
    144 app.get('/out/:suite([a-zA-Z0-9_-]+)/webworker/:filepath(*).as_worker.js', (req, res, next) => {
    145  const { suite, filepath } = req.params;
    146  const result = `\
    147 import { g } from '/out/${suite}/${filepath}.spec.js';
    148 import { wrapTestGroupForWorker } from '/out/common/runtime/helper/wrap_for_worker.js';
    149 
    150 wrapTestGroupForWorker(g);
    151 `;
    152  res.setHeader('Content-Type', 'application/javascript');
    153  res.send(result);
    154 });
    155 
    156 // Serve all other .js files by fetching the source .ts file and compiling it.
    157 app.get('/out/**/*.js', async (req, res, next) => {
    158  const jsUrl = path.relative('/out', req.url);
    159  const tsUrl = jsUrl.replace(/\.js$/, '.ts');
    160  if (compileCache.has(tsUrl)) {
    161    res.setHeader('Content-Type', 'application/javascript');
    162    res.send(compileCache.get(tsUrl));
    163    return;
    164  }
    165 
    166  let absPath = path.join(srcDir, tsUrl);
    167  if (!fs.existsSync(absPath)) {
    168    // The .ts file doesn't exist. Try .js file in case this is a .js/.d.ts pair.
    169    absPath = path.join(srcDir, jsUrl);
    170  }
    171 
    172  try {
    173    const result = await babel.transformFileAsync(absPath, babelConfig);
    174    if (result && result.code) {
    175      compileCache.set(tsUrl, result.code);
    176 
    177      res.setHeader('Content-Type', 'application/javascript');
    178      res.send(result.code);
    179    } else {
    180      throw new Error(`Failed compile ${tsUrl}.`);
    181    }
    182  } catch (err) {
    183    next(err);
    184  }
    185 });
    186 
    187 // Serve everything else (not .js) as static, and directories as directory listings.
    188 app.use('/out', serveIndex(path.resolve(srcDir, '../src')));
    189 app.use('/out', express.static(path.resolve(srcDir, '../src')));
    190 
    191 void (async () => {
    192  let host = '127.0.0.1';
    193  if (process.argv.length >= 3) {
    194    if (process.argv.length !== 3) usage(1);
    195    if (process.argv[2] === '0.0.0.0') {
    196      host = '0.0.0.0';
    197    } else {
    198      usage(1);
    199    }
    200  }
    201 
    202  console.log(`Finding an available port on ${host}...`);
    203  const kPortFinderStart = 8080;
    204  const port = await portfinder.getPortPromise({ host, port: kPortFinderStart });
    205 
    206  watcher.on('ready', () => {
    207    // Listen on the available port.
    208    app.listen(port, host, () => {
    209      console.log('Standalone test runner running at:');
    210      if (host === '0.0.0.0') {
    211        for (const iface of Object.values(os.networkInterfaces())) {
    212          for (const details of iface || []) {
    213            if (details.family === 'IPv4') {
    214              console.log(`  http://${details.address}:${port}/standalone/`);
    215            }
    216          }
    217        }
    218      } else {
    219        console.log(`  http://${host}:${port}/standalone/`);
    220      }
    221    });
    222  });
    223 })();