diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 08c8cde2d..14b47632d 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -4,6 +4,27 @@ Breaking changes and upgrade notes for downstream projects. --- +## @casl/ability v6 → v7 + @casl/vue v2 → v3 (2026-05-22) + +`@casl/ability` `^6.8.1` → `^7.0.0` and `@casl/vue` `^2.2.6` → `^3.0.0`, bumped **together**. + +### What changed (this repo) + +- **`package.json`** — `@casl/ability` `^7.0.0` + `@casl/vue` `^3.0.0`. They must move together: `@casl/vue@2` rejects `@casl/ability@^7` as a peer (build breaks), and `@casl/vue@3` requires `@casl/ability@^7`. +- **`src/lib/helpers/ability.js`** — the shared ability is no longer wrapped with Vue's `reactive()`. v7 freezes the ability's internal rule structures; a `reactive()` proxy over the frozen rules array throws a Proxy get-invariant `TypeError` on the first `.can()` call. Components here call `ability.can()` directly inside computeds, so reactivity must be preserved — the instance is now wrapped with a small local `toReactiveAbility()` helper that tracks the ability's `updated` event via a `ref` read inside `possibleRulesFor` (the path every `can`/`cannot`/`relevantRuleFor` call funnels through). This mirrors `@casl/vue`'s internal `reactiveAbility`, which v3 declares in its `.d.ts` but does **not** export at runtime, so it cannot simply be imported. +- **No change** to `src/main.js` (`app.use(abilitiesPlugin, ability)` API preserved) or to the `subject()` helper used in components (unchanged in v7). + +### Downstream action required + +`package.json` does not auto-propagate via `/update-stack` (`--ours`); the `ability.js` fix does (devkit file, `--theirs`): + +1. Bump **both** `@casl/ability` to `^7.0.0` and `@casl/vue` to `^3.0.0` together, then reinstall. +2. After `/update-stack`, verify `src/lib/helpers/ability.js` wraps the ability with the local `toReactiveAbility()` helper (tracking the `updated` event), not Vue's `reactive()`. +3. If you wrote custom CASL code: never wrap a v7 ability in Vue's `reactive()` (use an `updated`-event wrapper like `toReactiveAbility()` above). If you instantiated `Ability`/`PureAbility` directly, switch to `createMongoAbility` — v7's `Ability` export no longer carries the default conditions matcher. +4. Run unit + e2e to confirm the ability plugin and `Can`/`subject`/computed `can()` usages still work. + +--- + ## PageHeader split + tabs spacing/alignment refactor (2026-05-21, v2) **Non-breaking for default consumers** — affects 3 stack layouts (Account, Organization detail, Admin) which were already refactored upstream. Downstream projects only need to act if they wrote a custom layout using `PageHeader` + `CoreSurfaceTabBar` directly, or relied on the (now removed) `#tabs` slot on `PageHeader`. diff --git a/package-lock.json b/package-lock.json index 8abd482c4..b67703c75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@casl/ability": "^6.8.1", - "@casl/vue": "^2.2.6", + "@casl/ability": "^7.0.0", + "@casl/vue": "^3.0.0", "@fortawesome/fontawesome-free": "^7.2.0", "@tryghost/content-api": "^1.12.8", "@unhead/vue": "^3.1.0", @@ -260,24 +260,24 @@ } }, "node_modules/@casl/ability": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.1.tgz", - "integrity": "sha512-VX5DD1JbSP/DdewZnNwXXaCzve+0pLe14mcUj2l93CdOFAQUT/ylAptNqxf3Wc/jlsuSanAgXza4Z1Iq23dzpQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-7.0.0.tgz", + "integrity": "sha512-QhwRflkTucpdS2uw1XScrzLWbgLYJGvPoq2Xm5OjeRci3dwtPixxnjUKJ04Ss1ivNS9tZQ8y4sjWeelWsrwo4g==", "license": "MIT", "dependencies": { - "@ucast/mongo2js": "^1.3.0" + "@ucast/mongo2js": "^2.0.0" }, "funding": { "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" } }, "node_modules/@casl/vue": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@casl/vue/-/vue-2.2.6.tgz", - "integrity": "sha512-2OofMpdFWHPx99oqAYo4/Ym3P5vSUwUJA/XGvlqichiCLvo7GMu0R7yszxMYm80v9VkQ4ETMG1r4GdDDS+rFYg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@casl/vue/-/vue-3.0.0.tgz", + "integrity": "sha512-ZLJeVchZvmtz0VLTvHwsH4epE+CREBSbFQi0bIUmi0vZh4tF8B4Ylp6YiRWCP4Am7O5caJJqxARQGya2XUX4fw==", "license": "MIT", "peerDependencies": { - "@casl/ability": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", + "@casl/ability": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, @@ -3292,38 +3292,38 @@ } }, "node_modules/@ucast/core": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", - "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-2.0.0.tgz", + "integrity": "sha512-4XVx6LzPXZGvnZO5jp39cm/G4UvuwvEdtmg+9+4+zl6uFkCcB7UJacvtMYeBE56GJVT99Zqy6Pii7dGJq3Kz9Q==", "license": "Apache-2.0" }, "node_modules/@ucast/js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.1.0.tgz", - "integrity": "sha512-eJ7yQeYtMK85UZjxoxBEbTWx6UMxEXKbjVyp+NlzrT5oMKV5Gpo/9bjTl3r7msaXTVC8iD9NJacqJ8yp7joX+Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ucast/js/-/js-4.0.1.tgz", + "integrity": "sha512-9O5xPBvwEWQk2WvO69Eh2WJB8QljVZ2vRVdFvfnKjlZwWXcYxp1lqLBhwXBU1AtuSgCvKhJPkXdjKJggUmAmQQ==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "1.10.2" + "@ucast/core": "2.0.0" } }, "node_modules/@ucast/mongo": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz", - "integrity": "sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-3.0.0.tgz", + "integrity": "sha512-kwuSH+kdB4GCR0LGhy/PEDm4PCflur89AlK82kNiYD0FvsA8A/p+0sx7m+/R8mMFAlmlkAd3VXp7sM/cLLYWYg==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "^1.4.1" + "@ucast/core": "2.0.0" } }, "node_modules/@ucast/mongo2js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.4.1.tgz", - "integrity": "sha512-9aeg5cmqwRQnKCXHN6I17wk83Rcm487bHelaG8T4vfpWneAI469wSI3Srnbu+PuZ5znWRbnwtVq9RgPL+bN6CA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-2.0.0.tgz", + "integrity": "sha512-vNBZzRnsfLr/TSxEoxz6W6hHQ5tmWsfEeC0nCq5z8RezC1AqIRy3cfHm8AGvlGtcn+cTSFQcZremfqnz6wm+nQ==", "license": "Apache-2.0", "dependencies": { - "@ucast/core": "1.10.2", - "@ucast/js": "3.1.0", - "@ucast/mongo": "2.4.3" + "@ucast/core": "2.0.0", + "@ucast/js": "4.0.1", + "@ucast/mongo": "3.0.0" } }, "node_modules/@unhead/bundler": { diff --git a/package.json b/package.json index 382cf7055..6fbb5d15a 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "snyk-protect": "snyk protect" }, "dependencies": { - "@casl/ability": "^6.8.1", - "@casl/vue": "^2.2.6", + "@casl/ability": "^7.0.0", + "@casl/vue": "^3.0.0", "@fortawesome/fontawesome-free": "^7.2.0", "@tryghost/content-api": "^1.12.8", "@unhead/vue": "^3.1.0", diff --git a/src/lib/helpers/ability.js b/src/lib/helpers/ability.js index 0611d2007..7cd907aad 100644 --- a/src/lib/helpers/ability.js +++ b/src/lib/helpers/ability.js @@ -2,14 +2,39 @@ * CASL ability helpers. */ import { createMongoAbility } from '@casl/ability'; -import { reactive } from 'vue'; +import { ref } from 'vue'; + +/** + * @desc Make a CASL ability reactive for Vue without Vue's `reactive()`. + * @casl/ability v7 freezes its internal rule structures, so a `reactive()` + * proxy over the frozen rules array throws a Proxy get-invariant TypeError on + * the first `.can()` call. Instead we track the ability's `updated` event with + * a ref, read inside `possibleRulesFor` (the path every can/cannot/relevantRuleFor + * call funnels through), so component computeds calling `ability.can()` re-run + * when rules change. Mirrors @casl/vue's internal reactiveAbility, which v3 + * declares in its types but does not export at runtime. + * @param {import('@casl/ability').MongoAbility} ability - CASL ability instance + * @returns {import('@casl/ability').MongoAbility} the same instance, made reactive + */ +const toReactiveAbility = (ability) => { + const trigger = ref(true); + ability.on('updated', () => { + trigger.value = !trigger.value; + }); + const possibleRulesFor = ability.possibleRulesFor.bind(ability); + ability.possibleRulesFor = (...args) => { + void trigger.value; // reactive read — tracks ability updates in computeds + return possibleRulesFor(...args); // forward all args (action, subject, field) unchanged + }; + ability.can = ability.can.bind(ability); + ability.cannot = ability.cannot.bind(ability); + return ability; +}; /** * @desc Reactive CASL ability instance initialised with an empty rule set. - * Wrapping the instance with `reactive` keeps Vue templates in sync - * whenever the rules are updated. */ -export const ability = reactive(createMongoAbility([])); +export const ability = toReactiveAbility(createMongoAbility([])); /** * @desc Replace the current ability rules with a new set received from the