feat: add cli
This commit is contained in:
parent
cabed13294
commit
ea5c63a250
|
|
@ -22,7 +22,7 @@
|
||||||
"pnpm": "^8.6.12",
|
"pnpm": "^8.6.12",
|
||||||
"simple-git-hooks": "^2.9.0",
|
"simple-git-hooks": "^2.9.0",
|
||||||
"turbo": "^1.10.13",
|
"turbo": "^1.10.13",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.2.2",
|
||||||
"vitest": "^0.34.3"
|
"vitest": "^0.34.3"
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"clean": "rimraf dist && rimraf components",
|
"clean": "rimraf dist && rimraf components",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"start:dev": "cross-env COMPONENTS_REGISTRY_URL=http://localhost:3000 node dist/index.js",
|
"start:dev": "cross-env COMPONENTS_REGISTRY_URL=http://localhost:3001 node dist/index.js",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"release": "changeset version",
|
"release": "changeset version",
|
||||||
"pub:beta": "pnpm build && pnpm publish --no-git-checks --access public --tag beta",
|
"pub:beta": "pnpm build && pnpm publish --no-git-checks --access public --tag beta",
|
||||||
|
|
@ -46,6 +46,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antfu/ni": "^0.21.6",
|
"@antfu/ni": "^0.21.6",
|
||||||
|
"@babel/core": "^7.22.11",
|
||||||
|
"@babel/parser": "^7.22.11",
|
||||||
|
"@babel/plugin-transform-typescript": "^7.22.11",
|
||||||
"chalk": "5.3.0",
|
"chalk": "5.3.0",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"cosmiconfig": "^8.2.0",
|
"cosmiconfig": "^8.2.0",
|
||||||
|
|
@ -63,10 +66,12 @@
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/babel__core": "^7.20.1",
|
||||||
"@types/diff": "^5.0.3",
|
"@types/diff": "^5.0.3",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/lodash.template": "^4.5.1",
|
"@types/lodash.template": "^4.5.1",
|
||||||
"@types/prompts": "^2.4.4",
|
"@types/prompts": "^2.4.4",
|
||||||
|
"@vitest/ui": "^0.34.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"tsup": "^7.2.0",
|
"tsup": "^7.2.0",
|
||||||
"type-fest": "^4.3.0",
|
"type-fest": "^4.3.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import { execa } from 'execa'
|
||||||
|
import ora from 'ora'
|
||||||
|
import prompts from 'prompts'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { getConfig } from '@/src/utils/get-config'
|
||||||
|
import { getPackageManager } from '@/src/utils/get-package-manager'
|
||||||
|
import { handleError } from '@/src/utils/handle-error'
|
||||||
|
import { logger } from '@/src/utils/logger'
|
||||||
|
import {
|
||||||
|
fetchTree,
|
||||||
|
getItemTargetPath,
|
||||||
|
getRegistryBaseColor,
|
||||||
|
getRegistryIndex,
|
||||||
|
resolveTree,
|
||||||
|
} from '@/src/utils/registry'
|
||||||
|
import { transform } from '@/src/utils/transformers'
|
||||||
|
|
||||||
|
const addOptionsSchema = z.object({
|
||||||
|
components: z.array(z.string()).optional(),
|
||||||
|
yes: z.boolean(),
|
||||||
|
overwrite: z.boolean(),
|
||||||
|
cwd: z.string(),
|
||||||
|
path: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const add = new Command()
|
||||||
|
.name('add')
|
||||||
|
.description('add a component to your project')
|
||||||
|
.argument('[components...]', 'the components to add')
|
||||||
|
.option('-y, --yes', 'skip confirmation prompt.', false)
|
||||||
|
.option('-o, --overwrite', 'overwrite existing files.', false)
|
||||||
|
.option(
|
||||||
|
'-c, --cwd <cwd>',
|
||||||
|
'the working directory. defaults to the current directory.',
|
||||||
|
process.cwd(),
|
||||||
|
)
|
||||||
|
.option('-p, --path <path>', 'the path to add the component to.')
|
||||||
|
.action(async (components, opts) => {
|
||||||
|
try {
|
||||||
|
const options = addOptionsSchema.parse({
|
||||||
|
components,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cwd = path.resolve(options.cwd)
|
||||||
|
|
||||||
|
if (!existsSync(cwd)) {
|
||||||
|
logger.error(`The path ${cwd} does not exist. Please try again.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getConfig(cwd)
|
||||||
|
if (!config) {
|
||||||
|
logger.warn(
|
||||||
|
`Configuration is missing. Please run ${chalk.green( 'init' )} to create a components.json file.`,
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryIndex = await getRegistryIndex()
|
||||||
|
|
||||||
|
let selectedComponents = options.components
|
||||||
|
if (!options.components?.length) {
|
||||||
|
const { components } = await prompts({
|
||||||
|
type: 'autocompleteMultiselect',
|
||||||
|
name: 'components',
|
||||||
|
message: 'Which components would you like to add?',
|
||||||
|
hint: 'Space to select. A to toggle all. Enter to submit.',
|
||||||
|
instructions: false,
|
||||||
|
choices: registryIndex.map(entry => ({
|
||||||
|
title: entry.name,
|
||||||
|
value: entry.name,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
selectedComponents = components
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedComponents?.length) {
|
||||||
|
logger.warn('No components selected. Exiting.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = await resolveTree(registryIndex, selectedComponents)
|
||||||
|
const payload = await fetchTree(config.style, tree)
|
||||||
|
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||||
|
|
||||||
|
if (!payload.length) {
|
||||||
|
logger.warn('Selected components not found. Exiting.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.yes) {
|
||||||
|
const { proceed } = await prompts({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'proceed',
|
||||||
|
message: 'Ready to install components and dependencies. Proceed?',
|
||||||
|
initial: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!proceed)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = ora('Installing components...').start()
|
||||||
|
for (const item of payload) {
|
||||||
|
spinner.text = `Installing ${item.name}...`
|
||||||
|
const targetDir = await getItemTargetPath(
|
||||||
|
config,
|
||||||
|
item,
|
||||||
|
options.path ? path.resolve(cwd, options.path) : undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!targetDir)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (!existsSync(targetDir))
|
||||||
|
await fs.mkdir(targetDir, { recursive: true })
|
||||||
|
|
||||||
|
const existingComponent = item.files.filter(file =>
|
||||||
|
existsSync(path.resolve(targetDir, file.name)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingComponent.length && !options.overwrite) {
|
||||||
|
if (selectedComponents.includes(item.name)) {
|
||||||
|
logger.warn(
|
||||||
|
`Component ${item.name} already exists. Use ${chalk.green(
|
||||||
|
'--overwrite',
|
||||||
|
)} to overwrite.`,
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of item.files) {
|
||||||
|
let filePath = path.resolve(targetDir, file.name)
|
||||||
|
|
||||||
|
// Run transformers.
|
||||||
|
const content = await transform({
|
||||||
|
filename: file.name,
|
||||||
|
raw: file.content,
|
||||||
|
config,
|
||||||
|
baseColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// if (!config.tsx)
|
||||||
|
// filePath = filePath.replace(/\.tsx$/, '.jsx')
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies.
|
||||||
|
if (item.dependencies?.length) {
|
||||||
|
const packageManager = await getPackageManager(cwd)
|
||||||
|
await execa(
|
||||||
|
packageManager,
|
||||||
|
[
|
||||||
|
packageManager === 'npm' ? 'install' : 'add',
|
||||||
|
...item.dependencies,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spinner.succeed('Done.')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import { type Change, diffLines } from 'diff'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import type { Config } from '@/src/utils/get-config'
|
||||||
|
import { getConfig } from '@/src/utils/get-config'
|
||||||
|
import { handleError } from '@/src/utils/handle-error'
|
||||||
|
import { logger } from '@/src/utils/logger'
|
||||||
|
import {
|
||||||
|
fetchTree,
|
||||||
|
getItemTargetPath,
|
||||||
|
getRegistryBaseColor,
|
||||||
|
getRegistryIndex,
|
||||||
|
} from '@/src/utils/registry'
|
||||||
|
import type { registryIndexSchema } from '@/src/utils/registry/schema'
|
||||||
|
import { transform } from '@/src/utils/transformers'
|
||||||
|
|
||||||
|
const updateOptionsSchema = z.object({
|
||||||
|
component: z.string().optional(),
|
||||||
|
yes: z.boolean(),
|
||||||
|
cwd: z.string(),
|
||||||
|
path: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const diff = new Command()
|
||||||
|
.name('diff')
|
||||||
|
.description('check for updates against the registry')
|
||||||
|
.argument('[component]', 'the component name')
|
||||||
|
.option('-y, --yes', 'skip confirmation prompt.', false)
|
||||||
|
.option(
|
||||||
|
'-c, --cwd <cwd>',
|
||||||
|
'the working directory. defaults to the current directory.',
|
||||||
|
process.cwd(),
|
||||||
|
)
|
||||||
|
.action(async (name, opts) => {
|
||||||
|
try {
|
||||||
|
const options = updateOptionsSchema.parse({
|
||||||
|
component: name,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cwd = path.resolve(options.cwd)
|
||||||
|
|
||||||
|
if (!existsSync(cwd)) {
|
||||||
|
logger.error(`The path ${cwd} does not exist. Please try again.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getConfig(cwd)
|
||||||
|
if (!config) {
|
||||||
|
logger.warn(
|
||||||
|
`Configuration is missing. Please run ${chalk.green(
|
||||||
|
'init',
|
||||||
|
)} to create a components.json file.`,
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const registryIndex = await getRegistryIndex()
|
||||||
|
|
||||||
|
if (!options.component) {
|
||||||
|
const targetDir = config.resolvedPaths.components
|
||||||
|
|
||||||
|
// Find all components that exist in the project.
|
||||||
|
const projectComponents = registryIndex.filter((item) => {
|
||||||
|
for (const file of item.files) {
|
||||||
|
const filePath = path.resolve(targetDir, file)
|
||||||
|
if (existsSync(filePath))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check for updates.
|
||||||
|
const componentsWithUpdates = []
|
||||||
|
for (const component of projectComponents) {
|
||||||
|
const changes = await diffComponent(component, config)
|
||||||
|
if (changes.length) {
|
||||||
|
componentsWithUpdates.push({
|
||||||
|
name: component.name,
|
||||||
|
changes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!componentsWithUpdates.length) {
|
||||||
|
logger.info('No updates found.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('The following components have updates available:')
|
||||||
|
for (const component of componentsWithUpdates) {
|
||||||
|
logger.info(`- ${component.name}`)
|
||||||
|
for (const change of component.changes)
|
||||||
|
logger.info(` - ${change.filePath}`)
|
||||||
|
}
|
||||||
|
logger.break()
|
||||||
|
logger.info(
|
||||||
|
`Run ${chalk.green('diff <component>')} to see the changes.`,
|
||||||
|
)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show diff for a single component.
|
||||||
|
const component = registryIndex.find(
|
||||||
|
item => item.name === options.component,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
logger.error(
|
||||||
|
`The component ${chalk.green(options.component)} does not exist.`,
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changes = await diffComponent(component, config)
|
||||||
|
|
||||||
|
if (!changes.length) {
|
||||||
|
logger.info(`No updates found for ${options.component}.`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
logger.info(`- ${change.filePath}`)
|
||||||
|
printDiff(change.patch)
|
||||||
|
logger.info('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function diffComponent(
|
||||||
|
component: z.infer<typeof registryIndexSchema>[number],
|
||||||
|
config: Config,
|
||||||
|
) {
|
||||||
|
const payload = await fetchTree(config.style, [component])
|
||||||
|
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||||
|
|
||||||
|
const changes = []
|
||||||
|
|
||||||
|
for (const item of payload) {
|
||||||
|
const targetDir = await getItemTargetPath(config, item)
|
||||||
|
|
||||||
|
if (!targetDir)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for (const file of item.files) {
|
||||||
|
const filePath = path.resolve(targetDir, file.name)
|
||||||
|
|
||||||
|
if (!existsSync(filePath))
|
||||||
|
continue
|
||||||
|
|
||||||
|
const fileContent = await fs.readFile(filePath, 'utf8')
|
||||||
|
|
||||||
|
const registryContent = await transform({
|
||||||
|
filename: file.name,
|
||||||
|
raw: file.content,
|
||||||
|
config,
|
||||||
|
baseColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
const patch = diffLines(registryContent as string, fileContent)
|
||||||
|
if (patch.length > 1) {
|
||||||
|
changes.push({
|
||||||
|
file: file.name,
|
||||||
|
filePath,
|
||||||
|
patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Does is it need to async?
|
||||||
|
function printDiff(diff: Change[]) {
|
||||||
|
diff.forEach((part) => {
|
||||||
|
if (part) {
|
||||||
|
if (part.added)
|
||||||
|
return process.stdout.write(chalk.green(part.value))
|
||||||
|
|
||||||
|
if (part.removed)
|
||||||
|
return process.stdout.write(chalk.red(part.value))
|
||||||
|
|
||||||
|
return process.stdout.write(part.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import { execa } from 'execa'
|
||||||
|
import template from 'lodash.template'
|
||||||
|
import ora from 'ora'
|
||||||
|
import prompts from 'prompts'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import * as templates from '@/src/utils/templates'
|
||||||
|
import {
|
||||||
|
getRegistryBaseColor,
|
||||||
|
getRegistryBaseColors,
|
||||||
|
getRegistryStyles,
|
||||||
|
} from '@/src/utils/registry'
|
||||||
|
import { logger } from '@/src/utils/logger'
|
||||||
|
import { handleError } from '@/src/utils/handle-error'
|
||||||
|
import { getPackageManager } from '@/src/utils/get-package-manager'
|
||||||
|
import {
|
||||||
|
type Config,
|
||||||
|
DEFAULT_COMPONENTS,
|
||||||
|
DEFAULT_TAILWIND_CONFIG,
|
||||||
|
DEFAULT_TAILWIND_CSS,
|
||||||
|
DEFAULT_TAILWIND_CSS_NUXT,
|
||||||
|
DEFAULT_UTILS,
|
||||||
|
getConfig,
|
||||||
|
rawConfigSchema,
|
||||||
|
resolveConfigPaths,
|
||||||
|
} from '@/src/utils/get-config'
|
||||||
|
|
||||||
|
const PROJECT_DEPENDENCIES = {
|
||||||
|
base: [
|
||||||
|
'tailwindcss-animate',
|
||||||
|
'class-variance-authority',
|
||||||
|
'clsx',
|
||||||
|
'tailwind-merge',
|
||||||
|
],
|
||||||
|
vue: [
|
||||||
|
'tailwindcss',
|
||||||
|
'postcss',
|
||||||
|
'autoprefixer',
|
||||||
|
],
|
||||||
|
nuxt: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const initOptionsSchema = z.object({
|
||||||
|
cwd: z.string(),
|
||||||
|
yes: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const init = new Command()
|
||||||
|
.name('init')
|
||||||
|
.description('initialize your project and install dependencies')
|
||||||
|
.option('-y, --yes', 'skip confirmation prompt.', false)
|
||||||
|
.option(
|
||||||
|
'-c, --cwd <cwd>',
|
||||||
|
'the working directory. defaults to the current directory.',
|
||||||
|
process.cwd(),
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
const options = initOptionsSchema.parse(opts)
|
||||||
|
const cwd = path.resolve(options.cwd)
|
||||||
|
|
||||||
|
// Ensure target directory exists.
|
||||||
|
if (!existsSync(cwd)) {
|
||||||
|
logger.error(`The path ${cwd} does not exist. Please try again.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read config.
|
||||||
|
const existingConfig = await getConfig(cwd)
|
||||||
|
const config = await promptForConfig(cwd, existingConfig, options.yes)
|
||||||
|
|
||||||
|
await runInit(cwd, config)
|
||||||
|
|
||||||
|
logger.info('')
|
||||||
|
logger.info(
|
||||||
|
`${chalk.green('Success!')} Project initialization completed.`,
|
||||||
|
)
|
||||||
|
logger.info('')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function promptForConfig(
|
||||||
|
cwd: string,
|
||||||
|
defaultConfig: Config | null = null,
|
||||||
|
skip = false,
|
||||||
|
) {
|
||||||
|
const highlight = (text: string) => chalk.cyan(text)
|
||||||
|
|
||||||
|
const styles = await getRegistryStyles()
|
||||||
|
const baseColors = await getRegistryBaseColors()
|
||||||
|
|
||||||
|
const options = await prompts([
|
||||||
|
{
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'typescript',
|
||||||
|
message: `Would you like to use ${highlight('TypeScript')} (recommended)?`,
|
||||||
|
initial: defaultConfig?.typescript ?? true,
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'framework',
|
||||||
|
message: `Which ${highlight('framework')} are you using?`,
|
||||||
|
choices: [
|
||||||
|
{title: 'Nuxt', value: 'nuxt'},
|
||||||
|
{title: 'Vite + Vue', value: 'vue'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'style',
|
||||||
|
message: `Which ${highlight('style')} would you like to use?`,
|
||||||
|
choices: styles.map(style => ({
|
||||||
|
title: style.label,
|
||||||
|
value: style.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'tailwindBaseColor',
|
||||||
|
message: `Which color would you like to use as ${highlight(
|
||||||
|
'base color',
|
||||||
|
)}?`,
|
||||||
|
choices: baseColors.map(color => ({
|
||||||
|
title: color.label,
|
||||||
|
value: color.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tailwindCss',
|
||||||
|
message: `Where is your ${highlight('Tailwind CSS')} file?`,
|
||||||
|
initial: (prev,values) => defaultConfig?.tailwind.css ?? values.framework === 'nuxt' ? DEFAULT_TAILWIND_CSS_NUXT : DEFAULT_TAILWIND_CSS
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'tailwindCssVariables',
|
||||||
|
message: `Would you like to use ${highlight(
|
||||||
|
'CSS variables',
|
||||||
|
)} for colors?`,
|
||||||
|
initial: defaultConfig?.tailwind.cssVariables ?? true,
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tailwindConfig',
|
||||||
|
message: `Where is your ${highlight('tailwind.config.js')} located?`,
|
||||||
|
initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'components',
|
||||||
|
message: `Configure the import alias for ${highlight('components')}:`,
|
||||||
|
initial: defaultConfig?.aliases.components ?? DEFAULT_COMPONENTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'utils',
|
||||||
|
message: `Configure the import alias for ${highlight('utils')}:`,
|
||||||
|
initial: defaultConfig?.aliases.utils ?? DEFAULT_UTILS,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const config = rawConfigSchema.parse({
|
||||||
|
// $schema: 'https://ui.shadcn.com/schema.json',
|
||||||
|
style: options.style,
|
||||||
|
typescript: options.typescript,
|
||||||
|
framework: options.framework,
|
||||||
|
tailwind: {
|
||||||
|
config: options.tailwindConfig,
|
||||||
|
css: options.tailwindCss,
|
||||||
|
baseColor: options.tailwindBaseColor,
|
||||||
|
cssVariables: options.tailwindCssVariables,
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
utils: options.utils,
|
||||||
|
components: options.components,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!skip) {
|
||||||
|
const { proceed } = await prompts({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'proceed',
|
||||||
|
message: `Write configuration to ${highlight('components.json')}. Proceed?`,
|
||||||
|
initial: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!proceed)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file.
|
||||||
|
logger.info('')
|
||||||
|
const spinner = ora('Writing components.json...').start()
|
||||||
|
const targetPath = path.resolve(cwd, 'components.json')
|
||||||
|
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8')
|
||||||
|
spinner.succeed()
|
||||||
|
|
||||||
|
return await resolveConfigPaths(cwd, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runInit(cwd: string, config: Config) {
|
||||||
|
const spinner = ora('Initializing project...')?.start()
|
||||||
|
|
||||||
|
// Ensure all resolved paths directories exist.
|
||||||
|
for (const [key, resolvedPath] of Object.entries(config.resolvedPaths)) {
|
||||||
|
// Determine if the path is a file or directory.
|
||||||
|
// TODO: is there a better way to do this?
|
||||||
|
let dirname = path.extname(resolvedPath)
|
||||||
|
? path.dirname(resolvedPath)
|
||||||
|
: resolvedPath
|
||||||
|
|
||||||
|
// If the utils alias is set to something like "@/lib/utils",
|
||||||
|
// assume this is a file and remove the "utils" file name.
|
||||||
|
// TODO: In future releases we should add support for individual utils.
|
||||||
|
if (key === 'utils' && resolvedPath.endsWith('/utils')) {
|
||||||
|
// Remove /utils at the end.
|
||||||
|
dirname = dirname.replace(/\/utils$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(dirname))
|
||||||
|
await fs.mkdir(dirname, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = config.typescript ? 'ts' : 'js'
|
||||||
|
|
||||||
|
// Write tailwind config.
|
||||||
|
await fs.writeFile(
|
||||||
|
config.resolvedPaths.tailwindConfig,
|
||||||
|
config.tailwind.cssVariables
|
||||||
|
? template(config.framework === 'nuxt' ? templates.NUXT_TAILWIND_CONFIG_WITH_VARIABLES : templates.TAILWIND_CONFIG_WITH_VARIABLES)({ extension })
|
||||||
|
: template(config.framework === 'nuxt' ? templates.NUXT_TAILWIND_CONFIG : templates.TAILWIND_CONFIG)({ extension }),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Write css file.
|
||||||
|
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||||
|
if (baseColor) {
|
||||||
|
await fs.writeFile(
|
||||||
|
config.resolvedPaths.tailwindCss,
|
||||||
|
config.tailwind.cssVariables
|
||||||
|
? baseColor.cssVarsTemplate
|
||||||
|
: baseColor.inlineColorsTemplate,
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cn file.
|
||||||
|
await fs.writeFile(
|
||||||
|
`${config.resolvedPaths.utils}.${extension}`,
|
||||||
|
extension === 'ts' ? templates.UTILS : templates.UTILS_JS,
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner?.succeed()
|
||||||
|
|
||||||
|
// Install dependencies.
|
||||||
|
const dependenciesSpinner = ora('Installing dependencies...')?.start()
|
||||||
|
const packageManager = await getPackageManager(cwd)
|
||||||
|
|
||||||
|
// TODO: add support for other icon libraries.
|
||||||
|
const deps = PROJECT_DEPENDENCIES.base.concat(
|
||||||
|
config.framework === 'nuxt' ? PROJECT_DEPENDENCIES.nuxt : PROJECT_DEPENDENCIES.vue,
|
||||||
|
).concat(
|
||||||
|
config.style === 'new-york' ? [] : ['lucide-vue-next'],
|
||||||
|
)
|
||||||
|
|
||||||
|
await execa(
|
||||||
|
packageManager,
|
||||||
|
[packageManager === 'npm' ? 'install' : 'add', ...deps],
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dependenciesSpinner?.succeed()
|
||||||
|
}
|
||||||
|
|
@ -3,20 +3,29 @@ import process from 'node:process'
|
||||||
|
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
|
|
||||||
|
import { add } from '@/src/commands/add'
|
||||||
|
import { diff } from '@/src/commands/diff'
|
||||||
|
import { init } from '@/src/commands/init'
|
||||||
|
import { getPackageInfo } from '@/src/utils/get-package-info'
|
||||||
|
|
||||||
process.on('SIGINT', () => process.exit(0))
|
process.on('SIGINT', () => process.exit(0))
|
||||||
process.on('SIGTERM', () => process.exit(0))
|
process.on('SIGTERM', () => process.exit(0))
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const packageInfo = await getPackageInfo()
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
.name('shadcn-vue')
|
.name('shadcn-vue')
|
||||||
.description('add components and dependencies to your project')
|
.description('add components and dependencies to your project')
|
||||||
.version(
|
.version(
|
||||||
'1.0.0',
|
packageInfo.version || '1.0.0',
|
||||||
'-v, --version',
|
'-v, --version',
|
||||||
'display the version number',
|
'display the version number',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
program.addCommand(init).addCommand(add).addCommand(diff)
|
||||||
|
|
||||||
program.parse()
|
program.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
await main()
|
main()
|
||||||
|
|
|
||||||
96
packages/cli/src/utils/get-config.ts
Normal file
96
packages/cli/src/utils/get-config.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import path from "path"
|
||||||
|
import { resolveImport } from "@/src/utils/resolve-import"
|
||||||
|
import { cosmiconfig } from 'cosmiconfig'
|
||||||
|
import { loadConfig } from 'tsconfig-paths'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
export const DEFAULT_STYLE = 'default'
|
||||||
|
export const DEFAULT_COMPONENTS = '@/components'
|
||||||
|
export const DEFAULT_UTILS = '@/utils'
|
||||||
|
export const DEFAULT_TAILWIND_CSS = 'src/style.css'
|
||||||
|
export const DEFAULT_TAILWIND_CSS_NUXT = 'assets/style/tailwind.css'
|
||||||
|
export const DEFAULT_TAILWIND_CONFIG = 'tailwind.config.js'
|
||||||
|
export const DEFAULT_TAILWIND_BASE_COLOR = 'slate'
|
||||||
|
|
||||||
|
// TODO: Figure out if we want to support all cosmiconfig formats.
|
||||||
|
// A simple components.json file would be nice.
|
||||||
|
const explorer = cosmiconfig('components', {
|
||||||
|
searchPlaces: ['components.json'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const rawConfigSchema = z
|
||||||
|
.object({
|
||||||
|
$schema: z.string().optional(),
|
||||||
|
style: z.string(),
|
||||||
|
typescript: z.boolean().default(false),
|
||||||
|
tailwind: z.object({
|
||||||
|
config: z.string(),
|
||||||
|
css: z.string(),
|
||||||
|
baseColor: z.string(),
|
||||||
|
cssVariables: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
framework: z.string(),
|
||||||
|
aliases: z.object({
|
||||||
|
components: z.string(),
|
||||||
|
utils: z.string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export type RawConfig = z.infer<typeof rawConfigSchema>
|
||||||
|
|
||||||
|
export const configSchema = rawConfigSchema
|
||||||
|
.extend({
|
||||||
|
resolvedPaths: z.object({
|
||||||
|
tailwindConfig: z.string(),
|
||||||
|
tailwindCss: z.string(),
|
||||||
|
utils: z.string(),
|
||||||
|
components: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Config = z.infer<typeof configSchema>
|
||||||
|
|
||||||
|
export async function getConfig(cwd: string) {
|
||||||
|
const config = await getRawConfig(cwd)
|
||||||
|
|
||||||
|
if (!config)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return await resolveConfigPaths(cwd, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveConfigPaths(cwd: string, config: RawConfig) {
|
||||||
|
// Read tsconfig.json.
|
||||||
|
const tsConfig = await loadConfig(cwd)
|
||||||
|
|
||||||
|
if (tsConfig.resultType === 'failed') {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load tsconfig.json. ${tsConfig.message ?? ''}`.trim(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configSchema.parse({
|
||||||
|
...config,
|
||||||
|
resolvedPaths: {
|
||||||
|
tailwindConfig: path.resolve(cwd, config.tailwind.config),
|
||||||
|
tailwindCss: path.resolve(cwd, config.tailwind.css),
|
||||||
|
utils: await resolveImport(config.aliases.utils, tsConfig),
|
||||||
|
components: await resolveImport(config.aliases.components, tsConfig),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRawConfig(cwd: string): Promise<RawConfig | null> {
|
||||||
|
try {
|
||||||
|
const configResult = await explorer.search(cwd)
|
||||||
|
|
||||||
|
if (!configResult)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return rawConfigSchema.parse(configResult.config)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error(`Invalid configuration found in ${cwd}/components.json.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/cli/src/utils/get-package-info.ts
Normal file
9
packages/cli/src/utils/get-package-info.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import { type PackageJson } from 'type-fest'
|
||||||
|
|
||||||
|
export function getPackageInfo() {
|
||||||
|
const packageJsonPath = path.join('package.json')
|
||||||
|
|
||||||
|
return fs.readJSONSync(packageJsonPath) as PackageJson
|
||||||
|
}
|
||||||
16
packages/cli/src/utils/get-package-manager.ts
Normal file
16
packages/cli/src/utils/get-package-manager.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { detect } from '@antfu/ni'
|
||||||
|
|
||||||
|
export async function getPackageManager(
|
||||||
|
targetDir: string,
|
||||||
|
): Promise<'yarn' | 'pnpm' | 'bun' | 'npm'> {
|
||||||
|
const packageManager = await detect({ programmatic: true, cwd: targetDir })
|
||||||
|
|
||||||
|
if (packageManager === 'yarn@berry')
|
||||||
|
return 'yarn'
|
||||||
|
if (packageManager === 'pnpm@6')
|
||||||
|
return 'pnpm'
|
||||||
|
if (packageManager === 'bun')
|
||||||
|
return 'bun'
|
||||||
|
|
||||||
|
return packageManager ?? 'npm'
|
||||||
|
}
|
||||||
45
packages/cli/src/utils/get-project-info.ts
Normal file
45
packages/cli/src/utils/get-project-info.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
|
||||||
|
export async function getProjectInfo() {
|
||||||
|
const info = {
|
||||||
|
tsconfig: null,
|
||||||
|
isNuxt: false,
|
||||||
|
isVueVite: false,
|
||||||
|
srcDir: false,
|
||||||
|
componentsUiDir: false,
|
||||||
|
srcComponentsUiDir: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tsconfig = await getTsConfig()
|
||||||
|
|
||||||
|
return {
|
||||||
|
tsconfig,
|
||||||
|
isNuxt: existsSync(path.resolve('./nuxt.config.js')) || existsSync(path.resolve('./nuxt.config.ts')),
|
||||||
|
isVueVite: existsSync(path.resolve('./vite.config.js')) || existsSync(path.resolve('./vite.config.ts')),
|
||||||
|
srcDir: existsSync(path.resolve('./src')),
|
||||||
|
srcComponentsUiDir: existsSync(path.resolve('./src/components/ui')),
|
||||||
|
componentsUiDir: existsSync(path.resolve('./components/ui')),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTsConfig() {
|
||||||
|
try {
|
||||||
|
const tsconfigPath = path.join('tsconfig.json')
|
||||||
|
const tsconfig = await fs.readJSON(tsconfigPath)
|
||||||
|
|
||||||
|
if (!tsconfig)
|
||||||
|
throw new Error('tsconfig.json is missing')
|
||||||
|
|
||||||
|
return tsconfig
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/cli/src/utils/handle-error.ts
Normal file
17
packages/cli/src/utils/handle-error.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// import { logger } from '@/src/utils/logger'
|
||||||
|
import {consola} from 'consola'
|
||||||
|
|
||||||
|
export function handleError(error: unknown) {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
consola.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
consola.error(error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.error('Something went wrong. Please try again.')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
19
packages/cli/src/utils/logger.ts
Normal file
19
packages/cli/src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
export const logger = {
|
||||||
|
error(...args: unknown[]) {
|
||||||
|
console.log(chalk.red(...args))
|
||||||
|
},
|
||||||
|
warn(...args: unknown[]) {
|
||||||
|
console.log(chalk.yellow(...args))
|
||||||
|
},
|
||||||
|
info(...args: unknown[]) {
|
||||||
|
console.log(chalk.cyan(...args))
|
||||||
|
},
|
||||||
|
success(...args: unknown[]) {
|
||||||
|
console.log(chalk.green(...args))
|
||||||
|
},
|
||||||
|
break() {
|
||||||
|
console.log("")
|
||||||
|
},
|
||||||
|
}
|
||||||
155
packages/cli/src/utils/registry/index.ts
Normal file
155
packages/cli/src/utils/registry/index.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import path from 'node:path'
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||||
|
import fetch from 'node-fetch'
|
||||||
|
import type * as z from 'zod'
|
||||||
|
import {
|
||||||
|
registryBaseColorSchema,
|
||||||
|
registryIndexSchema,
|
||||||
|
registryWithContentSchema,
|
||||||
|
stylesSchema,
|
||||||
|
} from '@/src/utils/registry/schema'
|
||||||
|
import type { registryItemWithContentSchema } from '@/src/utils/registry/schema'
|
||||||
|
import type { Config } from '@/src/utils/get-config'
|
||||||
|
|
||||||
|
const baseUrl = process.env.COMPONENTS_REGISTRY_URL ?? 'https://ui.shadcn.com'
|
||||||
|
const agent = process.env.https_proxy
|
||||||
|
? new HttpsProxyAgent(process.env.https_proxy)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
export async function getRegistryIndex() {
|
||||||
|
try {
|
||||||
|
const [result] = await fetchRegistry(['index.json'])
|
||||||
|
|
||||||
|
return registryIndexSchema.parse(result)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error('Failed to fetch components from registry.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRegistryStyles() {
|
||||||
|
try {
|
||||||
|
const [result] = await fetchRegistry(['styles/index.json'])
|
||||||
|
|
||||||
|
return stylesSchema.parse(result)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error('Failed to fetch styles from registry.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRegistryBaseColors() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'slate',
|
||||||
|
label: 'Slate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gray',
|
||||||
|
label: 'Gray',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zinc',
|
||||||
|
label: 'Zinc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'neutral',
|
||||||
|
label: 'Neutral',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stone',
|
||||||
|
label: 'Stone',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRegistryBaseColor(baseColor: string) {
|
||||||
|
try {
|
||||||
|
const [result] = await fetchRegistry([`colors/${baseColor}.json`])
|
||||||
|
|
||||||
|
return registryBaseColorSchema.parse(result)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error('Failed to fetch base color from registry.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTree(
|
||||||
|
index: z.infer<typeof registryIndexSchema>,
|
||||||
|
names: string[],
|
||||||
|
) {
|
||||||
|
const tree: z.infer<typeof registryIndexSchema> = []
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const entry = index.find(entry => entry.name === name)
|
||||||
|
|
||||||
|
if (!entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
tree.push(entry)
|
||||||
|
|
||||||
|
if (entry.registryDependencies) {
|
||||||
|
const dependencies = await resolveTree(index, entry.registryDependencies)
|
||||||
|
tree.push(...dependencies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.filter(
|
||||||
|
(component, index, self) =>
|
||||||
|
self.findIndex(c => c.name === component.name) === index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTree(
|
||||||
|
style: string,
|
||||||
|
tree: z.infer<typeof registryIndexSchema>,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const paths = tree.map(item => `styles/${style}/${item.name}.json`)
|
||||||
|
const result = await fetchRegistry(paths)
|
||||||
|
|
||||||
|
return registryWithContentSchema.parse(result)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error('Failed to fetch tree from registry.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getItemTargetPath(
|
||||||
|
config: Config,
|
||||||
|
item: Pick<z.infer<typeof registryItemWithContentSchema>, 'type'>,
|
||||||
|
override?: string,
|
||||||
|
) {
|
||||||
|
// Allow overrides for all items but ui.
|
||||||
|
if (override && item.type !== 'components:ui')
|
||||||
|
return override
|
||||||
|
|
||||||
|
const [parent, type] = item.type.split(':')
|
||||||
|
if (!(parent in config.resolvedPaths))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return path.join(
|
||||||
|
config.resolvedPaths[parent as keyof typeof config.resolvedPaths],
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRegistry(paths: string[]) {
|
||||||
|
try {
|
||||||
|
const results = await Promise.all(
|
||||||
|
paths.map(async (path) => {
|
||||||
|
const response = await fetch(`${baseUrl}/registry/${path}`, {
|
||||||
|
agent,
|
||||||
|
})
|
||||||
|
return await response.json()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error)
|
||||||
|
throw new Error(`Failed to fetch registry from ${baseUrl}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
packages/cli/src/utils/registry/schema.ts
Normal file
43
packages/cli/src/utils/registry/schema.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
// TODO: Extract this to a shared package.
|
||||||
|
export const registryItemSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
dependencies: z.array(z.string()).optional(),
|
||||||
|
registryDependencies: z.array(z.string()).optional(),
|
||||||
|
files: z.array(z.string()),
|
||||||
|
type: z.enum(['components:ui', 'components:component', 'components:example']),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registryIndexSchema = z.array(registryItemSchema)
|
||||||
|
|
||||||
|
export const registryItemWithContentSchema = registryItemSchema.extend({
|
||||||
|
files: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registryWithContentSchema = z.array(registryItemWithContentSchema)
|
||||||
|
|
||||||
|
export const stylesSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const registryBaseColorSchema = z.object({
|
||||||
|
inlineColors: z.object({
|
||||||
|
light: z.record(z.string(), z.string()),
|
||||||
|
dark: z.record(z.string(), z.string()),
|
||||||
|
}),
|
||||||
|
cssVars: z.object({
|
||||||
|
light: z.record(z.string(), z.string()),
|
||||||
|
dark: z.record(z.string(), z.string()),
|
||||||
|
}),
|
||||||
|
inlineColorsTemplate: z.string(),
|
||||||
|
cssVarsTemplate: z.string(),
|
||||||
|
})
|
||||||
13
packages/cli/src/utils/resolve-import.ts
Normal file
13
packages/cli/src/utils/resolve-import.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { type ConfigLoaderSuccessResult, createMatchPath } from 'tsconfig-paths'
|
||||||
|
|
||||||
|
export function resolveImport(
|
||||||
|
importPath: string,
|
||||||
|
config: Pick<ConfigLoaderSuccessResult, 'absoluteBaseUrl' | 'paths'>,
|
||||||
|
) {
|
||||||
|
return createMatchPath(config.absoluteBaseUrl, config.paths)(
|
||||||
|
importPath,
|
||||||
|
undefined,
|
||||||
|
() => true,
|
||||||
|
['.ts', '.tsx'],
|
||||||
|
)
|
||||||
|
}
|
||||||
227
packages/cli/src/utils/templates.ts
Normal file
227
packages/cli/src/utils/templates.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
export const UTILS = `import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const UTILS_JS = `import { clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TAILWIND_CONFIG = `/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const TAILWIND_CONFIG_WITH_VARIABLES = `/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const NUXT_TAILWIND_CONFIG = `/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}`
|
||||||
|
|
||||||
|
export const NUXT_TAILWIND_CONFIG_WITH_VARIABLES = `/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}`
|
||||||
51
packages/cli/src/utils/transformers/index.ts
Normal file
51
packages/cli/src/utils/transformers/index.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { promises as fs } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { Project, ScriptKind, type SourceFile } from 'ts-morph'
|
||||||
|
import type * as z from 'zod'
|
||||||
|
import type { Config } from '@/src/utils/get-config'
|
||||||
|
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||||
|
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
||||||
|
import { transformImport } from '@/src/utils/transformers/transform-import'
|
||||||
|
|
||||||
|
export interface TransformOpts {
|
||||||
|
filename: string
|
||||||
|
raw: string
|
||||||
|
config: Config
|
||||||
|
baseColor?: z.infer<typeof registryBaseColorSchema>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Transformer<Output = SourceFile> = (
|
||||||
|
opts: TransformOpts & {
|
||||||
|
sourceFile: SourceFile
|
||||||
|
}
|
||||||
|
) => Promise<Output>
|
||||||
|
|
||||||
|
const transformers: Transformer[] = [
|
||||||
|
transformImport,
|
||||||
|
transformCssVars,
|
||||||
|
]
|
||||||
|
|
||||||
|
const project = new Project({
|
||||||
|
compilerOptions: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createTempSourceFile(filename: string) {
|
||||||
|
const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-'))
|
||||||
|
return path.join(dir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transform(opts: TransformOpts) {
|
||||||
|
const tempFile = await createTempSourceFile(opts.filename)
|
||||||
|
const sourceFile = project.createSourceFile(tempFile, opts.raw, {
|
||||||
|
scriptKind: ScriptKind.TSX,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const transformer of transformers)
|
||||||
|
transformer({ sourceFile, ...opts })
|
||||||
|
|
||||||
|
// return await transformJsx({
|
||||||
|
// sourceFile,
|
||||||
|
// ...opts,
|
||||||
|
// })
|
||||||
|
}
|
||||||
103
packages/cli/src/utils/transformers/transform-css-vars.ts
Normal file
103
packages/cli/src/utils/transformers/transform-css-vars.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { SyntaxKind } from 'ts-morph'
|
||||||
|
import type * as z from 'zod'
|
||||||
|
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||||
|
import type { Transformer } from '@/src/utils/transformers'
|
||||||
|
|
||||||
|
export const transformCssVars: Transformer = async ({
|
||||||
|
sourceFile,
|
||||||
|
config,
|
||||||
|
baseColor,
|
||||||
|
}) => {
|
||||||
|
// No transform if using css variables.
|
||||||
|
if (config.tailwind?.cssVariables || !baseColor?.inlineColors)
|
||||||
|
return sourceFile
|
||||||
|
|
||||||
|
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((node) => {
|
||||||
|
const value = node.getText()
|
||||||
|
if (value) {
|
||||||
|
const valueWithColorMapping = applyColorMapping(
|
||||||
|
value.replace(/"/g, ''),
|
||||||
|
baseColor.inlineColors,
|
||||||
|
)
|
||||||
|
node.replaceWithText(`"${valueWithColorMapping.trim()}"`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return sourceFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits a className into variant-name-alpha.
|
||||||
|
// eg. hover:bg-primary-100 -> [hover, bg-primary, 100]
|
||||||
|
export function splitClassName(className: string): (string | null)[] {
|
||||||
|
if (!className.includes('/') && !className.includes(':'))
|
||||||
|
return [null, className, null]
|
||||||
|
|
||||||
|
const parts: (string | null)[] = []
|
||||||
|
// First we split to find the alpha.
|
||||||
|
const [rest, alpha] = className.split('/')
|
||||||
|
|
||||||
|
// Check if rest has a colon.
|
||||||
|
if (!rest.includes(':'))
|
||||||
|
return [null, rest, alpha]
|
||||||
|
|
||||||
|
// Next we split the rest by the colon.
|
||||||
|
const split = rest.split(':')
|
||||||
|
|
||||||
|
// We take the last item from the split as the name.
|
||||||
|
const name = split.pop()
|
||||||
|
|
||||||
|
// We glue back the rest of the split.
|
||||||
|
const variant = split.join(':')
|
||||||
|
|
||||||
|
// Finally we push the variant, name and alpha.
|
||||||
|
parts.push(variant ?? null, name ?? null, alpha ?? null)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIXES = ['bg-', 'text-', 'border-', 'ring-offset-', 'ring-']
|
||||||
|
|
||||||
|
export function applyColorMapping(
|
||||||
|
input: string,
|
||||||
|
mapping: z.infer<typeof registryBaseColorSchema>['inlineColors'],
|
||||||
|
) {
|
||||||
|
// Handle border classes.
|
||||||
|
if (input.includes(' border '))
|
||||||
|
input = input.replace(' border ', ' border border-border ')
|
||||||
|
|
||||||
|
// Build color mappings.
|
||||||
|
const classNames = input.split(' ')
|
||||||
|
const lightMode: string[] = []
|
||||||
|
const darkMode: string[] = []
|
||||||
|
for (const className of classNames) {
|
||||||
|
const [variant, value, modifier] = splitClassName(className)
|
||||||
|
const prefix = PREFIXES.find(prefix => value?.startsWith(prefix))
|
||||||
|
if (!prefix) {
|
||||||
|
if (!lightMode.includes(className))
|
||||||
|
lightMode.push(className)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const needle = value?.replace(prefix, '')
|
||||||
|
if (needle && needle in mapping.light) {
|
||||||
|
lightMode.push(
|
||||||
|
[variant, `${prefix}${mapping.light[needle]}`]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(':') + (modifier ? `/${modifier}` : ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
darkMode.push(
|
||||||
|
['dark', variant, `${prefix}${mapping.dark[needle]}`]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(':') + (modifier ? `/${modifier}` : ''),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lightMode.includes(className))
|
||||||
|
lightMode.push(className)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lightMode.join(' ')} ${darkMode.join(' ').trim()}`
|
||||||
|
}
|
||||||
32
packages/cli/src/utils/transformers/transform-import.ts
Normal file
32
packages/cli/src/utils/transformers/transform-import.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Transformer } from '@/src/utils/transformers'
|
||||||
|
|
||||||
|
export const transformImport: Transformer = async ({ sourceFile, config }) => {
|
||||||
|
const importDeclarations = sourceFile.getImportDeclarations()
|
||||||
|
|
||||||
|
for (const importDeclaration of importDeclarations) {
|
||||||
|
const moduleSpecifier = importDeclaration.getModuleSpecifierValue()
|
||||||
|
|
||||||
|
// Replace @/registry/[style] with the components alias.
|
||||||
|
if (moduleSpecifier.startsWith('@/registry/')) {
|
||||||
|
importDeclaration.setModuleSpecifier(
|
||||||
|
moduleSpecifier.replace(
|
||||||
|
/^@\/registry\/[^/]+/,
|
||||||
|
config.aliases.components,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace `import { cn } from "@/lib/utils"`
|
||||||
|
if (moduleSpecifier == '@/lib/utils') {
|
||||||
|
const namedImports = importDeclaration.getNamedImports()
|
||||||
|
const cnImport = namedImports.find(i => i.getName() === 'cn')
|
||||||
|
if (cnImport) {
|
||||||
|
importDeclaration.setModuleSpecifier(
|
||||||
|
moduleSpecifier.replace(/^@\/lib\/utils/, config.aliases.utils),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceFile
|
||||||
|
}
|
||||||
13
packages/cli/tsconfig.json
Normal file
13
packages/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"display": "Default",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSources": false,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user