mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add ai chat example and update flags structure
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { PackageManager, ProjectAddons, ProjectFrontend } from "../types";
|
||||
import type {
|
||||
ProjectAddons,
|
||||
ProjectFrontend,
|
||||
ProjectPackageManager,
|
||||
} from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { setupTauri } from "./tauri-setup";
|
||||
|
||||
export async function setupAddons(
|
||||
projectDir: string,
|
||||
addons: ProjectAddons[],
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
frontends: ProjectFrontend[],
|
||||
) {
|
||||
const hasWebFrontend = frontends.includes("web");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from "node:path";
|
||||
import type { AvailableDependencies } from "../constants";
|
||||
import type { BackendFramework, Runtime } from "../types";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupBackendDependencies(
|
||||
projectDir: string,
|
||||
framework: BackendFramework,
|
||||
runtime: Runtime,
|
||||
framework: ProjectBackend,
|
||||
runtime: ProjectRuntime,
|
||||
): Promise<void> {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
|
||||
await fixGitignoreFiles(projectDir);
|
||||
|
||||
await setupBackendFramework(projectDir, options.backendFramework);
|
||||
await setupBackendFramework(projectDir, options.backend);
|
||||
await setupBackendDependencies(
|
||||
projectDir,
|
||||
options.backendFramework,
|
||||
options.backend,
|
||||
options.runtime,
|
||||
);
|
||||
|
||||
@@ -58,19 +58,20 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
await setupAuthTemplate(
|
||||
projectDir,
|
||||
options.auth,
|
||||
options.backendFramework,
|
||||
options.backend,
|
||||
options.orm,
|
||||
options.database,
|
||||
);
|
||||
await setupAuth(projectDir, options.auth);
|
||||
|
||||
await setupRuntime(projectDir, options.runtime, options.backendFramework);
|
||||
await setupRuntime(projectDir, options.runtime, options.backend);
|
||||
|
||||
await setupExamples(
|
||||
projectDir,
|
||||
options.examples,
|
||||
options.orm,
|
||||
options.auth,
|
||||
options.backend,
|
||||
options.frontend,
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
ProjectConfig,
|
||||
ProjectDatabase,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
|
||||
export async function createReadme(projectDir: string, options: ProjectConfig) {
|
||||
@@ -80,7 +80,7 @@ function generateFeaturesList(
|
||||
auth: boolean,
|
||||
addons: ProjectAddons[],
|
||||
orm: ProjectOrm,
|
||||
runtime: Runtime,
|
||||
runtime: ProjectRuntime,
|
||||
): string {
|
||||
const addonsList = [
|
||||
"- **TypeScript** - For type safety and improved developer experience",
|
||||
|
||||
@@ -46,6 +46,13 @@ export async function setupEnvironmentVariables(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.examples?.includes("ai") &&
|
||||
!envContent.includes("GOOGLE_GENERATIVE_AI_API_KEY")
|
||||
) {
|
||||
envContent += "\nGOOGLE_GENERATIVE_AI_API_KEY=";
|
||||
}
|
||||
|
||||
await fs.writeFile(envPath, envContent.trim());
|
||||
|
||||
if (options.frontend.includes("web")) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { ProjectFrontend, ProjectOrm } from "../types";
|
||||
import type { ProjectBackend, ProjectFrontend, ProjectOrm } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupExamples(
|
||||
projectDir: string,
|
||||
examples: string[],
|
||||
orm: ProjectOrm,
|
||||
auth: boolean,
|
||||
backend: ProjectBackend,
|
||||
frontend: ProjectFrontend[] = ["web"],
|
||||
): Promise<void> {
|
||||
console.log("EXAMPLEs:", examples);
|
||||
const hasWebFrontend = frontend.includes("web");
|
||||
|
||||
const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web"));
|
||||
|
||||
if (examples.includes("todo") && hasWebFrontend && webAppExists) {
|
||||
@@ -19,6 +21,135 @@ export async function setupExamples(
|
||||
} else {
|
||||
await cleanupTodoFiles(projectDir, orm);
|
||||
}
|
||||
|
||||
if (
|
||||
examples.includes("ai") &&
|
||||
backend === "hono" &&
|
||||
hasWebFrontend &&
|
||||
webAppExists
|
||||
) {
|
||||
await setupAIExample(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAIExample(projectDir: string): Promise<void> {
|
||||
const aiExampleDir = path.join(PKG_ROOT, "template/examples/ai");
|
||||
|
||||
if (await fs.pathExists(aiExampleDir)) {
|
||||
await fs.copy(aiExampleDir, projectDir);
|
||||
|
||||
await updateHeaderWithAILink(projectDir);
|
||||
|
||||
const clientDir = path.join(projectDir, "apps/web");
|
||||
addPackageDependency({
|
||||
dependencies: ["ai"],
|
||||
projectDir: clientDir,
|
||||
});
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
addPackageDependency({
|
||||
dependencies: ["ai", "@ai-sdk/google"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
await updateServerIndexWithAIRoute(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateServerIndexWithAIRoute(projectDir: string): Promise<void> {
|
||||
const serverIndexPath = path.join(projectDir, "apps/server/src/index.ts");
|
||||
|
||||
if (await fs.pathExists(serverIndexPath)) {
|
||||
let indexContent = await fs.readFile(serverIndexPath, "utf8");
|
||||
const isHono = indexContent.includes("hono");
|
||||
|
||||
if (isHono) {
|
||||
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
|
||||
|
||||
const aiRouteHandler = `
|
||||
app.post("/ai", async (c) => {
|
||||
const body = await c.req.json();
|
||||
const messages = body.messages || [];
|
||||
|
||||
const result = streamText({
|
||||
model: google("gemini-2.0-flash-exp"),
|
||||
messages,
|
||||
});
|
||||
|
||||
c.header("X-Vercel-AI-Data-Stream", "v1");
|
||||
c.header("Content-Type", "text/plain; charset=utf-8");
|
||||
|
||||
return stream(c, (stream) => stream.pipe(result.toDataStream()));
|
||||
});`;
|
||||
|
||||
// Add the import section
|
||||
if (indexContent.includes("import {")) {
|
||||
const lastImportIndex = indexContent.lastIndexOf("import");
|
||||
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
|
||||
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
|
||||
${importSection}
|
||||
${indexContent.substring(endOfLastImport + 1)}`;
|
||||
} else {
|
||||
indexContent = `${importSection}
|
||||
|
||||
${indexContent}`;
|
||||
}
|
||||
|
||||
// Add the route handler
|
||||
const trpcHandlerIndex =
|
||||
indexContent.indexOf('app.use("/trpc"') ||
|
||||
indexContent.indexOf("app.use(trpc(");
|
||||
if (trpcHandlerIndex !== -1) {
|
||||
indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler}
|
||||
|
||||
${indexContent.substring(trpcHandlerIndex)}`;
|
||||
} else {
|
||||
// Add it near the end before export
|
||||
const exportIndex = indexContent.indexOf("export default");
|
||||
if (exportIndex !== -1) {
|
||||
indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler}
|
||||
|
||||
${indexContent.substring(exportIndex)}`;
|
||||
} else {
|
||||
indexContent = `${indexContent}
|
||||
|
||||
${aiRouteHandler}`;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(serverIndexPath, indexContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHeaderWithAILink(projectDir: string): Promise<void> {
|
||||
const headerPath = path.join(
|
||||
projectDir,
|
||||
"apps/web/src/components/header.tsx",
|
||||
);
|
||||
|
||||
if (await fs.pathExists(headerPath)) {
|
||||
let headerContent = await fs.readFile(headerPath, "utf8");
|
||||
|
||||
if (headerContent.includes('{ to: "/todos"')) {
|
||||
headerContent = headerContent.replace(
|
||||
/{ to: "\/todos", label: "Todos" },/,
|
||||
`{ to: "/todos", label: "Todos" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
} else if (headerContent.includes('{ to: "/dashboard"')) {
|
||||
headerContent = headerContent.replace(
|
||||
/{ to: "\/dashboard", label: "Dashboard" },/,
|
||||
`{ to: "/dashboard", label: "Dashboard" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
} else {
|
||||
headerContent = headerContent.replace(
|
||||
/const links = \[\s*{ to: "\/", label: "Home" },/,
|
||||
`const links = [\n { to: "/", label: "Home" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeFile(headerPath, headerContent);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupTodoExample(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { log, spinner } from "@clack/prompts";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectAddons } from "../types";
|
||||
import type { ProjectAddons, ProjectPackageManager } from "../types";
|
||||
|
||||
export async function installDependencies({
|
||||
projectDir,
|
||||
@@ -9,7 +9,7 @@ export async function installDependencies({
|
||||
addons = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
packageManager: PackageManager;
|
||||
packageManager: ProjectPackageManager;
|
||||
addons?: ProjectAddons[];
|
||||
}) {
|
||||
const s = spinner();
|
||||
@@ -17,20 +17,10 @@ export async function installDependencies({
|
||||
try {
|
||||
s.start(`Running ${packageManager} install...`);
|
||||
|
||||
switch (packageManager) {
|
||||
case "npm":
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} install`;
|
||||
break;
|
||||
case "pnpm":
|
||||
case "bun":
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
})`${packageManager} install`;
|
||||
break;
|
||||
}
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} install`;
|
||||
|
||||
s.stop("Dependencies installed successfully");
|
||||
|
||||
@@ -48,14 +38,17 @@ export async function installDependencies({
|
||||
|
||||
async function runBiomeCheck(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
) {
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start("Running Biome format check...");
|
||||
|
||||
await $({ cwd: projectDir })`${packageManager} biome check --write .`;
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} biome check --write .`;
|
||||
|
||||
s.stop("Biome check completed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
PackageManager,
|
||||
ProjectAddons,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
ProjectPackageManager,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
|
||||
export function displayPostInstallInstructions(
|
||||
database: ProjectDatabase,
|
||||
projectName: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
depsInstalled: boolean,
|
||||
orm: ProjectOrm,
|
||||
addons: ProjectAddons[],
|
||||
runtime: Runtime,
|
||||
runtime: ProjectRuntime,
|
||||
frontends: ProjectFrontend[],
|
||||
) {
|
||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||
@@ -67,7 +67,7 @@ function getDatabaseInstructions(
|
||||
database: ProjectDatabase,
|
||||
orm?: ProjectOrm,
|
||||
runCmd?: string,
|
||||
runtime?: Runtime,
|
||||
runtime?: ProjectRuntime,
|
||||
): string {
|
||||
const instructions = [];
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { BackendFramework, Runtime } from "../types";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupRuntime(
|
||||
projectDir: string,
|
||||
runtime: Runtime,
|
||||
backendFramework: BackendFramework,
|
||||
runtime: ProjectRuntime,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const serverIndexPath = path.join(serverDir, "src/index.ts");
|
||||
@@ -34,7 +34,7 @@ async function setupBunRuntime(
|
||||
serverDir: string,
|
||||
serverIndexPath: string,
|
||||
indexContent: string,
|
||||
backendFramework: BackendFramework,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
@@ -62,7 +62,7 @@ async function setupNodeRuntime(
|
||||
serverDir: string,
|
||||
serverIndexPath: string,
|
||||
indexContent: string,
|
||||
backendFramework: BackendFramework,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
@@ -3,12 +3,12 @@ import { log, spinner } from "@clack/prompts";
|
||||
import { $, execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager } from "../types";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupTauri(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
): Promise<void> {
|
||||
const s = spinner();
|
||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type {
|
||||
BackendFramework,
|
||||
ProjectBackend,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
@@ -42,7 +42,7 @@ export async function setupFrontendTemplates(
|
||||
|
||||
export async function setupBackendFramework(
|
||||
projectDir: string,
|
||||
framework: BackendFramework,
|
||||
framework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
|
||||
if (await fs.pathExists(frameworkDir)) {
|
||||
@@ -88,7 +88,7 @@ export async function setupOrmTemplate(
|
||||
export async function setupAuthTemplate(
|
||||
projectDir: string,
|
||||
auth: boolean,
|
||||
framework: BackendFramework,
|
||||
framework: ProjectBackend,
|
||||
orm: ProjectOrm,
|
||||
database: ProjectDatabase,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user