Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> CLI for installing Watermelon React Native components

Beautiful, accessible UI components for React Native and Expo applications. Install components with a single command!
Beautiful, accessible UI components for React Native and Expo applications. Install components from one or more shadcn-style registries with a single command.

## Installation

Expand All @@ -18,7 +18,8 @@ watermelon init

# Add components
watermelon add button
watermelon add text
watermelon add @watermelon/button
watermelon add @aceternity/card
```

## Commands
Expand All @@ -27,16 +28,93 @@ watermelon add text

Initialize Watermelon in your React Native/Expo project.

### `watermelon add <component>`
### `watermelon add <component...>`

Add one or more components to your project.

Examples:

```bash
watermelon add button
watermelon add @watermelon/button
watermelon add @aceternity/card --dry-run
watermelon add button --path src
watermelon add button --force
```

Flags:

- `--force` overwrite existing files
- `--path <path>` install into a custom directory
- `--dry-run` preview dependencies and file changes

## Configuration

Watermelon stores project config in `watermelon.json`.

```json
{
"registries": {
"@watermelon": {
"name": "@watermelon",
"homepage": "https://registry.watermelon.dev",
"url": "https://registry.watermelon.dev/{name}.json",
"description": "Official Watermelon component registry."
},
"@aceternity": {
"name": "@aceternity",
"homepage": "https://aceternity.com",
"url": "https://aceternity.com/r/{name}.json",
"description": "Aceternity UI component registry."
}
},
"defaultRegistry": "https://registry.watermelon.dev/{name}.json"
}
```

See [examples/watermelon.json](./examples/watermelon.json) for a complete example.

## Registry Format

Any registry can work if it serves:

- `/<component>.json`
- `/files/<file>`

It can also use a shadcn-style directory entry with a URL template such as:

```json
{
"name": "@8bitcn",
"homepage": "https://www.8bitcn.com",
"url": "https://www.8bitcn.com/r/{name}.json",
"description": "A set of 8-bit styled retro components."
}
```

Example manifest:

```json
{
"name": "button",
"dependencies": ["clsx"],
"files": [
{
"path": "components/ui/button.tsx",
"url": "https://registry.watermelon.dev/files/components/ui/button.tsx"
}
]
}
```

See [examples/button.json](./examples/button.json) for a full example.

## Features

- 🚀 **Fast Installation** - Install components in seconds
- 📦 **Auto Dependencies** - Automatically installs required packages
- 🎨 **Customizable** - Tailwind-based styling with NativeWind
- 📱 **Cross-Platform** - Works on iOS, Android, and Web
- 📦 **Auto Dependencies** - Automatically installs only missing packages
- 🌐 **Multi-Registry** - Resolve scoped components across different registries
- 🧪 **Dry Runs** - Preview file changes before writing anything
- 🔧 **Type-Safe** - Full TypeScript support

## Available Components
Expand Down
150 changes: 74 additions & 76 deletions apps/cli/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ const program = new commander_1.Command();
program
.name('watermelon')
.description('CLI for Watermelon Design System')
.version('0.0.1');
.version('0.0.2');
program
.command('init')
.description('Initialize Watermelon in your project')
.action(async () => {
console.log(chalk_1.default.bold.cyan('🍉 Welcome to Watermelon!\n'));
console.log(chalk_1.default.bold.cyan('Watermelon setup\n'));
const cwd = process.cwd();
const existingConfig = await (0, config_js_1.getConfig)(cwd);
if (existingConfig) {
console.log(chalk_1.default.yellow('⚠️ Watermelon is already initialized in this project.'));
console.log(chalk_1.default.yellow('Watermelon is already initialized in this project.'));
const { overwrite } = await (0, prompts_1.default)({
type: 'confirm',
name: 'overwrite',
Expand Down Expand Up @@ -77,13 +77,9 @@ program
};
const spinner = (0, ora_1.default)('Setting up project...').start();
try {
// Save config
await (0, config_js_1.setConfig)(cwd, config);
// Create directories
await (0, files_js_1.ensureComponentsDirectory)(cwd, config);
// Create utils file
await (0, files_js_1.ensureUtilsFile)(cwd, config);
// Install required dependencies
const requiredDeps = [
'clsx',
'tailwind-merge',
Expand All @@ -95,97 +91,99 @@ program
spinner.text = 'Installing dependencies...';
await (0, dependencies_js_1.installDependencies)(missingDeps, cwd);
}
spinner.succeed(chalk_1.default.green('✓ Watermelon initialized successfully!'));
console.log('\n' + chalk_1.default.bold('Next steps:'));
console.log(chalk_1.default.gray(' 1. Add components: ') + chalk_1.default.cyan('watermelon add button'));
console.log(chalk_1.default.gray(' 2. Import in your app: ') + chalk_1.default.cyan('import { Button } from "@/components/ui/button"'));
console.log('');
spinner.succeed(chalk_1.default.green('Watermelon initialized successfully.'));
console.log(`\n${chalk_1.default.bold('Next steps')}`);
console.log(` ${chalk_1.default.gray('Add a component:')} ${chalk_1.default.cyan('watermelon add button')}`);
console.log(` ${chalk_1.default.gray('Use a namespace:')} ${chalk_1.default.cyan('watermelon add @aceternity/card')}`);
}
catch (error) {
spinner.fail('Failed to initialize Watermelon');
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
console.error(chalk_1.default.red(getErrorMessage(error)));
process.exit(1);
}
});
program
.command('add')
.description('Add components to your project')
.argument('[components...]', 'components to add')
.action(async (components) => {
.description('Add components from one or more registries')
.argument('<components...>', 'components to add')
.option('-f, --force', 'overwrite existing files')
.option('-p, --path <path>', 'custom install directory')
.option('--dry-run', 'preview changes without writing files or installing dependencies')
.action(async (components, options) => {
const cwd = process.cwd();
const config = await (0, config_js_1.getConfig)(cwd);
if (!config) {
console.log(chalk_1.default.red('✗ Watermelon is not initialized in this project.'));
console.log(chalk_1.default.gray(' Run ') + chalk_1.default.cyan('watermelon init') + chalk_1.default.gray(' first.'));
return;
}
if (!components || components.length === 0) {
console.log(chalk_1.default.red('✗ Please specify at least one component.'));
console.log(chalk_1.default.gray(' Example: ') + chalk_1.default.cyan('watermelon add button'));
return;
console.log(chalk_1.default.red('Watermelon is not initialized in this project.'));
console.log(`${chalk_1.default.gray('Run')} ${chalk_1.default.cyan('watermelon init')} ${chalk_1.default.gray('first.')}`);
process.exit(1);
}
const spinner = (0, ora_1.default)('Fetching registry...').start();
const spinner = (0, ora_1.default)('Resolving component manifests...').start();
try {
const registry = await (0, registry_js_1.fetchRegistry)();
spinner.succeed('Registry fetched');
// Validate all components exist
for (const componentName of components) {
if (!registry.components[componentName]) {
console.log(chalk_1.default.red(`✗ Component "${componentName}" not found in registry.`));
console.log(chalk_1.default.gray(' Available components: ') +
Object.keys(registry.components).join(', '));
return;
}
const resolvedComponents = await (0, registry_js_1.collectComponents)(components, config);
spinner.succeed(`Resolved ${resolvedComponents.length} component${resolvedComponents.length === 1 ? '' : 's'}.`);
const dependencyNames = Array.from(new Set(resolvedComponents.flatMap((component) => component.manifest.dependencies ?? []))).sort();
const missingDependencies = await (0, dependencies_js_1.getMissingDependencies)(dependencyNames, cwd);
if (options.dryRun) {
printDryRunSummary(resolvedComponents, missingDependencies, options.path);
}
// Get all dependencies
const allComponents = new Set();
for (const componentName of components) {
const deps = (0, registry_js_1.getAllComponentDependencies)(componentName, registry);
deps.forEach(dep => allComponents.add(dep.name));
else if (missingDependencies.length > 0) {
await (0, dependencies_js_1.installDependencies)(missingDependencies, cwd);
}
console.log(chalk_1.default.bold(`\nComponents to install: `) +
Array.from(allComponents).join(', '));
// Collect all npm dependencies
const allNpmDeps = new Set();
for (const componentName of allComponents) {
const component = registry.components[componentName];
if (component.dependencies) {
component.dependencies.forEach(dep => allNpmDeps.add(dep));
}
const fileSpinner = (0, ora_1.default)(options.dryRun ? 'Planning file changes...' : 'Installing component files...').start();
const plannedFiles = await (0, files_js_1.installFiles)(resolvedComponents, {
cwd,
config,
installPath: options.path,
force: options.force,
dryRun: options.dryRun,
downloadFile: (component, file) => (0, registry_js_1.downloadRegistryFile)(component.specifier.fileBaseUrl, file),
});
fileSpinner.succeed(options.dryRun
? `Planned ${plannedFiles.length} file change${plannedFiles.length === 1 ? '' : 's'}.`
: `Installed ${plannedFiles.length} file${plannedFiles.length === 1 ? '' : 's'}.`);
if (options.dryRun) {
printDryRunFiles(plannedFiles);
return;
}
// Check for missing dependencies
const missingDeps = await (0, dependencies_js_1.getMissingDependencies)(Array.from(allNpmDeps), cwd);
if (missingDeps.length > 0) {
console.log(chalk_1.default.yellow(`\n📦 Installing ${missingDeps.length} dependencies...\n`));
await (0, dependencies_js_1.installDependencies)(missingDeps, cwd);
console.log(`\n${chalk_1.default.bold.green('Installed components')}`);
for (const component of resolvedComponents) {
console.log(` ${chalk_1.default.cyan(component.specifier.raw)}`);
}
// Install components
console.log(chalk_1.default.bold('\n📝 Installing components...\n'));
for (const componentName of allComponents) {
const componentSpinner = (0, ora_1.default)(`Installing ${componentName}...`).start();
const component = registry.components[componentName];
try {
for (const file of component.files) {
// Fetch the component file content
const content = await (0, registry_js_1.fetchComponentFile)(file.path);
// Transform imports to use user's aliases
const transformedContent = (0, files_js_1.transformImports)(content, config);
// Write to disk
await (0, files_js_1.writeComponent)(file.path, transformedContent, cwd);
}
componentSpinner.succeed(chalk_1.default.green(`${componentName} installed`));
}
catch (error) {
componentSpinner.fail(chalk_1.default.red(`Failed to install ${componentName}`));
throw error;
}
if (missingDependencies.length > 0) {
console.log(`\n${chalk_1.default.bold('Installed dependencies')}`);
console.log(` ${missingDependencies.join(', ')}`);
}
console.log(chalk_1.default.bold.green('\n✓ All components installed successfully!\n'));
}
catch (error) {
spinner.fail('Failed to add components');
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
console.error(chalk_1.default.red(getErrorMessage(error)));
process.exit(1);
}
});
program.parse();
function printDryRunSummary(components, missingDependencies, installPath) {
console.log(`\n${chalk_1.default.bold('Dry run')}`);
console.log(` ${chalk_1.default.gray('Components:')} ${components.map((component) => component.specifier.raw).join(', ')}`);
if (installPath) {
console.log(` ${chalk_1.default.gray('Install path:')} ${installPath}`);
}
if (missingDependencies.length > 0) {
console.log(` ${chalk_1.default.gray('Missing dependencies:')} ${missingDependencies.join(', ')}`);
}
else {
console.log(` ${chalk_1.default.gray('Missing dependencies:')} none`);
}
}
function printDryRunFiles(plannedFiles) {
if (plannedFiles.length === 0) {
return;
}
console.log(`\n${chalk_1.default.bold('Files')}`);
for (const file of plannedFiles) {
const label = file.exists ? chalk_1.default.yellow('overwrite') : chalk_1.default.green('create');
console.log(` ${label} ${file.relativePath}`);
}
}
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
Loading
Loading