qunitx-cli 0.5.8 → 0.7.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.
package/deno.lock CHANGED
@@ -6,7 +6,6 @@
6
6
  "npm:@types/picomatch@*": "4.0.2",
7
7
  "npm:@types/ws@*": "8.18.1",
8
8
  "npm:cheerio@*": "1.2.0",
9
- "npm:cheerio@^1.2.0": "1.2.0",
10
9
  "npm:chokidar@*": "5.0.0",
11
10
  "npm:chokidar@5": "5.0.0",
12
11
  "npm:cors@^2.8.6": "2.8.6",
@@ -14,9 +13,7 @@
14
13
  "npm:esbuild@~0.27.3": "0.27.4",
15
14
  "npm:express@^5.2.1": "5.2.1",
16
15
  "npm:js-yaml@*": "4.1.1",
17
- "npm:js-yaml@^4.1.1": "4.1.1",
18
16
  "npm:kleur@*": "4.1.5",
19
- "npm:kleur@^4.1.5": "4.1.5",
20
17
  "npm:picomatch@*": "4.0.3",
21
18
  "npm:picomatch@^4.0.3": "4.0.3",
22
19
  "npm:prettier@^3.8.1": "3.8.1",
@@ -1326,13 +1323,10 @@
1326
1323
  ],
1327
1324
  "packageJson": {
1328
1325
  "dependencies": [
1329
- "npm:cheerio@^1.2.0",
1330
1326
  "npm:chokidar@5",
1331
1327
  "npm:cors@^2.8.6",
1332
1328
  "npm:esbuild@~0.27.3",
1333
1329
  "npm:express@^5.2.1",
1334
- "npm:js-yaml@^4.1.1",
1335
- "npm:kleur@^4.1.5",
1336
1330
  "npm:picomatch@^4.0.3",
1337
1331
  "npm:prettier@^3.8.1",
1338
1332
  "npm:puppeteer@^24.38.0",
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
- import kleur from 'kleur';
2
+ import { green } from '../utils/color.js';
3
3
  import findProjectRoot from '../utils/find-project-root.js';
4
4
  import pathExists from '../utils/path-exists.js';
5
5
  import readBoilerplate from '../utils/read-boilerplate.js';
@@ -29,5 +29,5 @@ export default async function generateTestFiles() {
29
29
  await fs.mkdir(targetFolderPaths.join('/'), { recursive: true });
30
30
  await fs.writeFile(path, testJSContent.replace('{{moduleName}}', moduleName));
31
31
 
32
- console.log(kleur.green(`${path} written`));
32
+ console.log(green(`${path} written`));
33
33
  }
@@ -1,8 +1,8 @@
1
- import kleur from 'kleur';
1
+ import { blue, magenta } from '../utils/color.js';
2
2
  import pkg from '../../package.json' with { type: 'json' };
3
3
 
4
- const highlight = (text) => kleur.magenta().bold(text);
5
- const color = (text) => kleur.blue(text);
4
+ const highlight = (text) => magenta().bold(text);
5
+ const color = (text) => blue(text);
6
6
 
7
7
  /** Prints qunitx-cli usage information to stdout. */
8
8
  export default function displayHelpOutput() {
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
- import kleur from 'kleur';
2
+ import { blue } from '../../utils/color.js';
3
3
  import esbuild from 'esbuild';
4
4
  import timeCounter from '../../utils/time-counter.js';
5
5
  import runUserModule from '../../utils/run-user-module.js';
@@ -141,7 +141,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
141
141
  let QUNIT_RESULT;
142
142
  let targetError;
143
143
  try {
144
- console.log('#', kleur.blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
144
+ console.log('#', blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
145
145
 
146
146
  const testsDone = new Promise((resolve) => {
147
147
  config._testRunDone = resolve;
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import { normalize } from 'node:path';
3
3
  import { availableParallelism } from 'node:os';
4
4
  import Puppeteer from 'puppeteer';
5
- import kleur from 'kleur';
5
+ import { blue, yellow } from '../utils/color.js';
6
6
  import runTestsInBrowser, { buildTestBundle } from './run/tests-in-browser.js';
7
7
  import setupBrowser from '../setup/browser.js';
8
8
  import fileWatcher from '../setup/file-watcher.js';
@@ -170,7 +170,7 @@ async function buildCachedContent(config, htmlPaths) {
170
170
  } else {
171
171
  console.log(
172
172
  '#',
173
- kleur.yellow(
173
+ yellow(
174
174
  `WARNING: Static html file with no {{content}} detected. Therefore ignoring ${filePath}`,
175
175
  ),
176
176
  );
@@ -225,11 +225,11 @@ function splitIntoGroups(files, groupCount) {
225
225
  function logWatcherAndKeyboardShortcutInfo(config, _server) {
226
226
  console.log(
227
227
  '#',
228
- kleur.blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
228
+ blue(`Watching files... You can browse the tests on http://localhost:${config.port} ...`),
229
229
  );
230
230
  console.log(
231
231
  '#',
232
- kleur.blue(
232
+ blue(
233
233
  `Shortcuts: Press "qq" to abort running tests, "qa" to run all the tests, "qf" to run last failing test, "ql" to repeat last test`,
234
234
  ),
235
235
  );
@@ -1,5 +1,5 @@
1
1
  import chokidar from 'chokidar';
2
- import kleur from 'kleur';
2
+ import { green, magenta, red, yellow } from '../utils/color.js';
3
3
 
4
4
  /**
5
5
  * Starts chokidar watchers for each lookup path and calls `onEventFunc` on JS/TS file changes, debounced via a global flag.
@@ -41,12 +41,12 @@ export function handleWatchEvent(config, extensions, event, filePath, onEventFun
41
41
 
42
42
  console.log(
43
43
  '#',
44
- kleur.magenta().bold('=================================================================='),
44
+ magenta().bold('=================================================================='),
45
45
  );
46
46
  console.log('#', getEventColor(event), filePath.split(config.projectRoot)[1]);
47
47
  console.log(
48
48
  '#',
49
- kleur.magenta().bold('=================================================================='),
49
+ magenta().bold('=================================================================='),
50
50
  );
51
51
 
52
52
  if (!config._building) {
@@ -90,10 +90,10 @@ export function mutateFSTree(fsTree, event, path) {
90
90
 
91
91
  function getEventColor(event) {
92
92
  if (event === 'change') {
93
- return kleur.yellow('CHANGED:');
93
+ return yellow('CHANGED:');
94
94
  } else if (event === 'add' || event === 'addDir') {
95
- return kleur.green('ADDED:');
95
+ return green('ADDED:');
96
96
  } else if (event === 'unlink' || event === 'unlinkDir') {
97
- return kleur.red('REMOVED:');
97
+ return red('REMOVED:');
98
98
  }
99
99
  }
@@ -1,4 +1,4 @@
1
- import kleur from 'kleur';
1
+ import { blue } from '../utils/color.js';
2
2
  import listenToKeyboardKey from '../utils/listen-to-keyboard-key.js';
3
3
  import runTestsInBrowser from '../commands/run/tests-in-browser.js';
4
4
 
@@ -16,10 +16,7 @@ export default function setupKeyboardEvents(config, cachedContent, connections)
16
16
  abortBrowserQUnit(config, connections);
17
17
 
18
18
  if (!config.lastFailedTestFiles) {
19
- console.log(
20
- '#',
21
- kleur.blue(`QUnitX: No tests failed so far, so repeating the last test run`),
22
- );
19
+ console.log('#', blue(`QUnitX: No tests failed so far, so repeating the last test run`));
23
20
  return runTestsInBrowser(config, cachedContent, connections, config.lastRanTestFiles);
24
21
  }
25
22
 
@@ -1,5 +1,4 @@
1
- // @deno-types="npm:@types/js-yaml"
2
- import yaml from 'js-yaml';
1
+ import dumpYaml from './dump-yaml.js';
3
2
  import indentString from '../utils/indent-string.js';
4
3
 
5
4
  // tape TAP output: ['operator', 'stack', 'at', 'expected', 'actual']
@@ -32,7 +31,7 @@ export default function TAPDisplayTestResult(COUNTER, details) {
32
31
  console.log(' ---');
33
32
  console.log(
34
33
  indentString(
35
- yaml.dump({
34
+ dumpYaml({
36
35
  name: `Assertion #${index + 1}`, // TODO: check what happens on runtime errors
37
36
  actual: assertion.actual
38
37
  ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer()))
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Minimal YAML serializer for TAP assertion failure blocks.
3
+ * Handles the fixed schema: { name, actual, expected, message, stack, at }.
4
+ * Values for actual/expected are pre-sanitized via JSON.parse(JSON.stringify(...)).
5
+ */
6
+
7
+ // Single compiled regex covering all cases where a plain YAML scalar would be misread:
8
+ // - YAML reserved words (null, true, false, ~, yes, no, on, off — case-insensitive)
9
+ // - Starts with a YAML indicator: { [ ! | > ' " % @ `
10
+ // - Block indicators that need a space: - ? : at start of string
11
+ // - Document separator: ---
12
+ // - Looks like a number (integer, float, hex, octal, scientific)
13
+ // - Timestamp-like strings that YAML 1.1 auto-casts to Date
14
+ // - Contains ': ' (key–value) or '#' anywhere (comment)
15
+ // - Empty string
16
+ // Also covers single-letter YAML 1.1 booleans: y/Y → true, n/N → false
17
+ const NEEDS_QUOTING =
18
+ /^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
19
+
20
+ function needsQuoting(str) {
21
+ return NEEDS_QUOTING.test(str);
22
+ }
23
+
24
+ function dumpString(str, indent) {
25
+ if (str === '') return "''";
26
+ if (str.includes('\n')) {
27
+ // Block scalar |- (strip trailing newline), each line indented by current indent + 2
28
+ return '|-\n' + str.replace(/^/gm, `${indent} `);
29
+ }
30
+ if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
31
+ return str;
32
+ }
33
+
34
+ function dumpValue(value, indent) {
35
+ if (value === null || value === undefined) return 'null';
36
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value);
37
+ if (typeof value === 'string') return dumpString(value, indent);
38
+ if (Array.isArray(value)) {
39
+ if (value.length === 0) return '[]';
40
+ const next = `${indent} `;
41
+ return '\n' + value.map((v) => `${next}- ${dumpValue(v, next)}`).join('\n');
42
+ }
43
+ // Plain object
44
+ const entries = Object.entries(value);
45
+ if (entries.length === 0) return '{}';
46
+ const next = `${indent} `;
47
+ return '\n' + entries.map(([k, v]) => `${next}${k}: ${dumpValue(v, next)}`).join('\n');
48
+ }
49
+
50
+ // Emits `key: value\n` or `key:\n ...\n` — no trailing space before block scalars.
51
+ function yamlLine(key, value) {
52
+ const v = dumpValue(value, '');
53
+ return v[0] === '\n' ? `${key}:${v}\n` : `${key}: ${v}\n`;
54
+ }
55
+
56
+ /**
57
+ * Serializes the fixed TAP assertion object to a YAML string.
58
+ * Uses a template literal (no Object.entries overhead) for the known top-level keys.
59
+ * @returns {string}
60
+ */
61
+ export default function dumpYaml({ name, actual, expected, message, stack, at }) {
62
+ return (
63
+ `name: ${dumpString(name, '')}\n` +
64
+ yamlLine('actual', actual) +
65
+ yamlLine('expected', expected) +
66
+ yamlLine('message', message) +
67
+ yamlLine('stack', stack) +
68
+ yamlLine('at', at)
69
+ );
70
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Minimal ANSI color helpers. Respects NO_COLOR, NODE_DISABLE_COLORS, FORCE_COLOR, and TTY
3
+ * detection — same logic as kleur.
4
+ *
5
+ * Use `createColors(enabled)` in tests to exercise both enabled and disabled branches directly.
6
+ */
7
+
8
+ export function createColors(enabled) {
9
+ const c = (open, close) => (text) =>
10
+ enabled ? `\x1b[${open}m${text}\x1b[${close}m` : String(text);
11
+
12
+ const red = c(31, 39);
13
+ const green = c(32, 39);
14
+ const yellow = c(33, 39);
15
+ const blue = c(34, 39);
16
+
17
+ /** `magenta(text)` — colored text. `magenta()` — chainable: `.bold(text)`. */
18
+ const magenta = (text) => {
19
+ if (text !== undefined) return enabled ? `\x1b[35m${text}\x1b[39m` : String(text);
20
+ return { bold: (t) => (enabled ? `\x1b[35m\x1b[1m${t}\x1b[22m\x1b[39m` : String(t)) };
21
+ };
22
+
23
+ return { red, green, yellow, blue, magenta };
24
+ }
25
+
26
+ const enabled =
27
+ !process.env.NODE_DISABLE_COLORS &&
28
+ process.env.NO_COLOR == null &&
29
+ process.env.TERM !== 'dumb' &&
30
+ ((process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== '0') || !!process.stdout?.isTTY);
31
+
32
+ const _c = createColors(enabled);
33
+
34
+ /** ANSI red text. */
35
+ export function red(text) {
36
+ return _c.red(text);
37
+ }
38
+ /** ANSI green text. */
39
+ export function green(text) {
40
+ return _c.green(text);
41
+ }
42
+ /** ANSI yellow text. */
43
+ export function yellow(text) {
44
+ return _c.yellow(text);
45
+ }
46
+ /** ANSI blue text. */
47
+ export function blue(text) {
48
+ return _c.blue(text);
49
+ }
50
+ /** ANSI magenta text. Call without arguments to chain: `magenta().bold(text)`. */
51
+ export function magenta(text) {
52
+ return _c.magenta(text);
53
+ }
@@ -1,22 +1,18 @@
1
- import { load } from 'cheerio';
2
-
3
- const ABSOLUTE_URL_REGEX = new RegExp('^(?:[a-z]+:)?//', 'i');
1
+ const ABSOLUTE_URL_REGEX = /^(?:[a-z]+:)?\/\//i;
2
+ const SCRIPT_SRC_REGEX = /<script[^>]+\bsrc=['"]([^'"]+)['"]/gi;
3
+ const LINK_HREF_REGEX = /<link[^>]+\bhref=['"]([^'"]+)['"]/gi;
4
4
 
5
5
  /**
6
6
  * Parses an HTML string and returns all internal (non-absolute-URL) `<script src>` and `<link href>` paths.
7
7
  * @returns {string[]}
8
8
  */
9
9
  export default function findInternalAssetsFromHTML(htmlContent) {
10
- const $ = load(htmlContent);
11
- const internalJSFiles = $('script[src]')
12
- .toArray()
13
- .map((scriptNode) => $(scriptNode).attr('src'))
10
+ const links = [...htmlContent.matchAll(LINK_HREF_REGEX)]
11
+ .map((m) => m[1])
14
12
  .filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
15
- const internalCSSFiles = $('link[href]')
16
- .toArray()
17
- .map((scriptNode) => $(scriptNode).attr('href'))
13
+ const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)]
14
+ .map((m) => m[1])
18
15
  .filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
19
16
 
20
- return internalCSSFiles.concat(internalJSFiles);
21
- // TODO: maybe needs normalization ? .map((fileReferencePath) => fileReferencePath.replace('/assets', `${projectRoot}/tmp/assets`));
17
+ return links.concat(scripts);
22
18
  }
@@ -1,4 +1,4 @@
1
- import kleur from 'kleur';
1
+ import { red } from './color.js';
2
2
 
3
3
  /**
4
4
  * Dynamically imports `modulePath` and calls its default export with `params`; exits with code 1 on error.
@@ -15,7 +15,7 @@ export default async function runUserModule(modulePath, params, scriptPosition)
15
15
  : null;
16
16
  }
17
17
  } catch (error) {
18
- console.log('#', kleur.red(`QUnitX ${scriptPosition} script failed:`));
18
+ console.log('#', red(`QUnitX ${scriptPosition} script failed:`));
19
19
  console.trace(error);
20
20
  console.error(error);
21
21
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.5.8",
4
+ "version": "0.7.0",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "main": "cli.js",
7
7
  "author": "Izel Nakri",
@@ -43,11 +43,8 @@
43
43
  "url": "https://github.com/izelnakri/qunitx-cli.git"
44
44
  },
45
45
  "dependencies": {
46
- "cheerio": "^1.2.0",
47
46
  "chokidar": "^5.0.0",
48
47
  "esbuild": "^0.27.3",
49
- "js-yaml": "^4.1.1",
50
- "kleur": "^4.1.5",
51
48
  "picomatch": "^4.0.3",
52
49
  "puppeteer": "^24.38.0",
53
50
  "ws": "^8.19.0"
package/.dockerignore DELETED
@@ -1,15 +0,0 @@
1
- .claude
2
- .env
3
- .github
4
- .gitignore
5
- benches
6
- CHANGELOG.md
7
- cliff.toml
8
- docs
9
- flake.lock
10
- flake.nix
11
- Makefile
12
- scripts
13
- test
14
- tmp
15
- TODO