feat: wire global runtime injection and playground

This commit is contained in:
stanig2106
2025-11-14 06:15:33 +01:00
parent 534bc39197
commit f12bc0ce85
18 changed files with 516 additions and 54 deletions

View File

@@ -7,9 +7,23 @@ describe('ssr', async () => {
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
})
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
it('renders the allowed branches when permissions pass', async () => {
const html = await $fetch('/')
expect(html).toContain('<div>basic</div>')
expect(html).toContain('<h1>basic</h1>')
expect(html).toContain('id="can-view"')
expect(html).toContain('id="can-edit"')
expect(html).toContain('Creation contrat')
})
it('falls back to the `v-cannot` branch when the permission is missing', async () => {
const html = await $fetch('/')
expect(html).not.toContain('id="can-delete"')
expect(html).toContain('id="cannot-delete"')
expect(html).toContain('Suppression interdite')
})
it('exposes the can proxy globally for template usage', async () => {
const html = await $fetch('/')
expect(html).toMatch(/id="path-display"[\s\S]*employee\.view/)
})
})

View File

@@ -1,6 +1,82 @@
<template>
<div>basic</div>
<main>
<h1>basic</h1>
<section>
<button
id="can-view"
v-can="canProxy.employee.view"
>
Voir
</button>
<button
v-if="isReady"
id="can-edit"
v-can="canProxy.employee.edit"
>
Editer
</button>
<p
id="cannot-edit"
v-cannot
>
Refus
</p>
</section>
<section>
<template v-if="showContracts">
<p v-can="canProxy.contract.create">
Creation contrat
</p>
<p v-cannot>
Pas de creation
</p>
</template>
<p v-else>
Section contrats masquee, aucune directive appliquee.
</p>
</section>
<section>
<h2>Suppression</h2>
<button
id="can-delete"
v-can="canProxy.employee.delete"
>
Supprimer
</button>
<p
id="cannot-delete"
v-cannot
>
Suppression interdite
</p>
</section>
<section>
<p id="path-display">
{{ String(canProxy.employee.view) }}
</p>
</section>
</main>
</template>
<script setup>
<script setup lang="ts">
const isReady = true
const showContracts = true
interface FixturePermissions {
employee: {
view: boolean
edit: boolean
delete: boolean
}
contract: {
create: boolean
}
}
const nuxtApp = useNuxtApp()
const canProxy = nuxtApp.$can as unknown as FixturePermissions
</script>

View File

@@ -1,7 +1,14 @@
import MyModule from '../../../src/module'
import NuxtCan from '../../../src/module'
export default defineNuxtConfig({
modules: [
MyModule,
NuxtCan,
],
nuxtCan: {
permissions: {
employee: ['view', 'edit', 'delete'],
contract: ['create'],
},
canFunctionImport: '~/permissions/__can__',
},
})

View File

@@ -0,0 +1,10 @@
const granted = new Set([
'employee.view',
'employee.edit',
'contract.create',
])
export function __can__(path: string[]) {
const key = Array.isArray(path) ? path.join('.') : String(path)
return granted.has(key)
}

View File

@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest'
import { transformCan } from '../src/runtime/transformer/transform-can'
const TEST_FILE = `${process.cwd()}/components/Test.vue`
const buildSFC = (template: string) => `<template>\n${template}\n</template>`
const runTransform = (template: string) => {
const result = transformCan({ code: buildSFC(template), id: TEST_FILE })
return result?.code ?? ''
}
describe('transformCan', () => {
it('injects __can__ guards and a matching v-cannot block', () => {
const code = runTransform(`
<button v-can="can.employee.view">Voir</button>
<p v-cannot>Refus</p>
`)
expect(code).toContain(`v-if="__can__(['employee', 'view'])"`)
expect(code).toContain(`v-if="!(__can__(['employee', 'view']))"`)
})
it('merges existing v-if expressions with the generated guard', () => {
const code = runTransform(`
<div v-if="isReady" v-can="can.contract.create" />
`)
expect(code).toContain(`v-if="(isReady) && __can__(['contract', 'create'])"`)
})
it('throws when v-cannot is used without a preceding v-can', () => {
const exec = () => transformCan({
code: buildSFC('<p v-cannot>Denied</p>'),
id: TEST_FILE,
})
expect(exec).toThrow(/must immediately follow its `v-can`/)
})
it('throws when the expression does not start with can.*', () => {
const exec = () => transformCan({
code: buildSFC('<button v-can="permissions.employee.view" />'),
id: TEST_FILE,
})
expect(exec).toThrow(/expressions must start with `can\.`/)
})
it('throws when v-can is added to a v-else branch', () => {
const exec = () => transformCan({
code: buildSFC(`
<div v-if="ready"></div>
<div v-else v-can="can.employee.view"></div>
`),
id: TEST_FILE,
})
expect(exec).toThrow(/cannot be used on `v-else`/)
})
})