UNPKG

starwind

Version:

Add beautifully designed components to your Astro applications

668 lines (647 loc) 22.1 kB
// src/commands/init.ts import path from "path"; import * as p3 from "@clack/prompts"; import semver2 from "semver"; // src/templates/starwind.css.ts var tailwindConfig = `@import "tailwindcss"; @import "tw-animate-css"; @plugin "@tailwindcss/forms"; @variant dark (&:where(.dark, .dark *)); @theme { --animate-accordion-down: accordion-down 0.2s ease-out; --animate-accordion-up: accordion-up 0.2s ease-out; @keyframes accordion-down { from { height: 0; } to { height: var(--starwind-accordion-content-height); } } @keyframes accordion-up { from { height: var(--starwind-accordion-content-height); } to { height: 0; } } } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-info: var(--info); --color-info-foreground: var(--info-foreground); --color-success: var(--success); --color-success-foreground: var(--success-foreground); --color-warning: var(--warning); --color-warning-foreground: var(--warning-foreground); --color-error: var(--error); --color-error-foreground: var(--error-foreground); --color-border: var(--border); --color-input: var(--input); --color-outline: var(--outline); --radius-xs: calc(var(--radius) - 0.375rem); --radius-sm: calc(var(--radius) - 0.25rem); --radius-md: calc(var(--radius) - 0.125rem); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 0.25rem); --radius-2xl: calc(var(--radius) + 0.5rem); --radius-3xl: calc(var(--radius) + 1rem); } @layer base { :root { --background: var(--color-neutral-50); --foreground: var(--color-neutral-950); --card: var(--color-neutral-50); --card-foreground: var(--color-neutral-950); --popover: var(--color-neutral-50); --popover-foreground: var(--color-neutral-950); --primary: var(--color-blue-700); --primary-foreground: var(--color-neutral-50); --secondary: var(--color-fuchsia-700); --secondary-foreground: var(--color-neutral-50); --muted: var(--color-neutral-100); --muted-foreground: var(--color-neutral-600); --accent: var(--color-neutral-200); --accent-foreground: var(--color-neutral-900); --info: var(--color-sky-300); --info-foreground: var(--color-sky-950); --success: var(--color-green-300); --success-foreground: var(--color-green-950); --warning: var(--color-amber-300); --warning-foreground: var(--color-amber-950); --error: var(--color-red-700); --error-foreground: var(--color-neutral-50); --border: var(--color-neutral-200); --input: var(--color-neutral-200); --outline: var(--color-blue-600); --radius: 0.5rem; } .dark { --background: var(--color-neutral-950); --foreground: var(--color-neutral-50); --card: var(--color-neutral-950); --card-foreground: var(--color-neutral-50); --popover: var(--color-neutral-950); --popover-foreground: var(--color-neutral-50); --primary: var(--color-blue-700); --primary-foreground: var(--color-neutral-50); --secondary: var(--color-fuchsia-300); --secondary-foreground: var(--color-neutral-950); --muted: var(--color-neutral-900); --muted-foreground: var(--color-neutral-400); --accent: var(--color-neutral-900); --accent-foreground: var(--color-neutral-100); --info: var(--color-sky-300); --info-foreground: var(--color-sky-950); --success: var(--color-green-300); --success-foreground: var(--color-green-950); --warning: var(--color-amber-300); --warning-foreground: var(--color-amber-950); --error: var(--color-red-800); --error-foreground: var(--color-neutral-50); --border: var(--color-neutral-800); --input: var(--color-neutral-800); --outline: var(--color-blue-600); --radius: 0.5rem; } * { @apply border-border; } *:focus-visible { @apply outline-outline; } html { @apply bg-background text-foreground scheme-light dark:scheme-dark; } button { @apply cursor-pointer; } } @layer utilities { /* transition-colors but without outline-color transition property */ .starwind-transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-timing-function: var(--default-transition-timing-function); transition-duration: var(--default-transition-duration); } } `; // src/utils/astro-config.ts import * as p from "@clack/prompts"; import fs2 from "fs-extra"; import semver from "semver"; // src/utils/fs.ts import fs from "fs-extra"; async function ensureDirectory(dir) { await fs.ensureDir(dir); } async function readJsonFile(filePath) { return fs.readJson(filePath); } async function writeJsonFile(filePath, data) { await fs.writeJson(filePath, data, { spaces: 2 }); } async function fileExists(filePath) { return fs.pathExists(filePath); } async function writeCssFile(filePath, content) { await fs.writeFile(filePath, content, "utf-8"); } // src/utils/highlighter.ts import chalk from "chalk"; var highlighter = { error: chalk.red, warn: chalk.yellow, info: chalk.cyan, infoBright: chalk.cyanBright, success: chalk.greenBright, underline: chalk.underline, title: chalk.bgBlue }; // src/utils/astro-config.ts var CONFIG_EXTENSIONS = ["ts", "js", "mjs", "cjs"]; async function findAstroConfig() { for (const ext of CONFIG_EXTENSIONS) { const configPath = `astro.config.${ext}`; if (await fileExists(configPath)) { return configPath; } } return null; } async function getAstroVersion() { try { const pkg = await readJsonFile("package.json"); if (pkg.dependencies?.astro) { const astroVersion = pkg.dependencies.astro.replace(/^\^|~/, ""); return astroVersion; } p.log.error( highlighter.error( "Astro seems not installed in your project, please check your package.json" ) ); return null; } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; p.log.error(highlighter.error(`Failed to check Astro version: ${errorMessage}`)); return null; } } async function setupAstroConfig() { try { let configPath = await findAstroConfig(); let content = ""; if (configPath) { content = await fs2.readFile(configPath, "utf-8"); } else { configPath = "astro.config.ts"; content = `import { defineConfig } from "astro/config"; export default defineConfig({}); `; } if (!content.includes('import tailwindcss from "@tailwindcss/vite"')) { content = `import tailwindcss from "@tailwindcss/vite"; ${content}`; } const configStart = content.indexOf("defineConfig(") + "defineConfig(".length; const configEnd = content.lastIndexOf(");"); let config = content.slice(configStart, configEnd); config = config.trim().replace(/^{|}$/g, "").trim(); const astroVersion = await getAstroVersion(); if (astroVersion && semver.lt(astroVersion, "5.7.0")) { if (!config.includes("experimental")) { config += ` experimental: { svg: true, },`; } else if (!config.includes("svg: true") && !config.includes("svg: {")) { const expEnd = config.indexOf("experimental:") + "experimental:".length; const blockStart = config.indexOf("{", expEnd) + 1; config = config.slice(0, blockStart) + ` svg: true,` + config.slice(blockStart); } } if (!config.includes("vite:")) { config += ` vite: { plugins: [tailwindcss()], },`; } else if (!config.includes("plugins: [")) { const viteEnd = config.indexOf("vite:") + "vite:".length; const blockStart = config.indexOf("{", viteEnd) + 1; config = config.slice(0, blockStart) + ` plugins: [tailwindcss()],` + config.slice(blockStart); } else if (!config.includes("tailwindcss()")) { const pluginsStart = config.indexOf("plugins:") + "plugins:".length; const arrayStart = config.indexOf("[", pluginsStart) + 1; config = config.slice(0, arrayStart) + `tailwindcss(), ` + config.slice(arrayStart); } const newContent = `${content.slice(0, configStart)}{ ${config} }${content.slice(configEnd)}`; await fs2.writeFile(configPath, newContent, "utf-8"); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; p.log.error(highlighter.error(`Failed to setup Astro config: ${errorMessage}`)); return false; } } // src/utils/constants.ts var MIN_ASTRO_VERSION = "5.0.0"; var PATHS = { STARWIND_CORE: "@starwind-ui/core", STARWIND_CORE_COMPONENTS: "src/components", STARWIND_REMOTE_COMPONENT_REGISTRY: "https://starwind.dev/registry.json", LOCAL_CSS_FILE: "src/styles/starwind.css", LOCAL_CONFIG_FILE: "starwind.config.json", LOCAL_STYLES_DIR: "src/styles", LOCAL_COMPONENTS_DIR: "src/components" }; var ASTRO_PACKAGES = { core: "astro@latest" }; var OTHER_PACKAGES = { tailwindCore: "tailwindcss@^4", tailwindVite: "@tailwindcss/vite@^4", tailwindForms: "@tailwindcss/forms@^0.5", tailwindAnimate: "tw-animate-css@^1", tailwindVariants: "tailwind-variants@^2", tailwindMerge: "tailwind-merge@^3", tablerIcons: "@tabler/icons@^3" }; function getOtherPackages() { return Object.values(OTHER_PACKAGES); } // src/utils/config.ts var defaultConfig = { $schema: "https://starwind.dev/config-schema.json", tailwind: { css: "src/styles/starwind.css", baseColor: "neutral", cssVariables: true }, // aliases: { // components: "@/components", // }, componentDir: "src/components/starwind", components: [] }; async function getConfig() { try { if (await fileExists(PATHS.LOCAL_CONFIG_FILE)) { const config = await readJsonFile(PATHS.LOCAL_CONFIG_FILE); return { ...defaultConfig, ...config, components: Array.isArray(config.components) ? config.components : [] }; } } catch (error) { console.error("Error reading config:", error); } return defaultConfig; } async function updateConfig(updates, options = { appendComponents: true }) { const currentConfig = await getConfig(); const currentComponents = Array.isArray(currentConfig.components) ? currentConfig.components : []; const newConfig = { ...currentConfig, tailwind: { ...currentConfig.tailwind, ...updates.tailwind || {} }, componentDir: updates.componentDir ? updates.componentDir : currentConfig.componentDir, components: updates.components ? options.appendComponents ? [...currentComponents, ...updates.components] : updates.components : currentComponents }; try { await writeJsonFile(PATHS.LOCAL_CONFIG_FILE, newConfig); } catch (error) { throw new Error( `Failed to update config: ${error instanceof Error ? error.message : "Unknown error"}` ); } } // src/utils/package-manager.ts import * as p2 from "@clack/prompts"; import { execa } from "execa"; async function requestPackageManager() { const pm = await p2.select({ message: "Select your preferred package manager", options: [ { value: "pnpm", label: "pnpm", hint: "Default" }, { value: "npm", label: "npm" }, { value: "yarn", label: "yarn" }, { value: "bun", label: "bun" } ] }); if (p2.isCancel(pm)) { p2.log.warn("No package manager selected, defaulting to npm"); return "npm"; } return pm; } async function getDefaultPackageManager() { if (await fileExists("yarn.lock")) { return "yarn"; } else if (await fileExists("pnpm-lock.yaml")) { return "pnpm"; } else { return "npm"; } } async function installDependencies(packages, pm, dev = false, force = false) { const args = [ pm === "npm" ? "install" : "add", ...packages, dev ? pm === "npm" || pm === "pnpm" ? "-D" : "--dev" : "", force ? "--force" : "" ].filter(Boolean); await execa(pm, args); } // src/utils/sleep.ts var sleep = async (ms) => { await new Promise((resolve) => setTimeout(resolve, ms)); }; // src/commands/init.ts async function init(withinAdd = false, options) { if (!withinAdd) { p3.intro(highlighter.title(" Welcome to the Starwind CLI ")); } try { if (!await fileExists("package.json")) { throw new Error( "No package.json found. Please run this command in the root of your project." ); } const pkg = await readJsonFile("package.json"); const installTasks = []; const configTasks = []; let configChoices; if (options?.defaults) { configChoices = { installLocation: PATHS.LOCAL_COMPONENTS_DIR, cssFile: PATHS.LOCAL_CSS_FILE, twBaseColor: "neutral" }; if (!withinAdd) { p3.log.info("Using default configuration values"); } } else { configChoices = await p3.group( { // ask where to install components installLocation: () => p3.text({ message: "What is your components directory?", placeholder: PATHS.LOCAL_COMPONENTS_DIR, initialValue: PATHS.LOCAL_COMPONENTS_DIR, validate(value) { if (value.length === 0) return `Value is required!`; if (path.isAbsolute(value)) return `Please use a relative path`; if (value.includes("..")) return `Path traversal is not allowed`; const invalidChars = /[<>:"|?*]/; if (invalidChars.test(value)) return `Path contains invalid characters`; const systemDirs = ["windows", "program files", "system32"]; if (systemDirs.some((dir) => value.toLowerCase().startsWith(dir))) { return `Cannot install in system directories`; } } }), // ask where to add the css file cssFile: () => p3.text({ message: `Where would you like to add the Tailwind ${highlighter.info(".css")} file?`, placeholder: PATHS.LOCAL_CSS_FILE, initialValue: PATHS.LOCAL_CSS_FILE, validate(value) { if (value.length === 0) return `Value is required!`; if (!value.endsWith(".css")) return `File must end with .css extension`; if (path.isAbsolute(value)) return `Please use a relative path`; if (value.includes("..")) return `Path traversal is not allowed`; const invalidChars = /[<>:"|?*]/; if (invalidChars.test(value)) return `Path contains invalid characters`; const systemDirs = ["windows", "program files", "system32"]; if (systemDirs.some((dir) => value.toLowerCase().startsWith(dir))) { return `Cannot use system directories`; } const basename = path.basename(value, ".css"); if (!basename || basename.trim().length === 0) { return `Invalid filename`; } } }), twBaseColor: () => p3.select({ message: "What Tailwind base color would you like to use?", initialValue: "neutral", options: [ { label: "Neutral (default)", value: "neutral" }, { label: "Stone", value: "stone" }, { label: "Zinc", value: "zinc" }, { label: "Gray", value: "gray" }, { label: "Slate", value: "slate" } ] }) }, { // On Cancel callback that wraps the group // So if the user cancels one of the prompts in the group this function will be called onCancel: () => { p3.cancel("Operation cancelled."); process.exit(0); } } ); } const cssFileDir = path.dirname(configChoices.cssFile); const componentInstallDir = path.join(configChoices.installLocation, "starwind"); configTasks.push({ title: "Creating project structure", task: async () => { await ensureDirectory(componentInstallDir); await ensureDirectory(cssFileDir); await sleep(250); return "Created project structure"; } }); configTasks.push({ title: "Setup Astro config file", task: async () => { const success = await setupAstroConfig(); if (!success) { throw new Error("Failed to setup Astro config"); } await sleep(250); return "Astro config setup completed"; } }); const cssFileExists = await fileExists(configChoices.cssFile); let updatedTailwindConfig = tailwindConfig; if (configChoices.twBaseColor !== "neutral") { updatedTailwindConfig = updatedTailwindConfig.replace( /--color-neutral-/g, `--color-${configChoices.twBaseColor}-` ); } if (cssFileExists) { const shouldOverride = options?.defaults ? true : await p3.confirm({ message: `${highlighter.info(configChoices.cssFile)} already exists. Do you want to override it?` }); if (p3.isCancel(shouldOverride)) { p3.cancel("Operation cancelled"); return process.exit(0); } if (!shouldOverride) { p3.log.info("Skipping Tailwind CSS configuration"); } else { configTasks.push({ title: "Creating Tailwind CSS configuration", task: async () => { await writeCssFile(configChoices.cssFile, updatedTailwindConfig); await sleep(250); return "Created Tailwind configuration"; } }); } } else { configTasks.push({ title: "Creating Tailwind CSS configuration", task: async () => { await writeCssFile(configChoices.cssFile, updatedTailwindConfig); await sleep(250); return "Created Tailwind configuration"; } }); } configTasks.push({ title: "Updating project configuration", task: async () => { await updateConfig({ tailwind: { css: configChoices.cssFile, baseColor: configChoices.twBaseColor, cssVariables: true }, // aliases: { // components: "@/components", // }, componentDir: configChoices.installLocation, components: [] }); await sleep(250); return "Updated project starwind configuration"; } }); const pm = options?.defaults ? await getDefaultPackageManager() : await requestPackageManager(); if (pkg.dependencies?.astro) { const astroVersion = pkg.dependencies.astro.replace(/^\^|~/, ""); if (!semver2.gte(astroVersion, MIN_ASTRO_VERSION)) { const shouldUpgrade = options?.defaults ? true : await p3.confirm({ message: `Starwind requires Astro v${MIN_ASTRO_VERSION} or higher. Would you like to upgrade from v${astroVersion}?`, initialValue: true }); if (p3.isCancel(shouldUpgrade)) { p3.cancel("Operation cancelled"); return process.exit(0); } if (!shouldUpgrade) { p3.cancel("Astro v5 or higher is required to use Starwind"); return process.exit(1); } installTasks.push({ title: "Upgrading Astro", task: async () => { await installDependencies([ASTRO_PACKAGES.core], pm); return "Upgraded Astro successfully"; } }); } } else { const shouldInstall2 = options?.defaults ? true : await p3.confirm({ message: `Starwind requires Astro v${MIN_ASTRO_VERSION} or higher. Would you like to install it?`, initialValue: true }); if (p3.isCancel(shouldInstall2)) { p3.cancel("Operation cancelled"); return process.exit(0); } if (!shouldInstall2) { p3.cancel("Astro is required to use Starwind"); return process.exit(1); } installTasks.push({ title: `Installing ${ASTRO_PACKAGES.core}`, task: async () => { await installDependencies([ASTRO_PACKAGES.core], pm); return `Installed ${highlighter.info(ASTRO_PACKAGES.core)} successfully`; } }); } const otherPackages = getOtherPackages(); const shouldInstall = options?.defaults ? true : await p3.confirm({ message: `Install ${highlighter.info(otherPackages.join(", "))} using ${highlighter.info(pm)}?` }); if (p3.isCancel(shouldInstall)) { p3.cancel("Operation cancelled"); return process.exit(0); } if (shouldInstall) { installTasks.push({ title: `Installing packages`, task: async () => { await installDependencies(getOtherPackages(), pm, false, false); return `${highlighter.info("Packages installed successfully")}`; } }); } else { p3.log.warn( highlighter.warn(`Skipped installation of packages. Make sure to install them manually`) ); } if (installTasks.length > 0) { await p3.tasks(installTasks); } if (configTasks.length > 0) { await p3.tasks(configTasks); } await sleep(250); p3.note( `Make sure your layout imports the ${highlighter.infoBright(configChoices.cssFile)} file`, "Next steps" ); if (!withinAdd) { sleep(1e3); p3.outro("Enjoy using Starwind UI \u{1F680}"); } } catch (error) { p3.log.error(error instanceof Error ? error.message : "Failed to add components"); p3.cancel("Operation cancelled"); process.exit(1); } } export { PATHS, fileExists, getConfig, updateConfig, highlighter, requestPackageManager, installDependencies, sleep, init }; //# sourceMappingURL=chunk-HDAZQTOL.js.map