From 7d823307fa3b8a3d42953590fd55cf8686e97395 Mon Sep 17 00:00:00 2001 From: stanig2106 Date: Fri, 14 Nov 2025 19:23:18 +0100 Subject: [PATCH] feat: mirror v-can guard chains --- CHANGELOG.md | 10 + README.md | 90 ++++- ROADMAP.md | 12 +- package.json | 2 +- playground/app.vue | 84 ++--- playground/permissions/__can__.ts | 4 +- src/module/typegen.ts | 2 +- src/runtime/plugin.ts | 2 +- src/runtime/transformer/patches.ts | 364 +++++++++++++++++--- test/chains.test.ts | 21 ++ test/fixtures/basic/permissions/__can__.ts | 4 +- test/fixtures/chains/app.vue | 31 ++ test/fixtures/chains/nuxt.config.ts | 12 + test/fixtures/chains/package.json | 5 + test/fixtures/chains/permissions/__can__.ts | 6 + test/transform-can.test.ts | 62 +++- 16 files changed, 573 insertions(+), 138 deletions(-) create mode 100644 test/chains.test.ts create mode 100644 test/fixtures/chains/app.vue create mode 100644 test/fixtures/chains/nuxt.config.ts create mode 100644 test/fixtures/chains/package.json create mode 100644 test/fixtures/chains/permissions/__can__.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bea126..9605a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2025-11-15 +### Added +- Automatically mirror `v-can` guards across `v-else-if` / `v-else` branches and surface the inferred expression in documentation and fixtures. +- New `chains` playground fixture plus SSR tests that cover guard mirroring and explicit `v-cannot` fallbacks. + +### Changed +- Enforce adjacency rules for implicit `v-cannot` blocks while allowing wrapped fallbacks when the permission expression is provided. +- Expand README guidance with explicit examples for guard mirroring, wrapped fallbacks, and known DX errors. + ## [1.0.1] - 2025-11-14 ### Changed - Publish as the scoped package `@eduvia-app/nuxt-can` and document the scoped install steps. @@ -26,5 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - English README describing usage, playground, and contribution guide. - Roadmap and release prep guidance. +[1.1.0]: https://github.com/eduvia-app/nuxt-can/releases/tag/v1.1.0 [1.0.1]: https://github.com/eduvia-app/nuxt-can/releases/tag/v1.0.1 [1.0.0]: https://github.com/eduvia-app/nuxt-can/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 89f31cb..8ae4619 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Provide the `__can__` implementation referenced above: // permissions/can.ts const permissionsStore = usePermissionsStore() -export function __can__(path: string[]) { +export function __can__(...path: string[]) { return permissionsStore.check(path.join('.')) } ``` @@ -72,18 +72,94 @@ Now you can write directives that stay type-safe: …and the compiler rewrites them into plain conditionals: ```vue - - -

Access denied

+ + +

Access denied

+``` + +## Directive Patterns + +### Guard entire `v-if` / `v-else-if` / `v-else` chains + +Once the first branch of a conditional chain carries `v-can`, the transformer automatically mirrors that guard (with the same permission path) on every subsequent `v-else-if` and `v-else`. You can still repeat the directive manually for clarity, but it’s no longer required. + +```vue +
+ Draft state +
+
+ Pending state +
+
+ Fallback state +
+
+ Missing permission +
+``` + +Transforms into: + +```vue +
+ Draft state +
+
+ Pending state +
+
+ Fallback state +
+
+ Missing permission +
+``` + +### Pass arguments to `v-cannot` + +`v-cannot` can mirror the permission expression used by its matching `v-can` by adding the same argument (`v-cannot="can.foo.bar"`). When no argument is specified, the directive must immediately follow the preceding `v-can` block so the transformer can re-use that context. + +```vue + +

Contact your admin to unlock submissions.

+ + + + +
+

Only editors can update this contract.

+
+``` + +Both `v-cannot` branches above compile to `v-if="!__can__('contract', 'submit')"` and `v-if="!__can__('contract', 'edit')"` respectively. + +### Keep `v-cannot` next to its `v-can` + +When `v-cannot` omits an expression, it must immediately follow the guarded block: + +```vue +
Ready!
+

Not allowed

+ +
+

Not allowed

+
+ +
+

Not allowed

+
``` ## Usage Rules & Errors The transformer validates every template and throws descriptive errors when: -- `v-cannot` does not immediately follow its matching `v-can`. -- `v-can` appears on an element already using `v-else` / `v-else-if`. -- `v-cannot` uses an argument, modifiers, or a `v-if` condition. +- `v-can` expressions differ within the same `v-if` / `v-else-if` / `v-else` block (the guard is mirrored automatically, but mixed expressions are disallowed). +- `v-cannot` without an argument is separated from its originating `v-can`. +- `v-cannot` mixes in modifiers or a `v-if` condition (keep it standalone). - Multiple `v-cannot` blocks exist for the same `v-can`. - The expression is not a static dotted path like `can.resource.action`. diff --git a/ROADMAP.md b/ROADMAP.md index 7f4652d..2a135fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -110,7 +110,7 @@ Avant : Après compilation : ```vue - + ``` ### Exemple 2 — `v-if` + `v-can` @@ -125,7 +125,7 @@ Avant : Après compilation : ```vue -
+
Modifier le contrat
``` @@ -141,8 +141,8 @@ Avant : Après compilation : ```vue - -

Acces refuse

+ +

Acces refuse

``` ### Exemple 4 — `v-if` / `v-else-if` / `v-else` + `v-can` / `v-cannot` @@ -166,8 +166,8 @@ Résultat : ```vue