test-inventory 0.6.1 → 0.8.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 (49) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +4 -3
  3. package/dist/case-titles.d.ts +27 -0
  4. package/dist/case-titles.js +329 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +6 -5
  7. package/dist/config-dependencies.js +8 -3
  8. package/dist/config-runs.d.ts +22 -0
  9. package/dist/config-runs.js +263 -3
  10. package/dist/config.d.ts +18 -1
  11. package/dist/config.js +160 -14
  12. package/dist/dependency-hash.d.ts +13 -5
  13. package/dist/dependency-hash.js +56 -12
  14. package/dist/diagnostics.js +3 -0
  15. package/dist/extract.d.ts +19 -1
  16. package/dist/extract.js +391 -41
  17. package/dist/files.js +3 -2
  18. package/dist/filter.js +1 -0
  19. package/dist/framework-version.d.ts +13 -2
  20. package/dist/framework-version.js +64 -5
  21. package/dist/frameworks/bindings.d.ts +14 -0
  22. package/dist/frameworks/bindings.js +99 -6
  23. package/dist/frameworks/common.d.ts +6 -0
  24. package/dist/frameworks/common.js +27 -0
  25. package/dist/frameworks/jest.d.ts +46 -0
  26. package/dist/frameworks/jest.js +442 -0
  27. package/dist/frameworks/playwright.d.ts +3 -1
  28. package/dist/frameworks/playwright.js +14 -5
  29. package/dist/frameworks/test-functions.d.ts +169 -0
  30. package/dist/frameworks/test-functions.js +492 -0
  31. package/dist/frameworks/vitest.d.ts +13 -58
  32. package/dist/frameworks/vitest.js +84 -378
  33. package/dist/imported-values.d.ts +22 -0
  34. package/dist/imported-values.js +149 -0
  35. package/dist/model.d.ts +11 -0
  36. package/dist/options.d.ts +16 -1
  37. package/dist/options.js +9 -2
  38. package/dist/parse.js +17 -2
  39. package/dist/records.js +57 -15
  40. package/dist/resolve.js +39 -36
  41. package/dist/scan-file.d.ts +5 -0
  42. package/dist/scan-file.js +15 -8
  43. package/dist/scan-source.js +3 -1
  44. package/dist/scan.js +31 -10
  45. package/dist/types.d.ts +24 -6
  46. package/dist/values.d.ts +49 -1
  47. package/dist/values.js +531 -19
  48. package/package.json +1 -1
  49. package/schema/v1.json +33 -4
@@ -1,8 +1,12 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- const PACKAGES = { playwright: '@playwright/test', vitest: 'vitest' };
3
+ import { globSync } from 'tinyglobby';
4
+ import { OptionsError } from "./options.js";
5
+ const PACKAGES = { playwright: '@playwright/test', vitest: 'vitest', jest: 'jest' };
6
+ /** Each framework's name in messages. */
7
+ export const FRAMEWORK_NAMES = { playwright: 'Playwright', vitest: 'Vitest', jest: 'Jest' };
4
8
  /** Oldest supported version of each framework. */
5
- export const OLDEST = { playwright: [1, 42], vitest: [3, 0] };
9
+ export const OLDEST = { playwright: [1, 42], vitest: [3, 0], jest: [29, 0] };
6
10
  /** A file's text, or null when it is missing or can't be read. */
7
11
  function readText(file) {
8
12
  try {
@@ -71,12 +75,25 @@ function declaredVersion(folder, name) {
71
75
  }
72
76
  /**
73
77
  * The framework version from the installed package, then a lockfile, then package.json, each looked for in the root
74
- * and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found.
78
+ * and the folders above it. A version range, as in `^4.0.0`, is used only when nothing else is found. When none of
79
+ * those has the framework, as in a monorepo whose root leaves it to a package such as `code/`, the shallowest package
80
+ * below the root that declares it and holds one of the scanned `files` is read the same way, from its own folder up.
75
81
  */
76
- export function detectFrameworkVersion(root, framework) {
82
+ export function detectFrameworkVersion(root, framework, files) {
77
83
  const name = PACKAGES[framework];
84
+ const found = detectIn(root, folders(root), name);
85
+ if (found)
86
+ return found;
87
+ // In path order, then shallowest first; the sort is stable, so packages as deep stay in path order.
88
+ const nested = globSync('**/package.json', { cwd: root, ignore: ['**/node_modules/**'] })
89
+ .sort()
90
+ .sort((a, b) => a.split('/').length - b.split('/').length)
91
+ .map((file) => path.dirname(file))
92
+ .find((folder) => files.some((file) => file.startsWith(`${folder}/`)) && declaredVersion(path.join(root, folder), name) !== null);
93
+ return nested === undefined ? null : detectIn(root, folders(path.join(root, nested)), name);
94
+ }
95
+ function detectIn(root, all, name) {
78
96
  const source = (folder, file) => path.relative(root, path.join(folder, file)).replaceAll('\\', '/');
79
- const all = folders(root);
80
97
  for (const folder of all) {
81
98
  const version = installedVersion(folder, name);
82
99
  if (version)
@@ -94,3 +111,45 @@ export function detectFrameworkVersion(root, framework) {
94
111
  }
95
112
  return null;
96
113
  }
114
+ /** Config file names of each runner. A Vite config alone doesn't count, as Vite apps without Vitest have one. */
115
+ const CONFIG_NAMES = {
116
+ playwright: /^playwright\.config\.[cm]?[jt]s$/,
117
+ vitest: /^vitest\.config\.[cm]?[jt]s$/,
118
+ jest: /^jest\.config\.([cm]?[jt]s|json)$/,
119
+ };
120
+ /** Whether a package.json lists a runner, or for Jest has a `jest` key, its config. */
121
+ function declares(manifest, framework) {
122
+ const json = readJson(manifest);
123
+ const fields = ['devDependencies', 'dependencies', 'peerDependencies', 'optionalDependencies'];
124
+ return (fields.some((field) => json?.[field]?.[PACKAGES[framework]] !== undefined) ||
125
+ (framework === 'jest' && json?.jest !== undefined));
126
+ }
127
+ /**
128
+ * The runner a project uses, for a scan without `framework`: the one whose package a package.json in the root or a
129
+ * folder below it, outside `ignore`, lists, or whose config is there. Only when none is found there do the package.json files above the
130
+ * root count, as a monorepo may declare its runner once at its top. Throws an `OptionsError` when the project uses
131
+ * none of them or several, naming what it found.
132
+ */
133
+ export function detectFramework(root, ignore) {
134
+ const below = globSync(['**/package.json', '**/*.config.*'], {
135
+ cwd: root,
136
+ ignore: ['**/node_modules/**', '**/.git/**', ...ignore],
137
+ });
138
+ const manifests = below.filter((file) => path.basename(file) === 'package.json').map((file) => path.join(root, file));
139
+ const above = folders(root)
140
+ .slice(1)
141
+ .map((folder) => path.join(folder, 'package.json'));
142
+ const all = ['playwright', 'vitest', 'jest'];
143
+ let found = all.filter((framework) => below.some((file) => CONFIG_NAMES[framework].test(path.basename(file))) ||
144
+ manifests.some((manifest) => declares(manifest, framework)));
145
+ if (found.length === 0)
146
+ found = all.filter((framework) => above.some((manifest) => declares(manifest, framework)));
147
+ if (found.length === 1)
148
+ return found[0];
149
+ if (found.length === 0) {
150
+ throw new OptionsError('options.framework is needed: no package.json in the root, below it or above it lists @playwright/test, vitest or jest, and no config of theirs is in the root or below.');
151
+ }
152
+ const names = found.map((framework) => FRAMEWORK_NAMES[framework]);
153
+ const values = found.map((framework) => `"${framework}"`);
154
+ throw new OptionsError(`options.framework is needed, as the project uses ${names.slice(0, -1).join(', ')} and ${names.at(-1)}: scan it once with each of ${values.join(', ')}.`);
155
+ }
@@ -19,6 +19,8 @@ export interface TestObjects {
19
19
  returnsTestObject(name: string): boolean;
20
20
  /** Whether re-exporting `imported` from `source` exports a test object, or a function that returns one. */
21
21
  reexports(source: string, imported: string, exported: Exported): boolean;
22
+ /** What the test object the module exports as `name` stands for, as `it.skip` stands for `it` with `skip`. */
23
+ exportedStart?(name: string): unknown;
22
24
  }
23
25
  /**
24
26
  * The functions a module declares at its top level, by name, to tell whether one returns a test object: every value it
@@ -54,6 +56,13 @@ export declare function allDeclarators(program: Program): VariableDeclarator[];
54
56
  export declare function moduleImports(program: Program, isFramework: (source: string) => boolean): Map<string, ModuleImport>;
55
57
  /** The module of `require('module')`, or null for any other expression. */
56
58
  export declare function requiredModule(expression: Expression): string | null;
59
+ /**
60
+ * What a CommonJS module exports under `name`: `exports.name = value`, `module.exports.name = value` or a key of
61
+ * `module.exports = { ... }`, and for `default`, `module.exports` itself; the last one written wins; null otherwise.
62
+ */
63
+ export declare function commonJsExport(program: Program, name: string): Expression | null;
64
+ /** The names a CommonJS module exports one by one, as `exports.name = value` and `module.exports.name = value` do. */
65
+ export declare function commonJsNames(program: Program): string[];
57
66
  /** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
58
67
  export declare function moduleExportsValue(statement: Node): Expression | null;
59
68
  /** Applies `bind` to each declarator until no more names are bound, so declaration order doesn't matter. */
@@ -103,10 +112,15 @@ export declare function followModule(source: string, name: string, readModule: M
103
112
  interface Followed {
104
113
  exported: boolean;
105
114
  reason: string | null;
115
+ /** What the exported test object stands for, when the framework's test objects say. */
116
+ start?: unknown;
117
+ /** The module writes a function under the name, a helper that registers tests itself, which the walk follows. */
118
+ isHelper?: boolean;
106
119
  }
107
120
  /** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
108
121
  export declare function followImport(name: string, imported: ModuleImport, readModule: ModuleReader | null, file: FileContext, framework: Framework): {
109
122
  exported: boolean;
110
123
  problem: Problem | null;
124
+ start: unknown;
111
125
  };
112
126
  export {};
@@ -1,4 +1,4 @@
1
- import { childNodes, isFunctionNode, propertyName, returnValues, unwrap } from "../ast.js";
1
+ import { childNodes, isFunctionNode, propertyKey, propertyName, returnValues, unwrap, } from "../ast.js";
2
2
  import { parseFile } from "../parse.js";
3
3
  import { snippet } from "../source.js";
4
4
  import { problem, text } from "./common.js";
@@ -125,6 +125,18 @@ export function moduleImports(program, isFramework) {
125
125
  imports.set(specifier.local.name, { source: statement.source.value, imported, node: specifier });
126
126
  }
127
127
  }
128
+ // `const { test } = require('./fixtures')` imports as `import { test } from './fixtures'` does.
129
+ for (const declarator of topLevelDeclarators(program)) {
130
+ const source = declarator.init ? requiredModule(declarator.init) : null;
131
+ if (source === null || !/^\.\.?\//.test(source) || declarator.id.type !== 'ObjectPattern')
132
+ continue;
133
+ for (const property of declarator.id.properties) {
134
+ const key = property.type === 'Property' ? propertyKey(property) : null;
135
+ if (key !== null && property.type === 'Property' && property.value.type === 'Identifier') {
136
+ imports.set(property.value.name, { source, imported: key, node: declarator });
137
+ }
138
+ }
139
+ }
128
140
  return imports;
129
141
  }
130
142
  /** The module of `require('module')`, or null for any other expression. */
@@ -135,6 +147,59 @@ export function requiredModule(expression) {
135
147
  const argument = node.arguments.at(0);
136
148
  return argument?.type === 'Literal' && typeof argument.value === 'string' ? argument.value : null;
137
149
  }
150
+ /**
151
+ * What a CommonJS module exports under `name`: `exports.name = value`, `module.exports.name = value` or a key of
152
+ * `module.exports = { ... }`, and for `default`, `module.exports` itself; the last one written wins; null otherwise.
153
+ */
154
+ export function commonJsExport(program, name) {
155
+ let found = null;
156
+ for (const statement of program.body) {
157
+ const whole = moduleExportsValue(statement);
158
+ if (whole) {
159
+ const object = unwrap(whole);
160
+ if (name === 'default')
161
+ found = whole;
162
+ else if (object.type === 'ObjectExpression') {
163
+ for (const property of object.properties) {
164
+ if (property.type === 'Property' && propertyKey(property) === name)
165
+ found = property.value;
166
+ }
167
+ }
168
+ continue;
169
+ }
170
+ if (statement.type !== 'ExpressionStatement')
171
+ continue;
172
+ const expression = unwrap(statement.expression);
173
+ if (expression.type !== 'AssignmentExpression' || expression.left.type !== 'MemberExpression')
174
+ continue;
175
+ if (propertyName(expression.left) !== name)
176
+ continue;
177
+ const target = unwrap(expression.left.object);
178
+ const isExports = target.type === 'Identifier' && target.name === 'exports';
179
+ const isModuleExports = target.type === 'MemberExpression' &&
180
+ target.object.type === 'Identifier' &&
181
+ target.object.name === 'module' &&
182
+ propertyName(target) === 'exports';
183
+ if (isExports || isModuleExports)
184
+ found = expression.right;
185
+ }
186
+ return found;
187
+ }
188
+ /** The names a CommonJS module exports one by one, as `exports.name = value` and `module.exports.name = value` do. */
189
+ export function commonJsNames(program) {
190
+ const names = new Set();
191
+ for (const statement of program.body) {
192
+ if (statement.type !== 'ExpressionStatement')
193
+ continue;
194
+ const expression = unwrap(statement.expression);
195
+ if (expression.type !== 'AssignmentExpression' || expression.left.type !== 'MemberExpression')
196
+ continue;
197
+ const name = propertyName(expression.left);
198
+ if (name !== null && commonJsExport(program, name) === expression.right)
199
+ names.add(name);
200
+ }
201
+ return [...names];
202
+ }
138
203
  /** What `module.exports = value` assigns, the default export of a CommonJS module; null for any other statement. */
139
204
  export function moduleExportsValue(statement) {
140
205
  if (statement.type !== 'ExpressionStatement')
@@ -187,6 +252,9 @@ export function exportsTestObject(program, name, objects, exported = 'test objec
187
252
  case 'specifier':
188
253
  return own.source === null ? local(own.local) : objects.reexports(own.source, own.local, exported);
189
254
  }
255
+ const commonJs = exported === 'test object' && name !== 'default' ? commonJsExport(program, name) : null;
256
+ if (commonJs)
257
+ return objects.isTestObject(commonJs);
190
258
  return program.body.some((statement) => {
191
259
  const source = starReexport(statement);
192
260
  return source !== null && objects.reexports(source, name, exported);
@@ -382,12 +450,21 @@ function followExports(module, name, framework, exported, within, followed) {
382
450
  return { exported: false, reason: 'could not be parsed' };
383
451
  const followFrom = (from, imported, kind) => followModule(from, imported, module.readModule, framework, kind, within, followed);
384
452
  const objects = framework.testObjects(program, (from, imported, kind = 'test object') => followFrom(from, imported, kind).exported);
385
- if (exportsTestObject(program, name, objects, exported))
386
- return { exported: true, reason: null };
453
+ if (exportsTestObject(program, name, objects, exported)) {
454
+ return { exported: true, reason: null, start: objects.exportedStart?.(name) };
455
+ }
456
+ // A function the module writes and exports is a helper that registers tests itself, which the walk follows.
457
+ if (exported === 'test object' && exportsFunction(program, name)) {
458
+ return { exported: false, reason: null, isHelper: true };
459
+ }
387
460
  const unrecognized = (kind) => `does not export ${kind === 'function' ? 'a function that returns a test object' : `a ${framework.name} test object`} that could be recognized`;
388
461
  const origin = importOrigin(program, name, framework.isSource, exported);
389
462
  if (origin) {
390
- const end = followFrom(origin.source, origin.imported, origin.exported).reason ?? unrecognized(origin.exported);
463
+ const followed = followFrom(origin.source, origin.imported, origin.exported);
464
+ // A helper function re-exported, as `import helper from './a'; export { helper }`, is no broken test object.
465
+ if (followed.isHelper && origin.how === 're-exports')
466
+ return followed;
467
+ const end = followed.reason ?? unrecognized(origin.exported);
391
468
  return { exported: false, reason: `${origin.how} it from '${origin.source}', which ${end}` };
392
469
  }
393
470
  if (importsFrom(program, framework.isSource))
@@ -397,6 +474,22 @@ function followExports(module, name, framework, exported, within, followed) {
397
474
  const call = exported === 'test object' ? creatingCall(program, name, source) : null;
398
475
  return { exported: false, reason: call ? `builds it with ${call}, which is not followed` : null };
399
476
  }
477
+ /** Whether a module writes the function it exports as `name`: declared, written in place, or held in a variable. */
478
+ function exportsFunction(program, name) {
479
+ const own = ownExport(program, name);
480
+ if (own?.kind === 'function')
481
+ return true;
482
+ if (own?.kind === 'default') {
483
+ const value = unwrap(own.value);
484
+ return value.type === 'FunctionDeclaration' || isFunctionNode(value);
485
+ }
486
+ const local = own?.kind === 'variable' ? name : own?.kind === 'specifier' && own.source === null ? own.local : null;
487
+ if (local === null)
488
+ return false;
489
+ const declared = program.body.some((statement) => statement.type === 'FunctionDeclaration' && statement.id?.name === local);
490
+ const declarator = topLevelDeclarators(program).find((item) => item.id.type === 'Identifier' && item.id.name === local);
491
+ return declared || isFunctionNode(declarator?.init ? unwrap(declarator.init) : null);
492
+ }
400
493
  /**
401
494
  * The call that creates what the module exports as `name`, a variable or its default export, as `defineBrokerTest()`;
402
495
  * null for other values.
@@ -412,9 +505,9 @@ function creatingCall(program, name, source) {
412
505
  }
413
506
  /** Follows the import of `name` in a test file, with the diagnostic to report when it isn't a test object. */
414
507
  export function followImport(name, imported, readModule, file, framework) {
415
- const { exported, reason } = followModule(imported.source, imported.imported, readModule, framework);
508
+ const { exported, reason, start } = followModule(imported.source, imported.imported, readModule, framework);
416
509
  const problemFound = reason
417
510
  ? problem('unresolved-test-import', imported.node, `'${name}' is imported from '${imported.source}', which ${reason}; tests that use it are missing.`, snippet(text(file, imported.node)))
418
511
  : null;
419
- return { exported, problem: problemFound };
512
+ return { exported, problem: problemFound, start };
420
513
  }
@@ -116,6 +116,12 @@ export declare function readOptions(file: FileContext, scopes: readonly Node[],
116
116
  export declare function unresolvedOptions(file: FileContext, options: Options, result: string): Problem[];
117
117
  /** The names a function takes from the object pattern of one of its parameters, in source order. */
118
118
  export declare function destructuredNames(fn: FunctionNode | null, index: number | null): string[];
119
+ /**
120
+ * The members of a declared value that `name` gets: none in `const name = test`, `['describe', 'serial']` in
121
+ * `const { describe: { serial: name } } = test`, and none for the rest of an object, which keeps the other members.
122
+ * Null when it gets something else: a default, an array element, or a member under a computed key.
123
+ */
124
+ export declare function boundMembers(pattern: Node, name: string): string[] | null;
119
125
  /**
120
126
  * Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
121
127
  * pattern, so that any key of it may be used.
@@ -185,6 +185,13 @@ export function readTitle(file, scopes, node, options) {
185
185
  if (value === null && options.functionNames && expression?.type === 'Identifier') {
186
186
  value = file.values.isFunction(expression.name, scopes) ? expression.name : null;
187
187
  }
188
+ // A function or class written in place gives its name, as `describe(function OrderBook() {}, ...)` does.
189
+ const written = expression && unwrap(expression);
190
+ if (value === null &&
191
+ options.functionNames &&
192
+ (written?.type === 'FunctionExpression' || written?.type === 'ClassExpression')) {
193
+ value = written.id?.name ?? '';
194
+ }
188
195
  if (value !== null) {
189
196
  const withoutTags = options.stripTags ? value.replace(/@\S+/g, ' ').replace(/\s+/g, ' ').trim() : value;
190
197
  return { title: { text: value, withoutTags, isDynamic: false }, problem: null };
@@ -287,6 +294,26 @@ export function destructuredNames(fn, index) {
287
294
  const names = parameter.properties.map((property) => (property.type === 'Property' ? propertyKey(property) : null));
288
295
  return names.filter((name) => name !== null);
289
296
  }
297
+ /**
298
+ * The members of a declared value that `name` gets: none in `const name = test`, `['describe', 'serial']` in
299
+ * `const { describe: { serial: name } } = test`, and none for the rest of an object, which keeps the other members.
300
+ * Null when it gets something else: a default, an array element, or a member under a computed key.
301
+ */
302
+ export function boundMembers(pattern, name) {
303
+ if (pattern.type === 'Identifier')
304
+ return pattern.name === name ? [] : null;
305
+ if (pattern.type !== 'ObjectPattern')
306
+ return null;
307
+ for (const property of pattern.properties) {
308
+ if (property.type === 'RestElement')
309
+ return boundMembers(property.argument, name);
310
+ const members = boundMembers(property.value, name);
311
+ const key = propertyKey(property);
312
+ if (members)
313
+ return key === null ? null : [key, ...members];
314
+ }
315
+ return null;
316
+ }
290
317
  /**
291
318
  * Whether a function takes one of its parameters whole, as in `(fixtures) => ...`, or keeps the rest of its object
292
319
  * pattern, so that any key of it may be used.
@@ -0,0 +1,46 @@
1
+ import type { CallExpression } from 'oxc-parser';
2
+ import type { ApiCall, CallContext, ModuleReader } from '../model.ts';
3
+ import type { ParsedFile } from '../parse.ts';
4
+ import type { HookCounts } from '../types.ts';
5
+ import { type FileContext, type Link } from './common.ts';
6
+ import { SuiteAdapter, type Kind } from './test-functions.ts';
7
+ export declare class JestAdapter extends SuiteAdapter {
8
+ constructor(parsed: ParsedFile, file: FileContext, readModule: ModuleReader | null);
9
+ classify(call: CallExpression, context: CallContext): ApiCall | null;
10
+ protected isAliasLink(_kind: Kind, link: Link): boolean;
11
+ protected hook(call: CallExpression, name: keyof HookCounts, context: CallContext): ApiCall;
12
+ /**
13
+ * `jest.retryTimes(n, options)`. Jest keeps one count for the file and reads it as each test runs, so the last call
14
+ * made while the file loads applies to every test, wherever it is written. From 30.5, `entireDescribe` keeps the
15
+ * count for the describe it is called in instead, and retries the describe as a whole.
16
+ */
17
+ private retryCall;
18
+ /** Whether an expression is Jest's `jest` object: the global, a name imported from Jest, or a member of the module. */
19
+ private isJest;
20
+ private chainCall;
21
+ private declaration;
22
+ /** Whether a table makes `describe.each` throw, as jest-each does on an empty array or a value that isn't a table. */
23
+ private isInvalidTable;
24
+ /** Jest's order: `todo` and `skip` set the test's mode, `only` runs it, and `failing` applies when it runs. */
25
+ private states;
26
+ /**
27
+ * How many tests a table registers. No table, an empty array, or a tagged template whose first line isn't a line of names or
28
+ * whose values don't fill its rows, makes Jest register one test with the title as written, which fails.
29
+ */
30
+ private caseCount;
31
+ /**
32
+ * The cases of a table whose values the scan can work out, as Jest passes them: a case that is an array is spread
33
+ * into the callback's arguments when every case is one; otherwise each case is passed whole. A tagged template gives
34
+ * one object per row, keyed by the names in its first line.
35
+ */
36
+ private tableCases;
37
+ }
38
+ /**
39
+ * What jest-each's heading pattern, `/^\n\s*[^\s]+\s*(\|\s*[^\s]+\s*)*\n/`, matches at the start of a table's first
40
+ * string. The pattern backtracks exponentially on a long run of `|` it can't match, which a scanned file could hang the
41
+ * scan with, so its tries are worked out once per position, from the end. `heading[i]` is where the match ends when a
42
+ * heading, `\s*[^\s]+\s*`, starts at `i`, and `rest[i]` where it ends when the heading before ends at `i`: on a `|`,
43
+ * another heading first, else the line break. The pattern tries the whole word first, with its spaces given back one by
44
+ * one, then shorter words, of which only one that ends before a `|` can go on.
45
+ */
46
+ export declare function headingsMatch(text: string): string | null;