mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add ultracite, oxlint, fumadocs addons (#427)
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user