create-willow-design-system
Version:
Initialize a new project with Willow Design System
543 lines (480 loc) ⢠16 kB
JavaScript
#!/usr/bin/env node
import { execSync } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora from 'ora';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const WILLOW_REGISTRY_URL = 'https://iridescent-brigadeiros-fe4174.netlify.app/r';
const FALLBACK_REGISTRY_URL = 'https://iridescent-brigadeiros-fe4174.netlify.app/api/registry/ui';
async function init() {
console.log(chalk.blue.bold('\nš³ Welcome to Willow Design System\n'));
// Get project details
const answers = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: 'What is your project name?',
default: 'my-willow-app',
},
{
type: 'list',
name: 'framework',
message: 'Which framework are you using?',
choices: ['Next.js', 'Vite', 'Remix'],
default: 'Next.js',
},
{
type: 'confirm',
name: 'typescript',
message: 'Would you like to use TypeScript?',
default: true,
},
{
type: 'confirm',
name: 'installExamples',
message: 'Would you like to install example components?',
default: true,
}
]);
const spinner = ora('Setting up your project...').start();
try {
// Create project directory
await fs.mkdir(answers.projectName, { recursive: true });
process.chdir(answers.projectName);
// Initialize project based on framework
spinner.text = 'Creating project structure...';
if (answers.framework === 'Next.js') {
// Create package.json first
const packageJson = {
name: answers.projectName,
version: "0.1.0",
private: true,
scripts: {
dev: "next dev",
build: "next build",
start: "next start",
lint: "next lint"
}
};
await fs.writeFile('package.json', JSON.stringify(packageJson, null, 2));
// Create Next.js project structure
await fs.mkdir('app', { recursive: true });
await fs.mkdir('public', { recursive: true });
await fs.mkdir('components', { recursive: true });
// Create app layout
const layoutContent = `import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}`;
await fs.writeFile('app/layout.tsx', layoutContent);
// Create app page
const pageContent = `export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold">Welcome to Willow Design System</h1>
</main>
)
}`;
await fs.writeFile('app/page.tsx', pageContent);
// Create next.config.js
const nextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig`;
await fs.writeFile('next.config.js', nextConfig);
// Create tsconfig.json
const tsConfig = {
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
};
await fs.writeFile('tsconfig.json', JSON.stringify(tsConfig, null, 2));
// Create globals.css
await fs.writeFile('app/globals.css', '/* Temporary file - will be replaced */');
// Create postcss.config.js
const postcssConfig = `module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}`;
await fs.writeFile('postcss.config.js', postcssConfig);
// Create .eslintrc.json
const eslintConfig = {
extends: "next/core-web-vitals"
};
await fs.writeFile('.eslintrc.json', JSON.stringify(eslintConfig, null, 2));
// Create .gitignore
const gitignore = `# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts`;
await fs.writeFile('.gitignore', gitignore);
} else if (answers.framework === 'Vite') {
execSync(`npm create vite@latest . -- --template ${answers.typescript ? 'react-ts' : 'react'}`, {
stdio: 'pipe'
});
}
// Install dependencies based on framework
if (answers.framework === 'Next.js') {
spinner.text = 'Installing Next.js dependencies...';
// Install core Next.js dependencies
execSync('npm install next@latest react@latest react-dom@latest', { stdio: 'pipe' });
execSync('npm install -D typescript @types/react @types/react-dom @types/node eslint eslint-config-next tailwindcss postcss autoprefixer', { stdio: 'pipe' });
} else if (answers.framework !== 'Next.js') {
spinner.text = 'Installing Tailwind CSS...';
execSync('npm install -D tailwindcss postcss autoprefixer', { stdio: 'pipe' });
execSync('npx tailwindcss init -p', { stdio: 'pipe' });
}
// Configure Willow Design System
spinner.text = 'Configuring Willow Design System...';
// Create components.json
const componentsConfig = {
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": answers.framework === 'Next.js',
"tsx": answers.typescript,
"tailwind": {
"config": "tailwind.config.js",
"css": answers.framework === 'Next.js' ? "app/globals.css" : "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"willow": {
"url": "https://iridescent-brigadeiros-fe4174.netlify.app/r"
}
}
};
await fs.writeFile('components.json', JSON.stringify(componentsConfig, null, 2));
// Update tailwind.config.js with Willow theme
spinner.text = 'Configuring Tailwind with Willow theme...';
const tailwindConfig = `import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// Willow Primary Colors
'willow-primary': {
50: '#f0f5ff',
100: '#e5edff',
200: '#cddbfe',
300: '#b4c6fc',
400: '#8da2fb',
500: '#6b7ff8',
600: '#4c5ff6',
700: '#254df2',
800: '#2040d8',
900: '#1939bc',
950: '#0f2481',
},
// Success Colors
'success': {
DEFAULT: '#1fc16b',
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#1fc16b',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
// Warning Colors
'warning': {
DEFAULT: '#ff8447',
50: '#fff7ed',
100: '#ffedd5',
200: '#fed7aa',
300: '#fdba74',
400: '#fb923c',
500: '#ff8447',
600: '#ea580c',
700: '#c2410c',
800: '#9a3412',
900: '#7c2d12',
},
// Destructive Colors
'destructive': {
DEFAULT: '#ef4444',
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
// Info Colors
'info': {
DEFAULT: '#7666ff',
50: '#f5f3ff',
100: '#ede9fe',
200: '#ddd6fe',
300: '#c4b5fd',
400: '#a78bfa',
500: '#8b5cf6',
600: '#7666ff',
700: '#6d28d9',
800: '#5b21b6',
900: '#4c1d95',
},
// State Colors
'state-success-lighter': '#e8fbef',
'state-warning-lighter': '#fffaeb',
},
boxShadow: {
'button-primary': '0px 4px 4px 0px rgba(35, 40, 102, 0.06), 0px 1px 3px 0px rgba(0, 0, 0, 0.1)',
'button-primary-hover': '0px 6px 8px 0px rgba(35, 40, 102, 0.1), 0px 2px 4px 0px rgba(0, 0, 0, 0.12)',
'button-primary-active': '0px 2px 3px 0px rgba(35, 40, 102, 0.05), 0px 1px 2px 0px rgba(0, 0, 0, 0.08)',
'button-secondary': '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)',
'button-secondary-hover': '0px 10px 15px -3px rgba(0, 0, 0, 0.15), 0px 4px 6px -2px rgba(0, 0, 0, 0.08)',
'button-secondary-active': '0px 2px 4px -1px rgba(0, 0, 0, 0.08), 0px 1px 2px -1px rgba(0, 0, 0, 0.04)',
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;`;
await fs.writeFile('tailwind.config.js', tailwindConfig);
// Install required Willow dependencies
spinner.text = 'Installing Willow dependencies...';
const willowDeps = [
'clsx',
'tailwind-merge',
'class-variance-authority',
'lucide-react',
'@radix-ui/react-slot',
'tailwindcss-animate'
];
try {
execSync(`npm install ${willowDeps.join(' ')} --save-exact`, {
stdio: 'pipe'
});
} catch (error) {
// If that fails, install one by one
for (const dep of willowDeps) {
try {
execSync(`npm install ${dep} --save-exact`, { stdio: 'pipe' });
} catch (e) {
console.warn(chalk.yellow(`Warning: Could not install ${dep}, you may need to install it manually`));
}
}
}
// Install shadcn CLI
execSync('npm install -D shadcn@latest', { stdio: 'pipe' });
// Create utils
spinner.text = 'Setting up utilities...';
await fs.mkdir('lib', { recursive: true });
const utilsContent = `import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}`;
await fs.writeFile('lib/utils.ts', utilsContent);
// For TypeScript projects, create utils.js as well for compatibility
if (answers.typescript) {
const utilsJs = `import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}`;
await fs.writeFile('lib/utils.js', utilsJs);
}
// Install example components if requested
if (answers.installExamples) {
spinner.text = 'Installing Willow components...';
// First, let's download the components directly from the registry
const components = ['button', 'card', 'badge', 'input', 'select'];
await fs.mkdir('components/ui', { recursive: true });
for (const component of components) {
try {
// Use Willow CLI for simpler component installation
execSync(`npx willow-cli add ${component} --yes`, { stdio: 'pipe' });
spinner.text = `Installed ${component} component via Willow CLI`;
} catch (error) {
console.warn(chalk.yellow(`\nWarning: Could not install ${component}: ${error.message}`));
}
}
}
// Add Willow fonts to CSS
spinner.text = 'Adding Willow fonts...';
const cssContent = `@tailwind base;
@tailwind components;
@tailwind utilities;
/* Willow Fonts - with multiple format support */
@font-face {
font-family: 'Codec Pro';
src: url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Regular.woff2') format('woff2'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Regular.woff') format('woff'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Regular.otf') format('opentype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Codec Pro';
src: url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Bold.woff2') format('woff2'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Bold.woff') format('woff'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Bold.otf') format('opentype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Codec Pro';
src: url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Light.woff2') format('woff2'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Light.woff') format('woff'),
url('https://iridescent-brigadeiros-fe4174.netlify.app/cdn/fonts/Codec-Pro-Light.otf') format('opentype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 225 73% 57%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 225 73% 57%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 225 73% 57%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 225 73% 57%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: 'Codec Pro', system-ui, -apple-system, sans-serif;
}
}`;
const cssPath = answers.framework === 'Next.js' ? 'app/globals.css' : 'src/index.css';
await fs.writeFile(cssPath, cssContent);
spinner.succeed(chalk.green('ā
Willow Design System initialized successfully!'));
console.log(`
${chalk.blue.bold('š Your Willow project is ready!')}
${chalk.yellow('Next steps:')}
${chalk.gray('cd')} ${answers.projectName}
${chalk.gray('npm run dev')}
${chalk.yellow('Install more components:')}
${chalk.gray('willow add accordion')}
${chalk.gray('willow add tabs')}
${chalk.gray('willow add modal')}
${chalk.yellow('Documentation:')}
${chalk.gray('https://iridescent-brigadeiros-fe4174.netlify.app/docs')}
`);
} catch (error) {
spinner.fail('Error setting up project');
console.error(chalk.red(error.message));
process.exit(1);
}
}
init().catch(console.error);