@@ -1,27 +1,149 @@
11import { fileURLToPath } from 'node:url'
2-import { kebabCase } from 'scule'
2+import { readFile } from 'node:fs/promises'
3+import { join } from 'pathe'
4+import { globSync } from 'tinyglobby'
5+import { camelCase, kebabCase, pascalCase } from 'scule'
36import { genExport } from 'knitwork'
47import colors from 'tailwindcss/colors'
5-import { addTemplate, addTypeTemplate, hasNuxtModule } from '@nuxt/kit'
8+import { addTemplate, addTypeTemplate, hasNuxtModule, logger, updateTemplates } from '@nuxt/kit'
69import type { Nuxt, NuxtTemplate, NuxtTypeTemplate } from '@nuxt/schema'
710import type { Resolver } from '@nuxt/kit'
811import type { ModuleOptions } from './module'
912import * as theme from './theme'
1013import * as themeProse from './theme/prose'
1114import * as themeContent from './theme/content'
121513-export function buildTemplates(options: ModuleOptions) {
14-return Object.entries(theme).reduce((acc, [key, component]) => {
15-acc[key] = typeof component === 'function' ? component(options as Required<ModuleOptions>) : component
16-return acc
17-}, {} as Record<string, any>)
16+/**
17+ * Build a dependency graph of components by scanning their source files
18+ */
19+async function buildComponentDependencyGraph(componentDir: string, prefix: string): Promise<Map<string, Set<string>>> {
20+const dependencyGraph = new Map<string, Set<string>>()
21+22+const componentFiles = globSync(['**/*.vue'], {
23+cwd: componentDir,
24+absolute: true
25+})
26+27+const componentPattern = new RegExp(`<${prefix}([A-Z][a-zA-Z]+)|\\b${prefix}([A-Z][a-zA-Z]+)\\b`, 'g')
28+29+for (const componentFile of componentFiles) {
30+try {
31+const content = await readFile(componentFile, 'utf-8')
32+const componentName = pascalCase(componentFile.split('/').pop()!.replace('.vue', ''))
33+const dependencies = new Set<string>()
34+35+const matches = content.matchAll(componentPattern)
36+for (const match of matches) {
37+const depName = match[1] || match[2]
38+if (depName && depName !== componentName) {
39+dependencies.add(depName)
40+}
41+}
42+43+dependencyGraph.set(componentName, dependencies)
44+} catch {
45+// Ignore files that can't be read
46+}
47+}
48+49+return dependencyGraph
50+}
51+52+/**
53+ * Recursively resolve all dependencies for a component
54+ */
55+function resolveComponentDependencies(
56+component: string,
57+dependencyGraph: Map<string, Set<string>>,
58+resolved: Set<string> = new Set()
59+): Set<string> {
60+if (resolved.has(component)) {
61+return resolved
62+}
63+64+resolved.add(component)
65+const dependencies = dependencyGraph.get(component)
66+67+if (dependencies) {
68+for (const dep of dependencies) {
69+resolveComponentDependencies(dep, dependencyGraph, resolved)
70+}
71+}
72+73+return resolved
74+}
75+76+/**
77+ * Detect components used in the project by scanning source files
78+ */
79+async function detectUsedComponents(
80+rootDir: string,
81+prefix: string,
82+componentDir: string,
83+includeComponents?: string[]
84+): Promise<Set<string> | undefined> {
85+const detectedComponents = new Set<string>()
86+87+// Add manually specified components
88+if (includeComponents && includeComponents.length > 0) {
89+for (const component of includeComponents) {
90+detectedComponents.add(component)
91+}
92+}
93+94+// Scan all source files for component usage
95+const appFiles = globSync(['**/*.{vue,ts,js,tsx,jsx}'], {
96+cwd: rootDir,
97+ignore: ['node_modules/**', '.nuxt/**', 'dist/**']
98+})
99+100+// Pattern to match:
101+// - <UButton in templates
102+// - UButton in script (imports, usage)
103+const componentPattern = new RegExp(`<${prefix}([A-Z][a-zA-Z]+)|\\b${prefix}([A-Z][a-zA-Z]+)\\b`, 'g')
104+105+for (const file of appFiles) {
106+try {
107+const filePath = join(rootDir, file)
108+const content = await readFile(filePath, 'utf-8')
109+const matches = content.matchAll(componentPattern)
110+111+for (const match of matches) {
112+const componentName = match[1] || match[2]
113+if (componentName) {
114+detectedComponents.add(componentName)
115+}
116+}
117+} catch {
118+// Ignore files that can't be read
119+}
120+}
121+122+if (detectedComponents.size === 0) {
123+return undefined
124+}
125+126+// Build dependency graph of components
127+const dependencyGraph = await buildComponentDependencyGraph(componentDir, prefix)
128+129+// Resolve all dependencies for detected components
130+const allComponents = new Set<string>()
131+for (const component of detectedComponents) {
132+const resolved = resolveComponentDependencies(component, dependencyGraph)
133+for (const resolvedComponent of resolved) {
134+allComponents.add(resolvedComponent)
135+}
136+}
137+138+return allComponents
18139}
1914020-export function getTemplates(options: ModuleOptions, uiConfig: Record<string, any>, nuxt?: Nuxt) {
141+export function getTemplates(options: ModuleOptions, uiConfig: Record<string, any>, nuxt?: Nuxt, resolve?: Resolver['resolve']) {
21142const templates: NuxtTemplate[] = []
2214323144let hasProse = false
24145let hasContent = false
146+let previousDetectedComponents: Set<string> | undefined
2514726148const isDev = process.argv.includes('--uiDev')
27149@@ -91,6 +213,60 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
91213}
92214}
93215216+async function getSources() {
217+let sources = ''
218+219+if (!!nuxt && !!resolve && options.experimental?.componentDetection) {
220+const detectedComponents = await detectUsedComponents(
221+nuxt.options.rootDir,
222+options.prefix!,
223+resolve!('./runtime/components'),
224+Array.isArray(options.experimental.componentDetection) ? options.experimental.componentDetection : undefined
225+)
226+227+if (detectedComponents && detectedComponents.size > 0) {
228+if (previousDetectedComponents) {
229+const newComponents = Array.from(detectedComponents).filter(
230+component => !previousDetectedComponents!.has(component)
231+)
232+if (newComponents.length > 0) {
233+logger.success(`Nuxt UI detected new components: ${newComponents.join(', ')}`)
234+}
235+} else {
236+logger.success(`Nuxt UI detected ${detectedComponents.size} components in use (including dependencies)`)
237+}
238+239+previousDetectedComponents = detectedComponents
240+241+const sourcesList: string[] = []
242+243+if (hasProse) {
244+sourcesList.push('@source "./ui/prose";')
245+}
246+247+for (const component of detectedComponents) {
248+const kebabComponent = kebabCase(component)
249+const camelComponent = camelCase(component)
250+251+if (hasContent && (themeContent as any)[camelComponent]) {
252+sourcesList.push(`@source "./ui/content/${kebabComponent}.ts";`)
253+} else if ((theme as any)[camelComponent]) {
254+sourcesList.push(`@source "./ui/${kebabComponent}.ts";`)
255+}
256+}
257+258+sources = sourcesList.join('\n')
259+} else {
260+if (!previousDetectedComponents || previousDetectedComponents.size > 0) {
261+logger.info('Nuxt UI detected no components in use, including all components')
262+}
263+previousDetectedComponents = new Set()
264+}
265+}
266+267+return sources || '@source "./ui";'
268+}
269+94270if (!!nuxt && ((hasNuxtModule('@nuxtjs/mdc') || options.mdc) || (hasNuxtModule('@nuxt/content') || options.content))) {
95271hasProse = true
96272@@ -116,7 +292,10 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
116292templates.push({
117293filename: 'ui.css',
118294write: true,
119-getContents: () => `@source "./ui";
295+getContents: async () => {
296+const sources = await getSources()
297+298+return `${sources}
120299121300@theme static {
122301 --color-old-neutral-50: ${colors.neutral[50]};
@@ -182,6 +361,7 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
182361 --fill-inverted: var(--ui-border-inverted);
183362}
184363`
364+}
185365})
186366187367templates.push({
@@ -253,7 +433,7 @@ export {}
253433}
254434255435export function addTemplates(options: ModuleOptions, nuxt: Nuxt, resolve: Resolver['resolve']) {
256-const templates = getTemplates(options, nuxt.options.appConfig.ui, nuxt)
436+const templates = getTemplates(options, nuxt.options.appConfig.ui, nuxt, resolve)
257437for (const template of templates) {
258438if (template.filename!.endsWith('.d.ts')) {
259439addTypeTemplate(template as NuxtTypeTemplate)
@@ -265,4 +445,12 @@ export function addTemplates(options: ModuleOptions, nuxt: Nuxt, resolve: Resolv
265445nuxt.hook('prepare:types', ({ references }) => {
266446references.push({ path: resolve('./runtime/types/app.config.d.ts') })
267447})
448+449+if (options.experimental?.componentDetection && nuxt.options.dev) {
450+nuxt.hook('builder:watch', async (_, path) => {
451+if (/\.(?:vue|ts|js|tsx|jsx)$/.test(path)) {
452+await updateTemplates({ filter: template => template.filename === 'ui.css' })
453+}
454+})
455+}
268456}