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:
9
.changeset/sad-months-drop.md
Normal file
9
.changeset/sad-months-drop.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added addons: fumadocs, ultracite, oxlint
|
||||||
|
|
||||||
|
Added bunfig.toml with isolated linker
|
||||||
|
|
||||||
|
Grouped addon prompts
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { Frontend, ProjectConfig } from "./types";
|
import type { Addons, Frontend, ProjectConfig } from "./types";
|
||||||
import { getUserPkgManager } from "./utils/get-package-manager";
|
import { getUserPkgManager } from "./utils/get-package-manager";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -55,10 +55,12 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
"@tauri-apps/cli": "^2.4.0",
|
"@tauri-apps/cli": "^2.4.0",
|
||||||
|
|
||||||
"@biomejs/biome": "^2.0.0",
|
"@biomejs/biome": "^2.1.2",
|
||||||
|
oxlint: "^1.8.0",
|
||||||
|
ultracite: "5.1.1",
|
||||||
|
|
||||||
husky: "^9.1.7",
|
husky: "^9.1.7",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^16.1.2",
|
||||||
|
|
||||||
tsx: "^4.19.2",
|
tsx: "^4.19.2",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.11",
|
||||||
@@ -119,13 +121,16 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||||
|
|
||||||
export const ADDON_COMPATIBILITY = {
|
export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
|
||||||
pwa: ["tanstack-router", "react-router", "solid", "next"],
|
pwa: ["tanstack-router", "react-router", "solid", "next"],
|
||||||
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
|
tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"],
|
||||||
biome: [],
|
biome: [],
|
||||||
husky: [],
|
husky: [],
|
||||||
turborepo: [],
|
turborepo: [],
|
||||||
starlight: [],
|
starlight: [],
|
||||||
|
ultracite: [],
|
||||||
|
oxlint: [],
|
||||||
|
fumadocs: [],
|
||||||
none: [],
|
none: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type EnvVariable,
|
type EnvVariable,
|
||||||
} from "../project-generation/env-setup";
|
} from "../project-generation/env-setup";
|
||||||
|
|
||||||
export async function setupCloudflareD1(config: ProjectConfig): Promise<void> {
|
export async function setupCloudflareD1(config: ProjectConfig) {
|
||||||
const { projectDir } = config;
|
const { projectDir } = config;
|
||||||
|
|
||||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type EnvVariable,
|
type EnvVariable,
|
||||||
} from "../project-generation/env-setup";
|
} from "../project-generation/env-setup";
|
||||||
|
|
||||||
export async function setupDockerCompose(config: ProjectConfig): Promise<void> {
|
export async function setupDockerCompose(config: ProjectConfig) {
|
||||||
const { database, projectDir, projectName } = config;
|
const { database, projectDir, projectName } = config;
|
||||||
|
|
||||||
if (database === "none" || database === "sqlite") {
|
if (database === "none" || database === "sqlite") {
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function displayManualSetupInstructions() {
|
|||||||
DATABASE_URL="your_connection_string"`);
|
DATABASE_URL="your_connection_string"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
|
export async function setupNeonPostgres(config: ProjectConfig) {
|
||||||
const { packageManager, projectDir } = config;
|
const { packageManager, projectDir } = config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ DATABASE_URL=your_database_url
|
|||||||
DATABASE_AUTH_TOKEN=your_auth_token`);
|
DATABASE_AUTH_TOKEN=your_auth_token`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
export async function setupTurso(config: ProjectConfig) {
|
||||||
const { orm, projectDir } = config;
|
const { orm, projectDir } = config;
|
||||||
const _isDrizzle = orm === "drizzle";
|
const _isDrizzle = orm === "drizzle";
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function exitWithError(message: string): never {
|
|||||||
|
|
||||||
export async function addAddonsToProject(
|
export async function addAddonsToProject(
|
||||||
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
|
input: AddInput & { addons: Addons[]; suppressInstallMessage?: boolean },
|
||||||
): Promise<void> {
|
) {
|
||||||
try {
|
try {
|
||||||
const projectDir = input.projectDir || process.cwd();
|
const projectDir = input.projectDir || process.cwd();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function exitWithError(message: string): never {
|
|||||||
|
|
||||||
export async function addDeploymentToProject(
|
export async function addDeploymentToProject(
|
||||||
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
|
input: AddInput & { webDeploy: WebDeploy; suppressInstallMessage?: boolean },
|
||||||
): Promise<void> {
|
) {
|
||||||
try {
|
try {
|
||||||
const projectDir = input.projectDir || process.cwd();
|
const projectDir = input.projectDir || process.cwd();
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export async function createProjectHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addAddonsHandler(input: AddInput): Promise<void> {
|
export async function addAddonsHandler(input: AddInput) {
|
||||||
try {
|
try {
|
||||||
const projectDir = input.projectDir || process.cwd();
|
const projectDir = input.projectDir || process.cwd();
|
||||||
const detectedConfig = await detectProjectConfig(projectDir);
|
const detectedConfig = await detectProjectConfig(projectDir);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface EnvVariable {
|
|||||||
export async function addEnvVariablesToFile(
|
export async function addEnvVariablesToFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
variables: EnvVariable[],
|
variables: EnvVariable[],
|
||||||
): Promise<void> {
|
) {
|
||||||
await fs.ensureDir(path.dirname(filePath));
|
await fs.ensureDir(path.dirname(filePath));
|
||||||
|
|
||||||
let envContent = "";
|
let envContent = "";
|
||||||
@@ -84,9 +84,7 @@ export async function addEnvVariablesToFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupEnvironmentVariables(
|
export async function setupEnvironmentVariables(config: ProjectConfig) {
|
||||||
config: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const { backend, frontend, database, auth, examples, dbSetup, projectDir } =
|
const { backend, frontend, database, auth, examples, dbSetup, projectDir } =
|
||||||
config;
|
config;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { log } from "@clack/prompts";
|
|||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
|
|
||||||
export async function initializeGit(
|
export async function initializeGit(projectDir: string, useGit: boolean) {
|
||||||
projectDir: string,
|
|
||||||
useGit: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
if (!useGit) return;
|
if (!useGit) return;
|
||||||
|
|
||||||
const gitVersionResult = await $({
|
const gitVersionResult = await $({
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ export async function displayPostInstallInstructions(
|
|||||||
output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (addons?.includes("fumadocs")) {
|
||||||
|
output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
||||||
|
}
|
||||||
|
|
||||||
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
|
||||||
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
|
||||||
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ProjectConfig } from "../../types";
|
|||||||
export async function updatePackageConfigurations(
|
export async function updatePackageConfigurations(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
options: ProjectConfig,
|
options: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
await updateRootPackageJson(projectDir, options);
|
await updateRootPackageJson(projectDir, options);
|
||||||
if (options.backend !== "convex") {
|
if (options.backend !== "convex") {
|
||||||
await updateServerPackageJson(projectDir, options);
|
await updateServerPackageJson(projectDir, options);
|
||||||
@@ -19,7 +19,7 @@ export async function updatePackageConfigurations(
|
|||||||
async function updateRootPackageJson(
|
async function updateRootPackageJson(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
options: ProjectConfig,
|
options: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
||||||
if (!(await fs.pathExists(rootPackageJsonPath))) return;
|
if (!(await fs.pathExists(rootPackageJsonPath))) return;
|
||||||
|
|
||||||
@@ -185,18 +185,6 @@ async function updateRootPackageJson(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.addons.includes("biome")) {
|
|
||||||
scripts.check = "biome check --write .";
|
|
||||||
}
|
|
||||||
if (options.addons.includes("husky")) {
|
|
||||||
scripts.prepare = "husky";
|
|
||||||
packageJson["lint-staged"] = {
|
|
||||||
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
|
|
||||||
"biome check --write .",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execa(options.packageManager, ["-v"], {
|
const { stdout } = await execa(options.packageManager, ["-v"], {
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
@@ -235,7 +223,7 @@ async function updateRootPackageJson(
|
|||||||
async function updateServerPackageJson(
|
async function updateServerPackageJson(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
options: ProjectConfig,
|
options: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
const serverPackageJsonPath = path.join(
|
const serverPackageJsonPath = path.join(
|
||||||
projectDir,
|
projectDir,
|
||||||
"apps/server/package.json",
|
"apps/server/package.json",
|
||||||
@@ -287,7 +275,7 @@ async function updateServerPackageJson(
|
|||||||
async function updateConvexPackageJson(
|
async function updateConvexPackageJson(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
options: ProjectConfig,
|
options: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
const convexPackageJsonPath = path.join(
|
const convexPackageJsonPath = path.join(
|
||||||
projectDir,
|
projectDir,
|
||||||
"packages/backend/package.json",
|
"packages/backend/package.json",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ async function processAndCopyFiles(
|
|||||||
destDir: string,
|
destDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
overwrite = true,
|
overwrite = true,
|
||||||
): Promise<void> {
|
) {
|
||||||
const sourceFiles = await globby(sourcePattern, {
|
const sourceFiles = await globby(sourcePattern, {
|
||||||
cwd: baseSourceDir,
|
cwd: baseSourceDir,
|
||||||
dot: true,
|
dot: true,
|
||||||
@@ -54,7 +54,7 @@ async function processAndCopyFiles(
|
|||||||
export async function copyBaseTemplate(
|
export async function copyBaseTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
const templateDir = path.join(PKG_ROOT, "templates/base");
|
const templateDir = path.join(PKG_ROOT, "templates/base");
|
||||||
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ export async function copyBaseTemplate(
|
|||||||
export async function setupFrontendTemplates(
|
export async function setupFrontendTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
const hasReactWeb = context.frontend.some((f) =>
|
const hasReactWeb = context.frontend.some((f) =>
|
||||||
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(f),
|
||||||
);
|
);
|
||||||
@@ -241,7 +241,7 @@ export async function setupFrontendTemplates(
|
|||||||
export async function setupBackendFramework(
|
export async function setupBackendFramework(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (context.backend === "none") {
|
if (context.backend === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@ export async function setupBackendFramework(
|
|||||||
export async function setupDbOrmTemplates(
|
export async function setupDbOrmTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (
|
if (
|
||||||
context.backend === "convex" ||
|
context.backend === "convex" ||
|
||||||
context.orm === "none" ||
|
context.orm === "none" ||
|
||||||
@@ -357,7 +357,7 @@ export async function setupDbOrmTemplates(
|
|||||||
export async function setupAuthTemplate(
|
export async function setupAuthTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (context.backend === "convex" || !context.auth) return;
|
if (context.backend === "convex" || !context.auth) return;
|
||||||
|
|
||||||
const serverAppDir = path.join(projectDir, "apps/server");
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
@@ -529,7 +529,7 @@ export async function setupAuthTemplate(
|
|||||||
export async function setupAddonsTemplate(
|
export async function setupAddonsTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (!context.addons || context.addons.length === 0) return;
|
if (!context.addons || context.addons.length === 0) return;
|
||||||
|
|
||||||
for (const addon of context.addons) {
|
for (const addon of context.addons) {
|
||||||
@@ -567,7 +567,7 @@ export async function setupAddonsTemplate(
|
|||||||
export async function setupExamplesTemplate(
|
export async function setupExamplesTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (
|
if (
|
||||||
!context.examples ||
|
!context.examples ||
|
||||||
context.examples.length === 0 ||
|
context.examples.length === 0 ||
|
||||||
@@ -773,10 +773,7 @@ export async function setupExamplesTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleExtras(
|
export async function handleExtras(projectDir: string, context: ProjectConfig) {
|
||||||
projectDir: string,
|
|
||||||
context: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const extrasDir = path.join(PKG_ROOT, "templates/extras");
|
const extrasDir = path.join(PKG_ROOT, "templates/extras");
|
||||||
const hasNativeWind = context.frontend.includes("native-nativewind");
|
const hasNativeWind = context.frontend.includes("native-nativewind");
|
||||||
const hasUnistyles = context.frontend.includes("native-unistyles");
|
const hasUnistyles = context.frontend.includes("native-unistyles");
|
||||||
@@ -790,6 +787,14 @@ export async function handleExtras(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.packageManager === "bun") {
|
||||||
|
const bunfigSrc = path.join(extrasDir, "bunfig.toml");
|
||||||
|
const bunfigDest = path.join(projectDir, "bunfig.toml");
|
||||||
|
if (await fs.pathExists(bunfigSrc)) {
|
||||||
|
await fs.copy(bunfigSrc, bunfigDest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
context.packageManager === "pnpm" &&
|
context.packageManager === "pnpm" &&
|
||||||
(hasNative || context.frontend.includes("nuxt"))
|
(hasNative || context.frontend.includes("nuxt"))
|
||||||
@@ -818,7 +823,7 @@ export async function handleExtras(
|
|||||||
export async function setupDockerComposeTemplates(
|
export async function setupDockerComposeTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (context.dbSetup !== "docker" || context.database === "none") {
|
if (context.dbSetup !== "docker" || context.database === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -838,7 +843,7 @@ export async function setupDockerComposeTemplates(
|
|||||||
export async function setupDeploymentTemplates(
|
export async function setupDeploymentTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
if (context.webDeploy === "none") {
|
if (context.webDeploy === "none") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { log } from "@clack/prompts";
|
import { log } from "@clack/prompts";
|
||||||
|
import { execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { Frontend, ProjectConfig } from "../../types";
|
import type { Frontend, PackageManager, ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
import { setupFumadocs } from "./fumadocs-setup";
|
||||||
import { setupStarlight } from "./starlight-setup";
|
import { setupStarlight } from "./starlight-setup";
|
||||||
import { setupTauri } from "./tauri-setup";
|
import { setupTauri } from "./tauri-setup";
|
||||||
|
import { setupUltracite } from "./ultracite-setup";
|
||||||
import { addPwaToViteConfig } from "./vite-pwa-setup";
|
import { addPwaToViteConfig } from "./vite-pwa-setup";
|
||||||
|
|
||||||
export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
|
export async function setupAddons(config: ProjectConfig, isAddCommand = false) {
|
||||||
const { addons, frontend, projectDir } = config;
|
const { addons, frontend, projectDir, packageManager } = config;
|
||||||
const hasReactWebFrontend =
|
const hasReactWebFrontend =
|
||||||
frontend.includes("react-router") ||
|
frontend.includes("react-router") ||
|
||||||
frontend.includes("tanstack-router") ||
|
frontend.includes("tanstack-router") ||
|
||||||
@@ -53,15 +57,37 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
|||||||
) {
|
) {
|
||||||
await setupTauri(config);
|
await setupTauri(config);
|
||||||
}
|
}
|
||||||
if (addons.includes("biome")) {
|
const hasUltracite = addons.includes("ultracite");
|
||||||
|
const hasBiome = addons.includes("biome");
|
||||||
|
const hasHusky = addons.includes("husky");
|
||||||
|
const hasOxlint = addons.includes("oxlint");
|
||||||
|
|
||||||
|
if (hasUltracite) {
|
||||||
|
await setupUltracite(config, hasHusky);
|
||||||
|
} else {
|
||||||
|
if (hasBiome) {
|
||||||
await setupBiome(projectDir);
|
await setupBiome(projectDir);
|
||||||
}
|
}
|
||||||
if (addons.includes("husky")) {
|
if (hasHusky) {
|
||||||
await setupHusky(projectDir);
|
let linter: "biome" | "oxlint" | undefined;
|
||||||
|
if (hasOxlint) {
|
||||||
|
linter = "oxlint";
|
||||||
|
} else if (hasBiome) {
|
||||||
|
linter = "biome";
|
||||||
|
}
|
||||||
|
await setupHusky(projectDir, linter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addons.includes("oxlint")) {
|
||||||
|
await setupOxlint(projectDir, packageManager);
|
||||||
}
|
}
|
||||||
if (addons.includes("starlight")) {
|
if (addons.includes("starlight")) {
|
||||||
await setupStarlight(config);
|
await setupStarlight(config);
|
||||||
}
|
}
|
||||||
|
if (addons.includes("fumadocs")) {
|
||||||
|
await setupFumadocs(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWebAppDir(projectDir: string, frontends: Frontend[]): string {
|
function getWebAppDir(projectDir: string, frontends: Frontend[]): string {
|
||||||
@@ -77,7 +103,7 @@ function getWebAppDir(projectDir: string, frontends: Frontend[]): string {
|
|||||||
return path.join(projectDir, "apps/web");
|
return path.join(projectDir, "apps/web");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupBiome(projectDir: string) {
|
export async function setupBiome(projectDir: string) {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["@biomejs/biome"],
|
devDependencies: ["@biomejs/biome"],
|
||||||
projectDir,
|
projectDir,
|
||||||
@@ -96,7 +122,10 @@ async function setupBiome(projectDir: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupHusky(projectDir: string) {
|
export async function setupHusky(
|
||||||
|
projectDir: string,
|
||||||
|
linter?: "biome" | "oxlint",
|
||||||
|
) {
|
||||||
await addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["husky", "lint-staged"],
|
devDependencies: ["husky", "lint-staged"],
|
||||||
projectDir,
|
projectDir,
|
||||||
@@ -111,11 +140,21 @@ async function setupHusky(projectDir: string) {
|
|||||||
prepare: "husky",
|
prepare: "husky",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (linter === "oxlint") {
|
||||||
|
packageJson["lint-staged"] = {
|
||||||
|
"**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint",
|
||||||
|
};
|
||||||
|
} else if (linter === "biome") {
|
||||||
packageJson["lint-staged"] = {
|
packageJson["lint-staged"] = {
|
||||||
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
|
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
|
||||||
"biome check --write .",
|
"biome check --write .",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
packageJson["lint-staged"] = {
|
||||||
|
"**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
}
|
}
|
||||||
@@ -157,3 +196,33 @@ async function setupPwa(projectDir: string, frontends: Frontend[]) {
|
|||||||
await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
|
await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupOxlint(projectDir: string, packageManager: PackageManager) {
|
||||||
|
await addPackageDependency({
|
||||||
|
devDependencies: ["oxlint"],
|
||||||
|
projectDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(projectDir, "package.json");
|
||||||
|
if (await fs.pathExists(packageJsonPath)) {
|
||||||
|
const packageJson = await fs.readJson(packageJsonPath);
|
||||||
|
|
||||||
|
packageJson.scripts = {
|
||||||
|
...packageJson.scripts,
|
||||||
|
check: "oxlint",
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const oxlintInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
"oxlint@latest --init",
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(oxlintInitCommand, {
|
||||||
|
cwd: projectDir,
|
||||||
|
env: { CI: "true" },
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { AvailableDependencies } from "../../constants";
|
|||||||
import type { Frontend, ProjectConfig } from "../../types";
|
import type { Frontend, ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupApi(config: ProjectConfig): Promise<void> {
|
export async function setupApi(config: ProjectConfig) {
|
||||||
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
const { api, projectName, frontend, backend, packageManager, projectDir } =
|
||||||
config;
|
config;
|
||||||
const isConvex = backend === "convex";
|
const isConvex = backend === "convex";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import pc from "picocolors";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupAuth(config: ProjectConfig): Promise<void> {
|
export async function setupAuth(config: ProjectConfig) {
|
||||||
const { auth, frontend, backend, projectDir } = config;
|
const { auth, frontend, backend, projectDir } = config;
|
||||||
if (backend === "convex" || !auth) {
|
if (backend === "convex" || !auth) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import type { AvailableDependencies } from "../../constants";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupBackendDependencies(
|
export async function setupBackendDependencies(config: ProjectConfig) {
|
||||||
config: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const { backend, runtime, api, projectDir } = config;
|
const { backend, runtime, api, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex") {
|
if (backend === "convex") {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup
|
|||||||
import { setupSupabase } from "../database-providers/supabase-setup";
|
import { setupSupabase } from "../database-providers/supabase-setup";
|
||||||
import { setupTurso } from "../database-providers/turso-setup";
|
import { setupTurso } from "../database-providers/turso-setup";
|
||||||
|
|
||||||
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
export async function setupDatabase(config: ProjectConfig) {
|
||||||
const { database, orm, dbSetup, backend, projectDir } = config;
|
const { database, orm, dbSetup, backend, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex" || database === "none") {
|
if (backend === "convex" || database === "none") {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { AvailableDependencies } from "../../constants";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
export async function setupExamples(config: ProjectConfig) {
|
||||||
const { examples, frontend, backend, projectDir } = config;
|
const { examples, frontend, backend, projectDir } = config;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
96
apps/cli/src/helpers/setup/fumadocs-setup.ts
Normal file
96
apps/cli/src/helpers/setup/fumadocs-setup.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { cancel, isCancel, log, select } from "@clack/prompts";
|
||||||
|
import consola from "consola";
|
||||||
|
import { execa } from "execa";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import type { ProjectConfig } from "../../types";
|
||||||
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
|
type FumadocsTemplate =
|
||||||
|
| "next-mdx"
|
||||||
|
| "next-content-collections"
|
||||||
|
| "react-router-mdx-remote"
|
||||||
|
| "tanstack-start-mdx-remote";
|
||||||
|
|
||||||
|
const TEMPLATES = {
|
||||||
|
"next-mdx": {
|
||||||
|
label: "Next.js: Fumadocs MDX",
|
||||||
|
hint: "Recommended template with MDX support",
|
||||||
|
value: "+next+fuma-docs-mdx",
|
||||||
|
},
|
||||||
|
"next-content-collections": {
|
||||||
|
label: "Next.js: Content Collections",
|
||||||
|
hint: "Template using Next.js content collections",
|
||||||
|
value: "+next+content-collections",
|
||||||
|
},
|
||||||
|
"react-router-mdx-remote": {
|
||||||
|
label: "React Router: MDX Remote",
|
||||||
|
hint: "Template for React Router with MDX remote",
|
||||||
|
value: "react-router",
|
||||||
|
},
|
||||||
|
"tanstack-start-mdx-remote": {
|
||||||
|
label: "Tanstack Start: MDX Remote",
|
||||||
|
hint: "Template for Tanstack Start with MDX remote",
|
||||||
|
value: "tanstack-start",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function setupFumadocs(config: ProjectConfig) {
|
||||||
|
const { packageManager, projectDir } = config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Setting up Fumadocs...");
|
||||||
|
|
||||||
|
const template = await select<FumadocsTemplate>({
|
||||||
|
message: "Choose a template",
|
||||||
|
options: Object.entries(TEMPLATES).map(([key, template]) => ({
|
||||||
|
value: key as FumadocsTemplate,
|
||||||
|
label: template.label,
|
||||||
|
hint: template.hint,
|
||||||
|
})),
|
||||||
|
initialValue: "next-mdx",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(template)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateArg = TEMPLATES[template].value;
|
||||||
|
|
||||||
|
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint`;
|
||||||
|
|
||||||
|
const fumadocsInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
commandWithArgs,
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(fumadocsInitCommand, {
|
||||||
|
cwd: path.join(projectDir, "apps"),
|
||||||
|
env: { CI: "true" },
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
|
||||||
|
const packageJsonPath = path.join(fumadocsDir, "package.json");
|
||||||
|
|
||||||
|
if (await fs.pathExists(packageJsonPath)) {
|
||||||
|
const packageJson = await fs.readJson(packageJsonPath);
|
||||||
|
packageJson.name = "fumadocs";
|
||||||
|
|
||||||
|
if (packageJson.scripts?.dev) {
|
||||||
|
packageJson.scripts.dev = `${packageJson.scripts.dev} --port=4000`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.success("Fumadocs setup successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(pc.red("Failed to set up Fumadocs"));
|
||||||
|
if (error instanceof Error) {
|
||||||
|
consola.error(pc.red(error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import pc from "picocolors";
|
|||||||
import type { Backend, ProjectConfig } from "../../types";
|
import type { Backend, ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
export async function setupRuntime(config: ProjectConfig) {
|
||||||
const { runtime, backend, projectDir } = config;
|
const { runtime, backend, projectDir } = config;
|
||||||
|
|
||||||
if (backend === "convex" || backend === "next" || runtime === "none") {
|
if (backend === "convex" || backend === "next" || runtime === "none") {
|
||||||
@@ -28,9 +28,7 @@ export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateCloudflareWorkerTypes(
|
export async function generateCloudflareWorkerTypes(config: ProjectConfig) {
|
||||||
config: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
if (config.runtime !== "workers") {
|
if (config.runtime !== "workers") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,10 +63,7 @@ export async function generateCloudflareWorkerTypes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupBunRuntime(
|
async function setupBunRuntime(serverDir: string, _backend: Backend) {
|
||||||
serverDir: string,
|
|
||||||
_backend: Backend,
|
|
||||||
): Promise<void> {
|
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
const packageJsonPath = path.join(serverDir, "package.json");
|
||||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||||
|
|
||||||
@@ -88,10 +83,7 @@ async function setupBunRuntime(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupNodeRuntime(
|
async function setupNodeRuntime(serverDir: string, backend: Backend) {
|
||||||
serverDir: string,
|
|
||||||
backend: Backend,
|
|
||||||
): Promise<void> {
|
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
const packageJsonPath = path.join(serverDir, "package.json");
|
||||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||||
|
|
||||||
@@ -123,7 +115,7 @@ async function setupNodeRuntime(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupWorkersRuntime(serverDir: string): Promise<void> {
|
async function setupWorkersRuntime(serverDir: string) {
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
const packageJsonPath = path.join(serverDir, "package.json");
|
||||||
if (!(await fs.pathExists(packageJsonPath))) return;
|
if (!(await fs.pathExists(packageJsonPath))) return;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pc from "picocolors";
|
|||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
export async function setupStarlight(config: ProjectConfig) {
|
||||||
const { packageManager, projectDir } = config;
|
const { packageManager, projectDir } = config;
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { ProjectConfig } from "../../types";
|
|||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
export async function setupTauri(config: ProjectConfig) {
|
||||||
const { packageManager, frontend, projectDir } = config;
|
const { packageManager, frontend, projectDir } = config;
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||||
|
|||||||
141
apps/cli/src/helpers/setup/ultracite-setup.ts
Normal file
141
apps/cli/src/helpers/setup/ultracite-setup.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { cancel, isCancel, log, multiselect } from "@clack/prompts";
|
||||||
|
import { execa } from "execa";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import type { ProjectConfig } from "../../types";
|
||||||
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
import { setupBiome } from "./addons-setup";
|
||||||
|
|
||||||
|
type UltraciteEditor = "vscode" | "zed";
|
||||||
|
type UltraciteRule =
|
||||||
|
| "vscode-copilot"
|
||||||
|
| "cursor"
|
||||||
|
| "windsurf"
|
||||||
|
| "zed"
|
||||||
|
| "claude"
|
||||||
|
| "codex";
|
||||||
|
|
||||||
|
const EDITORS = {
|
||||||
|
vscode: {
|
||||||
|
label: "VSCode / Cursor / Windsurf",
|
||||||
|
hint: "Visual Studio Code editor configuration",
|
||||||
|
},
|
||||||
|
zed: {
|
||||||
|
label: "Zed",
|
||||||
|
hint: "Zed editor configuration",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const RULES = {
|
||||||
|
"vscode-copilot": {
|
||||||
|
label: "VS Code Copilot",
|
||||||
|
hint: "GitHub Copilot integration for VS Code",
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
label: "Cursor",
|
||||||
|
hint: "Cursor AI editor configuration",
|
||||||
|
},
|
||||||
|
windsurf: {
|
||||||
|
label: "Windsurf",
|
||||||
|
hint: "Windsurf editor configuration",
|
||||||
|
},
|
||||||
|
zed: {
|
||||||
|
label: "Zed",
|
||||||
|
hint: "Zed editor rules",
|
||||||
|
},
|
||||||
|
claude: {
|
||||||
|
label: "Claude",
|
||||||
|
hint: "Claude AI integration",
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
label: "Codex",
|
||||||
|
hint: "Codex AI integration",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function setupUltracite(config: ProjectConfig, hasHusky: boolean) {
|
||||||
|
const { packageManager, projectDir } = config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.info("Setting up Ultracite...");
|
||||||
|
|
||||||
|
await setupBiome(projectDir);
|
||||||
|
|
||||||
|
const editors = await multiselect<UltraciteEditor>({
|
||||||
|
message: "Choose editors",
|
||||||
|
options: Object.entries(EDITORS).map(([key, editor]) => ({
|
||||||
|
value: key as UltraciteEditor,
|
||||||
|
label: editor.label,
|
||||||
|
hint: editor.hint,
|
||||||
|
})),
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(editors)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = await multiselect<UltraciteRule>({
|
||||||
|
message: "Choose rules",
|
||||||
|
options: Object.entries(RULES).map(([key, rule]) => ({
|
||||||
|
value: key as UltraciteRule,
|
||||||
|
label: rule.label,
|
||||||
|
hint: rule.hint,
|
||||||
|
})),
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(rules)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ultraciteArgs = ["init", "--pm", packageManager];
|
||||||
|
|
||||||
|
if (editors.length > 0) {
|
||||||
|
ultraciteArgs.push("--editors", ...editors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules.length > 0) {
|
||||||
|
ultraciteArgs.push("--rules", ...rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasHusky) {
|
||||||
|
ultraciteArgs.push("--features", "husky", "lint-staged");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ultraciteArgsString = ultraciteArgs.join(" ");
|
||||||
|
const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
|
||||||
|
|
||||||
|
const ultraciteInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
commandWithArgs,
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(ultraciteInitCommand, {
|
||||||
|
cwd: projectDir,
|
||||||
|
env: { CI: "true" },
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasHusky) {
|
||||||
|
await addPackageDependency({
|
||||||
|
devDependencies: ["husky", "lint-staged"],
|
||||||
|
projectDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await addPackageDependency({
|
||||||
|
devDependencies: ["ultracite"],
|
||||||
|
projectDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
log.success("Ultracite setup successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
log.error(pc.red("Failed to set up Ultracite"));
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(pc.red(error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
|||||||
export async function addPwaToViteConfig(
|
export async function addPwaToViteConfig(
|
||||||
viteConfigPath: string,
|
viteConfigPath: string,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
): Promise<void> {
|
) {
|
||||||
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
|
||||||
if (!sourceFile) {
|
if (!sourceFile) {
|
||||||
throw new Error("vite config not found");
|
throw new Error("vite config not found");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { setupSvelteWorkersDeploy } from "./workers-svelte-setup";
|
|||||||
import { setupTanstackStartWorkersDeploy } from "./workers-tanstack-start-setup";
|
import { setupTanstackStartWorkersDeploy } from "./workers-tanstack-start-setup";
|
||||||
import { setupWorkersVitePlugin } from "./workers-vite-setup";
|
import { setupWorkersVitePlugin } from "./workers-vite-setup";
|
||||||
|
|
||||||
export async function setupWebDeploy(config: ProjectConfig): Promise<void> {
|
export async function setupWebDeploy(config: ProjectConfig) {
|
||||||
const { webDeploy, frontend, projectDir } = config;
|
const { webDeploy, frontend, projectDir } = config;
|
||||||
const { packageManager } = config;
|
const { packageManager } = config;
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export async function setupWebDeploy(config: ProjectConfig): Promise<void> {
|
|||||||
async function setupWorkersWebDeploy(
|
async function setupWorkersWebDeploy(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
pkgManager: PackageManager,
|
pkgManager: PackageManager,
|
||||||
): Promise<void> {
|
) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
if (!(await fs.pathExists(webAppDir))) {
|
if (!(await fs.pathExists(webAppDir))) {
|
||||||
@@ -65,7 +65,7 @@ async function setupWorkersWebDeploy(
|
|||||||
async function setupNextWorkersDeploy(
|
async function setupNextWorkersDeploy(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
_packageManager: PackageManager,
|
_packageManager: PackageManager,
|
||||||
): Promise<void> {
|
) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
if (!(await fs.pathExists(webAppDir))) return;
|
if (!(await fs.pathExists(webAppDir))) return;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { tsProject } from "../../utils/ts-morph";
|
|||||||
export async function setupNuxtWorkersDeploy(
|
export async function setupNuxtWorkersDeploy(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
packageManager: PackageManager,
|
packageManager: PackageManager,
|
||||||
): Promise<void> {
|
) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
if (!(await fs.pathExists(webAppDir))) return;
|
if (!(await fs.pathExists(webAppDir))) return;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { tsProject } from "../../utils/ts-morph";
|
|||||||
export async function setupSvelteWorkersDeploy(
|
export async function setupSvelteWorkersDeploy(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
packageManager: PackageManager,
|
packageManager: PackageManager,
|
||||||
): Promise<void> {
|
) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
if (!(await fs.pathExists(webAppDir))) return;
|
if (!(await fs.pathExists(webAppDir))) return;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
|||||||
export async function setupTanstackStartWorkersDeploy(
|
export async function setupTanstackStartWorkersDeploy(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
packageManager: PackageManager,
|
packageManager: PackageManager,
|
||||||
): Promise<void> {
|
) {
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
if (!(await fs.pathExists(webAppDir))) return;
|
if (!(await fs.pathExists(webAppDir))) return;
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import {
|
|||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
import { ensureArrayProperty, tsProject } from "../../utils/ts-morph";
|
||||||
|
|
||||||
export async function setupWorkersVitePlugin(
|
export async function setupWorkersVitePlugin(projectDir: string) {
|
||||||
projectDir: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const webAppDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
const viteConfigPath = path.join(webAppDir, "vite.config.ts");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
import { cancel, groupMultiselect, isCancel } from "@clack/prompts";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { DEFAULT_CONFIG } from "../constants";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
import { type Addons, AddonsSchema, type Frontend } from "../types";
|
import { type Addons, AddonsSchema, type Frontend } from "../types";
|
||||||
@@ -13,38 +13,48 @@ type AddonOption = {
|
|||||||
hint: string;
|
hint: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAddonDisplay(
|
function getAddonDisplay(addon: Addons): { label: string; hint: string } {
|
||||||
addon: Addons,
|
|
||||||
isRecommended = false,
|
|
||||||
): { label: string; hint: string } {
|
|
||||||
let label: string;
|
let label: string;
|
||||||
let hint: string;
|
let hint: string;
|
||||||
|
|
||||||
if (addon === "turborepo") {
|
switch (addon) {
|
||||||
label = isRecommended ? "Turborepo (Recommended)" : "Turborepo";
|
case "turborepo":
|
||||||
hint = "High-performance build system for JavaScript and TypeScript";
|
label = "Turborepo";
|
||||||
} else if (addon === "pwa") {
|
hint = "High-performance build system";
|
||||||
|
break;
|
||||||
|
case "pwa":
|
||||||
label = "PWA (Progressive Web App)";
|
label = "PWA (Progressive Web App)";
|
||||||
hint = "Make your app installable and work offline";
|
hint = "Make your app installable and work offline";
|
||||||
} else if (addon === "tauri") {
|
break;
|
||||||
label = isRecommended ? "Tauri Desktop App" : "Tauri";
|
case "tauri":
|
||||||
|
label = "Tauri";
|
||||||
hint = "Build native desktop apps from your web frontend";
|
hint = "Build native desktop apps from your web frontend";
|
||||||
} else if (addon === "biome") {
|
break;
|
||||||
|
case "biome":
|
||||||
label = "Biome";
|
label = "Biome";
|
||||||
hint = isRecommended
|
hint = "Format, lint, and more";
|
||||||
? "Add Biome for linting and formatting"
|
break;
|
||||||
: "Fast formatter and linter for JavaScript, TypeScript, JSX";
|
case "oxlint":
|
||||||
} else if (addon === "husky") {
|
label = "Oxlint";
|
||||||
|
hint = "Rust-powered linter";
|
||||||
|
break;
|
||||||
|
case "ultracite":
|
||||||
|
label = "Ultracite";
|
||||||
|
hint = "Zero-config Biome preset with AI integration";
|
||||||
|
break;
|
||||||
|
case "husky":
|
||||||
label = "Husky";
|
label = "Husky";
|
||||||
hint = isRecommended
|
hint = "Modern native Git hooks made easy";
|
||||||
? "Add Git hooks with Husky, lint-staged (requires Biome)"
|
break;
|
||||||
: "Git hooks made easy";
|
case "starlight":
|
||||||
} else if (addon === "starlight") {
|
|
||||||
label = "Starlight";
|
label = "Starlight";
|
||||||
hint = isRecommended
|
hint = "Build stellar docs with astro";
|
||||||
? "Add Astro Starlight documentation site"
|
break;
|
||||||
: "Documentation site with Astro";
|
case "fumadocs":
|
||||||
} else {
|
label = "Fumadocs";
|
||||||
|
hint = "Build excellent documentation site";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
label = addon;
|
label = addon;
|
||||||
hint = `Add ${addon}`;
|
hint = `Add ${addon}`;
|
||||||
}
|
}
|
||||||
@@ -52,6 +62,12 @@ function getAddonDisplay(
|
|||||||
return { label, hint };
|
return { label, hint };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ADDON_GROUPS = {
|
||||||
|
Documentation: ["starlight", "fumadocs"],
|
||||||
|
Linting: ["biome", "oxlint", "ultracite"],
|
||||||
|
Other: ["turborepo", "pwa", "tauri", "husky"],
|
||||||
|
};
|
||||||
|
|
||||||
export async function getAddonsChoice(
|
export async function getAddonsChoice(
|
||||||
addons?: Addons[],
|
addons?: Addons[],
|
||||||
frontends?: Frontend[],
|
frontends?: Frontend[],
|
||||||
@@ -59,38 +75,48 @@ export async function getAddonsChoice(
|
|||||||
if (addons !== undefined) return addons;
|
if (addons !== undefined) return addons;
|
||||||
|
|
||||||
const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
|
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) {
|
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);
|
||||||
const { label, hint } = getAddonDisplay(addon, true);
|
const option = { value: addon, label, hint };
|
||||||
|
|
||||||
allPossibleOptions.push({
|
if (ADDON_GROUPS.Documentation.includes(addon)) {
|
||||||
value: addon,
|
groupedOptions.Documentation.push(option);
|
||||||
label,
|
} else if (ADDON_GROUPS.Linting.includes(addon)) {
|
||||||
hint,
|
groupedOptions.Linting.push(option);
|
||||||
});
|
} else if (ADDON_GROUPS.Other.includes(addon)) {
|
||||||
|
groupedOptions.Other.push(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = allPossibleOptions.sort((a, b) => {
|
Object.keys(groupedOptions).forEach((group) => {
|
||||||
if (a.value === "turborepo") return -1;
|
if (groupedOptions[group].length === 0) {
|
||||||
if (b.value === "turborepo") return 1;
|
delete groupedOptions[group];
|
||||||
return 0;
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) =>
|
const initialValues = DEFAULT_CONFIG.addons.filter((addonValue) =>
|
||||||
|
Object.values(groupedOptions).some((options) =>
|
||||||
options.some((opt) => opt.value === addonValue),
|
options.some((opt) => opt.value === addonValue),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await multiselect({
|
const response = await groupMultiselect<Addons>({
|
||||||
message: "Select addons",
|
message: "Select addons",
|
||||||
options: options,
|
options: groupedOptions,
|
||||||
initialValues: initialValues,
|
initialValues: initialValues,
|
||||||
required: false,
|
required: false,
|
||||||
|
selectableGroups: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) {
|
if (isCancel(response)) {
|
||||||
@@ -98,10 +124,6 @@ export async function getAddonsChoice(
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.includes("husky") && !response.includes("biome")) {
|
|
||||||
response.push("biome");
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,34 +131,48 @@ export async function getAddonsToAdd(
|
|||||||
frontend: Frontend[],
|
frontend: Frontend[],
|
||||||
existingAddons: Addons[] = [],
|
existingAddons: Addons[] = [],
|
||||||
): Promise<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(
|
const compatibleAddons = getCompatibleAddons(
|
||||||
allAddons,
|
AddonsSchema.options.filter((addon) => addon !== "none"),
|
||||||
frontend,
|
frontendArray,
|
||||||
existingAddons,
|
existingAddons,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const addon of compatibleAddons) {
|
for (const addon of compatibleAddons) {
|
||||||
const { label, hint } = getAddonDisplay(addon, false);
|
const { label, hint } = getAddonDisplay(addon);
|
||||||
|
const option = { value: addon, label, hint };
|
||||||
|
|
||||||
options.push({
|
if (ADDON_GROUPS.Documentation.includes(addon)) {
|
||||||
value: addon,
|
groupedOptions.Documentation.push(option);
|
||||||
label,
|
} else if (ADDON_GROUPS.Linting.includes(addon)) {
|
||||||
hint,
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await multiselect<Addons>({
|
const response = await groupMultiselect<Addons>({
|
||||||
message: "Select addons",
|
message: "Select addons to add",
|
||||||
options: options,
|
options: groupedOptions,
|
||||||
required: false,
|
required: false,
|
||||||
|
selectableGroups: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) {
|
if (isCancel(response)) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function getFrontendChoice(
|
|||||||
if (frontendOptions !== undefined) return frontendOptions;
|
if (frontendOptions !== undefined) return frontendOptions;
|
||||||
|
|
||||||
const frontendTypes = await multiselect({
|
const frontendTypes = await multiselect({
|
||||||
message: "Select platforms to develop for",
|
message: "Select project type",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: "web",
|
value: "web",
|
||||||
|
|||||||
@@ -39,7 +39,18 @@ export const FrontendSchema = z
|
|||||||
export type Frontend = z.infer<typeof FrontendSchema>;
|
export type Frontend = z.infer<typeof FrontendSchema>;
|
||||||
|
|
||||||
export const AddonsSchema = z
|
export const AddonsSchema = z
|
||||||
.enum(["pwa", "tauri", "starlight", "biome", "husky", "turborepo", "none"])
|
.enum([
|
||||||
|
"pwa",
|
||||||
|
"tauri",
|
||||||
|
"starlight",
|
||||||
|
"biome",
|
||||||
|
"husky",
|
||||||
|
"turborepo",
|
||||||
|
"fumadocs",
|
||||||
|
"ultracite",
|
||||||
|
"oxlint",
|
||||||
|
"none",
|
||||||
|
])
|
||||||
.describe("Additional addons");
|
.describe("Additional addons");
|
||||||
export type Addons = z.infer<typeof AddonsSchema>;
|
export type Addons = z.infer<typeof AddonsSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const addPackageDependency = async (opts: {
|
|||||||
dependencies?: AvailableDependencies[];
|
dependencies?: AvailableDependencies[];
|
||||||
devDependencies?: AvailableDependencies[];
|
devDependencies?: AvailableDependencies[];
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
}): Promise<void> => {
|
}) => {
|
||||||
const { dependencies = [], devDependencies = [], projectDir } = opts;
|
const { dependencies = [], devDependencies = [], projectDir } = opts;
|
||||||
|
|
||||||
const pkgJsonPath = path.join(projectDir, "package.json");
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { isTelemetryEnabled } from "./telemetry";
|
|||||||
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "";
|
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "";
|
||||||
const POSTHOG_HOST = process.env.POSTHOG_HOST;
|
const POSTHOG_HOST = process.env.POSTHOG_HOST;
|
||||||
|
|
||||||
export async function trackProjectCreation(
|
export async function trackProjectCreation(config: ProjectConfig) {
|
||||||
config: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const posthog = new PostHog(POSTHOG_API_KEY, {
|
const posthog = new PostHog(POSTHOG_API_KEY, {
|
||||||
host: POSTHOG_HOST,
|
host: POSTHOG_HOST,
|
||||||
flushAt: 1,
|
flushAt: 1,
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { getLatestCLIVersion } from "./get-latest-cli-version";
|
|||||||
|
|
||||||
const BTS_CONFIG_FILE = "bts.jsonc";
|
const BTS_CONFIG_FILE = "bts.jsonc";
|
||||||
|
|
||||||
export async function writeBtsConfig(
|
export async function writeBtsConfig(projectConfig: ProjectConfig) {
|
||||||
projectConfig: ProjectConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const btsConfig: BetterTStackConfig = {
|
const btsConfig: BetterTStackConfig = {
|
||||||
version: getLatestCLIVersion(),
|
version: getLatestCLIVersion(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -94,7 +92,7 @@ export async function readBtsConfig(
|
|||||||
export async function updateBtsConfig(
|
export async function updateBtsConfig(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
updates: Partial<Pick<BetterTStackConfig, "addons" | "webDeploy">>,
|
updates: Partial<Pick<BetterTStackConfig, "addons" | "webDeploy">>,
|
||||||
): Promise<void> {
|
) {
|
||||||
try {
|
try {
|
||||||
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
const configPath = path.join(projectDir, BTS_CONFIG_FILE);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { log } from "@clack/prompts";
|
import { log } from "@clack/prompts";
|
||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
|
|
||||||
export async function openUrl(url: string): Promise<void> {
|
export async function openUrl(url: string) {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
let command: string;
|
let command: string;
|
||||||
let args: string[] = [];
|
let args: string[] = [];
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function fetchSponsors(
|
|||||||
return sponsors;
|
return sponsors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displaySponsors(sponsors: SponsorEntry[]): void {
|
export function displaySponsors(sponsors: SponsorEntry[]) {
|
||||||
if (sponsors.length === 0) {
|
if (sponsors.length === 0) {
|
||||||
log.info("No sponsors found. You can be the first one! ✨");
|
log.info("No sponsors found. You can be the first one! ✨");
|
||||||
outro(
|
outro(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function processTemplate(
|
|||||||
srcPath: string,
|
srcPath: string,
|
||||||
destPath: string,
|
destPath: string,
|
||||||
context: ProjectConfig,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
) {
|
||||||
try {
|
try {
|
||||||
const templateContent = await fs.readFile(srcPath, "utf-8");
|
const templateContent = await fs.readFile(srcPath, "utf-8");
|
||||||
const template = handlebars.compile(templateContent);
|
const template = handlebars.compile(templateContent);
|
||||||
|
|||||||
@@ -500,9 +500,7 @@ export function processAndValidateFlags(
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateConfigCompatibility(
|
export function validateConfigCompatibility(config: Partial<ProjectConfig>) {
|
||||||
config: Partial<ProjectConfig>,
|
|
||||||
): void {
|
|
||||||
const effectiveDatabase = config.database;
|
const effectiveDatabase = config.database;
|
||||||
const effectiveBackend = config.backend;
|
const effectiveBackend = config.backend;
|
||||||
const effectiveFrontend = config.frontend;
|
const effectiveFrontend = config.frontend;
|
||||||
@@ -607,11 +605,6 @@ export function validateConfigCompatibility(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons.includes("husky") && !config.addons.includes("biome")) {
|
|
||||||
consola.warn(
|
|
||||||
"Husky addon is recommended to be used with Biome for lint-staged configuration.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
config.addons = [...new Set(config.addons)];
|
config.addons = [...new Set(config.addons)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"!**/.nuxt",
|
"!**/.nuxt",
|
||||||
"!bts.jsonc",
|
"!bts.jsonc",
|
||||||
"!**/.expo",
|
"!**/.expo",
|
||||||
"!**/.wrangler"
|
"!**/.wrangler",
|
||||||
|
"!**/.source"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
22
apps/cli/templates/addons/ultracite/biome.json.hbs
Normal file
22
apps/cli/templates/addons/ultracite/biome.json.hbs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"includes": [
|
||||||
|
"**",
|
||||||
|
"!**/.next",
|
||||||
|
"!**/dist",
|
||||||
|
"!**/.turbo",
|
||||||
|
"!**/dev-dist",
|
||||||
|
"!**/.zed",
|
||||||
|
"!**/.vscode",
|
||||||
|
"!**/routeTree.gen.ts",
|
||||||
|
"!**/src-tauri",
|
||||||
|
"!**/.nuxt",
|
||||||
|
"!bts.jsonc",
|
||||||
|
"!**/.expo",
|
||||||
|
"!**/.wrangler",
|
||||||
|
"!**/.source"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export default function SignInForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function SignUpForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function SignInForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function SignUpForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function SignInForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function SignUpForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function SignInForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function SignUpForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function SignInForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function SignUpForm({
|
|||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void form.handleSubmit();
|
form.handleSubmit();
|
||||||
}}
|
}}
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
|
|||||||
2
apps/cli/templates/extras/bunfig.toml
Normal file
2
apps/cli/templates/extras/bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[install]
|
||||||
|
linker = "isolated"
|
||||||
@@ -10,17 +10,6 @@
|
|||||||
"start": "vite",
|
"start": "vite",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"@tanstack/react-router-devtools": "^1.114.27",
|
|
||||||
"@tanstack/router-plugin": "^1.114.27",
|
|
||||||
"@types/node": "^22.13.13",
|
|
||||||
"@types/react": "^19.0.12",
|
|
||||||
"@types/react-dom": "^19.0.4",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"tailwindcss": "^4.0.15",
|
|
||||||
"vite": "^6.2.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"radix-ui": "^1.4.2",
|
"radix-ui": "^1.4.2",
|
||||||
@@ -37,5 +26,17 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"zod": "^4.0.2"
|
"zod": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tanstack/react-router-devtools": "^1.114.27",
|
||||||
|
"@tanstack/router-plugin": "^1.114.27",
|
||||||
|
"@types/node": "^22.13.13",
|
||||||
|
"@types/react": "^19.0.12",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"tailwindcss": "^4.0.15",
|
||||||
|
"vite": "^6.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ const config = {
|
|||||||
source: "/ingest/decide",
|
source: "/ingest/decide",
|
||||||
destination: "https://us.i.posthog.com/decide",
|
destination: "https://us.i.posthog.com/decide",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/docs/:path*.mdx",
|
||||||
|
destination: "/llms.mdx/:path*",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
19
apps/web/public/icon/ultracite.svg
Normal file
19
apps/web/public/icon/ultracite.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 117 118"
|
||||||
|
width="48"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
role="img"
|
||||||
|
aria-labelledby="ultraciteTitle"
|
||||||
|
>
|
||||||
|
<title id="ultraciteTitle">Ultracite</title>
|
||||||
|
<g fill="currentColor">
|
||||||
|
<path d="m73.4415 3.08447-11.5112-3.08447-9.7007 36.2036-8.7579-32.68485-11.5116 3.08447 9.4625 35.31358-23.5687-23.5686-8.42691 8.4269 25.85191 25.8521-32.19444-8.6265-3.08446 11.5112 35.1764 9.4256c-.4027-1.7371-.6158-3.5471-.6158-5.4068 0-13.1637 10.6713-23.8349 23.835-23.8349s23.8349 10.6712 23.8349 23.8349c0 1.8477-.2104 3.6466-.6082 5.3734l31.9687 8.566 3.084-11.5113-35.3158-9.463 32.1968-8.6271-3.085-11.5112-35.3147 9.4624 23.5686-23.5684-8.4269-8.42693-25.4933 25.49343z" />
|
||||||
|
<path d="m81.5886 65.0381c-.9869 4.1725-3.0705 7.9209-5.9294 10.9241l23.16 23.1603 8.4268-8.4269z" />
|
||||||
|
<path d="m75.4254 76.2044c-2.8935 2.9552-6.55 5.1606-10.6505 6.297l8.4275 31.4516 11.5113-3.084z" />
|
||||||
|
<path d="m64.345 82.6165c-1.9025.4891-3.8965.749-5.9514.749-2.2016 0-4.3335-.2985-6.3574-.8573l-8.4351 31.4808 11.5112 3.084z" />
|
||||||
|
<path d="m51.6292 82.3922c-4.0379-1.193-7.6294-3.4264-10.4637-6.3902l-23.217 23.2171 8.4269 8.4269z" />
|
||||||
|
<path d="m40.9741 75.7968c-2.7857-2.9824-4.8149-6.6808-5.7807-10.7889l-32.07328 8.5941 3.08444 11.5112z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,7 +1,11 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
import { execSync } from "node:child_process";
|
||||||
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
|
|
||||||
|
// TODO: write a more effiecient way of handling analytics
|
||||||
|
|
||||||
interface AnalyticsData {
|
interface AnalyticsData {
|
||||||
date: string;
|
date: string;
|
||||||
hour: number;
|
hour: number;
|
||||||
@@ -36,7 +40,7 @@ interface ProcessedAnalyticsData {
|
|||||||
totalRecords: number;
|
totalRecords: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateAnalyticsData(): Promise<void> {
|
async function generateAnalyticsData() {
|
||||||
try {
|
try {
|
||||||
console.log("🔄 Fetching analytics data...");
|
console.log("🔄 Fetching analytics data...");
|
||||||
|
|
||||||
@@ -157,18 +161,29 @@ async function generateAnalyticsData(): Promise<void> {
|
|||||||
totalRecords: processedData.length,
|
totalRecords: processedData.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const publicDir = join(process.cwd(), "public");
|
console.log("📤 Uploading to Cloudflare R2...");
|
||||||
if (!existsSync(publicDir)) {
|
|
||||||
mkdirSync(publicDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputPath = join(publicDir, "analytics-data.json");
|
const tempDir = mkdtempSync(join(tmpdir(), "analytics-"));
|
||||||
writeFileSync(outputPath, JSON.stringify(analyticsData, null, 2));
|
const tempFilePath = join(tempDir, "analytics-data.json");
|
||||||
|
|
||||||
|
writeFileSync(tempFilePath, JSON.stringify(analyticsData, null, 2));
|
||||||
|
|
||||||
|
const BUCKET_NAME = "bucket";
|
||||||
|
const key = "analytics-data.json";
|
||||||
|
const cmd = `npx wrangler r2 object put "${BUCKET_NAME}/${key}" --file="${tempFilePath}" --remote`;
|
||||||
|
|
||||||
|
console.log(`Uploading ${tempFilePath} to r2://${BUCKET_NAME}/${key} ...`);
|
||||||
|
try {
|
||||||
|
execSync(cmd, { stdio: "inherit" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to upload analytics data:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Generated analytics data with ${processedData.length} records`,
|
`✅ Generated analytics data with ${processedData.length} records`,
|
||||||
);
|
);
|
||||||
console.log(`📁 Saved to: ${outputPath}`);
|
console.log("📤 Uploaded to R2 bucket: bucket/analytics-data.json");
|
||||||
console.log(`🕒 Last data update: ${lastUpdated}`);
|
console.log(`🕒 Last data update: ${lastUpdated}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error generating analytics data:", error);
|
console.error("❌ Error generating analytics data:", error);
|
||||||
|
|||||||
@@ -129,11 +129,15 @@ const getBadgeColors = (category: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TechIcon: React.FC<{
|
function TechIcon({
|
||||||
|
icon,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
icon: string;
|
icon: string;
|
||||||
name: string;
|
name: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ icon, name, className }) => {
|
}) {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
@@ -168,7 +172,7 @@ const TechIcon: React.FC<{
|
|||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const getCategoryDisplayName = (categoryKey: string): string => {
|
const getCategoryDisplayName = (categoryKey: string): string => {
|
||||||
const result = categoryKey.replace(/([A-Z])/g, " $1");
|
const result = categoryKey.replace(/([A-Z])/g, " $1");
|
||||||
@@ -754,10 +758,36 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
nextStack.addons.includes("husky") &&
|
nextStack.addons.includes("husky") &&
|
||||||
!nextStack.addons.includes("biome")
|
!nextStack.addons.includes("biome") &&
|
||||||
|
!nextStack.addons.includes("oxlint")
|
||||||
) {
|
) {
|
||||||
notes.addons.notes.push(
|
notes.addons.notes.push(
|
||||||
"Husky addon is selected without Biome. Consider adding Biome for lint-staged integration.",
|
"Husky addon is selected without a linter. Consider adding Biome or Oxlint for lint-staged integration.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStack.addons.includes("ultracite")) {
|
||||||
|
if (nextStack.addons.includes("biome")) {
|
||||||
|
notes.addons.notes.push(
|
||||||
|
"Ultracite includes Biome setup. Biome addon will be removed.",
|
||||||
|
);
|
||||||
|
nextStack.addons = nextStack.addons.filter(
|
||||||
|
(addon) => addon !== "biome",
|
||||||
|
);
|
||||||
|
changed = true;
|
||||||
|
changes.push({
|
||||||
|
category: "addons",
|
||||||
|
message: "Biome addon removed (included in Ultracite)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextStack.addons.includes("oxlint") &&
|
||||||
|
nextStack.addons.includes("biome")
|
||||||
|
) {
|
||||||
|
notes.addons.notes.push(
|
||||||
|
"Both Oxlint and Biome are selected. Consider using only one linter.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,7 +997,26 @@ const generateCommand = (stackState: StackState): string => {
|
|||||||
|
|
||||||
if (!checkDefault("addons", stackState.addons)) {
|
if (!checkDefault("addons", stackState.addons)) {
|
||||||
if (stackState.addons.length > 0) {
|
if (stackState.addons.length > 0) {
|
||||||
flags.push(`--addons ${stackState.addons.join(" ")}`);
|
const validAddons = stackState.addons.filter((addon) =>
|
||||||
|
[
|
||||||
|
"pwa",
|
||||||
|
"tauri",
|
||||||
|
"starlight",
|
||||||
|
"biome",
|
||||||
|
"husky",
|
||||||
|
"turborepo",
|
||||||
|
"ultracite",
|
||||||
|
"fumadocs",
|
||||||
|
"oxlint",
|
||||||
|
].includes(addon),
|
||||||
|
);
|
||||||
|
if (validAddons.length > 0) {
|
||||||
|
flags.push(`--addons ${validAddons.join(" ")}`);
|
||||||
|
} else {
|
||||||
|
if (DEFAULT_STACK.addons.length > 0) {
|
||||||
|
flags.push("--addons none");
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (DEFAULT_STACK.addons.length > 0) {
|
if (DEFAULT_STACK.addons.length > 0) {
|
||||||
flags.push("--addons none");
|
flags.push("--addons none");
|
||||||
@@ -1687,7 +1736,10 @@ const StackBuilder = () => {
|
|||||||
<TechIcon
|
<TechIcon
|
||||||
icon={tech.icon}
|
icon={tech.icon}
|
||||||
name={tech.name}
|
name={tech.name}
|
||||||
className="mr-1.5 h-3 w-3 sm:h-4 sm:w-4"
|
className={cn(
|
||||||
|
"mr-1.5 h-3 w-3 sm:h-4 sm:w-4",
|
||||||
|
tech.className,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -414,14 +414,14 @@ export default function AnalyticsPage() {
|
|||||||
|
|
||||||
const loadAnalyticsData = useCallback(async () => {
|
const loadAnalyticsData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/analytics-data.json");
|
const response = await fetch("https://r2.amanv.dev/analytics-data.json");
|
||||||
const analyticsData = await response.json();
|
const analyticsData = await response.json();
|
||||||
|
|
||||||
setData(analyticsData.data || []);
|
setData(analyticsData.data || []);
|
||||||
setLastUpdated(analyticsData.lastUpdated || null);
|
setLastUpdated(analyticsData.lastUpdated || null);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Loaded ${analyticsData.data?.length || 0} records from static JSON`,
|
`Loaded ${analyticsData.data?.length || 0} records from R2 bucket`,
|
||||||
);
|
);
|
||||||
console.log(`Data generated at: ${analyticsData.generatedAt}`);
|
console.log(`Data generated at: ${analyticsData.generatedAt}`);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|||||||
11
apps/web/src/app/llms-full.txt/route.ts
Normal file
11
apps/web/src/app/llms-full.txt/route.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { getLLMText } from "@/lib/get-llm-text";
|
||||||
|
import { source } from "@/lib/source";
|
||||||
|
|
||||||
|
export const revalidate = false;
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const scan = source.getPages().map(getLLMText);
|
||||||
|
const scanned = await Promise.all(scan);
|
||||||
|
|
||||||
|
return new Response(scanned.join("\n\n"));
|
||||||
|
}
|
||||||
21
apps/web/src/app/llms.mdx/[[...slug]]/route.ts
Normal file
21
apps/web/src/app/llms.mdx/[[...slug]]/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getLLMText } from "@/lib/get-llm-text";
|
||||||
|
import { source } from "@/lib/source";
|
||||||
|
|
||||||
|
export const revalidate = false;
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ slug?: string[] }> },
|
||||||
|
) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const page = source.getPage(slug);
|
||||||
|
if (!page) notFound();
|
||||||
|
|
||||||
|
return new NextResponse(await getLLMText(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return source.generateParams();
|
||||||
|
}
|
||||||
@@ -1,4 +1,17 @@
|
|||||||
export const TECH_OPTIONS = {
|
import type { TechCategory } from "./types";
|
||||||
|
|
||||||
|
export const TECH_OPTIONS: Record<
|
||||||
|
TechCategory,
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
default?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}[]
|
||||||
|
> = {
|
||||||
api: [
|
api: [
|
||||||
{
|
{
|
||||||
id: "trpc",
|
id: "trpc",
|
||||||
@@ -97,6 +110,7 @@ export const TECH_OPTIONS = {
|
|||||||
description: "Expo with NativeWind (Tailwind)",
|
description: "Expo with NativeWind (Tailwind)",
|
||||||
icon: "/icon/expo.svg",
|
icon: "/icon/expo.svg",
|
||||||
color: "from-purple-400 to-purple-600",
|
color: "from-purple-400 to-purple-600",
|
||||||
|
className: "invert-0 dark:invert",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -105,6 +119,7 @@ export const TECH_OPTIONS = {
|
|||||||
description: "Expo with Unistyles",
|
description: "Expo with Unistyles",
|
||||||
icon: "/icon/expo.svg",
|
icon: "/icon/expo.svg",
|
||||||
color: "from-pink-400 to-pink-600",
|
color: "from-pink-400 to-pink-600",
|
||||||
|
className: "invert-0 dark:invert",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -368,6 +383,7 @@ export const TECH_OPTIONS = {
|
|||||||
description: "Default package manager",
|
description: "Default package manager",
|
||||||
icon: "/icon/npm.svg",
|
icon: "/icon/npm.svg",
|
||||||
color: "from-red-500 to-red-700",
|
color: "from-red-500 to-red-700",
|
||||||
|
className: "invert-0 dark:invert",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pnpm",
|
id: "pnpm",
|
||||||
@@ -388,8 +404,8 @@ export const TECH_OPTIONS = {
|
|||||||
addons: [
|
addons: [
|
||||||
{
|
{
|
||||||
id: "pwa",
|
id: "pwa",
|
||||||
name: "PWA",
|
name: "PWA (Progressive Web App)",
|
||||||
description: "Progressive Web App",
|
description: "Make your app installable and work offline",
|
||||||
icon: "",
|
icon: "",
|
||||||
color: "from-blue-500 to-blue-700",
|
color: "from-blue-500 to-blue-700",
|
||||||
default: false,
|
default: false,
|
||||||
@@ -397,7 +413,7 @@ export const TECH_OPTIONS = {
|
|||||||
{
|
{
|
||||||
id: "tauri",
|
id: "tauri",
|
||||||
name: "Tauri",
|
name: "Tauri",
|
||||||
description: "Desktop app support",
|
description: "Build native desktop apps",
|
||||||
icon: "/icon/tauri.svg",
|
icon: "/icon/tauri.svg",
|
||||||
color: "from-amber-500 to-amber-700",
|
color: "from-amber-500 to-amber-700",
|
||||||
default: false,
|
default: false,
|
||||||
@@ -405,7 +421,7 @@ export const TECH_OPTIONS = {
|
|||||||
{
|
{
|
||||||
id: "starlight",
|
id: "starlight",
|
||||||
name: "Starlight",
|
name: "Starlight",
|
||||||
description: "Documentation site with Astro",
|
description: "Build stellar docs with astro",
|
||||||
icon: "/icon/starlight.svg",
|
icon: "/icon/starlight.svg",
|
||||||
color: "from-teal-500 to-teal-700",
|
color: "from-teal-500 to-teal-700",
|
||||||
default: false,
|
default: false,
|
||||||
@@ -413,7 +429,7 @@ export const TECH_OPTIONS = {
|
|||||||
{
|
{
|
||||||
id: "biome",
|
id: "biome",
|
||||||
name: "Biome",
|
name: "Biome",
|
||||||
description: "Linting & formatting",
|
description: "Format, lint, and more",
|
||||||
icon: "/icon/biome.svg",
|
icon: "/icon/biome.svg",
|
||||||
color: "from-green-500 to-green-700",
|
color: "from-green-500 to-green-700",
|
||||||
default: false,
|
default: false,
|
||||||
@@ -421,15 +437,40 @@ export const TECH_OPTIONS = {
|
|||||||
{
|
{
|
||||||
id: "husky",
|
id: "husky",
|
||||||
name: "Husky",
|
name: "Husky",
|
||||||
description: "Git hooks & lint-staged",
|
description: "Modern native Git hooks made easy",
|
||||||
icon: "",
|
icon: "",
|
||||||
color: "from-purple-500 to-purple-700",
|
color: "from-purple-500 to-purple-700",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "ultracite",
|
||||||
|
name: "Ultracite",
|
||||||
|
description: "Biome preset with AI integration",
|
||||||
|
icon: "/icon/ultracite.svg",
|
||||||
|
color: "from-blue-500 to-blue-700",
|
||||||
|
className: "invert-0 dark:invert",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fumadocs",
|
||||||
|
name: "Fumadocs",
|
||||||
|
description: "Build excellent documentation site",
|
||||||
|
icon: "",
|
||||||
|
color: "from-indigo-500 to-indigo-700",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "oxlint",
|
||||||
|
name: "Oxlint",
|
||||||
|
description: "Rust-powered linter",
|
||||||
|
icon: "",
|
||||||
|
color: "from-orange-500 to-orange-700",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "turborepo",
|
id: "turborepo",
|
||||||
name: "Turborepo",
|
name: "Turborepo",
|
||||||
description: "Monorepo build system",
|
description: "High-performance build system",
|
||||||
icon: "/icon/turborepo.svg",
|
icon: "/icon/turborepo.svg",
|
||||||
color: "from-gray-400 to-gray-700",
|
color: "from-gray-400 to-gray-700",
|
||||||
default: true,
|
default: true,
|
||||||
|
|||||||
26
apps/web/src/lib/get-llm-text.ts
Normal file
26
apps/web/src/lib/get-llm-text.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { InferPageType } from "fumadocs-core/source";
|
||||||
|
import { remarkInclude } from "fumadocs-mdx/config";
|
||||||
|
import { remark } from "remark";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMdx from "remark-mdx";
|
||||||
|
import type { source } from "@/lib/source";
|
||||||
|
|
||||||
|
const processor = remark()
|
||||||
|
.use(remarkMdx)
|
||||||
|
// needed for Fumadocs MDX
|
||||||
|
.use(remarkInclude)
|
||||||
|
.use(remarkGfm);
|
||||||
|
|
||||||
|
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||||
|
const processed = await processor.process({
|
||||||
|
path: page.data._file.absolutePath,
|
||||||
|
value: page.data.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `# ${page.data.title}
|
||||||
|
URL: ${page.url}
|
||||||
|
|
||||||
|
${page.data.description}
|
||||||
|
|
||||||
|
${processed.value}`;
|
||||||
|
}
|
||||||
@@ -1,27 +1,19 @@
|
|||||||
export type TechCategory =
|
export type TechCategory =
|
||||||
| "core"
|
| "api"
|
||||||
| "frontend"
|
| "webFrontend"
|
||||||
|
| "nativeFrontend"
|
||||||
|
| "runtime"
|
||||||
| "backend"
|
| "backend"
|
||||||
| "database"
|
| "database"
|
||||||
| "auth"
|
|
||||||
| "orm"
|
| "orm"
|
||||||
| "router";
|
| "dbSetup"
|
||||||
|
| "webDeploy"
|
||||||
export interface TechNode {
|
| "auth"
|
||||||
id: string;
|
| "packageManager"
|
||||||
type: string;
|
| "addons"
|
||||||
position: { x: number; y: number };
|
| "examples"
|
||||||
data: {
|
| "git"
|
||||||
label: string;
|
| "install";
|
||||||
category: TechCategory;
|
|
||||||
description: string;
|
|
||||||
isDefault: boolean;
|
|
||||||
alternatives?: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
group?: TechCategory;
|
|
||||||
isStatic?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TechEdge {
|
export interface TechEdge {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user