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 @@
|
||||
<!--
|
||||
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
|
||||
# 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)
|
||||
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue) -->
|
||||
<!-- - [📖 Documentation](https://example.com) -->
|
||||
|
||||
## Features
|
||||
|
||||
<!-- Highlight some of the features your module provide here -->
|
||||
- ⛰ 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
|
||||
<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
|
||||
|
||||
11
bun.lock
11
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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
Nuxt module playground!
|
||||
<button v-if="true" id="btn-view" v-can="true">
|
||||
Voir le dossier 2
|
||||
</button>
|
||||
<div v-cannot>
|
||||
v-else
|
||||
</div>
|
||||
|
||||
<button v-if="isReady && can.employee.edit" id="btn-edit" v-can="true">
|
||||
Modifier le dossier
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
const isReady = true
|
||||
const can = {
|
||||
employee: {
|
||||
view: 'view_employee',
|
||||
edit: 'edit_employee',
|
||||
},
|
||||
}
|
||||
</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({
|
||||
modules: ['../src/module'],
|
||||
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 {
|
||||
0: string
|
||||
reporter: boolean
|
||||
}
|
||||
|
||||
export const CONFIG_KEY = 'nuxtCan'
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
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 })
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
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