test-inventory 0.7.0 → 0.9.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +16 -10
  3. package/dist/case-titles.d.ts +22 -0
  4. package/dist/case-titles.js +184 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +5 -5
  7. package/dist/config-dependencies.js +27 -3
  8. package/dist/config-runs.d.ts +117 -3
  9. package/dist/config-runs.js +1015 -19
  10. package/dist/config.d.ts +42 -1
  11. package/dist/config.js +287 -21
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +16 -1
  15. package/dist/extract.d.ts +2 -0
  16. package/dist/extract.js +89 -29
  17. package/dist/files.js +4 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +207 -7
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/bun.d.ts +42 -0
  24. package/dist/frameworks/bun.js +272 -0
  25. package/dist/frameworks/common.d.ts +3 -1
  26. package/dist/frameworks/common.js +18 -9
  27. package/dist/frameworks/cypress.d.ts +31 -0
  28. package/dist/frameworks/cypress.js +70 -0
  29. package/dist/frameworks/jasmine.d.ts +28 -0
  30. package/dist/frameworks/jasmine.js +218 -0
  31. package/dist/frameworks/jest.d.ts +46 -0
  32. package/dist/frameworks/jest.js +442 -0
  33. package/dist/frameworks/mocha.d.ts +50 -0
  34. package/dist/frameworks/mocha.js +306 -0
  35. package/dist/frameworks/node.d.ts +30 -0
  36. package/dist/frameworks/node.js +268 -0
  37. package/dist/frameworks/playwright.d.ts +13 -0
  38. package/dist/frameworks/playwright.js +57 -0
  39. package/dist/frameworks/test-functions.d.ts +194 -0
  40. package/dist/frameworks/test-functions.js +534 -0
  41. package/dist/frameworks/vitest.d.ts +15 -62
  42. package/dist/frameworks/vitest.js +69 -389
  43. package/dist/frameworks/webdriverio.d.ts +27 -0
  44. package/dist/frameworks/webdriverio.js +37 -0
  45. package/dist/imported-values.js +15 -1
  46. package/dist/model.d.ts +9 -2
  47. package/dist/node-scripts.d.ts +6 -0
  48. package/dist/node-scripts.js +85 -0
  49. package/dist/options.d.ts +16 -3
  50. package/dist/options.js +22 -3
  51. package/dist/parse.js +17 -2
  52. package/dist/records.d.ts +8 -2
  53. package/dist/records.js +94 -22
  54. package/dist/resolve.d.ts +21 -4
  55. package/dist/resolve.js +91 -52
  56. package/dist/scan-file.d.ts +8 -0
  57. package/dist/scan-file.js +21 -3
  58. package/dist/scan.js +62 -12
  59. package/dist/toml.d.ts +13 -0
  60. package/dist/toml.js +172 -0
  61. package/dist/types.d.ts +27 -11
  62. package/dist/values.d.ts +15 -0
  63. package/dist/values.js +120 -2
  64. package/dist/yaml.d.ts +12 -0
  65. package/dist/yaml.js +148 -0
  66. package/package.json +7 -6
  67. package/schema/v1.json +30 -8
package/dist/resolve.js CHANGED
@@ -272,39 +272,35 @@ function pnpmWorkspacePatterns(text) {
272
272
  }
273
273
  return patterns;
274
274
  }
275
- /** The folder of each package in the workspace a folder belongs to, by name; empty outside a workspace. */
276
- function workspacePackages(directory) {
277
- for (let folder = directory;; folder = path.dirname(folder)) {
278
- const workspaces = readJson(path.join(folder, 'package.json'))?.workspaces;
279
- const fromManifest = Array.isArray(workspaces) ? workspaces : record(workspaces)?.packages;
280
- const pnpm = readText(path.join(folder, 'pnpm-workspace.yaml'));
281
- // pnpm reads only pnpm-workspace.yaml, and npm, Yarn and Bun only package.json, so a repository that moved to
282
- // pnpm may still have an old list there. Packages from both are found.
283
- const lists = [Array.isArray(fromManifest) ? fromManifest : null, pnpm ? pnpmWorkspacePatterns(pnpm) : null];
284
- const patterns = lists.some((list) => list !== null)
285
- ? lists.flat().filter((item) => typeof item === 'string')
286
- : null;
287
- if (patterns) {
288
- // Packages are inside the workspace. A pattern or a link that leads out of its folder could send the search through
289
- // the whole disk, so neither is followed.
290
- const inside = patterns.filter((item) => !path.isAbsolute(item.replace(/^!/, '')) && !/(^|\/)\.\.(\/|$)/.test(item));
291
- const manifests = globSync(inside.filter((item) => !item.startsWith('!')).map((item) => `${item.replace(/\/+$/, '')}/package.json`), {
292
- cwd: folder,
293
- absolute: true,
294
- followSymbolicLinks: false,
295
- ignore: ['**/node_modules/**', ...inside.filter((item) => item.startsWith('!')).map((item) => item.slice(1))],
296
- });
297
- const packages = new Map();
298
- for (const manifest of manifests) {
299
- const name = readJson(manifest)?.name;
300
- if (typeof name === 'string')
301
- packages.set(name, path.dirname(manifest));
302
- }
303
- return packages;
304
- }
305
- if (path.dirname(folder) === folder)
306
- return new Map();
275
+ /** The folder of each package of the workspace a folder is the root of, by name; null when it isn't one. */
276
+ function workspaceAt(folder) {
277
+ const workspaces = readJson(path.join(folder, 'package.json'))?.workspaces;
278
+ const fromManifest = Array.isArray(workspaces) ? workspaces : record(workspaces)?.packages;
279
+ const pnpm = readText(path.join(folder, 'pnpm-workspace.yaml'));
280
+ // pnpm reads only pnpm-workspace.yaml, and npm, Yarn and Bun only package.json, so a repository that moved to
281
+ // pnpm may still have an old list there. Packages from both are found.
282
+ const lists = [Array.isArray(fromManifest) ? fromManifest : null, pnpm ? pnpmWorkspacePatterns(pnpm) : null];
283
+ const patterns = lists.some((list) => list !== null)
284
+ ? lists.flat().filter((item) => typeof item === 'string')
285
+ : null;
286
+ if (!patterns)
287
+ return null;
288
+ // Packages are inside the workspace. A pattern or a link that leads out of its folder could send the search through
289
+ // the whole disk, so neither is followed.
290
+ const inside = patterns.filter((item) => !path.isAbsolute(item.replace(/^!/, '')) && !/(^|\/)\.\.(\/|$)/.test(item));
291
+ const manifests = globSync(inside.filter((item) => !item.startsWith('!')).map((item) => `${item.replace(/\/+$/, '')}/package.json`), {
292
+ cwd: folder,
293
+ absolute: true,
294
+ followSymbolicLinks: false,
295
+ ignore: ['**/node_modules/**', ...inside.filter((item) => item.startsWith('!')).map((item) => item.slice(1))],
296
+ });
297
+ const packages = new Map();
298
+ for (const manifest of manifests) {
299
+ const name = readJson(manifest)?.name;
300
+ if (typeof name === 'string')
301
+ packages.set(name, path.dirname(manifest));
307
302
  }
303
+ return packages;
308
304
  }
309
305
  /**
310
306
  * A workspace package followed to its TypeScript source: through its link in node_modules, or, before an install, from
@@ -327,13 +323,34 @@ function workspaceSource(specifier, directory, packagesOf) {
327
323
  const local = packagesOf(directory).get(name);
328
324
  return local ? packageEntry(local, subpath) : null;
329
325
  }
326
+ /** A file a bare name stands for in the folders an import map adds; null when none has it. */
327
+ function searched(specifier, directory, map) {
328
+ const folders = [...map.paths];
329
+ for (let folder = directory;; folder = path.dirname(folder)) {
330
+ folders.push(...map.directories.map((name) => path.join(folder, name)));
331
+ if (path.dirname(folder) === folder)
332
+ break;
333
+ }
334
+ return folders.map((folder) => findFile(path.join(folder, specifier))).find((file) => file !== null) ?? null;
335
+ }
336
+ /** The names an alias maps a specifier to; null when it doesn't match. */
337
+ function mapped(specifier, alias) {
338
+ const { find, replacements } = alias;
339
+ if (typeof find === 'string') {
340
+ if (specifier !== find && !specifier.startsWith(`${find}/`))
341
+ return null;
342
+ return replacements.map((replacement) => replacement + specifier.slice(find.length));
343
+ }
344
+ return find.test(specifier) ? replacements.map((replacement) => specifier.replace(find, replacement)) : null;
345
+ }
330
346
  /**
331
- * Returns a reader for the modules a test file imports. It follows relative paths, tsconfig `paths` and `baseUrl` from
332
- * the tsconfig nearest the importing file, package.json `imports` (`#name`), and workspace packages whose entry is
333
- * TypeScript source. Each module it reads carries a reader for the modules that module imports in turn.
347
+ * Returns a reader for the modules a test file imports. It follows the import map of the config the file runs with,
348
+ * relative paths, tsconfig `paths` and `baseUrl` from the tsconfig nearest the importing file, package.json `imports`
349
+ * (`#name`), and workspace packages whose entry is TypeScript source. Each module it reads carries a reader for the
350
+ * modules that module imports in turn, with the same import map.
334
351
  */
335
352
  export function moduleResolver(root) {
336
- // Many test files import the same fixtures file and share a tsconfig; each is read once.
353
+ // Many test files import the same fixtures file and share a tsconfig; each is read once per import map.
337
354
  const modules = new Map();
338
355
  const tsconfigs = new Map();
339
356
  const pathOptions = new Map();
@@ -351,19 +368,26 @@ export function moduleResolver(root) {
351
368
  }
352
369
  return options;
353
370
  };
371
+ // The packages of the workspace a folder belongs to, empty outside one; each folder on the way up is looked at once.
354
372
  const packagesOf = (directory) => {
355
- let packages = workspaces.get(directory);
356
- if (!packages) {
357
- packages = workspacePackages(directory);
358
- workspaces.set(directory, packages);
373
+ const passed = [];
374
+ let packages;
375
+ for (let folder = directory; !packages; folder = path.dirname(folder)) {
376
+ packages = workspaces.get(folder);
377
+ if (packages)
378
+ break;
379
+ passed.push(folder);
380
+ packages = workspaceAt(folder) ?? (path.dirname(folder) === folder ? new Map() : null);
359
381
  }
382
+ for (const folder of passed)
383
+ workspaces.set(folder, packages);
360
384
  return packages;
361
385
  };
362
- const reader = (fromFile) => (specifier) => {
363
- const directory = path.dirname(fromFile);
386
+ /** The file a name stands for from a folder, or why it can't be followed. */
387
+ const locate = (specifier, directory, map) => {
364
388
  let candidates;
365
389
  // `.` and `..` are paths too: the index of the file's folder, or of the folder above.
366
- if (/^\.\.?(?:\/|$)/.test(specifier)) {
390
+ if (/^\.\.?(?:\/|$)/.test(specifier) || path.isAbsolute(specifier)) {
367
391
  candidates = [path.resolve(directory, specifier)];
368
392
  }
369
393
  else if (specifier.startsWith('#')) {
@@ -385,29 +409,44 @@ export function moduleResolver(root) {
385
409
  const workspace = fromBaseUrl ? null : workspaceSource(specifier, directory, packagesOf);
386
410
  if (workspace && 'reason' in workspace)
387
411
  return workspace;
388
- const file = fromBaseUrl ?? workspace?.file;
412
+ const file = fromBaseUrl ?? workspace?.file ?? (map && searched(specifier, directory, map));
389
413
  // No package name starts with `@/` or `~/`, so these are aliases, as Vite's `resolve.alias` sets them; any
390
414
  // other name with no file may still be a package or a built-in module, as TypeScript takes it to be.
391
415
  if (!file && /^[@~]\//.test(specifier)) {
392
- return { reason: targets ? 'could not be read' : 'is a path alias that no tsconfig paths define' };
416
+ return { reason: targets ? 'could not be read' : 'is a path alias that no tsconfig paths or config define' };
393
417
  }
394
418
  if (!file)
395
419
  return { reason: 'is not a local file, a path alias or a workspace package' };
396
420
  candidates = [file];
397
421
  }
398
422
  }
399
- const found = candidates.map(findFile).find((file) => file !== null);
400
- if (!found)
401
- return { reason: 'could not be read' };
402
- if (!modules.has(found)) {
423
+ return candidates.map(findFile).find((file) => file !== null) ?? { reason: 'could not be read' };
424
+ };
425
+ const reader = (fromFile, map = null) => (specifier) => {
426
+ const directory = path.dirname(fromFile);
427
+ const aliased = map?.aliases.map((alias) => mapped(specifier, alias)).find((names) => names !== null);
428
+ // The config maps the name to others, each a path or a name looked up as written, the first that is found.
429
+ const located = (aliased ?? [specifier]).map((name) => locate(name, directory, map));
430
+ const found = located.find((item) => typeof item === 'string');
431
+ if (found === undefined) {
432
+ return aliased
433
+ ? { reason: `is mapped by the config to ${aliased.join(' or ')}, which could not be read` }
434
+ : located[0];
435
+ }
436
+ let known = modules.get(map);
437
+ if (!known) {
438
+ known = new Map();
439
+ modules.set(map, known);
440
+ }
441
+ if (!known.has(found)) {
403
442
  const content = readContent(found);
404
- modules.set(found, content && {
443
+ known.set(found, content && {
405
444
  ...content,
406
445
  relativeFilePath: path.relative(root, found).replaceAll('\\', '/'),
407
- readModule: reader(found),
446
+ readModule: reader(found, map),
408
447
  });
409
448
  }
410
- return modules.get(found) ?? { reason: 'could not be read' };
449
+ return known.get(found) ?? { reason: 'could not be read' };
411
450
  };
412
451
  return reader;
413
452
  }
@@ -1,6 +1,8 @@
1
1
  import type { RunsWith } from './config-dependencies.ts';
2
2
  import { DependencyGraph, type SnapshotFiles } from './dependency-hash.ts';
3
3
  import { ExportedValues } from './imported-values.ts';
4
+ import { type MochaInterface } from './frameworks/mocha.ts';
5
+ import { type WebdriverIOFramework } from './frameworks/webdriverio.ts';
4
6
  import type { ModuleReader } from './model.ts';
5
7
  import { type CommentsMode, type FileConfig, type FileRecords } from './records.ts';
6
8
  import type { Framework } from './types.ts';
@@ -18,6 +20,12 @@ export interface FileSettings {
18
20
  isBenchmarkFile?: boolean | null;
19
21
  /** What the config the file uses sets for its tests; null without a config. */
20
22
  config?: FileConfig | null;
23
+ /** The interface Mocha's config sets for the tests: `describe` and `it`, or `suite` and `test`. */
24
+ mochaInterface?: MochaInterface;
25
+ /** The length Vitest's config shortens a value in a case title to; Vitest's default, 40, when unset. */
26
+ titleTruncate?: number;
27
+ /** The framework a WebdriverIO config runs the specs with. */
28
+ webdriverioFramework?: WebdriverIOFramework;
21
29
  /** Shared by the files of one scan, so that code many tests reach is read once. */
22
30
  graph?: DependencyGraph;
23
31
  /** Shared by the files of one scan, so that each module's exported values are worked out once. */
package/dist/scan-file.js CHANGED
@@ -6,19 +6,37 @@ import { ExportedValues } from "./imported-values.js";
6
6
  import { atLeast, problem } from "./frameworks/common.js";
7
7
  import { PlaywrightAdapter } from "./frameworks/playwright.js";
8
8
  import { VitestAdapter } from "./frameworks/vitest.js";
9
+ import { JestAdapter } from "./frameworks/jest.js";
10
+ import { MochaAdapter } from "./frameworks/mocha.js";
11
+ import { BunAdapter } from "./frameworks/bun.js";
12
+ import { CypressAdapter } from "./frameworks/cypress.js";
13
+ import { NodeAdapter } from "./frameworks/node.js";
14
+ import { WebdriverIOJasmineAdapter, WebdriverIOMochaAdapter, } from "./frameworks/webdriverio.js";
15
+ import { JasmineAdapter } from "./frameworks/jasmine.js";
9
16
  import { parseFile } from "./parse.js";
10
17
  import { buildRecords, fileRecord } from "./records.js";
11
18
  import { snippet } from "./source.js";
12
19
  import { Values } from "./values.js";
13
20
  const TYPE_TEST = /\.(test|spec)-d\.[cm]?[jt]sx?$/;
14
21
  const BENCHMARK = /\.(bench|benchmark)\.[cm]?[jt]sx?$/;
22
+ const ADAPTERS = {
23
+ playwright: (parsed, file, readModule) => new PlaywrightAdapter(parsed.program, file, readModule),
24
+ vitest: (parsed, file, readModule, settings) => new VitestAdapter(parsed, file, readModule, settings.titleTruncate),
25
+ jest: (parsed, file, readModule) => new JestAdapter(parsed, file, readModule),
26
+ mocha: (parsed, file, readModule, settings) => new MochaAdapter(parsed, file, readModule, settings.mochaInterface),
27
+ jasmine: (parsed, file, readModule) => new JasmineAdapter(parsed, file, readModule),
28
+ cypress: (parsed, file, readModule) => new CypressAdapter(parsed, file, readModule),
29
+ webdriverio: (parsed, file, readModule, settings) => settings.webdriverioFramework === 'jasmine'
30
+ ? new WebdriverIOJasmineAdapter(parsed, file, readModule)
31
+ : new WebdriverIOMochaAdapter(parsed, file, readModule, settings.mochaInterface),
32
+ node: (parsed, file, readModule) => new NodeAdapter(parsed, file, readModule),
33
+ bun: (parsed, file, readModule) => new BunAdapter(parsed, file, readModule),
34
+ };
15
35
  /** A file read for the scan: the scanned file, or a module a helper function is imported from. */
16
36
  function frame(parsed, relativeFilePath, settings, readModule, exports) {
17
37
  const values = new Values(exports.reader(readModule));
18
38
  const file = { source: parsed.source, values, version: settings.version };
19
- const adapter = settings.framework === 'playwright'
20
- ? new PlaywrightAdapter(parsed.program, file, readModule)
21
- : new VitestAdapter(parsed, file, readModule);
39
+ const adapter = ADAPTERS[settings.framework](parsed, file, readModule, settings);
22
40
  return { parsed, adapter, values: file.values, readModule, relativeFilePath };
23
41
  }
24
42
  /** The records of a file the scan read no tests from. */
package/dist/scan.js CHANGED
@@ -3,32 +3,38 @@ import { readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { glob } from 'tinyglobby';
5
5
  import { ConfigDependencies } from "./config-dependencies.js";
6
- import { ConfigRuns, fileConfig } from "./config-runs.js";
7
- import { Configs } from "./config.js";
6
+ import { ConfigRuns, fileConfig, importMap, mochaInterface, webdriverioFramework, webdriverioInterface, } from "./config-runs.js";
7
+ import { Configs, settingAt } from "./config.js";
8
+ import { atLeast } from "./frameworks/common.js";
8
9
  import { DependencyGraph } from "./dependency-hash.js";
9
10
  import { ExportedValues } from "./imported-values.js";
10
11
  import { fileDiagnostic } from "./diagnostics.js";
11
12
  import { findFiles, normalizePattern, readFailure } from "./files.js";
12
- import { detectFrameworkVersion, OLDEST } from "./framework-version.js";
13
+ import { detectFramework, detectFrameworkVersion, FRAMEWORK_NAMES, OLDEST } from "./framework-version.js";
13
14
  import { createInventory } from "./inventory.js";
14
- import { checkComments, checkFlag, checkFramework, checkObject, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
15
+ import { checkComments, checkFlag, checkFramework, checkObject, checkSignal, checkStrings, checkVersion, invalid, majorMinor, toPosixPath, } from "./options.js";
15
16
  import { moduleResolver } from "./resolve.js";
16
17
  import { noRecords, scanFile } from "./scan-file.js";
17
18
  import { globalsCheck } from "./vitest-config.js";
18
19
  function versionDiagnostics(framework, version, source, range) {
19
- const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
20
+ const name = FRAMEWORK_NAMES[framework];
20
21
  if (version === null) {
21
22
  return [
22
23
  fileDiagnostic('unknown-framework-version', source, `The ${name} version could not be read from the project, so the rules of the newest supported version apply.`),
23
24
  ];
24
25
  }
25
26
  const [major, minor] = majorMinor(version);
26
- const [oldestMajor, oldestMinor] = OLDEST[framework];
27
+ const patch = Number(/^v?\d+\.\d+\.(\d+)/.exec(version)?.[1] ?? 0);
28
+ const [oldestMajor, oldestMinor, oldestPatch = 0] = OLDEST[framework];
29
+ const oldest = OLDEST[framework].join('.');
27
30
  // A range gets the newest rules of its major, so only its major has to be supported.
28
- const tooOld = major < oldestMajor || (major === oldestMajor && minor < oldestMinor && range === null);
31
+ const tooOld = major < oldestMajor ||
32
+ (major === oldestMajor &&
33
+ range === null &&
34
+ (minor < oldestMinor || (minor === oldestMinor && patch < oldestPatch)));
29
35
  if (tooOld) {
30
36
  return [
31
- fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${oldestMajor}.${oldestMinor}, the oldest supported version; the results follow ${oldestMajor}.${oldestMinor}.`),
37
+ fileDiagnostic('unsupported-framework-version', source, `${name} ${version} is older than ${oldest}, the oldest supported version; the results follow ${oldest}.`),
32
38
  ];
33
39
  }
34
40
  if (range === null)
@@ -37,6 +43,29 @@ function versionDiagnostics(framework, version, source, range) {
37
43
  fileDiagnostic('approximate-framework-version', source, `Only the range "${range}" was found for ${name}, so the rules of the newest ${major}.x release apply.`),
38
44
  ];
39
45
  }
46
+ /**
47
+ * The length Vitest shortens a value in a case title to: `test.taskTitleValueFormatTruncate` from Vitest 5,
48
+ * `test.chaiConfig.truncateThreshold` before, from the project that runs the file; 40, Vitest's default, when it sets
49
+ * none or the scan can't read it.
50
+ */
51
+ function titleTruncate(placement, version) {
52
+ const key = atLeast(version, 5, 0)
53
+ ? ['test', 'taskTitleValueFormatTruncate']
54
+ : ['test', 'chaiConfig', 'truncateThreshold'];
55
+ const setting = placement.settings && settingAt(placement.settings, key);
56
+ return setting?.kind === 'value' && typeof setting.value === 'number' ? setting.value : 40;
57
+ }
58
+ /** Why Jest gives a file that uses its globals none: a project that runs it sets `injectGlobals` to false. */
59
+ function jestGlobals(placement) {
60
+ const off = placement.projects?.find(({ settings }) => {
61
+ const setting = settingAt(settings, ['injectGlobals']);
62
+ return setting?.kind === 'value' && setting.value === false;
63
+ });
64
+ if (!off)
65
+ return null;
66
+ const config = off.config.record.relativeFilePath;
67
+ return `The file uses describe, it or test without importing them, but ${config} sets injectGlobals to false, so Jest doesn't define them; import them from '@jest/globals'.`;
68
+ }
40
69
  /** For a pattern that names a folder, which matches no file, a pattern that matches the test files in it. */
41
70
  function folderHint(root, patterns, framework) {
42
71
  const folder = patterns.find((pattern) => {
@@ -100,7 +129,10 @@ function noFiles(root, patterns, framework, placements) {
100
129
  if ([...placements.values()].some((placement) => placement.config)) {
101
130
  return 'No test files were found from the configs in the root.';
102
131
  }
103
- const name = framework === 'playwright' ? 'Playwright' : 'Vitest';
132
+ if (framework === 'node') {
133
+ return "No patterns were given, and no file in the root matches what node --test runs: Node.js's default patterns, or the files a package.json script gives it.";
134
+ }
135
+ const name = FRAMEWORK_NAMES[framework];
104
136
  return `No patterns were given, and no ${name} config is in the root or a folder below it, so there are no test files to find.`;
105
137
  }
106
138
  /**
@@ -112,19 +144,24 @@ function noFiles(root, patterns, framework, placements) {
112
144
  export async function scan(options) {
113
145
  const checked = checkObject(options, 'options');
114
146
  const patterns = checked.patterns === undefined ? null : checkStrings(checked.patterns, 'options.patterns', true);
115
- const framework = checkFramework(checked.framework, 'options.framework');
116
147
  const ignore = checkStrings(checked.ignore, 'options.ignore', false);
117
148
  const comments = checkComments(checked.comments, 'options.comments');
118
149
  const expandCases = checkFlag(checked.expandCases, 'options.expandCases');
119
150
  const override = checkVersion(checked.frameworkVersion, 'options.frameworkVersion');
151
+ const signal = checkSignal(checked.signal, 'options.signal');
152
+ signal?.throwIfAborted();
120
153
  if (checked.root !== undefined && (typeof checked.root !== 'string' || checked.root === '')) {
121
154
  throw invalid('options.root', 'a non-empty string');
122
155
  }
123
156
  const root = path.resolve(checked.root ?? process.cwd());
124
157
  if (!existsSync(root) || !statSync(root).isDirectory())
125
158
  throw invalid('options.root', `a directory, and ${root} is not one`);
159
+ const framework = checked.framework === undefined
160
+ ? detectFramework(root, ignore)
161
+ : checkFramework(checked.framework, 'options.framework');
126
162
  // Without patterns, the configs choose among all JavaScript and TypeScript files.
127
163
  const found = await findFiles(root, patterns ?? [SCRIPT_FILES], ignore, framework);
164
+ signal?.throwIfAborted();
128
165
  const scanned = found.files.map((file) => file.relativeFilePath);
129
166
  const detected = override === null ? detectFrameworkVersion(root, framework, scanned) : null;
130
167
  const frameworkVersion = override ?? detected?.version ?? null;
@@ -173,6 +210,7 @@ export async function scan(options) {
173
210
  const results = [];
174
211
  const globalsFiles = [];
175
212
  for (const { absolutePath, relativeFilePath } of files) {
213
+ signal?.throwIfAborted();
176
214
  const placement = placements.get(absolutePath);
177
215
  if (placement.config)
178
216
  configs.use(placement.config);
@@ -198,10 +236,17 @@ export async function scan(options) {
198
236
  version: rules,
199
237
  comments,
200
238
  expandCases,
201
- readModule: readerFor(absolutePath),
239
+ readModule: readerFor(absolutePath, importMap(placement, framework)),
202
240
  root,
203
241
  isBenchmarkFile: placement.runs === null ? null : placement.runs === 'benchmark',
204
242
  config: fileConfig(placement, framework, rules, absolutePath),
243
+ mochaInterface: framework === 'mocha'
244
+ ? mochaInterface(placement, configs)
245
+ : framework === 'webdriverio'
246
+ ? webdriverioInterface(placement, configs)
247
+ : undefined,
248
+ webdriverioFramework: framework === 'webdriverio' ? webdriverioFramework(placement, configs) : undefined,
249
+ titleTruncate: framework === 'vitest' ? titleTruncate(placement, rules) : undefined,
205
250
  graph,
206
251
  exports,
207
252
  runsWith,
@@ -216,7 +261,12 @@ export async function scan(options) {
216
261
  }
217
262
  results.push({ ...result, file: { ...result.file, config } });
218
263
  }
219
- const missingGlobals = globalsCheck(root, configs, (file) => placements.get(file));
264
+ // Mocha, Jasmine, Cypress, WebdriverIO and Bun always set their globals, and Playwright and node:test have none.
265
+ const missingGlobals = framework === 'jest'
266
+ ? (file) => jestGlobals(placements.get(file))
267
+ : ['mocha', 'jasmine', 'cypress', 'webdriverio', 'node', 'bun'].includes(framework)
268
+ ? () => null
269
+ : globalsCheck(root, configs, (file) => placements.get(file));
220
270
  let globals = null;
221
271
  for (const { absolutePath, relativeFilePath } of globalsFiles) {
222
272
  const message = missingGlobals(absolutePath);
package/dist/toml.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Value } from './values.ts';
2
+ /** A line of TOML the reader doesn't take, with its line number. */
3
+ export declare class TomlError extends Error {
4
+ readonly line: number;
5
+ constructor(message: string, line: number);
6
+ }
7
+ type Table = Record<string, Value>;
8
+ /**
9
+ * The part of TOML a `bunfig.toml` uses: tables and dotted keys, strings, numbers, booleans, arrays over several lines,
10
+ * inline tables and comments. Dates and arrays of tables are refused with a `TomlError`.
11
+ */
12
+ export declare function readToml(text: string): Table;
13
+ export {};
package/dist/toml.js ADDED
@@ -0,0 +1,172 @@
1
+ /** A line of TOML the reader doesn't take, with its line number. */
2
+ export class TomlError extends Error {
3
+ line;
4
+ constructor(message, line) {
5
+ super(message);
6
+ this.line = line;
7
+ }
8
+ }
9
+ /**
10
+ * The part of TOML a `bunfig.toml` uses: tables and dotted keys, strings, numbers, booleans, arrays over several lines,
11
+ * inline tables and comments. Dates and arrays of tables are refused with a `TomlError`.
12
+ */
13
+ export function readToml(text) {
14
+ const root = {};
15
+ let table = root;
16
+ const reader = { text, at: 0, line: 1 };
17
+ for (;;) {
18
+ skipBlank(reader, true);
19
+ if (reader.at >= text.length)
20
+ return root;
21
+ if (text[reader.at] === '[') {
22
+ if (text[reader.at + 1] === '[')
23
+ throw new TomlError('Arrays of tables are not read', reader.line);
24
+ reader.at++;
25
+ const keys = readKeys(reader, ']');
26
+ reader.at++;
27
+ table = tableAt(root, keys, reader.line);
28
+ }
29
+ else {
30
+ const keys = readKeys(reader, '=');
31
+ reader.at++;
32
+ skipBlank(reader, false);
33
+ const value = readValue(reader);
34
+ const owner = tableAt(table, keys.slice(0, -1), reader.line);
35
+ owner[keys[keys.length - 1]] = value;
36
+ }
37
+ skipBlank(reader, false);
38
+ if (reader.at < text.length && text[reader.at] !== '\n') {
39
+ throw new TomlError(`Unexpected "${text[reader.at]}"`, reader.line);
40
+ }
41
+ }
42
+ }
43
+ /** Skips spaces and comments, and line breaks too when `lines` is set. */
44
+ function skipBlank(reader, lines) {
45
+ const { text } = reader;
46
+ while (reader.at < text.length) {
47
+ const char = text[reader.at];
48
+ if (char === '#') {
49
+ while (reader.at < text.length && text[reader.at] !== '\n')
50
+ reader.at++;
51
+ }
52
+ else if (char === ' ' || char === '\t' || char === '\r') {
53
+ reader.at++;
54
+ }
55
+ else if (char === '\n' && lines) {
56
+ reader.line++;
57
+ reader.at++;
58
+ }
59
+ else {
60
+ return;
61
+ }
62
+ }
63
+ }
64
+ /** A dotted key, bare or quoted, up to `end`. */
65
+ function readKeys(reader, end) {
66
+ const keys = [];
67
+ for (;;) {
68
+ skipBlank(reader, false);
69
+ const char = reader.text[reader.at];
70
+ if (char === '"' || char === "'")
71
+ keys.push(readString(reader));
72
+ else {
73
+ const bare = /^[A-Za-z0-9_-]+/.exec(reader.text.slice(reader.at));
74
+ if (!bare)
75
+ throw new TomlError('Expected a key', reader.line);
76
+ keys.push(bare[0]);
77
+ reader.at += bare[0].length;
78
+ }
79
+ skipBlank(reader, false);
80
+ if (reader.text[reader.at] === end)
81
+ return keys;
82
+ if (reader.text[reader.at] !== '.')
83
+ throw new TomlError(`Expected "${end}"`, reader.line);
84
+ reader.at++;
85
+ }
86
+ }
87
+ function tableAt(root, keys, line) {
88
+ let table = root;
89
+ for (const key of keys) {
90
+ table[key] ??= {};
91
+ const next = table[key];
92
+ if (typeof next !== 'object' || Array.isArray(next)) {
93
+ throw new TomlError(`${key} is not a table`, line);
94
+ }
95
+ table = next;
96
+ }
97
+ return table;
98
+ }
99
+ function readValue(reader) {
100
+ const { text } = reader;
101
+ const char = text[reader.at];
102
+ if (char === '"' || char === "'")
103
+ return readString(reader);
104
+ if (char === '[') {
105
+ reader.at++;
106
+ const items = [];
107
+ for (;;) {
108
+ skipBlank(reader, true);
109
+ if (text[reader.at] === ']')
110
+ break;
111
+ items.push(readValue(reader));
112
+ skipBlank(reader, true);
113
+ if (text[reader.at] === ',')
114
+ reader.at++;
115
+ else if (text[reader.at] !== ']')
116
+ throw new TomlError('Expected "," or "]"', reader.line);
117
+ }
118
+ reader.at++;
119
+ return items;
120
+ }
121
+ if (char === '{') {
122
+ reader.at++;
123
+ const table = {};
124
+ skipBlank(reader, false);
125
+ while (text[reader.at] !== '}') {
126
+ const keys = readKeys(reader, '=');
127
+ reader.at++;
128
+ skipBlank(reader, false);
129
+ tableAt(table, keys.slice(0, -1), reader.line)[keys[keys.length - 1]] = readValue(reader);
130
+ skipBlank(reader, false);
131
+ if (text[reader.at] === ',')
132
+ reader.at++;
133
+ skipBlank(reader, false);
134
+ }
135
+ reader.at++;
136
+ return table;
137
+ }
138
+ const word = /^[^\s,\]}#]+/.exec(text.slice(reader.at))?.[0] ?? '';
139
+ reader.at += word.length;
140
+ if (word === 'true' || word === 'false')
141
+ return word === 'true';
142
+ const number = word.replaceAll('_', '');
143
+ if (/^[+-]?(\d+(\.\d+)?([eE][+-]?\d+)?|inf|nan)$/.test(number)) {
144
+ return number.endsWith('inf') ? (number.startsWith('-') ? -Infinity : Infinity) : Number(number);
145
+ }
146
+ throw new TomlError(`Value ${word || text[reader.at]} is not read`, reader.line);
147
+ }
148
+ /** A basic string, with escapes, or a literal one; one line each. */
149
+ function readString(reader) {
150
+ const { text } = reader;
151
+ const quote = text[reader.at++];
152
+ let value = '';
153
+ while (text[reader.at] !== quote) {
154
+ const char = text[reader.at];
155
+ if (reader.at >= text.length || char === '\n')
156
+ throw new TomlError('The string is not closed', reader.line);
157
+ if (char === '\\' && quote === '"') {
158
+ const escape = text[reader.at + 1];
159
+ const escapes = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\' };
160
+ if (!(escape in escapes))
161
+ throw new TomlError(`Escape \\${escape} is not read`, reader.line);
162
+ value += escapes[escape];
163
+ reader.at += 2;
164
+ }
165
+ else {
166
+ value += char;
167
+ reader.at++;
168
+ }
169
+ }
170
+ reader.at++;
171
+ return value;
172
+ }