feat: Nuxt module (#197)

* feat: add nuxt module

* chore: bundle module

* chore: update package, readme

* docs: update nuxt installation with new module

* docs: remove Ui prefix to prevent confusion

* chore: cleanup
This commit is contained in:
zernonia 2023-12-01 11:31:27 +08:00 committed by GitHub
parent caa8994d22
commit 7586b5e15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 5855 additions and 107 deletions

View File

@ -26,31 +26,28 @@ npm install -D typescript
npm install -D @nuxtjs/tailwindcss
```
### Install `shadcn-nuxt` module (New ✨)
```bash
npm install -D shadcn-nuxt
```
### Configure `nuxt.config.ts`
<Callout class="mt-4">
**Tip:** It's better to use Nuxt `components:dirs` hook to extend auto-import components directories.
If you use `components` key in `nuxt.config.ts` default config will disposed
</Callout>
```ts
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss'],
hooks: {
'components:dirs': (dirs) => {
dirs.unshift({
path: '~/components/ui',
// this is required else Nuxt will autoImport `.ts` file
extensions: ['.vue'],
// prefix for your components, eg: UiButton
prefix: 'Ui',
// prevent adding another prefix component by it's path.
pathPrefix: false
})
}
modules: ['@nuxtjs/tailwindcss', 'shadcn-nuxt'],
shadcn: {
/**
* Prefix for all the imported component
*/
prefix: '',
/**
* Directory that the component lives in.
* @default "./components/ui"
*/
componentDir: './components/ui'
}
})
```
@ -133,7 +130,7 @@ The command above will add the `Button` component to your project. Nuxt autoImpo
```vue {3}
<template>
<div>
<UiButton>Click me</UiButton>
<Button>Click me</Button>
</div>
</template>
```

56
packages/module/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# Dependencies
node_modules
# Logs
*.log*
# Temp directories
.temp
.tmp
.cache
# Yarn
**/.yarn/cache
**/.yarn/*state*
# Generated dirs
dist
# Nuxt
.nuxt
.output
.data
.vercel_build_output
.build-*
.netlify
# Env
.env
# Testing
reports
coverage
*.lcov
.nyc_output
# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Intellij idea
*.iml
.idea
# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

1
packages/module/.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

104
packages/module/README.md Normal file
View File

@ -0,0 +1,104 @@
<!--
Get your module up and running quickly.
Find and replace all on all files (CMD+SHIFT+F):
- Name: Shadcn Nuxt
- Package name: shadcn-nuxt
- Description: My new Nuxt module
-->
# Shadcn Nuxt
[![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]
Shadcn Vue module for Nuxt.
- [✨ &nbsp;Release Notes](/CHANGELOG.md)
<!-- - [🏀 Online playground](https://stackblitz.com/github/radix-vue/shadcn-vue?file=playground%2Fapp.vue) -->
- [📖 &nbsp;Documentation](https://www.shadcn-vue.com/docs/installation/nuxt.html)
## Features
<!-- Highlight some of the features your module provide here -->
- ⛰ Auto-import correct and relevant components
- more to come...
## Quick Setup
1. Add `shadcn-nuxt` dependency to your project
```bash
# Using pnpm
pnpm add -D shadcn-nuxt
# Using yarn
yarn add --dev shadcn-nuxt
# Using npm
npm install --save-dev shadcn-nuxt
```
2. Add `shadcn-nuxt` to the `modules` section of `nuxt.config.ts`
```js
export default defineNuxtConfig({
modules: [
'shadcn-nuxt'
],
shadcn: {
/**
* Prefix for all the imported component
*/
prefix: '',
/**
* Directory that the component lives in.
* @default "./components/ui"
*/
componentDir: './components/ui'
}
})
```
That's it! You can now use Shadcn Nuxt in your Nuxt app ✨
## Development
```bash
# Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release
```
<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/shadcn-nuxt/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/shadcn-nuxt
[npm-downloads-src]: https://img.shields.io/npm/dm/shadcn-nuxt.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/shadcn-nuxt
[license-src]: https://img.shields.io/npm/l/shadcn-nuxt.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/shadcn-nuxt
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com

View File

@ -0,0 +1,51 @@
{
"name": "shadcn-nuxt",
"type": "module",
"version": "0.1.0",
"description": "Add shadcn-vue module to Nuxt",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/radix-vue/shadcn-vue.git",
"directory": "packages/module"
},
"exports": {
".": {
"types": "./dist/types.d.ts",
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"files": [
"dist"
],
"scripts": {
"prepack": "nuxt-module-build build",
"dev": "nuxi dev playground",
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest watch",
"release": "pnpm run prepack && pnpm publish && git push --follow-tags"
},
"dependencies": {
"@nuxt/kit": "^3.8.2",
"ts-morph": "^19.0.0"
},
"devDependencies": {
"@nuxt/devtools": "latest",
"@nuxt/eslint-config": "^0.2.0",
"@nuxt/module-builder": "^0.5.4",
"@nuxt/schema": "^3.8.2",
"@nuxt/test-utils": "^3.8.1",
"@types/node": "^20.9.3",
"nuxt": "^3.8.2",
"vitest": "^0.33.0"
}
}

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<div>
<UiButton :variant="'destructive'">
hi
</UiButton>
Nuxt module playground!
</div>
</template>

View File

@ -0,0 +1,15 @@
{
"style": "default",
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/tailwind.css",
"baseColor": "slate",
"cssVariables": true
},
"framework": "nuxt",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import { buttonVariants } from '.'
import { cn } from '@/lib/utils'
interface Props {
variant?: NonNullable<Parameters<typeof buttonVariants>[0]>['variant']
size?: NonNullable<Parameters<typeof buttonVariants>[0]>['size']
as?: string
}
withDefaults(defineProps<Props>(), {
as: 'button',
})
</script>
<template>
<component
:is="as"
:class="cn(buttonVariants({ variant, size }), $attrs.class ?? '')"
>
<slot />
</component>
</template>

View File

@ -0,0 +1,32 @@
import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useEmitAsProps, useForwardPropsEmits } from 'radix-vue'
const props = defineProps<DropdownMenuRootProps>()
const emits = defineEmits<DropdownMenuRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DropdownMenuRoot v-bind="forwarded">
<slot />
</DropdownMenuRoot>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import {
DropdownMenuCheckboxItem,
type DropdownMenuCheckboxItemEmits,
type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator,
useEmitAsProps,
} from 'radix-vue'
import { Check } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: string }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
</script>
<template>
<DropdownMenuCheckboxItem
v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class=" cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuItemIndicator>
<Check class="w-4 h-4" />
</DropdownMenuItemIndicator>
</span>
<slot />
</DropdownMenuCheckboxItem>
</template>

View File

@ -0,0 +1,4 @@
export { DropdownMenuPortal } from 'radix-vue'
export { default as DropdownMenu } from './DropdownMenu.vue'
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'

View File

@ -0,0 +1,7 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { camelize, getCurrentInstance, toHandlerKey } from 'vue'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,7 @@
export default defineNuxtConfig({
modules: ['../src/module'],
shadcn: {
prefix: 'Ui',
},
devtools: { enabled: true },
})

View File

@ -0,0 +1,22 @@
{
"name": "my-module-playground",
"type": "module",
"private": true,
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^6.10.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-vue-next": "^0.276.0",
"radix-vue": "^1.2.2",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"nuxt": "latest"
}
}

View File

@ -0,0 +1,73 @@
const animate = require('tailwindcss-animate')
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: 0 },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [animate],
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

View File

@ -0,0 +1,65 @@
import { readdirSync } from 'node:fs'
import { addComponent, createResolver, defineNuxtModule } from '@nuxt/kit'
import { Project } from 'ts-morph'
// Module options TypeScript interface definition
export interface ModuleOptions {
/**
* Prefix for all the imported component
*/
prefix?: string
/**
* Directory that the component lives in.
* @default "./components/ui"
*/
componentDir?: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'shadcn',
configKey: 'shadcn',
},
defaults: {
prefix: '',
componentDir: './components/ui',
},
async setup(options, nuxt) {
const IGNORE_DIR = '**/components/ui'
const COMPONENT_DIR_PATH = options.componentDir!
const ROOT_DIR_PATH = nuxt.options.rootDir
const { resolve } = createResolver(ROOT_DIR_PATH)
nuxt.options.ignore.push(IGNORE_DIR)
nuxt._ignore?.add(IGNORE_DIR)
nuxt._ignorePatterns?.push(IGNORE_DIR)
try {
readdirSync(resolve(COMPONENT_DIR_PATH))
.forEach(async (dir) => {
const filePath = resolve(COMPONENT_DIR_PATH, dir, 'index.ts')
const project = new Project()
project.addSourceFileAtPath(filePath)
const sourceFile = project.getSourceFileOrThrow(filePath)
const exportedDeclarations = sourceFile.getExportedDeclarations()
// Filter out non-component export
const exportedKeys = Array.from(exportedDeclarations.keys()).filter(key => /^[A-Z]/.test(key))
exportedKeys.forEach((key) => {
addComponent({
name: `${options.prefix}${key}`, // name of the component to be used in vue templates
export: key, // (optional) if the component is a named (rather than default) export
filePath: resolve(filePath),
})
})
})
}
catch (err) {
if (err instanceof Error)
console.warn(err.message)
}
},
})

View File

@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils'
describe('ssr', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
})
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
expect(html).toContain('<div>basic</div>')
})
})

View File

@ -0,0 +1,6 @@
<script setup>
</script>
<template>
<div>basic</div>
</template>

View File

@ -0,0 +1,7 @@
import MyModule from '../../../src/module'
export default defineNuxtConfig({
modules: [
MyModule,
],
})

View File

@ -0,0 +1,5 @@
{
"name": "basic",
"type": "module",
"private": true
}

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

File diff suppressed because it is too large Load Diff