shadcn-vue/packages/cli/src/utils/get-project-info.ts
2024-11-25 21:55:02 +08:00

243 lines
5.7 KiB
TypeScript

import type { Framework } from '@/src/utils/frameworks'
import type {
Config,
RawConfig,
} from '@/src/utils/get-config'
import { FRAMEWORKS } from '@/src/utils/frameworks'
import {
getConfig,
resolveConfigPaths,
} from '@/src/utils/get-config'
import { getPackageInfo } from '@/src/utils/get-package-info'
import fg from 'fast-glob'
import fs from 'fs-extra'
import path from 'pathe'
import { loadConfig } from 'tsconfig-paths'
import { z } from 'zod'
export interface ProjectInfo {
framework: Framework
typescript: boolean
tailwindConfigFile: string | null
tailwindCssFile: string | null
aliasPrefix: string | null
}
const PROJECT_SHARED_IGNORE = [
'**/node_modules/**',
'.nuxt',
'public',
'dist',
'build',
]
const TS_CONFIG_SCHEMA = z.object({
compilerOptions: z.object({
paths: z.record(z.string().or(z.array(z.string()))),
}),
})
export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
const [
configFiles,
typescript,
tailwindConfigFile,
tailwindCssFile,
aliasPrefix,
packageJson,
] = await Promise.all([
fg.glob('**/{nuxt,vite,astro}.config.*|composer.json', {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,
}),
isTypeScriptProject(cwd),
getTailwindConfigFile(cwd),
getTailwindCssFile(cwd),
getTsConfigAliasPrefix(cwd),
getPackageInfo(cwd, false),
])
const type: ProjectInfo = {
framework: FRAMEWORKS.vite, // TODO: Maybe add a manual installation
typescript,
tailwindConfigFile,
tailwindCssFile,
aliasPrefix,
}
// Nuxt.
if (configFiles.find(file => file.startsWith('nuxt.config.'))?.length) {
type.framework = FRAMEWORKS.nuxt
return type
}
// Astro.
if (configFiles.find(file => file.startsWith('astro.config.'))?.length) {
type.framework = FRAMEWORKS.astro
return type
}
// Laravel.
if (configFiles.find(file => file.startsWith('composer.json'))?.length) {
type.framework = FRAMEWORKS.laravel
return type
}
// Vite.
// We'll assume that it got caught by the Remix check above.
if (configFiles.find(file => file.startsWith('vite.config.'))?.length) {
type.framework = FRAMEWORKS.vite
return type
}
return type
}
export async function getTailwindCssFile(cwd: string) {
const files = await fg.glob(['**/*.css', '**/*.scss'], {
cwd,
deep: 5,
ignore: PROJECT_SHARED_IGNORE,
})
if (!files.length) {
return null
}
for (const file of files) {
const contents = await fs.readFile(path.resolve(cwd, file), 'utf8')
// Assume that if the file contains `@tailwind base` it's the main css file.
if (contents.includes('@tailwind base')) {
return file
}
}
return null
}
export async function getTailwindConfigFile(cwd: string) {
const files = await fg.glob('tailwind.config.*', {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,
})
if (!files.length) {
return null
}
return files[0]
}
export async function getTsConfigAliasPrefix(cwd: string) {
const tsConfig = await loadConfig(cwd)
if (
tsConfig?.resultType === 'failed'
|| !Object.entries(tsConfig?.paths).length
) {
return null
}
// This assume that the first alias is the prefix.
for (const [alias, paths] of Object.entries(tsConfig.paths)) {
if (
paths.includes('./*')
|| paths.includes('./src/*')
|| paths.includes('./app/*')
|| paths.includes('./resources/js/*') // Laravel.
) {
const cleanAlias = alias.replace(/\/\*$/, '') ?? null
// handle Nuxt
return cleanAlias === '#build' ? '@' : cleanAlias
}
}
// Use the first alias as the prefix.
return Object.keys(tsConfig?.paths)?.[0].replace(/\/\*$/, '') ?? null
}
export async function isTypeScriptProject(cwd: string) {
const files = await fg.glob('tsconfig.*', {
cwd,
deep: 1,
ignore: PROJECT_SHARED_IGNORE,
})
return files.length > 0
}
export async function getTsConfig(cwd: string) {
for (const fallback of [
'tsconfig.json',
'tsconfig.web.json',
'tsconfig.app.json',
]) {
const filePath = path.resolve(cwd, fallback)
if (!(await fs.pathExists(filePath))) {
continue
}
// We can't use fs.readJSON because it doesn't support comments.
const contents = await fs.readFile(filePath, 'utf8')
const cleanedContents = contents.replace(/\/\*\s*\*\//g, '')
const result = TS_CONFIG_SCHEMA.safeParse(JSON.parse(cleanedContents))
if (result.error) {
continue
}
return result.data
}
return null
}
export async function getProjectConfig(
cwd: string,
defaultProjectInfo: ProjectInfo | null = null,
): Promise<Config | null> {
// Check for existing component config.
const [existingConfig, projectInfo] = await Promise.all([
getConfig(cwd),
!defaultProjectInfo
? getProjectInfo(cwd)
: Promise.resolve(defaultProjectInfo),
])
if (existingConfig) {
return existingConfig
}
if (
!projectInfo
|| !projectInfo.tailwindConfigFile
|| !projectInfo.tailwindCssFile
) {
return null
}
const config: RawConfig = {
$schema: 'https://shadcn-vue.com/schema.json',
typescript: true,
style: 'new-york',
tailwind: {
config: projectInfo.tailwindConfigFile,
baseColor: 'zinc',
css: projectInfo.tailwindCssFile,
cssVariables: true,
prefix: '',
},
iconLibrary: 'lucide',
aliases: {
components: `${projectInfo.aliasPrefix}/components`,
ui: `${projectInfo.aliasPrefix}/components/ui`,
// hooks: `${projectInfo.aliasPrefix}/hooks`,
lib: `${projectInfo.aliasPrefix}/lib`,
utils: `${projectInfo.aliasPrefix}/lib/utils`,
},
}
return await resolveConfigPaths(cwd, config)
}