Skip to content

Commit

Permalink
feat(addon): add auto-import vue directives (#374)
Browse files Browse the repository at this point in the history
Co-authored-by: Bobbie Goede <[email protected]>
Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
3 people authored Sep 26, 2024
1 parent 1d346ff commit 225eb69
Show file tree
Hide file tree
Showing 22 changed files with 1,154 additions and 31 deletions.
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
shamefully-hoist=true
ignore-workspace-root-check=true
shell-emulator=true
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,77 @@ export const counter = ref(0)

We recommend using [Volar](https://github.com/johnsoncodehk/volar) for type checking, which will help you to identify the misusage.

### Vue Directives Auto Import and TypeScript Declaration Generation

In Vue's template, the usage of directives is in a different context than plain modules. Thus some custom transformations are required. To enable it, set `addons.vueDirectives` to `true`:

```ts
Unimport.vite({
addons: {
vueDirectives: true
}
})
```

#### Library Authors

When including directives in your presets, you should:
- provide the corresponding imports with `meta.vueDirective` set to `true`, otherwise, `unimport` will not be able to detect your directives.
- use named exports for your directives, or use default export and use `as` in the Import.
- set `dtsDisabled` to `true` if you provide a type declaration for your directives.

```ts
import type { InlinePreset } from 'unimport'
import { defineUnimportPreset } from 'unimport'

export const composables = defineUnimportPreset({
from: 'my-unimport-library/composables',
/* imports and other options */
})

export const directives = defineUnimportPreset({
from: 'my-unimport-library/directives',
// disable dts generation globally
dtsEnabled: false,
// you can declare the vue directive globally
meta: {
vueDirective: true
},
imports: [{
name: 'ClickOutside',
// disable dts generation per import
dtsEnabled: false,
// you can declare the vue directive per import
meta: {
vueDirective: true
}
}, {
name: 'default',
// you should declare `as` for default exports
as: 'Focus'
}]
})
```

#### Using Directory Scan and Local Directives

If you add a directory scan for your local directives in the project, you need to:
- provide `isDirective` in the `vueDirectives`: `unimport` will use it to detect them (will never be called for imports with `meta.vueDirective` set to `true`).
- use always named exports for your directives.

```ts
Unimport.vite({
dirs: ['./directives/**'],
addons: {
vueDirectives: {
isDirective: (normalizedImportFrom, _importEntry) => {
return normalizedImportFrom.includes('/directives/')
}
}
}
})
```

## 💻 Development

- Clone this repository
Expand Down
1 change: 1 addition & 0 deletions playground/composables-preset/dummy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const dummy = 'from manual composable preset'
105 changes: 105 additions & 0 deletions playground/configure-directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { InlinePreset } from '../src'
import type { UnimportPluginOptions } from '../src/unplugin'
import * as process from 'node:process'
import { resolve } from 'pathe'

export function resolvePresets(presets: InlinePreset[]) {
return presets.map((preset) => {
preset.from = resolve(process.cwd(), preset.from)
return preset
})
}

const usePresets = process.env.USE_PRESETS === 'true'

const unimportViteOptions: Partial<UnimportPluginOptions> = {
dts: true,
// eslint-disable-next-line no-console
debugLog: console.log,
presets: [
'vue',
{
from: resolve(process.cwd(), 'composables-preset/dummy.ts'),
imports: [{
name: 'dummy',
}],
},
],
dirs: ['./composables/**'],
addons: {
vueTemplate: true,
vueDirectives: {
isDirective(normalizeImportFrom, _importEntry) {
return normalizeImportFrom.includes('/directives/')
},
},
},
}

if (!usePresets) {
unimportViteOptions.dirsScanOptions = {
cwd: process.cwd().replace(/\\/g, '/'),
}
unimportViteOptions.dirs!.push('./directives/**')
}
else {
unimportViteOptions.presets!.push(...resolvePresets([{
from: 'directives/awesome-directive.ts',
imports: [{
name: 'default',
as: 'AwesomeDirective',
meta: {
vueDirective: true,
},
}],
}, {
from: 'directives/named-directive.ts',
imports: [{
name: 'NamedDirective',
meta: {
vueDirective: true,
},
}],
}, {
from: 'directives/mixed-directive.ts',
imports: [{
name: 'NamedMixedDirective',
meta: {
vueDirective: true,
},
}, {
name: 'default',
as: 'MixedDirective',
meta: {
vueDirective: true,
},
}],
}, {
from: 'directives/custom-directive.ts',
imports: [{
name: 'CustomDirective',
meta: {
vueDirective: true,
},
}],
}, {
from: 'directives/ripple-directive.ts',
imports: [{
name: 'vRippleDirective',
meta: {
vueDirective: true,
},
}],
}, {
from: 'directives/v-focus-directive.ts',
imports: [{
name: 'default',
as: 'FocusDirective',
meta: {
vueDirective: true,
},
}],
}]))
}

export { unimportViteOptions }
8 changes: 8 additions & 0 deletions playground/directives/awesome-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { DirectiveBinding } from 'vue'

export function AwesomeDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('AwesomeDirective', el, binding)
}

export default AwesomeDirective
24 changes: 24 additions & 0 deletions playground/directives/custom-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { DirectiveBinding } from 'vue'

function mounted(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('mounted', el, binding)
}

function unmounted(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('unmounted', el, binding)
}

function updated(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('updated', el, binding)
}

export const CustomDirective = {
mounted,
unmounted,
updated,
}

export default CustomDirective
11 changes: 11 additions & 0 deletions playground/directives/mixed-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DirectiveBinding } from 'vue'

export function NamedMixedDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('NamedMixedDirective', el, binding)
}

export default function DefaultMixedDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('DefaultMixedDirective', el, binding)
}
6 changes: 6 additions & 0 deletions playground/directives/named-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { DirectiveBinding } from 'vue'

export function NamedDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('NamedDirective', el, binding)
}
8 changes: 8 additions & 0 deletions playground/directives/ripple-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { DirectiveBinding } from 'vue'

function vRippleDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('FocusDirective', el, binding)
}

export { vRippleDirective }
8 changes: 8 additions & 0 deletions playground/directives/v-focus-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { DirectiveBinding } from 'vue'

function VFocusDirective(el: HTMLElement, binding: DirectiveBinding) {
// eslint-disable-next-line no-console
console.log('FocusDirective', el, binding)
}

export default VFocusDirective
2 changes: 2 additions & 0 deletions playground/multiple-directives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AwesomeDirective } from '../directives/awesome-directive'
export { CustomDirective } from '../directives/custom-directive'
2 changes: 2 additions & 0 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"private": true,
"scripts": {
"dev": "vite",
"dev:presets": "USE_PRESETS=true vite",
"build": "vite build",
"build:presets": "USE_PRESETS=true vite build",
"typecheck": "vue-tsc --noEmit",
"preview": "vite preview"
},
Expand Down
9 changes: 5 additions & 4 deletions playground/src/Options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ export default defineComponent({
</script>

<template>
<div>
<div v-focus-directive>
<h1>{{ count }} x {{ multiplier }} = {{ count * multiplier }}</h1>
<button @click="inc">
<button v-custom-directive v-ripple-directive @click="inc">
Inc
</button>
<button @click="bump">
<button v-awesome-directive v-named-directive @click="bump">
x1
</button>
<div>
<div v-named-mixed-directive v-mixed-directive>
{{ nested() }}
</div>
<pre>{{ dummy }}</pre>
</div>
</template>
9 changes: 5 additions & 4 deletions playground/src/Setup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ function inc() {
</script>

<template>
<div>
<div v-focus-directive>
<h1>{{ count }} x {{ multiplier }} = {{ count * multiplier }}</h1>
<button @click="inc">
<button v-custom-directive v-ripple-directive @click="inc">
Inc
</button>
<button @click="bump">
<button v-awesome-directive v-named-directive @click="bump">
x1
</button>
<div>
<div v-named-mixed-directive v-mixed-directive>
{{ nested() }}
</div>
<pre>{{ dummy }}</pre>
</div>
</template>
14 changes: 2 additions & 12 deletions playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@ import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import inspect from 'vite-plugin-inspect'
import unimport from '../src/unplugin'
import { unimportViteOptions } from './configure-directives'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
unimport.vite({
dts: true,
presets: [
'vue',
],
dirs: [
'./composables/**',
],
addons: {
vueTemplate: true,
},
}),
unimport.vite(unimportViteOptions),
inspect(),
],
})
3 changes: 2 additions & 1 deletion src/addons.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './addons/vue-template'
export { vueDirectivesAddon } from './addons/vue-directives'
export { vueTemplateAddon } from './addons/vue-template'
40 changes: 40 additions & 0 deletions src/addons/addons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Addon, UnimportOptions } from '../types'
import { VUE_DIRECTIVES_NAME, vueDirectivesAddon } from './vue-directives'
import { VUE_TEMPLATE_NAME, vueTemplateAddon } from './vue-template'

export function configureAddons(opts: Partial<UnimportOptions>) {
const addons: Addon[] = []

if (Array.isArray(opts.addons)) {
addons.push(...opts.addons)
}
else {
const addonsMap = new Map<string, Addon>()
if (opts.addons?.addons?.length) {
let i = 0
for (const addon of opts.addons.addons) {
addonsMap.set(addon.name || `external:custom-${i++}`, addon)
}
}

if (opts.addons?.vueTemplate) {
if (!addonsMap.has(VUE_TEMPLATE_NAME)) {
addonsMap.set(VUE_TEMPLATE_NAME, vueTemplateAddon())
}
}

if (opts.addons?.vueDirectives) {
if (!addonsMap.has(VUE_DIRECTIVES_NAME)) {
addonsMap.set(VUE_DIRECTIVES_NAME, vueDirectivesAddon(
typeof opts.addons.vueDirectives === 'object'
? opts.addons.vueDirectives
: undefined,
))
}
}

addons.push(...addonsMap.values())
}

return addons
}
Loading

0 comments on commit 225eb69

Please sign in to comment.