From 368100618f0d1662461cc4f374c013257412068d Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Sat, 5 Sep 2026 17:05:11 +0200 Subject: [PATCH 01/10] add eslint & ci --- .github/workflows/ci.yml | 64 ++++ eslint.config.mjs | 82 +++++ tests/lib-globals.cjs | 57 +++ tests/package-lock.json | 724 ++++++++++++++++++++++++++++++++++++++- tests/package.json | 5 +- tests/setup-globals.cjs | 36 +- 6 files changed, 935 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 eslint.config.mjs create mode 100644 tests/lib-globals.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..470dbf0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +# Run the test suite and the linter on every push and pull request. +# +# This is the first automation in this repository, and it exists mainly for the +# pull requests 4CAT opens automatically. Those carry `map_item` functions +# translated from Python by a language model, and until now nothing looked at +# them until a person did: a batch once arrived calling five helper functions +# that the same batch had deleted, and every one of those pull requests +# reported no problems. +# +# `npm test` is the Tier 1 suite: it loads every module, checks it parses and +# imports, and exercises the duplicate-handling and navigation-index logic. +# `npm run lint` adds ESLint's `no-undef` over the extension source, which is +# the check that catches a helper that is called but never defined. +# +# The comparator (`npm run test:compare`) is deliberately not here. It needs a +# reachable 4CAT, an API key and dataset keys, so it stays a local step. +# +# Note on draft pull requests: this workflow DOES run on them, and should. The +# `pull_request` event fires its `opened` type whatever the draft state, and +# 4CAT opens its sync pull requests as drafts on purpose, so filtering drafts +# out with `github.event.pull_request.draft == false` would skip exactly the +# pull requests this is here to check. Those arrive from a GitHub App token +# rather than the default GITHUB_TOKEN, which is what lets them start a +# workflow run at all. + +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Tests and lint + runs-on: ubuntu-latest + defaults: + run: + # The only package.json in the repository lives here. The lint script + # steps up to the root itself, so that ESLint sees the whole extension. + working-directory: tests + steps: + - name: Check out Zeeschuimer + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + cache-dependency-path: tests/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Run linter + run: npm run lint diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..6602883 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,82 @@ +/** + * ESLint configuration for Zeeschuimer. + * + * The one rule enabled is `no-undef`: does this code use a name that nothing + * defines? That is worth checking here because most of what the extension runs + * is loaded as plain background scripts rather than as modules, so the helpers + * in `js/lib.js` are free identifiers everywhere, and nothing else notices when + * one of them goes missing. The Jest suite loads each module and confirms it + * exports `map_item`, which a module referring to an undefined helper passes + * without complaint — the error only appears once `map_item` actually runs, on + * a researcher's machine. + * + * It also covers the `map_item` functions 4CAT generates and syncs in. Those + * are written by a language model, and a helper called but never defined has + * been the single most common way for a batch of them to arrive broken. + * + * The names those scripts share are worked out in `tests/lib-globals.cjs`, + * read from the source so that adding a helper to `js/lib.js` needs no edit + * here. The Jest setup reads the same file, so the two cannot drift apart. + * + * Run it with `npm run lint` from `tests/`. + */ +import { createRequire } from 'node:module'; + +// This file sits at the repository root so that the linter can see every +// script the manifest loads, but the dependencies live in `tests/`, which is +// the only part of Zeeschuimer with a package.json. Resolving from there finds +// both the `globals` package and the shared name list, which is CommonJS +// because the Jest setup file sharing it has to be. +const require = createRequire(new URL('tests/package.json', import.meta.url)); +const globals = require('globals'); +const { ALL_NAMES } = require('./lib-globals.cjs'); + +const zeeschuimer_globals = Object.fromEntries( + ALL_NAMES.map(name => [name, 'readonly']), +); + +export default [ + { + ignores: [ + 'inc/**', // third-party bundles, minified and not ours to fix + '**/node_modules/**', + '.claude/**', // scratch worktrees hold copies of every module + // The test harness is left out on purpose. Running it is a stronger + // check than `no-undef` could be, and most of what lives there is + // not extension code at all: a Firefox profile's prefs.js, and a + // stealth script written to run inside a page rather than in Node. + 'tests/**', + // The popup is left out for now, and not because it is clean: it + // reaches for `streamSaver` and `encode`, which come from scripts + // popup.html loads out of `js/` that are not in the repository at + // all, and it assigns `fileStream` and `writer` without declaring + // them. Those want deciding on their own rather than as part of + // switching a linter on, and until then including this directory + // would leave every run red, which makes the next real failure + // easy to miss. + 'popup/**', + ], + }, + { + // Capture and map_item modules. `modules/package.json` marks these as + // ES modules; they still reach for the background-script globals. + files: ['modules/**/*.js'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, + }, + rules: { 'no-undef': 'error' }, + }, + { + // The background scripts themselves, loaded by the manifest as plain + // scripts rather than as modules. + files: ['js/**/*.js'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'script', + globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, + }, + rules: { 'no-undef': 'error' }, + }, +]; diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs new file mode 100644 index 0000000..ba21f77 --- /dev/null +++ b/tests/lib-globals.cjs @@ -0,0 +1,57 @@ +/** + * The names Zeeschuimer's own scripts put into global scope. + * + * The manifest loads `inc/dexie.js`, `inc/he.js`, `js/lib.js`, + * `js/zs-background.js` and `modules/_loader.js` as plain background scripts, + * so their top-level declarations are shared globals. Module code — and the + * `map_item` functions generated from 4CAT — uses those names without + * declaring or importing anything. + * + * Two things need that list, and they need the same one: + * - `setup-globals.cjs`, which puts the helpers into scope for Jest. + * - `eslint.config.mjs`, which tells `no-undef` that these names exist. + * + * The names are read out of the source rather than typed here, so adding a + * helper to `js/lib.js` makes it available to both without editing this file. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const ROOT = path.join(__dirname, '..'); + +function read(...parts) { + return fs.readFileSync(path.join(ROOT, ...parts), 'utf8'); +} + +// `js/lib.js` declares its helpers unindented at the top level. Requiring +// column 0 keeps nested helpers — such as the `_traverse_data` inside +// `traverse_data` — from being treated as globals. +const LIB_SOURCE = read('js', 'lib.js'); +const LIB_DECLARATION = /^(?:function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/gm; +const LIB_NAMES = Array.from(LIB_SOURCE.matchAll(LIB_DECLARATION), m => m[1]); + +if (LIB_NAMES.length === 0) { + throw new Error( + 'lib-globals.cjs: no top-level function or class declarations found in ' + + 'js/lib.js. The pattern that finds them is broken, and everything ' + + 'relying on those names will fail: tests with a ReferenceError, the ' + + 'linter with a no-undef error on every module.' + ); +} + +// `js/zs-background.js` takes the other shape, assigning onto `window`. +const BACKGROUND_ASSIGNMENT = /^window\.([A-Za-z_$][A-Za-z0-9_$]*)\s*=/gm; +const BACKGROUND_NAMES = Array.from( + read('js', 'zs-background.js').matchAll(BACKGROUND_ASSIGNMENT), + m => m[1], +); + +// `inc/dexie.js` and `inc/he.js` are third-party bundles: minified, and +// wrapped so that nothing about them can be read off the source. Their names +// are written out here, and only change if one of those libraries is swapped. +const VENDORED_NAMES = ['Dexie', 'he']; + +const ALL_NAMES = [...new Set([...LIB_NAMES, ...BACKGROUND_NAMES, ...VENDORED_NAMES])]; + +module.exports = { LIB_SOURCE, LIB_NAMES, BACKGROUND_NAMES, VENDORED_NAMES, ALL_NAMES }; diff --git a/tests/package-lock.json b/tests/package-lock.json index ada8011..90a8bcd 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -11,7 +11,9 @@ "cross-fetch": "^4.0.0", "dexie": "^3.2.4", "dotenv": "^16.4.5", + "eslint": "10.9.1", "fake-indexeddb": "^5.0.1", + "globals": "17.12.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" } @@ -512,6 +514,218 @@ "dev": true, "license": "MIT" }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -963,6 +1177,20 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1012,6 +1240,13 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", @@ -1062,9 +1297,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -1085,6 +1320,16 @@ "acorn-walk": "^8.0.2" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -1111,6 +1356,23 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1706,6 +1968,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1939,6 +2208,216 @@ "source-map": "~0.6.1" } }, + "node_modules/eslint": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1953,6 +2432,32 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -2033,6 +2538,13 @@ "node": ">=18" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2040,6 +2552,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2050,6 +2569,19 @@ "bser": "2.1.1" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2077,6 +2609,27 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -2230,6 +2783,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2374,6 +2953,16 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2446,6 +3035,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2466,6 +3065,19 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3291,6 +3903,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3298,6 +3917,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3311,6 +3944,16 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3331,6 +3974,20 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3607,6 +4264,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3774,6 +4449,16 @@ "node": ">=8" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -4231,6 +4916,19 @@ "node": ">=12" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4302,6 +5000,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -4415,6 +5123,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/tests/package.json b/tests/package.json index 763321c..b8b086b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,13 +6,16 @@ "scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.cjs", "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.cjs --watch", - "test:compare": "node run-compare.mjs" + "test:compare": "node run-compare.mjs", + "lint": "cd .. && eslint ." }, "devDependencies": { "cross-fetch": "^4.0.0", "dexie": "^3.2.4", "dotenv": "^16.4.5", + "eslint": "10.9.1", "fake-indexeddb": "^5.0.1", + "globals": "17.12.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" } diff --git a/tests/setup-globals.cjs b/tests/setup-globals.cjs index b55e659..978447d 100644 --- a/tests/setup-globals.cjs +++ b/tests/setup-globals.cjs @@ -7,39 +7,17 @@ * MissingMappedField, strip_tags, normalize_url_encoding, ...). Without * this shim they'd hit ReferenceError as soon as a test invokes map_item. * - * Names are auto-discovered from lib.js by regex-matching top-level - * `function name(...)` and `class Name ...` declarations. Adding a helper - * to lib.js makes it available to tests without touching this file. + * Which names those are is worked out in lib-globals.cjs, which the ESLint + * config reads as well so both agree on what exists. Only lib.js is evaluated + * here: the other background scripts need a browser to run in, and nothing + * under test calls into them. */ -const fs = require('node:fs'); -const path = require('node:path'); - -const lib_source = fs.readFileSync( - path.join(__dirname, '..', 'js', 'lib.js'), - 'utf8', -); - -// Match `function name(` and `class Name {` / `class Name extends` at -// column 0 of a line. lib.js is a classic script with all top-level -// declarations unindented; requiring column 0 keeps nested helpers (like -// the `_traverse_data` IIFE inside `traverse_data`) from being exposed. -const NAME_PATTERN = /^(?:function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/gm; -const EXPOSED_NAMES = Array.from( - lib_source.matchAll(NAME_PATTERN), - m => m[1], -); - -if (EXPOSED_NAMES.length === 0) { - throw new Error( - 'setup-globals.cjs: no top-level function/class declarations found in js/lib.js — ' + - 'auto-discovery regex may be broken. Tests will ReferenceError if not fixed.' - ); -} +const { LIB_SOURCE, LIB_NAMES } = require('./lib-globals.cjs'); const factory = new Function(` -${lib_source} -return { ${EXPOSED_NAMES.join(', ')} }; +${LIB_SOURCE} +return { ${LIB_NAMES.join(', ')} }; `); Object.assign(globalThis, factory()); From d3df275e9aa5083c13a82b42b8e49ccf3030b4a2 Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 10:28:26 +0200 Subject: [PATCH 02/10] tests/CI: learn about espree and use to discover our declared functions/variables --- tests/lib-globals.cjs | 47 +++++++++++++++++++++++++++++++++-------- tests/package-lock.json | 1 + tests/package.json | 1 + 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs index ba21f77..5d32c10 100644 --- a/tests/lib-globals.cjs +++ b/tests/lib-globals.cjs @@ -17,6 +17,7 @@ const fs = require('node:fs'); const path = require('node:path'); +const espree = require('espree'); const ROOT = path.join(__dirname, '..'); @@ -24,19 +25,47 @@ function read(...parts) { return fs.readFileSync(path.join(ROOT, ...parts), 'utf8'); } -// `js/lib.js` declares its helpers unindented at the top level. Requiring -// column 0 keeps nested helpers — such as the `_traverse_data` inside -// `traverse_data` — from being treated as globals. const LIB_SOURCE = read('js', 'lib.js'); -const LIB_DECLARATION = /^(?:function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/gm; -const LIB_NAMES = Array.from(LIB_SOURCE.matchAll(LIB_DECLARATION), m => m[1]); + +// Identify the names one statement declares. A statement that declares nothing like a +// function call, an assignment, or an if block returns gives back an empty list. +function declared_names(statement) { + function declared_names(statement) { + if (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') { + // functions and classes + return [statement.id.name]; + } + if (statement.type === 'VariableDeclaration') { + // variables + return statement.declarations.map(declaration => { + if (declaration.id.type === 'Identifier') { + return declaration.id.name; + } else { + // multiple names for a single statement + const line = declaration.loc.start.line; + throw new Error( + `lib-globals.cjs: js/lib.js line ${line} declares names in a ` + + 'form this file does not read:\n\n' + + ` ${LIB_SOURCE.split('\n')[line - 1].trim()}\n\n` + + 'Add that form to declared_names(), or declare the names one ' + + 'per line.' + ); + } + }); + } +} + +// Every name js/lib.js declares at its top level. `body` holds the outermost +// statements only, so a helper written inside another one like`_traverse_data` +// inside `traverse_data` is not in the list. +const LIB_NAMES = espree + .parse(LIB_SOURCE, { ecmaVersion: 'latest', sourceType: 'script', loc: true }) + .body.flatMap(declared_names); if (LIB_NAMES.length === 0) { throw new Error( - 'lib-globals.cjs: no top-level function or class declarations found in ' + - 'js/lib.js. The pattern that finds them is broken, and everything ' + - 'relying on those names will fail: tests with a ReferenceError, the ' + - 'linter with a no-undef error on every module.' + 'lib-globals.cjs: found nothing declared at the top level of js/lib.js. ' + + 'The tests and the linter both read this list, and neither works without it.' ); } diff --git a/tests/package-lock.json b/tests/package-lock.json index 90a8bcd..9710341 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -12,6 +12,7 @@ "dexie": "^3.2.4", "dotenv": "^16.4.5", "eslint": "10.9.1", + "espree": "^11.2.0", "fake-indexeddb": "^5.0.1", "globals": "17.12.0", "jest": "^29.7.0", diff --git a/tests/package.json b/tests/package.json index b8b086b..dcd0237 100644 --- a/tests/package.json +++ b/tests/package.json @@ -14,6 +14,7 @@ "dexie": "^3.2.4", "dotenv": "^16.4.5", "eslint": "10.9.1", + "espree": "^11.2.0", "fake-indexeddb": "^5.0.1", "globals": "17.12.0", "jest": "^29.7.0", From 7b962e24ecafdd940e1ee6130a3219efb0c27c7e Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 11:15:44 +0200 Subject: [PATCH 03/10] lib-globals.cjs: read the script from the manifest (instead of hardcoded/regexed) --- tests/lib-globals.cjs | 112 ++++++++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs index 5d32c10..8097936 100644 --- a/tests/lib-globals.cjs +++ b/tests/lib-globals.cjs @@ -1,11 +1,10 @@ /** * The names Zeeschuimer's own scripts put into global scope. * - * The manifest loads `inc/dexie.js`, `inc/he.js`, `js/lib.js`, - * `js/zs-background.js` and `modules/_loader.js` as plain background scripts, - * so their top-level declarations are shared globals. Module code — and the - * `map_item` functions generated from 4CAT — uses those names without - * declaring or importing anything. + * manifest.json loads the scripts under `background` as plain scripts, so what + * they declare at their top level is shared. Module code — and the `map_item` + * functions generated from 4CAT — uses those names without declaring or + * importing anything. * * Two things need that list, and they need the same one: * - `setup-globals.cjs`, which puts the helpers into scope for Jest. @@ -25,12 +24,21 @@ function read(...parts) { return fs.readFileSync(path.join(ROOT, ...parts), 'utf8'); } -const LIB_SOURCE = read('js', 'lib.js'); +// The scripts that share the background page's global scope, in the order the +// browser loads them. +const BACKGROUND_SCRIPTS = JSON.parse(read('manifest.json')).background.scripts; + +// `inc/dexie.js` and `inc/he.js` are third-party bundles: minified, and wrapped +// so that nothing about them can be read off the source. Their names are written +// out here against the path the manifest loads them from. +const VENDORED_NAMES = { + 'inc/dexie.js': ['Dexie'], + 'inc/he.js': ['he'], +}; // Identify the names one statement declares. A statement that declares nothing like a -// function call, an assignment, or an if block returns gives back an empty list. -function declared_names(statement) { - function declared_names(statement) { +// function call, an assignment, or an if block gives back an empty list. +function declared_names(statement, script, source) { if (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') { // functions and classes return [statement.id.name]; @@ -38,29 +46,59 @@ function declared_names(statement) { if (statement.type === 'VariableDeclaration') { // variables return statement.declarations.map(declaration => { - if (declaration.id.type === 'Identifier') { - return declaration.id.name; - } else { - // multiple names for a single statement - const line = declaration.loc.start.line; - throw new Error( - `lib-globals.cjs: js/lib.js line ${line} declares names in a ` + - 'form this file does not read:\n\n' + - ` ${LIB_SOURCE.split('\n')[line - 1].trim()}\n\n` + - 'Add that form to declared_names(), or declare the names one ' + - 'per line.' - ); - } - }); + if (declaration.id.type === 'Identifier') { + return declaration.id.name; + } else { + const line = declaration.loc.start.line; + throw new Error( + `lib-globals.cjs: ${script} line ${line} declares names in a ` + + 'form this file does not read:\n\n' + + ` ${source.split('\n')[line - 1].trim()}\n\n` + + 'Add that form to declared_names(), or declare the names one ' + + 'per line.' + ); + } + }); } + return []; } -// Every name js/lib.js declares at its top level. `body` holds the outermost -// statements only, so a helper written inside another one like`_traverse_data` +// Identify the name one statement hangs off `window`. js/zs-background.js opens +// with `window.db = new Dexie(...)` and `window.zeeschuimer = {...}`, and this +// picks `db` and `zeeschuimer` out of them. +function assigned_names(statement) { + // First drop anything that is not an assignment + if (statement.expression?.type !== 'AssignmentExpression') { + return []; + } + + // Then take the name after the dot in `window.`. Only that spelling: the + // browser treats `window['db'] = ...` and `window[key] = ...` the same way, + // but the first hides the name inside a string and the second does not have + // one in the file at all. Neither appears in js/zs-background.js. + const target = statement.expression.left; + if (!target.computed && target.object?.name === 'window') { + return [target.property.name]; + } + + return []; +} + +// Every name a script puts into global scope. `body` holds the outermost +// statements only, so a helper written inside another one like `_traverse_data` // inside `traverse_data` is not in the list. -const LIB_NAMES = espree - .parse(LIB_SOURCE, { ecmaVersion: 'latest', sourceType: 'script', loc: true }) - .body.flatMap(declared_names); +function global_names(script, source) { + return espree + .parse(source, { ecmaVersion: 'latest', sourceType: 'script', loc: true }) + .body.flatMap(statement => [ + ...declared_names(statement, script, source), + ...assigned_names(statement), + ]); +} + +// setup-globals.cjs evaluates js/lib.js and pulls these names back out of it. +const LIB_SOURCE = read('js/lib.js'); +const LIB_NAMES = global_names('js/lib.js', LIB_SOURCE); if (LIB_NAMES.length === 0) { throw new Error( @@ -69,18 +107,8 @@ if (LIB_NAMES.length === 0) { ); } -// `js/zs-background.js` takes the other shape, assigning onto `window`. -const BACKGROUND_ASSIGNMENT = /^window\.([A-Za-z_$][A-Za-z0-9_$]*)\s*=/gm; -const BACKGROUND_NAMES = Array.from( - read('js', 'zs-background.js').matchAll(BACKGROUND_ASSIGNMENT), - m => m[1], -); - -// `inc/dexie.js` and `inc/he.js` are third-party bundles: minified, and -// wrapped so that nothing about them can be read off the source. Their names -// are written out here, and only change if one of those libraries is swapped. -const VENDORED_NAMES = ['Dexie', 'he']; - -const ALL_NAMES = [...new Set([...LIB_NAMES, ...BACKGROUND_NAMES, ...VENDORED_NAMES])]; +const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap( + script => VENDORED_NAMES[script] ?? global_names(script, read(script)) +))]; -module.exports = { LIB_SOURCE, LIB_NAMES, BACKGROUND_NAMES, VENDORED_NAMES, ALL_NAMES }; +module.exports = { LIB_SOURCE, LIB_NAMES, ALL_NAMES }; From 79856dde21fe6cfee4b15119aca9c5fdef43dc24 Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 11:34:52 +0200 Subject: [PATCH 04/10] lib-globals.cjs: add some guards for funny business --- tests/lib-globals.cjs | 45 +++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs index 8097936..9154be2 100644 --- a/tests/lib-globals.cjs +++ b/tests/lib-globals.cjs @@ -26,7 +26,14 @@ function read(...parts) { // The scripts that share the background page's global scope, in the order the // browser loads them. -const BACKGROUND_SCRIPTS = JSON.parse(read('manifest.json')).background.scripts; +const BACKGROUND_SCRIPTS = JSON.parse(read('manifest.json')).background?.scripts; + +if (!Array.isArray(BACKGROUND_SCRIPTS) || BACKGROUND_SCRIPTS.length === 0) { + throw new Error( + 'lib-globals.cjs: manifest.json has no background.scripts for this file to ' + + 'read.' + ); +} // `inc/dexie.js` and `inc/he.js` are third-party bundles: minified, and wrapped // so that nothing about them can be read off the source. Their names are written @@ -36,6 +43,15 @@ const VENDORED_NAMES = { 'inc/he.js': ['he'], }; +const unloaded = Object.keys(VENDORED_NAMES).filter(script => !BACKGROUND_SCRIPTS.includes(script)); +if (unloaded.length > 0) { + throw new Error( + `lib-globals.cjs: ${unloaded.join(', ')} named above, but manifest.json does ` + + 'not load it. A renamed file or a swapped-out library leaves an entry here ' + + 'that no longer does anything; drop it, or correct the path.' + ); +} + // Identify the names one statement declares. A statement that declares nothing like a // function call, an assignment, or an if block gives back an empty list. function declared_names(statement, script, source) { @@ -100,15 +116,24 @@ function global_names(script, source) { const LIB_SOURCE = read('js/lib.js'); const LIB_NAMES = global_names('js/lib.js', LIB_SOURCE); -if (LIB_NAMES.length === 0) { - throw new Error( - 'lib-globals.cjs: found nothing declared at the top level of js/lib.js. ' + - 'The tests and the linter both read this list, and neither works without it.' - ); -} +// Every script is expected to put something into global scope, so one that +// contributes nothing probably means this file could not read it rather than that +// there was nothing to find. +const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap(script => { + if (script in VENDORED_NAMES) { + return VENDORED_NAMES[script]; + } -const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap( - script => VENDORED_NAMES[script] ?? global_names(script, read(script)) -))]; + const names = global_names(script, read(script)); + if (names.length === 0) { + throw new Error( + `lib-globals.cjs: manifest.json loads ${script}, but no global names ` + + 'could be read out of it. Add them to VENDORED_NAMES above, keyed by ' + + `'${script}' or, if it really does declare nothing, list it there ` + + 'with an empty array.' + ); + } + return names; +}))]; module.exports = { LIB_SOURCE, LIB_NAMES, ALL_NAMES }; From 199dd3a6fc027b8ca42aca9f2f1488c800978e1d Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 12:35:48 +0200 Subject: [PATCH 05/10] eslint: derive better scripts vs modules --- eslint.config.mjs | 31 ++++++++++++++----------------- tests/lib-globals.cjs | 24 +++++++++++++++++------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6602883..94d85e8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,7 +29,7 @@ import { createRequire } from 'node:module'; // because the Jest setup file sharing it has to be. const require = createRequire(new URL('tests/package.json', import.meta.url)); const globals = require('globals'); -const { ALL_NAMES } = require('./lib-globals.cjs'); +const { ALL_NAMES, BACKGROUND_SCRIPTS } = require('./lib-globals.cjs'); const zeeschuimer_globals = Object.fromEntries( ALL_NAMES.map(name => [name, 'readonly']), @@ -39,21 +39,16 @@ export default [ { ignores: [ 'inc/**', // third-party bundles, minified and not ours to fix - '**/node_modules/**', '.claude/**', // scratch worktrees hold copies of every module - // The test harness is left out on purpose. Running it is a stronger - // check than `no-undef` could be, and most of what lives there is - // not extension code at all: a Firefox profile's prefs.js, and a - // stealth script written to run inside a page rather than in Node. + // The test harness is left out. Most of it is not extension code — a + // Firefox profile's prefs.js, a stealth script written to run inside a + // page — and what is gets loaded and run by `npm test`. 'tests/**', - // The popup is left out for now, and not because it is clean: it - // reaches for `streamSaver` and `encode`, which come from scripts - // popup.html loads out of `js/` that are not in the repository at - // all, and it assigns `fileStream` and `writer` without declaring - // them. Those want deciding on their own rather than as part of - // switching a linter on, and until then including this directory - // would leave every run red, which makes the next real failure - // easy to miss. + // The popup is left out. `popup/interface.js` uses `init_tooltips` from + // `popup/tooltips.js`, a separate script `popup/interface.html` loads + // into the same scope, and nothing here works out that list. The other + // names no-undef reports there are inside `download_blob`, which the + // file marks as unused. 'popup/**', ], }, @@ -61,6 +56,7 @@ export default [ // Capture and map_item modules. `modules/package.json` marks these as // ES modules; they still reach for the background-script globals. files: ['modules/**/*.js'], + ignores: BACKGROUND_SCRIPTS, languageOptions: { ecmaVersion: 'latest', sourceType: 'module', @@ -69,9 +65,10 @@ export default [ rules: { 'no-undef': 'error' }, }, { - // The background scripts themselves, loaded by the manifest as plain - // scripts rather than as modules. - files: ['js/**/*.js'], + // Plain scripts rather than modules: everything manifest.json lists under + // `background`, which is how `modules/_loader.js` lands here rather than + // above, plus anything else in `js/`. + files: ['js/**/*.js', ...BACKGROUND_SCRIPTS], languageOptions: { ecmaVersion: 'latest', sourceType: 'script', diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs index 9154be2..1b55f99 100644 --- a/tests/lib-globals.cjs +++ b/tests/lib-globals.cjs @@ -104,12 +104,22 @@ function assigned_names(statement) { // statements only, so a helper written inside another one like `_traverse_data` // inside `traverse_data` is not in the list. function global_names(script, source) { - return espree - .parse(source, { ecmaVersion: 'latest', sourceType: 'script', loc: true }) - .body.flatMap(statement => [ - ...declared_names(statement, script, source), - ...assigned_names(statement), - ]); + let parsed; + try { + parsed = espree.parse(source, { ecmaVersion: 'latest', sourceType: 'script', loc: true }); + } catch (error) { + throw new Error( + `lib-globals.cjs: cannot read ${script}, which manifest.json loads as a ` + + `plain script:\n\n line ${error.lineNumber}: ${error.message}\n\n` + + 'A background script cannot use import or export. If that is the problem ' + + 'here, the browser will not load the file either.' + ); + } + + return parsed.body.flatMap(statement => [ + ...declared_names(statement, script, source), + ...assigned_names(statement), + ]); } // setup-globals.cjs evaluates js/lib.js and pulls these names back out of it. @@ -136,4 +146,4 @@ const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap(script => { return names; }))]; -module.exports = { LIB_SOURCE, LIB_NAMES, ALL_NAMES }; +module.exports = { BACKGROUND_SCRIPTS, LIB_SOURCE, LIB_NAMES, ALL_NAMES }; From a822710cc2ddfb6d97f46715e0c9f968dbd84515 Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 13:21:54 +0200 Subject: [PATCH 06/10] esling: pin the linter (so we decide is we want to change it), use recommended rules (but exclude some style stuff so map_item isn't a pain); mini gab style change --- eslint.config.mjs | 43 +++++++++++++++++++++++++++++++---------- modules/gab.js | 1 - tests/package-lock.json | 22 +++++++++++++++++++++ tests/package.json | 1 + 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 94d85e8..84c62ff 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,15 @@ /** * ESLint configuration for Zeeschuimer. * - * The one rule enabled is `no-undef`: does this code use a name that nothing - * defines? That is worth checking here because most of what the extension runs - * is loaded as plain background scripts rather than as modules, so the helpers - * in `js/lib.js` are free identifiers everywhere, and nothing else notices when - * one of them goes missing. The Jest suite loads each module and confirms it - * exports `map_item`, which a module referring to an undefined helper passes - * without complaint — the error only appears once `map_item` actually runs, on - * a researcher's machine. + * The rules are ESLint's recommended set, with the handful listed further down + * switched off. The one doing most of the work is `no-undef`: does this code use + * a name that nothing defines? That is worth checking here because most of what + * the extension runs is loaded as plain background scripts rather than as + * modules, so the helpers in `js/lib.js` are free identifiers everywhere, and + * nothing else notices when one of them goes missing. The Jest suite loads each + * module and confirms it exports `map_item`, which a module referring to an + * undefined helper passes without complaint — the error only appears once + * `map_item` actually runs, on a researcher's machine. * * It also covers the `map_item` functions 4CAT generates and syncs in. Those * are written by a language model, and a helper called but never defined has @@ -29,12 +30,34 @@ import { createRequire } from 'node:module'; // because the Jest setup file sharing it has to be. const require = createRequire(new URL('tests/package.json', import.meta.url)); const globals = require('globals'); +const js = require('@eslint/js'); const { ALL_NAMES, BACKGROUND_SCRIPTS } = require('./lib-globals.cjs'); const zeeschuimer_globals = Object.fromEntries( ALL_NAMES.map(name => [name, 'readonly']), ); +const rules = { + ...js.configs.recommended.rules, + + // Every empty block in the codebase is a `catch` that means it. + 'no-empty': ['error', { allowEmptyCatch: true }], + + // Off because the `map_item` bodies 4CAT generates trip them, and a sync + // replaces those blocks whole, so a fix here does not survive one. None of + // them change what the code does. + 'no-extra-boolean-cast': 'off', // !!value ? "yes" : "no" + 'no-redeclare': 'off', // the same `var` declared twice in one function + 'no-unused-vars': 'off', // variables assigned and then never read + 'no-useless-assignment': 'off', // a value replaced before anything reads it + 'no-useless-escape': 'off', // \[ and \/ inside a character class + + // Off for now, and this one is not cosmetic: `obj.hasOwnProperty(key)` + // throws if the JSON a platform sent has a key of that name. The fix is + // `Object.hasOwn(obj, key)` at 31 places across js/ and modules/. + 'no-prototype-builtins': 'off', +}; + export default [ { ignores: [ @@ -62,7 +85,7 @@ export default [ sourceType: 'module', globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, }, - rules: { 'no-undef': 'error' }, + rules, }, { // Plain scripts rather than modules: everything manifest.json lists under @@ -74,6 +97,6 @@ export default [ sourceType: 'script', globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, }, - rules: { 'no-undef': 'error' }, + rules, }, ]; diff --git a/modules/gab.js b/modules/gab.js index 9e8b4e2..12a5f2e 100644 --- a/modules/gab.js +++ b/modules/gab.js @@ -66,7 +66,6 @@ export function capture(response, source_platform_url, source_url) { /// capture posts in search if (source_url.indexOf('search?') >= 0 && data.statuses && Array.isArray(data.statuses)) { for (let post of data.statuses) { - post["id"] = post.id; post["c"] = removeHtmlTagsUsingDOMParser(post.content); items.push(post); } diff --git a/tests/package-lock.json b/tests/package-lock.json index 9710341..ec1e7b3 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -8,6 +8,7 @@ "name": "zeeschuimer-db-tests", "version": "1.0.0", "devDependencies": { + "@eslint/js": "10.0.1", "cross-fetch": "^4.0.0", "dexie": "^3.2.4", "dotenv": "^16.4.5", @@ -637,6 +638,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, "node_modules/@eslint/object-schema": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", diff --git a/tests/package.json b/tests/package.json index dcd0237..d98441e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -10,6 +10,7 @@ "lint": "cd .. && eslint ." }, "devDependencies": { + "@eslint/js": "10.0.1", "cross-fetch": "^4.0.0", "dexie": "^3.2.4", "dotenv": "^16.4.5", From 299b60e2431eeec91f202fdd348044c410d9f65e Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 13:50:40 +0200 Subject: [PATCH 07/10] update ci to not allow multiple runs at once and add a timeout just in case --- .github/workflows/ci.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 470dbf0..3670587 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,9 @@ # # `npm test` is the Tier 1 suite: it loads every module, checks it parses and # imports, and exercises the duplicate-handling and navigation-index logic. -# `npm run lint` adds ESLint's `no-undef` over the extension source, which is -# the check that catches a helper that is called but never defined. +# `npm run lint` runs ESLint's recommended rules over the extension source. The +# one that matters most here is `no-undef`, which catches a helper that is +# called but never defined. # # The comparator (`npm run test:compare`) is deliberately not here. It needs a # reachable 4CAT, an API key and dataset keys, so it stays a local step. @@ -34,14 +35,24 @@ on: permissions: contents: read +# One run per branch: pushing again to a pull request cancels the run still +# going for the commit before it. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: name: Tests and lint runs-on: ubuntu-latest + # The whole job takes well under a minute; this is only here so that a hung + # step gives the runner back rather than holding it for the default six hours. + timeout-minutes: 10 defaults: run: - # The only package.json in the repository lives here. The lint script - # steps up to the root itself, so that ESLint sees the whole extension. + # The only package.json with dependencies and scripts lives here. The + # lint script steps up to the root itself, so that ESLint sees the whole + # extension. working-directory: tests steps: - name: Check out Zeeschuimer @@ -60,5 +71,8 @@ jobs: - name: Run tests run: npm test + # Runs even when the tests above failed: a batch that breaks a test often + # breaks the linter too, and reporting both saves a round trip. - name: Run linter + if: '!cancelled()' run: npm run lint From 2f34ec9b8b6d2218da01a8e6ff075697964a505f Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 13:56:05 +0200 Subject: [PATCH 08/10] remove dockerfile.test and update node version --- tests/Dockerfile.test | 16 ---------------- tests/README.md | 2 +- tests/package.json | 3 +++ 3 files changed, 4 insertions(+), 17 deletions(-) delete mode 100644 tests/Dockerfile.test diff --git a/tests/Dockerfile.test b/tests/Dockerfile.test deleted file mode 100644 index bf574fd..0000000 --- a/tests/Dockerfile.test +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:18-alpine - -WORKDIR /app/tests - -# Enable dynamic ESM import support for Jest -ENV NODE_OPTIONS=--experimental-vm-modules - -# Copy test files -COPY package.json jest.config.js ./ -COPY duplicate-behavior.test.js ./ - -# Install dependencies -RUN npm install - -# Run tests -CMD ["npm", "test"] diff --git a/tests/README.md b/tests/README.md index f203b60..a0efd8a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -58,7 +58,7 @@ Tests are defined in `tests.json` with the following structure: ### Jest suites **Prerequisites** -- Node.js (v18 or later) and npm +- Node.js (v20.19 or later, the floor ESLint sets) and npm - `cd tests && npm install` **Recommended: develop the tests inside Docker.** On Windows the global diff --git a/tests/package.json b/tests/package.json index d98441e..d62398c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -20,5 +20,8 @@ "globals": "17.12.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } } From 2133e8e3dbefc6b54860123d02566ddc9bb8170a Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 14:29:07 +0200 Subject: [PATCH 09/10] update readme about linter --- tests/README.md | 12 ++++++++++-- tests/setup-globals.cjs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/README.md b/tests/README.md index a0efd8a..b3e0314 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,15 @@ ## Tests for Zeeschuimer -This folder contains testing code for Zeeschuimer. There are three suites, -each with a different purpose and a different runtime environment: +This folder contains testing code for Zeeschuimer. Each check below has a +different purpose and a different runtime environment: | Suite | Tests | Environment | When it runs | Needs | |----------------------------------|-----------------------------------------------------------|--------------------|---------------------------------|----------------------------------------| | Selenium integration | Page captures real items from each supported platform | Real Firefox | Reviewer-supervised, manual | Firefox profile, sometimes a human | | Duplicate-behavior unit (Jest) | DB merge / keep / update semantics in isolation | jsdom + fake-IDB | `npm test` (every push) | None | | Module load smoke (Jest, Tier 1) | Each `modules/*.js` parses and imports cleanly | jsdom | `npm test` (every push) | None | +| Navigation index (Jest) | Tab and navigation bookkeeping in `js/zs-background.js` | jsdom + fake-IDB | `npm test` (every push) | None | +| ESLint | Every name `js/` and `modules/` use is defined somewhere | Node | `npm run lint` (every push) | None | | `map_item` comparator (Jest, Tier 2) | JS `map_item` output matches 4CAT's Python mapping per item | jsdom + cross-fetch | `npm run test:compare` (on demand) | Live 4CAT, API key, dataset key(s) | Hermetic suites (no external dependencies) live in `npm test`. Anything that @@ -151,6 +153,9 @@ npm test # watch mode for the same npm run test:watch +# ESLint over js/ and modules/ +npm run lint + # the comparator — every dataset key in FOURCAT_DATASETS npm run test:compare @@ -171,6 +176,9 @@ npm run test:compare -- --all comparator. Add a dataset to `FOURCAT_DATASETS` that covers the case; the comparator will pick it up. - **End-to-end user flow in the extension.** Selenium. +- **A name that nothing defines** — a helper a `map_item` calls but no file + declares. Nothing to add: ESLint's `no-undef` covers every name in `js/` and + `modules/` already. ### Why the environments differ diff --git a/tests/setup-globals.cjs b/tests/setup-globals.cjs index 978447d..101a957 100644 --- a/tests/setup-globals.cjs +++ b/tests/setup-globals.cjs @@ -9,8 +9,8 @@ * * Which names those are is worked out in lib-globals.cjs, which the ESLint * config reads as well so both agree on what exists. Only lib.js is evaluated - * here: the other background scripts need a browser to run in, and nothing - * under test calls into them. + * here; nav-index.test.js evaluates js/zs-background.js itself, with the browser + * API stubs that file needs. */ const { LIB_SOURCE, LIB_NAMES } = require('./lib-globals.cjs'); From f90697554737e75574db8bcd2efa18271ace1b43 Mon Sep 17 00:00:00 2001 From: Dale Wahl Date: Wed, 9 Sep 2026 15:30:23 +0200 Subject: [PATCH 10/10] clean up all my comments --- .github/workflows/ci.yml | 42 ++++++++------------------ create-zip-bash.sh | 2 +- create-zip.sh | 2 +- eslint.config.mjs | 65 ++++++++++++++++------------------------ tests/lib-globals.cjs | 25 +++++++--------- tests/setup-globals.cjs | 18 +++++------ 6 files changed, 59 insertions(+), 95 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3670587..3a63db4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,14 @@ -# Run the test suite and the linter on every push and pull request. +# Runs the test suite and the linter on every push and pull request. # -# This is the first automation in this repository, and it exists mainly for the -# pull requests 4CAT opens automatically. Those carry `map_item` functions -# translated from Python by a language model, and until now nothing looked at -# them until a person did: a batch once arrived calling five helper functions -# that the same batch had deleted, and every one of those pull requests -# reported no problems. +# `npm test` loads every module, checks it parses and imports, and exercises the +# duplicate-handling and navigation-index logic. `npm run lint` runs ESLint over +# the extension source. # -# `npm test` is the Tier 1 suite: it loads every module, checks it parses and -# imports, and exercises the duplicate-handling and navigation-index logic. -# `npm run lint` runs ESLint's recommended rules over the extension source. The -# one that matters most here is `no-undef`, which catches a helper that is -# called but never defined. +# The comparator (`npm run test:compare`) is not here. It needs a reachable +# 4CAT, an API key and dataset keys, so it stays a local step. # -# The comparator (`npm run test:compare`) is deliberately not here. It needs a -# reachable 4CAT, an API key and dataset keys, so it stays a local step. -# -# Note on draft pull requests: this workflow DOES run on them, and should. The -# `pull_request` event fires its `opened` type whatever the draft state, and -# 4CAT opens its sync pull requests as drafts on purpose, so filtering drafts -# out with `github.event.pull_request.draft == false` would skip exactly the -# pull requests this is here to check. Those arrive from a GitHub App token -# rather than the default GITHUB_TOKEN, which is what lets them start a -# workflow run at all. +# This runs on draft pull requests too. 4CAT opens its map_item sync pull +# requests as drafts, and those are the ones this is here to check. name: Tests @@ -35,8 +21,7 @@ on: permissions: contents: read -# One run per branch: pushing again to a pull request cancels the run still -# going for the commit before it. +# Pushing again to a branch cancels the run still going for the commit before it. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -45,14 +30,12 @@ jobs: test: name: Tests and lint runs-on: ubuntu-latest - # The whole job takes well under a minute; this is only here so that a hung - # step gives the runner back rather than holding it for the default six hours. + # The job takes well under a minute. This caps a hung step. timeout-minutes: 10 defaults: run: # The only package.json with dependencies and scripts lives here. The - # lint script steps up to the root itself, so that ESLint sees the whole - # extension. + # lint script steps up to the root itself. working-directory: tests steps: - name: Check out Zeeschuimer @@ -71,8 +54,7 @@ jobs: - name: Run tests run: npm test - # Runs even when the tests above failed: a batch that breaks a test often - # breaks the linter too, and reporting both saves a round trip. + # Runs even when the tests above failed, so one run reports both. - name: Run linter if: '!cancelled()' run: npm run lint diff --git a/create-zip-bash.sh b/create-zip-bash.sh index 3c03fca..437da50 100644 --- a/create-zip-bash.sh +++ b/create-zip-bash.sh @@ -2,4 +2,4 @@ VERSION=$(grep '"version"' manifest.json | cut -d'"' -f 4) sed -i -E "s/\"version\": \"v[^\"]+\"/\"version\": \"v$VERSION\"/g" .zenodo.json sed -i -E "s/v[0-9]+\.[0-9]+\.[0-9]+/v$VERSION/g" popup/interface.html -zip -r zeeschuimer-v$VERSION.zip . -x "*.DS_Store" "__MACOSX" js/mitm.js js/ponyfill-2.0.2.js js/streamsaver-2.0.3.js js/webtorrent.min.js -x "*.git*" -x "*.idea*" -x "create-zip.sh" -x "*.zip" -x "*.xpi" -x "tests*" -x "images/zeeschuimer-full.png" -x "images/chirico-full.png" -x "images/example_screenshot.png" +zip -r zeeschuimer-v$VERSION.zip . -x "*.DS_Store" "__MACOSX" js/mitm.js js/ponyfill-2.0.2.js js/streamsaver-2.0.3.js js/webtorrent.min.js -x "*.git*" -x "*.idea*" -x "create-zip.sh" -x "*.zip" -x "*.xpi" -x "tests*" -x "eslint.config.mjs" -x "images/zeeschuimer-full.png" -x "images/chirico-full.png" -x "images/example_screenshot.png" diff --git a/create-zip.sh b/create-zip.sh index 9e39c6a..ada1e53 100755 --- a/create-zip.sh +++ b/create-zip.sh @@ -2,4 +2,4 @@ VERSION=$(grep '"version"' manifest.json | cut -d'"' -f 4); sed -I '' -E "s/\"version\": \"v[^\"]+\"/\"version\": \"v$VERSION\"/g" .zenodo.json sed -I '' -E "s/v[0-9]+\.[0-9]+\.[0-9]+/v$VERSION/g" popup/interface.html -zip -r zeeschuimer-v$VERSION.zip . -x "*.DS_Store" "__MACOSX" js/mitm.js js/ponyfill-2.0.2.js js/streamsaver-2.0.3.js js/webtorrent.min.js -x "*.git*" -x "*.idea*" -x "*.sh" -x "*.zip" -x "*.xpi" -x "tests*" -x "images/zeeschuimer-full.png" -x "images/chirico-full.png" -x "images/example_screenshot.png" \ No newline at end of file +zip -r zeeschuimer-v$VERSION.zip . -x "*.DS_Store" "__MACOSX" js/mitm.js js/ponyfill-2.0.2.js js/streamsaver-2.0.3.js js/webtorrent.min.js -x "*.git*" -x "*.idea*" -x "*.sh" -x "*.zip" -x "*.xpi" -x "tests*" -x "eslint.config.mjs" -x "images/zeeschuimer-full.png" -x "images/chirico-full.png" -x "images/example_screenshot.png" \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 84c62ff..1cf6d73 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,33 +1,23 @@ /** * ESLint configuration for Zeeschuimer. * - * The rules are ESLint's recommended set, with the handful listed further down - * switched off. The one doing most of the work is `no-undef`: does this code use - * a name that nothing defines? That is worth checking here because most of what - * the extension runs is loaded as plain background scripts rather than as - * modules, so the helpers in `js/lib.js` are free identifiers everywhere, and - * nothing else notices when one of them goes missing. The Jest suite loads each - * module and confirms it exports `map_item`, which a module referring to an - * undefined helper passes without complaint — the error only appears once - * `map_item` actually runs, on a researcher's machine. + * The rules are ESLint's recommended set, with the ones listed below switched + * off. The one doing most of the work is `no-undef`: manifest.json loads most + * of the extension as plain background scripts sharing one global scope, so the + * helpers in `js/lib.js` are free identifiers everywhere, and nothing else + * notices when one goes missing. That covers the `map_item` functions 4CAT + * generates and syncs in, where a helper called but never defined has been the + * most common way for a batch to arrive broken. * - * It also covers the `map_item` functions 4CAT generates and syncs in. Those - * are written by a language model, and a helper called but never defined has - * been the single most common way for a batch of them to arrive broken. - * - * The names those scripts share are worked out in `tests/lib-globals.cjs`, - * read from the source so that adding a helper to `js/lib.js` needs no edit - * here. The Jest setup reads the same file, so the two cannot drift apart. + * The names those scripts share come from `tests/lib-globals.cjs`, which the + * Jest setup reads as well. * * Run it with `npm run lint` from `tests/`. */ import { createRequire } from 'node:module'; -// This file sits at the repository root so that the linter can see every -// script the manifest loads, but the dependencies live in `tests/`, which is -// the only part of Zeeschuimer with a package.json. Resolving from there finds -// both the `globals` package and the shared name list, which is CommonJS -// because the Jest setup file sharing it has to be. +// This file sits at the repository root, and the dependencies live in `tests/`. +// Resolving from there finds both the `globals` package and the shared name list. const require = createRequire(new URL('tests/package.json', import.meta.url)); const globals = require('globals'); const js = require('@eslint/js'); @@ -43,18 +33,17 @@ const rules = { // Every empty block in the codebase is a `catch` that means it. 'no-empty': ['error', { allowEmptyCatch: true }], - // Off because the `map_item` bodies 4CAT generates trip them, and a sync - // replaces those blocks whole, so a fix here does not survive one. None of - // them change what the code does. + // Off: the `map_item` bodies 4CAT generates trip these, and a sync replaces + // those blocks whole. None of them change what the code does. 'no-extra-boolean-cast': 'off', // !!value ? "yes" : "no" 'no-redeclare': 'off', // the same `var` declared twice in one function 'no-unused-vars': 'off', // variables assigned and then never read 'no-useless-assignment': 'off', // a value replaced before anything reads it 'no-useless-escape': 'off', // \[ and \/ inside a character class - // Off for now, and this one is not cosmetic: `obj.hasOwnProperty(key)` - // throws if the JSON a platform sent has a key of that name. The fix is - // `Object.hasOwn(obj, key)` at 31 places across js/ and modules/. + // Off, but not cosmetic: `obj.hasOwnProperty(key)` throws if the JSON a + // platform sent has a key of that name. `Object.hasOwn(obj, key)` is the + // fix, at 31 places across js/ and modules/. 'no-prototype-builtins': 'off', }; @@ -63,21 +52,19 @@ export default [ ignores: [ 'inc/**', // third-party bundles, minified and not ours to fix '.claude/**', // scratch worktrees hold copies of every module - // The test harness is left out. Most of it is not extension code — a - // Firefox profile's prefs.js, a stealth script written to run inside a - // page — and what is gets loaded and run by `npm test`. + // Not extension code: a Firefox profile's prefs.js, a stealth script + // written to run inside a page. What is, `npm test` runs. 'tests/**', - // The popup is left out. `popup/interface.js` uses `init_tooltips` from - // `popup/tooltips.js`, a separate script `popup/interface.html` loads - // into the same scope, and nothing here works out that list. The other - // names no-undef reports there are inside `download_blob`, which the - // file marks as unused. + // `popup/interface.js` uses `init_tooltips` from `popup/tooltips.js`, + // a separate script `popup/interface.html` loads into the same scope, + // and nothing here works out that list. The other names no-undef + // reports there are inside `download_blob`, which the file marks unused. 'popup/**', ], }, { // Capture and map_item modules. `modules/package.json` marks these as - // ES modules; they still reach for the background-script globals. + // ES modules; they still use the background-script globals. files: ['modules/**/*.js'], ignores: BACKGROUND_SCRIPTS, languageOptions: { @@ -88,9 +75,9 @@ export default [ rules, }, { - // Plain scripts rather than modules: everything manifest.json lists under - // `background`, which is how `modules/_loader.js` lands here rather than - // above, plus anything else in `js/`. + // Plain scripts: everything manifest.json lists under `background`, which + // is how `modules/_loader.js` lands here rather than above, plus the rest + // of `js/`. files: ['js/**/*.js', ...BACKGROUND_SCRIPTS], languageOptions: { ecmaVersion: 'latest', diff --git a/tests/lib-globals.cjs b/tests/lib-globals.cjs index 1b55f99..fdf0b53 100644 --- a/tests/lib-globals.cjs +++ b/tests/lib-globals.cjs @@ -6,12 +6,12 @@ * functions generated from 4CAT — uses those names without declaring or * importing anything. * - * Two things need that list, and they need the same one: + * Two things read that list: * - `setup-globals.cjs`, which puts the helpers into scope for Jest. * - `eslint.config.mjs`, which tells `no-undef` that these names exist. * - * The names are read out of the source rather than typed here, so adding a - * helper to `js/lib.js` makes it available to both without editing this file. + * The names come out of the source, so adding a helper to `js/lib.js` needs no + * edit here. */ const fs = require('node:fs'); @@ -24,8 +24,7 @@ function read(...parts) { return fs.readFileSync(path.join(ROOT, ...parts), 'utf8'); } -// The scripts that share the background page's global scope, in the order the -// browser loads them. +// The scripts that share the background page's global scope. const BACKGROUND_SCRIPTS = JSON.parse(read('manifest.json')).background?.scripts; if (!Array.isArray(BACKGROUND_SCRIPTS) || BACKGROUND_SCRIPTS.length === 0) { @@ -79,19 +78,17 @@ function declared_names(statement, script, source) { return []; } -// Identify the name one statement hangs off `window`. js/zs-background.js opens -// with `window.db = new Dexie(...)` and `window.zeeschuimer = {...}`, and this -// picks `db` and `zeeschuimer` out of them. +// Identify the name one statement hangs off `window`, as js/zs-background.js +// does with `window.db = new Dexie(...)` and `window.zeeschuimer = {...}`. function assigned_names(statement) { // First drop anything that is not an assignment if (statement.expression?.type !== 'AssignmentExpression') { return []; } - // Then take the name after the dot in `window.`. Only that spelling: the - // browser treats `window['db'] = ...` and `window[key] = ...` the same way, - // but the first hides the name inside a string and the second does not have - // one in the file at all. Neither appears in js/zs-background.js. + // Then take the name after the dot in `window.`. Only that spelling: + // `window['db']` hides the name in a string and `window[key]` has no name in + // the file at all. Neither appears in js/zs-background.js. const target = statement.expression.left; if (!target.computed && target.object?.name === 'window') { return [target.property.name]; @@ -127,8 +124,8 @@ const LIB_SOURCE = read('js/lib.js'); const LIB_NAMES = global_names('js/lib.js', LIB_SOURCE); // Every script is expected to put something into global scope, so one that -// contributes nothing probably means this file could not read it rather than that -// there was nothing to find. +// contributes nothing probably means this file could not read it rather than +// that there was nothing to find. const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap(script => { if (script in VENDORED_NAMES) { return VENDORED_NAMES[script]; diff --git a/tests/setup-globals.cjs b/tests/setup-globals.cjs index 101a957..56915c1 100644 --- a/tests/setup-globals.cjs +++ b/tests/setup-globals.cjs @@ -1,16 +1,14 @@ /** - * Make js/lib.js's helpers available as globals inside the Jest test - * environment, mirroring how the browser sees them after the manifest - * loads lib.js as a plain script. + * Put js/lib.js's helpers into global scope for Jest, the way the browser sees + * them after the manifest loads lib.js as a plain script. * - * map_item bodies reference these as free identifiers (MappedItem, - * MissingMappedField, strip_tags, normalize_url_encoding, ...). Without - * this shim they'd hit ReferenceError as soon as a test invokes map_item. + * `map_item` bodies use these as free identifiers — MappedItem, + * MissingMappedField, strip_tags, normalize_url_encoding and the rest — and + * would hit ReferenceError without this. * - * Which names those are is worked out in lib-globals.cjs, which the ESLint - * config reads as well so both agree on what exists. Only lib.js is evaluated - * here; nav-index.test.js evaluates js/zs-background.js itself, with the browser - * API stubs that file needs. + * lib-globals.cjs works out which names those are; the ESLint config reads it + * too. Only lib.js is evaluated here; nav-index.test.js evaluates + * js/zs-background.js itself, with the browser API stubs that file needs. */ const { LIB_SOURCE, LIB_NAMES } = require('./lib-globals.cjs');