mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add orpc
This commit is contained in:
5
.changeset/smart-days-argue.md
Normal file
5
.changeset/smart-days-argue.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": major
|
||||||
|
---
|
||||||
|
|
||||||
|
add orpc, make turborepo optional, use handlebarjs to scaffold template
|
||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/.git/objects/**": true,
|
||||||
|
"**/.git/subtree-cache/**": true,
|
||||||
|
"**/.hg/store/**": true,
|
||||||
|
"**/templates/**": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "create-better-t-stack",
|
"name": "create-better-t-stack",
|
||||||
"version": "1.13.2",
|
"version": "1.13.2",
|
||||||
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Aman Varshney",
|
"author": "Aman Varshney",
|
||||||
"bin": {
|
"bin": {
|
||||||
"create-better-t-stack": "dist/index.js"
|
"create-better-t-stack": "dist/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"template",
|
"templates",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"better-t-stack",
|
"better-t-stack",
|
||||||
"typescript",
|
"typescript",
|
||||||
"boilerplate",
|
"boilerplate",
|
||||||
"starter",
|
"starter",
|
||||||
"cli",
|
"cli",
|
||||||
"turborepo",
|
"turborepo",
|
||||||
"trpc",
|
"trpc",
|
||||||
"better-auth",
|
"better-auth",
|
||||||
"monorepo",
|
"monorepo",
|
||||||
"fullstack",
|
"fullstack",
|
||||||
"type-safety",
|
"type-safety",
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
"expo",
|
"expo",
|
||||||
"hono",
|
"hono",
|
||||||
"elysia",
|
"elysia",
|
||||||
"drizzle",
|
"drizzle",
|
||||||
"prisma",
|
"prisma",
|
||||||
"tanstack",
|
"tanstack",
|
||||||
"tailwind",
|
"tailwind",
|
||||||
"shadcn",
|
"shadcn",
|
||||||
"pwa",
|
"pwa",
|
||||||
"tauri",
|
"tauri",
|
||||||
"biome"
|
"biome"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git",
|
"url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git",
|
||||||
"directory": "apps/cli"
|
"directory": "apps/cli"
|
||||||
},
|
},
|
||||||
"homepage": "https://better-t-stack.pages.dev/",
|
"homepage": "https://better-t-stack.pages.dev/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
"check": "biome check --write .",
|
"check": "biome check --write .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.10.1",
|
"@clack/prompts": "^0.10.1",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"execa": "^8.0.1",
|
"execa": "^8.0.1",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"gradient-string": "^3.0.0",
|
"globby": "^14.1.0",
|
||||||
"picocolors": "^1.1.1",
|
"gradient-string": "^3.0.0",
|
||||||
"yargs": "^17.7.2"
|
"handlebars": "^4.7.8",
|
||||||
},
|
"picocolors": "^1.1.1",
|
||||||
"devDependencies": {
|
"yargs": "^17.7.2"
|
||||||
"@types/fs-extra": "^11.0.4",
|
},
|
||||||
"@types/node": "^20.17.30",
|
"devDependencies": {
|
||||||
"@types/yargs": "^17.0.33",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"tsup": "^8.4.0",
|
"@types/globby": "^9.1.0",
|
||||||
"typescript": "^5.8.3"
|
"@types/node": "^20.17.30",
|
||||||
}
|
"@types/yargs": "^17.0.33",
|
||||||
|
"tsup": "^8.4.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
|||||||
examples: [],
|
examples: [],
|
||||||
git: true,
|
git: true,
|
||||||
packageManager: getUserPkgManager(),
|
packageManager: getUserPkgManager(),
|
||||||
noInstall: false,
|
install: true,
|
||||||
dbSetup: "none",
|
dbSetup: "none",
|
||||||
backend: "hono",
|
backend: "hono",
|
||||||
runtime: "bun",
|
runtime: "bun",
|
||||||
|
api: "trpc",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dependencyVersionMap = {
|
export const dependencyVersionMap = {
|
||||||
@@ -69,10 +70,20 @@ export const dependencyVersionMap = {
|
|||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
|
||||||
|
turbo: "^2.4.2",
|
||||||
|
|
||||||
ai: "^4.2.8",
|
ai: "^4.2.8",
|
||||||
"@ai-sdk/google": "^1.2.3",
|
"@ai-sdk/google": "^1.2.3",
|
||||||
|
|
||||||
"@prisma/extension-accelerate": "^1.3.0",
|
"@prisma/extension-accelerate": "^1.3.0",
|
||||||
|
|
||||||
|
"@orpc/server": "^1.0.3",
|
||||||
|
"@orpc/react-query": "^1.0.3",
|
||||||
|
"@orpc/client": "^1.0.3",
|
||||||
|
|
||||||
|
"@trpc/tanstack-react-query": "^11.0.0",
|
||||||
|
"@trpc/server": "^11.0.0",
|
||||||
|
"@trpc/client": "^11.0.0",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import { PKG_ROOT } from "../constants";
|
import type { ProjectFrontend } from "../types";
|
||||||
import type {
|
|
||||||
ProjectAddons,
|
|
||||||
ProjectFrontend,
|
|
||||||
ProjectPackageManager,
|
|
||||||
} from "../types";
|
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
import { setupStarlight } from "./starlight-setup";
|
import { setupStarlight } from "./starlight-setup";
|
||||||
import { setupTauri } from "./tauri-setup";
|
import { setupTauri } from "./tauri-setup";
|
||||||
|
|
||||||
export async function setupAddons(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
addons: ProjectAddons[],
|
export async function setupAddons(config: ProjectConfig) {
|
||||||
packageManager: ProjectPackageManager,
|
const { projectName, addons, packageManager, frontend } = config;
|
||||||
frontends: ProjectFrontend[],
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
) {
|
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
frontends.includes("react-router") || frontends.includes("tanstack-router");
|
frontend.includes("react-router") || frontend.includes("tanstack-router");
|
||||||
|
|
||||||
|
if (addons.includes("turborepo")) {
|
||||||
|
await addPackageDependency({
|
||||||
|
devDependencies: ["turbo"],
|
||||||
|
projectDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (addons.includes("pwa") && hasWebFrontend) {
|
if (addons.includes("pwa") && hasWebFrontend) {
|
||||||
await setupPwa(projectDir, frontends);
|
await setupPwa(projectDir, frontend);
|
||||||
}
|
}
|
||||||
if (addons.includes("tauri") && hasWebFrontend) {
|
if (addons.includes("tauri") && hasWebFrontend) {
|
||||||
await setupTauri(projectDir, packageManager, frontends);
|
await setupTauri(config);
|
||||||
}
|
}
|
||||||
if (addons.includes("biome")) {
|
if (addons.includes("biome")) {
|
||||||
await setupBiome(projectDir);
|
await setupBiome(projectDir);
|
||||||
@@ -32,7 +33,7 @@ export async function setupAddons(
|
|||||||
await setupHusky(projectDir);
|
await setupHusky(projectDir);
|
||||||
}
|
}
|
||||||
if (addons.includes("starlight")) {
|
if (addons.includes("starlight")) {
|
||||||
await setupStarlight(projectDir, packageManager);
|
await setupStarlight(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +45,7 @@ export function getWebAppDir(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupBiome(projectDir: string) {
|
async function setupBiome(projectDir: string) {
|
||||||
const biomeTemplateDir = path.join(PKG_ROOT, "template/with-biome");
|
await addPackageDependency({
|
||||||
if (await fs.pathExists(biomeTemplateDir)) {
|
|
||||||
await fs.copy(biomeTemplateDir, projectDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
addPackageDependency({
|
|
||||||
devDependencies: ["@biomejs/biome"],
|
devDependencies: ["@biomejs/biome"],
|
||||||
projectDir,
|
projectDir,
|
||||||
});
|
});
|
||||||
@@ -68,12 +64,7 @@ async function setupBiome(projectDir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupHusky(projectDir: string) {
|
async function setupHusky(projectDir: string) {
|
||||||
const huskyTemplateDir = path.join(PKG_ROOT, "template/with-husky");
|
await addPackageDependency({
|
||||||
if (await fs.pathExists(huskyTemplateDir)) {
|
|
||||||
await fs.copy(huskyTemplateDir, projectDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
addPackageDependency({
|
|
||||||
devDependencies: ["husky", "lint-staged"],
|
devDependencies: ["husky", "lint-staged"],
|
||||||
projectDir,
|
projectDir,
|
||||||
});
|
});
|
||||||
@@ -98,81 +89,18 @@ async function setupHusky(projectDir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
|
async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) {
|
||||||
const pwaTemplateDir = path.join(PKG_ROOT, "template/with-pwa");
|
|
||||||
if (await fs.pathExists(pwaTemplateDir)) {
|
|
||||||
await fs.copy(pwaTemplateDir, projectDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientPackageDir = getWebAppDir(projectDir, frontends);
|
const clientPackageDir = getWebAppDir(projectDir, frontends);
|
||||||
|
|
||||||
if (!(await fs.pathExists(clientPackageDir))) {
|
if (!(await fs.pathExists(clientPackageDir))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["vite-plugin-pwa"],
|
dependencies: ["vite-plugin-pwa"],
|
||||||
devDependencies: ["@vite-pwa/assets-generator"],
|
devDependencies: ["@vite-pwa/assets-generator"],
|
||||||
projectDir: clientPackageDir,
|
projectDir: clientPackageDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
const viteConfigPath = path.join(clientPackageDir, "vite.config.ts");
|
|
||||||
if (await fs.pathExists(viteConfigPath)) {
|
|
||||||
let viteConfig = await fs.readFile(viteConfigPath, "utf8");
|
|
||||||
|
|
||||||
if (!viteConfig.includes("vite-plugin-pwa")) {
|
|
||||||
const firstImportMatch = viteConfig.match(
|
|
||||||
/^import .* from ['"](.*)['"]/m,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (firstImportMatch) {
|
|
||||||
viteConfig = viteConfig.replace(
|
|
||||||
firstImportMatch[0],
|
|
||||||
`import { VitePWA } from "vite-plugin-pwa";\n${firstImportMatch[0]}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
viteConfig = `import { VitePWA } from "vite-plugin-pwa";\n${viteConfig}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pwaPluginCode = `VitePWA({
|
|
||||||
registerType: "autoUpdate",
|
|
||||||
manifest: {
|
|
||||||
name: "My App",
|
|
||||||
short_name: "My App",
|
|
||||||
description: "My App",
|
|
||||||
theme_color: "#0c0c0c",
|
|
||||||
},
|
|
||||||
pwaAssets: {
|
|
||||||
disabled: false,
|
|
||||||
config: true,
|
|
||||||
},
|
|
||||||
devOptions: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
})`;
|
|
||||||
|
|
||||||
if (!viteConfig.includes("VitePWA(")) {
|
|
||||||
if (frontends.includes("react-router")) {
|
|
||||||
viteConfig = viteConfig.replace(
|
|
||||||
/plugins: \[\s*tailwindcss\(\)/,
|
|
||||||
`plugins: [\n tailwindcss(),\n ${pwaPluginCode}`,
|
|
||||||
);
|
|
||||||
} else if (frontends.includes("tanstack-router")) {
|
|
||||||
viteConfig = viteConfig.replace(
|
|
||||||
/plugins: \[\s*tailwindcss\(\)/,
|
|
||||||
`plugins: [\n tailwindcss(),\n ${pwaPluginCode}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
viteConfig = viteConfig.replace(
|
|
||||||
/plugins: \[/,
|
|
||||||
`plugins: [\n ${pwaPluginCode},`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(viteConfigPath, viteConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
|
const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
|
||||||
if (await fs.pathExists(clientPackageJsonPath)) {
|
if (await fs.pathExists(clientPackageJsonPath)) {
|
||||||
const packageJson = await fs.readJson(clientPackageJsonPath);
|
const packageJson = await fs.readJson(clientPackageJsonPath);
|
||||||
|
|||||||
37
apps/cli/src/helpers/api-setup.ts
Normal file
37
apps/cli/src/helpers/api-setup.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as path from "node:path";
|
||||||
|
import type { ProjectApi, ProjectConfig } from "../types";
|
||||||
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
|
export async function setupApi(config: ProjectConfig): Promise<void> {
|
||||||
|
if (config.api === "none") return;
|
||||||
|
const { api, projectName } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
|
const webDir = path.join(projectDir, "apps/web");
|
||||||
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
|
if (api === "orpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@orpc/react-query", "@orpc/server", "@orpc/client"],
|
||||||
|
projectDir: webDir,
|
||||||
|
});
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@orpc/server", "@orpc/client"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api === "trpc") {
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: [
|
||||||
|
"@trpc/tanstack-react-query",
|
||||||
|
"@trpc/server",
|
||||||
|
"@trpc/client",
|
||||||
|
],
|
||||||
|
projectDir: webDir,
|
||||||
|
});
|
||||||
|
await addPackageDependency({
|
||||||
|
dependencies: ["@trpc/server", "@trpc/client"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,40 +15,40 @@ export function generateAuthSecret(length = 32): string {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupAuth(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
enableAuth: boolean,
|
export async function setupAuth(config: ProjectConfig): Promise<void> {
|
||||||
frontends: ProjectFrontend[] = [],
|
const { projectName, auth, frontend } = config;
|
||||||
): Promise<void> {
|
if (!auth) {
|
||||||
if (!enableAuth) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const clientDir = path.join(projectDir, "apps/web");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
const nativeDir = path.join(projectDir, "apps/native");
|
const nativeDir = path.join(projectDir, "apps/native");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["better-auth"],
|
dependencies: ["better-auth"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
frontends.includes("react-router") ||
|
frontend.includes("react-router") ||
|
||||||
frontends.includes("tanstack-router") ||
|
frontend.includes("tanstack-router") ||
|
||||||
frontends.includes("tanstack-start")
|
frontend.includes("tanstack-start")
|
||||||
) {
|
) {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["better-auth"],
|
dependencies: ["better-auth"],
|
||||||
projectDir: clientDir,
|
projectDir: clientDir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (frontends.includes("native")) {
|
if (frontend.includes("native")) {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["better-auth", "@better-auth/expo"],
|
dependencies: ["better-auth", "@better-auth/expo"],
|
||||||
projectDir: nativeDir,
|
projectDir: nativeDir,
|
||||||
});
|
});
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@better-auth/expo"],
|
dependencies: ["@better-auth/expo"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import type { AvailableDependencies } from "../constants";
|
|||||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupBackendDependencies(
|
export async function setupBackendDependencies(
|
||||||
projectDir: string,
|
config: ProjectConfig,
|
||||||
framework: ProjectBackend,
|
|
||||||
runtime: ProjectRuntime,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { projectName, backend, runtime } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
|
const framework = backend;
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
const dependencies: AvailableDependencies[] = [];
|
const dependencies: AvailableDependencies[] = [];
|
||||||
@@ -40,7 +43,7 @@ export async function setupBackendDependencies(
|
|||||||
devDependencies.push("@types/bun");
|
devDependencies.push("@types/bun");
|
||||||
}
|
}
|
||||||
|
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies,
|
dependencies,
|
||||||
devDependencies,
|
devDependencies,
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cancel, spinner } from "@clack/prompts";
|
import { cancel, log, spinner } from "@clack/prompts";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
import { setupAddons } from "./addons-setup";
|
import { setupAddons } from "./addons-setup";
|
||||||
|
import { setupApi } from "./api-setup";
|
||||||
import { setupAuth } from "./auth-setup";
|
import { setupAuth } from "./auth-setup";
|
||||||
import { setupBackendDependencies } from "./backend-framework-setup";
|
import { setupBackendDependencies } from "./backend-framework-setup";
|
||||||
import { createReadme } from "./create-readme";
|
import { createReadme } from "./create-readme";
|
||||||
@@ -17,10 +18,13 @@ import { setupRuntime } from "./runtime-setup";
|
|||||||
import {
|
import {
|
||||||
copyBaseTemplate,
|
copyBaseTemplate,
|
||||||
fixGitignoreFiles,
|
fixGitignoreFiles,
|
||||||
|
handleExtras,
|
||||||
|
setupAddonsTemplate,
|
||||||
setupAuthTemplate,
|
setupAuthTemplate,
|
||||||
setupBackendFramework,
|
setupBackendFramework,
|
||||||
|
setupDbOrmTemplates,
|
||||||
|
setupExamplesTemplate,
|
||||||
setupFrontendTemplates,
|
setupFrontendTemplates,
|
||||||
setupOrmTemplate,
|
|
||||||
} from "./template-manager";
|
} from "./template-manager";
|
||||||
|
|
||||||
export async function createProject(options: ProjectConfig): Promise<string> {
|
export async function createProject(options: ProjectConfig): Promise<string> {
|
||||||
@@ -30,74 +34,47 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
await fs.ensureDir(projectDir);
|
await fs.ensureDir(projectDir);
|
||||||
|
|
||||||
await copyBaseTemplate(projectDir);
|
await copyBaseTemplate(projectDir, options);
|
||||||
await setupFrontendTemplates(projectDir, options.frontend);
|
await setupFrontendTemplates(projectDir, options);
|
||||||
|
|
||||||
await fixGitignoreFiles(projectDir);
|
await setupBackendFramework(projectDir, options);
|
||||||
|
await setupBackendDependencies(options);
|
||||||
|
|
||||||
await setupBackendFramework(projectDir, options.backend);
|
await setupDbOrmTemplates(projectDir, options);
|
||||||
await setupBackendDependencies(
|
|
||||||
projectDir,
|
|
||||||
options.backend,
|
|
||||||
options.runtime,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setupOrmTemplate(
|
await setupDatabase(options);
|
||||||
projectDir,
|
|
||||||
options.orm,
|
|
||||||
options.database,
|
|
||||||
options.auth,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setupDatabase(
|
await setupAuthTemplate(projectDir, options);
|
||||||
projectDir,
|
await setupAuth(options);
|
||||||
options.database,
|
|
||||||
options.orm,
|
|
||||||
options.packageManager,
|
|
||||||
options.dbSetup === "turso",
|
|
||||||
options.dbSetup === "prisma-postgres",
|
|
||||||
options.dbSetup === "mongodb-atlas",
|
|
||||||
options.dbSetup === "neon",
|
|
||||||
);
|
|
||||||
|
|
||||||
await setupAuthTemplate(
|
await setupAddonsTemplate(projectDir, options);
|
||||||
projectDir,
|
if (options.addons.length > 0 && options.addons[0] !== "none") {
|
||||||
options.auth,
|
await setupAddons(options);
|
||||||
options.backend,
|
|
||||||
options.orm,
|
|
||||||
options.database,
|
|
||||||
options.frontend,
|
|
||||||
);
|
|
||||||
await setupAuth(projectDir, options.auth, options.frontend);
|
|
||||||
|
|
||||||
await setupRuntime(projectDir, options.runtime, options.backend);
|
|
||||||
|
|
||||||
await setupExamples(
|
|
||||||
projectDir,
|
|
||||||
options.examples,
|
|
||||||
options.orm,
|
|
||||||
options.auth,
|
|
||||||
options.backend,
|
|
||||||
options.frontend,
|
|
||||||
);
|
|
||||||
|
|
||||||
await setupEnvironmentVariables(projectDir, options);
|
|
||||||
|
|
||||||
await initializeGit(projectDir, options.git);
|
|
||||||
|
|
||||||
if (options.addons.length > 0) {
|
|
||||||
await setupAddons(
|
|
||||||
projectDir,
|
|
||||||
options.addons,
|
|
||||||
options.packageManager,
|
|
||||||
options.frontend,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setupExamplesTemplate(projectDir, options);
|
||||||
|
await handleExtras(projectDir, options);
|
||||||
|
|
||||||
|
if (options.examples.length > 0 && options.examples[0] !== "none") {
|
||||||
|
await setupExamples(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setupApi(options);
|
||||||
|
|
||||||
|
await setupRuntime(options);
|
||||||
|
|
||||||
|
await setupEnvironmentVariables(options);
|
||||||
|
|
||||||
await updatePackageConfigurations(projectDir, options);
|
await updatePackageConfigurations(projectDir, options);
|
||||||
await createReadme(projectDir, options);
|
await createReadme(projectDir, options);
|
||||||
|
|
||||||
if (!options.noInstall) {
|
await initializeGit(projectDir, options.git);
|
||||||
|
|
||||||
|
await fixGitignoreFiles(projectDir, options);
|
||||||
|
|
||||||
|
log.success("Project template successfully scaffolded!");
|
||||||
|
|
||||||
|
if (options.install) {
|
||||||
await installDependencies({
|
await installDependencies({
|
||||||
projectDir,
|
projectDir,
|
||||||
packageManager: options.packageManager,
|
packageManager: options.packageManager,
|
||||||
@@ -105,22 +82,17 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
displayPostInstallInstructions(
|
displayPostInstallInstructions({
|
||||||
options.database,
|
...options,
|
||||||
options.projectName,
|
depsInstalled: options.install,
|
||||||
options.packageManager,
|
});
|
||||||
!options.noInstall,
|
|
||||||
options.orm,
|
|
||||||
options.addons,
|
|
||||||
options.runtime,
|
|
||||||
options.frontend,
|
|
||||||
);
|
|
||||||
|
|
||||||
return projectDir;
|
return projectDir;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
s.message(pc.red("Failed"));
|
s.stop(pc.red("Failed"));
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
cancel(pc.red(`Error during project creation: ${error.message}`));
|
cancel(pc.red(`Error during project creation: ${error.message}`));
|
||||||
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -10,50 +10,46 @@ import type {
|
|||||||
} from "../types";
|
} from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
||||||
import { setupNeonPostgres } from "./neon-setup";
|
|
||||||
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
||||||
import { setupTurso } from "./turso-setup";
|
import { setupTurso } from "./turso-setup";
|
||||||
|
|
||||||
export async function setupDatabase(
|
import { setupNeonPostgres } from "./neon-setup";
|
||||||
projectDir: string,
|
|
||||||
databaseType: ProjectDatabase,
|
import type { ProjectConfig } from "../types";
|
||||||
orm: ProjectOrm,
|
|
||||||
packageManager: ProjectPackageManager,
|
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
||||||
setupTursoDb: boolean,
|
const { projectName, database, orm, packageManager, dbSetup } = config;
|
||||||
setupPrismaPostgresDb: boolean,
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
setupMongoDBAtlasDb: boolean,
|
|
||||||
setupNeonPostgresDb: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
if (databaseType === "none") {
|
if (database === "none") {
|
||||||
await fs.remove(path.join(serverDir, "src/db"));
|
await fs.remove(path.join(serverDir, "src/db"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (orm === "prisma") {
|
if (orm === "prisma") {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@prisma/client"],
|
dependencies: ["@prisma/client"],
|
||||||
devDependencies: ["prisma"],
|
devDependencies: ["prisma"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
} else if (orm === "drizzle") {
|
} else if (orm === "drizzle") {
|
||||||
if (databaseType === "sqlite") {
|
if (database === "sqlite") {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "@libsql/client"],
|
dependencies: ["drizzle-orm", "@libsql/client"],
|
||||||
devDependencies: ["drizzle-kit"],
|
devDependencies: ["drizzle-kit"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
} else if (databaseType === "postgres") {
|
} else if (database === "postgres") {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "pg"],
|
dependencies: ["drizzle-orm", "pg"],
|
||||||
devDependencies: ["drizzle-kit", "@types/pg"],
|
devDependencies: ["drizzle-kit", "@types/pg"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
} else if (databaseType === "mysql") {
|
} else if (database === "mysql") {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["drizzle-orm", "mysql2"],
|
dependencies: ["drizzle-orm", "mysql2"],
|
||||||
devDependencies: ["drizzle-kit"],
|
devDependencies: ["drizzle-kit"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
@@ -61,16 +57,16 @@ export async function setupDatabase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseType === "sqlite" && setupTursoDb) {
|
if (database === "sqlite" && dbSetup === "turso") {
|
||||||
await setupTurso(projectDir, orm === "drizzle");
|
await setupTurso(config);
|
||||||
} else if (databaseType === "postgres") {
|
} else if (database === "postgres") {
|
||||||
if (orm === "prisma" && setupPrismaPostgresDb) {
|
if (orm === "prisma" && dbSetup === "prisma-postgres") {
|
||||||
await setupPrismaPostgres(projectDir, packageManager);
|
await setupPrismaPostgres(config);
|
||||||
} else if (setupNeonPostgresDb) {
|
} else if (dbSetup === "neon") {
|
||||||
await setupNeonPostgres(projectDir, packageManager);
|
await setupNeonPostgres(config);
|
||||||
}
|
}
|
||||||
} else if (databaseType === "mongodb" && setupMongoDBAtlasDb) {
|
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
||||||
await setupMongoDBAtlas(projectDir);
|
await setupMongoDBAtlas(config);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
s.stop(pc.red("Failed to set up database"));
|
s.stop(pc.red("Failed to set up database"));
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ async function addEnvVariablesToFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setupEnvironmentVariables(
|
export async function setupEnvironmentVariables(
|
||||||
projectDir: string,
|
config: ProjectConfig,
|
||||||
options: ProjectConfig,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { projectName } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
|
const options = config;
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const envPath = path.join(serverDir, ".env");
|
const envPath = path.join(serverDir, ".env");
|
||||||
|
|
||||||
|
|||||||
@@ -1,387 +1,35 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import { PKG_ROOT } from "../constants";
|
import { PKG_ROOT } from "../constants";
|
||||||
import type { ProjectBackend, ProjectFrontend, ProjectOrm } from "../types";
|
import type {
|
||||||
|
ProjectBackend,
|
||||||
|
ProjectConfig,
|
||||||
|
ProjectFrontend,
|
||||||
|
ProjectOrm,
|
||||||
|
} from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupExamples(
|
export async function setupExamples(config: ProjectConfig): Promise<void> {
|
||||||
projectDir: string,
|
const {
|
||||||
examples: string[],
|
projectName,
|
||||||
orm: ProjectOrm,
|
examples,
|
||||||
auth: boolean,
|
orm,
|
||||||
backend: ProjectBackend,
|
auth,
|
||||||
frontend: ProjectFrontend[] = ["tanstack-router"],
|
backend,
|
||||||
): Promise<void> {
|
frontend = ["tanstack-router"],
|
||||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
} = config;
|
||||||
const hasTanstackStart = frontend.includes("tanstack-start");
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const hasReactRouter = frontend.includes("react-router");
|
|
||||||
const hasWebFrontend =
|
|
||||||
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
|
||||||
|
|
||||||
let routerType: string;
|
|
||||||
if (hasTanstackRouter) {
|
|
||||||
routerType = "web-tanstack-router";
|
|
||||||
} else if (hasTanstackStart) {
|
|
||||||
routerType = "web-tanstack-start";
|
|
||||||
} else {
|
|
||||||
routerType = "web-react-router";
|
|
||||||
}
|
|
||||||
|
|
||||||
const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web"));
|
|
||||||
|
|
||||||
if (examples.includes("todo") && hasWebFrontend && webAppExists) {
|
|
||||||
await setupTodoExample(projectDir, orm, auth, routerType);
|
|
||||||
} else {
|
|
||||||
await cleanupTodoFiles(projectDir, orm);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
examples.includes("ai") &&
|
|
||||||
(backend === "hono" || backend === "express") &&
|
|
||||||
hasWebFrontend &&
|
|
||||||
webAppExists
|
|
||||||
) {
|
|
||||||
await setupAIExample(projectDir, routerType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupAIExample(
|
|
||||||
projectDir: string,
|
|
||||||
routerType: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const aiExampleDir = path.join(PKG_ROOT, "template/examples/ai");
|
|
||||||
|
|
||||||
if (await fs.pathExists(aiExampleDir)) {
|
|
||||||
const aiRouteSourcePath = path.join(
|
|
||||||
aiExampleDir,
|
|
||||||
`apps/${routerType}/src/routes/ai.tsx`,
|
|
||||||
);
|
|
||||||
const aiRouteTargetPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/web/src/routes/ai.tsx",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await fs.pathExists(aiRouteSourcePath)) {
|
|
||||||
await fs.copy(aiRouteSourcePath, aiRouteTargetPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateHeaderWithAILink(projectDir, routerType);
|
|
||||||
|
|
||||||
|
if (examples.includes("ai")) {
|
||||||
const clientDir = path.join(projectDir, "apps/web");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["ai"],
|
dependencies: ["ai"],
|
||||||
projectDir: clientDir,
|
projectDir: clientDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["ai", "@ai-sdk/google"],
|
dependencies: ["ai", "@ai-sdk/google"],
|
||||||
projectDir: serverDir,
|
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");
|
|
||||||
const isExpress = indexContent.includes("express");
|
|
||||||
|
|
||||||
if (isHono) {
|
|
||||||
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
|
|
||||||
|
|
||||||
const aiRouteHandler = `
|
|
||||||
// AI chat endpoint
|
|
||||||
app.post("/ai", async (c) => {
|
|
||||||
const body = await c.req.json();
|
|
||||||
const messages = body.messages || [];
|
|
||||||
|
|
||||||
const result = streamText({
|
|
||||||
model: google("gemini-1.5-flash"),
|
|
||||||
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()));
|
|
||||||
});`;
|
|
||||||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const exportIndex = indexContent.indexOf("export default");
|
|
||||||
if (exportIndex !== -1) {
|
|
||||||
indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler}
|
|
||||||
|
|
||||||
${indexContent.substring(exportIndex)}`;
|
|
||||||
} else {
|
|
||||||
indexContent = `${indexContent}
|
|
||||||
|
|
||||||
${aiRouteHandler}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isExpress) {
|
|
||||||
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";`;
|
|
||||||
|
|
||||||
const aiRouteHandler = `
|
|
||||||
// AI chat endpoint
|
|
||||||
app.post("/ai", async (req, res) => {
|
|
||||||
const { messages = [] } = req.body;
|
|
||||||
|
|
||||||
const result = streamText({
|
|
||||||
model: google("gemini-1.5-flash"),
|
|
||||||
messages,
|
|
||||||
});
|
|
||||||
|
|
||||||
result.pipeDataStreamToResponse(res);
|
|
||||||
});`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
indexContent.includes("import {") ||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trpcHandlerIndex = indexContent.indexOf('app.use("/trpc"');
|
|
||||||
if (trpcHandlerIndex !== -1) {
|
|
||||||
indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler}
|
|
||||||
|
|
||||||
${indexContent.substring(trpcHandlerIndex)}`;
|
|
||||||
} else {
|
|
||||||
const appListenIndex = indexContent.indexOf("app.listen(");
|
|
||||||
if (appListenIndex !== -1) {
|
|
||||||
const prevNewlineIndex = indexContent.lastIndexOf(
|
|
||||||
"\n",
|
|
||||||
appListenIndex,
|
|
||||||
);
|
|
||||||
indexContent = `${indexContent.substring(0, prevNewlineIndex)}${aiRouteHandler}
|
|
||||||
|
|
||||||
${indexContent.substring(prevNewlineIndex)}`;
|
|
||||||
} else {
|
|
||||||
indexContent = `${indexContent}
|
|
||||||
|
|
||||||
${aiRouteHandler}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(serverIndexPath, indexContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateHeaderWithAILink(
|
|
||||||
projectDir: string,
|
|
||||||
routerType: 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");
|
|
||||||
|
|
||||||
const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s;
|
|
||||||
const linksMatch = headerContent.match(linksPattern);
|
|
||||||
|
|
||||||
if (linksMatch) {
|
|
||||||
const linksContent = linksMatch[1];
|
|
||||||
if (!linksContent.includes('"/ai"')) {
|
|
||||||
const updatedLinks = `const links = [\n ${linksContent}${
|
|
||||||
linksContent.trim().endsWith(",") ? "" : ","
|
|
||||||
}\n { to: "/ai", label: "AI Chat" },\n ];`;
|
|
||||||
|
|
||||||
headerContent = headerContent.replace(linksPattern, updatedLinks);
|
|
||||||
await fs.writeFile(headerPath, headerContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupTodoExample(
|
|
||||||
projectDir: string,
|
|
||||||
orm: ProjectOrm,
|
|
||||||
auth: boolean,
|
|
||||||
routerType: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
|
|
||||||
|
|
||||||
if (await fs.pathExists(todoExampleDir)) {
|
|
||||||
const todoRouteSourcePath = path.join(
|
|
||||||
todoExampleDir,
|
|
||||||
`apps/${routerType}/src/routes/todos.tsx`,
|
|
||||||
);
|
|
||||||
const todoRouteTargetPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/web/src/routes/todos.tsx",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await fs.pathExists(todoRouteSourcePath)) {
|
|
||||||
await fs.copy(todoRouteSourcePath, todoRouteTargetPath, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orm !== "none") {
|
|
||||||
const todoRouterSourceFile = path.join(
|
|
||||||
todoExampleDir,
|
|
||||||
`apps/server/src/routers/with-${orm}-todo.ts`,
|
|
||||||
);
|
|
||||||
const todoRouterTargetFile = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/server/src/routers/todo.ts",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await fs.pathExists(todoRouterSourceFile)) {
|
|
||||||
await fs.copy(todoRouterSourceFile, todoRouterTargetFile, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateRouterIndexToIncludeTodo(projectDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateHeaderWithTodoLink(projectDir, routerType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateRouterIndexToIncludeTodo(
|
|
||||||
projectDir: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts");
|
|
||||||
|
|
||||||
if (await fs.pathExists(routerFile)) {
|
|
||||||
let routerContent = await fs.readFile(routerFile, "utf8");
|
|
||||||
|
|
||||||
if (!routerContent.includes("import { todoRouter }")) {
|
|
||||||
const lastImportIndex = routerContent.lastIndexOf("import");
|
|
||||||
const endOfImports = routerContent.indexOf("\n\n", lastImportIndex);
|
|
||||||
|
|
||||||
if (endOfImports !== -1) {
|
|
||||||
routerContent = `${routerContent.slice(0, endOfImports)}
|
|
||||||
import { todoRouter } from "./todo";${routerContent.slice(endOfImports)}`;
|
|
||||||
} else {
|
|
||||||
routerContent = `import { todoRouter } from "./todo";\n${routerContent}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const routerDefIndex = routerContent.indexOf(
|
|
||||||
"export const appRouter = router({",
|
|
||||||
);
|
|
||||||
if (routerDefIndex !== -1) {
|
|
||||||
const routerContentStart =
|
|
||||||
routerContent.indexOf("{", routerDefIndex) + 1;
|
|
||||||
routerContent = `${routerContent.slice(0, routerContentStart)}
|
|
||||||
todo: todoRouter,${routerContent.slice(routerContentStart)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(routerFile, routerContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateHeaderWithTodoLink(
|
|
||||||
projectDir: string,
|
|
||||||
routerType: 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");
|
|
||||||
|
|
||||||
const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s;
|
|
||||||
const linksMatch = headerContent.match(linksPattern);
|
|
||||||
|
|
||||||
if (linksMatch) {
|
|
||||||
const linksContent = linksMatch[1];
|
|
||||||
if (!linksContent.includes('"/todos"')) {
|
|
||||||
const updatedLinks = `const links = [\n ${linksContent}${
|
|
||||||
linksContent.trim().endsWith(",") ? "" : ","
|
|
||||||
}\n { to: "/todos", label: "Todos" },\n ];`;
|
|
||||||
|
|
||||||
headerContent = headerContent.replace(linksPattern, updatedLinks);
|
|
||||||
await fs.writeFile(headerPath, headerContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupTodoFiles(
|
|
||||||
projectDir: string,
|
|
||||||
orm: ProjectOrm,
|
|
||||||
): Promise<void> {
|
|
||||||
if (orm === "drizzle") {
|
|
||||||
const todoSchemaFile = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/server/src/db/schema/todo.ts",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(todoSchemaFile)) {
|
|
||||||
await fs.remove(todoSchemaFile);
|
|
||||||
}
|
|
||||||
} else if (orm === "prisma") {
|
|
||||||
const todoPrismaFile = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/server/prisma/schema/todo.prisma",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(todoPrismaFile)) {
|
|
||||||
await fs.remove(todoPrismaFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const todoRouterFile = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/server/src/routers/todo.ts",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(todoRouterFile)) {
|
|
||||||
await fs.remove(todoRouterFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateRouterIndex(projectDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateRouterIndex(projectDir: string): Promise<void> {
|
|
||||||
const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts");
|
|
||||||
|
|
||||||
if (await fs.pathExists(routerFile)) {
|
|
||||||
let routerContent = await fs.readFile(routerFile, "utf8");
|
|
||||||
routerContent = routerContent.replace(
|
|
||||||
/import { todoRouter } from ".\/todo";/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
routerContent = routerContent.replace(/todo: todoRouter,/, "");
|
|
||||||
await fs.writeFile(routerFile, routerContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import consola from "consola";
|
|||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
|
import type { ProjectConfig } from "../types";
|
||||||
import { commandExists } from "../utils/command-exists";
|
import { commandExists } from "../utils/command-exists";
|
||||||
|
|
||||||
type MongoDBConfig = {
|
type MongoDBConfig = {
|
||||||
@@ -129,7 +130,9 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMongoDBAtlas(projectDir: string) {
|
export async function setupMongoDBAtlas(config: ProjectConfig) {
|
||||||
|
const { projectName } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const mainSpinner = spinner();
|
const mainSpinner = spinner();
|
||||||
mainSpinner.start("Setting up MongoDB Atlas");
|
mainSpinner.start("Setting up MongoDB Atlas");
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { execa } from "execa";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectPackageManager } from "../types";
|
import type { ProjectPackageManager } from "../types";
|
||||||
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
type NeonConfig = {
|
type NeonConfig = {
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
@@ -13,41 +14,20 @@ type NeonConfig = {
|
|||||||
roleName: string;
|
roleName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildNeonCommand(
|
|
||||||
packageManager: string,
|
|
||||||
args: string[],
|
|
||||||
): { cmd: string; cmdArgs: string[] } {
|
|
||||||
let cmd: string;
|
|
||||||
let cmdArgs: string[];
|
|
||||||
|
|
||||||
switch (packageManager) {
|
|
||||||
case "pnpm":
|
|
||||||
cmd = "pnpm";
|
|
||||||
cmdArgs = ["dlx", "neonctl", ...args];
|
|
||||||
break;
|
|
||||||
case "bun":
|
|
||||||
cmd = "bunx";
|
|
||||||
cmdArgs = ["neonctl", ...args];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cmd = "npx";
|
|
||||||
cmdArgs = ["neonctl", ...args];
|
|
||||||
}
|
|
||||||
|
|
||||||
return { cmd, cmdArgs };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeNeonCommand(
|
async function executeNeonCommand(
|
||||||
packageManager: string,
|
packageManager: ProjectPackageManager,
|
||||||
args: string[],
|
commandArgsString: string,
|
||||||
spinnerText?: string,
|
spinnerText?: string,
|
||||||
) {
|
) {
|
||||||
const s = spinnerText ? spinner() : null;
|
const s = spinner();
|
||||||
try {
|
try {
|
||||||
const { cmd, cmdArgs } = buildNeonCommand(packageManager, args);
|
const fullCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
commandArgsString,
|
||||||
|
);
|
||||||
|
|
||||||
if (s) s.start(spinnerText);
|
if (s) s.start(spinnerText);
|
||||||
const result = await execa(cmd, cmdArgs);
|
const result = await execa(fullCommand, { shell: true });
|
||||||
if (s) s.stop(spinnerText);
|
if (s) s.stop(spinnerText);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -57,13 +37,10 @@ async function executeNeonCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isNeonAuthenticated(packageManager: string) {
|
async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
|
||||||
try {
|
try {
|
||||||
const { cmd, cmdArgs } = buildNeonCommand(packageManager, [
|
const commandArgsString = "neonctl projects list";
|
||||||
"projects",
|
const result = await executeNeonCommand(packageManager, commandArgsString);
|
||||||
"list",
|
|
||||||
]);
|
|
||||||
const result = await execa(cmd, cmdArgs);
|
|
||||||
return (
|
return (
|
||||||
!result.stdout.includes("not authenticated") &&
|
!result.stdout.includes("not authenticated") &&
|
||||||
!result.stdout.includes("error")
|
!result.stdout.includes("error")
|
||||||
@@ -73,11 +50,11 @@ async function isNeonAuthenticated(packageManager: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authenticateWithNeon(packageManager: string) {
|
async function authenticateWithNeon(packageManager: ProjectPackageManager) {
|
||||||
try {
|
try {
|
||||||
await executeNeonCommand(
|
await executeNeonCommand(
|
||||||
packageManager,
|
packageManager,
|
||||||
["auth"],
|
"neonctl auth",
|
||||||
"Authenticating with Neon...",
|
"Authenticating with Neon...",
|
||||||
);
|
);
|
||||||
log.success("Authenticated with Neon successfully!");
|
log.success("Authenticated with Neon successfully!");
|
||||||
@@ -90,12 +67,13 @@ async function authenticateWithNeon(packageManager: string) {
|
|||||||
|
|
||||||
async function createNeonProject(
|
async function createNeonProject(
|
||||||
projectName: string,
|
projectName: string,
|
||||||
packageManager: string,
|
packageManager: ProjectPackageManager,
|
||||||
): Promise<NeonConfig | null> {
|
): Promise<NeonConfig | null> {
|
||||||
try {
|
try {
|
||||||
|
const commandArgsString = `neonctl projects create --name "${projectName}" --output json`;
|
||||||
const { stdout } = await executeNeonCommand(
|
const { stdout } = await executeNeonCommand(
|
||||||
packageManager,
|
packageManager,
|
||||||
["projects", "create", "--name", projectName, "--output", "json"],
|
commandArgsString,
|
||||||
`Creating Neon project "${projectName}"...`,
|
`Creating Neon project "${projectName}"...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,10 +128,11 @@ function displayManualSetupInstructions() {
|
|||||||
DATABASE_URL="your_connection_string"`);
|
DATABASE_URL="your_connection_string"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupNeonPostgres(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
packageManager: ProjectPackageManager,
|
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
|
||||||
) {
|
const { projectName, packageManager } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
setupSpinner.start("Setting up Neon PostgreSQL");
|
setupSpinner.start("Setting up Neon PostgreSQL");
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,24 @@ import type {
|
|||||||
ProjectPackageManager,
|
ProjectPackageManager,
|
||||||
ProjectRuntime,
|
ProjectRuntime,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export function displayPostInstallInstructions(
|
export function displayPostInstallInstructions(
|
||||||
database: ProjectDatabase,
|
config: ProjectConfig & { depsInstalled: boolean },
|
||||||
projectName: string,
|
|
||||||
packageManager: ProjectPackageManager,
|
|
||||||
depsInstalled: boolean,
|
|
||||||
orm: ProjectOrm,
|
|
||||||
addons: ProjectAddons[],
|
|
||||||
runtime: ProjectRuntime,
|
|
||||||
frontends: ProjectFrontend[],
|
|
||||||
dbSetup?: ProjectDBSetup,
|
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
database,
|
||||||
|
projectName,
|
||||||
|
packageManager,
|
||||||
|
depsInstalled,
|
||||||
|
orm,
|
||||||
|
addons,
|
||||||
|
runtime,
|
||||||
|
frontend,
|
||||||
|
dbSetup,
|
||||||
|
} = config;
|
||||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||||
const cdCmd = `cd ${projectName}`;
|
const cdCmd = `cd ${projectName}`;
|
||||||
const hasHuskyOrBiome =
|
const hasHuskyOrBiome =
|
||||||
@@ -36,38 +42,59 @@ export function displayPostInstallInstructions(
|
|||||||
const lintingInstructions = hasHuskyOrBiome
|
const lintingInstructions = hasHuskyOrBiome
|
||||||
? getLintingInstructions(runCmd)
|
? getLintingInstructions(runCmd)
|
||||||
: "";
|
: "";
|
||||||
const nativeInstructions = frontends?.includes("native")
|
const nativeInstructions = frontend?.includes("native")
|
||||||
? getNativeInstructions()
|
? getNativeInstructions()
|
||||||
: "";
|
: "";
|
||||||
const pwaInstructions =
|
const pwaInstructions =
|
||||||
addons?.includes("pwa") && frontends?.includes("react-router")
|
addons?.includes("pwa") && frontend?.includes("react-router")
|
||||||
? getPwaInstructions()
|
? getPwaInstructions()
|
||||||
: "";
|
: "";
|
||||||
const starlightInstructions = addons?.includes("starlight")
|
const starlightInstructions = addons?.includes("starlight")
|
||||||
? getStarlightInstructions(runCmd)
|
? getStarlightInstructions(runCmd)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const hasTanstackRouter = frontends?.includes("tanstack-router");
|
const hasTanstackRouter = frontend?.includes("tanstack-router");
|
||||||
const hasTanstackStart = frontends?.includes("tanstack-start");
|
const hasTanstackStart = frontend?.includes("tanstack-start");
|
||||||
const hasReactRouter = frontends?.includes("react-router");
|
const hasReactRouter = frontend?.includes("react-router");
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
||||||
const hasNativeFrontend = frontends?.includes("native");
|
const hasNativeFrontend = frontend?.includes("native");
|
||||||
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
||||||
|
|
||||||
const webPort = hasReactRouter ? "5173" : "3001";
|
const webPort = hasReactRouter ? "5173" : "3001";
|
||||||
|
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");
|
||||||
|
|
||||||
consola.box(
|
consola.box(
|
||||||
`${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}
|
`${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}
|
||||||
${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
|
${
|
||||||
|
!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""
|
||||||
|
}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
|
||||||
|
|
||||||
${pc.bold("Your project will be available at:")}
|
${pc.bold("Your project will be available at:")}
|
||||||
${
|
${
|
||||||
hasFrontend
|
hasFrontend
|
||||||
? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n` : ""}`
|
? `${
|
||||||
: `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`
|
hasWebFrontend
|
||||||
}${pc.cyan("•")} API: http://localhost:3000
|
? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`
|
||||||
${addons?.includes("starlight") ? `${pc.cyan("•")} Docs: http://localhost:4321\n` : ""}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${starlightInstructions ? `\n${starlightInstructions.trim()}` : ""}
|
: ""
|
||||||
|
}`
|
||||||
|
: `${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} You are creating a backend-only app (no frontend selected)\n`
|
||||||
|
}${pc.cyan("•")} Backend: http://localhost:3000
|
||||||
|
${
|
||||||
|
addons?.includes("starlight")
|
||||||
|
? `${pc.cyan("•")} Docs: http://localhost:4321\n`
|
||||||
|
: ""
|
||||||
|
}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${
|
||||||
|
databaseInstructions ? `\n${databaseInstructions.trim()}` : ""
|
||||||
|
}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${
|
||||||
|
lintingInstructions ? `\n${lintingInstructions.trim()}` : ""
|
||||||
|
}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${
|
||||||
|
starlightInstructions ? `\n${starlightInstructions.trim()}` : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}
|
||||||
|
|
||||||
${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub:
|
${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub:
|
||||||
${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
|
${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
|
||||||
@@ -75,11 +102,15 @@ ${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNativeInstructions(): string {
|
function getNativeInstructions(): string {
|
||||||
return `${pc.yellow("NOTE:")} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`;
|
return `${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLintingInstructions(runCmd?: string): string {
|
function getLintingInstructions(runCmd?: string): string {
|
||||||
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n\n`;
|
return `${pc.bold("Linting and formatting:")}\n${pc.cyan(
|
||||||
|
"•",
|
||||||
|
)} Format and lint fix: ${`${runCmd} check`}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabaseInstructions(
|
function getDatabaseInstructions(
|
||||||
@@ -93,14 +124,18 @@ function getDatabaseInstructions(
|
|||||||
if (orm === "prisma") {
|
if (orm === "prisma") {
|
||||||
if (database === "sqlite") {
|
if (database === "sqlite") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`,
|
`${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} Turso support with Prisma is in Early Access and requires additional setup.`,
|
||||||
`${"Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso"}`,
|
`${"Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runtime === "bun") {
|
if (runtime === "bun") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
|
`${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +152,25 @@ function getDatabaseInstructions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTauriInstructions(runCmd?: string): string {
|
function getTauriInstructions(runCmd?: string): string {
|
||||||
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`;
|
return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan(
|
||||||
|
"•",
|
||||||
|
)} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan(
|
||||||
|
"•",
|
||||||
|
)} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPwaInstructions(): string {
|
function getPwaInstructions(): string {
|
||||||
return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`;
|
return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow(
|
||||||
|
"NOTE:",
|
||||||
|
)} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStarlightInstructions(runCmd?: string): string {
|
function getStarlightInstructions(runCmd?: string): string {
|
||||||
return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`;
|
return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan(
|
||||||
|
"•",
|
||||||
|
)} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan(
|
||||||
|
"•",
|
||||||
|
)} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import fs from "fs-extra";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectPackageManager } from "../types";
|
import type { ProjectPackageManager } from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
type PrismaConfig = {
|
type PrismaConfig = {
|
||||||
databaseUrl: string;
|
databaseUrl: string;
|
||||||
@@ -22,18 +23,17 @@ async function initPrismaDatabase(
|
|||||||
const prismaDir = path.join(serverDir, "prisma");
|
const prismaDir = path.join(serverDir, "prisma");
|
||||||
await fs.ensureDir(prismaDir);
|
await fs.ensureDir(prismaDir);
|
||||||
|
|
||||||
const initCmd =
|
|
||||||
packageManager === "npm"
|
|
||||||
? "npx"
|
|
||||||
: packageManager === "pnpm"
|
|
||||||
? "pnpm dlx"
|
|
||||||
: "bunx";
|
|
||||||
|
|
||||||
s.stop("Initializing Prisma. Follow the prompts below:");
|
s.stop("Initializing Prisma. Follow the prompts below:");
|
||||||
|
|
||||||
await execa(initCmd, ["prisma", "init", "--db"], {
|
const prismaInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
"prisma init --db",
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(prismaInitCommand, {
|
||||||
cwd: serverDir,
|
cwd: serverDir,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@@ -112,7 +112,7 @@ DATABASE_URL="your_database_url"`);
|
|||||||
|
|
||||||
async function addPrismaAccelerateExtension(serverDir: string) {
|
async function addPrismaAccelerateExtension(serverDir: string) {
|
||||||
try {
|
try {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@prisma/extension-accelerate"],
|
dependencies: ["@prisma/extension-accelerate"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
@@ -152,10 +152,11 @@ export default prisma;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupPrismaPostgres(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
packageManager: ProjectPackageManager = "npm",
|
export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||||
) {
|
const { projectName, packageManager } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
s.start("Setting up Prisma PostgreSQL");
|
s.start("Setting up Prisma PostgreSQL");
|
||||||
@@ -184,7 +185,9 @@ export async function setupPrismaPostgres(
|
|||||||
s.stop(pc.red("Prisma PostgreSQL setup failed"));
|
s.stop(pc.red("Prisma PostgreSQL setup failed"));
|
||||||
consola.error(
|
consola.error(
|
||||||
pc.red(
|
pc.red(
|
||||||
`Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`,
|
`Error during Prisma PostgreSQL setup: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { log } from "@clack/prompts";
|
|||||||
import { $, execa } from "execa";
|
import { $, execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { PKG_ROOT } from "../constants";
|
|
||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function updatePackageConfigurations(
|
export async function updatePackageConfigurations(
|
||||||
@@ -23,24 +22,71 @@ async function updateRootPackageJson(
|
|||||||
const packageJson = await fs.readJson(rootPackageJsonPath);
|
const packageJson = await fs.readJson(rootPackageJsonPath);
|
||||||
packageJson.name = options.projectName;
|
packageJson.name = options.projectName;
|
||||||
|
|
||||||
|
// Define script sets
|
||||||
|
const turboScripts = {
|
||||||
|
dev: "turbo dev",
|
||||||
|
build: "turbo build",
|
||||||
|
"check-types": "turbo check-types",
|
||||||
|
"dev:native": "turbo -F native dev",
|
||||||
|
"dev:web": "turbo -F web dev",
|
||||||
|
"dev:server": "turbo -F server dev",
|
||||||
|
"db:push": "turbo -F server db:push",
|
||||||
|
"db:studio": "turbo -F server db:studio",
|
||||||
|
};
|
||||||
|
|
||||||
|
const pnpmScripts = {
|
||||||
|
dev: "pnpm -r --parallel dev",
|
||||||
|
build: "pnpm -r build",
|
||||||
|
"check-types": "pnpm -r check-types",
|
||||||
|
"dev:native": "pnpm --filter native dev",
|
||||||
|
"dev:web": "pnpm --filter web dev",
|
||||||
|
"dev:server": "pnpm --filter server dev",
|
||||||
|
"db:push": "pnpm --filter server db:push",
|
||||||
|
"db:studio": "pnpm --filter server db:studio",
|
||||||
|
};
|
||||||
|
|
||||||
|
const npmScripts = {
|
||||||
|
dev: "npm run dev --workspaces",
|
||||||
|
build: "npm run build --workspaces",
|
||||||
|
"check-types": "npm run check-types --workspaces",
|
||||||
|
"dev:native": "npm run dev --workspace native",
|
||||||
|
"dev:web": "npm run dev --workspace web",
|
||||||
|
"dev:server": "npm run dev --workspace server",
|
||||||
|
"db:push": "npm run db:push --workspace server",
|
||||||
|
"db:studio": "npm run db:studio --workspace server",
|
||||||
|
};
|
||||||
|
|
||||||
|
const bunScripts = {
|
||||||
|
dev: "bun run --filter '*' dev",
|
||||||
|
build: "bun run --filter '*' build",
|
||||||
|
"check-types": "bun run --filter '*' check-types",
|
||||||
|
"dev:native": "bun run --filter native dev",
|
||||||
|
"dev:web": "bun run --filter web dev",
|
||||||
|
"dev:server": "bun run --filter server dev",
|
||||||
|
"db:push": "bun run --filter server db:push",
|
||||||
|
"db:studio": "bun run --filter server db:studio",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.addons.includes("turborepo")) {
|
||||||
|
packageJson.scripts = turboScripts;
|
||||||
|
} else {
|
||||||
|
if (options.packageManager === "pnpm") {
|
||||||
|
packageJson.scripts = pnpmScripts;
|
||||||
|
} else if (options.packageManager === "npm") {
|
||||||
|
packageJson.scripts = npmScripts;
|
||||||
|
} else if (options.packageManager === "bun") {
|
||||||
|
packageJson.scripts = bunScripts;
|
||||||
|
} else {
|
||||||
|
packageJson.scripts = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { stdout } = await execa(options.packageManager, ["-v"], {
|
const { stdout } = await execa(options.packageManager, ["-v"], {
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
});
|
});
|
||||||
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`;
|
||||||
|
|
||||||
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
|
||||||
if (options.packageManager === "pnpm") {
|
|
||||||
const pnpmWorkspaceTemplatePath = path.join(
|
|
||||||
PKG_ROOT,
|
|
||||||
"template/with-pnpm/pnpm-workspace.yaml",
|
|
||||||
);
|
|
||||||
const targetWorkspacePath = path.join(projectDir, "pnpm-workspace.yaml");
|
|
||||||
|
|
||||||
if (await fs.pathExists(pnpmWorkspaceTemplatePath)) {
|
|
||||||
await fs.copy(pnpmWorkspaceTemplatePath, targetWorkspacePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +103,7 @@ async function updateServerPackageJson(
|
|||||||
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
||||||
|
|
||||||
if (options.database !== "none") {
|
if (options.database !== "none") {
|
||||||
if (options.database === "sqlite") {
|
if (options.database === "sqlite" && options.orm === "drizzle") {
|
||||||
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
|
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,27 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
import type { ProjectBackend, ProjectConfig, ProjectRuntime } from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
export async function setupRuntime(
|
export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
||||||
projectDir: string,
|
const { projectName, runtime, backend } = config;
|
||||||
runtime: ProjectRuntime,
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
backendFramework: ProjectBackend,
|
if (backend === "next") {
|
||||||
): Promise<void> {
|
|
||||||
if (backendFramework === "next") {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
const serverIndexPath = path.join(serverDir, "src/index.ts");
|
|
||||||
|
|
||||||
const indexContent = await fs.readFile(serverIndexPath, "utf-8");
|
|
||||||
|
|
||||||
if (runtime === "bun") {
|
if (runtime === "bun") {
|
||||||
await setupBunRuntime(
|
await setupBunRuntime(serverDir, backend);
|
||||||
serverDir,
|
|
||||||
serverIndexPath,
|
|
||||||
indexContent,
|
|
||||||
backendFramework,
|
|
||||||
);
|
|
||||||
} else if (runtime === "node") {
|
} else if (runtime === "node") {
|
||||||
await setupNodeRuntime(
|
await setupNodeRuntime(serverDir, backend);
|
||||||
serverDir,
|
|
||||||
serverIndexPath,
|
|
||||||
indexContent,
|
|
||||||
backendFramework,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupBunRuntime(
|
async function setupBunRuntime(
|
||||||
serverDir: string,
|
serverDir: string,
|
||||||
serverIndexPath: string,
|
backend: ProjectBackend,
|
||||||
indexContent: string,
|
|
||||||
backendFramework: ProjectBackend,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
const packageJsonPath = path.join(serverDir, "package.json");
|
||||||
const packageJson = await fs.readJson(packageJsonPath);
|
const packageJson = await fs.readJson(packageJsonPath);
|
||||||
@@ -51,22 +34,15 @@ async function setupBunRuntime(
|
|||||||
|
|
||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["@types/bun"],
|
devDependencies: ["@types/bun"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (backendFramework === "hono") {
|
|
||||||
const updatedContent = `${indexContent}\n\nexport default app;\n`;
|
|
||||||
await fs.writeFile(serverIndexPath, updatedContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupNodeRuntime(
|
async function setupNodeRuntime(
|
||||||
serverDir: string,
|
serverDir: string,
|
||||||
serverIndexPath: string,
|
backend: ProjectBackend,
|
||||||
indexContent: string,
|
|
||||||
backendFramework: ProjectBackend,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
const packageJsonPath = path.join(serverDir, "package.json");
|
||||||
const packageJson = await fs.readJson(packageJsonPath);
|
const packageJson = await fs.readJson(packageJsonPath);
|
||||||
@@ -79,62 +55,20 @@ async function setupNodeRuntime(
|
|||||||
|
|
||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["tsx", "@types/node"],
|
devDependencies: ["tsx", "@types/node"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (backendFramework === "hono") {
|
if (backend === "hono") {
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
dependencies: ["@hono/node-server"],
|
dependencies: ["@hono/node-server"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
} else if (backend === "elysia") {
|
||||||
const importLine = 'import { serve } from "@hono/node-server";\n';
|
await addPackageDependency({
|
||||||
const serverCode = `
|
|
||||||
serve(
|
|
||||||
{
|
|
||||||
fetch: app.fetch,
|
|
||||||
port: 3000,
|
|
||||||
},
|
|
||||||
(info) => {
|
|
||||||
console.log(\`Server is running on http://localhost:\${info.port}\`);
|
|
||||||
},
|
|
||||||
);\n`;
|
|
||||||
|
|
||||||
if (!indexContent.includes("@hono/node-server")) {
|
|
||||||
const importEndIndex = indexContent.lastIndexOf("import");
|
|
||||||
const importSection = indexContent.substring(0, importEndIndex);
|
|
||||||
const restOfFile = indexContent.substring(importEndIndex);
|
|
||||||
|
|
||||||
const updatedContent =
|
|
||||||
importSection + importLine + restOfFile + serverCode;
|
|
||||||
await fs.writeFile(serverIndexPath, updatedContent);
|
|
||||||
}
|
|
||||||
} else if (backendFramework === "elysia") {
|
|
||||||
addPackageDependency({
|
|
||||||
dependencies: ["@elysiajs/node"],
|
dependencies: ["@elysiajs/node"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!indexContent.includes("@elysiajs/node")) {
|
|
||||||
const nodeImport = 'import { node } from "@elysiajs/node";\n';
|
|
||||||
|
|
||||||
const firstImportEnd = indexContent.indexOf(
|
|
||||||
"\n",
|
|
||||||
indexContent.indexOf("import"),
|
|
||||||
);
|
|
||||||
const before = indexContent.substring(0, firstImportEnd + 1);
|
|
||||||
const after = indexContent.substring(firstImportEnd + 1);
|
|
||||||
|
|
||||||
let updatedContent = before + nodeImport + after;
|
|
||||||
|
|
||||||
updatedContent = updatedContent.replace(
|
|
||||||
/const app = new Elysia\([^)]*\)/,
|
|
||||||
"const app = new Elysia({ adapter: node() })",
|
|
||||||
);
|
|
||||||
|
|
||||||
await fs.writeFile(serverIndexPath, updatedContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,20 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { log, spinner } from "@clack/prompts";
|
import { spinner } from "@clack/prompts";
|
||||||
import consola from "consola";
|
import consola from "consola";
|
||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectPackageManager } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
export async function setupStarlight(
|
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
||||||
projectDir: string,
|
const { projectName, packageManager } = config;
|
||||||
packageManager: ProjectPackageManager,
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
): Promise<void> {
|
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
s.start("Setting up Starlight documentation site...");
|
s.start("Setting up Starlight docs...");
|
||||||
|
|
||||||
let cmd: string;
|
const starlightArgs = [
|
||||||
let args: string[];
|
|
||||||
|
|
||||||
switch (packageManager) {
|
|
||||||
case "npm":
|
|
||||||
cmd = "npx";
|
|
||||||
args = ["create-astro@latest"];
|
|
||||||
break;
|
|
||||||
case "pnpm":
|
|
||||||
cmd = "pnpm";
|
|
||||||
args = ["dlx", "create-astro@latest"];
|
|
||||||
break;
|
|
||||||
case "bun":
|
|
||||||
cmd = "bunx";
|
|
||||||
args = ["create-astro@latest"];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cmd = "npx";
|
|
||||||
args = ["create-astro@latest"];
|
|
||||||
}
|
|
||||||
|
|
||||||
args = [
|
|
||||||
...args,
|
|
||||||
"docs",
|
"docs",
|
||||||
"--template",
|
"--template",
|
||||||
"starlight",
|
"starlight",
|
||||||
@@ -46,17 +24,26 @@ export async function setupStarlight(
|
|||||||
"--no-git",
|
"--no-git",
|
||||||
"--skip-houston",
|
"--skip-houston",
|
||||||
];
|
];
|
||||||
|
const starlightArgsString = starlightArgs.join(" ");
|
||||||
|
|
||||||
await execa(cmd, args, {
|
const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
|
||||||
|
|
||||||
|
const starlightInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
commandWithArgs,
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(starlightInitCommand, {
|
||||||
cwd: path.join(projectDir, "apps"),
|
cwd: path.join(projectDir, "apps"),
|
||||||
env: {
|
env: {
|
||||||
CI: "true",
|
CI: "true",
|
||||||
},
|
},
|
||||||
|
shell: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
s.stop("Starlight documentation site setup successfully!");
|
s.stop("Starlight docs setup successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
s.stop(pc.red("Failed to set up Starlight documentation site"));
|
s.stop(pc.red("Failed to set up Starlight docs"));
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
consola.error(pc.red(error.message));
|
consola.error(pc.red(error.message));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { log, spinner } from "@clack/prompts";
|
import { spinner } from "@clack/prompts";
|
||||||
import { consola } from "consola";
|
import { consola } from "consola";
|
||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectFrontend, ProjectPackageManager } from "../types";
|
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
|
||||||
|
|
||||||
export async function setupTauri(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
packageManager: ProjectPackageManager,
|
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
||||||
frontends: ProjectFrontend[],
|
const { projectName, packageManager, frontend } = config;
|
||||||
): Promise<void> {
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export async function setupTauri(
|
|||||||
try {
|
try {
|
||||||
s.start("Setting up Tauri desktop app support...");
|
s.start("Setting up Tauri desktop app support...");
|
||||||
|
|
||||||
addPackageDependency({
|
await addPackageDependency({
|
||||||
devDependencies: ["@tauri-apps/cli"],
|
devDependencies: ["@tauri-apps/cli"],
|
||||||
projectDir: clientPackageDir,
|
projectDir: clientPackageDir,
|
||||||
});
|
});
|
||||||
@@ -41,48 +41,35 @@ export async function setupTauri(
|
|||||||
await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
|
await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd: string;
|
const hasReactRouter = frontend.includes("react-router");
|
||||||
let args: string[];
|
|
||||||
|
|
||||||
switch (packageManager) {
|
|
||||||
case "npm":
|
|
||||||
cmd = "npx";
|
|
||||||
args = ["@tauri-apps/cli@latest"];
|
|
||||||
break;
|
|
||||||
case "pnpm":
|
|
||||||
cmd = "pnpm";
|
|
||||||
args = ["dlx", "@tauri-apps/cli@latest"];
|
|
||||||
break;
|
|
||||||
case "bun":
|
|
||||||
cmd = "bunx";
|
|
||||||
args = ["@tauri-apps/cli@latest"];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cmd = "npx";
|
|
||||||
args = ["@tauri-apps/cli@latest"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasReactRouter = frontends.includes("react-router");
|
|
||||||
const devUrl = hasReactRouter
|
const devUrl = hasReactRouter
|
||||||
? "http://localhost:5173"
|
? "http://localhost:5173"
|
||||||
: "http://localhost:3001";
|
: "http://localhost:3001";
|
||||||
|
|
||||||
args = [
|
const tauriArgs = [
|
||||||
...args,
|
|
||||||
"init",
|
"init",
|
||||||
`--app-name=${path.basename(projectDir)}`,
|
`--app-name=${path.basename(projectDir)}`,
|
||||||
`--window-title=${path.basename(projectDir)}`,
|
`--window-title=${path.basename(projectDir)}`,
|
||||||
"--frontend-dist=../dist",
|
"--frontend-dist=../dist",
|
||||||
`--dev-url=${devUrl}`,
|
`--dev-url=${devUrl}`,
|
||||||
`--before-dev-command=${packageManager} run dev`,
|
`--before-dev-command=\"${packageManager} run dev\"`,
|
||||||
`--before-build-command=${packageManager} run build`,
|
`--before-build-command=\"${packageManager} run build\"`,
|
||||||
];
|
];
|
||||||
|
const tauriArgsString = tauriArgs.join(" ");
|
||||||
|
|
||||||
await execa(cmd, args, {
|
const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
|
||||||
|
|
||||||
|
const tauriInitCommand = getPackageExecutionCommand(
|
||||||
|
packageManager,
|
||||||
|
commandWithArgs,
|
||||||
|
);
|
||||||
|
|
||||||
|
await execa(tauriInitCommand, {
|
||||||
cwd: clientPackageDir,
|
cwd: clientPackageDir,
|
||||||
env: {
|
env: {
|
||||||
CI: "true",
|
CI: "true",
|
||||||
},
|
},
|
||||||
|
shell: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
s.stop("Tauri desktop app support configured successfully!");
|
s.stop("Tauri desktop app support configured successfully!");
|
||||||
|
|||||||
@@ -1,493 +1,538 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import consola from "consola";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
|
import { globby } from "globby";
|
||||||
|
import pc from "picocolors";
|
||||||
import { PKG_ROOT } from "../constants";
|
import { PKG_ROOT } from "../constants";
|
||||||
import type {
|
import type { ProjectConfig } from "../types";
|
||||||
ProjectBackend,
|
import { processTemplate } from "../utils/template-processor";
|
||||||
ProjectDatabase,
|
|
||||||
ProjectFrontend,
|
|
||||||
ProjectOrm,
|
|
||||||
} from "../types";
|
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
|
||||||
|
|
||||||
/**
|
async function processAndCopyFiles(
|
||||||
* Copy base template structure but exclude app-specific folders that will be added based on options
|
sourcePattern: string | string[],
|
||||||
*/
|
baseSourceDir: string,
|
||||||
export async function copyBaseTemplate(projectDir: string): Promise<void> {
|
destDir: string,
|
||||||
const templateDir = path.join(PKG_ROOT, "template/base");
|
context: ProjectConfig,
|
||||||
|
overwrite = true,
|
||||||
|
): Promise<void> {
|
||||||
|
const sourceFiles = await globby(sourcePattern, {
|
||||||
|
cwd: baseSourceDir,
|
||||||
|
dot: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
absolute: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (!(await fs.pathExists(templateDir))) {
|
for (const relativeSrcPath of sourceFiles) {
|
||||||
throw new Error(`Template directory not found: ${templateDir}`);
|
const srcPath = path.join(baseSourceDir, relativeSrcPath);
|
||||||
}
|
let relativeDestPath = relativeSrcPath;
|
||||||
|
|
||||||
await fs.ensureDir(projectDir);
|
if (relativeSrcPath.endsWith(".hbs")) {
|
||||||
|
relativeDestPath = relativeSrcPath.slice(0, -4);
|
||||||
|
}
|
||||||
|
|
||||||
const rootFiles = await fs.readdir(templateDir);
|
const destPath = path.join(destDir, relativeDestPath);
|
||||||
for (const file of rootFiles) {
|
|
||||||
const srcPath = path.join(templateDir, file);
|
|
||||||
const destPath = path.join(projectDir, file);
|
|
||||||
|
|
||||||
if (file === "apps") continue;
|
await fs.ensureDir(path.dirname(destPath));
|
||||||
|
|
||||||
if (await fs.stat(srcPath).then((stat) => stat.isDirectory())) {
|
if (srcPath.endsWith(".hbs")) {
|
||||||
await fs.copy(srcPath, destPath);
|
await processTemplate(srcPath, destPath, context);
|
||||||
} else {
|
} else {
|
||||||
await fs.copy(srcPath, destPath);
|
if (!overwrite && (await fs.pathExists(destPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await fs.copy(srcPath, destPath, { overwrite: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fs.ensureDir(path.join(projectDir, "apps"));
|
export async function copyBaseTemplate(
|
||||||
|
projectDir: string,
|
||||||
const serverSrcDir = path.join(templateDir, "apps/server");
|
context: ProjectConfig,
|
||||||
const serverDestDir = path.join(projectDir, "apps/server");
|
): Promise<void> {
|
||||||
if (await fs.pathExists(serverSrcDir)) {
|
const templateDir = path.join(PKG_ROOT, "templates/base");
|
||||||
await fs.copy(serverSrcDir, serverDestDir);
|
await processAndCopyFiles(
|
||||||
}
|
["package.json", "_gitignore"],
|
||||||
|
templateDir,
|
||||||
|
projectDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupFrontendTemplates(
|
export async function setupFrontendTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
frontends: ProjectFrontend[],
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const hasTanstackWeb = frontends.includes("tanstack-router");
|
const webFrontends = context.frontend.filter(
|
||||||
const hasTanstackStart = frontends.includes("tanstack-start");
|
(f) =>
|
||||||
const hasReactRouterWeb = frontends.includes("react-router");
|
f === "tanstack-router" ||
|
||||||
const hasNextWeb = frontends.includes("next");
|
f === "react-router" ||
|
||||||
const hasNative = frontends.includes("native");
|
f === "tanstack-start" ||
|
||||||
|
f === "next",
|
||||||
|
);
|
||||||
|
const hasNative = context.frontend.includes("native");
|
||||||
|
|
||||||
if (hasTanstackWeb || hasReactRouterWeb || hasTanstackStart || hasNextWeb) {
|
if (webFrontends.length > 0) {
|
||||||
const webDir = path.join(projectDir, "apps/web");
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
await fs.ensureDir(webDir);
|
await fs.ensureDir(webAppDir);
|
||||||
|
|
||||||
const webBaseDir = path.join(PKG_ROOT, "template/base/apps/web-base");
|
const webBaseDir = path.join(PKG_ROOT, "templates/frontend/web-base");
|
||||||
if (await fs.pathExists(webBaseDir)) {
|
if (await fs.pathExists(webBaseDir)) {
|
||||||
await fs.copy(webBaseDir, webDir);
|
await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasTanstackWeb) {
|
for (const framework of webFrontends) {
|
||||||
const frameworkDir = path.join(
|
const frameworkSrcDir = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
"template/base/apps/web-tanstack-router",
|
`templates/frontend/${framework}`,
|
||||||
);
|
);
|
||||||
if (await fs.pathExists(frameworkDir)) {
|
if (await fs.pathExists(frameworkSrcDir)) {
|
||||||
await fs.copy(frameworkDir, webDir, { overwrite: true });
|
await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
|
||||||
}
|
|
||||||
} else if (hasTanstackStart) {
|
|
||||||
const frameworkDir = path.join(
|
|
||||||
PKG_ROOT,
|
|
||||||
"template/base/apps/web-tanstack-start",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(frameworkDir)) {
|
|
||||||
await fs.copy(frameworkDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
} else if (hasReactRouterWeb) {
|
|
||||||
const frameworkDir = path.join(
|
|
||||||
PKG_ROOT,
|
|
||||||
"template/base/apps/web-react-router",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(frameworkDir)) {
|
|
||||||
await fs.copy(frameworkDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
} else if (hasNextWeb) {
|
|
||||||
const frameworkDir = path.join(PKG_ROOT, "template/base/apps/web-next");
|
|
||||||
if (await fs.pathExists(frameworkDir)) {
|
|
||||||
await fs.copy(frameworkDir, webDir, { overwrite: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageJsonPath = path.join(webDir, "package.json");
|
if (context.api !== "none") {
|
||||||
if (await fs.pathExists(packageJsonPath)) {
|
const webFramework = webFrontends[0];
|
||||||
const packageJson = await fs.readJson(packageJsonPath);
|
|
||||||
packageJson.name = "web";
|
const apiWebBaseDir = path.join(
|
||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/web/base`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiWebBaseDir)) {
|
||||||
|
await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiWebFrameworkDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/web/${webFramework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiWebFrameworkDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
apiWebFrameworkDir,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNative) {
|
if (hasNative) {
|
||||||
const nativeSrcDir = path.join(PKG_ROOT, "template/base/apps/native");
|
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||||
const nativeDestDir = path.join(projectDir, "apps/native");
|
await fs.ensureDir(nativeAppDir);
|
||||||
|
|
||||||
if (await fs.pathExists(nativeSrcDir)) {
|
const nativeBaseDir = path.join(PKG_ROOT, "templates/frontend/native");
|
||||||
await fs.copy(nativeSrcDir, nativeDestDir);
|
if (await fs.pathExists(nativeBaseDir)) {
|
||||||
|
await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(
|
if (context.api !== "none") {
|
||||||
path.join(projectDir, ".npmrc"),
|
const apiNativeSrcDir = path.join(
|
||||||
"node-linker=hoisted\n",
|
PKG_ROOT,
|
||||||
);
|
`templates/api/${context.api}/native`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await fs.pathExists(apiNativeSrcDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
apiNativeSrcDir,
|
||||||
|
nativeAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupBackendFramework(
|
export async function setupBackendFramework(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
framework: ProjectBackend,
|
context: ProjectConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (framework === "next") {
|
if ((context.backend as string) === "none") return;
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
|
||||||
const nextTemplateDir = path.join(
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
PKG_ROOT,
|
await fs.ensureDir(serverAppDir);
|
||||||
"template/with-next/apps/server",
|
|
||||||
|
const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server-base");
|
||||||
|
if (await fs.pathExists(serverBaseDir)) {
|
||||||
|
await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(`Warning: server-base template not found at ${serverBaseDir}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.ensureDir(serverDir);
|
|
||||||
|
|
||||||
if (await fs.pathExists(nextTemplateDir)) {
|
|
||||||
await fs.copy(nextTemplateDir, serverDir, { overwrite: true });
|
|
||||||
|
|
||||||
const packageJsonPath = path.join(serverDir, "package.json");
|
|
||||||
if (await fs.pathExists(packageJsonPath)) {
|
|
||||||
const packageJson = await fs.readJson(packageJsonPath);
|
|
||||||
packageJson.name = "server";
|
|
||||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
|
const frameworkSrcDir = path.join(
|
||||||
if (await fs.pathExists(frameworkDir)) {
|
PKG_ROOT,
|
||||||
await fs.copy(frameworkDir, projectDir, { overwrite: true });
|
`templates/backend/${context.backend}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(frameworkSrcDir)) {
|
||||||
|
await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Backend template directory not found, skipping: ${frameworkSrcDir}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.api !== "none") {
|
||||||
|
const apiServerBaseDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/server/base`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiServerBaseDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
apiServerBaseDir,
|
||||||
|
serverAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiServerFrameworkDir = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/api/${context.api}/server/${context.backend}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(apiServerFrameworkDir)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
apiServerFrameworkDir,
|
||||||
|
serverAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupOrmTemplate(
|
export async function setupDbOrmTemplates(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
orm: ProjectOrm,
|
context: ProjectConfig,
|
||||||
database: ProjectDatabase,
|
|
||||||
auth: boolean,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (orm === "none" || database === "none") return;
|
if (context.orm === "none" || context.database === "none") return;
|
||||||
|
|
||||||
const ormTemplateDir = path.join(PKG_ROOT, getOrmTemplateDir(orm, database));
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
|
await fs.ensureDir(serverAppDir);
|
||||||
|
|
||||||
if (await fs.pathExists(ormTemplateDir)) {
|
const dbOrmSrcDir = path.join(
|
||||||
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
|
PKG_ROOT,
|
||||||
|
`templates/db/${context.orm}/${context.database}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!auth) {
|
if (await fs.pathExists(dbOrmSrcDir)) {
|
||||||
if (orm === "prisma") {
|
await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
|
||||||
const authSchemaPath = path.join(
|
} else {
|
||||||
projectDir,
|
consola.warn(
|
||||||
"apps/server/prisma/schema/auth.prisma",
|
pc.yellow(
|
||||||
);
|
`Warning: Database/ORM template directory not found, skipping: ${dbOrmSrcDir}`,
|
||||||
if (await fs.pathExists(authSchemaPath)) {
|
),
|
||||||
await fs.remove(authSchemaPath);
|
);
|
||||||
}
|
|
||||||
} else if (orm === "drizzle") {
|
|
||||||
const authSchemaPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"apps/server/src/db/schema/auth.ts",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(authSchemaPath)) {
|
|
||||||
await fs.remove(authSchemaPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupAuthTemplate(
|
export async function setupAuthTemplate(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
auth: boolean,
|
context: ProjectConfig,
|
||||||
framework: ProjectBackend,
|
|
||||||
orm: ProjectOrm,
|
|
||||||
database: ProjectDatabase,
|
|
||||||
frontends: ProjectFrontend[],
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!auth) return;
|
if (!context.auth) return;
|
||||||
|
|
||||||
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
if (await fs.pathExists(authTemplateDir)) {
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
const hasReactRouter = frontends.includes("react-router");
|
const nativeAppDir = path.join(projectDir, "apps/native");
|
||||||
const hasTanStackRouter = frontends.includes("tanstack-router");
|
const webFrontends = context.frontend.filter(
|
||||||
const hasTanStackStart = frontends.includes("tanstack-start");
|
(f) =>
|
||||||
const hasNextRouter = frontends.includes("next");
|
f === "tanstack-router" ||
|
||||||
|
f === "react-router" ||
|
||||||
|
f === "tanstack-start" ||
|
||||||
|
f === "next",
|
||||||
|
);
|
||||||
|
const hasNative = context.frontend.includes("native");
|
||||||
|
|
||||||
if (
|
if (await fs.pathExists(serverAppDir)) {
|
||||||
hasReactRouter ||
|
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
|
||||||
hasTanStackRouter ||
|
if (await fs.pathExists(authServerBaseSrc)) {
|
||||||
hasTanStackStart ||
|
await processAndCopyFiles(
|
||||||
hasNextRouter
|
"**/*",
|
||||||
) {
|
authServerBaseSrc,
|
||||||
const webDir = path.join(projectDir, "apps/web");
|
serverAppDir,
|
||||||
|
context,
|
||||||
const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base");
|
);
|
||||||
if (await fs.pathExists(webBaseAuthDir)) {
|
} else {
|
||||||
await fs.copy(webBaseAuthDir, webDir, { overwrite: true });
|
consola.warn(
|
||||||
}
|
pc.yellow(
|
||||||
|
`Warning: Base auth server template not found at ${authServerBaseSrc}`,
|
||||||
if (hasReactRouter) {
|
),
|
||||||
const reactRouterAuthDir = path.join(
|
);
|
||||||
authTemplateDir,
|
|
||||||
"apps/web-react-router",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(reactRouterAuthDir)) {
|
|
||||||
await fs.copy(reactRouterAuthDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTanStackRouter) {
|
|
||||||
const tanstackAuthDir = path.join(
|
|
||||||
authTemplateDir,
|
|
||||||
"apps/web-tanstack-router",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(tanstackAuthDir)) {
|
|
||||||
await fs.copy(tanstackAuthDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTanStackStart) {
|
|
||||||
const tanstackStartAuthDir = path.join(
|
|
||||||
authTemplateDir,
|
|
||||||
"apps/web-tanstack-start",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(tanstackStartAuthDir)) {
|
|
||||||
await fs.copy(tanstackStartAuthDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNextRouter) {
|
|
||||||
const nextAuthDir = path.join(authTemplateDir, "apps/web-next");
|
|
||||||
if (await fs.pathExists(nextAuthDir)) {
|
|
||||||
await fs.copy(nextAuthDir, webDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverAuthDir = path.join(authTemplateDir, "apps/server/src");
|
const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
|
||||||
const projectServerDir = path.join(projectDir, "apps/server/src");
|
if (await fs.pathExists(authServerNextSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
await fs.copy(
|
"**/*",
|
||||||
path.join(serverAuthDir, "lib/trpc.ts"),
|
authServerNextSrc,
|
||||||
path.join(projectServerDir, "lib/trpc.ts"),
|
serverAppDir,
|
||||||
{ overwrite: true },
|
context,
|
||||||
);
|
|
||||||
|
|
||||||
await fs.copy(
|
|
||||||
path.join(serverAuthDir, "routers/index.ts"),
|
|
||||||
path.join(projectServerDir, "routers/index.ts"),
|
|
||||||
{ overwrite: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (framework === "next") {
|
|
||||||
if (
|
|
||||||
await fs.pathExists(
|
|
||||||
path.join(authTemplateDir, "apps/server/src/with-next-app"),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const nextAppAuthDir = path.join(
|
|
||||||
authTemplateDir,
|
|
||||||
"apps/server/src/with-next-app",
|
|
||||||
);
|
|
||||||
const nextAppDestDir = path.join(projectDir, "apps/server/src/app");
|
|
||||||
|
|
||||||
await fs.ensureDir(nextAppDestDir);
|
|
||||||
|
|
||||||
const files = await fs.readdir(nextAppAuthDir);
|
|
||||||
for (const file of files) {
|
|
||||||
const srcPath = path.join(nextAppAuthDir, file);
|
|
||||||
const destPath = path.join(nextAppDestDir, file);
|
|
||||||
await fs.copy(srcPath, destPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextFileName = "with-next-context.ts";
|
|
||||||
await fs.copy(
|
|
||||||
path.join(serverAuthDir, "lib", contextFileName),
|
|
||||||
path.join(projectServerDir, "lib/context.ts"),
|
|
||||||
{ overwrite: true },
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Next auth server template not found at ${authServerNextSrc}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const authLibFileName = getAuthLibDir(orm, database);
|
if (context.orm !== "none" && context.database !== "none") {
|
||||||
const authLibSourceDir = path.join(serverAuthDir, authLibFileName);
|
const orm = context.orm;
|
||||||
if (await fs.pathExists(authLibSourceDir)) {
|
const db = context.database;
|
||||||
const files = await fs.readdir(authLibSourceDir);
|
let authDbSrc = "";
|
||||||
for (const file of files) {
|
if (orm === "drizzle") {
|
||||||
await fs.copy(
|
authDbSrc = path.join(
|
||||||
path.join(authLibSourceDir, file),
|
PKG_ROOT,
|
||||||
path.join(projectServerDir, "lib", file),
|
`templates/auth/server/db/drizzle/${db}`,
|
||||||
{ overwrite: true },
|
);
|
||||||
);
|
} else if (orm === "prisma") {
|
||||||
}
|
authDbSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/server/db/prisma/${db}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
|
||||||
|
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Auth template for ${orm}/${db} not found at ${authDbSrc}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
"Warning: apps/server directory does not exist, skipping server-side auth setup.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webFrontends.length > 0 && (await fs.pathExists(webAppDir))) {
|
||||||
|
const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/base");
|
||||||
|
if (await fs.pathExists(authWebBaseSrc)) {
|
||||||
|
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Base auth web template not found at ${authWebBaseSrc}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const framework of webFrontends) {
|
||||||
|
const authWebFrameworkSrc = path.join(
|
||||||
|
PKG_ROOT,
|
||||||
|
`templates/auth/web/${framework}`,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(authWebFrameworkSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
authWebFrameworkSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Auth web template for ${framework} not found at ${authWebFrameworkSrc}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNative && (await fs.pathExists(nativeAppDir))) {
|
||||||
|
const authNativeSrc = path.join(PKG_ROOT, "templates/auth/native");
|
||||||
|
if (await fs.pathExists(authNativeSrc)) {
|
||||||
|
await processAndCopyFiles("**/*", authNativeSrc, nativeAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
`Warning: Auth native template not found at ${authNativeSrc}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupAddonsTemplate(
|
||||||
|
projectDir: string,
|
||||||
|
context: ProjectConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
if (context.addons.includes("turborepo")) {
|
||||||
|
const turboSrcDir = path.join(PKG_ROOT, "templates/addons/turborepo");
|
||||||
|
if (await fs.pathExists(turboSrcDir)) {
|
||||||
|
await processAndCopyFiles("**/*", turboSrcDir, projectDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(pc.yellow("Warning: Turborepo addon template not found."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.addons.includes("husky")) {
|
||||||
|
const huskySrcDir = path.join(PKG_ROOT, "templates/addons/husky");
|
||||||
|
if (await fs.pathExists(huskySrcDir)) {
|
||||||
|
await processAndCopyFiles("**/*", huskySrcDir, projectDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(pc.yellow("Warning: Husky addon template not found."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.addons.includes("biome")) {
|
||||||
|
const biomeSrcDir = path.join(PKG_ROOT, "templates/addons/biome");
|
||||||
|
if (await fs.pathExists(biomeSrcDir)) {
|
||||||
|
await processAndCopyFiles("**/*", biomeSrcDir, projectDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(pc.yellow("Warning: Biome addon template not found."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.addons.includes("pwa")) {
|
||||||
|
const pwaSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web");
|
||||||
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
|
if (await fs.pathExists(pwaSrcDir)) {
|
||||||
|
if (await fs.pathExists(webAppDir)) {
|
||||||
|
await processAndCopyFiles("**/*", pwaSrcDir, webAppDir, context);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow(
|
||||||
|
"Warning: apps/web directory not found, cannot setup PWA addon.",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const contextFileName = `with-${framework}-context.ts`;
|
consola.warn(pc.yellow("Warning: PWA addon template not found."));
|
||||||
await fs.copy(
|
}
|
||||||
path.join(serverAuthDir, "lib", contextFileName),
|
}
|
||||||
path.join(projectServerDir, "lib/context.ts"),
|
}
|
||||||
{ overwrite: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const indexFileName = `with-${framework}-index.ts`;
|
export async function setupExamplesTemplate(
|
||||||
await fs.copy(
|
projectDir: string,
|
||||||
path.join(serverAuthDir, indexFileName),
|
context: ProjectConfig,
|
||||||
path.join(projectServerDir, "index.ts"),
|
): Promise<void> {
|
||||||
{ overwrite: true },
|
if (!context.examples || context.examples.length === 0) return;
|
||||||
);
|
|
||||||
|
|
||||||
const authLibFileName = getAuthLibDir(orm, database);
|
const serverAppDir = path.join(projectDir, "apps/server");
|
||||||
const authLibSourceDir = path.join(serverAuthDir, authLibFileName);
|
const webAppDir = path.join(projectDir, "apps/web");
|
||||||
if (await fs.pathExists(authLibSourceDir)) {
|
|
||||||
const files = await fs.readdir(authLibSourceDir);
|
for (const example of context.examples) {
|
||||||
for (const file of files) {
|
const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
|
||||||
await fs.copy(
|
|
||||||
path.join(authLibSourceDir, file),
|
if (await fs.pathExists(serverAppDir)) {
|
||||||
path.join(projectServerDir, "lib", file),
|
const exampleServerSrc = path.join(exampleBaseDir, "server");
|
||||||
{ overwrite: true },
|
if (await fs.pathExists(exampleServerSrc)) {
|
||||||
|
if (context.orm !== "none") {
|
||||||
|
const exampleOrmBaseSrc = path.join(
|
||||||
|
exampleServerSrc,
|
||||||
|
context.orm,
|
||||||
|
"base",
|
||||||
);
|
);
|
||||||
|
if (await fs.pathExists(exampleOrmBaseSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
exampleOrmBaseSrc,
|
||||||
|
serverAppDir,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.database !== "none") {
|
||||||
|
const exampleDbSchemaSrc = path.join(
|
||||||
|
exampleServerSrc,
|
||||||
|
context.orm,
|
||||||
|
context.database,
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(exampleDbSchemaSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
exampleDbSchemaSrc,
|
||||||
|
serverAppDir,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frontends.includes("native")) {
|
if (await fs.pathExists(webAppDir)) {
|
||||||
const nativeAuthDir = path.join(authTemplateDir, "apps/native");
|
const exampleWebSrc = path.join(exampleBaseDir, "web");
|
||||||
const projectNativeDir = path.join(projectDir, "apps/native");
|
if (await fs.pathExists(exampleWebSrc)) {
|
||||||
|
const webFrameworks = context.frontend.filter((f) =>
|
||||||
if (await fs.pathExists(nativeAuthDir)) {
|
[
|
||||||
await fs.copy(nativeAuthDir, projectNativeDir, { overwrite: true });
|
"next",
|
||||||
|
"react-router",
|
||||||
|
"tanstack-router",
|
||||||
|
"tanstack-start",
|
||||||
|
].includes(f),
|
||||||
|
);
|
||||||
|
for (const framework of webFrameworks) {
|
||||||
|
const exampleWebFrameworkSrc = path.join(exampleWebSrc, framework);
|
||||||
|
if (await fs.pathExists(exampleWebFrameworkSrc)) {
|
||||||
|
await processAndCopyFiles(
|
||||||
|
"**/*",
|
||||||
|
exampleWebFrameworkSrc,
|
||||||
|
webAppDir,
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addPackageDependency({
|
|
||||||
dependencies: ["@better-auth/expo"],
|
|
||||||
projectDir: path.join(projectDir, "apps/server"),
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateAuthConfigWithExpoPlugin(projectDir, orm, database);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to find a better way to handle this
|
export async function fixGitignoreFiles(
|
||||||
async function updateAuthConfigWithExpoPlugin(
|
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
orm: ProjectOrm,
|
context: ProjectConfig,
|
||||||
database: ProjectDatabase,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const serverDir = path.join(projectDir, "apps/server");
|
const gitignoreFiles = await globby(["**/.gitignore.hbs", "**/_gitignore"], {
|
||||||
|
cwd: projectDir,
|
||||||
|
dot: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
absolute: true,
|
||||||
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
||||||
|
});
|
||||||
|
|
||||||
let authFilePath: string | undefined;
|
for (const currentPath of gitignoreFiles) {
|
||||||
if (orm === "drizzle") {
|
const dir = path.dirname(currentPath);
|
||||||
if (database === "sqlite") {
|
const filename = path.basename(currentPath);
|
||||||
authFilePath = path.join(serverDir, "src/lib/auth.ts");
|
const destPath = path.join(dir, ".gitignore");
|
||||||
} else if (database === "postgres") {
|
|
||||||
authFilePath = path.join(serverDir, "src/lib/auth.ts");
|
|
||||||
}
|
|
||||||
} else if (orm === "prisma") {
|
|
||||||
if (database === "sqlite") {
|
|
||||||
authFilePath = path.join(serverDir, "src/lib/auth.ts");
|
|
||||||
} else if (database === "postgres") {
|
|
||||||
authFilePath = path.join(serverDir, "src/lib/auth.ts");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authFilePath && (await fs.pathExists(authFilePath))) {
|
try {
|
||||||
let authFileContent = await fs.readFile(authFilePath, "utf8");
|
if (filename === ".gitignore.hbs") {
|
||||||
|
await processTemplate(currentPath, destPath, context);
|
||||||
if (!authFileContent.includes("@better-auth/expo")) {
|
await fs.remove(currentPath);
|
||||||
const importLine = 'import { expo } from "@better-auth/expo";\n';
|
} else if (filename === "_gitignore") {
|
||||||
|
await fs.move(currentPath, destPath, { overwrite: true });
|
||||||
const lastImportIndex = authFileContent.lastIndexOf("import");
|
|
||||||
const afterLastImport =
|
|
||||||
authFileContent.indexOf("\n", lastImportIndex) + 1;
|
|
||||||
|
|
||||||
authFileContent =
|
|
||||||
authFileContent.substring(0, afterLastImport) +
|
|
||||||
importLine +
|
|
||||||
authFileContent.substring(afterLastImport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authFileContent.includes("plugins:")) {
|
|
||||||
authFileContent = authFileContent.replace(
|
|
||||||
/}\);/,
|
|
||||||
" plugins: [expo()],\n});",
|
|
||||||
);
|
|
||||||
} else if (!authFileContent.includes("expo()")) {
|
|
||||||
authFileContent = authFileContent.replace(
|
|
||||||
/plugins: \[(.*?)\]/s,
|
|
||||||
(match, plugins) => {
|
|
||||||
return `plugins: [${plugins}${plugins.trim() ? ", " : ""}expo()]`;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authFileContent.includes("my-better-t-app://")) {
|
|
||||||
authFileContent = authFileContent.replace(
|
|
||||||
/trustedOrigins: \[(.*?)\]/s,
|
|
||||||
(match, origins) => {
|
|
||||||
return `trustedOrigins: [${origins}${origins.trim() ? ", " : ""}"my-better-t-app://"]`;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(authFilePath, authFileContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fixGitignoreFiles(projectDir: string): Promise<void> {
|
|
||||||
const gitignorePaths = await findGitignoreFiles(projectDir);
|
|
||||||
|
|
||||||
for (const gitignorePath of gitignorePaths) {
|
|
||||||
if (await fs.pathExists(gitignorePath)) {
|
|
||||||
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
|
|
||||||
await fs.move(gitignorePath, targetPath, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all _gitignore files in the project recursively
|
|
||||||
*/
|
|
||||||
async function findGitignoreFiles(dir: string): Promise<string[]> {
|
|
||||||
const gitignoreFiles: string[] = [];
|
|
||||||
|
|
||||||
const gitignorePath = path.join(dir, "_gitignore");
|
|
||||||
if (await fs.pathExists(gitignorePath)) {
|
|
||||||
gitignoreFiles.push(gitignorePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
||||||
const subDirPath = path.join(dir, entry.name);
|
|
||||||
const subDirFiles = await findGitignoreFiles(subDirPath);
|
|
||||||
gitignoreFiles.push(...subDirFiles);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
consola.error(`Error processing gitignore file ${currentPath}:`, error);
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
}
|
||||||
|
|
||||||
return gitignoreFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
export async function handleExtras(
|
||||||
if (orm === "drizzle") {
|
projectDir: string,
|
||||||
if (database === "sqlite") return "template/with-drizzle-sqlite";
|
context: ProjectConfig,
|
||||||
if (database === "postgres") return "template/with-drizzle-postgres";
|
): Promise<void> {
|
||||||
if (database === "mysql") return "template/with-drizzle-mysql";
|
if (context.packageManager === "pnpm") {
|
||||||
|
const src = path.join(PKG_ROOT, "templates/extras/pnpm-workspace.yaml");
|
||||||
|
const dest = path.join(projectDir, "pnpm-workspace.yaml");
|
||||||
|
if (await fs.pathExists(src)) {
|
||||||
|
await fs.copy(src, dest);
|
||||||
|
} else {
|
||||||
|
consola.warn(
|
||||||
|
pc.yellow("Warning: pnpm-workspace.yaml template not found."),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orm === "prisma") {
|
|
||||||
if (database === "sqlite") return "template/with-prisma-sqlite";
|
|
||||||
if (database === "postgres") return "template/with-prisma-postgres";
|
|
||||||
if (database === "mysql") return "template/with-prisma-mysql";
|
|
||||||
if (database === "mongodb") return "template/with-prisma-mongodb";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "template/base";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
|
||||||
if (orm === "drizzle") {
|
|
||||||
if (database === "sqlite") return "with-drizzle-sqlite-lib";
|
|
||||||
if (database === "postgres") return "with-drizzle-postgres-lib";
|
|
||||||
if (database === "mysql") return "with-drizzle-mysql-lib";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orm === "prisma") {
|
|
||||||
if (database === "sqlite") return "with-prisma-sqlite-lib";
|
|
||||||
if (database === "postgres") return "with-prisma-postgres-lib";
|
|
||||||
if (database === "mysql") return "with-prisma-mysql-lib";
|
|
||||||
if (database === "mongodb") return "with-prisma-mongodb-lib";
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Invalid ORM or database configuration for auth setup");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,29 +202,22 @@ DATABASE_URL=your_database_url
|
|||||||
DATABASE_AUTH_TOKEN=your_auth_token`);
|
DATABASE_AUTH_TOKEN=your_auth_token`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupTurso(
|
import type { ProjectConfig } from "../types";
|
||||||
projectDir: string,
|
|
||||||
shouldSetupTurso: boolean,
|
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
||||||
) {
|
const { projectName, orm } = config;
|
||||||
|
const projectDir = path.resolve(process.cwd(), projectName);
|
||||||
|
const isDrizzle = orm === "drizzle";
|
||||||
const setupSpinner = spinner();
|
const setupSpinner = spinner();
|
||||||
setupSpinner.start("Setting up Turso database");
|
setupSpinner.start("Setting up Turso database");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!shouldSetupTurso) {
|
|
||||||
setupSpinner.stop("Skipping Turso setup");
|
|
||||||
await writeEnvFile(projectDir);
|
|
||||||
log.info(
|
|
||||||
pc.blue("Skipping Turso setup. Setting up empty configuration."),
|
|
||||||
);
|
|
||||||
displayManualSetupInstructions();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
const isMac = platform === "darwin";
|
const isMac = platform === "darwin";
|
||||||
const canInstallCLI = platform !== "win32";
|
const isLinux = platform === "linux";
|
||||||
|
const isWindows = platform === "win32";
|
||||||
|
|
||||||
if (!canInstallCLI) {
|
if (isWindows) {
|
||||||
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
||||||
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
|
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
|
||||||
await writeEnvFile(projectDir);
|
await writeEnvFile(projectDir);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createProject } from "./helpers/create-project";
|
|||||||
import { gatherConfig } from "./prompts/config-prompts";
|
import { gatherConfig } from "./prompts/config-prompts";
|
||||||
import type {
|
import type {
|
||||||
ProjectAddons,
|
ProjectAddons,
|
||||||
|
ProjectApi,
|
||||||
ProjectBackend,
|
ProjectBackend,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ProjectDBSetup,
|
ProjectDBSetup,
|
||||||
@@ -17,33 +18,13 @@ import type {
|
|||||||
ProjectOrm,
|
ProjectOrm,
|
||||||
ProjectPackageManager,
|
ProjectPackageManager,
|
||||||
ProjectRuntime,
|
ProjectRuntime,
|
||||||
|
YargsArgv,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { displayConfig } from "./utils/display-config";
|
import { displayConfig } from "./utils/display-config";
|
||||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
||||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||||
import { renderTitle } from "./utils/render-title";
|
import { renderTitle } from "./utils/render-title";
|
||||||
|
|
||||||
type YargsArgv = {
|
|
||||||
projectDirectory?: string;
|
|
||||||
|
|
||||||
yes?: boolean;
|
|
||||||
database?: ProjectDatabase;
|
|
||||||
orm?: ProjectOrm;
|
|
||||||
auth?: boolean;
|
|
||||||
frontend?: ProjectFrontend[];
|
|
||||||
addons?: ProjectAddons[];
|
|
||||||
examples?: ProjectExamples[];
|
|
||||||
git?: boolean;
|
|
||||||
packageManager?: ProjectPackageManager;
|
|
||||||
install?: boolean;
|
|
||||||
dbSetup?: ProjectDBSetup;
|
|
||||||
backend?: ProjectBackend;
|
|
||||||
runtime?: ProjectRuntime;
|
|
||||||
|
|
||||||
_: (string | number)[];
|
|
||||||
$0: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const exit = () => process.exit(0);
|
const exit = () => process.exit(0);
|
||||||
process.on("SIGINT", exit);
|
process.on("SIGINT", exit);
|
||||||
process.on("SIGTERM", exit);
|
process.on("SIGTERM", exit);
|
||||||
@@ -99,7 +80,15 @@ async function main() {
|
|||||||
type: "array",
|
type: "array",
|
||||||
string: true,
|
string: true,
|
||||||
describe: "Additional addons",
|
describe: "Additional addons",
|
||||||
choices: ["pwa", "tauri", "starlight", "biome", "husky", "none"],
|
choices: [
|
||||||
|
"pwa",
|
||||||
|
"tauri",
|
||||||
|
"starlight",
|
||||||
|
"biome",
|
||||||
|
"husky",
|
||||||
|
"turborepo",
|
||||||
|
"none",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.option("examples", {
|
.option("examples", {
|
||||||
type: "array",
|
type: "array",
|
||||||
@@ -119,7 +108,7 @@ async function main() {
|
|||||||
})
|
})
|
||||||
.option("install", {
|
.option("install", {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "Install dependencies (use --no-install to explicitly skip)",
|
describe: "Install dependencies",
|
||||||
})
|
})
|
||||||
.option("db-setup", {
|
.option("db-setup", {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -136,6 +125,11 @@ async function main() {
|
|||||||
describe: "Runtime",
|
describe: "Runtime",
|
||||||
choices: ["bun", "node"],
|
choices: ["bun", "node"],
|
||||||
})
|
})
|
||||||
|
.option("api", {
|
||||||
|
type: "string",
|
||||||
|
describe: "API type",
|
||||||
|
choices: ["trpc", "orpc"],
|
||||||
|
})
|
||||||
.completion()
|
.completion()
|
||||||
.recommendCommands()
|
.recommendCommands()
|
||||||
.version(getLatestCLIVersion())
|
.version(getLatestCLIVersion())
|
||||||
@@ -188,7 +182,9 @@ async function main() {
|
|||||||
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
|
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
outro(
|
outro(
|
||||||
pc.magenta(
|
pc.magenta(
|
||||||
`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`,
|
`Project created successfully in ${pc.bold(
|
||||||
|
elapsedTimeInSeconds,
|
||||||
|
)} seconds!`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -214,10 +210,10 @@ function processAndValidateFlags(
|
|||||||
): Partial<ProjectConfig> {
|
): Partial<ProjectConfig> {
|
||||||
const config: Partial<ProjectConfig> = {};
|
const config: Partial<ProjectConfig> = {};
|
||||||
|
|
||||||
|
// --- Database and ORM validation ---
|
||||||
if (options.database) {
|
if (options.database) {
|
||||||
config.database = options.database as ProjectDatabase;
|
config.database = options.database as ProjectDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.orm) {
|
if (options.orm) {
|
||||||
if (options.orm === "none") {
|
if (options.orm === "none") {
|
||||||
config.orm = "none";
|
config.orm = "none";
|
||||||
@@ -225,7 +221,6 @@ function processAndValidateFlags(
|
|||||||
config.orm = options.orm as ProjectOrm;
|
config.orm = options.orm as ProjectOrm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(config.database ?? options.database) === "mongodb" &&
|
(config.database ?? options.database) === "mongodb" &&
|
||||||
(config.orm ?? options.orm) === "drizzle"
|
(config.orm ?? options.orm) === "drizzle"
|
||||||
@@ -332,7 +327,6 @@ function processAndValidateFlags(
|
|||||||
if (options.backend) {
|
if (options.backend) {
|
||||||
config.backend = options.backend as ProjectBackend;
|
config.backend = options.backend as ProjectBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.runtime) {
|
if (options.runtime) {
|
||||||
config.runtime = options.runtime as ProjectRuntime;
|
config.runtime = options.runtime as ProjectRuntime;
|
||||||
}
|
}
|
||||||
@@ -353,12 +347,13 @@ function processAndValidateFlags(
|
|||||||
(f) =>
|
(f) =>
|
||||||
f === "tanstack-router" ||
|
f === "tanstack-router" ||
|
||||||
f === "react-router" ||
|
f === "react-router" ||
|
||||||
f === "tanstack-start",
|
f === "tanstack-start" ||
|
||||||
|
f === "next",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (webFrontends.length > 1) {
|
if (webFrontends.length > 1) {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router",
|
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -366,6 +361,34 @@ function processAndValidateFlags(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.api) {
|
||||||
|
config.api = options.api as ProjectApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveFrontend =
|
||||||
|
config.frontend ??
|
||||||
|
(options.frontend?.filter((f) => f !== "none") as ProjectFrontend[]) ??
|
||||||
|
(options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
||||||
|
|
||||||
|
const includesNative = effectiveFrontend?.includes("native");
|
||||||
|
|
||||||
|
const effectiveApi =
|
||||||
|
config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined);
|
||||||
|
|
||||||
|
if (includesNative && effectiveApi === "orpc") {
|
||||||
|
consola.fatal(
|
||||||
|
`oRPC API is not supported when using the 'native' frontend. Please use --api trpc or remove 'native' from --frontend.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includesNative && effectiveApi !== "trpc") {
|
||||||
|
if (!options.api || (options.yes && options.api !== "orpc")) {
|
||||||
|
config.api = "trpc";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Addons validation ---
|
||||||
if (options.addons && options.addons.length > 0) {
|
if (options.addons && options.addons.length > 0) {
|
||||||
if (options.addons.includes("none")) {
|
if (options.addons.includes("none")) {
|
||||||
if (options.addons.length > 1) {
|
if (options.addons.length > 1) {
|
||||||
@@ -383,9 +406,6 @@ function processAndValidateFlags(
|
|||||||
webSpecificAddons.includes(addon),
|
webSpecificAddons.includes(addon),
|
||||||
);
|
);
|
||||||
|
|
||||||
const effectiveFrontend =
|
|
||||||
config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
|
||||||
|
|
||||||
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
const hasCompatibleWebFrontend = effectiveFrontend?.some(
|
||||||
(f) => f === "tanstack-router" || f === "react-router",
|
(f) => f === "tanstack-router" || f === "react-router",
|
||||||
);
|
);
|
||||||
@@ -413,6 +433,7 @@ function processAndValidateFlags(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Examples validation ---
|
||||||
if (options.examples && options.examples.length > 0) {
|
if (options.examples && options.examples.length > 0) {
|
||||||
if (options.examples.includes("none")) {
|
if (options.examples.includes("none")) {
|
||||||
if (options.examples.length > 1) {
|
if (options.examples.length > 1) {
|
||||||
@@ -437,25 +458,22 @@ function processAndValidateFlags(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveFrontend =
|
const hasWebFrontendForExamples = effectiveFrontend?.some((f) =>
|
||||||
config.frontend ??
|
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
|
||||||
(options.frontend?.filter((f) => f !== "none") as ProjectFrontend[]) ??
|
f,
|
||||||
(options.yes ? DEFAULT_CONFIG.frontend : undefined);
|
),
|
||||||
|
|
||||||
const hasWebFrontend = effectiveFrontend?.some((f) =>
|
|
||||||
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasWebFrontend) {
|
if (!hasWebFrontendForExamples) {
|
||||||
if (options.frontend) {
|
if (options.frontend) {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"Examples require a web frontend (tanstack-router, react-router, or tanstack-start). Cannot use --examples with your frontend selection.",
|
"Examples require a web frontend (tanstack-router, react-router, tanstack-start, or next). Cannot use --examples with your frontend selection.",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else if (!options.yes) {
|
} else if (!options.yes) {
|
||||||
} else {
|
} else {
|
||||||
consola.fatal(
|
consola.fatal(
|
||||||
"Examples require a web frontend (tanstack-router, react-router, or tanstack-start) (default frontend incompatible).",
|
"Examples require a web frontend (tanstack-router, react-router, tanstack-start, or next) (default frontend incompatible).",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -465,18 +483,16 @@ function processAndValidateFlags(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Other flags ---
|
||||||
if (options.packageManager) {
|
if (options.packageManager) {
|
||||||
config.packageManager = options.packageManager as ProjectPackageManager;
|
config.packageManager = options.packageManager as ProjectPackageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.git !== undefined) {
|
if (options.git !== undefined) {
|
||||||
config.git = options.git;
|
config.git = options.git;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.install !== undefined) {
|
if (options.install !== undefined) {
|
||||||
config.noInstall = !options.install;
|
config.install = options.install;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectDirectory) {
|
if (projectDirectory) {
|
||||||
config.projectName = projectDirectory;
|
config.projectName = projectDirectory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export async function getAddonsChoice(
|
|||||||
label: "Husky",
|
label: "Husky",
|
||||||
hint: "Add Git hooks with Husky, lint-staged (requires Biome)",
|
hint: "Add Git hooks with Husky, lint-staged (requires Biome)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "turborepo" as const,
|
||||||
|
label: "Turborepo",
|
||||||
|
hint: "Optimize builds for monorepos",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const webAddonOptions = [
|
const webAddonOptions = [
|
||||||
|
|||||||
58
apps/cli/src/prompts/api.ts
Normal file
58
apps/cli/src/prompts/api.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { cancel, isCancel, select } from "@clack/prompts";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
|
import type { ProjectApi, ProjectFrontend } from "../types";
|
||||||
|
|
||||||
|
export async function getApiChoice(
|
||||||
|
Api?: ProjectApi | undefined,
|
||||||
|
frontend?: ProjectFrontend[],
|
||||||
|
): Promise<ProjectApi> {
|
||||||
|
if (Api) return Api;
|
||||||
|
|
||||||
|
const includesNative = frontend?.includes("native");
|
||||||
|
|
||||||
|
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 integration (skip API setup)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (includesNative) {
|
||||||
|
apiOptions = [
|
||||||
|
{
|
||||||
|
value: "trpc" as const,
|
||||||
|
label: "tRPC",
|
||||||
|
hint: "End-to-end typesafe APIs made easy (Required for Native frontend)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiType = await select<ProjectApi>({
|
||||||
|
message: "Select API type",
|
||||||
|
options: apiOptions,
|
||||||
|
initialValue: includesNative ? "trpc" : DEFAULT_CONFIG.api,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(apiType)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includesNative && apiType !== "trpc") {
|
||||||
|
return "trpc";
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiType;
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import type { ProjectFrontend } from "../types";
|
|||||||
export async function getAuthChoice(
|
export async function getAuthChoice(
|
||||||
auth: boolean | undefined,
|
auth: boolean | undefined,
|
||||||
hasDatabase: boolean,
|
hasDatabase: boolean,
|
||||||
frontends?: ProjectFrontend[],
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!hasDatabase) return false;
|
if (!hasDatabase) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { cancel, group, log } from "@clack/prompts";
|
import { cancel, group } from "@clack/prompts";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type {
|
import type {
|
||||||
ProjectAddons,
|
ProjectAddons,
|
||||||
|
ProjectApi,
|
||||||
ProjectBackend,
|
ProjectBackend,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ProjectDBSetup,
|
ProjectDBSetup,
|
||||||
@@ -13,6 +14,7 @@ import type {
|
|||||||
ProjectRuntime,
|
ProjectRuntime,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getAddonsChoice } from "./addons";
|
import { getAddonsChoice } from "./addons";
|
||||||
|
import { getApiChoice } from "./api";
|
||||||
import { getAuthChoice } from "./auth";
|
import { getAuthChoice } from "./auth";
|
||||||
import { getBackendFrameworkChoice } from "./backend-framework";
|
import { getBackendFrameworkChoice } from "./backend-framework";
|
||||||
import { getDatabaseChoice } from "./database";
|
import { getDatabaseChoice } from "./database";
|
||||||
@@ -20,7 +22,7 @@ import { getDBSetupChoice } from "./db-setup";
|
|||||||
import { getExamplesChoice } from "./examples";
|
import { getExamplesChoice } from "./examples";
|
||||||
import { getFrontendChoice } from "./frontend-option";
|
import { getFrontendChoice } from "./frontend-option";
|
||||||
import { getGitChoice } from "./git";
|
import { getGitChoice } from "./git";
|
||||||
import { getNoInstallChoice } from "./install";
|
import { getinstallChoice } from "./install";
|
||||||
import { getORMChoice } from "./orm";
|
import { getORMChoice } from "./orm";
|
||||||
import { getPackageManagerChoice } from "./package-manager";
|
import { getPackageManagerChoice } from "./package-manager";
|
||||||
import { getProjectName } from "./project-name";
|
import { getProjectName } from "./project-name";
|
||||||
@@ -35,11 +37,12 @@ type PromptGroupResults = {
|
|||||||
examples: ProjectExamples[];
|
examples: ProjectExamples[];
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: ProjectPackageManager;
|
packageManager: ProjectPackageManager;
|
||||||
noInstall: boolean;
|
install: boolean;
|
||||||
dbSetup: ProjectDBSetup;
|
dbSetup: ProjectDBSetup;
|
||||||
backend: ProjectBackend;
|
backend: ProjectBackend;
|
||||||
runtime: ProjectRuntime;
|
runtime: ProjectRuntime;
|
||||||
frontend: ProjectFrontend[];
|
frontend: ProjectFrontend[];
|
||||||
|
api: ProjectApi;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function gatherConfig(
|
export async function gatherConfig(
|
||||||
@@ -57,12 +60,9 @@ export async function gatherConfig(
|
|||||||
database: () => getDatabaseChoice(flags.database),
|
database: () => getDatabaseChoice(flags.database),
|
||||||
orm: ({ results }) =>
|
orm: ({ results }) =>
|
||||||
getORMChoice(flags.orm, results.database !== "none", results.database),
|
getORMChoice(flags.orm, results.database !== "none", results.database),
|
||||||
|
api: ({ results }) => getApiChoice(flags.api, results.frontend),
|
||||||
auth: ({ results }) =>
|
auth: ({ results }) =>
|
||||||
getAuthChoice(
|
getAuthChoice(flags.auth, results.database !== "none"),
|
||||||
flags.auth,
|
|
||||||
results.database !== "none",
|
|
||||||
results.frontend,
|
|
||||||
),
|
|
||||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||||
examples: ({ results }) =>
|
examples: ({ results }) =>
|
||||||
getExamplesChoice(
|
getExamplesChoice(
|
||||||
@@ -79,7 +79,7 @@ export async function gatherConfig(
|
|||||||
),
|
),
|
||||||
git: () => getGitChoice(flags.git),
|
git: () => getGitChoice(flags.git),
|
||||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||||
noInstall: () => getNoInstallChoice(flags.noInstall),
|
install: () => getinstallChoice(flags.install),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onCancel: () => {
|
onCancel: () => {
|
||||||
@@ -99,9 +99,10 @@ export async function gatherConfig(
|
|||||||
examples: result.examples,
|
examples: result.examples,
|
||||||
git: result.git,
|
git: result.git,
|
||||||
packageManager: result.packageManager,
|
packageManager: result.packageManager,
|
||||||
noInstall: result.noInstall,
|
install: result.install,
|
||||||
dbSetup: result.dbSetup,
|
dbSetup: result.dbSetup,
|
||||||
backend: result.backend,
|
backend: result.backend,
|
||||||
runtime: result.runtime,
|
runtime: result.runtime,
|
||||||
|
api: result.api,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { cancel, confirm, isCancel } from "@clack/prompts";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { DEFAULT_CONFIG } from "../constants";
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
|
|
||||||
export async function getNoInstallChoice(
|
export async function getinstallChoice(install?: boolean): Promise<boolean> {
|
||||||
noInstall?: boolean,
|
if (install !== undefined) return install;
|
||||||
): Promise<boolean> {
|
|
||||||
if (noInstall !== undefined) return noInstall;
|
|
||||||
|
|
||||||
const response = await confirm({
|
const response = await confirm({
|
||||||
message: "Install dependencies?",
|
message: "Install dependencies?",
|
||||||
initialValue: !DEFAULT_CONFIG.noInstall,
|
initialValue: DEFAULT_CONFIG.install,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCancel(response)) {
|
if (isCancel(response)) {
|
||||||
@@ -17,5 +15,5 @@ export async function getNoInstallChoice(
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type ProjectAddons =
|
|||||||
| "tauri"
|
| "tauri"
|
||||||
| "husky"
|
| "husky"
|
||||||
| "starlight"
|
| "starlight"
|
||||||
|
| "turborepo"
|
||||||
| "none";
|
| "none";
|
||||||
export type ProjectBackend = "hono" | "elysia" | "express" | "next";
|
export type ProjectBackend = "hono" | "elysia" | "express" | "next";
|
||||||
export type ProjectRuntime = "node" | "bun";
|
export type ProjectRuntime = "node" | "bun";
|
||||||
@@ -29,6 +30,7 @@ export type ProjectDBSetup =
|
|||||||
| "mongodb-atlas"
|
| "mongodb-atlas"
|
||||||
| "neon"
|
| "neon"
|
||||||
| "none";
|
| "none";
|
||||||
|
export type ProjectApi = "trpc" | "orpc" | "none";
|
||||||
|
|
||||||
export interface ProjectConfig {
|
export interface ProjectConfig {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@@ -41,23 +43,30 @@ export interface ProjectConfig {
|
|||||||
examples: ProjectExamples[];
|
examples: ProjectExamples[];
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: ProjectPackageManager;
|
packageManager: ProjectPackageManager;
|
||||||
noInstall: boolean;
|
install: boolean;
|
||||||
dbSetup: ProjectDBSetup;
|
dbSetup: ProjectDBSetup;
|
||||||
frontend: ProjectFrontend[];
|
frontend: ProjectFrontend[];
|
||||||
|
api: ProjectApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CLIOptions = {
|
export type YargsArgv = {
|
||||||
|
projectDirectory?: string;
|
||||||
|
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
database?: string;
|
database?: ProjectDatabase;
|
||||||
orm?: string;
|
orm?: ProjectOrm;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
frontend?: string[];
|
frontend?: ProjectFrontend[];
|
||||||
addons?: string[];
|
addons?: ProjectAddons[];
|
||||||
examples?: string[] | boolean;
|
examples?: ProjectExamples[];
|
||||||
git?: boolean;
|
git?: boolean;
|
||||||
packageManager?: string;
|
packageManager?: ProjectPackageManager;
|
||||||
install?: boolean;
|
install?: boolean;
|
||||||
dbSetup?: string;
|
dbSetup?: ProjectDBSetup;
|
||||||
backend?: string;
|
backend?: ProjectBackend;
|
||||||
runtime?: string;
|
runtime?: ProjectRuntime;
|
||||||
|
api?: ProjectApi;
|
||||||
|
|
||||||
|
_: (string | number)[];
|
||||||
|
$0: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,30 +3,41 @@ import fs from "fs-extra";
|
|||||||
|
|
||||||
import { type AvailableDependencies, dependencyVersionMap } from "../constants";
|
import { type AvailableDependencies, dependencyVersionMap } from "../constants";
|
||||||
|
|
||||||
export const addPackageDependency = (opts: {
|
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");
|
||||||
const pkgJson = fs.readJSONSync(pkgJsonPath);
|
|
||||||
|
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||||
|
|
||||||
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
if (!pkgJson.dependencies) pkgJson.dependencies = {};
|
||||||
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
|
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
|
||||||
|
|
||||||
for (const pkgName of dependencies) {
|
for (const pkgName of dependencies) {
|
||||||
const version = dependencyVersionMap[pkgName];
|
const version = dependencyVersionMap[pkgName];
|
||||||
pkgJson.dependencies[pkgName] = version;
|
if (version) {
|
||||||
|
pkgJson.dependencies[pkgName] = version;
|
||||||
|
} else {
|
||||||
|
console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pkgName of devDependencies) {
|
for (const pkgName of devDependencies) {
|
||||||
const version = dependencyVersionMap[pkgName];
|
const version = dependencyVersionMap[pkgName];
|
||||||
pkgJson.devDependencies[pkgName] = version;
|
if (version) {
|
||||||
|
pkgJson.devDependencies[pkgName] = version;
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Warning: Dev dependency ${pkgName} not found in version map.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeJSONSync(pkgJsonPath, pkgJson, {
|
await fs.writeJson(pkgJsonPath, pkgJson, {
|
||||||
spaces: 2,
|
spaces: 2,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,66 +2,109 @@ import pc from "picocolors";
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export function displayConfig(config: Partial<ProjectConfig>) {
|
export function displayConfig(config: Partial<ProjectConfig>) {
|
||||||
const configDisplay = [];
|
const configDisplay: string[] = [];
|
||||||
|
|
||||||
if (config.projectName) {
|
if (config.projectName) {
|
||||||
configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
|
configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.frontend !== undefined) {
|
if (config.frontend !== undefined) {
|
||||||
|
const frontend = Array.isArray(config.frontend)
|
||||||
|
? config.frontend
|
||||||
|
: [config.frontend];
|
||||||
const frontendText =
|
const frontendText =
|
||||||
config.frontend.length > 0 ? config.frontend.join(", ") : "none";
|
frontend.length > 0 && frontend[0] !== undefined && frontend[0] !== ""
|
||||||
|
? frontend.join(", ")
|
||||||
|
: "none";
|
||||||
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.backend !== undefined) {
|
if (config.backend !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Backend Framework:")} ${config.backend}`);
|
configDisplay.push(
|
||||||
|
`${pc.blue("Backend Framework:")} ${String(config.backend)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.runtime !== undefined) {
|
if (config.runtime !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Runtime:")} ${config.runtime}`);
|
configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.api !== undefined) {
|
||||||
|
configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.database !== undefined) {
|
if (config.database !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Database:")} ${config.database}`);
|
configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.orm !== undefined) {
|
if (config.orm !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`);
|
configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.auth !== undefined) {
|
if (config.auth !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`);
|
const authText =
|
||||||
|
typeof config.auth === "boolean"
|
||||||
|
? config.auth
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: String(config.auth);
|
||||||
|
configDisplay.push(`${pc.blue("Authentication:")} ${authText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.addons !== undefined) {
|
if (config.addons !== undefined) {
|
||||||
|
const addons = Array.isArray(config.addons)
|
||||||
|
? config.addons
|
||||||
|
: [config.addons];
|
||||||
const addonsText =
|
const addonsText =
|
||||||
config.addons.length > 0 ? config.addons.join(", ") : "none";
|
addons.length > 0 && addons[0] !== undefined ? addons.join(", ") : "none";
|
||||||
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
|
configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.examples !== undefined) {
|
if (config.examples !== undefined) {
|
||||||
|
const examples = Array.isArray(config.examples)
|
||||||
|
? config.examples
|
||||||
|
: [config.examples];
|
||||||
const examplesText =
|
const examplesText =
|
||||||
config.examples.length > 0 ? config.examples.join(", ") : "none";
|
examples.length > 0 && examples[0] !== undefined
|
||||||
|
? examples.join(", ")
|
||||||
|
: "none";
|
||||||
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
|
configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.git !== undefined) {
|
if (config.git !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);
|
const gitText =
|
||||||
|
typeof config.git === "boolean"
|
||||||
|
? config.git
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: String(config.git);
|
||||||
|
configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.packageManager !== undefined) {
|
if (config.packageManager !== undefined) {
|
||||||
configDisplay.push(
|
configDisplay.push(
|
||||||
`${pc.blue("Package Manager:")} ${config.packageManager}`,
|
`${pc.blue("Package Manager:")} ${String(config.packageManager)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.noInstall !== undefined) {
|
if (config.install !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`);
|
const installText =
|
||||||
|
typeof config.install === "boolean"
|
||||||
|
? config.install
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
: String(config.install);
|
||||||
|
configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.dbSetup !== undefined) {
|
if (config.dbSetup !== undefined) {
|
||||||
configDisplay.push(`${pc.blue("Database Setup:")} ${config.dbSetup}`);
|
configDisplay.push(
|
||||||
|
`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configDisplay.length === 0) {
|
||||||
|
return pc.yellow("No configuration selected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return configDisplay.join("\n");
|
return configDisplay.join("\n");
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.api) {
|
||||||
|
flags.push(`--api ${config.api}`);
|
||||||
|
}
|
||||||
|
|
||||||
flags.push(config.auth ? "--auth" : "--no-auth");
|
flags.push(config.auth ? "--auth" : "--no-auth");
|
||||||
flags.push(config.git ? "--git" : "--no-git");
|
flags.push(config.git ? "--git" : "--no-git");
|
||||||
flags.push(config.noInstall ? "--no-install" : "--install");
|
flags.push(config.install ? "--install" : "--no-install");
|
||||||
|
|
||||||
if (config.runtime) {
|
if (config.runtime) {
|
||||||
flags.push(`--runtime ${config.runtime}`);
|
flags.push(`--runtime ${config.runtime}`);
|
||||||
|
|||||||
23
apps/cli/src/utils/get-package-execution-command.ts
Normal file
23
apps/cli/src/utils/get-package-execution-command.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ProjectPackageManager } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate command for running a package without installing it globally,
|
||||||
|
* based on the selected package manager.
|
||||||
|
*
|
||||||
|
* @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
|
||||||
|
* @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
|
||||||
|
* @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
|
||||||
|
*/
|
||||||
|
export function getPackageExecutionCommand(
|
||||||
|
packageManager: ProjectPackageManager | null | undefined,
|
||||||
|
commandWithArgs: string,
|
||||||
|
): string {
|
||||||
|
switch (packageManager) {
|
||||||
|
case "pnpm":
|
||||||
|
return `pnpm dlx ${commandWithArgs}`;
|
||||||
|
case "bun":
|
||||||
|
return `bunx ${commandWithArgs}`;
|
||||||
|
default:
|
||||||
|
return `npx ${commandWithArgs}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
apps/cli/src/utils/template-processor.ts
Normal file
37
apps/cli/src/utils/template-processor.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a Handlebars template file and writes the output to the destination.
|
||||||
|
* @param srcPath Path to the source .hbs template file.
|
||||||
|
* @param destPath Path to write the processed file.
|
||||||
|
* @param context Data to be passed to the Handlebars template.
|
||||||
|
*/
|
||||||
|
export async function processTemplate(
|
||||||
|
srcPath: string,
|
||||||
|
destPath: string,
|
||||||
|
context: ProjectConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const templateContent = await fs.readFile(srcPath, "utf-8");
|
||||||
|
const template = handlebars.compile(templateContent);
|
||||||
|
const processedContent = template(context);
|
||||||
|
|
||||||
|
await fs.ensureDir(path.dirname(destPath));
|
||||||
|
await fs.writeFile(destPath, processedContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing template ${srcPath}:`, error);
|
||||||
|
throw new Error(`Failed to process template ${srcPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlebars.registerHelper("or", (a, b) => a || b);
|
||||||
|
|
||||||
|
handlebars.registerHelper("eq", (a, b) => a === b);
|
||||||
|
|
||||||
|
handlebars.registerHelper(
|
||||||
|
"includes",
|
||||||
|
(array, value) => Array.isArray(array) && array.includes(value),
|
||||||
|
);
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
|
||||||
import type { Context } from "./context";
|
|
||||||
|
|
||||||
export const t = initTRPC.context<Context>().create();
|
|
||||||
|
|
||||||
export const router = t.router;
|
|
||||||
|
|
||||||
export const publicProcedure = t.procedure;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { router, publicProcedure } from "../lib/trpc";
|
|
||||||
import { todoRouter } from "./todo";
|
|
||||||
|
|
||||||
export const appRouter = router({
|
|
||||||
healthCheck: publicProcedure.query(() => {
|
|
||||||
return "OK";
|
|
||||||
}),
|
|
||||||
todo: todoRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import type { AppRouter } from "../../../server/src/routers";
|
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
href={to}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { QueryCache, QueryClient } from '@tanstack/react-query';
|
|
||||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
|
||||||
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
|
||||||
import type { AppRouter } from '../../../server/src/routers';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { NavLink } from "react-router";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<NavLink
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
className={({ isActive }) => (isActive ? "font-bold" : "")}
|
|
||||||
end
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { reactRouter } from "@react-router/dev/vite";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
|
||||||
});
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
activeProps={{ className: "font-bold" }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
|
||||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
import path from "node:path";
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [tailwindcss(), TanStackRouterVite({}), react()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
"@": path.resolve(__dirname, "./src"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
activeProps={{ className: "font-bold" }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import {
|
|
||||||
QueryCache,
|
|
||||||
QueryClient,
|
|
||||||
QueryClientProvider,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
import { createRouter as createTanstackRouter } from "@tanstack/react-router";
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import type { AppRouter } from "../../server/src/routers";
|
|
||||||
import Loader from "./components/loader";
|
|
||||||
import "./index.css";
|
|
||||||
import { routeTree } from "./routeTree.gen";
|
|
||||||
import { TRPCProvider } from "./utils/trpc";
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
defaultOptions: { queries: { staleTime: 60 * 1000 } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const trpc = createTRPCOptionsProxy({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient: queryClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createRouter = () => {
|
|
||||||
const router = createTanstackRouter({
|
|
||||||
routeTree,
|
|
||||||
scrollRestoration: true,
|
|
||||||
defaultPreloadStaleTime: 0,
|
|
||||||
context: { trpc, queryClient },
|
|
||||||
defaultPendingComponent: () => <Loader />,
|
|
||||||
defaultNotFoundComponent: () => <div>Not Found</div>,
|
|
||||||
Wrap: ({ children }) => (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
|
||||||
{children}
|
|
||||||
</TRPCProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Register the router instance for type safety
|
|
||||||
declare module "@tanstack/react-router" {
|
|
||||||
interface Register {
|
|
||||||
router: ReturnType<typeof createRouter>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
|
||||||
import type { AppRouter } from "../../../server/src/routers";
|
|
||||||
|
|
||||||
export const { TRPCProvider, useTRPC, useTRPCClient } =
|
|
||||||
createTRPCContext<AppRouter>();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "better-t-stack",
|
|
||||||
"private": true,
|
|
||||||
"workspaces": ["apps/*"],
|
|
||||||
"scripts": {
|
|
||||||
"dev": "turbo dev",
|
|
||||||
"build": "turbo build",
|
|
||||||
"check-types": "turbo check-types",
|
|
||||||
"dev:native": "turbo -F native dev",
|
|
||||||
"dev:web": "turbo -F web dev",
|
|
||||||
"dev:server": "turbo -F server dev",
|
|
||||||
"db:push": "turbo -F server db:push",
|
|
||||||
"db:studio": "turbo -F server db:studio"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"turbo": "^2.4.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { router, publicProcedure } from "../lib/trpc";
|
|
||||||
import { todo } from "../db/schema/todo";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { db } from "../db";
|
|
||||||
|
|
||||||
export const todoRouter = router({
|
|
||||||
getAll: publicProcedure.query(async () => {
|
|
||||||
return await db.select().from(todo);
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: publicProcedure
|
|
||||||
.input(z.object({ text: z.string().min(1) }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await db.insert(todo).values({
|
|
||||||
text: input.text,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
toggle: publicProcedure
|
|
||||||
.input(z.object({ id: z.number(), completed: z.boolean() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await db
|
|
||||||
.update(todo)
|
|
||||||
.set({ completed: input.completed })
|
|
||||||
.where(eq(todo.id, input.id));
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: publicProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await db.delete(todo).where(eq(todo.id, input.id));
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { Context as ElysiaContext } from "elysia";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
|
|
||||||
export type CreateContextOptions = {
|
|
||||||
context: ElysiaContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: context.request.headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
|
||||||
import { fromNodeHeaders } from "better-auth/node";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
|
|
||||||
export async function createContext(opts: CreateExpressContextOptions) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: fromNodeHeaders(opts.req.headers),
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { Context as HonoContext } from "hono";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
|
|
||||||
export type CreateContextOptions = {
|
|
||||||
context: HonoContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: context.req.raw.headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { NextRequest } from "next/server";
|
|
||||||
import { auth } from "./auth";
|
|
||||||
|
|
||||||
export async function createContext(req: NextRequest) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: req.headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
session,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { protectedProcedure, publicProcedure, router } from "../lib/trpc";
|
|
||||||
import { todoRouter } from "./todo";
|
|
||||||
|
|
||||||
export const appRouter = router({
|
|
||||||
healthCheck: publicProcedure.query(() => {
|
|
||||||
return "OK";
|
|
||||||
}),
|
|
||||||
privateData: protectedProcedure.query(({ ctx }) => {
|
|
||||||
return {
|
|
||||||
message: "This is private",
|
|
||||||
user: ctx.session.user,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
todo: todoRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
||||||
import { db } from "../db";
|
|
||||||
import * as schema from "../db/schema/auth";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: drizzleAdapter(db, {
|
|
||||||
provider: "mysql",
|
|
||||||
schema: schema,
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
||||||
import { db } from "../db";
|
|
||||||
import * as schema from "../db/schema/auth";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: drizzleAdapter(db, {
|
|
||||||
provider: "pg",
|
|
||||||
schema: schema,
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
||||||
import { db } from "../db";
|
|
||||||
import * as schema from "../db/schema/auth";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: drizzleAdapter(db, {
|
|
||||||
provider: "sqlite",
|
|
||||||
schema: schema,
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|
||||||
import { toNodeHandler } from "better-auth/node";
|
|
||||||
import cors from "cors";
|
|
||||||
import express from "express";
|
|
||||||
import { auth } from "./lib/auth";
|
|
||||||
import { createContext } from "./lib/context";
|
|
||||||
import { appRouter } from "./routers/index";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
origin: process.env.CORS_ORIGIN || "",
|
|
||||||
methods: ["GET", "POST", "OPTIONS"],
|
|
||||||
allowedHeaders: ["Content-Type", "Authorization"],
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.all("/api/auth{/*path}", toNodeHandler(auth));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
|
|
||||||
app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext }));
|
|
||||||
|
|
||||||
|
|
||||||
app.get("/", (_req, res) => {
|
|
||||||
res.status(200).send("OK");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log("Server is running on port 3000");
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
import { trpcServer } from "@hono/trpc-server";
|
|
||||||
import "dotenv/config";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { cors } from "hono/cors";
|
|
||||||
import { logger } from "hono/logger";
|
|
||||||
import { auth } from "./lib/auth";
|
|
||||||
import { createContext } from "./lib/context";
|
|
||||||
import { appRouter } from "./routers/index";
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
app.use(logger());
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/*",
|
|
||||||
cors({
|
|
||||||
origin: process.env.CORS_ORIGIN || "",
|
|
||||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
||||||
allowHeaders: ["Content-Type", "Authorization"],
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/trpc/*",
|
|
||||||
trpcServer({
|
|
||||||
router: appRouter,
|
|
||||||
createContext: (_opts, context) => {
|
|
||||||
return createContext({ context });
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get("/", (c) => {
|
|
||||||
return c.text("OK");
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
||||||
import prisma from "../../prisma";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: prismaAdapter(prisma, {
|
|
||||||
provider: "mongodb",
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: { enabled: true },
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
||||||
import prisma from "../../prisma";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: prismaAdapter(prisma, {
|
|
||||||
provider: "mysql",
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: { enabled: true },
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
||||||
import prisma from "../../prisma";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: prismaAdapter(prisma, {
|
|
||||||
provider: "postgresql",
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: { enabled: true },
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { betterAuth } from "better-auth";
|
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
||||||
import prisma from "../../prisma";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
|
||||||
database: prismaAdapter(prisma, {
|
|
||||||
provider: "sqlite",
|
|
||||||
}),
|
|
||||||
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
|
||||||
emailAndPassword: { enabled: true },
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createAuthClient } from "better-auth/react";
|
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
|
||||||
baseURL: import.meta.env.VITE_SERVER_URL,
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
import UserMenu from "./user-menu";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
{ to: "/dashboard", label: "Dashboard" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
href={to}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { createAuthClient } from "better-auth/react";
|
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
|
||||||
baseURL: process.env.NEXT_PUBLIC_SERVER_URL,
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { QueryCache, QueryClient } from '@tanstack/react-query';
|
|
||||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
|
||||||
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
|
||||||
import type { AppRouter } from '../../../server/src/routers';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`,
|
|
||||||
fetch(url, options) {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { NavLink } from "react-router";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
import UserMenu from "./user-menu";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
{ to: "/dashboard", label: "Dashboard" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<NavLink
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
className={({ isActive }) => (isActive ? "font-bold" : "")}
|
|
||||||
end
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { AppRouter } from "../../../server/src/routers";
|
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
||||||
fetch(url, options) {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import { ModeToggle } from "./mode-toggle";
|
|
||||||
import UserMenu from "./user-menu";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
{ to: "/dashboard", label: "Dashboard" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
activeProps={{ className: "font-bold" }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ModeToggle />
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { AppRouter } from "../../../server/src/routers";
|
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
|
||||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
|
||||||
queryCache: new QueryCache({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message, {
|
|
||||||
action: {
|
|
||||||
label: "retry",
|
|
||||||
onClick: () => {
|
|
||||||
queryClient.invalidateQueries();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpcClient = createTRPCClient<AppRouter>({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
|
||||||
fetch(url, options) {
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
||||||
client: trpcClient,
|
|
||||||
queryClient,
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import UserMenu from "./user-menu";
|
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
const links = [
|
|
||||||
{ to: "/", label: "Home" },
|
|
||||||
{ to: "/dashboard", label: "Dashboard" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
|
||||||
<nav className="flex gap-4 text-lg">
|
|
||||||
{links.map(({ to, label }) => (
|
|
||||||
<Link
|
|
||||||
key={to}
|
|
||||||
to={to}
|
|
||||||
activeProps={{ className: "font-bold" }}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import { Elysia } from "elysia";
|
|
||||||
import { cors } from "@elysiajs/cors";
|
|
||||||
import { createContext } from "./lib/context";
|
|
||||||
import { appRouter } from "./routers/index";
|
|
||||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
||||||
|
|
||||||
const app = new Elysia()
|
|
||||||
.use(
|
|
||||||
cors({
|
|
||||||
origin: process.env.CORS_ORIGIN || "",
|
|
||||||
methods: ["GET", "POST", "OPTIONS"],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.all("/trpc/*", async (context) => {
|
|
||||||
const res = await fetchRequestHandler({
|
|
||||||
endpoint: "/trpc",
|
|
||||||
router: appRouter,
|
|
||||||
req: context.request,
|
|
||||||
createContext: () => createContext({ context }),
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
})
|
|
||||||
.get("/", () => "OK")
|
|
||||||
.listen(3000, () => {
|
|
||||||
console.log(`Server is running on http://localhost:3000`);
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { Context as ElysiaContext } from "elysia";
|
|
||||||
|
|
||||||
export type CreateContextOptions = {
|
|
||||||
context: ElysiaContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import "dotenv/config";
|
|
||||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|
||||||
import cors from "cors";
|
|
||||||
import express from "express";
|
|
||||||
import { createContext } from "./lib/context";
|
|
||||||
import { appRouter } from "./routers/index";
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
origin: process.env.CORS_ORIGIN || "",
|
|
||||||
methods: ["GET", "POST", "OPTIONS"],
|
|
||||||
allowedHeaders: ["Content-Type", "Authorization"],
|
|
||||||
credentials: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext }));
|
|
||||||
|
|
||||||
app.get("/", (_req, res) => {
|
|
||||||
res.status(200).send("OK");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log("Server is running on port 3000");
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
|
||||||
|
|
||||||
export async function createContext(opts: CreateExpressContextOptions) {
|
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { trpcServer } from "@hono/trpc-server";
|
|
||||||
import "dotenv/config";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { cors } from "hono/cors";
|
|
||||||
import { logger } from "hono/logger";
|
|
||||||
import { createContext } from "./lib/context";
|
|
||||||
import { appRouter } from "./routers/index";
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
|
|
||||||
app.use(logger());
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/*",
|
|
||||||
cors({
|
|
||||||
origin: process.env.CORS_ORIGIN || "",
|
|
||||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
"/trpc/*",
|
|
||||||
trpcServer({
|
|
||||||
router: appRouter,
|
|
||||||
createContext: (_opts, context) => {
|
|
||||||
return createContext({ context });
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get("/", (c) => {
|
|
||||||
return c.text("OK");
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { Context as HonoContext } from "hono";
|
|
||||||
|
|
||||||
export type CreateContextOptions = {
|
|
||||||
context: HonoContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createContext({ context }: CreateContextOptions) {
|
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { NextRequest } from "next/server";
|
|
||||||
|
|
||||||
export async function createContext(req: NextRequest) {
|
|
||||||
return {
|
|
||||||
session: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
105
apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs
Normal file
105
apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{{#if (eq backend 'next')}}
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export async function createContext(req: NextRequest) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: req.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
return {}
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'hono')}}
|
||||||
|
import type { Context as HonoContext } from "hono";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type CreateContextOptions = {
|
||||||
|
context: HonoContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: context.req.raw.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'elysia')}}
|
||||||
|
import type { Context as ElysiaContext } from "elysia";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type CreateContextOptions = {
|
||||||
|
context: ElysiaContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: context.request.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'express')}}
|
||||||
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
|
{{#if auth}}
|
||||||
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export async function createContext(opts: CreateExpressContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: fromNodeHeaders(opts.req.headers),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
// Default or fallback context if backend is not recognized or none
|
||||||
|
// This might need adjustment based on your default behavior
|
||||||
|
export async function createContext() {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type Context = Awaited<ReturnType<typeof createContext>>;
|
||||||
17
apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs
Normal file
17
apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ORPCError, os } from "@orpc/server";
|
||||||
|
import type { Context } from "./context";
|
||||||
|
|
||||||
|
export const o = os.$context<Context>();
|
||||||
|
|
||||||
|
export const publicProcedure = o;
|
||||||
|
|
||||||
|
{{#if auth}}
|
||||||
|
const requireAuth = o.middleware(async ({ context, next }) => {
|
||||||
|
if (!context.session?.user) {
|
||||||
|
throw new ORPCError("UNAUTHORIZED");
|
||||||
|
}
|
||||||
|
return next({ context });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const protectedProcedure = publicProcedure.use(requireAuth);
|
||||||
|
{{/if}}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{#if auth}}
|
||||||
|
import { createContext } from '@/lib/context'
|
||||||
|
{{/if}}
|
||||||
|
import { appRouter } from '@/routers'
|
||||||
|
import { RPCHandler } from '@orpc/server/fetch'
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
const handler = new RPCHandler(appRouter)
|
||||||
|
|
||||||
|
async function handleRequest(req: NextRequest) {
|
||||||
|
const { response } = await handler.handle(req, {
|
||||||
|
prefix: '/rpc',
|
||||||
|
context: {{#if auth}}await createContext(req){{else}}{}{{/if}},
|
||||||
|
})
|
||||||
|
|
||||||
|
return response ?? new Response('Not found', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = handleRequest
|
||||||
|
export const POST = handleRequest
|
||||||
|
export const PUT = handleRequest
|
||||||
|
export const PATCH = handleRequest
|
||||||
|
export const DELETE = handleRequest
|
||||||
57
apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs
Normal file
57
apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { createORPCClient } from "@orpc/client";
|
||||||
|
import { RPCLink } from "@orpc/client/fetch";
|
||||||
|
import { createORPCReactQueryUtils } from "@orpc/react-query";
|
||||||
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { appRouter } from "../../../server/src/routers/index";
|
||||||
|
import type { RouterClient } from "@orpc/server";
|
||||||
|
import { createContext, use } from 'react'
|
||||||
|
import type { RouterUtils } from '@orpc/react-query'
|
||||||
|
|
||||||
|
type ORPCReactUtils = RouterUtils<RouterClient<typeof appRouter>>
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`Error: ${error.message}`, {
|
||||||
|
action: {
|
||||||
|
label: "retry",
|
||||||
|
onClick: () => {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const link = new RPCLink({
|
||||||
|
{{#if (includes frontend "next")}}
|
||||||
|
url: `${process.env.NEXT_PUBLIC_SERVER_URL}/rpc`,
|
||||||
|
{{else}}
|
||||||
|
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
||||||
|
{{/if}}
|
||||||
|
{{#if auth}}
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const client: RouterClient<typeof appRouter> = createORPCClient(link)
|
||||||
|
|
||||||
|
export const orpc = createORPCReactQueryUtils(client)
|
||||||
|
|
||||||
|
|
||||||
|
export const ORPCContext = createContext<ORPCReactUtils | undefined>(undefined)
|
||||||
|
|
||||||
|
export function useORPC(): ORPCReactUtils {
|
||||||
|
const orpc = use(ORPCContext)
|
||||||
|
if (!orpc) {
|
||||||
|
throw new Error('ORPCContext is not set up properly')
|
||||||
|
}
|
||||||
|
return orpc
|
||||||
|
}
|
||||||
108
apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs
Normal file
108
apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{{#if (eq backend 'next')}}
|
||||||
|
import type { NextRequest } from "next/server";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export async function createContext(req: NextRequest) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: req.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'hono')}}
|
||||||
|
import type { Context as HonoContext } from "hono";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type CreateContextOptions = {
|
||||||
|
context: HonoContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: context.req.raw.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'elysia')}}
|
||||||
|
import type { Context as ElysiaContext } from "elysia";
|
||||||
|
{{#if auth}}
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type CreateContextOptions = {
|
||||||
|
context: ElysiaContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createContext({ context }: CreateContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: context.request.headers,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else if (eq backend 'express')}}
|
||||||
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
|
{{#if auth}}
|
||||||
|
import { fromNodeHeaders } from "better-auth/node";
|
||||||
|
import { auth } from "./auth";
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export async function createContext(opts: CreateExpressContextOptions) {
|
||||||
|
{{#if auth}}
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: fromNodeHeaders(opts.req.headers),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
};
|
||||||
|
{{else}}
|
||||||
|
// No auth configured
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
{{/if}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
// Default or fallback context if backend is not recognized or none
|
||||||
|
// This might need adjustment based on your default behavior
|
||||||
|
export async function createContext() {
|
||||||
|
return {
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
export type Context = Awaited<ReturnType<typeof createContext>>;
|
||||||
@@ -7,6 +7,7 @@ export const router = t.router;
|
|||||||
|
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
{{#if auth}}
|
||||||
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session) {
|
if (!ctx.session) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -22,3 +23,4 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
{{/if}}
|
||||||
100
apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs
Normal file
100
apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{{#if (includes frontend 'next')}}
|
||||||
|
{{!-- Next.js tRPC Client Setup --}}
|
||||||
|
import { QueryCache, QueryClient } from '@tanstack/react-query';
|
||||||
|
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||||
|
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||||
|
import type { AppRouter } from '../../../server/src/routers'; {{! Adjust path if necessary }}
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message, {
|
||||||
|
action: {
|
||||||
|
label: "retry",
|
||||||
|
onClick: () => {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const trpcClient = createTRPCClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
{{#if (includes frontend 'next')}}
|
||||||
|
url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`,
|
||||||
|
{{else}}
|
||||||
|
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||||
|
{{/if}}
|
||||||
|
{{#if auth}}
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
||||||
|
client: trpcClient,
|
||||||
|
queryClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
{{else if (includes frontend 'tanstack-start')}}
|
||||||
|
{{!-- TanStack Start tRPC Client Setup --}}
|
||||||
|
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
||||||
|
import type { AppRouter } from "../../../server/src/routers"; {{! Adjust path if necessary }}
|
||||||
|
|
||||||
|
export const { TRPCProvider, useTRPC, useTRPCClient } =
|
||||||
|
createTRPCContext<AppRouter>();
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
{{!-- Default Web tRPC Client Setup (TanStack Router, React Router, etc.) --}}
|
||||||
|
import type { AppRouter } from "../../../server/src/routers"; {{! Adjust path if necessary }}
|
||||||
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
|
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||||
|
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message, {
|
||||||
|
action: {
|
||||||
|
label: "retry",
|
||||||
|
onClick: () => {
|
||||||
|
queryClient.invalidateQueries();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trpcClient = createTRPCClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||||
|
{{#if auth}}
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{{/if}}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
||||||
|
client: trpcClient,
|
||||||
|
queryClient,
|
||||||
|
});
|
||||||
|
{{/if}}
|
||||||
30
apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs
Normal file
30
apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
|
||||||
|
{{#if (eq orm "prisma")}}
|
||||||
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||||
|
import prisma from "../../prisma";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: prismaAdapter(prisma, {
|
||||||
|
provider: "{{database}}"
|
||||||
|
}),
|
||||||
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
|
emailAndPassword: { enabled: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
{{else if (eq orm "drizzle")}}
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { db } from "../db";
|
||||||
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
{{#if (eq database "postgresql")}}provider: "pg",{{/if}}
|
||||||
|
{{#if (eq database "sqlite")}}provider: "sqlite",{{/if}}
|
||||||
|
{{#if (eq database "mysql")}}provider: "mysql",{{/if}}
|
||||||
|
schema: schema
|
||||||
|
}),
|
||||||
|
trustedOrigins: [process.env.CORS_ORIGIN || ""],
|
||||||
|
emailAndPassword: { enabled: true }
|
||||||
|
});
|
||||||
|
{{/if}}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user