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
+
+
+
+ Accès refusé
+
+```
+
+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 @@
-
- Nuxt module playground!
+
+
+ v-else
+
+
-
+
+
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)
+ : ''
+ 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
+}
diff --git a/src/runtime/transformer/nodes.ts b/src/runtime/transformer/nodes.ts
new file mode 100644
index 0000000..206fdd1
--- /dev/null
+++ b/src/runtime/transformer/nodes.ts
@@ -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
+}
diff --git a/src/runtime/transformer/patcher.ts b/src/runtime/transformer/patcher.ts
new file mode 100644
index 0000000..ac206a6
--- /dev/null
+++ b/src/runtime/transformer/patcher.ts
@@ -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)
+}
diff --git a/src/runtime/transformer/patches.ts b/src/runtime/transformer/patches.ts
new file mode 100644
index 0000000..8d067a6
--- /dev/null
+++ b/src/runtime/transformer/patches.ts
@@ -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})"`,
+ })
+}
diff --git a/src/runtime/transformer/reporter.ts b/src/runtime/transformer/reporter.ts
new file mode 100644
index 0000000..66e63e9
--- /dev/null
+++ b/src/runtime/transformer/reporter.ts
@@ -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'))
+}
diff --git a/src/runtime/transformer/transform-can.ts b/src/runtime/transformer/transform-can.ts
new file mode 100644
index 0000000..21b8383
--- /dev/null
+++ b/src/runtime/transformer/transform-can.ts
@@ -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,
+ }
+}
diff --git a/src/runtime/transformer/types.ts b/src/runtime/transformer/types.ts
new file mode 100644
index 0000000..a05efa7
--- /dev/null
+++ b/src/runtime/transformer/types.ts
@@ -0,0 +1,11 @@
+export interface Patch {
+ start: number
+ end: number
+ text: string
+}
+
+export interface DiffPayload {
+ before: string
+ after: string
+ id: string
+}
diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts
new file mode 100644
index 0000000..e4918a0
--- /dev/null
+++ b/src/runtime/utils.ts
@@ -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}`,
+ })
+}