From 534bc391977c67c76653a4404f5975f572fd1a49 Mon Sep 17 00:00:00 2001 From: stanig2106 Date: Fri, 14 Nov 2025 05:22:46 +0100 Subject: [PATCH] feat: add directive transformer runtime --- .zed/debug.json | 23 +++ README.md | 99 +++++++++--- bun.lock | 11 +- package.json | 5 +- playground/app.vue | 39 ++++- playground/nuxt.config.ts | 9 +- playground/permissions/__can__.ts | 12 ++ src/module.ts | 32 ++-- src/runtime/transformer/errors.ts | 30 ++++ src/runtime/transformer/nodes.ts | 5 + src/runtime/transformer/patcher.ts | 7 + src/runtime/transformer/patches.ts | 186 +++++++++++++++++++++++ src/runtime/transformer/reporter.ts | 19 +++ src/runtime/transformer/transform-can.ts | 45 ++++++ src/runtime/transformer/types.ts | 11 ++ src/runtime/utils.ts | 10 ++ 16 files changed, 506 insertions(+), 37 deletions(-) create mode 100644 .zed/debug.json create mode 100644 playground/permissions/__can__.ts create mode 100644 src/runtime/transformer/errors.ts create mode 100644 src/runtime/transformer/nodes.ts create mode 100644 src/runtime/transformer/patcher.ts create mode 100644 src/runtime/transformer/patches.ts create mode 100644 src/runtime/transformer/reporter.ts create mode 100644 src/runtime/transformer/transform-can.ts create mode 100644 src/runtime/transformer/types.ts create mode 100644 src/runtime/utils.ts diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..748f87f --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,23 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "adapter": "JavaScript", + "label": "package.json > dev", + "request": "launch", + "type": "pwa-node", + "args": [ + "run", + "dev" + ], + "cwd": "/Users/stani/Documents/campus-prive/apti-connect/nuxt-can", + "runtimeExecutable": "$ZED_CUSTOM_TYPESCRIPT_RUNNER", + "env": {}, + "runtimeArgs": [ + "--inspect-brk" + ], + "console": "integratedTerminal" + } +] diff --git a/README.md b/README.md index b168aa1..85a7b7e 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,100 @@ - - -# My Module +# nuxt-can [![npm version][npm-version-src]][npm-version-href] [![npm downloads][npm-downloads-src]][npm-downloads-href] [![License][license-src]][license-href] [![Nuxt][nuxt-src]][nuxt-href] -My new Nuxt module for doing amazing things. +`nuxt-can` apporte deux directives Vue (`v-can`, `v-cannot`) pour écrire des permissions ultra lisibles dans les templates Nuxt. Les directives sont transformées à la compilation en appels à une fonction `__can__` fournie par l’application hôte tout en restant 100 % tree-shake friendly et parfaitement typées côté TypeScript. - [✨  Release Notes](/CHANGELOG.md) - - ## Features - -- ⛰  Foo -- 🚠  Bar -- 🌲  Baz +- ✅ Directives `v-can` / `v-cannot` transformées en `v-if` / `!v-if` +- ✅ Merge automatique avec des `v-if` existants (pas de runtime inutile) +- ✅ Proxy global `can` + types générés selon votre configuration de permissions +- ✅ Import configurable de la fonction `__can__` (store, API, …) +- ✅ Erreurs compile-time claires (cas interdits détectés avant build) ## Quick Setup -Install the module to your Nuxt application with one command: +Installez le module dans votre application Nuxt : ```bash -npx nuxi module add my-module +npm install nuxt-can +# ou +npx nuxi module add nuxt-can ``` -That's it! You can now use My Module in your Nuxt app ✨ +Déclarez ensuite le module et configurez vos permissions : + +```ts +// nuxt.config.ts +import NuxtCan from 'nuxt-can' + +export default defineNuxtConfig({ + modules: [NuxtCan], + nuxtCan: { + permissions: { + employee: ['view', 'edit'], + contract: ['create'], + }, + canFunctionImport: '~/permissions/can', // chemin vers votre implémentation __can__ + }, +}) +``` + +Implémentez la fonction `__can__` dans le fichier ciblé (`~/permissions/can` dans l’exemple) : + +```ts +// permissions/can.ts +const permissionsStore = usePermissionsStore() + +export function __can__(path: string[]) { + return permissionsStore.check(path.join('.')) +} +``` + +Le module génère automatiquement un proxy `can` typé : + +```vue + +``` + +Résultat compilé : + +```vue + + +

Accès refusé

+``` + +### Règles et erreurs surveillées + +- `v-cannot` doit suivre immédiatement un `v-can` (aucun autre composant entre les deux). +- Un `v-can` ne peut pas cohabiter avec `v-else` / `v-else-if`. +- `v-cannot` n’accepte ni expression, ni argument, ni `v-if`. +- Un seul `v-cannot` par `v-can`. +- Les expressions doivent suivre le pattern `can.x.y` (au moins deux niveaux après `can`). + +Toute violation produit une erreur compile-time lisible. + +### Types générés + +À partir de la clé `permissions`, le module génère un fichier `.d.ts` qui expose : + +- `can` / `$can` sur `ComponentCustomProperties` +- `__can__` sur les templates/scripts Nuxt +- `NuxtApp.$can` et `NuxtApp.$__can__` + +La DX template + TypeScript reste donc impeccable sans configuration additionnelle. ## Contribution diff --git a/bun.lock b/bun.lock index 532d3b5..5d5dec3 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,9 @@ "name": "nuxt-can", "dependencies": { "@nuxt/kit": "^4.2.1", + "@vue/language-core": "^3.1.3", + "diff": "^8.0.2", + "htmlparser2": "^10.0.0", }, "devDependencies": { "@nuxt/devtools": "^3.1.0", @@ -813,7 +816,7 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], @@ -971,6 +974,8 @@ "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "http-shutdown": ["http-shutdown@1.2.2", "", {}, "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw=="], @@ -1707,6 +1712,8 @@ "@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="], + "@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], @@ -1723,6 +1730,8 @@ "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "eslint-plugin-import-x/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/package.json b/package.json index f67578b..69e944e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,10 @@ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit" }, "dependencies": { - "@nuxt/kit": "^4.2.1" + "@nuxt/kit": "^4.2.1", + "@vue/language-core": "^3.1.3", + "diff": "^8.0.2", + "htmlparser2": "^10.0.0" }, "devDependencies": { "@nuxt/devtools": "^3.1.0", diff --git a/playground/app.vue b/playground/app.vue index 58db5ce..ce77ab7 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,8 +1,41 @@ - + + diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 55315b0..1c5bb9f 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,5 +1,12 @@ export default defineNuxtConfig({ modules: ['../src/module'], devtools: { enabled: true }, - myModule: {}, + nuxtCan: { + reporter: true + // permissions: { + // employee: ['view', 'edit'], + // contract: ['create'], + // }, + // canFunctionImport: '~/permissions/__can__', + }, }) diff --git a/playground/permissions/__can__.ts b/playground/permissions/__can__.ts new file mode 100644 index 0000000..4e411b3 --- /dev/null +++ b/playground/permissions/__can__.ts @@ -0,0 +1,12 @@ +export function __can__(path: string[]) { + const key = path.join('.') + const allowed = new Set([ + 'employee.view', + 'employee.edit', + 'contract.create', + ]) + + // return allowed.has(key) + console.log('Checking permission:', key) + return false +} diff --git a/src/module.ts b/src/module.ts index 6e376ef..10b6de6 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,21 +1,31 @@ -import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit' +// modules/nuxt-can.ts +import { defineNuxtModule, addVitePlugin } from '@nuxt/kit' + +import { transformCan } from './runtime/transformer/transform-can' -// Module options TypeScript interface definition export interface ModuleOptions { - 0: string + reporter: boolean } +export const CONFIG_KEY = 'nuxtCan' + export default defineNuxtModule({ meta: { - name: 'my-module', - configKey: 'myModule', + name: 'nuxt-can', + configKey: CONFIG_KEY, }, - // Default configuration options of the Nuxt module - defaults: {}, - setup(_options, _nuxt) { - const resolver = createResolver(import.meta.url) - // Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack` - addPlugin(resolver.resolve('./runtime/plugin')) + defaults: { + reporter: false, + }, + + setup(options) { + addVitePlugin({ + name: 'vite-plugin-nuxt-can', + enforce: 'pre', + transform(code, id) { + return transformCan({ code, id, reporter: options.reporter }) + }, + }) }, }) diff --git a/src/runtime/transformer/errors.ts b/src/runtime/transformer/errors.ts new file mode 100644 index 0000000..36a88dc --- /dev/null +++ b/src/runtime/transformer/errors.ts @@ -0,0 +1,30 @@ +import type { SourceLocation } from '@vue/compiler-dom' + +export interface ErrorContext { + filename?: string +} + +export function raiseDirectiveError(ctx: ErrorContext, loc: SourceLocation, message: string): never { + const cwd = process.cwd() + const filename = ctx.filename + ? filenameRelativeToCwd(ctx.filename, cwd) + : '