diff --git a/cli/commands/init.mjs b/cli/commands/init.mjs new file mode 100644 index 00000000..22590ca0 --- /dev/null +++ b/cli/commands/init.mjs @@ -0,0 +1,50 @@ +import { existsSync, promises as fsp } from 'node:fs' +import { resolve } from 'pathe' +import { defineCommand } from 'citty' +import { consola } from 'consola' +import { camelCase, snakeCase } from 'scule' +import templates from '../utils/templates.mjs' + +export default defineCommand({ + meta: { + name: 'init', + description: 'Init a new component.' + }, + args: { + name: { + type: 'positional', + required: true, + description: 'Name of the component.' + } + }, + async setup ({ args }) { + const name = args.name + if (!name) { + consola.error('name argument is missing!') + process.exit(1) + } + + const path = resolve('.') + + for (const template of Object.keys(templates)) { + const { filename, contents } = templates[template]({ name }) + const filePath = resolve(path, filename) + + if (existsSync(filePath)) { + consola.error(`🚨 ${filePath} already exists!`) + continue + } + + await fsp.writeFile(filePath, contents.trim() + '\n') + + consola.success(`🪄 Generated ${filePath}!`) + } + + const themePath = resolve(path, 'src/theme/index.ts') + const theme = await fsp.readFile(themePath, 'utf-8') + const contents = `export { default as ${camelCase(name)} } from './${snakeCase(name)}'` + if (!theme.includes(contents)) { + await fsp.writeFile(themePath, theme.trim() + '\n' + contents + '\n') + } + } +}) diff --git a/cli/index.mjs b/cli/index.mjs new file mode 100644 index 00000000..a816fbe8 --- /dev/null +++ b/cli/index.mjs @@ -0,0 +1,14 @@ +import { defineCommand, runMain } from 'citty' +import init from './commands/init.mjs' + +const main = defineCommand({ + meta: { + name: 'nuxtui', + description: 'Nuxt UI CLI' + }, + subCommands: { + init + } +}) + +runMain(main) diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..255877ec --- /dev/null +++ b/cli/package.json @@ -0,0 +1,12 @@ +{ + "name": "nuxt-ui-cli", + "exports": { + ".": "./index.mjs" + }, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "pathe": "^1.1.2", + "scule": "^1.3.0" + } +} diff --git a/cli/utils/templates.mjs b/cli/utils/templates.mjs new file mode 100644 index 00000000..e6a18f7c --- /dev/null +++ b/cli/utils/templates.mjs @@ -0,0 +1,114 @@ +import { splitByCase, upperFirst, camelCase, snakeCase } from 'scule' + +const component = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)) + const camelName = camelCase(name) + const snakeName = snakeCase(name) + + return { + filename: `src/runtime/components/${upperName}.vue`, + contents: ` + + + + + + ` + } +} + +const theme = ({ name }) => { + const snakeName = snakeCase(name) + + return { + filename: `src/theme/${snakeName}.ts`, + contents: ` +export default { + slots: { + root: '' + }, + variants: { + + }, + defaultVariants: { + + } +} + ` + } +} + +const page = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)) + const snakeName = snakeCase(name) + + return { + filename: `playground/pages/${snakeName}.vue`, + contents: ` + + ` + } +} + +const test = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)) + + return { + filename: `test/components/${upperName}.spec.ts`, + contents: ` +import { describe, it, expect } from 'vitest' +import ${upperName}, { type ${upperName}Props } from '../../src/runtime/components/${upperName}.vue' +import ComponentRender from '../component-render' + +describe('${upperName}', () => { + it.each([ + ['basic case', {}], + ['with class', { props: { class: '' } }], + ['with ui', { props: { ui: {} } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ${upperName}Props, slots?: any }) => { + const html = await ComponentRender(nameOrHtml, options, ${upperName}) + expect(html).toMatchSnapshot() + }) +}) + ` + } +} + +export default { + component, + theme, + page, + test +} diff --git a/package.json b/package.json index 1ee83224..f30cfeab 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dev": "nuxi dev playground", "dev:build": "nuxi build playground", "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", + "cli": "node ./cli/index.mjs", "lint": "eslint .", "typecheck": "vue-tsc --noEmit", "test": "vitest" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3508a8c3..02190056 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,21 @@ importers: specifier: ^2.0.6 version: 2.0.6(typescript@5.4.2) + cli: + dependencies: + citty: + specifier: ^0.1.6 + version: 0.1.6 + consola: + specifier: ^3.2.3 + version: 3.2.3 + pathe: + specifier: ^1.1.2 + version: 1.1.2 + scule: + specifier: ^1.3.0 + version: 1.3.0 + modules/dev: dependencies: '@nuxt/kit': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c2ff44a1..8dbbd164 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: + - "cli" - "playground" - "modules/*" diff --git a/tsconfig.json b/tsconfig.json index 55e466b2..f537e877 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { // https://nuxt.com/docs/guide/concepts/typescript "extends": "./.nuxt/tsconfig.json", - "exclude": ["docs", "dist", "playground", "node_modules"] + "exclude": ["cli", "docs", "dist", "playground", "node_modules"] }