diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3a63db4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +# Runs the test suite and the linter on every push and pull request. +# +# `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. +# +# 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. +# +# 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 + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# 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 + +jobs: + test: + name: Tests and lint + runs-on: ubuntu-latest + # 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. + 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 + + # 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 new file mode 100644 index 0000000..1cf6d73 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,89 @@ +/** + * ESLint configuration for Zeeschuimer. + * + * 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. + * + * 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, 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'); +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: 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, 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', +}; + +export default [ + { + ignores: [ + 'inc/**', // third-party bundles, minified and not ours to fix + '.claude/**', // scratch worktrees hold copies of every module + // Not extension code: a Firefox profile's prefs.js, a stealth script + // written to run inside a page. What is, `npm test` runs. + 'tests/**', + // `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 use the background-script globals. + files: ['modules/**/*.js'], + ignores: BACKGROUND_SCRIPTS, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, + }, + rules, + }, + { + // 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', + sourceType: 'script', + globals: { ...globals.browser, ...globals.webextensions, ...zeeschuimer_globals }, + }, + 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/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..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 @@ -58,7 +60,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 @@ -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/lib-globals.cjs b/tests/lib-globals.cjs new file mode 100644 index 0000000..fdf0b53 --- /dev/null +++ b/tests/lib-globals.cjs @@ -0,0 +1,146 @@ +/** + * The names Zeeschuimer's own scripts put into global scope. + * + * 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 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 come out of the source, so adding a helper to `js/lib.js` needs no + * edit here. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const espree = require('espree'); + +const ROOT = path.join(__dirname, '..'); + +function read(...parts) { + return fs.readFileSync(path.join(ROOT, ...parts), 'utf8'); +} + +// 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) { + 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 +// out here against the path the manifest loads them from. +const VENDORED_NAMES = { + 'inc/dexie.js': ['Dexie'], + '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) { + 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 { + 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 []; +} + +// 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: + // `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]; + } + + 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. +function global_names(script, source) { + 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. +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. +const ALL_NAMES = [...new Set(BACKGROUND_SCRIPTS.flatMap(script => { + if (script in VENDORED_NAMES) { + return VENDORED_NAMES[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 = { BACKGROUND_SCRIPTS, LIB_SOURCE, LIB_NAMES, ALL_NAMES }; diff --git a/tests/package-lock.json b/tests/package-lock.json index ada8011..ec1e7b3 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -8,10 +8,14 @@ "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", + "eslint": "10.9.1", + "espree": "^11.2.0", "fake-indexeddb": "^5.0.1", + "globals": "17.12.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" } @@ -512,6 +516,239 @@ "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/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", + "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 +1200,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 +1263,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 +1320,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 +1343,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 +1379,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 +1991,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 +2231,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 +2455,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 +2561,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 +2575,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 +2592,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 +2632,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 +2806,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 +2976,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 +3058,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 +3088,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 +3926,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 +3940,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 +3967,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 +3997,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 +4287,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 +4472,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 +4939,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 +5023,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 +5146,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..d62398c 100644 --- a/tests/package.json +++ b/tests/package.json @@ -6,14 +6,22 @@ "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": { + "@eslint/js": "10.0.1", "cross-fetch": "^4.0.0", "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", "jest-environment-jsdom": "^29.7.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/tests/setup-globals.cjs b/tests/setup-globals.cjs index b55e659..56915c1 100644 --- a/tests/setup-globals.cjs +++ b/tests/setup-globals.cjs @@ -1,45 +1,21 @@ /** - * 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. * - * 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. + * 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 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());