From 9c0bbe94266645572152f0ac71e13ea89277e0e4 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 20:08:29 +0300 Subject: [PATCH 1/7] upgrade deps --- package-lock.json | 733 ++++++++++++++++++---------------------------- package.json | 4 +- 2 files changed, 281 insertions(+), 456 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e83434..9c96de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,22 +12,25 @@ "tinyqueue": "^3.0.0" }, "devDependencies": { - "eslint": "^9.6.0", - "eslint-config-mourner": "^4.0.1" + "eslint": "^10.5.0", + "eslint-config-mourner": "^5.0.1" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "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" } @@ -46,9 +49,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "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": { @@ -56,62 +59,127 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", - "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "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": "^2.1.4", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@eslint/core": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "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" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", - "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" + "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": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "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": { @@ -129,9 +197,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "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": { @@ -142,78 +210,69 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@stylistic/eslint-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz", + "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/types": "^8.56.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">= 8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.0.0 || ^10.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz", - "integrity": "sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==", + "node_modules/@stylistic/eslint-plugin/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@types/eslint": "^8.56.10", - "acorn": "^8.11.3", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "eslint": ">=8.40.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "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", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } + "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "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" }, @@ -224,10 +283,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", "bin": { @@ -248,9 +321,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -264,115 +337,33 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "18 || 20 || >=22" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=7.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -385,13 +376,13 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -423,29 +414,33 @@ } }, "node_modules/eslint": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", - "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/config-array": "^0.17.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.6.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.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.3.0", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "@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": "^8.0.1", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", - "esquery": "^1.5.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", @@ -454,89 +449,95 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-mourner": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-4.0.1.tgz", - "integrity": "sha512-seylu4qdUc7kabx42zEedJiCyRDYqwsZLFPPnMRntmOFOQv6cKoM+FqhYpLKZ5dF7A7wW6mCympQSkg6RVxGlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-mourner/-/eslint-config-mourner-5.0.1.tgz", + "integrity": "sha512-+bJjgJanAP1Nk0HIO1aV7CJZLa7zl/ymaFSff+HeSLHr9ifuHc8nX9dNdrjO7GnGdrgRMrmrAq3TCqfwj6NHiQ==", "dev": true, "license": "ISC", "dependencies": { - "@eslint/js": "^9.5.0", - "@stylistic/eslint-plugin-js": "^2.2.2" + "@eslint/js": "^10.0.1", + "@stylistic/eslint-plugin": "^5.10.0", + "globals": "^17.6.0" } }, "node_modules/eslint-scope": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "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": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "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.12.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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": { @@ -600,16 +601,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -655,9 +646,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -675,9 +666,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", "dev": true, "license": "MIT", "engines": { @@ -687,43 +678,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "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-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -757,16 +721,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -774,19 +728,6 @@ "dev": true, "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -848,30 +789,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, @@ -932,19 +869,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -965,6 +889,19 @@ "node": ">=8" } }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -985,72 +922,6 @@ "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1074,52 +945,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", diff --git a/package.json b/package.json index 3fd1580..333cd35 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "tinyqueue": "^3.0.0" }, "devDependencies": { - "eslint": "^9.6.0", - "eslint-config-mourner": "^4.0.1" + "eslint": "^10.5.0", + "eslint-config-mourner": "^5.0.1" }, "scripts": { "pretest": "eslint *.js test/*.js", From ed266e57b13942b7ff05a7a814059ac1523c3f09 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 20:42:40 +0300 Subject: [PATCH 2/7] perf optimizations (early exit + flatten input) --- polylabel.js | 125 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 35 deletions(-) diff --git a/polylabel.js b/polylabel.js index 395d6b5..5f22b2a 100644 --- a/polylabel.js +++ b/polylabel.js @@ -25,20 +25,40 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { return result; } + // flatten the polygon rings into a single contiguous coordinate buffer for + // cache-friendly, pointer-chase-free access in the hot distance loop + let numPoints = 0; + for (const ring of polygon) numPoints += ring.length; + const coords = new Float64Array(numPoints * 2); + const ringIndices = []; // [start, end) pairs into coords for each ring + let c = 0; + for (const ring of polygon) { + const start = c; + for (let i = 0; i < ring.length; i++) { + coords[c++] = ring[i][0]; + coords[c++] = ring[i][1]; + } + ringIndices.push(start, c); + } + // a priority queue of cells in order of their "potential" (max distance to polygon) const cellQueue = new Queue([], (a, b) => b.max - a.max); // take centroid as the first best guess - let bestCell = getCentroidCell(polygon); + let bestCell = getCentroidCell(polygon, coords, ringIndices); // second guess: bounding box centroid - const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, polygon); + const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringIndices, -Infinity, null); if (bboxCell.d > bestCell.d) bestCell = bboxCell; let numProbes = 2; - function potentiallyQueue(x, y, h) { - const cell = new Cell(x, y, h, polygon); + function potentiallyQueue(x, y, h, seed) { + // a cell is only useful if it can beat the best (d > bestCell.d) or is + // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both fail + // once d ≤ threshold, so the distance scan can bail there early. + const threshold = bestCell.d - Math.max(0, h * Math.SQRT2 - precision); + const cell = new Cell(x, y, h, coords, ringIndices, threshold, seed); numProbes++; if (cell.max > bestCell.d + precision) cellQueue.push(cell); @@ -53,23 +73,23 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { let h = cellSize / 2; for (let x = minX; x < maxX; x += cellSize) { for (let y = minY; y < maxY; y += cellSize) { - potentiallyQueue(x + h, y + h, h); + potentiallyQueue(x + h, y + h, h, null); } } while (cellQueue.length) { // pick the most promising cell from the queue - const {max, x, y, h: ch} = cellQueue.pop(); + const cell = cellQueue.pop(); // do not drill down further if there's no chance of a better solution - if (max - bestCell.d <= precision) break; - - // split the cell into four cells - h = ch / 2; - potentiallyQueue(x - h, y - h, h); - potentiallyQueue(x + h, y - h, h); - potentiallyQueue(x - h, y + h, h); - potentiallyQueue(x + h, y + h, h); + if (cell.max - bestCell.d <= precision) break; + + // split the cell into four cells, seeding each with the parent's nearest segment + h = cell.h / 2; + potentiallyQueue(cell.x - h, cell.y - h, h, cell); + potentiallyQueue(cell.x + h, cell.y - h, h, cell); + potentiallyQueue(cell.x - h, cell.y + h, h, cell); + potentiallyQueue(cell.x + h, cell.y + h, h, cell); } if (debug) { @@ -81,28 +101,65 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { return result; } -function Cell(x, y, h, polygon) { +// coordinates of the segment found nearest in the last pointToPolygonDist call, +// used to seed the scan of refined child cells (a child is almost always nearest +// to the same segment as its parent) +let nsx1 = 0, nsy1 = 0, nsx2 = 0, nsy2 = 0; + +function Cell(x, y, h, coords, ringIndices, maxD, seed) { this.x = x; // cell center x this.y = y; // cell center y this.h = h; // half the cell size - this.d = pointToPolygonDist(x, y, polygon); // distance from cell center to polygon + this.d = pointToPolygonDist(x, y, coords, ringIndices, maxD, seed); // distance from cell center to polygon + // remember the nearest segment so child cells can seed their scan with it + this.nsx1 = nsx1; this.nsy1 = nsy1; this.nsx2 = nsx2; this.nsy2 = nsy2; this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell } -// signed distance from point to polygon outline (negative if point is outside) -function pointToPolygonDist(x, y, polygon) { +// signed distance from point to polygon outline (negative if point is outside). +// maxD is a distance threshold: if a partial result proves the point is no +// farther than maxD from the outline, the scan bails out early and returns maxD, +// since the caller has already determined such a cell can't beat the best. +// seed is the parent cell (or null); its nearest segment is checked first so +// boundary cells reach the early-out threshold without scanning the whole outline. +function pointToPolygonDist(x, y, coords, ringIndices, maxD, seed) { let inside = false; let minDistSq = Infinity; + const thresholdSq = maxD > 0 ? maxD * maxD : -1; - for (const ring of polygon) { - for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) { - const a = ring[i]; - const b = ring[j]; + if (seed !== null) { + nsx1 = seed.nsx1; nsy1 = seed.nsy1; nsx2 = seed.nsx2; nsy2 = seed.nsy2; + minDistSq = getSegDistSq(x, y, nsx1, nsy1, nsx2, nsy2); + if (minDistSq <= thresholdSq) return maxD; + } + + for (let r = 0; r < ringIndices.length; r += 2) { + const start = ringIndices[r]; + const end = ringIndices[r + 1]; + + // previous vertex (b), starting from the last point in the ring + let bx = coords[end - 2]; + let by = coords[end - 1]; + + for (let i = start; i < end; i += 2) { + const ax = coords[i]; + const ay = coords[i + 1]; + + if ((ay > y !== by > y) && + (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside; + + const distSq = getSegDistSq(x, y, ax, ay, bx, by); + if (distSq < minDistSq) { + minDistSq = distSq; + nsx1 = ax; nsy1 = ay; nsx2 = bx; nsy2 = by; - if ((a[1] > y !== b[1] > y) && - (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside; + // the point is already close enough to the outline that this cell + // can't possibly contain a better label position — stop scanning + if (minDistSq <= thresholdSq) return maxD; + } - minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b)); + bx = ax; + by = ay; } } @@ -110,7 +167,7 @@ function pointToPolygonDist(x, y, polygon) { } // get polygon centroid -function getCentroidCell(polygon) { +function getCentroidCell(polygon, coords, ringIndices) { let area = 0; let x = 0; let y = 0; @@ -124,24 +181,22 @@ function getCentroidCell(polygon) { y += (a[1] + b[1]) * f; area += f * 3; } - const centroid = new Cell(x / area, y / area, 0, polygon); - if (area === 0 || centroid.d < 0) return new Cell(points[0][0], points[0][1], 0, polygon); + const centroid = new Cell(x / area, y / area, 0, coords, ringIndices, -Infinity, null); + if (area === 0 || centroid.d < 0) return new Cell(points[0][0], points[0][1], 0, coords, ringIndices, -Infinity, null); return centroid; } // get squared distance from a point to a segment -function getSegDistSq(px, py, a, b) { - let x = a[0]; - let y = a[1]; - let dx = b[0] - x; - let dy = b[1] - y; +function getSegDistSq(px, py, x, y, bx, by) { + let dx = bx - x; + let dy = by - y; if (dx !== 0 || dy !== 0) { const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); if (t > 1) { - x = b[0]; - y = b[1]; + x = bx; + y = by; } else if (t > 0) { x += dx * t; From a4f4ab15e0db63e9034dc9a6ab23c3f774aa36bb Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 20:50:22 +0300 Subject: [PATCH 3/7] simplify bookkeeping --- polylabel.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/polylabel.js b/polylabel.js index 5f22b2a..f44b8c9 100644 --- a/polylabel.js +++ b/polylabel.js @@ -101,35 +101,34 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { return result; } -// coordinates of the segment found nearest in the last pointToPolygonDist call, -// used to seed the scan of refined child cells (a child is almost always nearest -// to the same segment as its parent) -let nsx1 = 0, nsy1 = 0, nsx2 = 0, nsy2 = 0; - function Cell(x, y, h, coords, ringIndices, maxD, seed) { this.x = x; // cell center x this.y = y; // cell center y this.h = h; // half the cell size - this.d = pointToPolygonDist(x, y, coords, ringIndices, maxD, seed); // distance from cell center to polygon - // remember the nearest segment so child cells can seed their scan with it - this.nsx1 = nsx1; this.nsy1 = nsy1; this.nsx2 = nsx2; this.nsy2 = nsy2; - this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell + // nsx1..nsy2 hold the nearest segment found below, so child cells can seed + // their scan with it (a child is almost always nearest to the same segment) + this.nsx1 = 0; this.nsy1 = 0; this.nsx2 = 0; this.nsy2 = 0; + this.d = pointToPolygonDist(this, coords, ringIndices, maxD, seed); // distance from cell center to polygon + this.max = this.d + h * Math.SQRT2; // max distance to polygon within a cell } -// signed distance from point to polygon outline (negative if point is outside). -// maxD is a distance threshold: if a partial result proves the point is no -// farther than maxD from the outline, the scan bails out early and returns maxD, -// since the caller has already determined such a cell can't beat the best. -// seed is the parent cell (or null); its nearest segment is checked first so -// boundary cells reach the early-out threshold without scanning the whole outline. -function pointToPolygonDist(x, y, coords, ringIndices, maxD, seed) { +// signed distance from cell center to polygon outline (negative if outside), +// also recording the nearest segment on the cell. maxD is a distance threshold: +// if a partial result proves the center is no farther than maxD from the outline, +// the scan bails out early and returns maxD, since the caller has already +// determined such a cell can't beat the best. seed is the parent cell (or null); +// its nearest segment is checked first so boundary cells reach the early-out +// threshold without scanning the whole outline. +function pointToPolygonDist(cell, coords, ringIndices, maxD, seed) { + const x = cell.x; + const y = cell.y; let inside = false; let minDistSq = Infinity; const thresholdSq = maxD > 0 ? maxD * maxD : -1; if (seed !== null) { - nsx1 = seed.nsx1; nsy1 = seed.nsy1; nsx2 = seed.nsx2; nsy2 = seed.nsy2; - minDistSq = getSegDistSq(x, y, nsx1, nsy1, nsx2, nsy2); + cell.nsx1 = seed.nsx1; cell.nsy1 = seed.nsy1; cell.nsx2 = seed.nsx2; cell.nsy2 = seed.nsy2; + minDistSq = getSegDistSq(x, y, seed.nsx1, seed.nsy1, seed.nsx2, seed.nsy2); if (minDistSq <= thresholdSq) return maxD; } @@ -151,7 +150,7 @@ function pointToPolygonDist(x, y, coords, ringIndices, maxD, seed) { const distSq = getSegDistSq(x, y, ax, ay, bx, by); if (distSq < minDistSq) { minDistSq = distSq; - nsx1 = ax; nsy1 = ay; nsx2 = bx; nsy2 = by; + cell.nsx1 = ax; cell.nsy1 = ay; cell.nsx2 = bx; cell.nsy2 = by; // the point is already close enough to the outline that this cell // can't possibly contain a better label position — stop scanning From a8cba1f62d3de55ad7fd402e827873fc431edbae Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 20:57:45 +0300 Subject: [PATCH 4/7] add proper benchmark --- bench.js | 24 ++++++++++++++++++++++++ debug.js | 13 ------------- test/test.js | 6 ++---- 3 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 bench.js delete mode 100644 debug.js diff --git a/bench.js b/bench.js new file mode 100644 index 0000000..e25c1d2 --- /dev/null +++ b/bench.js @@ -0,0 +1,24 @@ +import polylabel from './polylabel.js'; +import water1 from './test/fixtures/water1.json' with {type: 'json'}; +import water2 from './test/fixtures/water2.json' with {type: 'json'}; + +const cases = [ + ['water1 precision 1', water1, 1], + ['water1 precision 50', water1, 50], + ['water2 precision 1', water2, 1], +]; + +const RUNS = 100; + +for (const [name, polygon, precision] of cases) { + const times = []; + for (let r = 0; r < RUNS; r++) { + const t = performance.now(); + polylabel(polygon, precision); + times.push(performance.now() - t); + } + times.sort((a, b) => a - b); + const min = times[0]; + const median = times[times.length >> 1]; + console.log(`${name}: min ${min.toFixed(2)}ms, median ${median.toFixed(2)}ms`); +} diff --git a/debug.js b/debug.js deleted file mode 100644 index e495c3d..0000000 --- a/debug.js +++ /dev/null @@ -1,13 +0,0 @@ - -import polylabel from './polylabel.js'; -import {readFileSync} from 'fs'; - -const polygon = JSON.parse(readFileSync(new URL('./test/fixtures/water1.json', import.meta.url))); - -console.log(`num points: ${[].concat(...polygon).length}`); - -console.time('find point'); -const result = polylabel(polygon, 1, true); -console.timeEnd('find point'); - -console.log(result); diff --git a/test/test.js b/test/test.js index 34d8ffa..7b737e3 100644 --- a/test/test.js +++ b/test/test.js @@ -1,10 +1,8 @@ import polylabel from '../polylabel.js'; import test from 'node:test'; import assert from 'node:assert/strict'; -import {readFileSync} from 'fs'; - -const water1 = JSON.parse(readFileSync(new URL('fixtures/water1.json', import.meta.url))); -const water2 = JSON.parse(readFileSync(new URL('fixtures/water2.json', import.meta.url))); +import water1 from './fixtures/water1.json' with {type: 'json'}; +import water2 from './fixtures/water2.json' with {type: 'json'}; test('finds pole of inaccessibility for water1 and precision 1', () => { const p = polylabel(water1, 1); From 18ffe6c02a53f8a5ce218e1c49a264a35a61b8af Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 20:59:14 +0300 Subject: [PATCH 5/7] update CI --- .github/workflows/test.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71b4c9f..f2e58d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,16 +4,10 @@ jobs: node: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v1 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: - node-version: 20 - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test \ No newline at end of file + node-version: 24 + cache: npm + - run: npm ci + - run: npm test From 3cbd34fbfbceef60ba68cb509ad9c25624fb1501 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 24 Jun 2026 21:06:31 +0300 Subject: [PATCH 6/7] small simplifications --- polylabel.js | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/polylabel.js b/polylabel.js index f44b8c9..25ab74f 100644 --- a/polylabel.js +++ b/polylabel.js @@ -30,25 +30,24 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { let numPoints = 0; for (const ring of polygon) numPoints += ring.length; const coords = new Float64Array(numPoints * 2); - const ringIndices = []; // [start, end) pairs into coords for each ring + const ringEnds = []; // end offset into coords for each ring (start = previous end, or 0) let c = 0; for (const ring of polygon) { - const start = c; for (let i = 0; i < ring.length; i++) { coords[c++] = ring[i][0]; coords[c++] = ring[i][1]; } - ringIndices.push(start, c); + ringEnds.push(c); } // a priority queue of cells in order of their "potential" (max distance to polygon) const cellQueue = new Queue([], (a, b) => b.max - a.max); // take centroid as the first best guess - let bestCell = getCentroidCell(polygon, coords, ringIndices); + let bestCell = getCentroidCell(coords, ringEnds); // second guess: bounding box centroid - const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringIndices, -Infinity, null); + const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringEnds, -Infinity, null); if (bboxCell.d > bestCell.d) bestCell = bboxCell; let numProbes = 2; @@ -58,7 +57,7 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both fail // once d ≤ threshold, so the distance scan can bail there early. const threshold = bestCell.d - Math.max(0, h * Math.SQRT2 - precision); - const cell = new Cell(x, y, h, coords, ringIndices, threshold, seed); + const cell = new Cell(x, y, h, coords, ringEnds, threshold, seed); numProbes++; if (cell.max > bestCell.d + precision) cellQueue.push(cell); @@ -101,14 +100,14 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { return result; } -function Cell(x, y, h, coords, ringIndices, maxD, seed) { +function Cell(x, y, h, coords, ringEnds, maxD, seed) { this.x = x; // cell center x this.y = y; // cell center y this.h = h; // half the cell size // nsx1..nsy2 hold the nearest segment found below, so child cells can seed // their scan with it (a child is almost always nearest to the same segment) this.nsx1 = 0; this.nsy1 = 0; this.nsx2 = 0; this.nsy2 = 0; - this.d = pointToPolygonDist(this, coords, ringIndices, maxD, seed); // distance from cell center to polygon + this.d = pointToPolygonDist(this, coords, ringEnds, maxD, seed); // distance from cell center to polygon this.max = this.d + h * Math.SQRT2; // max distance to polygon within a cell } @@ -119,7 +118,7 @@ function Cell(x, y, h, coords, ringIndices, maxD, seed) { // determined such a cell can't beat the best. seed is the parent cell (or null); // its nearest segment is checked first so boundary cells reach the early-out // threshold without scanning the whole outline. -function pointToPolygonDist(cell, coords, ringIndices, maxD, seed) { +function pointToPolygonDist(cell, coords, ringEnds, maxD, seed) { const x = cell.x; const y = cell.y; let inside = false; @@ -132,9 +131,9 @@ function pointToPolygonDist(cell, coords, ringIndices, maxD, seed) { if (minDistSq <= thresholdSq) return maxD; } - for (let r = 0; r < ringIndices.length; r += 2) { - const start = ringIndices[r]; - const end = ringIndices[r + 1]; + let start = 0; + for (let r = 0; r < ringEnds.length; r++) { + const end = ringEnds[r]; // previous vertex (b), starting from the last point in the ring let bx = coords[end - 2]; @@ -160,28 +159,31 @@ function pointToPolygonDist(cell, coords, ringIndices, maxD, seed) { bx = ax; by = ay; } + start = end; } return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq); } -// get polygon centroid -function getCentroidCell(polygon, coords, ringIndices) { +// get polygon centroid (over the outer ring, coords[0..ringEnds[0])) +function getCentroidCell(coords, ringEnds) { let area = 0; let x = 0; let y = 0; - const points = polygon[0]; - - for (let i = 0, len = points.length, j = len - 1; i < len; j = i++) { - const a = points[i]; - const b = points[j]; - const f = a[0] * b[1] - b[0] * a[1]; - x += (a[0] + b[0]) * f; - y += (a[1] + b[1]) * f; + const end = ringEnds[0]; + + for (let i = 0, j = end - 2; i < end; j = i, i += 2) { + const ax = coords[i]; + const ay = coords[i + 1]; + const bx = coords[j]; + const by = coords[j + 1]; + const f = ax * by - bx * ay; + x += (ax + bx) * f; + y += (ay + by) * f; area += f * 3; } - const centroid = new Cell(x / area, y / area, 0, coords, ringIndices, -Infinity, null); - if (area === 0 || centroid.d < 0) return new Cell(points[0][0], points[0][1], 0, coords, ringIndices, -Infinity, null); + const centroid = new Cell(x / area, y / area, 0, coords, ringEnds, -Infinity, null); + if (area === 0 || centroid.d < 0) return new Cell(coords[0], coords[1], 0, coords, ringEnds, -Infinity, null); return centroid; } From d6f9c5d420d0d27f0d609d36b25221b043ce7e79 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 25 Jun 2026 01:34:54 +0300 Subject: [PATCH 7/7] massively speed up distance checks --- polylabel.js | 133 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 102 insertions(+), 31 deletions(-) diff --git a/polylabel.js b/polylabel.js index 25ab74f..6414e41 100644 --- a/polylabel.js +++ b/polylabel.js @@ -1,6 +1,9 @@ import Queue from 'tinyqueue'; +// number of consecutive edges grouped under a single bounding box for block-skip +const K = 32; + export default function polylabel(polygon, precision = 1.0, debug = false) { // find the bounding box of the outer ring let minX = Infinity; @@ -40,14 +43,16 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { ringEnds.push(c); } + const blocks = buildBlocks(coords, ringEnds); + // a priority queue of cells in order of their "potential" (max distance to polygon) const cellQueue = new Queue([], (a, b) => b.max - a.max); // take centroid as the first best guess - let bestCell = getCentroidCell(coords, ringEnds); + let bestCell = getCentroidCell(coords, ringEnds, blocks); // second guess: bounding box centroid - const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringEnds, -Infinity, null); + const bboxCell = new Cell(minX + width / 2, minY + height / 2, 0, coords, ringEnds, blocks, -Infinity, null); if (bboxCell.d > bestCell.d) bestCell = bboxCell; let numProbes = 2; @@ -57,7 +62,7 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { // worth subdividing (max = d + h·√2 > bestCell.d + precision). Both fail // once d ≤ threshold, so the distance scan can bail there early. const threshold = bestCell.d - Math.max(0, h * Math.SQRT2 - precision); - const cell = new Cell(x, y, h, coords, ringEnds, threshold, seed); + const cell = new Cell(x, y, h, coords, ringEnds, blocks, threshold, seed); numProbes++; if (cell.max > bestCell.d + precision) cellQueue.push(cell); @@ -100,14 +105,14 @@ export default function polylabel(polygon, precision = 1.0, debug = false) { return result; } -function Cell(x, y, h, coords, ringEnds, maxD, seed) { +function Cell(x, y, h, coords, ringEnds, blocks, maxD, seed) { this.x = x; // cell center x this.y = y; // cell center y this.h = h; // half the cell size // nsx1..nsy2 hold the nearest segment found below, so child cells can seed // their scan with it (a child is almost always nearest to the same segment) this.nsx1 = 0; this.nsy1 = 0; this.nsx2 = 0; this.nsy2 = 0; - this.d = pointToPolygonDist(this, coords, ringEnds, maxD, seed); // distance from cell center to polygon + this.d = pointToPolygonDist(this, coords, ringEnds, blocks, maxD, seed); // distance from cell center to polygon this.max = this.d + h * Math.SQRT2; // max distance to polygon within a cell } @@ -118,7 +123,7 @@ function Cell(x, y, h, coords, ringEnds, maxD, seed) { // determined such a cell can't beat the best. seed is the parent cell (or null); // its nearest segment is checked first so boundary cells reach the early-out // threshold without scanning the whole outline. -function pointToPolygonDist(cell, coords, ringEnds, maxD, seed) { +function pointToPolygonDist(cell, coords, ringEnds, blocks, maxD, seed) { const x = cell.x; const y = cell.y; let inside = false; @@ -131,42 +136,108 @@ function pointToPolygonDist(cell, coords, ringEnds, maxD, seed) { if (minDistSq <= thresholdSq) return maxD; } - let start = 0; - for (let r = 0; r < ringEnds.length; r++) { - const end = ringEnds[r]; + const stride = K * 2; + const numRings = ringEnds.length; + let g = 0; // running block index into bboxes + let ringStart = 0; + + for (let r = 0; r < numRings; r++) { + const ringEnd = ringEnds[r]; + + // previous vertex (b), starting from the last point in the ring; carried + // across blocks so each block's first edge connects to the prior vertex + let bx = coords[ringEnd - 2]; + let by = coords[ringEnd - 1]; + + for (let s = ringStart; s < ringEnd; s += stride, g += 4) { + let end = s + stride; + if (end > ringEnd) end = ringEnd; + const bminX = blocks[g], bminY = blocks[g + 1], bmaxX = blocks[g + 2], bmaxY = blocks[g + 3]; + + // lower bound on the distance from (x, y) to any edge in this block + const dx = x < bminX ? bminX - x : x > bmaxX ? x - bmaxX : 0; + const dy = y < bminY ? bminY - y : y > bmaxY ? y - bmaxY : 0; + const skipDist = dx * dx + dy * dy >= minDistSq; + + // this block's edges can only flip ray-cast parity if its bbox straddles + // y and extends right of x; else no edge crosses the rightward ray + const skipCross = y < bminY || y >= bmaxY || x > bmaxX; + + if (skipDist && skipCross) { + bx = coords[end - 2]; + by = coords[end - 1]; + continue; + } - // previous vertex (b), starting from the last point in the ring - let bx = coords[end - 2]; - let by = coords[end - 1]; + for (let i = s; i < end; i += 2) { + const ax = coords[i]; + const ay = coords[i + 1]; - for (let i = start; i < end; i += 2) { - const ax = coords[i]; - const ay = coords[i + 1]; + if (!skipCross && (ay > y !== by > y) && + (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside; - if ((ay > y !== by > y) && - (x < (bx - ax) * (y - ay) / (by - ay) + ax)) inside = !inside; + if (!skipDist) { + const distSq = getSegDistSq(x, y, ax, ay, bx, by); + if (distSq < minDistSq) { + minDistSq = distSq; + cell.nsx1 = ax; cell.nsy1 = ay; cell.nsx2 = bx; cell.nsy2 = by; - const distSq = getSegDistSq(x, y, ax, ay, bx, by); - if (distSq < minDistSq) { - minDistSq = distSq; - cell.nsx1 = ax; cell.nsy1 = ay; cell.nsx2 = bx; cell.nsy2 = by; + // the point is already close enough to the outline that this cell + // can't possibly contain a better label position — stop scanning + if (minDistSq <= thresholdSq) return maxD; + } + } - // the point is already close enough to the outline that this cell - // can't possibly contain a better label position — stop scanning - if (minDistSq <= thresholdSq) return maxD; + bx = ax; + by = ay; } - - bx = ax; - by = ay; } - start = end; + ringStart = ringEnd; } return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq); } +// precompute one bounding box per block of K consecutive edges (over both +// endpoints of every edge in it) so the distance scan can skip whole blocks in +// O(1). The block layout mirrors the flattened coords/ringEnds and is re-derived +// in the scan, so only the bboxes need storing: a flat [minX,minY,maxX,maxY] run +// per block, sized upfront from the ring lengths. +function buildBlocks(coords, ringEnds) { + const stride = K * 2; + let numBlocks = 0; + let ringStart = 0; + for (let r = 0; r < ringEnds.length; r++) { + numBlocks += Math.ceil((ringEnds[r] - ringStart) / stride); + ringStart = ringEnds[r]; + } + + const blocks = new Float64Array(numBlocks * 4); + let g = 0; + ringStart = 0; + for (let r = 0; r < ringEnds.length; r++) { + const ringEnd = ringEnds[r]; + for (let s = ringStart; s < ringEnd; s += stride, g += 4) { + const end = s + stride < ringEnd ? s + stride : ringEnd; + const prev = s === ringStart ? ringEnd - 2 : s - 2; + + let minX = coords[prev], minY = coords[prev + 1]; + let maxX = minX, maxY = minY; + for (let i = s; i < end; i += 2) { + const px = coords[i], py = coords[i + 1]; + if (px < minX) minX = px; else if (px > maxX) maxX = px; + if (py < minY) minY = py; else if (py > maxY) maxY = py; + } + blocks[g] = minX; blocks[g + 1] = minY; blocks[g + 2] = maxX; blocks[g + 3] = maxY; + } + ringStart = ringEnd; + } + + return blocks; +} + // get polygon centroid (over the outer ring, coords[0..ringEnds[0])) -function getCentroidCell(coords, ringEnds) { +function getCentroidCell(coords, ringEnds, blocks) { let area = 0; let x = 0; let y = 0; @@ -182,8 +253,8 @@ function getCentroidCell(coords, ringEnds) { y += (ay + by) * f; area += f * 3; } - const centroid = new Cell(x / area, y / area, 0, coords, ringEnds, -Infinity, null); - if (area === 0 || centroid.d < 0) return new Cell(coords[0], coords[1], 0, coords, ringEnds, -Infinity, null); + const centroid = new Cell(x / area, y / area, 0, coords, ringEnds, blocks, -Infinity, null); + if (area === 0 || centroid.d < 0) return new Cell(coords[0], coords[1], 0, coords, ringEnds, blocks, -Infinity, null); return centroid; }