feat: add directive transformer runtime
This commit is contained in:
23
.zed/debug.json
Normal file
23
.zed/debug.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
99
README.md
99
README.md
@@ -1,41 +1,100 @@
|
|||||||
<!--
|
# nuxt-can
|
||||||
Get your module up and running quickly.
|
|
||||||
|
|
||||||
Find and replace all on all files (CMD+SHIFT+F):
|
|
||||||
- Name: My Module
|
|
||||||
- Package name: my-module
|
|
||||||
- Description: My new Nuxt module
|
|
||||||
-->
|
|
||||||
|
|
||||||
# My Module
|
|
||||||
|
|
||||||
[![npm version][npm-version-src]][npm-version-href]
|
[![npm version][npm-version-src]][npm-version-href]
|
||||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||||
[![License][license-src]][license-href]
|
[![License][license-src]][license-href]
|
||||||
[![Nuxt][nuxt-src]][nuxt-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)
|
- [✨ Release Notes](/CHANGELOG.md)
|
||||||
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue) -->
|
|
||||||
<!-- - [📖 Documentation](https://example.com) -->
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<!-- Highlight some of the features your module provide here -->
|
- ✅ Directives `v-can` / `v-cannot` transformées en `v-if` / `!v-if`
|
||||||
- ⛰ Foo
|
- ✅ Merge automatique avec des `v-if` existants (pas de runtime inutile)
|
||||||
- 🚠 Bar
|
- ✅ Proxy global `can` + types générés selon votre configuration de permissions
|
||||||
- 🌲 Baz
|
- ✅ Import configurable de la fonction `__can__` (store, API, …)
|
||||||
|
- ✅ Erreurs compile-time claires (cas interdits détectés avant build)
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
Install the module to your Nuxt application with one command:
|
Installez le module dans votre application Nuxt :
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
|
<template>
|
||||||
|
<button v-can="can.employee.view">Voir le dossier</button>
|
||||||
|
<button v-if="isReady" v-can="can.employee.edit">
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<p v-cannot>Accès refusé</p>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultat compilé :
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<button v-if="__can__(['employee', 'view'])">Voir le dossier</button>
|
||||||
|
<button v-if="(isReady) && __can__(['employee', 'edit'])">Modifier</button>
|
||||||
|
<p v-if="!(__can__(['employee', 'edit']))">Accès refusé</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
## Contribution
|
||||||
|
|||||||
11
bun.lock
11
bun.lock
@@ -5,6 +5,9 @@
|
|||||||
"name": "nuxt-can",
|
"name": "nuxt-can",
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@nuxt/devtools": "^3.1.0",
|
"@nuxt/devtools": "^3.1.0",
|
||||||
@@ -813,7 +816,7 @@
|
|||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"@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-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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|||||||
@@ -34,7 +34,10 @@
|
|||||||
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@nuxt/devtools": "^3.1.0",
|
"@nuxt/devtools": "^3.1.0",
|
||||||
|
|||||||
@@ -1,8 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<button v-if="true" id="btn-view" v-can="true">
|
||||||
Nuxt module playground!
|
Voir le dossier 2
|
||||||
|
</button>
|
||||||
|
<div v-cannot>
|
||||||
|
v-else
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button v-if="isReady && can.employee.edit" id="btn-edit" v-can="true">
|
||||||
|
Modifier le dossier
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
|
const isReady = true
|
||||||
|
const can = {
|
||||||
|
employee: {
|
||||||
|
view: 'view_employee',
|
||||||
|
edit: 'edit_employee',
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding: 2rem;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: ['../src/module'],
|
modules: ['../src/module'],
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
myModule: {},
|
nuxtCan: {
|
||||||
|
reporter: true
|
||||||
|
// permissions: {
|
||||||
|
// employee: ['view', 'edit'],
|
||||||
|
// contract: ['create'],
|
||||||
|
// },
|
||||||
|
// canFunctionImport: '~/permissions/__can__',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
12
playground/permissions/__can__.ts
Normal file
12
playground/permissions/__can__.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
export interface ModuleOptions {
|
||||||
0: string
|
reporter: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CONFIG_KEY = 'nuxtCan'
|
||||||
|
|
||||||
export default defineNuxtModule<ModuleOptions>({
|
export default defineNuxtModule<ModuleOptions>({
|
||||||
meta: {
|
meta: {
|
||||||
name: 'my-module',
|
name: 'nuxt-can',
|
||||||
configKey: 'myModule',
|
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`
|
defaults: {
|
||||||
addPlugin(resolver.resolve('./runtime/plugin'))
|
reporter: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(options) {
|
||||||
|
addVitePlugin({
|
||||||
|
name: 'vite-plugin-nuxt-can',
|
||||||
|
enforce: 'pre',
|
||||||
|
transform(code, id) {
|
||||||
|
return transformCan({ code, id, reporter: options.reporter })
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
30
src/runtime/transformer/errors.ts
Normal file
30
src/runtime/transformer/errors.ts
Normal file
@@ -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)
|
||||||
|
: '<template>'
|
||||||
|
const position = `${filename}:${loc.start.line}:${loc.start.column}`
|
||||||
|
const error = new Error(`[nuxt-can] ${message} (${position})`)
|
||||||
|
error.name = 'NuxtCanError'
|
||||||
|
const stackLine = ` at ${position}`
|
||||||
|
if (error.stack) {
|
||||||
|
const [head, ...rest] = error.stack.split('\n')
|
||||||
|
error.stack = [head, stackLine, ...rest.slice(1)].join('\n')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
error.stack = `${error.name}: ${error.message}\n${stackLine}`
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
function filenameRelativeToCwd(filename: string, cwd: string): string {
|
||||||
|
return filename.startsWith(cwd)
|
||||||
|
? filename.slice(cwd.length).replace(/^\//, '')
|
||||||
|
: filename
|
||||||
|
}
|
||||||
5
src/runtime/transformer/nodes.ts
Normal file
5
src/runtime/transformer/nodes.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { TemplateChildNode } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export function isTemplateChild(node: unknown): node is TemplateChildNode {
|
||||||
|
return !!node && typeof node === 'object' && 'type' in node
|
||||||
|
}
|
||||||
7
src/runtime/transformer/patcher.ts
Normal file
7
src/runtime/transformer/patcher.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { Patch } from './types'
|
||||||
|
|
||||||
|
export function applyPatches(source: string, patches: Patch[]): string {
|
||||||
|
return [...patches]
|
||||||
|
.sort((a, b) => b.start - a.start)
|
||||||
|
.reduce((acc, patch) => acc.slice(0, patch.start) + patch.text + acc.slice(patch.end), source)
|
||||||
|
}
|
||||||
186
src/runtime/transformer/patches.ts
Normal file
186
src/runtime/transformer/patches.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import {
|
||||||
|
type RootNode,
|
||||||
|
type TemplateChildNode,
|
||||||
|
type ElementNode,
|
||||||
|
type DirectiveNode,
|
||||||
|
NodeTypes,
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
import type { Patch } from './types'
|
||||||
|
import { isTemplateChild } from './nodes'
|
||||||
|
import { raiseDirectiveError } from './errors'
|
||||||
|
|
||||||
|
interface WalkerContext {
|
||||||
|
templateStart: number
|
||||||
|
patches: Patch[]
|
||||||
|
filename?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingCan {
|
||||||
|
expression: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectCanPatches(ast: RootNode, templateStart: number, filename?: string): Patch[] {
|
||||||
|
const ctx: WalkerContext = {
|
||||||
|
templateStart,
|
||||||
|
patches: [],
|
||||||
|
filename,
|
||||||
|
}
|
||||||
|
|
||||||
|
walkChildren(ast.children, ctx)
|
||||||
|
|
||||||
|
return ctx.patches
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkChildren(children: TemplateChildNode[], ctx: WalkerContext): void {
|
||||||
|
let pendingCan: PendingCan | null = null
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
if (!isTemplateChild(child)) {
|
||||||
|
pendingCan = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === NodeTypes.TEXT) {
|
||||||
|
if (child.content.trim().length > 0) {
|
||||||
|
pendingCan = null
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type === NodeTypes.COMMENT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child.type !== NodeTypes.ELEMENT) {
|
||||||
|
pendingCan = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPending = handleElement(child as ElementNode, ctx, pendingCan)
|
||||||
|
pendingCan = nextPending
|
||||||
|
|
||||||
|
if (Array.isArray(child.children) && child.children.length > 0) {
|
||||||
|
walkChildren(child.children, ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleElement(element: ElementNode, ctx: WalkerContext, pendingCan: PendingCan | null): PendingCan | null {
|
||||||
|
let canDirective: DirectiveNode | null = null
|
||||||
|
let cannotDirective: DirectiveNode | null = null
|
||||||
|
let ifDirective: DirectiveNode | null = null
|
||||||
|
|
||||||
|
for (const prop of element.props) {
|
||||||
|
if (prop.type !== NodeTypes.DIRECTIVE) continue
|
||||||
|
if (prop.name === 'can') canDirective = prop
|
||||||
|
if (prop.name === 'cannot') cannotDirective = prop
|
||||||
|
if (prop.name === 'if') ifDirective = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canDirective && cannotDirective) {
|
||||||
|
raiseDirectiveError(ctx, cannotDirective.loc, '`v-can` and `v-cannot` cannot be used on the same element.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cannotDirective) {
|
||||||
|
transformCannotDirective({
|
||||||
|
directive: cannotDirective,
|
||||||
|
ctx,
|
||||||
|
pendingCan,
|
||||||
|
ifDirective,
|
||||||
|
})
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canDirective) {
|
||||||
|
const expression = transformCanDirective({
|
||||||
|
canDirective,
|
||||||
|
ifDirective,
|
||||||
|
ctx,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCanDirective(params: {
|
||||||
|
canDirective: DirectiveNode
|
||||||
|
ifDirective: DirectiveNode | null
|
||||||
|
ctx: WalkerContext
|
||||||
|
}): string {
|
||||||
|
const { canDirective, ifDirective, ctx } = params
|
||||||
|
|
||||||
|
if (canDirective.exp?.type !== NodeTypes.SIMPLE_EXPRESSION) {
|
||||||
|
raiseDirectiveError(ctx, canDirective.loc, '`v-can` expects a static expression (for example `v-can="can.x.y"`).')
|
||||||
|
}
|
||||||
|
|
||||||
|
const canExpression = canDirective.exp.content?.trim()
|
||||||
|
if (!canExpression) {
|
||||||
|
raiseDirectiveError(ctx, canDirective.loc, '`v-can` requires a valid permission expression such as `can.employee.view`.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifDirective && ifDirective.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
|
||||||
|
const ifExpression = (ifDirective.exp.content || 'true').trim() || 'true'
|
||||||
|
|
||||||
|
ctx.patches.push({
|
||||||
|
start: ctx.templateStart + ifDirective.loc.start.offset,
|
||||||
|
end: ctx.templateStart + ifDirective.loc.end.offset,
|
||||||
|
text: `v-if="(${canExpression}) && (${ifExpression})"`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.patches.push({
|
||||||
|
start: ctx.templateStart + canDirective.loc.start.offset,
|
||||||
|
end: ctx.templateStart + canDirective.loc.end.offset,
|
||||||
|
text: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctx.patches.push({
|
||||||
|
start: ctx.templateStart + canDirective.loc.start.offset,
|
||||||
|
end: ctx.templateStart + canDirective.loc.end.offset,
|
||||||
|
text: `v-if="${canExpression}"`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return canExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformCannotDirective(params: {
|
||||||
|
directive: DirectiveNode
|
||||||
|
pendingCan: PendingCan | null
|
||||||
|
ifDirective: DirectiveNode | null
|
||||||
|
ctx: WalkerContext
|
||||||
|
}): void {
|
||||||
|
const { directive, pendingCan, ctx, ifDirective } = params
|
||||||
|
|
||||||
|
if (directive.exp) {
|
||||||
|
raiseDirectiveError(ctx, directive.loc, '`v-cannot` must not carry an expression (use `v-cannot` by itself).')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.arg) {
|
||||||
|
raiseDirectiveError(ctx, directive.loc, '`v-cannot` does not accept arguments.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directive.modifiers.length) {
|
||||||
|
raiseDirectiveError(ctx, directive.loc, '`v-cannot` does not accept modifiers.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ifDirective) {
|
||||||
|
raiseDirectiveError(ctx, directive.loc, '`v-cannot` cannot be combined with `v-if`; remove the extra condition.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingCan) {
|
||||||
|
raiseDirectiveError(ctx, directive.loc, '`v-cannot` must immediately follow its `v-can`, and there can be only one `v-cannot` per `v-can` block.')
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.patches.push({
|
||||||
|
start: ctx.templateStart + directive.loc.start.offset,
|
||||||
|
end: ctx.templateStart + directive.loc.end.offset,
|
||||||
|
text: `v-if="!(${pendingCan.expression})"`,
|
||||||
|
})
|
||||||
|
}
|
||||||
19
src/runtime/transformer/reporter.ts
Normal file
19
src/runtime/transformer/reporter.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { diffLines } from 'diff'
|
||||||
|
import pc from 'picocolors'
|
||||||
|
|
||||||
|
import type { DiffPayload } from './types'
|
||||||
|
|
||||||
|
export function reportTemplateDiff({ before, after, id }: DiffPayload): void {
|
||||||
|
const diffs = diffLines(before, after)
|
||||||
|
const body = diffs
|
||||||
|
.map((part) => {
|
||||||
|
if (part.added) return pc.green(part.value)
|
||||||
|
if (part.removed) return pc.red(part.value)
|
||||||
|
return part.value
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
console.log(pc.bold(`\n===== DIFF ${id} =====`))
|
||||||
|
console.log(body)
|
||||||
|
console.log(pc.bold('============================\n'))
|
||||||
|
}
|
||||||
45
src/runtime/transformer/transform-can.ts
Normal file
45
src/runtime/transformer/transform-can.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { parse as parseSFC } from '@vue/compiler-sfc'
|
||||||
|
import { parse as parseTemplate } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
import { collectCanPatches } from './patches'
|
||||||
|
import { applyPatches } from './patcher'
|
||||||
|
import { reportTemplateDiff } from './reporter'
|
||||||
|
|
||||||
|
interface TransformInput {
|
||||||
|
code: string
|
||||||
|
id: string
|
||||||
|
reporter?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformCan({ code, id, reporter }: TransformInput) {
|
||||||
|
if (!id.endsWith('.vue')) return null
|
||||||
|
|
||||||
|
const sfc = parseSFC(code)
|
||||||
|
const tpl = sfc.descriptor.template
|
||||||
|
if (!tpl) return null
|
||||||
|
|
||||||
|
const templateStart = tpl.loc.start.offset
|
||||||
|
const templateEnd = tpl.loc.end.offset
|
||||||
|
const before = code.slice(templateStart, templateEnd)
|
||||||
|
|
||||||
|
if (!before.includes('v-can'))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const ast = parseTemplate(before, { comments: true })
|
||||||
|
const patches = collectCanPatches(ast, templateStart, id)
|
||||||
|
if (!patches.length) return null
|
||||||
|
|
||||||
|
const nextCode = applyPatches(code, patches)
|
||||||
|
|
||||||
|
if (reporter)
|
||||||
|
reportTemplateDiff({
|
||||||
|
before: code,
|
||||||
|
after: nextCode,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: nextCode,
|
||||||
|
map: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/runtime/transformer/types.ts
Normal file
11
src/runtime/transformer/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Patch {
|
||||||
|
start: number
|
||||||
|
end: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiffPayload {
|
||||||
|
before: string
|
||||||
|
after: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
10
src/runtime/utils.ts
Normal file
10
src/runtime/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { SourceLocation, TransformContext } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export const reportError = (context: TransformContext, message: string, loc: SourceLocation) => {
|
||||||
|
context.onError({
|
||||||
|
name: 'CanDirectiveError',
|
||||||
|
code: 0,
|
||||||
|
loc: loc,
|
||||||
|
message: `[nuxt-can] ${message}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user