GitHub

@@ -1,27 +1,149 @@

11

import { 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'

36

import { genExport } from 'knitwork'

47

import colors from 'tailwindcss/colors'

5-

import { addTemplate, addTypeTemplate, hasNuxtModule } from '@nuxt/kit'

8+

import { addTemplate, addTypeTemplate, hasNuxtModule, logger, updateTemplates } from '@nuxt/kit'

69

import type { Nuxt, NuxtTemplate, NuxtTypeTemplate } from '@nuxt/schema'

710

import type { Resolver } from '@nuxt/kit'

811

import type { ModuleOptions } from './module'

912

import * as theme from './theme'

1013

import * as themeProse from './theme/prose'

1114

import * 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']) {

21142

const templates: NuxtTemplate[] = []

2214323144

let hasProse = false

24145

let hasContent = false

146+

let previousDetectedComponents: Set<string> | undefined

2514726148

const 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+94270

if (!!nuxt && ((hasNuxtModule('@nuxtjs/mdc') || options.mdc) || (hasNuxtModule('@nuxt/content') || options.content))) {

95271

hasProse = true

96272

@@ -116,7 +292,10 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an

116292

templates.push({

117293

filename: 'ui.css',

118294

write: 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

})

186366187367

templates.push({

@@ -253,7 +433,7 @@ export {}

253433

}

254434255435

export 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)

257437

for (const template of templates) {

258438

if (template.filename!.endsWith('.d.ts')) {

259439

addTypeTemplate(template as NuxtTypeTemplate)

@@ -265,4 +445,12 @@ export function addTemplates(options: ModuleOptions, nuxt: Nuxt, resolve: Resolv

265445

nuxt.hook('prepare:types', ({ references }) => {

266446

references.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

}

Read the original on github.com ↗