feat(web): improve docs and refactor cli (#476)

This commit is contained in:
Aman Varshney
2025-08-08 16:00:10 +05:30
committed by GitHub
parent defa0e9464
commit 51cfb35912
34 changed files with 1336 additions and 1154 deletions

View File

@@ -1,6 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import type { API, Backend, Frontend } from "../types";
import { allowedApisForFrontends } from "../utils/compatibility-rules";
export async function getApiChoice(
Api?: API | undefined,
@@ -11,46 +12,30 @@ export async function getApiChoice(
return "none";
}
if (Api) return Api;
const allowed = allowedApisForFrontends(frontend ?? []);
const includesNuxt = frontend?.includes("nuxt");
const includesSvelte = frontend?.includes("svelte");
const includesSolid = frontend?.includes("solid");
let apiOptions = [
{
value: "trpc" as const,
label: "tRPC",
hint: "End-to-end typesafe APIs made easy",
},
{
value: "orpc" as const,
label: "oRPC",
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
},
{
value: "none" as const,
label: "None",
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
},
];
if (includesNuxt || includesSvelte || includesSolid) {
apiOptions = [
{
value: "orpc" as const,
label: "oRPC",
hint: `End-to-end type-safe APIs (Recommended for ${
includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"
} frontend)`,
},
{
value: "none" as const,
label: "None",
hint: "No API layer",
},
];
if (Api) {
return allowed.includes(Api) ? Api : allowed[0];
}
const apiOptions = allowed.map((a) =>
a === "trpc"
? {
value: "trpc" as const,
label: "tRPC",
hint: "End-to-end typesafe APIs made easy",
}
: a === "orpc"
? {
value: "orpc" as const,
label: "oRPC",
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
}
: {
value: "none" as const,
label: "None",
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
},
);
const apiType = await select<API>({
message: "Select API type",

View File

@@ -2,6 +2,10 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { API, Backend, Database, Examples, Frontend } from "../types";
import {
isExampleAIAllowed,
isExampleTodoAllowed,
} from "../utils/compatibility-rules";
export async function getExamplesChoice(
examples?: Examples[],
@@ -30,15 +34,17 @@ export async function getExamplesChoice(
if (noFrontendSelected) return [];
let response: Examples[] | symbol = [];
const options: { value: Examples; label: string; hint: string }[] = [
{
const options: { value: Examples; label: string; hint: string }[] = [];
if (isExampleTodoAllowed(backend, database)) {
options.push({
value: "todo" as const,
label: "Todo App",
hint: "A simple CRUD example app",
},
];
});
}
if (backend !== "elysia" && !frontends?.includes("solid")) {
if (isExampleAIAllowed(backend, frontends ?? [])) {
options.push({
value: "ai" as const,
label: "AI Chat",
@@ -46,11 +52,15 @@ export async function getExamplesChoice(
});
}
if (options.length === 0) return [];
response = await multiselect<Examples>({
message: "Include examples",
options: options,
required: false,
initialValues: DEFAULT_CONFIG.examples,
initialValues: DEFAULT_CONFIG.examples?.filter((ex) =>
options.some((o) => o.value === ex),
),
});
if (isCancel(response)) {

View File

@@ -2,6 +2,7 @@ import { cancel, isCancel, multiselect, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend } from "../types";
import { isFrontendAllowedWithBackend } from "../utils/compatibility-rules";
export async function getFrontendChoice(
frontendOptions?: Frontend[],
@@ -73,12 +74,9 @@ export async function getFrontendChoice(
},
];
const webOptions = allWebOptions.filter((option) => {
if (backend === "convex") {
return option.value !== "solid";
}
return true;
});
const webOptions = allWebOptions.filter((option) =>
isFrontendAllowedWithBackend(option.value, backend),
);
const webFramework = await select<Frontend>({
message: "Choose web",

View File

@@ -1,10 +1,17 @@
import path from "node:path";
import { cancel, isCancel, text } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import { ProjectNameSchema } from "../types";
function isPathWithinCwd(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
const rel = path.relative(process.cwd(), resolved);
return !rel.startsWith("..") && !path.isAbsolute(rel);
}
function validateDirectoryName(name: string): string | undefined {
if (name === ".") return undefined;
@@ -23,7 +30,11 @@ export async function getProjectName(initialName?: string): Promise<string> {
const finalDirName = path.basename(initialName);
const validationError = validateDirectoryName(finalDirName);
if (!validationError) {
return initialName;
const projectDir = path.resolve(process.cwd(), initialName);
if (isPathWithinCwd(projectDir)) {
return initialName;
}
consola.error(pc.red("Project path must be within current directory"));
}
}
@@ -56,7 +67,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
if (nameToUse !== ".") {
const projectDir = path.resolve(process.cwd(), nameToUse);
if (!projectDir.startsWith(process.cwd())) {
if (!isPathWithinCwd(projectDir)) {
return "Project path must be within current directory";
}
}

View File

@@ -1,7 +1,8 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG, WEB_FRAMEWORKS } from "../constants";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
import { WEB_FRAMEWORKS } from "../utils/compatibility";
function hasWebFrontend(frontends: Frontend[]): boolean {
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));