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 })();