Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
54 changes: 27 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 29 additions & 4 deletions src/lib/helpers/ability.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Comment thread
PierreBrisorgueil marked this conversation as resolved.
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
Expand Down
Loading