mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): improve docs and refactor cli (#476)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user