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.
- package/{cli.js → cli.ts} +7 -7
- package/deno.lock +6 -2
- package/lib/commands/{generate.js → generate.ts} +4 -4
- package/lib/commands/{help.js → help.ts} +1 -1
- package/lib/commands/{init.js → init.ts} +15 -7
- package/lib/commands/run/{tests-in-browser.js → tests-in-browser.ts} +28 -14
- package/lib/commands/{run.js → run.ts} +25 -17
- package/lib/servers/{http.js → http.ts} +84 -33
- package/lib/setup/bind-server-to-port.ts +14 -0
- package/lib/setup/{browser.js → browser.ts} +14 -18
- package/lib/setup/config.ts +55 -0
- package/lib/setup/{file-watcher.js → file-watcher.ts} +19 -5
- package/lib/setup/{fs-tree.js → fs-tree.ts} +16 -16
- package/lib/setup/{keyboard-events.js → keyboard-events.ts} +10 -5
- package/lib/setup/{test-file-paths.js → test-file-paths.ts} +20 -32
- package/lib/setup/{web-server.js → web-server.ts} +13 -16
- package/lib/setup/{write-output-static-files.js → write-output-static-files.ts} +6 -2
- package/lib/tap/{display-final-result.js → display-final-result.ts} +6 -3
- package/lib/tap/{display-test-result.js → display-test-result.ts} +22 -6
- package/lib/tap/{dump-yaml.js → dump-yaml.ts} +19 -5
- package/lib/types.ts +61 -0
- package/lib/utils/{chromium-args.js → chromium-args.ts} +1 -1
- package/lib/utils/{color.js → color.ts} +24 -11
- package/lib/utils/{early-chrome.js → early-chrome.ts} +5 -5
- package/lib/utils/{find-chrome.js → find-chrome.ts} +2 -2
- package/lib/utils/{find-internal-assets-from-html.js → find-internal-assets-from-html.ts} +1 -1
- package/lib/utils/{find-project-root.js → find-project-root.ts} +4 -4
- package/lib/utils/{indent-string.js → indent-string.ts} +6 -2
- package/lib/utils/{listen-to-keyboard-key.js → listen-to-keyboard-key.ts} +11 -7
- package/lib/utils/{parse-cli-flags.js → parse-cli-flags.ts} +21 -8
- package/lib/utils/{path-exists.js → path-exists.ts} +2 -2
- package/lib/utils/{perf-logger.js → perf-logger.ts} +1 -1
- package/lib/utils/{pre-launch-chrome.js → pre-launch-chrome.ts} +5 -1
- package/lib/utils/{read-boilerplate.js → read-boilerplate.ts} +1 -1
- package/lib/utils/{resolve-port-number-for.js → resolve-port-number-for.ts} +2 -2
- package/lib/utils/{run-user-module.js → run-user-module.ts} +6 -2
- package/lib/utils/{search-in-parent-directories.js → search-in-parent-directories.ts} +5 -2
- package/lib/utils/{time-counter.js → time-counter.ts} +2 -2
- package/package.json +14 -14
- package/lib/setup/bind-server-to-port.js +0 -9
- package/lib/setup/config.js +0 -48
- /package/lib/setup/{default-project-config-values.js → default-project-config-values.ts} +0 -0
package/{cli.js → cli.ts}
RENAMED
|
@@ -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.
|
|
4
|
-
import displayHelpOutput from './lib/commands/help.
|
|
5
|
-
import initializeProject from './lib/commands/init.
|
|
6
|
-
import generateTestFiles from './lib/commands/generate.
|
|
7
|
-
import setupConfig from './lib/setup/config.
|
|
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.
|
|
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.
|
|
3
|
-
import findProjectRoot from '../utils/find-project-root.
|
|
4
|
-
import pathExists from '../utils/path-exists.
|
|
5
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
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,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.
|
|
4
|
-
import pathExists from '../utils/path-exists.
|
|
5
|
-
import defaultProjectConfigValues from '../setup/default-project-config-values.
|
|
6
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
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(
|
|
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(
|
|
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.
|
|
2
|
+
import { blue } from '../../utils/color.ts';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
|
-
import timeCounter from '../../utils/time-counter.
|
|
5
|
-
import runUserModule from '../../utils/run-user-module.
|
|
6
|
-
import TAPDisplayFinalResult from '../../tap/display-final-result.
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
6
|
-
import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.
|
|
7
|
-
import fileWatcher from '../setup/file-watcher.
|
|
8
|
-
import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.
|
|
9
|
-
import runUserModule from '../utils/run-user-module.
|
|
10
|
-
import setupKeyboardEvents from '../setup/keyboard-events.
|
|
11
|
-
import writeOutputStaticFiles from '../setup/write-output-static-files.
|
|
12
|
-
import timeCounter from '../utils/time-counter.
|
|
13
|
-
import TAPDisplayFinalResult from '../tap/display-final-result.
|
|
14
|
-
import readBoilerplate from '../utils/read-boilerplate.
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
2
|
-
import bindServerToPort from './bind-server-to-port.
|
|
3
|
-
import findChrome from '../utils/find-chrome.
|
|
4
|
-
import CHROMIUM_ARGS from '../utils/chromium-args.
|
|
5
|
-
import { earlyBrowserPromise } from '../utils/early-chrome.
|
|
6
|
-
import { perfLog } from '../utils/perf-logger.
|
|
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.
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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),
|