import { fileURLToPath } from 'node:url' import { kebabCase } from 'scule' import { addTemplate, addTypeTemplate } from '@nuxt/kit' import type { Nuxt, NuxtTemplate, NuxtTypeTemplate } from '@nuxt/schema' import type { Resolver } from '@nuxt/kit' import type { ModuleOptions } from './module' import * as theme from './theme' export function buildTemplates(options: ModuleOptions) { return Object.entries(theme).reduce((acc, [key, component]) => { acc[key] = typeof component === 'function' ? component(options as Required) : component return acc }, {} as Record) } export function getTemplates(options: ModuleOptions, uiConfig: Record) { const templates: NuxtTemplate[] = [] for (const component in theme) { templates.push({ filename: `ui/${kebabCase(component)}.ts`, write: true, getContents: async () => { const template = (theme as any)[component] const result = typeof template === 'function' ? template(options) : template const variants = Object.keys(result.variants || {}) let json = JSON.stringify(result, null, 2) for (const variant of variants) { json = json.replace(new RegExp(`("${variant}": "[^"]+")`, 'g'), '$1 as const') json = json.replace(new RegExp(`("${variant}": \\[\\s*)((?:"[^"]+",?\\s*)+)(\\])`, 'g'), (_, before, match, after) => { const replaced = match.replace(/("[^"]+")/g, '$1 as const') return `${before}${replaced}${after}` }) } // For local development, directly import from theme if (process.env.DEV) { return [ `import template from ${JSON.stringify(fileURLToPath(new URL(`./theme/${kebabCase(component)}`, import.meta.url)))}`, `const result = typeof template === 'function' ? template(${JSON.stringify(options)}) : template`, `const json = ${json}`, `export default result as typeof json` ].join('\n') } return `export default ${json}` } }) } templates.push({ filename: 'ui/index.ts', write: true, getContents: () => Object.keys(theme).map(component => `export { default as ${component} } from './${kebabCase(component)}'`).join('\n') }) // FIXME: `typeof colors[number]` should include all colors from the theme templates.push({ filename: 'types/ui.d.ts', getContents: () => `import * as ui from '#build/ui' import type { DeepPartial } from '#ui/types/utils' import colors from 'tailwindcss/colors' const icons = ${JSON.stringify(uiConfig.icons)}; type NeutralColor = 'slate' | 'gray' | 'zinc' | 'neutral' | 'stone' type Color = Exclude type AppConfigUI = { colors?: { ${options.theme?.colors?.map(color => `${color}?: Color`).join('\n\t\t')} neutral?: NeutralColor } icons?: Partial } & DeepPartial declare module '@nuxt/schema' { interface AppConfigInput { ui?: AppConfigUI } } export {} ` }) templates.push({ filename: 'ui-image-component.ts', write: true, getContents: ({ app }) => { const image = app?.components?.find(c => c.pascalName === 'NuxtImg' && !c.filePath.includes('nuxt/dist/app')) return image ? `export { default } from "${image.filePath}"` : 'export default "img"' } }) return templates } export function addTemplates(options: ModuleOptions, nuxt: Nuxt, resolve: Resolver['resolve']) { const templates = getTemplates(options, nuxt.options.appConfig.ui) for (const template of templates) { if (template.filename!.endsWith('.d.ts')) { addTypeTemplate(template as NuxtTypeTemplate) } else { addTemplate(template) } } nuxt.hook('prepare:types', ({ references }) => { references.push({ path: resolve('./runtime/types/app.config.d.ts') }) }) }