qunitx-cli 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/{cli.js → cli.ts} +7 -7
  2. package/deno.lock +6 -2
  3. package/lib/commands/{generate.js → generate.ts} +4 -4
  4. package/lib/commands/{help.js → help.ts} +1 -1
  5. package/lib/commands/{init.js → init.ts} +15 -7
  6. package/lib/commands/run/{tests-in-browser.js → tests-in-browser.ts} +28 -14
  7. package/lib/commands/{run.js → run.ts} +25 -17
  8. package/lib/servers/{http.js → http.ts} +84 -33
  9. package/lib/setup/bind-server-to-port.ts +14 -0
  10. package/lib/setup/{browser.js → browser.ts} +14 -18
  11. package/lib/setup/config.ts +55 -0
  12. package/lib/setup/{file-watcher.js → file-watcher.ts} +19 -5
  13. package/lib/setup/{fs-tree.js → fs-tree.ts} +16 -16
  14. package/lib/setup/{keyboard-events.js → keyboard-events.ts} +10 -5
  15. package/lib/setup/{test-file-paths.js → test-file-paths.ts} +20 -32
  16. package/lib/setup/{web-server.js → web-server.ts} +13 -16
  17. package/lib/setup/{write-output-static-files.js → write-output-static-files.ts} +6 -2
  18. package/lib/tap/{display-final-result.js → display-final-result.ts} +6 -3
  19. package/lib/tap/{display-test-result.js → display-test-result.ts} +22 -6
  20. package/lib/tap/{dump-yaml.js → dump-yaml.ts} +19 -5
  21. package/lib/types.ts +61 -0
  22. package/lib/utils/{chromium-args.js → chromium-args.ts} +1 -1
  23. package/lib/utils/{color.js → color.ts} +24 -11
  24. package/lib/utils/{early-chrome.js → early-chrome.ts} +5 -5
  25. package/lib/utils/{find-chrome.js → find-chrome.ts} +2 -2
  26. package/lib/utils/{find-internal-assets-from-html.js → find-internal-assets-from-html.ts} +1 -1
  27. package/lib/utils/{find-project-root.js → find-project-root.ts} +4 -4
  28. package/lib/utils/{indent-string.js → indent-string.ts} +6 -2
  29. package/lib/utils/{listen-to-keyboard-key.js → listen-to-keyboard-key.ts} +11 -7
  30. package/lib/utils/{parse-cli-flags.js → parse-cli-flags.ts} +21 -8
  31. package/lib/utils/{path-exists.js → path-exists.ts} +2 -2
  32. package/lib/utils/{perf-logger.js → perf-logger.ts} +1 -1
  33. package/lib/utils/{pre-launch-chrome.js → pre-launch-chrome.ts} +5 -1
  34. package/lib/utils/{read-boilerplate.js → read-boilerplate.ts} +1 -1
  35. package/lib/utils/{resolve-port-number-for.js → resolve-port-number-for.ts} +2 -2
  36. package/lib/utils/{run-user-module.js → run-user-module.ts} +6 -2
  37. package/lib/utils/{search-in-parent-directories.js → search-in-parent-directories.ts} +5 -2
  38. package/lib/utils/{time-counter.js → time-counter.ts} +2 -2
  39. package/package.json +14 -14
  40. package/lib/setup/bind-server-to-port.js +0 -9
  41. package/lib/setup/config.js +0 -48
  42. /package/lib/setup/{default-project-config-values.js → default-project-config-values.ts} +0 -0
@@ -1,10 +1,10 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
2
  import process from 'node:process';
3
- import './lib/utils/early-chrome.js';
4
- import displayHelpOutput from './lib/commands/help.js';
5
- import initializeProject from './lib/commands/init.js';
6
- import generateTestFiles from './lib/commands/generate.js';
7
- import setupConfig from './lib/setup/config.js';
3
+ import './lib/utils/early-chrome.ts';
4
+ import displayHelpOutput from './lib/commands/help.ts';
5
+ import initializeProject from './lib/commands/init.ts';
6
+ import generateTestFiles from './lib/commands/generate.ts';
7
+ import setupConfig from './lib/setup/config.ts';
8
8
 
9
9
  process.title = 'qunitx';
10
10
 
@@ -24,7 +24,7 @@ process.title = 'qunitx';
24
24
  // lets playwright-core start loading while config is being assembled.
25
25
  const [config, { default: run }] = await Promise.all([
26
26
  setupConfig(),
27
- import('./lib/commands/run.js'),
27
+ import('./lib/commands/run.ts'),
28
28
  ]);
29
29
 
30
30
  try {
package/deno.lock CHANGED
@@ -11,10 +11,10 @@
11
11
  "npm:express@^5.2.1": "5.2.1",
12
12
  "npm:js-yaml@^4.1.1": "4.1.1",
13
13
  "npm:picomatch@*": "4.0.4",
14
- "npm:picomatch@^4.0.4": "4.0.4",
15
14
  "npm:playwright-core@^1.58.2": "1.58.2",
16
15
  "npm:prettier@^3.8.1": "3.8.1",
17
16
  "npm:qunitx@^1.0.4": "1.0.4",
17
+ "npm:typescript@5": "5.9.3",
18
18
  "npm:ws@*": "8.20.0",
19
19
  "npm:ws@^8.20.0": "8.20.0"
20
20
  },
@@ -603,6 +603,10 @@
603
603
  "mime-types"
604
604
  ]
605
605
  },
606
+ "typescript@5.9.3": {
607
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
608
+ "bin": true
609
+ },
606
610
  "undici-types@7.18.2": {
607
611
  "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="
608
612
  },
@@ -631,10 +635,10 @@
631
635
  "npm:esbuild@~0.27.3",
632
636
  "npm:express@^5.2.1",
633
637
  "npm:js-yaml@^4.1.1",
634
- "npm:picomatch@^4.0.4",
635
638
  "npm:playwright-core@^1.58.2",
636
639
  "npm:prettier@^3.8.1",
637
640
  "npm:qunitx@^1.0.4",
641
+ "npm:typescript@5",
638
642
  "npm:ws@^8.20.0"
639
643
  ]
640
644
  }
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
- import { green } from '../utils/color.js';
3
- import findProjectRoot from '../utils/find-project-root.js';
4
- import pathExists from '../utils/path-exists.js';
5
- import readBoilerplate from '../utils/read-boilerplate.js';
2
+ import { green } from '../utils/color.ts';
3
+ import findProjectRoot from '../utils/find-project-root.ts';
4
+ import pathExists from '../utils/path-exists.ts';
5
+ import readBoilerplate from '../utils/read-boilerplate.ts';
6
6
 
7
7
  /**
8
8
  * Generates a new test file from the boilerplate template.
@@ -1,4 +1,4 @@
1
- import { blue, magenta } from '../utils/color.js';
1
+ import { blue, magenta } from '../utils/color.ts';
2
2
  import pkg from '../../package.json' with { type: 'json' };
3
3
 
4
4
  const highlight = (text) => magenta().bold(text);
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import findProjectRoot from '../utils/find-project-root.js';
4
- import pathExists from '../utils/path-exists.js';
5
- import defaultProjectConfigValues from '../setup/default-project-config-values.js';
6
- import readBoilerplate from '../utils/read-boilerplate.js';
3
+ import findProjectRoot from '../utils/find-project-root.ts';
4
+ import pathExists from '../utils/path-exists.ts';
5
+ import defaultProjectConfigValues from '../setup/default-project-config-values.ts';
6
+ import readBoilerplate from '../utils/read-boilerplate.ts';
7
7
 
8
8
  /** Bootstraps a new qunitx project: writes the test HTML template, updates package.json, and optionally writes tsconfig.json. */
9
9
  export default async function initializeProject() {
@@ -23,7 +23,11 @@ export default async function initializeProject() {
23
23
  ]);
24
24
  }
25
25
 
26
- async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
26
+ async function writeTestsHTML(
27
+ projectRoot: string,
28
+ config: { htmlPaths: string[]; output: string },
29
+ oldPackageJSON: Record<string, unknown>,
30
+ ): Promise<unknown[]> {
27
31
  const testHTMLTemplateBuffer = await readBoilerplate('setup/tests.hbs');
28
32
 
29
33
  return await Promise.all(
@@ -51,13 +55,17 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
51
55
  );
52
56
  }
53
57
 
54
- async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
58
+ async function rewritePackageJSON(
59
+ projectRoot: string,
60
+ config: unknown,
61
+ oldPackageJSON: Record<string, unknown>,
62
+ ): Promise<void> {
55
63
  const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
56
64
 
57
65
  await fs.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
58
66
  }
59
67
 
60
- async function writeTSConfigIfNeeded(projectRoot) {
68
+ async function writeTSConfigIfNeeded(projectRoot: string): Promise<void> {
61
69
  const targetPath = `${projectRoot}/tsconfig.json`;
62
70
  if (!(await pathExists(targetPath))) {
63
71
  const tsConfigTemplate = await readBoilerplate('setup/tsconfig.json');
@@ -1,12 +1,14 @@
1
1
  import fs from 'node:fs/promises';
2
- import { blue } from '../../utils/color.js';
2
+ import { blue } from '../../utils/color.ts';
3
3
  import esbuild from 'esbuild';
4
- import timeCounter from '../../utils/time-counter.js';
5
- import runUserModule from '../../utils/run-user-module.js';
6
- import TAPDisplayFinalResult from '../../tap/display-final-result.js';
4
+ import timeCounter from '../../utils/time-counter.ts';
5
+ import runUserModule from '../../utils/run-user-module.ts';
6
+ import TAPDisplayFinalResult from '../../tap/display-final-result.ts';
7
+ import type { Config, CachedContent, Connections } from '../../types.ts';
8
+ import type HTTPServer from '../../servers/http.ts';
7
9
 
8
10
  class BundleError extends Error {
9
- constructor(message) {
11
+ constructor(message: unknown) {
10
12
  super(message);
11
13
  this.name = 'BundleError';
12
14
  this.message = `esbuild Bundle Error: ${message}`.split('\n').join('\n# ');
@@ -17,7 +19,7 @@ class BundleError extends Error {
17
19
  * Pre-builds the esbuild bundle for all test files and caches the result in `cachedContent`.
18
20
  * @returns {Promise<void>}
19
21
  */
20
- export async function buildTestBundle(config, cachedContent) {
22
+ export async function buildTestBundle(config: Config, cachedContent: CachedContent): Promise<void> {
21
23
  const { projectRoot, output } = config;
22
24
  const allTestFilePaths = Object.keys(config.fsTree);
23
25
 
@@ -52,11 +54,11 @@ export async function buildTestBundle(config, cachedContent) {
52
54
  * @returns {Promise<object>}
53
55
  */
54
56
  export default async function runTestsInBrowser(
55
- config,
56
- cachedContent = {},
57
- connections,
58
- targetTestFilesToFilter,
59
- ) {
57
+ config: Config,
58
+ cachedContent: CachedContent = {} as CachedContent,
59
+ connections: Connections,
60
+ targetTestFilesToFilter?: string[],
61
+ ): Promise<Connections | undefined> {
60
62
  const { projectRoot, output } = config;
61
63
  const allTestFilePaths = Object.keys(config.fsTree);
62
64
  const runHasFilter = !!targetTestFilesToFilter;
@@ -124,7 +126,11 @@ export default async function runTestsInBrowser(
124
126
  return connections;
125
127
  }
126
128
 
127
- function buildFilteredTests(filteredTests, outputPath, config) {
129
+ function buildFilteredTests(
130
+ filteredTests: string[],
131
+ outputPath: string,
132
+ config: Config,
133
+ ): Promise<esbuild.BuildResult> {
128
134
  return esbuild.build({
129
135
  stdin: {
130
136
  contents: filteredTests.map((f) => `import "${f}";`).join(''),
@@ -137,7 +143,11 @@ function buildFilteredTests(filteredTests, outputPath, config) {
137
143
  });
138
144
  }
139
145
 
140
- async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
146
+ async function runTestInsideHTMLFile(
147
+ filePath: string,
148
+ { page, server, browser }: Connections,
149
+ config: Config,
150
+ ): Promise<void> {
141
151
  let QUNIT_RESULT;
142
152
  let targetError;
143
153
  let timeoutHandle;
@@ -192,7 +202,11 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
192
202
  }
193
203
  }
194
204
 
195
- async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false) {
205
+ async function failOnNonWatchMode(
206
+ watchMode: boolean = false,
207
+ connections: { server?: HTTPServer; browser?: { close(): Promise<void> } } = {},
208
+ groupMode: boolean = false,
209
+ ): Promise<void> {
196
210
  if (!watchMode) {
197
211
  if (groupMode) {
198
212
  // Parent orchestrator handles cleanup and exit; signal failure via throw.
@@ -1,23 +1,24 @@
1
- import setupBrowser, { launchBrowser } from '../setup/browser.js';
1
+ import setupBrowser, { launchBrowser } from '../setup/browser.ts';
2
2
  import fs from 'node:fs/promises';
3
3
  import { normalize } from 'node:path';
4
4
  import { availableParallelism } from 'node:os';
5
- import { blue, yellow } from '../utils/color.js';
6
- import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.js';
7
- import fileWatcher from '../setup/file-watcher.js';
8
- import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.js';
9
- import runUserModule from '../utils/run-user-module.js';
10
- import setupKeyboardEvents from '../setup/keyboard-events.js';
11
- import writeOutputStaticFiles from '../setup/write-output-static-files.js';
12
- import timeCounter from '../utils/time-counter.js';
13
- import TAPDisplayFinalResult from '../tap/display-final-result.js';
14
- import readBoilerplate from '../utils/read-boilerplate.js';
5
+ import { blue, yellow } from '../utils/color.ts';
6
+ import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.ts';
7
+ import fileWatcher from '../setup/file-watcher.ts';
8
+ import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.ts';
9
+ import runUserModule from '../utils/run-user-module.ts';
10
+ import setupKeyboardEvents from '../setup/keyboard-events.ts';
11
+ import writeOutputStaticFiles from '../setup/write-output-static-files.ts';
12
+ import timeCounter from '../utils/time-counter.ts';
13
+ import TAPDisplayFinalResult from '../tap/display-final-result.ts';
14
+ import readBoilerplate from '../utils/read-boilerplate.ts';
15
+ import type { Config, CachedContent } from '../types.ts';
15
16
 
16
17
  /**
17
18
  * Runs qunitx tests in headless Chrome, either in watch mode or concurrent batch mode.
18
19
  * @returns {Promise<void>}
19
20
  */
20
- export default async function run(config) {
21
+ export default async function run(config: Config): Promise<void> {
21
22
  const cachedContent = await buildCachedContent(config, config.htmlPaths);
22
23
 
23
24
  if (config.watch) {
@@ -185,7 +186,7 @@ export default async function run(config) {
185
186
  }
186
187
  }
187
188
 
188
- async function buildCachedContent(config, htmlPaths) {
189
+ async function buildCachedContent(config: Config, htmlPaths: string[]): Promise<CachedContent> {
189
190
  const htmlBuffers = await Promise.all(config.htmlPaths.map((htmlPath) => fs.readFile(htmlPath)));
190
191
  const cachedContent = htmlPaths.reduce(
191
192
  (result, _htmlPath, index) => {
@@ -228,7 +229,10 @@ async function buildCachedContent(config, htmlPaths) {
228
229
  return addCachedContentMainHTML(config.projectRoot, cachedContent);
229
230
  }
230
231
 
231
- async function addCachedContentMainHTML(projectRoot, cachedContent) {
232
+ async function addCachedContentMainHTML(
233
+ projectRoot: string,
234
+ cachedContent: CachedContent,
235
+ ): Promise<CachedContent> {
232
236
  const mainHTMLPath = Object.keys(cachedContent.dynamicContentHTMLs)[0];
233
237
  if (mainHTMLPath) {
234
238
  cachedContent.mainHTML = {
@@ -244,13 +248,13 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
244
248
  return cachedContent;
245
249
  }
246
250
 
247
- function splitIntoGroups(files, groupCount) {
251
+ function splitIntoGroups(files: string[], groupCount: number): string[][] {
248
252
  const groups = Array.from({ length: groupCount }, () => []);
249
253
  files.forEach((file, i) => groups[i % groupCount].push(file));
250
254
  return groups.filter((g) => g.length > 0);
251
255
  }
252
256
 
253
- function logWatcherAndKeyboardShortcutInfo(config, _server) {
257
+ function logWatcherAndKeyboardShortcutInfo(config: Config, _server: unknown): void {
254
258
  console.log(
255
259
  '#',
256
260
  blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
@@ -263,7 +267,11 @@ function logWatcherAndKeyboardShortcutInfo(config, _server) {
263
267
  );
264
268
  }
265
269
 
266
- function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
270
+ function normalizeInternalAssetPathFromHTML(
271
+ projectRoot: string,
272
+ assetPath: string,
273
+ htmlPath: string,
274
+ ): string {
267
275
  const currentDirectory = htmlPath ? htmlPath.split('/').slice(0, -1).join('/') : projectRoot;
268
276
  return assetPath.startsWith('./')
269
277
  ? normalize(`${currentDirectory}/${assetPath.slice(2)}`)
@@ -1,10 +1,44 @@
1
1
  import http from 'node:http';
2
2
  // @deno-types="npm:@types/ws"
3
3
  import WebSocket, { WebSocketServer } from 'ws';
4
- import bindServerToPort from '../setup/bind-server-to-port.js';
4
+ import bindServerToPort from '../setup/bind-server-to-port.ts';
5
+
6
+ declare module 'node:http' {
7
+ interface IncomingMessage {
8
+ send: (data: string) => void;
9
+ path: string;
10
+ query: Record<string, string>;
11
+ params: Record<string, string>;
12
+ }
13
+ interface ServerResponse {
14
+ json: (data: unknown) => void;
15
+ }
16
+ }
17
+
18
+ type NodeServerWithWSS = http.Server & { wss: WebSocketServer };
19
+
20
+ /** Route handler function signature for registered GET/POST/etc. routes. */
21
+ export type RouteHandler = (
22
+ req: http.IncomingMessage,
23
+ res: http.ServerResponse,
24
+ ) => void | Promise<void>;
25
+ /** Middleware function signature — call `next()` to continue the chain. */
26
+ export type Middleware = (
27
+ req: http.IncomingMessage,
28
+ res: http.ServerResponse,
29
+ next: () => void,
30
+ ) => void;
31
+
32
+ interface Route {
33
+ path: string;
34
+ handler: RouteHandler;
35
+ paramNames: string[];
36
+ isWildcard: boolean;
37
+ paramValues?: string[];
38
+ }
5
39
 
6
40
  /** Map of file extensions to their corresponding MIME type strings. */
7
- export const MIME_TYPES = {
41
+ export const MIME_TYPES: Record<string, string> = {
8
42
  html: 'text/html; charset=UTF-8',
9
43
  js: 'application/javascript',
10
44
  css: 'text/css',
@@ -17,13 +51,27 @@ export const MIME_TYPES = {
17
51
 
18
52
  /** Minimal HTTP + WebSocket server used to serve test bundles and push reload events. */
19
53
  export default class HTTPServer {
54
+ /** Registered routes keyed by HTTP method then path. */
55
+ routes: Record<string, Record<string, Route>>;
56
+ /** Registered middleware functions, applied in order before each route handler. */
57
+ middleware: Middleware[];
58
+ /** Underlying Node.js HTTP server instance. */
59
+ _server: http.Server;
60
+ /** WebSocket server attached to the HTTP server for live-reload broadcasts. */
61
+ wss: WebSocketServer;
62
+
20
63
  /**
21
64
  * Creates and starts a plain `http.createServer` instance on the given port.
22
65
  * @returns {Promise<object>}
23
66
  */
24
- static serve(config = { port: 1234 }, handler) {
25
- const onListen = config.onListen || ((_server) => {});
26
- const onError = config.onError || ((_error) => {});
67
+ static serve(
68
+ config: { port: number; onListen?: (s: object) => void; onError?: (e: Error) => void } = {
69
+ port: 1234,
70
+ },
71
+ handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
72
+ ): Promise<http.Server> {
73
+ const onListen = config.onListen || ((_server: object) => {});
74
+ const onError = config.onError || ((_error: Error) => {});
27
75
 
28
76
  return new Promise((resolve, reject) => {
29
77
  const server = http.createServer((req, res) => {
@@ -38,14 +86,13 @@ export default class HTTPServer {
38
86
  onListen(Object.assign({ hostname: '127.0.0.1', server }, config));
39
87
  resolve(server);
40
88
  });
41
-
42
- server.wss = new WebSocketServer({ server });
43
- server.wss.on('error', (error) => {
89
+ (server as NodeServerWithWSS).wss = new WebSocketServer({ server });
90
+ (server as NodeServerWithWSS).wss.on('error', (error: Error) => {
44
91
  console.log('# [WebSocketServer] Error:');
45
92
  console.trace(error);
46
93
  });
47
94
 
48
- bindServerToPort(server, config);
95
+ bindServerToPort(server as unknown as HTTPServer, config as { port: number });
49
96
  });
50
97
  }
51
98
 
@@ -58,11 +105,11 @@ export default class HTTPServer {
58
105
  };
59
106
  this.middleware = [];
60
107
  this._server = http.createServer((req, res) => {
61
- res.send = (data) => {
108
+ req.send = (data: string) => {
62
109
  res.setHeader('Content-Type', 'text/plain');
63
110
  res.end(data);
64
111
  };
65
- res.json = (data) => {
112
+ res.json = (data: unknown) => {
66
113
  res.setHeader('Content-Type', 'application/json');
67
114
  res.end(JSON.stringify(data));
68
115
  };
@@ -81,13 +128,13 @@ export default class HTTPServer {
81
128
  * Promise that resolves once the server is fully closed.
82
129
  * @returns {Promise<void>}
83
130
  */
84
- close() {
131
+ close(): Promise<void> {
85
132
  this._server.closeAllConnections?.();
86
- return new Promise((resolve) => this._server.close(resolve));
133
+ return new Promise((resolve) => this._server.close(resolve as () => void));
87
134
  }
88
135
 
89
136
  /** Registers a GET route handler. */
90
- get(path, handler) {
137
+ get(path: string, handler: RouteHandler): void {
91
138
  this.#registerRouteHandler('GET', path, handler);
92
139
  }
93
140
 
@@ -95,9 +142,9 @@ export default class HTTPServer {
95
142
  * Starts listening on the given port (0 = OS-assigned).
96
143
  * @returns {Promise<void>}
97
144
  */
98
- listen(port = 0, callback = () => {}) {
145
+ listen(port = 0, callback: () => void = () => {}): Promise<void> {
99
146
  return new Promise((resolve, reject) => {
100
- const onError = (err) => {
147
+ const onError = (err: Error) => {
101
148
  this._server.off('listening', onListening);
102
149
  reject(err);
103
150
  };
@@ -112,7 +159,7 @@ export default class HTTPServer {
112
159
  }
113
160
 
114
161
  /** Broadcasts a message to all connected WebSocket clients. */
115
- publish(data) {
162
+ publish(data: string): void {
116
163
  this.wss.clients.forEach((client) => {
117
164
  if (client.readyState === WebSocket.OPEN) {
118
165
  client.send(data);
@@ -121,26 +168,26 @@ export default class HTTPServer {
121
168
  }
122
169
 
123
170
  /** Registers a POST route handler. */
124
- post(path, handler) {
171
+ post(path: string, handler: RouteHandler): void {
125
172
  this.#registerRouteHandler('POST', path, handler);
126
173
  }
127
174
 
128
175
  /** Registers a DELETE route handler. */
129
- delete(path, handler) {
176
+ delete(path: string, handler: RouteHandler): void {
130
177
  this.#registerRouteHandler('DELETE', path, handler);
131
178
  }
132
179
 
133
180
  /** Registers a PUT route handler. */
134
- put(path, handler) {
181
+ put(path: string, handler: RouteHandler): void {
135
182
  this.#registerRouteHandler('PUT', path, handler);
136
183
  }
137
184
 
138
185
  /** Adds a middleware function to the chain. */
139
- use(middleware) {
186
+ use(middleware: Middleware): void {
140
187
  this.middleware.push(middleware);
141
188
  }
142
189
 
143
- #registerRouteHandler(method, path, handler) {
190
+ #registerRouteHandler(method: string, path: string, handler: RouteHandler): void {
144
191
  if (!this.routes[method]) {
145
192
  this.routes[method] = {};
146
193
  }
@@ -153,13 +200,13 @@ export default class HTTPServer {
153
200
  };
154
201
  }
155
202
 
156
- #handleRequest(req, res) {
203
+ #handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
157
204
  const { method, url } = req;
158
- const urlObj = new URL(url, 'http://localhost');
205
+ const urlObj = new URL(url!, 'http://localhost');
159
206
  const pathname = urlObj.pathname;
160
207
  req.path = pathname;
161
208
  req.query = Object.fromEntries(urlObj.searchParams);
162
- const matchingRoute = this.#findRouteHandler(method, pathname);
209
+ const matchingRoute = this.#findRouteHandler(method!, pathname);
163
210
 
164
211
  if (matchingRoute) {
165
212
  req.params = this.#extractParams(matchingRoute, pathname);
@@ -171,7 +218,11 @@ export default class HTTPServer {
171
218
  }
172
219
  }
173
220
 
174
- #runMiddleware(req, res, callback) {
221
+ #runMiddleware(
222
+ req: http.IncomingMessage,
223
+ res: http.ServerResponse,
224
+ callback: RouteHandler,
225
+ ): void {
175
226
  let index = 0;
176
227
  const next = () => {
177
228
  if (index >= this.middleware.length) {
@@ -185,7 +236,7 @@ export default class HTTPServer {
185
236
  next();
186
237
  }
187
238
 
188
- #findRouteHandler(method, url) {
239
+ #findRouteHandler(method: string, url: string): Route | null {
189
240
  const routes = this.routes[method];
190
241
  if (!routes) {
191
242
  return null;
@@ -219,7 +270,7 @@ export default class HTTPServer {
219
270
  );
220
271
  }
221
272
 
222
- #matchPathSegments(path, url) {
273
+ #matchPathSegments(path: string, url: string): boolean {
223
274
  const pathSegments = path.split('/');
224
275
  const urlSegments = url.split('/');
225
276
 
@@ -243,26 +294,26 @@ export default class HTTPServer {
243
294
  return true;
244
295
  }
245
296
 
246
- #buildRegexPattern(path, _paramNames) {
297
+ #buildRegexPattern(path: string, _paramNames: string[]): string {
247
298
  let regexPattern = path.replace(/:[^/]+/g, '([^/]+)');
248
299
  regexPattern = regexPattern.replace(/\//g, '\\/');
249
300
 
250
301
  return regexPattern;
251
302
  }
252
303
 
253
- #extractParamNames(path) {
304
+ #extractParamNames(path: string): string[] {
254
305
  const paramRegex = /:(\w+)/g;
255
306
  const paramMatches = path.match(paramRegex);
256
307
 
257
308
  return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
258
309
  }
259
310
 
260
- #extractParams(route, _url) {
311
+ #extractParams(route: Route, _url: string): Record<string, string> {
261
312
  const { paramNames, paramValues } = route;
262
- const params = {};
313
+ const params: Record<string, string> = {};
263
314
 
264
315
  for (let i = 0; i < paramNames.length; i++) {
265
- params[paramNames[i]] = paramValues[i];
316
+ params[paramNames[i]] = paramValues![i];
266
317
  }
267
318
 
268
319
  return params;
@@ -0,0 +1,14 @@
1
+ import type HTTPServer from '../servers/http.ts';
2
+
3
+ /**
4
+ * Binds an HTTPServer to an OS-assigned port and writes the resolved port back to `config.port`.
5
+ * @returns {Promise<object>}
6
+ */
7
+ export default async function bindServerToPort(
8
+ server: HTTPServer,
9
+ config: { port: number },
10
+ ): Promise<HTTPServer> {
11
+ await server.listen(0);
12
+ config.port = (server._server.address() as import('node:net').AddressInfo).port;
13
+ return server;
14
+ }
@@ -1,15 +1,17 @@
1
- import setupWebServer from './web-server.js';
2
- import bindServerToPort from './bind-server-to-port.js';
3
- import findChrome from '../utils/find-chrome.js';
4
- import CHROMIUM_ARGS from '../utils/chromium-args.js';
5
- import { earlyBrowserPromise } from '../utils/early-chrome.js';
6
- import { perfLog } from '../utils/perf-logger.js';
1
+ import setupWebServer from './web-server.ts';
2
+ import bindServerToPort from './bind-server-to-port.ts';
3
+ import findChrome from '../utils/find-chrome.ts';
4
+ import CHROMIUM_ARGS from '../utils/chromium-args.ts';
5
+ import { earlyBrowserPromise } from '../utils/early-chrome.ts';
6
+ import { perfLog } from '../utils/perf-logger.ts';
7
+ import type { Browser } from 'playwright-core';
8
+ import type { Config, CachedContent, Connections } from '../types.ts';
7
9
 
8
10
  // Playwright-core starts loading the moment run.js imports this module.
9
11
  // browser.js is intentionally the first import in run.js so playwright-core
10
12
  // starts loading before heavier deps (esbuild, chokidar) queue up I/O reads
11
13
  // and saturate libuv's thread pool, which would delay the dynamic import resolution.
12
- // early-chrome.js (statically imported by cli.js) already started Chrome pre-launch,
14
+ // early-chrome.ts (statically imported by cli.ts) already started Chrome pre-launch,
13
15
  // so both race in parallel — Chrome is typically ready when playwright-core finishes.
14
16
  const playwrightCorePromise = import('playwright-core');
15
17
  perfLog('browser.js: playwright-core import started');
@@ -21,7 +23,7 @@ perfLog('browser.js: playwright-core import started');
21
23
  * For firefox/webkit: uses playwright's standard launch (requires `npx playwright install [browser]`).
22
24
  * @returns {Promise<object>}
23
25
  */
24
- export async function launchBrowser(config) {
26
+ export async function launchBrowser(config: Config): Promise<Browser> {
25
27
  const browserName = config.browser || 'chromium';
26
28
 
27
29
  if (browserName === 'chromium') {
@@ -60,16 +62,10 @@ export async function launchBrowser(config) {
60
62
  * @returns {Promise<{server: object, browser: object, page: object}>}
61
63
  */
62
64
  export default async function setupBrowser(
63
- config = {
64
- port: 1234,
65
- debug: false,
66
- watch: false,
67
- timeout: 10000,
68
- browser: 'chromium',
69
- },
70
- cachedContent,
71
- existingBrowser = null,
72
- ) {
65
+ config: Config,
66
+ cachedContent: CachedContent,
67
+ existingBrowser: Browser | null = null,
68
+ ): Promise<Connections> {
73
69
  const setupStart = Date.now();
74
70
  const [server, resolvedExistingBrowser] = await Promise.all([
75
71
  setupWebServer(config, cachedContent),