From 22841074bb62cdebbbc6b89e7e9c8ac7a45f278d Mon Sep 17 00:00:00 2001 From: Romain Hamel Date: Wed, 5 Mar 2025 13:22:13 +0100 Subject: [PATCH 1/7] refactor!: replace collection settings by nuxt's components entry --- README.md | 13 +- .../components/BaseButtonExampleWithTiti.vue | 0 .../my-ui/BaseButtonExampleWithTata.vue | 3 + .../my-ui/BaseButtonExampleWithToto.vue | 3 + playground/components/BaseButton.vue | 57 +++++ playground/my-ui/BaseButton.vue | 3 + playground/nuxt.config.ts | 4 + src/module.ts | 208 ++++++++++-------- src/runtime/server/api/collections.get.ts | 14 +- src/types.d.ts | 1 - test/basic.test.ts | 8 +- test/custom-collection.test.ts | 61 +++++ test/fixtures/basic/ui/Button.vue | 3 + test/fixtures/custom-collection/app.vue | 3 + .../components/BasicComponentExample.vue | 0 .../compodium/ui/ButtonExample.vue | 3 + .../components/BasicComponent.vue | 7 + .../custom-collection/my-ui/Button.vue | 3 + .../fixtures/custom-collection/nuxt.config.ts | 7 + test/fixtures/custom-collection/package.json | 5 + 20 files changed, 295 insertions(+), 111 deletions(-) create mode 100644 playground/compodium/components/BaseButtonExampleWithTiti.vue create mode 100644 playground/compodium/my-ui/BaseButtonExampleWithTata.vue create mode 100644 playground/compodium/my-ui/BaseButtonExampleWithToto.vue create mode 100644 playground/components/BaseButton.vue create mode 100644 playground/my-ui/BaseButton.vue create mode 100644 test/custom-collection.test.ts create mode 100644 test/fixtures/basic/ui/Button.vue create mode 100644 test/fixtures/custom-collection/app.vue create mode 100644 test/fixtures/custom-collection/compodium/components/BasicComponentExample.vue create mode 100644 test/fixtures/custom-collection/compodium/ui/ButtonExample.vue create mode 100644 test/fixtures/custom-collection/components/BasicComponent.vue create mode 100644 test/fixtures/custom-collection/my-ui/Button.vue create mode 100644 test/fixtures/custom-collection/nuxt.config.ts create mode 100644 test/fixtures/custom-collection/package.json diff --git a/README.md b/README.md index 6187d273..79c92386 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,11 @@ Configure Compodium in your Nuxt project by customizing the settings in your `nu ```ts export default defineNuxtConfig({ compodium: { - /* Customize your component collections */ - collections: [ - { name: 'Components', path: 'components/' } - ], - /* Whether to include default collections for third-party libraries. */ includeDefaultCollections: true, - /* Customize the directory for preview examples */ - examples: 'compodium/', + /* Customize compodium's base directory */ + dir: 'compodium/', /* Customize the preview component path. */ previewComponent: 'compodium/preview.vue', @@ -136,11 +131,11 @@ By default, Compodium will detect the UI libraries you have installed and automa ### Component Examples -You can provide examples for your components in the `compodium/examples` folder. Examples will be matched to components based on the filename. Each example must be named after its corresponding component, followed by the `Example` keyword and an optional label. +You can provide examples for your components in the `compodium/` folder. Examples will be matched to components based on the filename. Each example must be named after its corresponding component, followed by the `Example` keyword and an optional label. ```bash compodium -└── components # The collection's name +└── components # The components base directory ├── BaseInputExampleDisabled.vue # Will be added to the BaseInput component. ├── BaseButtonExample.vue # Will be the main example for the BaseButton component. └── BaseButtonExampleWithLabel.vue # Will be added to the BaseButton component. diff --git a/playground/compodium/components/BaseButtonExampleWithTiti.vue b/playground/compodium/components/BaseButtonExampleWithTiti.vue new file mode 100644 index 00000000..e69de29b diff --git a/playground/compodium/my-ui/BaseButtonExampleWithTata.vue b/playground/compodium/my-ui/BaseButtonExampleWithTata.vue new file mode 100644 index 00000000..fbd80976 --- /dev/null +++ b/playground/compodium/my-ui/BaseButtonExampleWithTata.vue @@ -0,0 +1,3 @@ + diff --git a/playground/compodium/my-ui/BaseButtonExampleWithToto.vue b/playground/compodium/my-ui/BaseButtonExampleWithToto.vue new file mode 100644 index 00000000..04d85e1b --- /dev/null +++ b/playground/compodium/my-ui/BaseButtonExampleWithToto.vue @@ -0,0 +1,3 @@ + diff --git a/playground/components/BaseButton.vue b/playground/components/BaseButton.vue new file mode 100644 index 00000000..4eb0ed7d --- /dev/null +++ b/playground/components/BaseButton.vue @@ -0,0 +1,57 @@ + + + diff --git a/playground/my-ui/BaseButton.vue b/playground/my-ui/BaseButton.vue new file mode 100644 index 00000000..04d85e1b --- /dev/null +++ b/playground/my-ui/BaseButton.vue @@ -0,0 +1,3 @@ + diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 04a71ccb..df0dd9e3 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,5 +1,9 @@ export default defineNuxtConfig({ modules: ['@nuxt/ui', '../src/module'], + components: [ + { path: 'components/' }, + { path: 'my-ui/', prefix: 'D' } + ], devtools: { enabled: true }, css: ['~/assets/css/main.css'], compatibilityDate: '2025-02-12' diff --git a/src/module.ts b/src/module.ts index a888f74f..e5bdb36d 100644 --- a/src/module.ts +++ b/src/module.ts @@ -7,7 +7,7 @@ import { camelCase, pascalCase } from 'scule' import sirv from 'sirv' import { scanComponents } from './nuxt' import { getComponentCollection } from './runtime/utils' -import { join } from 'pathe' +import { basename, join } from 'pathe' import { defu } from 'defu' import { defaultProps } from './runtime/libs/defaults' import { watch } from 'chokidar' @@ -18,9 +18,6 @@ import { joinURL } from 'ufo' import { getLibraryCollections } from './runtime/libs' export interface ModuleOptions { - /* Customize your component collections */ - collections?: CollectionConfig[] - /* Whether to include default collections for third-party libraries. */ includeDefaultCollections: boolean @@ -63,14 +60,7 @@ export default defineNuxtModule({ if (!nuxt.options.dev) return - addComponentsDir({ path: resolve('./runtime/components') }) - const appResolver = createResolver(nuxt.options.rootDir) - - options.collections ??= [ - { name: 'Components', path: nuxt.options.future?.compatibilityVersion === 4 ? 'app/components/' : 'components/' } - ] - const libraryCollections = options.includeDefaultCollections ? await getLibraryCollections(nuxt.options, appResolver) : [] let previewComponent = appResolver.resolve(options.previewComponent) @@ -83,13 +73,13 @@ export default defineNuxtModule({ getContents: () => `export { default } from '${previewComponent}'` }) - nuxt.options.appConfig.compodium = { - collections: options.collections.map(c => ({ ...c, id: c.id ?? camelCase(c.name), path: appResolver.resolve(c.path) })).concat(libraryCollections), - matchUIColors: options.extras?.ui?.matchColors - } - const appConfig = nuxt.options.appConfig - appConfig.compodium = defu(nuxt.options.appConfig.compodium as any, { defaultProps }) + + appConfig.compodium = defu(appConfig.compodium as any, { + matchUIColors: options.extras?.ui?.matchColors, + defaultProps, + componentsPath: join(nuxt.options.buildDir, '/compodium/components.json') + }) nuxt.hooks.hookOnce('app:resolve', (app) => { const rootComponent = app.rootComponent @@ -106,76 +96,103 @@ export default defineNuxtModule({ if (pages.length) pages.push({ path: '/__compodium__/renderer', file: resolve('./runtime/renderer-placeholder.vue') }) }) - const examplesDirs = [ - appResolver.resolve(options.examples), - ...options.collections.filter(c => c.examplesPath).map(c => appResolver.resolve(c.examplesPath as string)) - ] - - const libraryExampleDirs = libraryCollections.map(c => ({ path: resolve(c.examplePath), pattern: '**/*.{vue,ts,tsx}', prefix: c.prefix })) - - const exampleComponents = options.examples - ? (await scanComponents([...examplesDirs.map(examplesDir => ({ - path: examplesDir, - pattern: '**/*.{vue,ts,tsx}' - })), ...libraryExampleDirs], nuxt.options.rootDir)).map(c => ({ ...c, isExample: true })) - : [] - - // Watch for changes in example directory - const examplesWatcher = watch([...examplesDirs, ...libraryExampleDirs.map(e => e.path)], { - persistent: true, - awaitWriteFinish: { - stabilityThreshold: 200, - pollInterval: 100 - } - }) + // This file will be read directly server side. This is a hack after realising that virtual module didn't work with HMR server side. + nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {} + nuxt.options.nitro.virtual['#compodium/nitro/dirs'] = () => { + return readFileSync(join(nuxt.options.buildDir, '/compodium/dirs.mjs'), 'utf-8') + } + nuxt.options.nitro.virtual['#compodium/nitro/collections'] = () => { + return readFileSync(join(nuxt.options.buildDir, '/compodium/collections.mjs'), 'utf-8') + } + + nuxt.hooks.hookOnce('components:dirs', async (dirs) => { + const collections = dirs.map((dir) => { + const path = typeof dir === 'string' ? dir : dir.path + const id = basename(path) + return { + ...typeof dir === 'string' ? {} : dir, + path, + name: pascalCase(id), + id + } + }) + + const examplesDirs = collections.map(collection => ({ + ...collection, + path: appResolver.resolve(joinURL(options.examples, collection.id)), + pattern: '**/*.{vue,ts,tsx}' + })) + + const libraryExampleDirs = libraryCollections.map(c => ({ + path: resolve(c.examplePath), + pattern: '**/*.{vue,ts,tsx}', + prefix: c.prefix + })) + + // @ts-expect-error type not resolved + appConfig.compodium.collections = collections.concat(libraryCollections) + + const exampleComponents = options.examples + ? (await scanComponents([ + ...examplesDirs, + ...libraryExampleDirs + ], nuxt.options.rootDir)).map(c => ({ ...c, isExample: true })) + : [] + + // Watch for changes in example directory + const examplesWatcher = watch([...examplesDirs, ...libraryExampleDirs].map(e => e.path), { + persistent: true, + awaitWriteFinish: { + stabilityThreshold: 200, + pollInterval: 100 + } + }) + + examplesWatcher.on('add', async (path) => { + const comps = await scanComponents(examplesDirs, nuxt.options.rootDir) + const newExample = comps.find(c => c.filePath === path) + if (newExample) { + exampleComponents.push({ ...newExample, isExample: true }) + await updateTemplates({ + filter: template => template.filename === 'compodium/components.json' + }) + } + }) - // FIXME: This might cause a race condition with the vite plugin. - examplesWatcher.on('add', async (path) => { - const comps = await scanComponents(examplesDirs.map(examplesDir => ({ path: examplesDir, pattern: '**/*.{vue,ts,tsx}' })), nuxt.options.rootDir) - const newExample = comps.find(c => c.filePath === path) - if (newExample) { - exampleComponents.push({ ...newExample, isExample: true }) + examplesWatcher.on('unlink', async (path) => { + const index = exampleComponents.findIndex(c => c.filePath === path) + if (index !== -1) exampleComponents.splice(index, 1) await updateTemplates({ filter: template => template.filename === 'compodium/components.json' }) - } - }) - - examplesWatcher.on('unlink', async (path) => { - const index = exampleComponents.findIndex(c => c.filePath === path) - if (index !== -1) exampleComponents.splice(index, 1) - await updateTemplates({ - filter: template => template.filename === 'compodium/components.json' }) - }) - addTemplate({ - filename: 'compodium/components.json', - write: true, - getContents: ({ nuxt, app }) => { - const collections = (nuxt.options.appConfig.compodium as any).collections - const components = [...app.components, ...exampleComponents] - return JSON.stringify(components.reduce((acc, component) => { - const collection = getComponentCollection(component, collections) - - const componentId = camelCase(component.kebabName) - const baseName = collection?.prefix - ? component.pascalName.replace(new RegExp(`^${pascalCase(collection?.prefix)}`), '') - : component.pascalName - - acc[componentId] = { - ...component, - baseName, - componentId, - collectionId: collection?.id, - docUrl: collection?.getDocUrl?.(component.pascalName) - } - return acc - }, {} as Record), null, 2) - } - }) + addTemplate({ + filename: 'compodium/components.json', + write: true, + getContents: ({ app }) => { + const collections = (appConfig.compodium as any).collections + const components = [...app.components, ...exampleComponents].reduce((acc, component) => { + const collection = getComponentCollection(component, collections) + const componentId = camelCase(component.kebabName) + const baseName = collection?.prefix + ? component.pascalName.replace(new RegExp(`^${pascalCase(collection?.prefix)}`), '') + : component.pascalName + + acc[componentId] = { + ...component, + baseName, + componentId, + collectionId: collection?.id, + docUrl: collection?.getDocUrl?.(component.pascalName) + } + return acc + }, {} as Record) + + return JSON.stringify(components, null, 2) + } + }) - nuxt.hooks.hookOnce('components:dirs', (dirs) => { addTemplate({ filename: 'compodium/dirs.mjs', write: true, @@ -183,7 +200,28 @@ export default defineNuxtModule({ return `export default ${JSON.stringify([...dirs, ...examplesDirs, ...libraryExampleDirs])}` } }) - addVitePlugin(compodiumVite({ dirs: [...dirs, ...examplesDirs, ...libraryExampleDirs] })) + + addTemplate({ + filename: 'compodium/collections.mjs', + write: true, + getContents: () => { + return `export default ${JSON.stringify(collections.concat(libraryCollections))}` + } + }) + + // This file will be read directly server side. This is a hack after realising that virtual module didn't work with HMR server side. + nuxt.options.nitro.virtual!['#compodium/nitro/dirs'] = () => { + return readFileSync(join(nuxt.options.buildDir, '/compodium/dirs.mjs'), 'utf-8') + } + nuxt.options.nitro.virtual!['#compodium/nitro/collections'] = () => { + return readFileSync(join(nuxt.options.buildDir, '/compodium/collections.mjs'), 'utf-8') + } + + addVitePlugin(compodiumVite({ + dirs: [...dirs, ...examplesDirs, ...libraryExampleDirs] + })) + + addComponentsDir({ path: resolve('./runtime/components'), priority: -1 }) }) if (process.env.COMPODIUM_LOCAL === 'true') { @@ -225,14 +263,6 @@ export default defineNuxtModule({ }) } - // This file will be read directly server side. This is a hack after realising that virtual module didn't work with HMR server side. - nuxt.options.nitro.virtual = nuxt.options.nitro.virtual || {} - nuxt.options.nitro.virtual['#compodium/nitro/dirs'] = () => { - return readFileSync(join(nuxt.options.buildDir, '/compodium/dirs.mjs'), 'utf-8') - } - - (appConfig.compodium as any).componentsPath = join(nuxt.options.buildDir, '/compodium/components.json') - addServerHandler({ method: 'get', route: '/__compodium__/api/collections', diff --git a/src/runtime/server/api/collections.get.ts b/src/runtime/server/api/collections.get.ts index f7c8ba02..1563463b 100644 --- a/src/runtime/server/api/collections.get.ts +++ b/src/runtime/server/api/collections.get.ts @@ -2,12 +2,12 @@ import fs from 'node:fs/promises' import { defineEventHandler } from 'h3' import type { Component, ComponentCollection, Collection, ComponentExample } from '../../../types' import { useAppConfig } from '#imports' -import { pascalCase } from 'scule' import { getComponentCollection } from '../../utils' export default defineEventHandler(async () => { - const config = useAppConfig().compodium as any - const collections = config.collections as Collection[] + const config = (useAppConfig().compodium) as any + // @ts-expect-error virtual file + const collections: Collection[] = await import('#compodium/nitro/collections').then(i => i.default) const componentsRaw = await fs.readFile(config.componentsPath, 'utf-8') const components = Object.values(JSON.parse(componentsRaw)) as (Component | ComponentExample)[] @@ -17,10 +17,8 @@ export default defineEventHandler(async () => { const collection = getComponentCollection(component, collections) if (!collection || component.isExample) return acc - const collectionPrefix = collection.external ? '' : pascalCase(collection.name) - - const componentExamples = examples?.filter(e => e.pascalName.match(`${collectionPrefix}${component.pascalName}Example`)) - const mainExample = componentExamples.find(e => e.pascalName === `${collectionPrefix}${component.pascalName}Example`) + const componentExamples = examples?.filter(e => e.pascalName.startsWith(`${component.pascalName}Example`)) + const mainExample = componentExamples.find(e => e.pascalName === `${component.pascalName}Example`) acc[collection.id] ??= { ...collection, components: {} } acc[collection.id].components[component.componentId] = { @@ -30,7 +28,7 @@ export default defineEventHandler(async () => { componentId: component.componentId, collectionId: collection.id, examples: componentExamples.filter(e => e.pascalName !== mainExample?.pascalName).map(e => ({ - name: e.pascalName.replace(`${collectionPrefix}${component.pascalName}Example`, ''), + name: e.pascalName.replace(`${component.pascalName}Example`, ''), ...e, collectionId: collection.id, componentId: component.componentId diff --git a/src/types.d.ts b/src/types.d.ts index cffdba46..73d7cc93 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -67,7 +67,6 @@ export type Collection = { icon?: string prefix?: string ignore?: string[] - examplesPath?: string } export type ComponentCollection = Collection & { diff --git a/test/basic.test.ts b/test/basic.test.ts index cb50f248..7f8926c3 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -41,13 +41,13 @@ describe('basic', async () => { const collections = await $fetch>('/__compodium__/api/collections') expect(collections.components.components.basicComponent.examples).toEqual([ expect.objectContaining({ - baseName: 'ComponentsBasicComponentExampleWithSuffix', + baseName: 'BasicComponentExampleWithSuffix', collectionId: 'components', componentId: 'basicComponent', shortPath: 'compodium/components/BasicComponentExampleWithSuffix.vue', isExample: true, name: 'WithSuffix', - pascalName: 'ComponentsBasicComponentExampleWithSuffix' + pascalName: 'BasicComponentExampleWithSuffix' }) ]) }) @@ -55,7 +55,7 @@ describe('basic', async () => { it('overrides component with main example', async () => { const collections = await $fetch>('/__compodium__/api/collections') expect(collections.components.components.basicComponent).toEqual(expect.objectContaining({ - pascalName: 'ComponentsBasicComponentExample', + pascalName: 'BasicComponentExample', shortPath: 'compodium/components/BasicComponentExample.vue', collectionId: 'components', componentId: 'basicComponent' @@ -65,7 +65,7 @@ describe('basic', async () => { describe('examples api', () => { it('works', async () => { - const example = await $fetch('/__compodium__/api/example/componentsBasicComponentExample') + const example = await $fetch('/__compodium__/api/example/basicComponentExample') expect(example).toMatchInlineSnapshot(` "