feat(cli): add ultracite, oxlint, fumadocs addons (#427)

This commit is contained in:
Aman Varshney
2025-07-29 00:13:51 +05:30
committed by GitHub
parent 82a4f42eca
commit 216c242f7d
66 changed files with 794 additions and 251 deletions

View File

@@ -1,4 +1,4 @@
import { cancel, isCancel, multiselect } from "@clack/prompts";
import { cancel, groupMultiselect, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import { type Addons, AddonsSchema, type Frontend } from "../types";
@@ -13,45 +13,61 @@ type AddonOption = {
hint: string;
};
function getAddonDisplay(
addon: Addons,
isRecommended = false,
): { label: string; hint: string } {
function getAddonDisplay(addon: Addons): { label: string; hint: string } {
let label: string;
let hint: string;
if (addon === "turborepo") {
label = isRecommended ? "Turborepo (Recommended)" : "Turborepo";
hint = "High-performance build system for JavaScript and TypeScript";
} else if (addon === "pwa") {
label = "PWA (Progressive Web App)";
hint = "Make your app installable and work offline";
} else if (addon === "tauri") {
label = isRecommended ? "Tauri Desktop App" : "Tauri";
hint = "Build native desktop apps from your web frontend";
} else if (addon === "biome") {
label = "Biome";
hint = isRecommended
? "Add Biome for linting and formatting"
: "Fast formatter and linter for JavaScript, TypeScript, JSX";
} else if (addon === "husky") {
label = "Husky";
hint = isRecommended
? "Add Git hooks with Husky, lint-staged (requires Biome)"
: "Git hooks made easy";
} else if (addon === "starlight") {
label = "Starlight";
hint = isRecommended
? "Add Astro Starlight documentation site"
: "Documentation site with Astro";
} else {
label = addon;
hint = `Add ${addon}`;
switch (addon) {
case "turborepo":
label = "Turborepo";
hint = "High-performance build system";
break;
case "pwa":
label = "PWA (Progressive Web App)";
hint = "Make your app installable and work offline";
break;
case "tauri":
label = "Tauri";
hint = "Build native desktop apps from your web frontend";
break;
case "biome":
label = "Biome";
hint = "Format, lint, and more";
break;
case "oxlint":
label = "Oxlint";
hint = "Rust-powered linter";
break;
case "ultracite":
label = "Ultracite";
hint = "Zero-config Biome preset with AI integration";
break;
case "husky":
label = "Husky";
hint = "Modern native Git hooks made easy";
break;
case "starlight":
label = "Starlight";
hint = "Build stellar docs with astro";
break;
case "fumadocs":
label = "Fumadocs";
hint = "Build excellent documentation site";
break;
default:
label = addon;
hint = `Add ${addon}`;
}
return { label, hint };
}
const ADDON_GROUPS = {
Documentation: ["starlight", "fumadocs"],
Linting: ["biome", "oxlint", "ultracite"],
Other: ["turborepo", "pwa", "tauri", "husky"],
};
export async function getAddonsChoice(
addons?: Addons[],
frontends?: Frontend[],
@@ -59,38 +75,48 @@ export async function getAddonsChoice(
if (addons !== undefined) return addons;
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
const groupedOptions: Record<string, AddonOption[]> = {
Documentation: [],
Linting: [],
Other: [],
};
const allPossibleOptions: AddonOption[] = [];
const frontendsArray = frontends || [];
for (const addon of allAddons) {
const { isCompatible } = validateAddonCompatibility(addon, frontends || []);
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
if (!isCompatible) continue;
if (isCompatible) {
const { label, hint } = getAddonDisplay(addon, true);
const { label, hint } = getAddonDisplay(addon);
const option = { value: addon, label, hint };
allPossibleOptions.push({
value: addon,
label,
hint,
});
if (ADDON_GROUPS.Documentation.includes(addon)) {
groupedOptions.Documentation.push(option);
} else if (ADDON_GROUPS.Linting.includes(addon)) {
groupedOptions.Linting.push(option);
} else if (ADDON_GROUPS.Other.includes(addon)) {
groupedOptions.Other.push(option);
}
}
const options = allPossibleOptions.sort((a, b) => {
if (a.value === "turborepo") return -1;
if (b.value === "turborepo") return 1;
return 0;
Object.keys(groupedOptions).forEach((group) => {
if (groupedOptions[group].length === 0) {
delete groupedOptions[group];
}
});
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) =>
options.some((opt) => opt.value === addonValue),
Object.values(groupedOptions).some((options) =>
options.some((opt) => opt.value === addonValue),
),
);
const response = await multiselect({
const response = await groupMultiselect<Addons>({
message: "Select addons",
options: options,
options: groupedOptions,
initialValues: initialValues,
required: false,
selectableGroups: false,
});
if (isCancel(response)) {
@@ -98,10 +124,6 @@ export async function getAddonsChoice(
process.exit(0);
}
if (response.includes("husky") && !response.includes("biome")) {
response.push("biome");
}
return response;
}
@@ -109,34 +131,48 @@ export async function getAddonsToAdd(
frontend: Frontend[],
existingAddons: Addons[] = [],
): Promise<Addons[]> {
const options: AddonOption[] = [];
const groupedOptions: Record<string, AddonOption[]> = {
Documentation: [],
Linting: [],
Other: [],
};
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
const frontendArray = frontend || [];
const compatibleAddons = getCompatibleAddons(
allAddons,
frontend,
AddonsSchema.options.filter((addon) => addon !== "none"),
frontendArray,
existingAddons,
);
for (const addon of compatibleAddons) {
const { label, hint } = getAddonDisplay(addon, false);
const { label, hint } = getAddonDisplay(addon);
const option = { value: addon, label, hint };
options.push({
value: addon,
label,
hint,
});
if (ADDON_GROUPS.Documentation.includes(addon)) {
groupedOptions.Documentation.push(option);
} else if (ADDON_GROUPS.Linting.includes(addon)) {
groupedOptions.Linting.push(option);
} else if (ADDON_GROUPS.Other.includes(addon)) {
groupedOptions.Other.push(option);
}
}
if (options.length === 0) {
Object.keys(groupedOptions).forEach((group) => {
if (groupedOptions[group].length === 0) {
delete groupedOptions[group];
}
});
if (Object.keys(groupedOptions).length === 0) {
return [];
}
const response = await multiselect<Addons>({
message: "Select addons",
options: options,
const response = await groupMultiselect<Addons>({
message: "Select addons to add",
options: groupedOptions,
required: false,
selectableGroups: false,
});
if (isCancel(response)) {

View File

@@ -10,7 +10,7 @@ export async function getFrontendChoice(
if (frontendOptions !== undefined) return frontendOptions;
const frontendTypes = await multiselect({
message: "Select platforms to develop for",
message: "Select project type",
options: [
{
value: "web",