starwind
Version:
Add beautifully designed components to your Astro applications
668 lines (647 loc) • 22.1 kB
JavaScript
// 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