diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6eb4c..7b34969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `update-dependents` discovers intermediate NPM dependents at runtime, prompts to release them, waits for npm publish, and folds those versions into a single downstream pull request +- `--update-dependents` flag to `oneblink-release repository` to wait for npm publish after release, then run `update-dependents` +- `--force-update-dependency`, `--force-publish-intermediate-dependency`, and `--ticket` flags for `update-dependents` + +### Changed + +- **[BREAKING]** `--force` for `update-dependents` now skips all prompts and requires `--ticket` (use `--force-update-dependency` for the previous behaviour) +- `update-dependents` also bumps repositories that depend on newly released intermediate NPM packages even when they do not depend on the original package + ### Fixed - github action permissions to allow docs and github release jobs +### Removed + +- `product-cognito-hosted-login-css` repository + ## [4.0.0] - 2026-02-02 ### Removed diff --git a/package-lock.json b/package-lock.json index f087538..176a35c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,8 @@ "eslint-plugin-prettier": "^5.5.5", "fixpack": "^4.0.0", "prettier-plugin-jsdoc": "^1.8.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.10" }, "engines": { "node": ">=24", @@ -269,6 +270,13 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@microsoft/eslint-plugin-sdl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-1.1.0.tgz", @@ -441,6 +449,16 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.142.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", + "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -495,6 +513,269 @@ "node": ">=12" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.2.tgz", + "integrity": "sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.2.tgz", + "integrity": "sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.2.tgz", + "integrity": "sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.2.tgz", + "integrity": "sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -513,6 +794,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/changelog-parser": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/@types/changelog-parser/-/changelog-parser-2.8.4.tgz", @@ -537,6 +836,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -942,9 +1248,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -952,13 +1258,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1022,6 +1328,119 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -1052,9 +1471,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": { @@ -1295,6 +1714,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -1374,9 +1803,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1465,6 +1894,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1628,6 +2067,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -1833,6 +2279,16 @@ "node": ">=8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2054,6 +2510,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2279,9 +2742,9 @@ } }, "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2302,13 +2765,13 @@ } }, "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2557,6 +3020,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2593,6 +3066,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend-object": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", @@ -2785,9 +3268,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "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" }, @@ -2821,6 +3304,21 @@ "node": ">=12" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3800,15 +4298,25 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { "js-yaml": "bin/js-yaml.js" } }, @@ -3949,6 +4457,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/line-reader": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/line-reader/-/line-reader-0.2.4.tgz", @@ -4016,6 +4797,16 @@ "node": "20 || >=22" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4565,9 +5356,9 @@ } }, "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": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4593,6 +5384,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4749,6 +5559,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -5060,6 +5884,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5067,9 +5898,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5088,6 +5919,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5447,6 +6307,39 @@ "node": ">=0.12" } }, + "node_modules/rolldown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.2.tgz", + "integrity": "sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.142.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.2", + "@rolldown/binding-darwin-arm64": "1.2.2", + "@rolldown/binding-darwin-x64": "1.2.2", + "@rolldown/binding-freebsd-x64": "1.2.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.2", + "@rolldown/binding-linux-arm64-gnu": "1.2.2", + "@rolldown/binding-linux-arm64-musl": "1.2.2", + "@rolldown/binding-linux-ppc64-gnu": "1.2.2", + "@rolldown/binding-linux-s390x-gnu": "1.2.2", + "@rolldown/binding-linux-x64-gnu": "1.2.2", + "@rolldown/binding-linux-x64-musl": "1.2.2", + "@rolldown/binding-openharmony-arm64": "1.2.2", + "@rolldown/binding-win32-arm64-msvc": "1.2.2", + "@rolldown/binding-win32-x64-msvc": "1.2.2" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -5669,6 +6562,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5690,6 +6590,16 @@ "node": ">=6" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -5722,6 +6632,20 @@ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "license": "CC0-1.0" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -6009,15 +6933,32 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -6045,9 +6986,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -6057,10 +6998,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "license": "MIT", "engines": { "node": ">=14.14" @@ -6319,6 +7270,200 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vite": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.23", + "rolldown": "~1.2.0", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -6429,6 +7574,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -6511,9 +7673,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 0bb4431..03c397b 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "eslint-plugin-prettier": "^5.5.5", "fixpack": "^4.0.0", "prettier-plugin-jsdoc": "^1.8.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.1.10" }, "directories": { "test": "test" @@ -80,7 +81,7 @@ "prerelease": "npm run build", "pretest": "npm run eslint && npm run typescript && fixpack", "release": "node dist/bin.js repository --no-name --cwd .", - "test": "echo \"no tests\"", + "test": "vitest run", "typescript": "tsc --noEmit" }, "type": "module" diff --git a/src/bin.ts b/src/bin.ts index 3a723b7..e96c779 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -14,6 +14,8 @@ import startProductRelease from './startProductRelease.js' import promptForReleaseName from './promptForReleaseName.js' import getRepositoryPlugin from './repositories-plugins/plugins-factory.js' import startUpdateDependents from './startUpdateDependents.js' +import waitForNpmPackageVersion from './waitForNpmPackageVersion.js' +import { readPackageUp } from 'read-package-up' const cli = meow( ` @@ -33,32 +35,50 @@ ${chalk.bold('Examples')} oneblink-release product --name="Inappropriate Release Name" ${chalk.bold.blue( - 'oneblink-release repository [next-version] [--no-git] [--name] [--no-name] [--cwd path]', + 'oneblink-release repository [next-version] [--no-git] [--name] [--no-name] [--cwd path] [--update-dependents] [--force] [--force-update-dependency] [--force-publish-intermediate-dependency] [--ticket]', )} ${chalk.grey('Release a single repository.')} - next-version ..... The next version, will prompt for this if not supplied, - must be a valid semver number. + next-version ............................. The next version, will prompt for this if not supplied, + must be a valid semver number. - --no-git ....... Skip committing changes and creating an annotated git tag. + --no-git ............................... Skip committing changes and creating an annotated git tag. - --increment .... Increment the version automatically using "major" | "minor" | "patch". + --increment ............................ Increment the version automatically using "major" | "minor" | "patch". - --name ......... Skip the question to enter a name for the release by passing - a release name as a flag. + --name ................................. Skip the question to enter a name for the release by passing + a release name as a flag. - --no-name ...... Skip the question to enter a name for the release. Use - option when running a release for an open source repository. + --no-name .............................. Skip the question to enter a name for the release. Use + option when running a release for an open source repository. - --cwd .......... Directory of the repository to release relative to the - current working directory, defaults to the current - working directory. + --cwd .................................. Directory of the repository to release relative to the + current working directory, defaults to the current + working directory. + + --update-dependents .................... After the release is tagged, wait for the NPM package to be + published, then run update-dependents. + + --force ................................ When used with --update-dependents, skip all update-dependents + prompts. Requires --ticket. + + --force-update-dependency .............. When used with --update-dependents, skip prompts confirming + dependency updates. + + --force-publish-intermediate-dependency When used with --update-dependents, skip prompts confirming + intermediate NPM package releases. + + --ticket ............................... Ticket to associate with update-dependents pull requests + (e.g. ON-4323). Required with --force. ${chalk.bold('Examples')} oneblink-release repository oneblink-release repository --no-name + oneblink-release repository --no-name --update-dependents + oneblink-release repository --no-name --update-dependents --force-update-dependency + oneblink-release repository --no-name --update-dependents --force --ticket ON-4323 oneblink-release repository --name="Inappropriate Release Name" oneblink-release repository --increment="major" oneblink-release repository --increment="minor" @@ -67,19 +87,33 @@ ${chalk.bold('Examples')} oneblink-release repository 1.1.1 --cwd ../path/to/code oneblink-release repository 1.1.1-uat.1 --no-git +${chalk.bold.blue( + 'oneblink-release update-dependents [--cwd path] [--force] [--force-update-dependency] [--force-publish-intermediate-dependency] [--ticket]', +)} + ${chalk.grey('Update all product code bases that depend on an NPM package.')} - --cwd .......... Directory of the repository that is the dependency relative - to the current working directory, defaults to the current - working directory. + --cwd .................................... Directory of the repository that is the dependency relative + to the current working directory, defaults to the current + working directory. + + --force .................................. Skip all prompts. Requires --ticket. - --force ........ Update all dependencies without prompting the user. + --force-update-dependency ................ Skip prompts confirming dependency updates. + + --force-publish-intermediate-dependency .. Skip prompts confirming intermediate NPM package releases. + + --ticket ................................. Ticket to associate with pull requests (e.g. ON-4323). + Required with --force. ${chalk.bold('Examples')} oneblink-release update-dependents oneblink-release update-dependents --cwd ../path/to/code - oneblink-release update-dependents --force + oneblink-release update-dependents --force-update-dependency + oneblink-release update-dependents --force-publish-intermediate-dependency + oneblink-release update-dependents --force-update-dependency --force-publish-intermediate-dependency + oneblink-release update-dependents --force --ticket ON-4323 `, { importMeta: import.meta, @@ -113,6 +147,21 @@ ${chalk.bold('Examples')} type: 'boolean', default: false, }, + updateDependents: { + type: 'boolean', + default: false, + }, + forceUpdateDependency: { + type: 'boolean', + default: false, + }, + forcePublishIntermediateDependency: { + type: 'boolean', + default: false, + }, + ticket: { + type: 'string', + }, }, }, ) @@ -156,6 +205,10 @@ async function run(): Promise { await startUpdateDependents({ cwd, force: cli.flags.force, + forceUpdateDependency: cli.flags.forceUpdateDependency, + forcePublishIntermediateDependency: + cli.flags.forcePublishIntermediateDependency, + ticket: cli.flags.ticket, }) break } @@ -200,6 +253,47 @@ async function run(): Promise { releaseName, repositoryPlugin, }) + + if (cli.flags.updateDependents) { + if (!cli.flags.git) { + throw new Error( + 'Cannot use "--update-dependents" with "--no-git" because the package will not be published.', + ) + } + if (!repositoryPlugin.supportsDependencyUpdates) { + throw new Error( + `"${repositoryPlugin.displayType}" repositories do not support updating dependents.`, + ) + } + + const packageResult = await readPackageUp({ + cwd, + }) + const packageName = packageResult?.packageJson.name + if (!packageName) { + throw new Error( + `Could not determine the package name for updating dependents in: ${cwd}`, + ) + } + if (packageResult.packageJson.private) { + throw new Error( + `Cannot use "--update-dependents" for private package "${packageName}" because it is not published to npm.`, + ) + } + + await waitForNpmPackageVersion({ + packageName, + version: input, + }) + await startUpdateDependents({ + cwd, + force: cli.flags.force, + forceUpdateDependency: cli.flags.forceUpdateDependency, + forcePublishIntermediateDependency: + cli.flags.forcePublishIntermediateDependency, + ticket: cli.flags.ticket, + }) + } break } case undefined: { diff --git a/src/enumerateProductRepositories.ts b/src/enumerateProductRepositories.ts index 75cc514..9a4d35d 100644 --- a/src/enumerateProductRepositories.ts +++ b/src/enumerateProductRepositories.ts @@ -2,13 +2,13 @@ import path from 'path' import prepareCloneRepository from './prepareCloneRepository.js' import { RepositoryType } from './repositories-plugins/plugins-factory.js' -type Repository = { +export type ProductRepository = { label: string repositoryName: string isPublic: boolean } & RepositoryType -const productRepositories: Repository[] = [ +export const productRepositories: readonly ProductRepository[] = [ { label: '@oneblink/apps-react (NPM package)', repositoryName: 'apps-react', @@ -45,12 +45,6 @@ const productRepositories: Repository[] = [ isPublic: false, type: 'NODE_JS', }, - { - label: 'Cognito Hosted Login CSS', - repositoryName: 'product-cognito-hosted-login-css', - isPublic: false, - type: 'NODE_JS', - }, { label: 'Console', repositoryName: 'product-console', @@ -136,7 +130,7 @@ export default async function enumerateProductRepositories( fn: (options: { cloneUrl: string repositoryWorkingDirectory: string - productRepository: Repository + productRepository: ProductRepository }) => Promise, ) { for (const productRepository of productRepositories) { diff --git a/src/startUpdateDependents.ts b/src/startUpdateDependents.ts index f7b1efb..8558a18 100644 --- a/src/startUpdateDependents.ts +++ b/src/startUpdateDependents.ts @@ -1,17 +1,56 @@ import { readPackageUp } from 'read-package-up' -import enumerateProductRepositories from './enumerateProductRepositories.js' +import { + productRepositories, + ProductRepository, +} from './enumerateProductRepositories.js' import getRepositoryPlugin from './repositories-plugins/plugins-factory.js' import enquirer from 'enquirer' import executeCommand from './executeCommand.js' import boxen from 'boxen' +import chalk from 'chalk' +import prepareCloneRepository from './prepareCloneRepository.js' +import topologicallySortByPackageDependencies from './topologicallySortByPackageDependencies.js' +import waitForNpmPackageVersion from './waitForNpmPackageVersion.js' +import promptForNextVersion from './promptForNextVersion.js' +import promptForReleaseName from './promptForReleaseName.js' +import startRepositoryRelease from './startRepositoryRelease.js' +import getPreRelease from './getPreRelease.js' +import semver from 'semver' +import { + buildDependencyBumpCommitMessage, + DependentCandidate, + getDependencyInstallSpecs, + getDependentCandidates, + getDownstreamRepositoryNames, + getIntermediateNpmDependents, + getRepositoriesNeedingDependencyUpdates, + ScannedProductRepository, +} from './updateDependentsPlanning.js' + +type RetainedClone = { + repositoryWorkingDirectory: string + removeRepositoryWorkingDirectory: () => Promise +} + +const TICKET_PATTERN = /^[a-z]{1,3}-\d+$/i export default async function startUpdateDependents({ cwd, force = false, + forceUpdateDependency = false, + forcePublishIntermediateDependency = false, + ticket: ticketFlag, }: { cwd: string force?: boolean + forceUpdateDependency?: boolean + forcePublishIntermediateDependency?: boolean + ticket?: string }) { + const shouldForceUpdateDependency = force || forceUpdateDependency + const shouldForcePublishIntermediateDependency = + force || forcePublishIntermediateDependency + const dependencyRepositoryPlugin = await getRepositoryPlugin({ cwd, }) @@ -30,82 +69,188 @@ export default async function startUpdateDependents({ } const dependency = dependencyResult.packageJson.name + const dependencyVersion = dependencyResult.packageJson.version console.log('Beginning to update the dependents of:', dependency) - const { ticket } = await enquirer.prompt<{ - ticket: string - }>({ - type: 'input', - name: 'ticket', - message: `Ticket to associate with pull requests? (e.g. ON-4323, AP-4323, MS-4323)`, - required: true, - validate: (input) => { - if (!/^[a-z]{1,3}-\d+$/i.test(input)) { - return 'Ticket must be 1-3 alpha characters, then a hyphen followed by a number' - } - return true - }, - result: (input) => input.toUpperCase(), + const ticket = await resolveTicket({ + ticketFlag, + force, }) - const { isUpdatingTypes } = await enquirer.prompt<{ - isUpdatingTypes: 'yes' | 'no' - }>({ - type: 'select', - name: 'isUpdatingTypes', - message: `Would you like to update "@oneblink/types" as well?`, - choices: [ - { - message: 'Yes, update @oneblink/types', - name: 'yes', - }, - { - message: 'No! "@oneblink/types" does not need to be updated.', - name: 'no', - }, - ], + const isUpdatingTypes = await resolveIsUpdatingTypes({ + force, }) - const createPullRequestUrls: string[] = [] + const retainedClones = new Map() - await enumerateProductRepositories( - async ({ productRepository, repositoryWorkingDirectory }) => { - const repositoryPlugin = await getRepositoryPlugin({ - cwd: repositoryWorkingDirectory, - repositoryType: productRepository, + try { + const { scannedRepositories } = await scanProductRepositories({ + retainedClones, + }) + + const candidates = getDependentCandidates({ + scannedRepositories, + dependency, + dependencyVersion, + }) + + for (const scannedRepository of scannedRepositories) { + logSkippedRepository({ + scannedRepository, + dependency, + dependencyVersion, + candidates, }) - if (!repositoryPlugin.supportsDependencyUpdates) { + } + + if (!candidates.length) { + console.log(`No product repositories need "${dependency}" updated.`) + return + } + + const intermediates = getIntermediateNpmDependents({ + candidates, + scannedRepositories, + }) + const sortedIntermediates = + topologicallySortByPackageDependencies(intermediates) + + await pruneRetainedClones({ + retainedClones, + repositoryNamesToRetain: getRepositoryNamesToRetain({ + candidates, + intermediates, + scannedRepositories, + }), + }) + + const releasedPackageVersions = new Map([ + [dependency, dependencyVersion], + ]) + const releasedRepositoryNames = new Set() + const releasedPackageSummaries: string[] = [] + const createPullRequestUrls: string[] = [] + + for (const intermediate of sortedIntermediates) { + const downstreamRepositoryNames = getDownstreamRepositoryNames({ + scannedRepositories, + intermediate, + }) + + let shouldRelease: 'yes' | 'no' + if (shouldForcePublishIntermediateDependency) { + shouldRelease = 'yes' console.log( - `Skipping "${productRepository.repositoryName}" as "${repositoryPlugin.displayType}" does not support updating dependencies.`, + `Auto-releasing "${intermediate.packageName}" because it is depended on by: ${downstreamRepositoryNames.join(', ')}`, ) - return + } else { + const releaseResult = await enquirer.prompt<{ + shouldRelease: 'yes' | 'no' + }>({ + type: 'select', + name: 'shouldRelease', + message: `"${intermediate.productRepository.repositoryName}" (${intermediate.packageName}) also needs a release because it is depended on by: ${downstreamRepositoryNames.join(', ')}. Update "${dependency}" and release "${intermediate.packageName}" now so downstream pull requests can include both version bumps?`, + choices: [ + { + message: `Yes, update and release "${intermediate.packageName}"`, + name: 'yes', + }, + { + message: `No, only open a dependency bump pull request for "${intermediate.productRepository.repositoryName}"`, + name: 'no', + }, + ], + }) + shouldRelease = releaseResult.shouldRelease } - const result = await readPackageUp({ - cwd: repositoryWorkingDirectory, - }) - const currentDependencyVersion = - result?.packageJson.dependencies?.[dependency] - if (!currentDependencyVersion) { + if (shouldRelease === 'no') { console.log( - `Skipping "${productRepository.repositoryName}" as it does not contain "${dependency}" as a dependency.`, + chalk.yellow( + `Skipping release of "${intermediate.packageName}". Downstream pull requests may conflict later when "${intermediate.packageName}" is released and dependents are updated again.`, + ), ) - return + continue } - if ( - currentDependencyVersion === `^${dependencyResult.packageJson.version}` - ) { + + const repositoryWorkingDirectory = getRetainedWorkingDirectory({ + retainedClones, + repositoryName: intermediate.productRepository.repositoryName, + }) + + const bumpedPackageNames = await installDependencyUpdates({ + cwd: repositoryWorkingDirectory, + dependencies: intermediate.dependencies, + releasedPackageVersions, + isUpdatingTypes: isUpdatingTypes === 'yes', + }) + + if (!bumpedPackageNames.length && isUpdatingTypes !== 'yes') { console.log( - `Skipping "${productRepository.repositoryName}" as it's version of "${dependency}" is already "${currentDependencyVersion}".`, + `Skipping release of "${intermediate.packageName}" as there were no dependency updates to apply.`, ) - return + continue } + await commitDependencyUpdates({ + cwd: repositoryWorkingDirectory, + ticket, + bumpedPackageNames, + isUpdatingTypes: isUpdatingTypes === 'yes', + }) + + const repositoryPlugin = await getRepositoryPlugin({ + cwd: repositoryWorkingDirectory, + repositoryType: intermediate.productRepository, + }) + const nextVersion = await resolveNextVersion({ + repositoryPlugin, + force, + }) + const preRelease = getPreRelease(nextVersion) + const releaseName = + force || intermediate.productRepository.isPublic || preRelease + ? undefined + : await promptForReleaseName() + + await startRepositoryRelease({ + nextVersion, + git: true, + releaseName, + repositoryPlugin, + }) + + await waitForNpmPackageVersion({ + packageName: intermediate.packageName, + version: nextVersion, + }) + + releasedPackageVersions.set(intermediate.packageName, nextVersion) + releasedRepositoryNames.add(intermediate.productRepository.repositoryName) + releasedPackageSummaries.push( + `${intermediate.packageName}@${nextVersion}`, + ) + } + + const updateTargets = getRepositoriesNeedingDependencyUpdates({ + scannedRepositories, + releasedPackageVersions, + excludeRepositoryNames: releasedRepositoryNames, + }) + + for (const candidate of updateTargets) { + const { bumpedPackageNames: packagesToUpdate } = getDependencyInstallSpecs( + { + dependencies: candidate.dependencies, + releasedPackageVersions, + }, + ) + let isUpdating: 'yes' | 'no' - if (force) { + if (shouldForceUpdateDependency) { isUpdating = 'yes' console.log( - `Auto-updating "${dependency}" in "${productRepository.repositoryName}" (${currentDependencyVersion} > ${dependencyResult.packageJson.version})`, + `Auto-updating ${packagesToUpdate.join(', ')} in "${candidate.productRepository.repositoryName}"`, ) } else { const updateResult = await enquirer.prompt<{ @@ -113,14 +258,14 @@ export default async function startUpdateDependents({ }>({ type: 'select', name: 'isUpdating', - message: `Would you like to update "${dependency}" in "${productRepository.repositoryName}" (${currentDependencyVersion} > ${dependencyResult.packageJson.version})?`, + message: `Would you like to update ${packagesToUpdate.join(', ')} in "${candidate.productRepository.repositoryName}"?`, choices: [ { message: 'Yes, update dependency!', name: 'yes', }, { - message: `No! "${productRepository.repositoryName}" does not need to be updated.`, + message: `No! "${candidate.productRepository.repositoryName}" does not need to be updated.`, name: 'no', }, ], @@ -128,59 +273,486 @@ export default async function startUpdateDependents({ isUpdating = updateResult.isUpdating } if (isUpdating === 'no') { - return + continue } + const repositoryWorkingDirectory = await ensureRetainedWorkingDirectory({ + retainedClones, + repositoryName: candidate.productRepository.repositoryName, + }) + await executeCommand( 'git', ['checkout', '-b', ticket], repositoryWorkingDirectory, ) - if (isUpdatingTypes === 'yes') { - await executeCommand( - 'npm', - ['install', '--package-lock-only', '-D', '@oneblink/types'], - repositoryWorkingDirectory, + + const bumpedPackageNames = await installDependencyUpdates({ + cwd: repositoryWorkingDirectory, + dependencies: candidate.dependencies, + releasedPackageVersions, + isUpdatingTypes: isUpdatingTypes === 'yes', + }) + + if (!bumpedPackageNames.length && isUpdatingTypes !== 'yes') { + console.log( + `Skipping "${candidate.productRepository.repositoryName}" as there were no dependency updates to apply.`, ) + continue } - await executeCommand( - 'npm', - [ - 'install', - '--package-lock-only', - '--save', - `${dependency}@${dependencyResult.packageJson.version}`, - ], - repositoryWorkingDirectory, - ) - await executeCommand('git', ['add', '-A'], repositoryWorkingDirectory) - await executeCommand( - 'git', - ['commit', '--message', `${ticket} # Bumped ${dependency}`], - repositoryWorkingDirectory, - ) + + await commitDependencyUpdates({ + cwd: repositoryWorkingDirectory, + ticket, + bumpedPackageNames, + isUpdatingTypes: isUpdatingTypes === 'yes', + }) await executeCommand( 'git', ['push', '-u', 'origin', ticket], repositoryWorkingDirectory, ) createPullRequestUrls.push( - `https://github.com/oneblink/${productRepository.repositoryName}/pull/new/${ticket}`, + `https://github.com/oneblink/${candidate.productRepository.repositoryName}/pull/new/${ticket}`, + ) + } + + if (releasedPackageSummaries.length) { + console.log( + boxen( + `The following NPM packages were released during this run: + + ${releasedPackageSummaries.join(` + `)}`, + { + padding: 1, + }, + ), ) + } + + if (createPullRequestUrls.length) { + console.log( + boxen( + `The following Pull Requests can be created: + + ${createPullRequestUrls.join(` + `)}`, + { + padding: 1, + }, + ), + ) + } + } finally { + await removeRetainedClones(retainedClones) + } +} + +async function resolveTicket({ + ticketFlag, + force, +}: { + ticketFlag: string | undefined + force: boolean +}): Promise { + if (ticketFlag) { + if (!TICKET_PATTERN.test(ticketFlag)) { + throw new Error( + 'Ticket must be 1-3 alpha characters, then a hyphen followed by a number', + ) + } + return ticketFlag.toUpperCase() + } + + if (force) { + throw new Error( + 'Cannot use "--force" without "--ticket" because all prompts are skipped.', + ) + } + + const { ticket } = await enquirer.prompt<{ + ticket: string + }>({ + type: 'input', + name: 'ticket', + message: `Ticket to associate with pull requests? (e.g. ON-4323, AP-4323, MS-4323)`, + required: true, + validate: (input) => { + if (!TICKET_PATTERN.test(input)) { + return 'Ticket must be 1-3 alpha characters, then a hyphen followed by a number' + } + return true }, + result: (input) => input.toUpperCase(), + }) + + return ticket +} + +async function resolveIsUpdatingTypes({ + force, +}: { + force: boolean +}): Promise<'yes' | 'no'> { + if (force) { + console.log( + 'Skipping "@oneblink/types" update because "--force" was provided.', + ) + return 'no' + } + + const { isUpdatingTypes } = await enquirer.prompt<{ + isUpdatingTypes: 'yes' | 'no' + }>({ + type: 'select', + name: 'isUpdatingTypes', + message: `Would you like to update "@oneblink/types" as well?`, + choices: [ + { + message: 'Yes, update @oneblink/types', + name: 'yes', + }, + { + message: 'No! "@oneblink/types" does not need to be updated.', + name: 'no', + }, + ], + }) + + return isUpdatingTypes +} + +async function resolveNextVersion({ + repositoryPlugin, + force, +}: { + repositoryPlugin: Awaited> + force: boolean +}): Promise { + if (!force) { + const { nextVersion } = await promptForNextVersion({ + repositoryPlugin, + noPreRelease: false, + }) + return nextVersion + } + + const currentVersion = await repositoryPlugin.getCurrentVersion() + const currentSemverVersion = semver.parse(currentVersion) + if (!currentSemverVersion) { + throw new Error( + `Could not determine current version for ${repositoryPlugin.displayType} repository: ${repositoryPlugin.cwd}`, + ) + } + + const autoIncrementVersion = + await repositoryPlugin.autoIncrementVersion?.(currentSemverVersion) + if (autoIncrementVersion) { + console.log( + `Auto-selecting next version "${autoIncrementVersion}" because "--force" was provided.`, + ) + return autoIncrementVersion + } + + const nextSemverVersion = currentSemverVersion.inc( + currentSemverVersion.prerelease.length ? 'prerelease' : 'patch', ) + if (!nextSemverVersion) { + throw new Error( + `Could not auto-increment version "${currentVersion}" because "--force" was provided.`, + ) + } + + console.log( + `Auto-selecting next version "${nextSemverVersion.version}" because "--force" was provided.`, + ) + return nextSemverVersion.version +} + +async function scanProductRepositories({ + retainedClones, +}: { + retainedClones: Map +}): Promise<{ + scannedRepositories: ScannedProductRepository[] +}> { + const scannedRepositories: ScannedProductRepository[] = [] + + for (const productRepository of productRepositories) { + const { + cloneRepository, + repositoryWorkingDirectory, + removeRepositoryWorkingDirectory, + } = await prepareCloneRepository({ + repositoryName: productRepository.repositoryName, + }) + + try { + await cloneRepository() + + const scannedRepository = await readScannedProductRepository({ + productRepository, + repositoryWorkingDirectory, + }) + scannedRepositories.push(scannedRepository) + + if (scannedRepository.supportsDependencyUpdates) { + retainedClones.set(productRepository.repositoryName, { + repositoryWorkingDirectory, + removeRepositoryWorkingDirectory, + }) + } else { + await removeRepositoryWorkingDirectory() + } + } catch (error) { + await removeRepositoryWorkingDirectory() + throw error + } + } - if (createPullRequestUrls.length) { + return { + scannedRepositories, + } +} + +async function readScannedProductRepository({ + productRepository, + repositoryWorkingDirectory, +}: { + productRepository: ProductRepository + repositoryWorkingDirectory: string +}): Promise { + const repositoryPlugin = await getRepositoryPlugin({ + cwd: repositoryWorkingDirectory, + repositoryType: productRepository, + }) + if (!repositoryPlugin.supportsDependencyUpdates) { console.log( - boxen( - `The following Pull Requests can be created: + `Skipping "${productRepository.repositoryName}" as "${repositoryPlugin.displayType}" does not support updating dependencies.`, + ) + return { + productRepository, + packageName: undefined, + packageVersion: undefined, + dependencies: {}, + supportsDependencyUpdates: false, + } + } - ${createPullRequestUrls.join(` - `)}`, - { - padding: 1, - }, - ), + const result = await readPackageUp({ + cwd: repositoryWorkingDirectory, + }) + return { + productRepository, + packageName: result?.packageJson.name, + packageVersion: result?.packageJson.version, + dependencies: result?.packageJson.dependencies || {}, + supportsDependencyUpdates: true, + } +} + +function getRepositoryNamesToRetain({ + candidates, + intermediates, + scannedRepositories, +}: { + candidates: DependentCandidate[] + intermediates: DependentCandidate[] + scannedRepositories: ScannedProductRepository[] +}): Set { + const intermediatePackageNames = new Set( + intermediates.map((intermediate) => intermediate.packageName), + ) + const repositoryNamesToRetain = new Set( + candidates.map((candidate) => candidate.productRepository.repositoryName), + ) + + for (const scannedRepository of scannedRepositories) { + const dependsOnIntermediate = [...intermediatePackageNames].some( + (packageName) => scannedRepository.dependencies[packageName], + ) + if (dependsOnIntermediate) { + repositoryNamesToRetain.add( + scannedRepository.productRepository.repositoryName, + ) + } + } + + return repositoryNamesToRetain +} + +async function pruneRetainedClones({ + retainedClones, + repositoryNamesToRetain, +}: { + retainedClones: Map + repositoryNamesToRetain: Set +}) { + for (const [repositoryName, retainedClone] of retainedClones) { + if (repositoryNamesToRetain.has(repositoryName)) { + continue + } + await retainedClone.removeRepositoryWorkingDirectory() + retainedClones.delete(repositoryName) + } +} + +function getRetainedWorkingDirectory({ + retainedClones, + repositoryName, +}: { + retainedClones: Map + repositoryName: string +}): string { + const retainedClone = retainedClones.get(repositoryName) + if (!retainedClone) { + throw new Error( + `Expected retained clone for "${repositoryName}" but none was found.`, + ) + } + return retainedClone.repositoryWorkingDirectory +} + +async function ensureRetainedWorkingDirectory({ + retainedClones, + repositoryName, +}: { + retainedClones: Map + repositoryName: string +}): Promise { + const existingClone = retainedClones.get(repositoryName) + if (existingClone) { + return existingClone.repositoryWorkingDirectory + } + + const { + cloneRepository, + repositoryWorkingDirectory, + removeRepositoryWorkingDirectory, + } = await prepareCloneRepository({ + repositoryName, + }) + await cloneRepository() + retainedClones.set(repositoryName, { + repositoryWorkingDirectory, + removeRepositoryWorkingDirectory, + }) + return repositoryWorkingDirectory +} + +async function removeRetainedClones( + retainedClones: Map, +) { + for (const retainedClone of retainedClones.values()) { + await retainedClone.removeRepositoryWorkingDirectory() + } + retainedClones.clear() +} + +function logSkippedRepository({ + scannedRepository, + dependency, + dependencyVersion, + candidates, +}: { + scannedRepository: ScannedProductRepository + dependency: string + dependencyVersion: string + candidates: DependentCandidate[] +}) { + if (!scannedRepository.supportsDependencyUpdates) { + return + } + + if ( + candidates.some( + (candidate) => + candidate.productRepository.repositoryName === + scannedRepository.productRepository.repositoryName, + ) + ) { + return + } + + const currentDependencyVersion = scannedRepository.dependencies[dependency] + if (!currentDependencyVersion) { + console.log( + `Skipping "${scannedRepository.productRepository.repositoryName}" as it does not contain "${dependency}" as a dependency.`, + ) + return + } + if (currentDependencyVersion === `^${dependencyVersion}`) { + console.log( + `Skipping "${scannedRepository.productRepository.repositoryName}" as it's version of "${dependency}" is already "${currentDependencyVersion}".`, + ) + return + } + if (!scannedRepository.packageName || !scannedRepository.packageVersion) { + console.log( + `Skipping "${scannedRepository.productRepository.repositoryName}" as a package name/version could not be determined.`, + ) + } +} + +async function installDependencyUpdates({ + cwd, + dependencies, + releasedPackageVersions, + isUpdatingTypes, +}: { + cwd: string + dependencies: Record + releasedPackageVersions: Map + isUpdatingTypes: boolean +}): Promise { + const { bumpedPackageNames, installSpecs } = getDependencyInstallSpecs({ + dependencies, + releasedPackageVersions, + }) + + if (isUpdatingTypes) { + await executeCommand( + 'npm', + ['install', '--package-lock-only', '-D', '@oneblink/types'], + cwd, ) } + + if (installSpecs.length) { + await executeCommand( + 'npm', + ['install', '--package-lock-only', '--save', ...installSpecs], + cwd, + ) + } + + return bumpedPackageNames +} + +async function commitDependencyUpdates({ + cwd, + ticket, + bumpedPackageNames, + isUpdatingTypes, +}: { + cwd: string + ticket: string + bumpedPackageNames: string[] + isUpdatingTypes: boolean +}) { + await executeCommand('git', ['add', '-A'], cwd) + await executeCommand( + 'git', + [ + 'commit', + '--message', + buildDependencyBumpCommitMessage({ + ticket, + bumpedPackageNames, + isUpdatingTypes, + }), + ], + cwd, + ) } diff --git a/src/topologicallySortByPackageDependencies.ts b/src/topologicallySortByPackageDependencies.ts new file mode 100644 index 0000000..fd0780b --- /dev/null +++ b/src/topologicallySortByPackageDependencies.ts @@ -0,0 +1,43 @@ +export default function topologicallySortByPackageDependencies< + T extends { + packageName: string + dependencies: Record + }, +>(packages: T[]): T[] { + const packagesByName = new Map( + packages.map((pkg) => [pkg.packageName, pkg] as const), + ) + const sorted: T[] = [] + const visited = new Set() + const visiting = new Set() + + function visit(pkg: T) { + if (visited.has(pkg.packageName)) { + return + } + if (visiting.has(pkg.packageName)) { + throw new Error( + `Circular dependency detected while ordering NPM packages to release (involving "${pkg.packageName}")`, + ) + } + + visiting.add(pkg.packageName) + + for (const dependencyName of Object.keys(pkg.dependencies)) { + const dependency = packagesByName.get(dependencyName) + if (dependency) { + visit(dependency) + } + } + + visiting.delete(pkg.packageName) + visited.add(pkg.packageName) + sorted.push(pkg) + } + + for (const pkg of packages) { + visit(pkg) + } + + return sorted +} diff --git a/src/updateDependentsPlanning.ts b/src/updateDependentsPlanning.ts new file mode 100644 index 0000000..a6a77e8 --- /dev/null +++ b/src/updateDependentsPlanning.ts @@ -0,0 +1,172 @@ +import { ProductRepository } from './enumerateProductRepositories.js' + +export type ScannedProductRepository = { + productRepository: ProductRepository + packageName: string | undefined + packageVersion: string | undefined + dependencies: Record + supportsDependencyUpdates: boolean +} + +export type DependentCandidate = { + productRepository: ProductRepository + packageName: string + packageVersion: string + dependencies: Record + currentSourceDependencyVersion: string +} + +export function getDependentCandidates({ + scannedRepositories, + dependency, + dependencyVersion, +}: { + scannedRepositories: ScannedProductRepository[] + dependency: string + dependencyVersion: string +}): DependentCandidate[] { + const expectedDependencyVersion = `^${dependencyVersion}` + const candidates: DependentCandidate[] = [] + + for (const scannedRepository of scannedRepositories) { + if (!scannedRepository.supportsDependencyUpdates) { + continue + } + + const currentDependencyVersion = scannedRepository.dependencies[dependency] + if (!currentDependencyVersion) { + continue + } + if (currentDependencyVersion === expectedDependencyVersion) { + continue + } + if (!scannedRepository.packageName || !scannedRepository.packageVersion) { + continue + } + + candidates.push({ + productRepository: scannedRepository.productRepository, + packageName: scannedRepository.packageName, + packageVersion: scannedRepository.packageVersion, + dependencies: scannedRepository.dependencies, + currentSourceDependencyVersion: currentDependencyVersion, + }) + } + + return candidates +} + +export function getIntermediateNpmDependents({ + candidates, + scannedRepositories, +}: { + candidates: DependentCandidate[] + scannedRepositories: ScannedProductRepository[] +}): DependentCandidate[] { + return candidates.filter((candidate) => { + if (candidate.productRepository.type !== 'NPM') { + return false + } + + return scannedRepositories.some( + (scannedRepository) => + scannedRepository.productRepository.repositoryName !== + candidate.productRepository.repositoryName && + !!scannedRepository.dependencies[candidate.packageName], + ) + }) +} + +export function getDownstreamRepositoryNames({ + scannedRepositories, + intermediate, +}: { + scannedRepositories: ScannedProductRepository[] + intermediate: DependentCandidate +}): string[] { + return scannedRepositories + .filter( + (scannedRepository) => + scannedRepository.productRepository.repositoryName !== + intermediate.productRepository.repositoryName && + !!scannedRepository.dependencies[intermediate.packageName], + ) + .map( + (scannedRepository) => + scannedRepository.productRepository.repositoryName, + ) +} + +export function getRepositoriesNeedingDependencyUpdates({ + scannedRepositories, + releasedPackageVersions, + excludeRepositoryNames, +}: { + scannedRepositories: ScannedProductRepository[] + releasedPackageVersions: Map + excludeRepositoryNames?: Set +}): DependentCandidate[] { + const candidatesByRepositoryName = new Map() + + for (const [packageName, version] of releasedPackageVersions) { + for (const candidate of getDependentCandidates({ + scannedRepositories, + dependency: packageName, + dependencyVersion: version, + })) { + const repositoryName = candidate.productRepository.repositoryName + if (excludeRepositoryNames?.has(repositoryName)) { + continue + } + if (!candidatesByRepositoryName.has(repositoryName)) { + candidatesByRepositoryName.set(repositoryName, candidate) + } + } + } + + return [...candidatesByRepositoryName.values()] +} + +export function getDependencyInstallSpecs({ + dependencies, + releasedPackageVersions, +}: { + dependencies: Record + releasedPackageVersions: Map +}): { + bumpedPackageNames: string[] + installSpecs: string[] +} { + const bumpedPackageNames: string[] = [] + const installSpecs: string[] = [] + + for (const [packageName, version] of releasedPackageVersions) { + const currentVersion = dependencies[packageName] + if (currentVersion && currentVersion !== `^${version}`) { + installSpecs.push(`${packageName}@${version}`) + bumpedPackageNames.push(packageName) + } + } + + return { + bumpedPackageNames, + installSpecs, + } +} + +export function buildDependencyBumpCommitMessage({ + ticket, + bumpedPackageNames, + isUpdatingTypes, +}: { + ticket: string + bumpedPackageNames: string[] + isUpdatingTypes: boolean +}): string { + const commitPackageNames = [...bumpedPackageNames] + if (isUpdatingTypes) { + commitPackageNames.push('@oneblink/types') + } + + return `${ticket} # Bumped ${commitPackageNames.join(', ')}` +} diff --git a/src/waitForNpmPackageVersion.ts b/src/waitForNpmPackageVersion.ts new file mode 100644 index 0000000..e820874 --- /dev/null +++ b/src/waitForNpmPackageVersion.ts @@ -0,0 +1,51 @@ +import { setTimeout } from 'node:timers/promises' +import { execa } from 'execa' +import wrapWithLoading from './wrapWithLoading.js' + +const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000 +const DEFAULT_POLL_INTERVAL_MS = 15_000 + +export default async function waitForNpmPackageVersion({ + packageName, + version, + timeoutMs = DEFAULT_TIMEOUT_MS, + pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, +}: { + packageName: string + version: string + timeoutMs?: number + pollIntervalMs?: number +}): Promise { + await wrapWithLoading( + { + startText: `Waiting for ${packageName}@${version} to be published to npm`, + failText: `Timed out waiting for ${packageName}@${version} to be published to npm`, + }, + async (spinner) => { + const startedAt = Date.now() + + while (Date.now() - startedAt < timeoutMs) { + const { stdout, exitCode } = await execa( + 'npm', + ['view', `${packageName}@${version}`, 'version'], + { + reject: false, + }, + ) + + if (exitCode === 0 && stdout.trim() === version) { + spinner.succeed(`${packageName}@${version} is available on npm`) + return + } + + const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000) + spinner.text = `Waiting for ${packageName}@${version} to be published to npm (${elapsedSeconds}s)` + await setTimeout(pollIntervalMs) + } + + throw new Error( + `Timed out after ${timeoutMs}ms waiting for ${packageName}@${version} to be published to npm. Investigate why the package was not published (for example CI or npm publish failures), then decide whether it is safe to run the command again from the beginning.`, + ) + }, + ) +} diff --git a/test/topologicallySortByPackageDependencies.test.ts b/test/topologicallySortByPackageDependencies.test.ts new file mode 100644 index 0000000..7b052f2 --- /dev/null +++ b/test/topologicallySortByPackageDependencies.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' +import topologicallySortByPackageDependencies from '../src/topologicallySortByPackageDependencies.js' + +describe('topologicallySortByPackageDependencies', () => { + it('orders packages so dependencies are released before dependents', () => { + const sorted = topologicallySortByPackageDependencies([ + { + packageName: '@oneblink/apps-react', + dependencies: { + '@oneblink/sdk-core': '^10.0.0', + '@oneblink/storage': '^7.0.0', + }, + }, + { + packageName: '@oneblink/storage', + dependencies: { + '@oneblink/sdk-core': '^10.0.0', + }, + }, + ]) + + expect(sorted.map((pkg) => pkg.packageName)).toEqual([ + '@oneblink/storage', + '@oneblink/apps-react', + ]) + }) + + it('keeps unrelated packages in a stable visit order', () => { + const sorted = topologicallySortByPackageDependencies([ + { + packageName: '@oneblink/cli', + dependencies: {}, + }, + { + packageName: '@oneblink/sdk', + dependencies: {}, + }, + ]) + + expect(sorted.map((pkg) => pkg.packageName)).toEqual([ + '@oneblink/cli', + '@oneblink/sdk', + ]) + }) + + it('throws when a circular dependency is detected', () => { + expect(() => + topologicallySortByPackageDependencies([ + { + packageName: 'a', + dependencies: { + b: '^1.0.0', + }, + }, + { + packageName: 'b', + dependencies: { + a: '^1.0.0', + }, + }, + ]), + ).toThrow('Circular dependency detected') + }) +}) diff --git a/test/updateDependentsPlanning.test.ts b/test/updateDependentsPlanning.test.ts new file mode 100644 index 0000000..39992cc --- /dev/null +++ b/test/updateDependentsPlanning.test.ts @@ -0,0 +1,404 @@ +import { describe, expect, it } from 'vitest' +import { ProductRepository } from '../src/enumerateProductRepositories.js' +import { + buildDependencyBumpCommitMessage, + getDependencyInstallSpecs, + getDependentCandidates, + getDownstreamRepositoryNames, + getIntermediateNpmDependents, + getRepositoriesNeedingDependencyUpdates, + ScannedProductRepository, +} from '../src/updateDependentsPlanning.js' + +const sdkCore = createProductRepository({ + label: '@oneblink/sdk-core (NPM package)', + repositoryName: 'sdk-core-js', + type: 'NPM', + isPublic: true, +}) + +const appsReact = createProductRepository({ + label: '@oneblink/apps-react (NPM package)', + repositoryName: 'apps-react', + type: 'NPM', + isPublic: true, +}) + +const formsRenderer = createProductRepository({ + label: 'Forms Renderer', + repositoryName: 'product-forms-renderer', + type: 'NODE_JS', + isPublic: false, +}) + +const formsCdn = createProductRepository({ + label: 'Embedded Forms Script', + repositoryName: 'forms-cdn', + type: 'CDN_HOSTING', + isPublic: true, +}) + +const nugetSdk = createProductRepository({ + label: 'OneBlink.SDK (Nuget package)', + repositoryName: 'sdk-dotnet', + type: 'NUGET', + isPublic: true, + relativeProjectFile: 'OneBlink.SDK/OneBlink.SDK.csproj', +}) + +describe('getDependentCandidates', () => { + it('returns repositories that depend on an outdated source package version', () => { + const candidates = getDependentCandidates({ + scannedRepositories: [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + createScannedRepository({ + productRepository: formsRenderer, + packageName: 'forms-renderer', + packageVersion: '2.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + '@oneblink/apps-react': '^11.0.0', + }, + }), + createScannedRepository({ + productRepository: nugetSdk, + supportsDependencyUpdates: false, + }), + ], + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + expect(candidates.map((candidate) => candidate.packageName)).toEqual([ + '@oneblink/apps-react', + 'forms-renderer', + ]) + }) + + it('skips repositories already on the expected dependency version', () => { + const candidates = getDependentCandidates({ + scannedRepositories: [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^10.1.0', + }, + }), + ], + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + expect(candidates).toEqual([]) + }) + + it('skips repositories that do not depend on the source package', () => { + const candidates = getDependentCandidates({ + scannedRepositories: [ + createScannedRepository({ + productRepository: sdkCore, + packageName: '@oneblink/sdk-core', + packageVersion: '10.1.0', + dependencies: {}, + }), + ], + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + expect(candidates).toEqual([]) + }) +}) + +describe('getIntermediateNpmDependents', () => { + it('identifies NPM packages that other scanned repositories also depend on', () => { + const scannedRepositories = [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + createScannedRepository({ + productRepository: formsRenderer, + packageName: 'forms-renderer', + packageVersion: '2.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + '@oneblink/apps-react': '^11.0.0', + }, + }), + createScannedRepository({ + productRepository: formsCdn, + packageName: 'forms-cdn', + packageVersion: '1.0.0', + dependencies: { + '@oneblink/apps-react': '^11.0.0', + }, + }), + ] + const candidates = getDependentCandidates({ + scannedRepositories, + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + const intermediates = getIntermediateNpmDependents({ + candidates, + scannedRepositories, + }) + + expect(intermediates.map((candidate) => candidate.packageName)).toEqual([ + '@oneblink/apps-react', + ]) + }) + + it('treats an NPM candidate as intermediate when only non-source dependents use it', () => { + const scannedRepositories = [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + createScannedRepository({ + productRepository: formsCdn, + packageName: 'forms-cdn', + packageVersion: '1.0.0', + dependencies: { + '@oneblink/apps-react': '^11.0.0', + }, + }), + ] + const candidates = getDependentCandidates({ + scannedRepositories, + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + expect( + getIntermediateNpmDependents({ + candidates, + scannedRepositories, + }).map((candidate) => candidate.packageName), + ).toEqual(['@oneblink/apps-react']) + }) + + it('does not treat NODE_JS repositories as intermediate NPM packages', () => { + const scannedRepositories = [ + createScannedRepository({ + productRepository: formsRenderer, + packageName: 'forms-renderer', + packageVersion: '2.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + ] + const candidates = getDependentCandidates({ + scannedRepositories, + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + + expect( + getIntermediateNpmDependents({ + candidates, + scannedRepositories, + }), + ).toEqual([]) + }) +}) + +describe('getDownstreamRepositoryNames', () => { + it('includes repositories that depend on the intermediate even without the source package', () => { + const scannedRepositories = [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + createScannedRepository({ + productRepository: formsRenderer, + packageName: 'forms-renderer', + packageVersion: '2.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + '@oneblink/apps-react': '^11.0.0', + }, + }), + createScannedRepository({ + productRepository: formsCdn, + packageName: 'forms-cdn', + packageVersion: '1.0.0', + dependencies: { + '@oneblink/apps-react': '^11.0.0', + }, + }), + ] + const candidates = getDependentCandidates({ + scannedRepositories, + dependency: '@oneblink/sdk-core', + dependencyVersion: '10.1.0', + }) + const [intermediate] = getIntermediateNpmDependents({ + candidates, + scannedRepositories, + }) + + expect( + getDownstreamRepositoryNames({ + scannedRepositories, + intermediate, + }), + ).toEqual(['product-forms-renderer', 'forms-cdn']) + }) +}) + +describe('getRepositoriesNeedingDependencyUpdates', () => { + it('includes repositories that only depend on a released intermediate package', () => { + const scannedRepositories = [ + createScannedRepository({ + productRepository: appsReact, + packageName: '@oneblink/apps-react', + packageVersion: '11.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + }, + }), + createScannedRepository({ + productRepository: formsRenderer, + packageName: 'forms-renderer', + packageVersion: '2.0.0', + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + '@oneblink/apps-react': '^11.0.0', + }, + }), + createScannedRepository({ + productRepository: formsCdn, + packageName: 'forms-cdn', + packageVersion: '1.0.0', + dependencies: { + '@oneblink/apps-react': '^11.0.0', + }, + }), + ] + + expect( + getRepositoriesNeedingDependencyUpdates({ + scannedRepositories, + releasedPackageVersions: new Map([ + ['@oneblink/sdk-core', '10.1.0'], + ['@oneblink/apps-react', '11.2.0'], + ]), + excludeRepositoryNames: new Set(['apps-react']), + }).map((candidate) => candidate.productRepository.repositoryName), + ).toEqual(['product-forms-renderer', 'forms-cdn']) + }) +}) + +describe('getDependencyInstallSpecs', () => { + it('returns install specs for outdated released packages that are already dependencies', () => { + expect( + getDependencyInstallSpecs({ + dependencies: { + '@oneblink/sdk-core': '^9.0.0', + '@oneblink/apps-react': '^11.0.0', + lodash: '^4.0.0', + }, + releasedPackageVersions: new Map([ + ['@oneblink/sdk-core', '10.1.0'], + ['@oneblink/apps-react', '11.2.0'], + ['@oneblink/storage', '7.1.0'], + ]), + }), + ).toEqual({ + bumpedPackageNames: ['@oneblink/sdk-core', '@oneblink/apps-react'], + installSpecs: [ + '@oneblink/sdk-core@10.1.0', + '@oneblink/apps-react@11.2.0', + ], + }) + }) + + it('skips packages already at the released version', () => { + expect( + getDependencyInstallSpecs({ + dependencies: { + '@oneblink/sdk-core': '^10.1.0', + }, + releasedPackageVersions: new Map([['@oneblink/sdk-core', '10.1.0']]), + }), + ).toEqual({ + bumpedPackageNames: [], + installSpecs: [], + }) + }) +}) + +describe('buildDependencyBumpCommitMessage', () => { + it('includes bumped package names', () => { + expect( + buildDependencyBumpCommitMessage({ + ticket: 'ON-1234', + bumpedPackageNames: ['@oneblink/sdk-core', '@oneblink/apps-react'], + isUpdatingTypes: false, + }), + ).toBe('ON-1234 # Bumped @oneblink/sdk-core, @oneblink/apps-react') + }) + + it('appends @oneblink/types when types are being updated', () => { + expect( + buildDependencyBumpCommitMessage({ + ticket: 'ON-1234', + bumpedPackageNames: ['@oneblink/sdk-core'], + isUpdatingTypes: true, + }), + ).toBe('ON-1234 # Bumped @oneblink/sdk-core, @oneblink/types') + }) +}) + +function createProductRepository( + productRepository: ProductRepository, +): ProductRepository { + return productRepository +} + +function createScannedRepository({ + productRepository, + packageName, + packageVersion, + dependencies = {}, + supportsDependencyUpdates = true, +}: { + productRepository: ProductRepository + packageName?: string + packageVersion?: string + dependencies?: Record + supportsDependencyUpdates?: boolean +}): ScannedProductRepository { + return { + productRepository, + packageName, + packageVersion, + dependencies, + supportsDependencyUpdates, + } +} diff --git a/test/waitForNpmPackageVersion.test.ts b/test/waitForNpmPackageVersion.test.ts new file mode 100644 index 0000000..ff79427 --- /dev/null +++ b/test/waitForNpmPackageVersion.test.ts @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { execaMock, setTimeoutMock, wrapWithLoadingMock } = vi.hoisted(() => ({ + execaMock: vi.fn(), + setTimeoutMock: vi.fn(async () => undefined), + wrapWithLoadingMock: vi.fn( + async ( + _options: { startText: string; failText: string }, + fn: (spinner: { succeed: ReturnType; text: string }) => Promise, + ) => { + const spinner = { + succeed: vi.fn(), + text: '', + } + await fn(spinner) + return undefined + }, + ), +})) + +vi.mock('execa', () => ({ + execa: execaMock, +})) + +vi.mock('node:timers/promises', () => ({ + setTimeout: setTimeoutMock, +})) + +vi.mock('../src/wrapWithLoading.js', () => ({ + default: wrapWithLoadingMock, +})) + +import waitForNpmPackageVersion from '../src/waitForNpmPackageVersion.js' + +describe('waitForNpmPackageVersion', () => { + beforeEach(() => { + vi.clearAllMocks() + setTimeoutMock.mockResolvedValue(undefined) + }) + + it('resolves when npm view returns the expected version', async () => { + execaMock.mockResolvedValueOnce({ + stdout: '10.1.0\n', + exitCode: 0, + }) + + await waitForNpmPackageVersion({ + packageName: '@oneblink/sdk-core', + version: '10.1.0', + timeoutMs: 1_000, + pollIntervalMs: 10, + }) + + expect(execaMock).toHaveBeenCalledWith( + 'npm', + ['view', '@oneblink/sdk-core@10.1.0', 'version'], + { reject: false }, + ) + expect(setTimeoutMock).not.toHaveBeenCalled() + }) + + it('polls until the package version becomes available', async () => { + execaMock + .mockResolvedValueOnce({ + stdout: '', + exitCode: 1, + }) + .mockResolvedValueOnce({ + stdout: '10.1.0', + exitCode: 0, + }) + + await waitForNpmPackageVersion({ + packageName: '@oneblink/sdk-core', + version: '10.1.0', + timeoutMs: 1_000, + pollIntervalMs: 25, + }) + + expect(execaMock).toHaveBeenCalledTimes(2) + expect(setTimeoutMock).toHaveBeenCalledWith(25) + }) + + it('throws when the package is not published before the timeout', async () => { + execaMock.mockResolvedValue({ + stdout: '', + exitCode: 1, + }) + + const nowSpy = vi.spyOn(Date, 'now') + nowSpy.mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValue(2_000) + + await expect( + waitForNpmPackageVersion({ + packageName: '@oneblink/sdk-core', + version: '10.1.0', + timeoutMs: 1_000, + pollIntervalMs: 10, + }), + ).rejects.toThrow( + 'Timed out after 1000ms waiting for @oneblink/sdk-core@10.1.0 to be published to npm. Investigate why the package was not published (for example CI or npm publish failures), then decide whether it is safe to run the command again from the beginning.', + ) + + nowSpy.mockRestore() + }) +}) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 5d5c990..d6ae085 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - "include": ["./src", "eslint.config.mjs"], + "include": ["./src", "./test", "eslint.config.mjs", "vitest.config.ts"], "compilerOptions": { "rootDir": "./" } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fee72c8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +})