playwright-mutation-gate 0.0.1 → 0.2.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/README.md CHANGED
@@ -1,39 +1,267 @@
1
1
  # playwright-mutation-gate
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/playwright-mutation-gate)](https://www.npmjs.com/package/playwright-mutation-gate)
4
+ [![CI](https://github.com/VladyslavDmitriiev/playwright-mutation-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/VladyslavDmitriiev/playwright-mutation-gate/actions)
5
+ [![license](https://img.shields.io/npm/l/playwright-mutation-gate)](./LICENSE)
6
+
3
7
  > A passing test only means something if it can fail.
4
8
 
5
- **Status: v0.0.1 is a name reservation.** The actual CLI is in development and ships as v0.1.0.
9
+ `playwright-mutation-gate` inverts the assertion you marked as primary and
10
+ re-runs the test. If it stays green, the assertion is hollow and the gate
11
+ fails your CI run instead of reporting a lie.
12
+
13
+ <p align="center">
14
+ <img src="media/demo.svg" alt="Terminal recording: 3 killed, 1 survived, gate fails" width="840">
15
+ </p>
6
16
 
7
- ## What it will do
17
+ <details><summary>Text version (if the SVG above does not render)</summary>
8
18
 
9
- `playwright-mutation-gate` checks that your green Playwright tests are green for a reason.
10
- You mark the primary assertion in each test:
19
+ ```
20
+ fixtures/demo.spec.ts
21
+ killed shows error for invalid email :8
22
+ killed hides error for valid email :16
23
+ killed shows success for valid email :24
24
+ survived hides success initially :31
25
+
26
+ 3 killed, 1 survived, 0 cannot-mutate, 0 errors
27
+ Gate FAILED: hollow, unverified or broken assertions above.
28
+ ```
29
+ </details>
30
+
31
+ ## Quick start
32
+
33
+ Requires **Node >= 20** and a working **Playwright** setup (any version that
34
+ supports `playwright test`).
35
+
36
+ ```bash
37
+ npm install -D playwright-mutation-gate
38
+ ```
39
+
40
+ Mark the primary assertion in each test with `// @primary-assert`. Put it at
41
+ the end of the assertion line, or on its own line right above it. The assertion
42
+ itself must be a single-line `expect(...)` call.
11
43
 
12
44
  ```ts
13
- test('checkout shows order confirmation', async ({ page }) => {
45
+ test('checkout shows confirmation', async ({ page }) => {
14
46
  // ...
47
+ await expect(page.getByText('Order confirmed')).toBeVisible(); // @primary-assert
48
+ });
49
+
50
+ // Also valid: the marker on its own line above the assertion.
51
+ test('cart badge updates', async ({ page }) => {
15
52
  // @primary-assert
16
- await expect(page.getByText('Order confirmed')).toBeVisible();
53
+ await expect(page.getByTestId('cart-count')).toHaveText('1');
54
+ });
55
+ ```
56
+
57
+ Run the gate:
58
+
59
+ ```bash
60
+ npx playwright-mutation-gate "tests/**/*.spec.ts"
61
+ ```
62
+
63
+ ## How marking works
64
+
65
+ The gate inverts the marked assertion
66
+ (`expect(x).toBeVisible()` becomes `expect(x).not.toBeVisible()`) in a
67
+ temporary copy of the spec and re-runs the test in isolation.
68
+
69
+ - **killed** -- the test failed under inversion. The assertion is alive.
70
+ - **survived** -- the test still passed. The assertion checks nothing useful.
71
+ - **cannot-mutate** -- the assertion shape could not be inverted safely. The
72
+ gate reports it instead of guessing.
73
+
74
+ One marker per `test()` block. Markers inside `test.describe` or `test.use`
75
+ callbacks work the same way -- the gate scans each `test()` individually,
76
+ nesting does not matter.
77
+
78
+ By default, tests without a marker fail the gate (sentinel mode). This means
79
+ a newly added test without `// @primary-assert` shows up as:
80
+
81
+ ```
82
+ unmarked adds item to cart :42
83
+ ```
84
+
85
+ Pass `--no-require-sentinel` to opt out while you're marking an existing suite.
86
+
87
+ ## Custom test fixtures
88
+
89
+ If your tests are declared through a custom fixture instead of the bare `test`,
90
+ the gate finds it automatically in two cases: a local `.extend` chain and a
91
+ renamed import.
92
+
93
+ ```ts
94
+ import { test as base } from '@playwright/test';
95
+ const authedTest = base.extend({ /* ... */ }); // auto-detected
96
+
97
+ authedTest('sees dashboard', async ({ page }) => {
98
+ await expect(page.getByText('Welcome')).toBeVisible(); // @primary-assert
99
+ });
100
+ ```
101
+
102
+ When the fixture is imported by its own name from another module, name it with
103
+ `--test-id` (repeatable):
104
+
105
+ ```bash
106
+ npx playwright-mutation-gate tests/dashboard.spec.ts --test-id authedTest
107
+ ```
108
+
109
+ A runtime `authedTest.skip(condition)` inside a test is respected: if the test
110
+ skips, the mutation reports `cannot-mutate` rather than a false `survived`.
111
+
112
+ ## CI recipe
113
+
114
+ ```yaml
115
+ # .github/workflows/mutation-gate.yml
116
+ name: mutation-gate
117
+
118
+ on: [push, pull_request]
119
+
120
+ jobs:
121
+ gate:
122
+ runs-on: ubuntu-latest
123
+ steps:
124
+ - uses: actions/checkout@v4
125
+ - uses: actions/setup-node@v4
126
+ with:
127
+ node-version: 22
128
+ cache: npm
129
+ - run: npm ci
130
+ - run: npx playwright install --with-deps chromium
131
+ - run: npx playwright-mutation-gate "tests/**/*.spec.ts"
132
+ ```
133
+
134
+ The gate exits 0 only when every marked assertion is killed and sentinel
135
+ mode is satisfied. Any other outcome exits 1.
136
+
137
+ Cost scales linearly: every marked assertion is one extra `playwright test`
138
+ run. That is fine for a handful of specs, but on a large suite you probably
139
+ do not want the full sweep on every push. Two patterns that work well:
140
+
141
+ - **Gate only what changed.** The CLI takes plain file paths, so feed it the
142
+ spec files the PR touched:
143
+
144
+ ```yaml
145
+ - run: |
146
+ SPECS=$(git diff --name-only origin/${{ github.base_ref }}... -- 'tests/**/*.spec.ts')
147
+ [ -n "$SPECS" ] && npx playwright-mutation-gate $SPECS || echo "no specs changed"
148
+ ```
149
+
150
+ - **Full sweep on a schedule.** Run the whole glob nightly instead of per
151
+ commit. A hollow assertion does not get less hollow between merges;
152
+ catching it a day later is fine. `--playwright-args` is available if you
153
+ need to shard or point at a specific config.
154
+
155
+ ## CLI reference
156
+
157
+ ```
158
+ npx playwright-mutation-gate [options] <specs...>
159
+
160
+ Options:
161
+ --json machine-readable JSON report
162
+ --no-require-sentinel do not fail on tests without a marker
163
+ --playwright-args <args> extra arguments for playwright test
164
+ --test-id <name> treat <name> as a test builder (repeatable)
165
+ -V, --version output the version number
166
+ -h, --help display help for command
167
+ ```
168
+
169
+ ## Programmatic API
170
+
171
+ Since v0.2 the package also exposes a library entry, so a tool can run the gate
172
+ in-process and inspect the structured report instead of shelling out to the CLI
173
+ and parsing `--json`.
174
+
175
+ ```ts
176
+ import { runSpec, summarize } from 'playwright-mutation-gate';
177
+
178
+ const spec = await runSpec('tests/checkout.spec.ts', {
179
+ testIds: ['authedTest'], // extra test builders, same as --test-id
180
+ playwrightArgs: ['--project=chromium', '--retries=0'],
17
181
  });
182
+ const summary = summarize([spec], { requireSentinel: true });
183
+
184
+ if (!summary.gatePassed) {
185
+ const hollow = spec.results.filter((r) => r.verdict === 'survived');
186
+ console.error(`${hollow.length} assertion(s) do not gate the result`);
187
+ process.exit(1);
188
+ }
18
189
  ```
19
190
 
20
- The gate then inverts that assertion (`expect(x).toBeVisible()` `expect(x).not.toBeVisible()`)
21
- in a temporary copy of the spec and re-runs the test:
191
+ `runSpec(specPath, opts?)` returns a `SpecResult` (per-assertion `verdict`:
192
+ `killed` / `survived` / `cannot-mutate` / `error`, plus unmarked/stray/malformed
193
+ markers). `summarize(specs, opts)` folds one or more results into a `Summary`
194
+ with the `gatePassed` verdict. `jsonReport` and `renderTable` produce the CLI's
195
+ two output formats. Advanced: `planSpecMutations` and `invertAssertLine` expose
196
+ the mutation planner and the single-line inverter directly. Full types ship with
197
+ the package.
22
198
 
23
- - test fails → the assertion is alive (**killed** mutation, good)
24
- - test still passes → the assertion checks nothing (**survived**, a hollow test), so the
25
- gate exits 1 and fails your CI run
199
+ ## Limitations
26
200
 
27
- Tests without a `@primary-assert` marker also fail the gate by default, so nothing slips
28
- through unmarked.
201
+ **The gate proves an assertion can fail, not that it checks the right thing.**
202
+ A weak but falsifiable assertion passes: a test that asserts a site-wide header
203
+ (one that also renders on the error page) is killed by inversion and clears the
204
+ gate, even though it would not catch the feature breaking. What the gate
205
+ guarantees is narrower and mechanical: every marked assertion actually runs and
206
+ is load-bearing, so the test cannot stay green regardless of what the app does.
207
+ Judging whether the assertion is the *right* one still takes review, or
208
+ application-code mutation testing (Stryker-style), which is a different tool.
209
+
210
+ **A kill is slightly generous by construction.** `killed` means the mutated
211
+ run failed, not that the flipped assertion is what failed. Playwright actions
212
+ carry implicit assertions (`click()` fails on its own if the element never
213
+ appears), and an ordinarily flaky step also counts, so a test can die before
214
+ it ever reaches the inverted line. The asymmetry works in your favor:
215
+ `survived` has no such excuse. A test that stays green with its own primary
216
+ assertion inverted is hollow, full stop.
217
+
218
+ String-based inversion, not AST. This keeps the tool small and dependency-free
219
+ but means some shapes report `cannot-mutate`:
220
+
221
+ - **Single line only.** `expect(...)` and its matcher must start on the marked
222
+ line. Multi-line chains report cannot-mutate.
223
+ - **One assertion per line.** Two `expect` calls on the same line (including
224
+ asymmetric matchers in arguments, nested expects inside `toPass`) report
225
+ cannot-mutate.
226
+ - **No aliases.** Assertions using a re-exported or aliased `expect` are not
227
+ recognized.
228
+ - **Custom matchers** must follow the `toXxx` convention. Names that shadow
229
+ built-ins (`toString`, `toFixed`, ...) are denylisted.
230
+ - **Supported chain:**
231
+ `[await] expect[.soft|.poll](...)[.resolves|.rejects][.not].toXxx(`
232
+
233
+ `cannot-mutate` is always safe: the gate tells you it could not verify the
234
+ assertion, never silently skips it.
235
+
236
+ Skipped tests get the same honesty. A test disabled with `test.skip` never
237
+ executes, so there is nothing to observe when its assertion is inverted.
238
+ Rather than guess, the gate reports `cannot-mutate` with the reason ("the
239
+ test was skipped at runtime (conditional test.skip), so the inverted
240
+ assertion never ran"). In
241
+ practice this is a feature: a `test.skip(true, ...)` from months ago looks
242
+ identical to a healthy passing spec in normal CI output, and the gate is
243
+ often what surfaces it.
29
244
 
30
245
  ## Why
31
246
 
32
- Stryker and friends mutate your application code to score unit tests. E2E suites have the
33
- opposite blind spot: the assertion itself is often hollow. It passes against a broken app.
34
- This tool mutates the assertions, not the app.
247
+ AI code generators write tests fast. The tests look right: good names, proper
248
+ selectors, green checkmarks. But the assertions are often hollow: a forgotten
249
+ `await` whose result nobody checks, an assertion guarded behind a branch that
250
+ never executes, a `not.toBeVisible` against a selector that never existed, a
251
+ test silently disabled with `skip`.
252
+
253
+ Stryker and friends mutate your *application code* to score unit tests. E2E
254
+ suites have the opposite blind spot: the *assertion itself* is often the weak
255
+ link. It passes against a broken app. `playwright-mutation-gate` mutates the
256
+ assertions, not the app.
257
+
258
+ If a green test cannot turn red when its assertion is inverted, it was never
259
+ testing anything. The gate catches that before it reaches production.
260
+
261
+ ## Contributing
35
262
 
36
- v0.1.0 coming soon.
263
+ Issues and PRs are welcome. Run `npm test` before submitting -- it covers
264
+ lint, typecheck, unit, e2e and integration tests in one pass.
37
265
 
38
266
  ## License
39
267
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { Command, CommanderError } from 'commander';
4
+ import { jsonReport, renderTable, summarize } from './report.js';
5
+ import { runSpec } from './run.js';
6
+ // Exit codes: 0 = gate passed, 1 = gate failed (survived / unverified /
7
+ // marker mistakes), 2 = usage or environment error (bad flags, missing file).
8
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9
+ const program = new Command()
10
+ .name('playwright-mutation-gate')
11
+ .description('Invert each @primary-assert assertion and re-run the test.\n' +
12
+ 'A test that stays green under inversion is hollow and fails the gate.')
13
+ .version(pkg.version)
14
+ .argument('<specs...>', 'Playwright spec files to gate')
15
+ .option('--json', 'print a machine-readable JSON report instead of the table', false)
16
+ .option('--no-require-sentinel', 'do not fail the gate when a test() has no @primary-assert marker')
17
+ .option('--playwright-args <args>', 'extra arguments passed to `npx playwright test`, split on spaces\n' +
18
+ '(values containing spaces are not supported)')
19
+ .option('--test-id <name>', 'treat <name> as a test builder in addition to `test` (repeatable);\n' +
20
+ 'for custom fixtures imported by name (import { authedTest } from ...).\n' +
21
+ 'Local `const x = test.extend(...)` and `test as x` renames are auto-detected', (value, acc) => acc.concat(value), [])
22
+ .showHelpAfterError()
23
+ .addHelpText('after', '\nMarking:\n' +
24
+ ' Put `// @primary-assert` at the end of the primary assertion line,\n' +
25
+ ' or on its own line right above it. One marker per test().\n' +
26
+ ' await expect(page.getByText(\'Saved\')).toBeVisible(); // @primary-assert')
27
+ .action(async (specPaths, opts) => {
28
+ const missing = specPaths.filter((p) => !existsSync(p));
29
+ if (missing.length > 0) {
30
+ console.error(`spec file not found: ${missing.join(', ')}`);
31
+ process.exit(2);
32
+ }
33
+ const playwrightArgs = (opts.playwrightArgs ?? '').split(/\s+/).filter(Boolean);
34
+ const results = [];
35
+ for (const spec of specPaths) {
36
+ process.stderr.write(`gating ${spec}\n`);
37
+ try {
38
+ results.push(await runSpec(spec, { playwrightArgs, testIds: opts.testId }));
39
+ }
40
+ catch (err) {
41
+ console.error(`failed to gate ${spec}: ${err instanceof Error ? err.message : String(err)}`);
42
+ process.exit(2);
43
+ }
44
+ }
45
+ const gate = { requireSentinel: opts.requireSentinel };
46
+ if (opts.json) {
47
+ console.log(jsonReport(results, gate));
48
+ }
49
+ else {
50
+ const color = process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
51
+ console.log(renderTable(results, { color, ...gate }));
52
+ }
53
+ process.exit(summarize(results, gate).gatePassed ? 0 : 1);
54
+ });
55
+ program.exitOverride();
56
+ try {
57
+ await program.parseAsync();
58
+ }
59
+ catch (err) {
60
+ // commander has already printed its message; map usage errors to exit 2,
61
+ // keep 0 for --help / --version.
62
+ const code = err instanceof CommanderError && err.exitCode === 0 ? 0 : 2;
63
+ process.exit(code);
64
+ }
@@ -0,0 +1,4 @@
1
+ export { runSpec, sweepStaleMutationFiles, DEFAULT_TIMEOUT_MS, type SpecResult, type MutationResult, type Verdict, type RunOptions, } from './run.js';
2
+ export { summarize, jsonReport, renderTable, type Summary, type GateOptions, type RenderOptions, } from './report.js';
3
+ export { planSpecMutations, MARKER, type SpecPlan, type Mutation, type PlanOptions, } from './mutate.js';
4
+ export { invertAssertLine, type InvertResult } from './invert.js';
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // Programmatic entry point.
2
+ //
3
+ // The CLI (`dist/cli.js`) is one consumer of this library; another is any tool
4
+ // that wants to run the gate in-process and inspect the report itself — e.g. a
5
+ // custom test-pipeline that already knows which specs changed and wants the
6
+ // structured `SpecResult`/`Summary` without shelling out and parsing `--json`.
7
+ //
8
+ // Curated surface: run a spec, summarize/render the results, and (advanced) plan
9
+ // or invert individual mutations. The tokenizer (`lex.ts`) and the on-disk
10
+ // mutant-file mechanics stay internal — `runSpec` orchestrates them for you.
11
+ export { runSpec, sweepStaleMutationFiles, DEFAULT_TIMEOUT_MS, } from './run.js';
12
+ export { summarize, jsonReport, renderTable, } from './report.js';
13
+ // Advanced: build or inspect the mutation plan directly.
14
+ export { planSpecMutations, MARKER, } from './mutate.js';
15
+ export { invertAssertLine } from './invert.js';
@@ -0,0 +1,16 @@
1
+ export type InvertResult = {
2
+ ok: string;
3
+ } | {
4
+ cannotMutate: string;
5
+ };
6
+ /**
7
+ * Invert the assertion on a single line of a Playwright spec:
8
+ * `expect(x).toY()` <-> `expect(x).not.toY()`.
9
+ *
10
+ * Purely string-based. Supported chain shape:
11
+ * `[await] expect[.soft|.poll](...) [.resolves|.rejects] [.not] .toXxx(`
12
+ * The matcher call must start on this line; its arguments may continue on
13
+ * following lines. Everything else returns `cannotMutate` with a reason
14
+ * rather than guessing.
15
+ */
16
+ export declare function invertAssertLine(line: string): InvertResult;
package/dist/invert.js ADDED
@@ -0,0 +1,116 @@
1
+ import { CODE, findCloseMasked, lex } from './lex.js';
2
+ // Built-in toXxx methods that look like matchers but are not assertions.
3
+ const NOT_MATCHERS = new Set([
4
+ 'toString',
5
+ 'toLocaleString',
6
+ 'toJSON',
7
+ 'toISOString',
8
+ 'toFixed',
9
+ 'toExponential',
10
+ 'toPrecision',
11
+ 'toDateString',
12
+ 'toTimeString',
13
+ 'toUTCString',
14
+ 'toLocaleDateString',
15
+ 'toLocaleTimeString',
16
+ 'toLocaleLowerCase',
17
+ 'toLocaleUpperCase',
18
+ 'toLowerCase',
19
+ 'toUpperCase',
20
+ 'toReversed',
21
+ 'toSorted',
22
+ 'toSpliced',
23
+ ]);
24
+ const isWord = (c) => c !== undefined && /[\w$]/.test(c);
25
+ /**
26
+ * Invert the assertion on a single line of a Playwright spec:
27
+ * `expect(x).toY()` <-> `expect(x).not.toY()`.
28
+ *
29
+ * Purely string-based. Supported chain shape:
30
+ * `[await] expect[.soft|.poll](...) [.resolves|.rejects] [.not] .toXxx(`
31
+ * The matcher call must start on this line; its arguments may continue on
32
+ * following lines. Everything else returns `cannotMutate` with a reason
33
+ * rather than guessing.
34
+ */
35
+ export function invertAssertLine(line) {
36
+ const kind = lex(line);
37
+ // First CODE index at or after j: skips whitespace, comments and other
38
+ // non-code chars (so a comment inside the chain does not break the walk).
39
+ const skip = (j) => {
40
+ while (j < line.length && (kind[j] !== CODE || /\s/.test(line[j])))
41
+ j++;
42
+ return j;
43
+ };
44
+ const anchors = [...line.matchAll(/\bexpect\s*[.(]/g)].filter((m) => kind[m.index] === CODE);
45
+ if (anchors.length === 0) {
46
+ return { cannotMutate: 'no expect() call found on this line' };
47
+ }
48
+ if (anchors.length > 1) {
49
+ return { cannotMutate: 'multiple expect calls on one line' };
50
+ }
51
+ // Position of the `.` or `(` right after the `expect` token.
52
+ let i = anchors[0].index + anchors[0][0].length - 1;
53
+ if (line[i] === '.') {
54
+ i = skip(i + 1);
55
+ const start = i;
56
+ while (isWord(line[i]))
57
+ i++;
58
+ const form = line.slice(start, i);
59
+ if (form !== 'soft' && form !== 'poll') {
60
+ return { cannotMutate: `unsupported expect form "expect.${form}"` };
61
+ }
62
+ i = skip(i);
63
+ if (line[i] !== '(') {
64
+ return { cannotMutate: `expect.${form} is not called on this line` };
65
+ }
66
+ }
67
+ const close = findCloseMasked(line, kind, i);
68
+ if (close === -1) {
69
+ return { cannotMutate: 'expect(...) does not close on this line (multi-line expect)' };
70
+ }
71
+ // Walk the chain after expect(...): modifiers, optional .not, then the matcher.
72
+ i = skip(close + 1);
73
+ let notStart = -1;
74
+ let notEnd = -1;
75
+ for (;;) {
76
+ if (i >= line.length) {
77
+ return { cannotMutate: 'no matcher call after expect(...) on this line' };
78
+ }
79
+ if (line[i] !== '.') {
80
+ return { cannotMutate: `unexpected "${line[i]}" after expect(...) chain` };
81
+ }
82
+ const dot = i;
83
+ i = skip(i + 1);
84
+ const start = i;
85
+ while (isWord(line[i]))
86
+ i++;
87
+ const id = line.slice(start, i);
88
+ if (id === '') {
89
+ return { cannotMutate: 'unexpected character in expect(...) chain' };
90
+ }
91
+ const lookahead = skip(i);
92
+ if (line[lookahead] === '(') {
93
+ if (!/^to[A-Z]/.test(id) || NOT_MATCHERS.has(id)) {
94
+ return { cannotMutate: `".${id}" does not look like an assertion matcher` };
95
+ }
96
+ const inverted = notStart !== -1
97
+ ? line.slice(0, notStart) + line.slice(notEnd)
98
+ : line.slice(0, dot) + '.not' + line.slice(dot);
99
+ return { ok: inverted };
100
+ }
101
+ if (id === 'not') {
102
+ if (notStart !== -1) {
103
+ return { cannotMutate: 'double .not in expect(...) chain' };
104
+ }
105
+ notStart = dot;
106
+ notEnd = i;
107
+ i = skip(i);
108
+ continue;
109
+ }
110
+ if (id === 'resolves' || id === 'rejects') {
111
+ i = skip(i);
112
+ continue;
113
+ }
114
+ return { cannotMutate: `unsupported chain segment ".${id}"` };
115
+ }
116
+ }
package/dist/lex.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared single-pass lexer for the string-based spec analysis. Classifies
3
+ * every char of a source (a whole file or a single line) so the scanners in
4
+ * invert.ts and mutate.ts agree about what is code. One implementation on
5
+ * purpose: two hand-rolled lexers had already diverged once.
6
+ */
7
+ /** Char inside a string, template literal, block comment or regex literal. */
8
+ export declare const NON_CODE = 0;
9
+ /** Executable source. */
10
+ export declare const CODE = 1;
11
+ /** Char of a `//` comment that starts in code context. */
12
+ export declare const LINE_COMMENT = 2;
13
+ /**
14
+ * Classify every char of `src` as CODE, LINE_COMMENT or NON_CODE. Handles
15
+ * strings with escapes, template literals with nested `${}` expressions,
16
+ * `//` and block comments, and regex literals (heuristic: see startsRegex).
17
+ */
18
+ export declare function lex(src: string): Uint8Array;
19
+ /**
20
+ * Find the `)` matching the `(` at `open`, counting only CODE chars.
21
+ * Returns -1 when it does not close within `src`.
22
+ */
23
+ export declare function findCloseMasked(src: string, kind: Uint8Array, open: number): number;