qunitx 0.5.3 → 0.6.0

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.
@@ -122,8 +122,8 @@ async function runTestInsideHTMLFile(filePath, { page, server }, config) {
122
122
  let targetError;
123
123
  try {
124
124
  await wait(350);
125
- console.log('#', kleur.blue(`QUnitX running: http://localhost:${server.config.port}${filePath}`));
126
- await page.goto(`http://localhost:${server.config.port}${filePath}`, { timeout: 0 });
125
+ console.log('#', kleur.blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
126
+ await page.goto(`http://localhost:${config.port}${filePath}`, { timeout: 0 });
127
127
  await page.evaluate(() => {
128
128
  window.IS_PUPPETEER = true;
129
129
  });
@@ -142,7 +142,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
142
142
 
143
143
  function logWatcherAndKeyboardShortcutInfo(config, server) {
144
144
  if (config.browser) {
145
- console.log('#', kleur.blue(`Watching files... You can browse the tests on http://localhost:${server.config.port} ...`)); // NOTE: maybe add also qx to exit
145
+ console.log('#', kleur.blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`)); // NOTE: maybe add also qx to exit
146
146
  console.log('#', kleur.blue(`Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`)); // NOTE: maybe add also qx to test specific
147
147
  } else {
148
148
  console.log('#', kleur.blue(`Watching files...`));
@@ -0,0 +1,229 @@
1
+ import http from 'node:http';
2
+ import WebSocket, { WebSocketServer } from 'ws';
3
+ import bindServerToPort from '../setup/bind-server-to-port.js';
4
+
5
+ export const MIME_TYPES = {
6
+ html: "text/html; charset=UTF-8",
7
+ js: "application/javascript",
8
+ css: "text/css",
9
+ png: "image/png",
10
+ jpg: "image/jpg",
11
+ gif: "image/gif",
12
+ ico: "image/x-icon",
13
+ svg: "image/svg+xml",
14
+ };
15
+
16
+ export default class HTTPServer {
17
+ static serve(config = { port: 1234 }, handler) {
18
+ let onListen = config.onListen || ((server) => {});
19
+ let onError = config.onError || ((error) => {});
20
+
21
+ return new Promise((resolve, reject) => {
22
+ let server = http.createServer((req, res) => {
23
+ return handler(req, res);
24
+ });
25
+ server = server;
26
+ server.on('error', (error) => {
27
+ onError(error);
28
+ reject(error);
29
+ }).once('listening', () => {
30
+ onListen(Object.assign({ hostname: '127.0.0.1', server }, config));
31
+ resolve(server);
32
+ })
33
+
34
+ server.wss = new WebSocketServer({ server });
35
+ server.wss.on('error', (error) => {
36
+ console.log('# [WebSocketServer] Error:');
37
+ console.trace(error);
38
+ });
39
+
40
+ bindServerToPort(server, config)
41
+ });
42
+ }
43
+
44
+ constructor() {
45
+ this.routes = {
46
+ GET: {},
47
+ POST: {},
48
+ DELETE: {},
49
+ PUT: {}
50
+ };
51
+ this.middleware = [];
52
+ this._server = http.createServer((req, res) => {
53
+ res.send = (data) => {
54
+ res.setHeader('Content-Type', 'text/plain');
55
+ res.end(data);
56
+ };
57
+ res.json = (data) => {
58
+ res.setHeader('Content-Type', 'application/json');
59
+ res.end(JSON.stringify(data));
60
+ };
61
+
62
+ return this.handleRequest(req, res);
63
+ });
64
+ this.wss = new WebSocketServer({ server: this._server });
65
+ this.wss.on('error', (error) => {
66
+ console.log('### WebSocketServer Error:');
67
+ console.log(error);
68
+ });
69
+ }
70
+
71
+ get(path, handler) {
72
+ this.registerRouteHandler('GET', path, handler);
73
+ }
74
+
75
+ listen(port = 0, callback = () => {}) {
76
+ return new Promise((resolve, reject) => {
77
+ this._server.listen(port, (error) => {
78
+ if (error) {
79
+ reject(error);
80
+ } else {
81
+ resolve(callback());
82
+ }
83
+ });
84
+ });
85
+ }
86
+
87
+ publish(data) {
88
+ this.wss.clients.forEach((client) => {
89
+ if (client.readyState === WebSocket.OPEN) {
90
+ client.send(data);
91
+ }
92
+ });
93
+ }
94
+
95
+ post(path, handler) {
96
+ this.registerRouteHandler('POST', path, handler);
97
+ }
98
+
99
+ delete(path, handler) {
100
+ this.registerRouteHandler('DELETE', path, handler);
101
+ }
102
+
103
+ put(path, handler) {
104
+ this.registerRouteHandler('PUT', path, handler);
105
+ }
106
+
107
+ use(middleware) {
108
+ this.middleware.push(middleware);
109
+ }
110
+
111
+ registerRouteHandler(method, path, handler) {
112
+ if (!this.routes[method]) {
113
+ this.routes[method] = {};
114
+ }
115
+
116
+ this.routes[method][path] = {
117
+ path,
118
+ handler,
119
+ paramNames: this.extractParamNames(path),
120
+ isWildcard: path === '/*'
121
+ };
122
+ }
123
+
124
+ handleRequest(req, res) {
125
+ const { method, url } = req;
126
+ const matchingRoute = this.findRouteHandler(method, url);
127
+
128
+ if (matchingRoute) {
129
+ req.params = this.extractParams(matchingRoute, url);
130
+ this.runMiddleware(req, res, matchingRoute.handler);
131
+ } else {
132
+ res.statusCode = 404;
133
+ res.setHeader('Content-Type', 'text/plain');
134
+ res.end('Not found');
135
+ }
136
+ }
137
+
138
+ runMiddleware(req, res, callback) {
139
+ let index = 0;
140
+ const next = () => {
141
+ if (index >= this.middleware.length) {
142
+ callback(req, res);
143
+ } else {
144
+ const middleware = this.middleware[index];
145
+ index++;
146
+ middleware(req, res, next);
147
+ }
148
+ };
149
+ next();
150
+ }
151
+
152
+ findRouteHandler(method, url) {
153
+ const routes = this.routes[method];
154
+ if (!routes) {
155
+ return null;
156
+ }
157
+
158
+ return routes[url] || Object.values(routes).find(route => {
159
+ const { path, isWildcard } = route;
160
+
161
+ if (!isWildcard && !path.includes(':')) {
162
+ return false;
163
+ }
164
+
165
+ if (isWildcard || this.matchPathSegments(path, url)) {
166
+ if (route.paramNames.length > 0) {
167
+ const regexPattern = this.buildRegexPattern(path, route.paramNames);
168
+ const regex = new RegExp(`^${regexPattern}$`);
169
+ const regexMatches = regex.exec(url);
170
+ if (regexMatches) {
171
+ route.paramValues = regexMatches.slice(1);
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+
177
+ return false;
178
+ }) || routes['/*'] || null;
179
+ }
180
+
181
+ matchPathSegments(path, url) {
182
+ const pathSegments = path.split('/');
183
+ const urlSegments = url.split('/');
184
+
185
+ if (pathSegments.length !== urlSegments.length) {
186
+ return false;
187
+ }
188
+
189
+ for (let i = 0; i < pathSegments.length; i++) {
190
+ const pathSegment = pathSegments[i];
191
+ const urlSegment = urlSegments[i];
192
+
193
+ if (pathSegment.startsWith(':')) {
194
+ continue;
195
+ }
196
+
197
+ if (pathSegment !== urlSegment) {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ return true;
203
+ }
204
+
205
+ buildRegexPattern(path, paramNames) {
206
+ let regexPattern = path.replace(/:[^/]+/g, '([^/]+)');
207
+ regexPattern = regexPattern.replace(/\//g, '\\/');
208
+
209
+ return regexPattern;
210
+ }
211
+
212
+ extractParamNames(path) {
213
+ const paramRegex = /:(\w+)/g;
214
+ const paramMatches = path.match(paramRegex);
215
+
216
+ return paramMatches ? paramMatches.map(match => match.slice(1)) : [];
217
+ }
218
+
219
+ extractParams(route, url) {
220
+ const { paramNames, paramValues } = route;
221
+ const params = {};
222
+
223
+ for (let i = 0; i < paramNames.length; i++) {
224
+ params[paramNames[i]] = paramValues[i];
225
+ }
226
+
227
+ return params;
228
+ }
229
+ }
@@ -1,7 +1,7 @@
1
1
  import resolvePortNumberFor from '../utils/resolve-port-number-for.js';
2
2
 
3
3
  // NOTE: there was a race condition between socket.connection and server.listen, check if nanoexpress fixes it
4
- export default async function bindServerToPort(config, server) {
4
+ export default async function bindServerToPort(server, config) {
5
5
  try {
6
6
  let port = await resolvePortNumberFor(config.port);
7
7
 
@@ -1,5 +1,3 @@
1
- import fs from 'node:fs/promises';
2
- import kleur from 'kleur';
3
1
  import Puppeteer from 'puppeteer';
4
2
  import setupWebServer from './web-server.js';
5
3
  import bindServerToPort from './bind-server-to-port.js';
@@ -16,9 +14,9 @@ export default async function setupBrowser(config = {
16
14
  headless: 'new',
17
15
  }),
18
16
  ]);
19
- let [page, ..._] = await Promise.all([
17
+ let [page, _] = await Promise.all([
20
18
  browser.newPage(),
21
- bindServerToPort(config, server)
19
+ bindServerToPort(server, config)
22
20
  ]);
23
21
 
24
22
  page.on('console', async (msg) => {
@@ -1,37 +1,21 @@
1
- import fs from 'node:fs/promises';
1
+ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import kleur from 'kleur';
4
- import express from 'nanoexpress';
5
- import staticServe from '@nanoexpress/middleware-static-serve';
6
3
  import findInternalAssetsFromHTML from '../utils/find-internal-assets-from-html.js';
7
4
  import TAPDisplayTestResult from '../tap/display-test-result.js';
5
+ import pathExists from '../utils/path-exists.js';
6
+ import HTTPServer, { MIME_TYPES } from '../servers/http.js';
7
+
8
+ const fsPromise = fs.promises;
8
9
 
9
10
  export default async function setupWebServer(config = {
10
11
  port: 1234, debug: false, watch: false, timeout: 10000
11
12
  }, cachedContent) {
12
- let ServerConsole = ['log', 'error', 'done', 'warn', 'info'].reduce((result, type) => {
13
- return Object.assign(result, {
14
- [type]: (...messages) => {
15
- console.log(`# HTTPServer[${type}] :`, ...messages);
16
- }
17
- });
18
- }, {});
19
- ServerConsole.debug = (...messages) => {
20
- if (config.debug) {
21
- console.log('#', kleur.blue(`HTTPServer`), ...messages);
22
- }
23
- };
13
+ let STATIC_FILES_PATH = path.join(config.projectRoot, config.output);
14
+ let server = new HTTPServer();
24
15
 
25
- let app = express({ console: ServerConsole });
26
- let Decoder = new TextDecoder("utf-8");
27
-
28
- app.ws('/', {
29
- open(ws) {
30
- ws.subscribe('refresh');
31
- ws.subscribe('abort');
32
- },
33
- message(ws, message) {
34
- const { event, details, abort } = JSON.parse(Decoder.decode(message));
16
+ server.wss.on('connection', function connection(socket) {
17
+ socket.on('message', function message(data) {
18
+ const { event, details, abort } = JSON.parse(data);
35
19
 
36
20
  if (event === "connection") {
37
21
  console.log('TAP version 13');
@@ -42,67 +26,75 @@ export default async function setupWebServer(config = {
42
26
 
43
27
  TAPDisplayTestResult(config.COUNTER, details)
44
28
  }
45
- }
29
+ });
46
30
  });
47
31
 
48
- app.get('/', async (req, res) => {
49
- let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(app.config.port, config);
32
+ server.get('/', async (req, res) => {
33
+ let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
50
34
  let htmlContent = escapeAndInjectTestsToHTML(
51
35
  replaceAssetPaths(cachedContent.mainHTML.html, cachedContent.mainHTML.filePath, config.projectRoot),
52
36
  TEST_RUNTIME_TO_INJECT,
53
37
  cachedContent.allTestCode
54
38
  );
55
39
 
56
- res.send(htmlContent);
40
+ res.write(htmlContent);
41
+ res.end();
57
42
 
58
- return await fs.writeFile(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
43
+ return await fsPromise.writeFile(`${config.projectRoot}/${config.output}/index.html`, htmlContent);
59
44
  });
60
45
 
61
- app.get('/qunitx.html', async (req, res) => {
62
- let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(app.config.port, config);
46
+ server.get('/qunitx.html', async (req, res) => {
47
+ let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
63
48
  let htmlContent = escapeAndInjectTestsToHTML(
64
49
  replaceAssetPaths(cachedContent.mainHTML.html, cachedContent.mainHTML.filePath, config.projectRoot),
65
50
  TEST_RUNTIME_TO_INJECT,
66
51
  cachedContent.filteredTestCode
67
52
  );
68
53
 
69
- res.send(htmlContent);
54
+ res.write(htmlContent);
55
+ res.end();
70
56
 
71
- return await fs.writeFile(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
57
+ return await fsPromise.writeFile(`${config.projectRoot}/${config.output}/qunitx.html`, htmlContent);
72
58
  });
73
59
 
74
- app.use(staticServe(path.join(config.projectRoot, config.output), {
75
- mode: 'live',
76
- lastModified: true,
77
- index: false
78
- }));
79
-
80
- app.get('/*', async (req, res) => {
81
- if (config.debug) {
82
- console.log(`# [HTTPServer] ${req.method} ${req.path}`);
83
- }
84
-
85
- let dynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
86
- if (dynamicHTML) {
87
- let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(app.config.port, config);
60
+ server.get('/*', async (req, res) => {
61
+ let possibleDynamicHTML = cachedContent.dynamicContentHTMLs[`${config.projectRoot}${req.path}`];
62
+ if (possibleDynamicHTML) {
63
+ let TEST_RUNTIME_TO_INJECT = testRuntimeToInject(config.port, config);
88
64
  let htmlContent = escapeAndInjectTestsToHTML(
89
- dynamicHTML,
65
+ possibleDynamicHTML,
90
66
  TEST_RUNTIME_TO_INJECT,
91
67
  cachedContent.allTestCode
92
68
  );
93
69
 
94
- res.send(htmlContent);
70
+ res.write(htmlContent);
71
+ res.end();
72
+
73
+ return await fsPromise.writeFile(`${config.projectRoot}/${config.output}${req.path}`, htmlContent);
74
+ }
75
+
76
+ let url = req.url;
77
+ let requestStartedAt = new Date();
78
+ let filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join('');
79
+ let statusCode = await pathExists(filePath) ? 200 : 404;
80
+
81
+ res.writeHead(statusCode, {
82
+ "Content-Type": req.headers.accept?.includes('text/html')
83
+ ? MIME_TYPES.html
84
+ : MIME_TYPES[path.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html
85
+ });
95
86
 
96
- return await fs.writeFile(`${config.projectRoot}/${config.output}${req.path}`, htmlContent);
87
+ if (statusCode === 404) {
88
+ res.end();
89
+ } else {
90
+ fs.createReadStream(filePath)
91
+ .pipe(res);
97
92
  }
98
93
 
99
- return res.send({
100
- code: 404,
101
- message: 'Not found'
102
- })
94
+ console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${new Date() - requestStartedAt}ms`);
103
95
  });
104
96
 
105
- return app;
97
+ return server;
106
98
  }
107
99
 
108
100
  function replaceAssetPaths(html, htmlPath, projectRoot) {
@@ -1,5 +1,3 @@
1
- import fs from 'node:fs/promises';
2
-
3
1
  // { inputs: [], browser: true, debug: true, watch: true, failFast: true, htmlPaths: [], output }
4
2
  export default async function(projectRoot) {
5
3
  const providedFlags = process.argv.slice(2).reduce((result, arg) => {
@@ -10,7 +10,7 @@ export default async function runUserModule(modulePath, params, scriptPosition)
10
10
  }
11
11
  } catch (error) {
12
12
  console.log('#', kleur.red(`QUnitX ${scriptPosition} script failed:`));
13
- console.log(error);
13
+ console.trace(error);
14
14
  console.error(error);
15
15
 
16
16
  return process.exit(1);
package/lol.ts ADDED
@@ -0,0 +1,18 @@
1
+ Deno.serve((req) => {
2
+ let timer: number;
3
+ const body = new ReadableStream({
4
+ async start(controller) {
5
+ timer = setInterval(() => {
6
+ controller.enqueue("Hello, World!\n");
7
+ }, 1000);
8
+ },
9
+ cancel() {
10
+ clearInterval(timer);
11
+ },
12
+ });
13
+ return new Response(body.pipeThrough(new TextEncoderStream()), {
14
+ headers: {
15
+ "content-type": "text/plain; charset=utf-8",
16
+ },
17
+ });
18
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx",
3
3
  "type": "module",
4
- "version": "0.5.3",
4
+ "version": "0.6.0",
5
5
  "description": "Experimental improvements, suggestions for qunit CLI",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -52,18 +52,17 @@
52
52
  "test:sanity-second": "./cli.js test/helpers/passing-tests.js test/helpers/passing-tests.ts"
53
53
  },
54
54
  "dependencies": {
55
- "@nanoexpress/middleware-static-serve": "^1.0.4",
56
55
  "cheerio": "^1.0.0-rc.10",
57
56
  "chokidar": "^3.5.3",
58
57
  "esbuild": "^0.17.19",
59
58
  "js-yaml": "^4.1.0",
60
59
  "jsdom": "^22.0.0",
61
60
  "kleur": "^4.1.5",
62
- "nanoexpress": "^6.2.1",
63
61
  "picomatch": "^2.3.1",
64
62
  "puppeteer": "20.2.0",
65
63
  "recursive-lookup": "1.1.0",
66
- "ts-node": "^10.7.0"
64
+ "ts-node": "^10.7.0",
65
+ "ws": "^8.13.0"
67
66
  },
68
67
  "devDependencies": {
69
68
  "auto-changelog": "^2.4.0",