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.
- package/CHANGELOG.md +180 -0
- package/README.md +16 -10
- package/dist/case-titles.d.ts +22 -0
- package/dist/case-titles.js +184 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +5 -5
- package/dist/config-dependencies.js +27 -3
- package/dist/config-runs.d.ts +117 -3
- package/dist/config-runs.js +1015 -19
- package/dist/config.d.ts +42 -1
- package/dist/config.js +287 -21
- package/dist/dependency-hash.d.ts +13 -5
- package/dist/dependency-hash.js +56 -12
- package/dist/diagnostics.js +16 -1
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +89 -29
- package/dist/files.js +4 -2
- package/dist/filter.js +1 -0
- package/dist/framework-version.d.ts +13 -2
- package/dist/framework-version.js +207 -7
- package/dist/frameworks/bindings.d.ts +14 -0
- package/dist/frameworks/bindings.js +99 -6
- package/dist/frameworks/bun.d.ts +42 -0
- package/dist/frameworks/bun.js +272 -0
- package/dist/frameworks/common.d.ts +3 -1
- package/dist/frameworks/common.js +18 -9
- package/dist/frameworks/cypress.d.ts +31 -0
- package/dist/frameworks/cypress.js +70 -0
- package/dist/frameworks/jasmine.d.ts +28 -0
- package/dist/frameworks/jasmine.js +218 -0
- package/dist/frameworks/jest.d.ts +46 -0
- package/dist/frameworks/jest.js +442 -0
- package/dist/frameworks/mocha.d.ts +50 -0
- package/dist/frameworks/mocha.js +306 -0
- package/dist/frameworks/node.d.ts +30 -0
- package/dist/frameworks/node.js +268 -0
- package/dist/frameworks/playwright.d.ts +13 -0
- package/dist/frameworks/playwright.js +57 -0
- package/dist/frameworks/test-functions.d.ts +194 -0
- package/dist/frameworks/test-functions.js +534 -0
- package/dist/frameworks/vitest.d.ts +15 -62
- package/dist/frameworks/vitest.js +69 -389
- package/dist/frameworks/webdriverio.d.ts +27 -0
- package/dist/frameworks/webdriverio.js +37 -0
- package/dist/imported-values.js +15 -1
- package/dist/model.d.ts +9 -2
- package/dist/node-scripts.d.ts +6 -0
- package/dist/node-scripts.js +85 -0
- package/dist/options.d.ts +16 -3
- package/dist/options.js +22 -3
- package/dist/parse.js +17 -2
- package/dist/records.d.ts +8 -2
- package/dist/records.js +94 -22
- package/dist/resolve.d.ts +21 -4
- package/dist/resolve.js +91 -52
- package/dist/scan-file.d.ts +8 -0
- package/dist/scan-file.js +21 -3
- package/dist/scan.js +62 -12
- package/dist/toml.d.ts +13 -0
- package/dist/toml.js +172 -0
- package/dist/types.d.ts +27 -11
- package/dist/values.d.ts +15 -0
- package/dist/values.js +120 -2
- package/dist/yaml.d.ts +12 -0
- package/dist/yaml.js +148 -0
- package/package.json +7 -6
- 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
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
332
|
-
* the tsconfig nearest the importing file, package.json `imports`
|
|
333
|
-
* TypeScript source. Each module it reads carries a reader for the
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
workspaces.
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
|
449
|
+
return known.get(found) ?? { reason: 'could not be read' };
|
|
411
450
|
};
|
|
412
451
|
return reader;
|
|
413
452
|
}
|
package/dist/scan-file.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 ||
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|