summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/common/tools/dev_server.ts
blob: 2e0aca21ddd4bcc7f398cb82016c119947dfa9db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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';

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) => {
  res.header('Origin-Trial', [
    // Token for http://localhost:8080
    'AvyDIV+RJoYs8fn3W6kIrBhWw0te0klraoz04mw/nPb8VTus3w5HCdy+vXqsSzomIH745CT6B5j1naHgWqt/tw8AAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjYzNzE4Mzk5fQ==',
  ]);
  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/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 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);
  }
});

const host = '0.0.0.0';
const port = 8080;
// Find an available port, starting at 8080.
portfinder.getPort({ host, port }, (err, port) => {
  if (err) {
    throw err;
  }
  watcher.on('ready', () => {
    // Listen on the available port.
    app.listen(port, host, () => {
      console.log('Standalone test runner running at:');
      for (const iface of Object.values(os.networkInterfaces())) {
        for (const details of iface || []) {
          if (details.family === 'IPv4') {
            console.log(`  http://${details.address}:${port}/standalone/`);
          }
        }
      }
    });
  });
});

// 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')));