docs-please 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/app/app.config.ts +13 -0
- package/app/app.vue +17 -0
- package/app/assets/css/main.css +367 -0
- package/app/components/Icons.ts +163 -0
- package/app/components/app/AppFooter.vue +24 -0
- package/app/components/app/AppHeader.vue +58 -0
- package/app/components/content/BrowserFrame.vue +21 -0
- package/app/components/content/Callout.vue +80 -0
- package/app/components/content/Caution.vue +25 -0
- package/app/components/content/CodeBlockCommand.vue +92 -0
- package/app/components/content/CodeCollapsibleWrapper.vue +50 -0
- package/app/components/content/CodeTabs.vue +14 -0
- package/app/components/content/ComponentPreview.vue +71 -0
- package/app/components/content/ComponentsList.vue +24 -0
- package/app/components/content/CopyButton.vue +39 -0
- package/app/components/content/FeatureCard.vue +25 -0
- package/app/components/content/LinkedCard.vue +19 -0
- package/app/components/content/Note.vue +25 -0
- package/app/components/content/ProseA.vue +18 -0
- package/app/components/content/ProseBlockQuote.vue +8 -0
- package/app/components/content/ProseCode.vue +8 -0
- package/app/components/content/ProseH1.vue +7 -0
- package/app/components/content/ProseH2.vue +8 -0
- package/app/components/content/ProseH3.vue +9 -0
- package/app/components/content/ProseH4.vue +9 -0
- package/app/components/content/ProseH5.vue +7 -0
- package/app/components/content/ProseH6.vue +7 -0
- package/app/components/content/ProseHr.vue +6 -0
- package/app/components/content/ProseIcon.vue +32 -0
- package/app/components/content/ProseImg.vue +6 -0
- package/app/components/content/ProseLi.vue +8 -0
- package/app/components/content/ProseOl.vue +8 -0
- package/app/components/content/ProseP.vue +14 -0
- package/app/components/content/ProsePre.vue +80 -0
- package/app/components/content/ProseStrong.vue +8 -0
- package/app/components/content/ProseTable.vue +26 -0
- package/app/components/content/ProseTd.vue +8 -0
- package/app/components/content/ProseTh.vue +8 -0
- package/app/components/content/ProseTr.vue +8 -0
- package/app/components/content/ProseUl.vue +8 -0
- package/app/components/content/Step.vue +18 -0
- package/app/components/content/Steps.vue +18 -0
- package/app/components/content/Tabs.vue +129 -0
- package/app/components/content/TabsItem.vue +26 -0
- package/app/components/content/Tip.vue +25 -0
- package/app/components/content/UButton.vue +34 -0
- package/app/components/content/UColorModeImage.vue +48 -0
- package/app/components/content/UPageCard.vue +83 -0
- package/app/components/content/UPageGrid.vue +18 -0
- package/app/components/content/UPageHero.vue +92 -0
- package/app/components/content/UPageSection.vue +90 -0
- package/app/components/content/Warning.vue +25 -0
- package/app/components/docs/DocsPageHeader.vue +20 -0
- package/app/components/docs/DocsPageNav.vue +31 -0
- package/app/components/docs/DocsSidebar.vue +97 -0
- package/app/components/docs/DocsTableOfContents.vue +64 -0
- package/app/components/ui/accordion/Accordion.vue +22 -0
- package/app/components/ui/accordion/AccordionContent.vue +23 -0
- package/app/components/ui/accordion/AccordionItem.vue +24 -0
- package/app/components/ui/accordion/AccordionTrigger.vue +37 -0
- package/app/components/ui/accordion/index.ts +4 -0
- package/app/components/ui/alert/Alert.vue +21 -0
- package/app/components/ui/alert/AlertDescription.vue +17 -0
- package/app/components/ui/alert/AlertTitle.vue +17 -0
- package/app/components/ui/alert/index.ts +28 -0
- package/app/components/ui/button/Button.vue +29 -0
- package/app/components/ui/button/index.ts +38 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +17 -0
- package/app/components/ui/card/CardContent.vue +17 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +17 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/collapsible/Collapsible.vue +19 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +15 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +15 -0
- package/app/components/ui/collapsible/index.ts +3 -0
- package/app/components/ui/separator/Separator.vue +29 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/switch/Switch.vue +35 -0
- package/app/components/ui/switch/index.ts +1 -0
- package/app/components/ui/table/Table.vue +16 -0
- package/app/components/ui/table/TableBody.vue +14 -0
- package/app/components/ui/table/TableCaption.vue +14 -0
- package/app/components/ui/table/TableCell.vue +21 -0
- package/app/components/ui/table/TableEmpty.vue +34 -0
- package/app/components/ui/table/TableFooter.vue +14 -0
- package/app/components/ui/table/TableHead.vue +14 -0
- package/app/components/ui/table/TableHeader.vue +14 -0
- package/app/components/ui/table/TableRow.vue +14 -0
- package/app/components/ui/table/index.ts +9 -0
- package/app/components/ui/tabs/Tabs.vue +15 -0
- package/app/components/ui/tabs/TabsContent.vue +20 -0
- package/app/components/ui/tabs/TabsList.vue +23 -0
- package/app/components/ui/tabs/TabsTrigger.vue +27 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/tooltip/Tooltip.vue +19 -0
- package/app/components/ui/tooltip/TooltipContent.vue +34 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useConfig.ts +24 -0
- package/app/composables/useNavigation.ts +43 -0
- package/app/layouts/default.vue +12 -0
- package/app/layouts/docs.vue +27 -0
- package/app/lib/utils.ts +7 -0
- package/app/pages/[...slug].vue +97 -0
- package/app/pages/index.vue +29 -0
- package/app/plugins/ssr-width.ts +5 -0
- package/content.config.ts +36 -0
- package/i18n/locales/en.json +14 -0
- package/modules/config.ts +38 -0
- package/modules/css.ts +38 -0
- package/modules/shadcn.ts +116 -0
- package/nuxt.config.ts +125 -0
- package/nuxt.schema.ts +68 -0
- package/package.json +81 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TooltipTriggerProps } from 'reka-ui'
|
|
3
|
+
import { TooltipTrigger } from 'reka-ui'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<TooltipTriggerProps>()
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<TooltipTrigger
|
|
10
|
+
data-slot="tooltip-trigger"
|
|
11
|
+
v-bind="props"
|
|
12
|
+
>
|
|
13
|
+
<slot />
|
|
14
|
+
</TooltipTrigger>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSharedComposable } from '@vueuse/core'
|
|
2
|
+
|
|
3
|
+
const COOKIE_NAME = 'docs_config'
|
|
4
|
+
export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun'
|
|
5
|
+
export type InstallationType = 'cli' | 'manual'
|
|
6
|
+
|
|
7
|
+
export const useConfig = createSharedComposable(() => {
|
|
8
|
+
const config = useCookie<{
|
|
9
|
+
packageManager: PackageManager
|
|
10
|
+
installationType: InstallationType
|
|
11
|
+
}>(
|
|
12
|
+
COOKIE_NAME,
|
|
13
|
+
{
|
|
14
|
+
default: () => ({ packageManager: 'bun', installationType: 'cli' }),
|
|
15
|
+
path: '/',
|
|
16
|
+
maxAge: 31536000,
|
|
17
|
+
sameSite: 'lax',
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
config,
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from '@nuxt/content'
|
|
2
|
+
|
|
3
|
+
export type NavigationItemType = 'page' | 'group'
|
|
4
|
+
|
|
5
|
+
export interface NavigationItem {
|
|
6
|
+
title: string
|
|
7
|
+
path: string
|
|
8
|
+
stem?: string
|
|
9
|
+
children?: NavigationItem[]
|
|
10
|
+
page?: false
|
|
11
|
+
type?: NavigationItemType
|
|
12
|
+
[key: string]: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function navigationItemType(item: ContentNavigationItem): NavigationItemType {
|
|
16
|
+
if (item.children && item.children.length > 0) {
|
|
17
|
+
return 'group'
|
|
18
|
+
}
|
|
19
|
+
return 'page'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mapWithType(item: ContentNavigationItem): NavigationItem {
|
|
23
|
+
return {
|
|
24
|
+
...item,
|
|
25
|
+
type: navigationItemType(item),
|
|
26
|
+
children: item.children?.map(child => mapWithType(child)),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function useNavigation(): Promise<{ data: Ref<NavigationItem[]> }> {
|
|
31
|
+
const { data } = useAsyncData('navigation', () => {
|
|
32
|
+
return queryCollectionNavigation('docs')
|
|
33
|
+
}, {
|
|
34
|
+
default: () => ([]),
|
|
35
|
+
transform: (data) => {
|
|
36
|
+
return data.map(item => mapWithType(item))
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
data,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<template>
|
|
5
|
+
<div class="relative flex min-h-svh flex-col bg-background">
|
|
6
|
+
<AppHeader />
|
|
7
|
+
<div class="container flex-1">
|
|
8
|
+
<div class="flex gap-8 py-8">
|
|
9
|
+
<!-- Sidebar -->
|
|
10
|
+
<aside class="hidden w-64 shrink-0 lg:block">
|
|
11
|
+
<DocsSidebar />
|
|
12
|
+
</aside>
|
|
13
|
+
|
|
14
|
+
<!-- Main content -->
|
|
15
|
+
<main class="min-w-0 flex-1">
|
|
16
|
+
<slot />
|
|
17
|
+
</main>
|
|
18
|
+
|
|
19
|
+
<!-- Table of contents -->
|
|
20
|
+
<aside class="hidden w-56 shrink-0 xl:block">
|
|
21
|
+
<DocsTableOfContents />
|
|
22
|
+
</aside>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<AppFooter />
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
package/app/lib/utils.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({
|
|
3
|
+
layout: 'docs',
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
|
|
8
|
+
const { data: page } = await useAsyncData(`docs-${route.path}`, () =>
|
|
9
|
+
queryCollection('docs').path(route.path).first())
|
|
10
|
+
|
|
11
|
+
const { data: surround } = await useAsyncData(`surround-${route.path}`, () =>
|
|
12
|
+
queryCollectionItemSurroundings('docs', route.path))
|
|
13
|
+
|
|
14
|
+
// Extract TOC from page body
|
|
15
|
+
const _toc = computed(() => {
|
|
16
|
+
if (!page.value?.body)
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
const headings: { id: string, text: string, depth: number }[] = []
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
function extractHeadings(node: any) {
|
|
23
|
+
if (node.tag && node.tag.match(/^h[2-4]$/)) {
|
|
24
|
+
const id = node.props?.id
|
|
25
|
+
const text = extractText(node.children)
|
|
26
|
+
const depth = Number.parseInt(node.tag.charAt(1))
|
|
27
|
+
if (id && text) {
|
|
28
|
+
headings.push({ id, text, depth })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (node.children) {
|
|
32
|
+
for (const child of node.children) {
|
|
33
|
+
extractHeadings(child)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
function extractText(children: any[]): string {
|
|
40
|
+
if (!children)
|
|
41
|
+
return ''
|
|
42
|
+
return children
|
|
43
|
+
.map((child) => {
|
|
44
|
+
if (typeof child === 'string')
|
|
45
|
+
return child
|
|
46
|
+
if (child.children)
|
|
47
|
+
return extractText(child.children)
|
|
48
|
+
return ''
|
|
49
|
+
})
|
|
50
|
+
.join('')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (page.value.body.children) {
|
|
54
|
+
for (const child of page.value.body.children) {
|
|
55
|
+
extractHeadings(child)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return headings
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
useSeoMeta({
|
|
63
|
+
title: page.value?.title,
|
|
64
|
+
description: page.value?.description,
|
|
65
|
+
})
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div v-if="page">
|
|
70
|
+
<DocsPageHeader
|
|
71
|
+
:title="page.title"
|
|
72
|
+
:description="page.description"
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<UiSeparator class="my-6" />
|
|
76
|
+
|
|
77
|
+
<div class="prose dark:prose-invert max-w-none">
|
|
78
|
+
<ContentRenderer :value="page" />
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<DocsPageNav
|
|
82
|
+
:prev="surround?.[0]"
|
|
83
|
+
:next="surround?.[1]"
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div
|
|
87
|
+
v-else
|
|
88
|
+
class="py-12 text-center"
|
|
89
|
+
>
|
|
90
|
+
<h1 class="text-2xl font-bold">
|
|
91
|
+
Page not found
|
|
92
|
+
</h1>
|
|
93
|
+
<p class="mt-2 text-muted-foreground">
|
|
94
|
+
The page you're looking for doesn't exist.
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({
|
|
3
|
+
layout: 'default',
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
const { data: page } = await useAsyncData('landing', () =>
|
|
7
|
+
queryCollection('landing').first())
|
|
8
|
+
|
|
9
|
+
if (!page.value) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const title = page.value.seo?.title || page.value.title
|
|
14
|
+
const description = page.value.seo?.description || page.value.description
|
|
15
|
+
|
|
16
|
+
useSeoMeta({
|
|
17
|
+
title,
|
|
18
|
+
ogTitle: title,
|
|
19
|
+
description,
|
|
20
|
+
ogDescription: description,
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<ContentRenderer
|
|
26
|
+
v-if="page"
|
|
27
|
+
:value="page"
|
|
28
|
+
/>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
|
|
2
|
+
import { useNuxt } from '@nuxt/kit'
|
|
3
|
+
import { joinURL } from 'ufo'
|
|
4
|
+
|
|
5
|
+
const { options } = useNuxt()
|
|
6
|
+
const cwd = joinURL(options.rootDir, 'content')
|
|
7
|
+
|
|
8
|
+
const docsSchema = z.object({
|
|
9
|
+
links: z.array(z.object({
|
|
10
|
+
label: z.string(),
|
|
11
|
+
icon: z.string(),
|
|
12
|
+
to: z.string(),
|
|
13
|
+
target: z.string().optional(),
|
|
14
|
+
})).optional(),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export default defineContentConfig({
|
|
18
|
+
collections: {
|
|
19
|
+
landing: defineCollection({
|
|
20
|
+
type: 'page',
|
|
21
|
+
source: {
|
|
22
|
+
cwd,
|
|
23
|
+
include: 'index.md',
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
docs: defineCollection({
|
|
27
|
+
type: 'page',
|
|
28
|
+
source: {
|
|
29
|
+
cwd,
|
|
30
|
+
include: '**',
|
|
31
|
+
exclude: ['index.md'],
|
|
32
|
+
},
|
|
33
|
+
schema: docsSchema,
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"or": "or",
|
|
4
|
+
"error": {
|
|
5
|
+
"title": "Something went wrong",
|
|
6
|
+
"description": "An error occurred while loading this page."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"docs": {
|
|
10
|
+
"toc": "On this page",
|
|
11
|
+
"edit": "Edit this page",
|
|
12
|
+
"report": "Report an issue"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineNuxtModule } from '@nuxt/kit'
|
|
2
|
+
import { defu } from 'defu'
|
|
3
|
+
import { getGitBranch, getGitEnv } from '../utils/git'
|
|
4
|
+
import { getPackageJsonMetadata, inferSiteURL } from '../utils/meta'
|
|
5
|
+
|
|
6
|
+
export default defineNuxtModule({
|
|
7
|
+
meta: {
|
|
8
|
+
name: 'docs-config',
|
|
9
|
+
},
|
|
10
|
+
async setup(_options, nuxt) {
|
|
11
|
+
const dir = nuxt.options.rootDir
|
|
12
|
+
const url = inferSiteURL()
|
|
13
|
+
const meta = await getPackageJsonMetadata(dir)
|
|
14
|
+
const gitInfo = getGitEnv()
|
|
15
|
+
const siteName = meta.name || gitInfo?.name || 'Docs'
|
|
16
|
+
|
|
17
|
+
nuxt.options.appConfig.docs = defu(nuxt.options.appConfig.docs, {
|
|
18
|
+
title: siteName,
|
|
19
|
+
description: meta.description || '',
|
|
20
|
+
url,
|
|
21
|
+
github: {
|
|
22
|
+
owner: gitInfo?.owner,
|
|
23
|
+
name: gitInfo?.name,
|
|
24
|
+
url: gitInfo?.url,
|
|
25
|
+
branch: getGitBranch(),
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// SEO defaults
|
|
30
|
+
nuxt.options.app.head = defu(nuxt.options.app.head, {
|
|
31
|
+
title: siteName,
|
|
32
|
+
titleTemplate: `%s - ${siteName}`,
|
|
33
|
+
meta: [
|
|
34
|
+
{ name: 'description', content: meta.description || '' },
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
})
|
package/modules/css.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
2
|
+
import { joinURL } from 'ufo'
|
|
3
|
+
import { resolveModulePath } from 'exsolve'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtModule({
|
|
6
|
+
meta: {
|
|
7
|
+
name: 'docs-css',
|
|
8
|
+
},
|
|
9
|
+
async setup(_options, nuxt) {
|
|
10
|
+
const dir = nuxt.options.rootDir
|
|
11
|
+
const resolver = createResolver(import.meta.url)
|
|
12
|
+
|
|
13
|
+
const contentDir = joinURL(dir, 'content')
|
|
14
|
+
const layerDir = resolver.resolve('../app')
|
|
15
|
+
const mainCssPath = resolver.resolve('../app/assets/css/main.css')
|
|
16
|
+
const tailwindPath = resolveModulePath('tailwindcss', { from: import.meta.url, conditions: ['style'] })
|
|
17
|
+
|
|
18
|
+
// Create a CSS template that includes source directives for Tailwind
|
|
19
|
+
const cssTemplate = addTemplate({
|
|
20
|
+
filename: 'docs-layer.css',
|
|
21
|
+
getContents: () => {
|
|
22
|
+
return `@import ${JSON.stringify(tailwindPath)};
|
|
23
|
+
|
|
24
|
+
@source "${contentDir.replace(/\\/g, '/')}/**/*";
|
|
25
|
+
@source "${layerDir.replace(/\\/g, '/')}/**/*";
|
|
26
|
+
@source "../../app.config.ts";
|
|
27
|
+
|
|
28
|
+
@import ${JSON.stringify(mainCssPath)};`
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Remove main.css from nuxt.options.css if present (we import it in docs-layer.css)
|
|
33
|
+
nuxt.options.css = nuxt.options.css.filter(css => !css.includes('main.css'))
|
|
34
|
+
|
|
35
|
+
// Add the generated CSS file to Nuxt - unshift to load first
|
|
36
|
+
nuxt.options.css.unshift(cssTemplate.dst)
|
|
37
|
+
},
|
|
38
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
addComponent,
|
|
5
|
+
addComponentsDir,
|
|
6
|
+
createResolver,
|
|
7
|
+
defineNuxtModule,
|
|
8
|
+
findPath,
|
|
9
|
+
useLogger,
|
|
10
|
+
} from '@nuxt/kit'
|
|
11
|
+
import { parseSync } from 'oxc-parser'
|
|
12
|
+
|
|
13
|
+
// Module options TypeScript interface definition
|
|
14
|
+
export interface ModuleOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Prefix for all the imported component.
|
|
17
|
+
* @default "Ui"
|
|
18
|
+
*/
|
|
19
|
+
prefix?: string
|
|
20
|
+
/**
|
|
21
|
+
* Directory that the component lives in.
|
|
22
|
+
* Will respect the Nuxt aliases.
|
|
23
|
+
* @link https://nuxt.com/docs/api/nuxt-config#alias
|
|
24
|
+
* @default "@/components/ui"
|
|
25
|
+
*/
|
|
26
|
+
componentDir?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default defineNuxtModule<ModuleOptions>({
|
|
30
|
+
meta: {
|
|
31
|
+
name: 'shadcn',
|
|
32
|
+
configKey: 'shadcn',
|
|
33
|
+
},
|
|
34
|
+
defaults: {
|
|
35
|
+
prefix: 'Ui',
|
|
36
|
+
componentDir: '@/components/ui',
|
|
37
|
+
},
|
|
38
|
+
async setup({ prefix, componentDir }, nuxt) {
|
|
39
|
+
const COMPONENT_DIR_PATH = componentDir!
|
|
40
|
+
const ROOT_DIR_PATH = nuxt.options.rootDir
|
|
41
|
+
|
|
42
|
+
const logger = useLogger('shadcn-nuxt')
|
|
43
|
+
logger.start('Setting up shadcn-nuxt module', { COMPONENT_DIR_PATH, ROOT_DIR_PATH })
|
|
44
|
+
|
|
45
|
+
// Build list of potential component directory paths from all layers
|
|
46
|
+
// _layers[0] is the app, subsequent entries are extended layers
|
|
47
|
+
// We check all layers and find the first existing component directory
|
|
48
|
+
const potentialPaths: string[] = []
|
|
49
|
+
|
|
50
|
+
for (const layer of nuxt.options._layers) {
|
|
51
|
+
const layerRoot = layer.cwd
|
|
52
|
+
// Resolve component directory relative to layer root
|
|
53
|
+
const componentPath = COMPONENT_DIR_PATH.replace(/^@\//, '')
|
|
54
|
+
|
|
55
|
+
// Try app/ subdirectory first (Nuxt 4 layer structure)
|
|
56
|
+
potentialPaths.push(join(layerRoot, 'app', componentPath))
|
|
57
|
+
// Also try direct path (traditional structure)
|
|
58
|
+
potentialPaths.push(join(layerRoot, componentPath))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger.info('Checking', { potentialPaths })
|
|
62
|
+
// Use findPath to find the first existing component directory
|
|
63
|
+
const componentsPath = (await findPath(potentialPaths, {}, 'dir')) || ROOT_DIR_PATH
|
|
64
|
+
|
|
65
|
+
logger.info('Decided on', { componentsPath })
|
|
66
|
+
|
|
67
|
+
// Create resolver relative to the found components path
|
|
68
|
+
const { resolve, resolvePath } = createResolver(componentsPath)
|
|
69
|
+
|
|
70
|
+
// Tell Nuxt to not scan `componentsDir` for auto imports as we will do it manually
|
|
71
|
+
// See https://github.com/unovue/shadcn-vue/pull/528#discussion_r1590206268
|
|
72
|
+
addComponentsDir({
|
|
73
|
+
path: componentsPath,
|
|
74
|
+
extensions: [],
|
|
75
|
+
ignore: ['**/*'],
|
|
76
|
+
}, {
|
|
77
|
+
prepend: true,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Manually scan `componentsDir` for components and register them for auto imports
|
|
81
|
+
try {
|
|
82
|
+
await Promise.all(readdirSync(componentsPath).map(async (dir) => {
|
|
83
|
+
try {
|
|
84
|
+
const filePath = await resolvePath(join(componentsPath, dir, 'index'), { extensions: ['.ts', '.js'] })
|
|
85
|
+
const content = readFileSync(filePath, { encoding: 'utf8' })
|
|
86
|
+
const ast = parseSync(filePath, content, {
|
|
87
|
+
sourceType: 'module',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const exportedKeys: string[] = ast.program.body
|
|
91
|
+
.filter(node => node.type === 'ExportNamedDeclaration')
|
|
92
|
+
// @ts-expect-error parse return any
|
|
93
|
+
.flatMap(node => node.specifiers?.map(specifier => specifier.exported?.name) || [])
|
|
94
|
+
.filter((key: string) => /^[A-Z]/.test(key))
|
|
95
|
+
|
|
96
|
+
exportedKeys.forEach((key) => {
|
|
97
|
+
addComponent({
|
|
98
|
+
name: `${prefix}${key}`, // name of the component to be used in vue templates
|
|
99
|
+
export: key, // (optional) if the component is a named (rather than default) export
|
|
100
|
+
filePath: resolve(filePath),
|
|
101
|
+
priority: 1,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof Error)
|
|
107
|
+
console.warn('Module error: ', err.message)
|
|
108
|
+
}
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof Error)
|
|
113
|
+
console.warn(err.message)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
})
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createResolver, useNuxt } from '@nuxt/kit'
|
|
2
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
3
|
+
|
|
4
|
+
const { resolve } = createResolver(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export default defineNuxtConfig({
|
|
7
|
+
modules: [
|
|
8
|
+
resolve('./modules/config'),
|
|
9
|
+
resolve('./modules/css'),
|
|
10
|
+
resolve('./modules/shadcn'),
|
|
11
|
+
'@nuxtjs/color-mode',
|
|
12
|
+
'@nuxt/content',
|
|
13
|
+
'@nuxt/image',
|
|
14
|
+
'@nuxt/icon',
|
|
15
|
+
'nuxt-og-image',
|
|
16
|
+
],
|
|
17
|
+
devtools: { enabled: true },
|
|
18
|
+
css: [resolve('./app/assets/css/main.css')],
|
|
19
|
+
colorMode: {
|
|
20
|
+
classSuffix: '',
|
|
21
|
+
preference: 'system',
|
|
22
|
+
fallback: 'light',
|
|
23
|
+
},
|
|
24
|
+
content: {
|
|
25
|
+
build: {
|
|
26
|
+
markdown: {
|
|
27
|
+
highlight: {
|
|
28
|
+
theme: {
|
|
29
|
+
light: 'github-light-default',
|
|
30
|
+
dark: 'github-dark',
|
|
31
|
+
},
|
|
32
|
+
langs: [
|
|
33
|
+
'ts',
|
|
34
|
+
'tsx',
|
|
35
|
+
'js',
|
|
36
|
+
'vue',
|
|
37
|
+
'vue-html',
|
|
38
|
+
'html',
|
|
39
|
+
'css',
|
|
40
|
+
'json',
|
|
41
|
+
'bash',
|
|
42
|
+
'shell',
|
|
43
|
+
'yaml',
|
|
44
|
+
'md',
|
|
45
|
+
'mdc',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
mdc: {
|
|
52
|
+
components: {
|
|
53
|
+
map: {
|
|
54
|
+
'browser-frame': 'BrowserFrame',
|
|
55
|
+
'callout': 'Callout',
|
|
56
|
+
'caution': 'Caution',
|
|
57
|
+
'icon': 'ProseIcon',
|
|
58
|
+
'note': 'Note',
|
|
59
|
+
'tip': 'Tip',
|
|
60
|
+
'warning': 'Warning',
|
|
61
|
+
'tabs': 'Tabs',
|
|
62
|
+
'tabs-item': 'TabsItem',
|
|
63
|
+
'u-color-mode-image': 'UColorModeImage',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
compatibilityDate: '2025-01-01',
|
|
68
|
+
nitro: {
|
|
69
|
+
prerender: {
|
|
70
|
+
crawlLinks: true,
|
|
71
|
+
failOnError: false,
|
|
72
|
+
autoSubfolderIndex: false,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
vite: {
|
|
76
|
+
plugins: [tailwindcss()],
|
|
77
|
+
},
|
|
78
|
+
hooks: {
|
|
79
|
+
'components:dirs': (dirs) => {
|
|
80
|
+
// Register app components from the layer directory
|
|
81
|
+
dirs.push({
|
|
82
|
+
path: resolve('./app/components/app'),
|
|
83
|
+
pathPrefix: false,
|
|
84
|
+
global: true,
|
|
85
|
+
})
|
|
86
|
+
// Register docs components from the layer directory
|
|
87
|
+
dirs.push({
|
|
88
|
+
path: resolve('./app/components/docs'),
|
|
89
|
+
pathPrefix: false,
|
|
90
|
+
global: true,
|
|
91
|
+
})
|
|
92
|
+
// Register content components from the layer directory
|
|
93
|
+
dirs.push({
|
|
94
|
+
path: resolve('./app/components/content'),
|
|
95
|
+
pathPrefix: false,
|
|
96
|
+
global: true,
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
'nitro:config': (nitroConfig) => {
|
|
100
|
+
const nuxt = useNuxt()
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
const i18nOptions = (nuxt.options as any).i18n
|
|
103
|
+
|
|
104
|
+
const routes: string[] = []
|
|
105
|
+
if (!i18nOptions) {
|
|
106
|
+
routes.push('/')
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
routes.push(...(i18nOptions.locales?.map((locale: string | { code: string }) =>
|
|
110
|
+
typeof locale === 'string' ? `/${locale}` : `/${locale.code}`) || []))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
nitroConfig.prerender = nitroConfig.prerender || {}
|
|
114
|
+
nitroConfig.prerender.routes = nitroConfig.prerender.routes || []
|
|
115
|
+
nitroConfig.prerender.routes.push(...routes)
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
ogImage: {
|
|
119
|
+
fonts: [
|
|
120
|
+
'Geist:400',
|
|
121
|
+
'Geist:500',
|
|
122
|
+
'Geist:600',
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
})
|