mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
Add todo example, remove yarn, change schema structure, update readme
This commit is contained in:
5
.changeset/true-singers-scream.md
Normal file
5
.changeset/true-singers-scream.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": major
|
||||||
|
---
|
||||||
|
|
||||||
|
stable release
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
# Create Better-T-Stack CLI
|
# Create Better-T-Stack CLI
|
||||||
|
|
||||||
> **Note:** This CLI is currently a work in progress (WIP).
|
An interactive CLI tool to quickly scaffold full-stack TypeScript applications with React, Hono, and tRPC. The Better-T-Stack provides a modern, type-safe development experience with the best tools from the TypeScript ecosystem.
|
||||||
|
|
||||||
An interactive CLI tool to quickly scaffold full-stack TypeScript applications using the Better-T-Stack framework.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -24,9 +22,12 @@ Follow the prompts to configure your project.
|
|||||||
- **Database Options**: SQLite (via Turso), PostgreSQL, or no database
|
- **Database Options**: SQLite (via Turso), PostgreSQL, or no database
|
||||||
- **ORM Selection**: Choose between Drizzle ORM or Prisma
|
- **ORM Selection**: Choose between Drizzle ORM or Prisma
|
||||||
- **Authentication**: Optional auth setup with Better-Auth
|
- **Authentication**: Optional auth setup with Better-Auth
|
||||||
- **Developer Experience**: Git initialization, various package manager support (npm, pnpm, yarn, bun)
|
- **Progressive Web App**: Add PWA support with service workers and installable apps
|
||||||
- **Deployment**: Optional Docker configuration
|
- **Desktop Apps**: Build native desktop apps with Tauri integration
|
||||||
- **CI/CD**: Optional GitHub Actions workflows
|
- **Code Quality**: Biome for linting and formatting
|
||||||
|
- **Git Hooks**: Husky with lint-staged for pre-commit checks
|
||||||
|
- **Examples**: Todo app with full CRUD functionality
|
||||||
|
- **Developer Experience**: Git initialization, various package manager support (npm, pnpm, bun)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -41,12 +42,17 @@ Options:
|
|||||||
--postgres Use PostgreSQL database
|
--postgres Use PostgreSQL database
|
||||||
--auth Include authentication
|
--auth Include authentication
|
||||||
--no-auth Disable authentication
|
--no-auth Disable authentication
|
||||||
--docker Include Docker setup
|
--pwa Include Progressive Web App support
|
||||||
|
--tauri Include Tauri desktop app support
|
||||||
|
--biome Include Biome for linting and formatting
|
||||||
|
--husky Include Husky, lint-staged for Git hooks
|
||||||
|
--no-addons Skip all additional addons
|
||||||
|
--examples <examples> Include specified examples
|
||||||
|
--no-examples Skip all examples
|
||||||
--git Initialize a new git repo (default)
|
--git Initialize a new git repo (default)
|
||||||
--no-git Skip git initialization
|
--no-git Skip git initialization
|
||||||
--npm Use npm as package manager
|
--npm Use npm as package manager
|
||||||
--pnpm Use pnpm as package manager
|
--pnpm Use pnpm as package manager
|
||||||
--yarn Use yarn as package manager
|
|
||||||
--bun Use bun as package manager
|
--bun Use bun as package manager
|
||||||
--drizzle Use Drizzle ORM
|
--drizzle Use Drizzle ORM
|
||||||
--prisma Use Prisma ORM
|
--prisma Use Prisma ORM
|
||||||
@@ -66,28 +72,11 @@ npx create-better-t-stack my-app -y
|
|||||||
|
|
||||||
Create a project with specific options:
|
Create a project with specific options:
|
||||||
```bash
|
```bash
|
||||||
npx create-better-t-stack my-app --postgres --prisma --auth --docker
|
npx create-better-t-stack my-app --postgres --prisma --auth --pwa --biome
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
The generated project follows a Turborepo monorepo structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-app/
|
|
||||||
├── packages/
|
|
||||||
│ ├── client/ # Frontend application (React, TanStack Router)
|
|
||||||
│ └── server/ # Backend API (Hono, tRPC)
|
|
||||||
├── package.json # Root package.json with Turborepo configuration
|
|
||||||
└── README.md # Project documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
Created by [Nitish Singh](https://github.com/FgrReloaded) & [Aman Varshney](https://github.com/AmanVarshney01)
|
Created by [Aman Varshney](https://github.com/AmanVarshney01) & [Nitish Singh](https://github.com/FgrReloaded)
|
||||||
|
|||||||
@@ -22,10 +22,7 @@
|
|||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["dist", "template"],
|
||||||
"dist",
|
|
||||||
"template"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.10.0",
|
"@clack/prompts": "^0.10.0",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
|||||||
git: true,
|
git: true,
|
||||||
packageManager: "npm",
|
packageManager: "npm",
|
||||||
noInstall: false,
|
noInstall: false,
|
||||||
|
examples: ["todo"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dependencyVersionMap = {
|
export const dependencyVersionMap = {
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cancel, spinner } from "@clack/prompts";
|
import { cancel, spinner } from "@clack/prompts";
|
||||||
import { $ } 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, ProjectDatabase, ProjectOrm } from "../types";
|
|
||||||
import { setupAddons } from "./addons-setup";
|
import { setupAddons } from "./addons-setup";
|
||||||
import { setupAuth } from "./auth-setup";
|
import { setupAuth } from "./auth-setup";
|
||||||
import { createReadme } from "./create-readme";
|
import { createReadme } from "./create-readme";
|
||||||
import { setupDatabase } from "./db-setup";
|
import { setupDatabase } from "./db-setup";
|
||||||
import { setupEnvironmentVariables } from "./env-setup";
|
import { setupEnvironmentVariables } from "./env-setup";
|
||||||
|
import { setupExamples } from "./examples-setup";
|
||||||
import { displayPostInstallInstructions } from "./post-installation";
|
import { displayPostInstallInstructions } from "./post-installation";
|
||||||
|
import { initializeGit, updatePackageConfigurations } from "./project-config";
|
||||||
|
import {
|
||||||
|
copyBaseTemplate,
|
||||||
|
fixGitignoreFiles,
|
||||||
|
setupAuthTemplate,
|
||||||
|
setupOrmTemplate,
|
||||||
|
} from "./template-manager";
|
||||||
|
|
||||||
export async function createProject(options: ProjectConfig): Promise<string> {
|
export async function createProject(options: ProjectConfig): Promise<string> {
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
@@ -18,99 +24,26 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.ensureDir(projectDir);
|
await fs.ensureDir(projectDir);
|
||||||
const templateDir = path.join(PKG_ROOT, "template/base");
|
|
||||||
if (!(await fs.pathExists(templateDir))) {
|
|
||||||
throw new Error(`Template directory not found: ${templateDir}`);
|
|
||||||
}
|
|
||||||
await fs.copy(templateDir, projectDir);
|
|
||||||
|
|
||||||
const gitignorePaths = [
|
await copyBaseTemplate(projectDir);
|
||||||
path.join(projectDir, "_gitignore"),
|
|
||||||
path.join(projectDir, "packages/client/_gitignore"),
|
|
||||||
path.join(projectDir, "packages/server/_gitignore"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const gitignorePath of gitignorePaths) {
|
await fixGitignoreFiles(projectDir);
|
||||||
if (await fs.pathExists(gitignorePath)) {
|
|
||||||
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
|
|
||||||
await fs.move(gitignorePath, targetPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.auth) {
|
await setupAuthTemplate(projectDir, options.auth);
|
||||||
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
|
|
||||||
if (await fs.pathExists(authTemplateDir)) {
|
|
||||||
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.orm !== "none" && options.database !== "none") {
|
await setupOrmTemplate(
|
||||||
const ormTemplateDir = path.join(
|
projectDir,
|
||||||
PKG_ROOT,
|
options.orm,
|
||||||
getOrmTemplateDir(options.orm, options.database),
|
options.database,
|
||||||
);
|
options.auth,
|
||||||
|
);
|
||||||
|
|
||||||
if (await fs.pathExists(ormTemplateDir)) {
|
await setupExamples(
|
||||||
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
|
projectDir,
|
||||||
const serverSrcPath = path.join(projectDir, "packages/server/src");
|
options.examples,
|
||||||
const baseLibPath = path.join(serverSrcPath, "lib");
|
options.orm,
|
||||||
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
|
options.auth,
|
||||||
|
);
|
||||||
if (options.auth) {
|
|
||||||
await fs.remove(baseLibPath);
|
|
||||||
await fs.move(withAuthLibPath, baseLibPath);
|
|
||||||
|
|
||||||
if (options.orm === "prisma") {
|
|
||||||
const schemaPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/prisma/schema.prisma",
|
|
||||||
);
|
|
||||||
const withAuthSchemaPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/prisma/with-auth-schema.prisma",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await fs.pathExists(withAuthSchemaPath)) {
|
|
||||||
await fs.remove(schemaPath);
|
|
||||||
await fs.move(withAuthSchemaPath, schemaPath);
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
|
||||||
const schemaPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/src/db/schema.ts",
|
|
||||||
);
|
|
||||||
const withAuthSchemaPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/src/db/with-auth-schema.ts",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await fs.pathExists(withAuthSchemaPath)) {
|
|
||||||
await fs.remove(schemaPath);
|
|
||||||
await fs.move(withAuthSchemaPath, schemaPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fs.remove(withAuthLibPath);
|
|
||||||
if (options.orm === "prisma") {
|
|
||||||
const withAuthSchema = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/prisma/with-auth-schema.prisma",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(withAuthSchema)) {
|
|
||||||
await fs.remove(withAuthSchema);
|
|
||||||
}
|
|
||||||
} else if (options.orm === "drizzle") {
|
|
||||||
const withAuthSchema = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/src/db/with-auth-schema.ts",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(withAuthSchema)) {
|
|
||||||
await fs.remove(withAuthSchema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await setupDatabase(
|
await setupDatabase(
|
||||||
projectDir,
|
projectDir,
|
||||||
@@ -118,18 +51,19 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
options.orm,
|
options.orm,
|
||||||
options.turso ?? options.database === "sqlite",
|
options.turso ?? options.database === "sqlite",
|
||||||
);
|
);
|
||||||
|
|
||||||
await setupAuth(projectDir, options.auth);
|
await setupAuth(projectDir, options.auth);
|
||||||
|
|
||||||
await setupEnvironmentVariables(projectDir, options);
|
await setupEnvironmentVariables(projectDir, options);
|
||||||
|
|
||||||
if (options.git) {
|
await initializeGit(projectDir, options.git);
|
||||||
await $({ cwd: projectDir })`git init`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.addons.length > 0) {
|
if (options.addons.length > 0) {
|
||||||
await setupAddons(projectDir, options.addons, options.packageManager);
|
await setupAddons(projectDir, options.addons, options.packageManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updatePackageConfigurations(projectDir, options);
|
await updatePackageConfigurations(projectDir, options);
|
||||||
|
|
||||||
await createReadme(projectDir, options);
|
await createReadme(projectDir, options);
|
||||||
|
|
||||||
displayPostInstallInstructions(
|
displayPostInstallInstructions(
|
||||||
@@ -151,69 +85,3 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePackageConfigurations(
|
|
||||||
projectDir: string,
|
|
||||||
options: ProjectConfig,
|
|
||||||
) {
|
|
||||||
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
|
||||||
if (await fs.pathExists(rootPackageJsonPath)) {
|
|
||||||
const packageJson = await fs.readJson(rootPackageJsonPath);
|
|
||||||
packageJson.name = options.projectName;
|
|
||||||
|
|
||||||
if (options.packageManager !== "bun") {
|
|
||||||
packageJson.packageManager =
|
|
||||||
options.packageManager === "npm"
|
|
||||||
? "npm@10.9.2"
|
|
||||||
: options.packageManager === "pnpm"
|
|
||||||
? "pnpm@10.6.4"
|
|
||||||
: options.packageManager === "yarn"
|
|
||||||
? "yarn@4.1.0"
|
|
||||||
: "bun@1.2.5";
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverPackageJsonPath = path.join(
|
|
||||||
projectDir,
|
|
||||||
"packages/server/package.json",
|
|
||||||
);
|
|
||||||
if (await fs.pathExists(serverPackageJsonPath)) {
|
|
||||||
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
|
||||||
|
|
||||||
if (options.database !== "none") {
|
|
||||||
if (options.database === "sqlite") {
|
|
||||||
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.orm === "prisma") {
|
|
||||||
serverPackageJson.scripts["db:push"] = "prisma db push";
|
|
||||||
serverPackageJson.scripts["db:studio"] = "prisma studio";
|
|
||||||
} else if (options.orm === "drizzle") {
|
|
||||||
serverPackageJson.scripts["db:push"] = "drizzle-kit push";
|
|
||||||
serverPackageJson.scripts["db:studio"] = "drizzle-kit studio";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
|
|
||||||
spaces: 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
|
||||||
if (orm === "drizzle") {
|
|
||||||
return database === "sqlite"
|
|
||||||
? "template/with-drizzle-sqlite"
|
|
||||||
: "template/with-drizzle-postgres";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orm === "prisma") {
|
|
||||||
return database === "sqlite"
|
|
||||||
? "template/with-prisma-sqlite"
|
|
||||||
: "template/with-prisma-postgres";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "template/base";
|
|
||||||
}
|
|
||||||
|
|||||||
130
apps/cli/src/helpers/examples-setup.ts
Normal file
130
apps/cli/src/helpers/examples-setup.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import { PKG_ROOT } from "../constants";
|
||||||
|
import type { ProjectOrm } from "../types";
|
||||||
|
|
||||||
|
export async function setupExamples(
|
||||||
|
projectDir: string,
|
||||||
|
examples: string[],
|
||||||
|
orm: ProjectOrm,
|
||||||
|
auth: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
if (examples.includes("todo")) {
|
||||||
|
await setupTodoExample(projectDir, orm, auth);
|
||||||
|
} else {
|
||||||
|
await cleanupTodoFiles(projectDir, orm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupTodoExample(
|
||||||
|
projectDir: string,
|
||||||
|
orm: ProjectOrm,
|
||||||
|
auth: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
|
||||||
|
if (await fs.pathExists(todoExampleDir)) {
|
||||||
|
const todoRouteDir = path.join(
|
||||||
|
todoExampleDir,
|
||||||
|
"packages/client/src/routes",
|
||||||
|
);
|
||||||
|
const targetRouteDir = path.join(projectDir, "packages/client/src/routes");
|
||||||
|
await fs.copy(todoRouteDir, targetRouteDir, { overwrite: true });
|
||||||
|
|
||||||
|
if (orm !== "none") {
|
||||||
|
const todoRouterSourceFile = path.join(
|
||||||
|
todoExampleDir,
|
||||||
|
`packages/server/src/routers/with-${orm}-todo.ts`,
|
||||||
|
);
|
||||||
|
const todoRouterTargetFile = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/server/src/routers/todo.ts",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await fs.pathExists(todoRouterSourceFile)) {
|
||||||
|
await fs.copy(todoRouterSourceFile, todoRouterTargetFile, {
|
||||||
|
overwrite: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateHeaderWithTodoLink(projectDir, auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateHeaderWithTodoLink(
|
||||||
|
projectDir: string,
|
||||||
|
auth: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
const headerPath = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/client/src/components/header.tsx",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await fs.pathExists(headerPath)) {
|
||||||
|
let headerContent = await fs.readFile(headerPath, "utf8");
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
headerContent = headerContent.replace(
|
||||||
|
/const links = \[\s*{ to: "\/", label: "Home" },\s*{ to: "\/dashboard", label: "Dashboard" },/,
|
||||||
|
`const links = [\n { to: "/", label: "Home" },\n { to: "/dashboard", label: "Dashboard" },\n { to: "/todos", label: "Todos" },`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
headerContent = headerContent.replace(
|
||||||
|
/const links = \[\s*{ to: "\/", label: "Home" },/,
|
||||||
|
`const links = [\n { to: "/", label: "Home" },\n { to: "/todos", label: "Todos" },`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(headerPath, headerContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanupTodoFiles(
|
||||||
|
projectDir: string,
|
||||||
|
orm: ProjectOrm,
|
||||||
|
): Promise<void> {
|
||||||
|
if (orm === "drizzle") {
|
||||||
|
const todoSchemaFile = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/server/src/db/schema/todo.ts",
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(todoSchemaFile)) {
|
||||||
|
await fs.remove(todoSchemaFile);
|
||||||
|
}
|
||||||
|
} else if (orm === "prisma") {
|
||||||
|
const todoPrismaFile = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/server/prisma/schema/todo.prisma",
|
||||||
|
);
|
||||||
|
if (await fs.pathExists(todoPrismaFile)) {
|
||||||
|
await fs.remove(todoPrismaFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoRouterFile = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/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,
|
||||||
|
"packages/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { log, spinner } from "@clack/prompts";
|
import { log, spinner } from "@clack/prompts";
|
||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectAddons } from "../types";
|
import type { PackageManager, ProjectAddons } from "../types";
|
||||||
import type { PackageManager } from "../utils/get-package-manager";
|
|
||||||
|
|
||||||
export async function installDependencies({
|
export async function installDependencies({
|
||||||
projectDir,
|
projectDir,
|
||||||
@@ -26,7 +25,6 @@ export async function installDependencies({
|
|||||||
})`${packageManager} install`;
|
})`${packageManager} install`;
|
||||||
break;
|
break;
|
||||||
case "pnpm":
|
case "pnpm":
|
||||||
case "yarn":
|
|
||||||
case "bun":
|
case "bun":
|
||||||
await $({
|
await $({
|
||||||
cwd: projectDir,
|
cwd: projectDir,
|
||||||
|
|||||||
76
apps/cli/src/helpers/project-config.ts
Normal file
76
apps/cli/src/helpers/project-config.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { $ } from "execa";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import type { ProjectConfig, ProjectDatabase, ProjectOrm } from "../types";
|
||||||
|
|
||||||
|
export async function updatePackageConfigurations(
|
||||||
|
projectDir: string,
|
||||||
|
options: ProjectConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
await updateRootPackageJson(projectDir, options);
|
||||||
|
await updateServerPackageJson(projectDir, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRootPackageJson(
|
||||||
|
projectDir: string,
|
||||||
|
options: ProjectConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const rootPackageJsonPath = path.join(projectDir, "package.json");
|
||||||
|
if (await fs.pathExists(rootPackageJsonPath)) {
|
||||||
|
const packageJson = await fs.readJson(rootPackageJsonPath);
|
||||||
|
packageJson.name = options.projectName;
|
||||||
|
|
||||||
|
if (options.packageManager !== "bun") {
|
||||||
|
packageJson.packageManager =
|
||||||
|
options.packageManager === "npm"
|
||||||
|
? "npm@10.9.2"
|
||||||
|
: options.packageManager === "pnpm"
|
||||||
|
? "pnpm@10.6.4"
|
||||||
|
: "bun@1.2.5";
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateServerPackageJson(
|
||||||
|
projectDir: string,
|
||||||
|
options: ProjectConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const serverPackageJsonPath = path.join(
|
||||||
|
projectDir,
|
||||||
|
"packages/server/package.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await fs.pathExists(serverPackageJsonPath)) {
|
||||||
|
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
|
||||||
|
|
||||||
|
if (options.database !== "none") {
|
||||||
|
if (options.database === "sqlite") {
|
||||||
|
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.orm === "prisma") {
|
||||||
|
serverPackageJson.scripts["db:push"] =
|
||||||
|
"prisma db push --schema ./prisma/schema";
|
||||||
|
serverPackageJson.scripts["db:studio"] = "prisma studio";
|
||||||
|
} else if (options.orm === "drizzle") {
|
||||||
|
serverPackageJson.scripts["db:push"] = "drizzle-kit push";
|
||||||
|
serverPackageJson.scripts["db:studio"] = "drizzle-kit studio";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
|
||||||
|
spaces: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeGit(
|
||||||
|
projectDir: string,
|
||||||
|
useGit: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
if (useGit) {
|
||||||
|
await $({ cwd: projectDir })`git init`;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
apps/cli/src/helpers/template-manager.ts
Normal file
83
apps/cli/src/helpers/template-manager.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import { PKG_ROOT } from "../constants";
|
||||||
|
import type { ProjectDatabase, ProjectOrm } from "../types";
|
||||||
|
|
||||||
|
export async function copyBaseTemplate(projectDir: string): Promise<void> {
|
||||||
|
const templateDir = path.join(PKG_ROOT, "template/base");
|
||||||
|
if (!(await fs.pathExists(templateDir))) {
|
||||||
|
throw new Error(`Template directory not found: ${templateDir}`);
|
||||||
|
}
|
||||||
|
await fs.copy(templateDir, projectDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupAuthTemplate(
|
||||||
|
projectDir: string,
|
||||||
|
auth: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!auth) return;
|
||||||
|
|
||||||
|
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
|
||||||
|
if (await fs.pathExists(authTemplateDir)) {
|
||||||
|
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupOrmTemplate(
|
||||||
|
projectDir: string,
|
||||||
|
orm: ProjectOrm,
|
||||||
|
database: ProjectDatabase,
|
||||||
|
auth: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
if (orm === "none" || database === "none") return;
|
||||||
|
|
||||||
|
const ormTemplateDir = path.join(PKG_ROOT, getOrmTemplateDir(orm, database));
|
||||||
|
|
||||||
|
if (await fs.pathExists(ormTemplateDir)) {
|
||||||
|
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
|
||||||
|
|
||||||
|
const serverSrcPath = path.join(projectDir, "packages/server/src");
|
||||||
|
const libPath = path.join(serverSrcPath, "lib");
|
||||||
|
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
if (await fs.pathExists(withAuthLibPath)) {
|
||||||
|
await fs.remove(libPath);
|
||||||
|
await fs.move(withAuthLibPath, libPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fs.remove(withAuthLibPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fixGitignoreFiles(projectDir: string): Promise<void> {
|
||||||
|
const gitignorePaths = [
|
||||||
|
path.join(projectDir, "_gitignore"),
|
||||||
|
path.join(projectDir, "packages/client/_gitignore"),
|
||||||
|
path.join(projectDir, "packages/server/_gitignore"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const gitignorePath of gitignorePaths) {
|
||||||
|
if (await fs.pathExists(gitignorePath)) {
|
||||||
|
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
|
||||||
|
await fs.move(gitignorePath, targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
|
||||||
|
if (orm === "drizzle") {
|
||||||
|
return database === "sqlite"
|
||||||
|
? "template/with-drizzle-sqlite"
|
||||||
|
: "template/with-drizzle-postgres";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orm === "prisma") {
|
||||||
|
return database === "sqlite"
|
||||||
|
? "template/with-prisma-sqlite"
|
||||||
|
: "template/with-prisma-postgres";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "template/base";
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { DEFAULT_CONFIG } from "./constants";
|
|||||||
import { createProject } from "./helpers/create-project";
|
import { createProject } from "./helpers/create-project";
|
||||||
import { installDependencies } from "./helpers/install-dependencies";
|
import { installDependencies } from "./helpers/install-dependencies";
|
||||||
import { gatherConfig } from "./prompts/config-prompts";
|
import { gatherConfig } from "./prompts/config-prompts";
|
||||||
import type { ProjectAddons, ProjectConfig } from "./types";
|
import type { ProjectAddons, ProjectConfig, ProjectExamples } 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";
|
||||||
@@ -35,11 +35,12 @@ async function main() {
|
|||||||
.option("--biome", "Include Biome for linting and formatting")
|
.option("--biome", "Include Biome for linting and formatting")
|
||||||
.option("--husky", "Include Husky, lint-staged for Git hooks")
|
.option("--husky", "Include Husky, lint-staged for Git hooks")
|
||||||
.option("--no-addons", "Skip all additional addons")
|
.option("--no-addons", "Skip all additional addons")
|
||||||
|
.option("--examples <examples>", "Include specified examples")
|
||||||
|
.option("--no-examples", "Skip all examples")
|
||||||
.option("--git", "Include git setup")
|
.option("--git", "Include git setup")
|
||||||
.option("--no-git", "Skip git initialization")
|
.option("--no-git", "Skip git initialization")
|
||||||
.option("--npm", "Use npm package manager")
|
.option("--npm", "Use npm package manager")
|
||||||
.option("--pnpm", "Use pnpm package manager")
|
.option("--pnpm", "Use pnpm package manager")
|
||||||
.option("--yarn", "Use yarn package manager")
|
|
||||||
.option("--bun", "Use bun package manager")
|
.option("--bun", "Use bun package manager")
|
||||||
.option("--drizzle", "Use Drizzle ORM")
|
.option("--drizzle", "Use Drizzle ORM")
|
||||||
.option("--prisma", "Use Prisma ORM (coming soon)")
|
.option("--prisma", "Use Prisma ORM (coming soon)")
|
||||||
@@ -68,7 +69,6 @@ async function main() {
|
|||||||
...("auth" in options && { auth: options.auth }),
|
...("auth" in options && { auth: options.auth }),
|
||||||
...(options.npm && { packageManager: "npm" }),
|
...(options.npm && { packageManager: "npm" }),
|
||||||
...(options.pnpm && { packageManager: " pnpm" }),
|
...(options.pnpm && { packageManager: " pnpm" }),
|
||||||
...(options.yarn && { packageManager: "yarn" }),
|
|
||||||
...(options.bun && { packageManager: "bun" }),
|
...(options.bun && { packageManager: "bun" }),
|
||||||
...("git" in options && { git: options.git }),
|
...("git" in options && { git: options.git }),
|
||||||
...("install" in options && { noInstall: !options.install }),
|
...("install" in options && { noInstall: !options.install }),
|
||||||
@@ -88,6 +88,16 @@ async function main() {
|
|||||||
...(options.husky ? ["husky"] : []),
|
...(options.husky ? ["husky"] : []),
|
||||||
] as ProjectAddons[]),
|
] as ProjectAddons[]),
|
||||||
}),
|
}),
|
||||||
|
...((options.examples || options.examples === false) && {
|
||||||
|
examples:
|
||||||
|
options.examples === false
|
||||||
|
? []
|
||||||
|
: typeof options.examples === "string"
|
||||||
|
? (options.examples
|
||||||
|
.split(",")
|
||||||
|
.filter((e) => e === "todo") as ProjectExamples[])
|
||||||
|
: [],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
if (!options.yes && Object.keys(flagConfig).length > 0) {
|
||||||
@@ -123,6 +133,9 @@ async function main() {
|
|||||||
addons: flagConfig.addons?.length
|
addons: flagConfig.addons?.length
|
||||||
? flagConfig.addons
|
? flagConfig.addons
|
||||||
: DEFAULT_CONFIG.addons,
|
: DEFAULT_CONFIG.addons,
|
||||||
|
examples: flagConfig.examples?.length
|
||||||
|
? flagConfig.examples
|
||||||
|
: DEFAULT_CONFIG.examples,
|
||||||
turso:
|
turso:
|
||||||
"turso" in options
|
"turso" in options
|
||||||
? options.turso
|
? options.turso
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import type {
|
|||||||
ProjectAddons,
|
ProjectAddons,
|
||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
ProjectDatabase,
|
ProjectDatabase,
|
||||||
|
ProjectExamples,
|
||||||
ProjectOrm,
|
ProjectOrm,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { getAddonsChoice } from "./addons";
|
import { getAddonsChoice } from "./addons";
|
||||||
import { getAuthChoice } from "./auth";
|
import { getAuthChoice } from "./auth";
|
||||||
import { getDatabaseChoice } from "./database";
|
import { getDatabaseChoice } from "./database";
|
||||||
|
import { getExamplesChoice } from "./examples";
|
||||||
import { getGitChoice } from "./git";
|
import { getGitChoice } from "./git";
|
||||||
import { getNoInstallChoice } from "./install";
|
import { getNoInstallChoice } from "./install";
|
||||||
import { getORMChoice } from "./orm";
|
import { getORMChoice } from "./orm";
|
||||||
@@ -23,6 +25,7 @@ interface PromptGroupResults {
|
|||||||
orm: ProjectOrm;
|
orm: ProjectOrm;
|
||||||
auth: boolean;
|
auth: boolean;
|
||||||
addons: ProjectAddons[];
|
addons: ProjectAddons[];
|
||||||
|
examples: ProjectExamples[];
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
noInstall: boolean;
|
noInstall: boolean;
|
||||||
@@ -47,6 +50,7 @@ export async function gatherConfig(
|
|||||||
? getTursoSetupChoice(flags.turso)
|
? getTursoSetupChoice(flags.turso)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
addons: () => getAddonsChoice(flags.addons),
|
addons: () => getAddonsChoice(flags.addons),
|
||||||
|
examples: () => getExamplesChoice(flags.examples),
|
||||||
git: () => getGitChoice(flags.git),
|
git: () => getGitChoice(flags.git),
|
||||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||||
noInstall: () => getNoInstallChoice(flags.noInstall),
|
noInstall: () => getNoInstallChoice(flags.noInstall),
|
||||||
@@ -65,6 +69,7 @@ export async function gatherConfig(
|
|||||||
orm: result.orm,
|
orm: result.orm,
|
||||||
auth: result.auth,
|
auth: result.auth,
|
||||||
addons: result.addons,
|
addons: result.addons,
|
||||||
|
examples: result.examples,
|
||||||
git: result.git,
|
git: result.git,
|
||||||
packageManager: result.packageManager,
|
packageManager: result.packageManager,
|
||||||
noInstall: result.noInstall,
|
noInstall: result.noInstall,
|
||||||
|
|||||||
30
apps/cli/src/prompts/examples.ts
Normal file
30
apps/cli/src/prompts/examples.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
|
import type { ProjectExamples } from "../types";
|
||||||
|
|
||||||
|
export async function getExamplesChoice(
|
||||||
|
examples?: ProjectExamples[],
|
||||||
|
): Promise<ProjectExamples[]> {
|
||||||
|
if (examples !== undefined) return examples;
|
||||||
|
|
||||||
|
const response = await multiselect<ProjectExamples>({
|
||||||
|
message: "Which examples would you like to include?",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "todo",
|
||||||
|
label: "Todo App",
|
||||||
|
hint: "A simple CRUD example app",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
required: false,
|
||||||
|
initialValues: DEFAULT_CONFIG.examples,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(response)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -24,11 +24,6 @@ export async function getPackageManagerChoice(
|
|||||||
label: "pnpm",
|
label: "pnpm",
|
||||||
hint: "Fast, disk space efficient package manager",
|
hint: "Fast, disk space efficient package manager",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "yarn",
|
|
||||||
label: "yarn",
|
|
||||||
hint: "Fast, reliable, and secure dependency management",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
initialValue: detectedPackageManager,
|
initialValue: detectedPackageManager,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export type ProjectDatabase = "sqlite" | "postgres" | "none";
|
export type ProjectDatabase = "sqlite" | "postgres" | "none";
|
||||||
export type ProjectOrm = "drizzle" | "prisma" | "none";
|
export type ProjectOrm = "drizzle" | "prisma" | "none";
|
||||||
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
export type PackageManager = "npm" | "pnpm" | "bun";
|
||||||
export type ProjectAddons = "pwa" | "tauri" | "biome" | "husky";
|
export type ProjectAddons = "pwa" | "tauri" | "biome" | "husky";
|
||||||
|
export type ProjectExamples = "todo";
|
||||||
|
|
||||||
export interface ProjectConfig {
|
export interface ProjectConfig {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@@ -9,6 +10,7 @@ export interface ProjectConfig {
|
|||||||
orm: ProjectOrm;
|
orm: ProjectOrm;
|
||||||
auth: boolean;
|
auth: boolean;
|
||||||
addons: ProjectAddons[];
|
addons: ProjectAddons[];
|
||||||
|
examples: ProjectExamples[];
|
||||||
git: boolean;
|
git: boolean;
|
||||||
packageManager: PackageManager;
|
packageManager: PackageManager;
|
||||||
noInstall?: boolean;
|
noInstall?: boolean;
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
|||||||
flags.push("--no-addons");
|
flags.push("--no-addons");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.examples && config.examples.length > 0) {
|
||||||
|
flags.push(`--examples ${config.examples.join(",")}`);
|
||||||
|
} else {
|
||||||
|
flags.push("--no-examples");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.database === "sqlite") {
|
||||||
|
if (config.turso) {
|
||||||
|
flags.push("--turso");
|
||||||
|
} else {
|
||||||
|
flags.push("--no-turso");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseCommand = "npx create-better-t-stack";
|
const baseCommand = "npx create-better-t-stack";
|
||||||
const projectName = config.projectName ? ` ${config.projectName}` : "";
|
const projectName = config.projectName ? ` ${config.projectName}` : "";
|
||||||
const flagString = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
const flagString = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
import type { PackageManager } from "../types";
|
||||||
|
|
||||||
export const getUserPkgManager: () => PackageManager = () => {
|
export const getUserPkgManager: () => PackageManager = () => {
|
||||||
const userAgent = process.env.npm_config_user_agent;
|
const userAgent = process.env.npm_config_user_agent;
|
||||||
|
|
||||||
if (userAgent?.startsWith("yarn")) {
|
|
||||||
return "yarn";
|
|
||||||
}
|
|
||||||
if (userAgent?.startsWith("pnpm")) {
|
if (userAgent?.startsWith("pnpm")) {
|
||||||
return "pnpm";
|
return "pnpm";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,26 @@ import { Link } from "@tanstack/react-router";
|
|||||||
import { ModeToggle } from "./mode-toggle";
|
import { ModeToggle } from "./mode-toggle";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const links = [
|
||||||
|
{ to: "/", label: "Home" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
<div className="flex flex-row items-center justify-between px-2 py-1">
|
||||||
<div className="flex gap-4 text-lg">
|
<nav className="flex gap-4 text-lg">
|
||||||
<Link
|
{links.map(({ to, label }) => (
|
||||||
to="/"
|
<Link
|
||||||
activeProps={{
|
key={to}
|
||||||
className: "font-bold",
|
to={to}
|
||||||
}}
|
activeProps={{ className: "font-bold" }}
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
>
|
>
|
||||||
Home
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
))}
|
||||||
to="/todos"
|
</nav>
|
||||||
activeProps={{
|
<div className="flex items-center gap-2">
|
||||||
className: "font-bold",
|
|
||||||
}}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
Todos
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ app.use(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get("/healthCheck", (c) => {
|
app.get("/", (c) => {
|
||||||
return c.text("OK");
|
return c.text("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { router, publicProcedure } from "../lib/trpc";
|
import { router, publicProcedure } from "../lib/trpc";
|
||||||
import { todo } from "../db/schema";
|
import { todo } from "../db/schema/todo";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
|
|
||||||
@@ -3,39 +3,27 @@ import { ModeToggle } from "./mode-toggle";
|
|||||||
import UserMenu from "./user-menu";
|
import UserMenu from "./user-menu";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const links = [
|
||||||
|
{ to: "/", label: "Home" },
|
||||||
|
{ to: "/dashboard", label: "Dashboard" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center justify-between px-2 py-1">
|
<div className="flex flex-row items-center justify-between px-2 py-1">
|
||||||
<div className="flex gap-4 text-lg">
|
<nav className="flex gap-4 text-lg">
|
||||||
<Link
|
{links.map(({ to, label }) => (
|
||||||
to="/"
|
<Link
|
||||||
activeProps={{
|
key={to}
|
||||||
className: "font-bold",
|
to={to}
|
||||||
}}
|
activeProps={{ className: "font-bold" }}
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
>
|
>
|
||||||
Home
|
{label}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
))}
|
||||||
to="/todos"
|
</nav>
|
||||||
activeProps={{
|
<div className="flex items-center gap-2">
|
||||||
className: "font-bold",
|
|
||||||
}}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
Todos
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/dashboard"
|
|
||||||
activeProps={{
|
|
||||||
className: "font-bold",
|
|
||||||
}}
|
|
||||||
activeOptions={{ exact: true }}
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ app.use(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.get("/healthCheck", (c) => {
|
app.get("/", (c) => {
|
||||||
return c.text("OK");
|
return c.text("OK");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/db/schema.ts",
|
schema: "./src/db/schema",
|
||||||
out: "./migrations",
|
out: "./src/db/migrations",
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.POSTGRES_URL || "",
|
url: process.env.POSTGRES_URL || "",
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
|
import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const todo = pgTable("todo", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
text: text("text").notNull(),
|
|
||||||
completed: boolean("completed").default(false).notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
|
import { pgTable, text, boolean, serial } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const todo = pgTable("todo", {
|
export const todo = pgTable("todo", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema";
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/db/schema.ts",
|
schema: "./src/db/schema",
|
||||||
out: "./migrations",
|
out: "./src/db/migrations",
|
||||||
dialect: "turso",
|
dialect: "turso",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.TURSO_CONNECTION_URL || "",
|
url: process.env.TURSO_CONNECTION_URL || "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema";
|
import * as schema from "../db/schema/auth";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "sqlite"
|
|
||||||
url = "file:./local.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
model Todo {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
text String
|
|
||||||
completed Boolean @default(false)
|
|
||||||
|
|
||||||
@@map("todo")
|
|
||||||
}
|
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @map("_id")
|
id String @id @map("_id")
|
||||||
name String
|
name String
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
previewFeatures = ["prismaSchemaFolder"]
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgres"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "sqlite"
|
|
||||||
url = "file:./local.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
model Todo {
|
model Todo {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
text String
|
text String
|
||||||
@@ -1,20 +1,3 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgres"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Todo {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
text String
|
|
||||||
completed Boolean @default(false)
|
|
||||||
|
|
||||||
@@map("todo")
|
|
||||||
}
|
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @map("_id")
|
id String @id @map("_id")
|
||||||
name String
|
name String
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:../local.db"
|
||||||
|
}
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgres"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Todo {
|
model Todo {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
text String
|
text String
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import prisma from "../../prisma";
|
|
||||||
import { publicProcedure, router } from "../lib/trpc";
|
|
||||||
|
|
||||||
export const todoRouter = router({
|
|
||||||
getAll: publicProcedure.query(async () => {
|
|
||||||
return await prisma.todo.findMany({
|
|
||||||
orderBy: {
|
|
||||||
id: "asc"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: publicProcedure
|
|
||||||
.input(z.object({ text: z.string().min(1) }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await prisma.todo.create({
|
|
||||||
data: {
|
|
||||||
text: input.text,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
toggle: publicProcedure
|
|
||||||
.input(z.object({ id: z.number(), completed: z.boolean() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
try {
|
|
||||||
return await prisma.todo.update({
|
|
||||||
where: { id: input.id },
|
|
||||||
data: { completed: input.completed },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Todo not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: publicProcedure
|
|
||||||
.input(z.object({ id: z.number() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
try {
|
|
||||||
return await prisma.todo.delete({
|
|
||||||
where: { id: input.id },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "Todo not found",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,26 +1,47 @@
|
|||||||
# web
|
# Better-T-Stack Website
|
||||||
|
|
||||||
This is a Next.js application generated with
|
This is the official documentation website for Better-T-Stack, built with Next.js and Fumadocs.
|
||||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
|
||||||
|
|
||||||
Run development server:
|
## Getting Started
|
||||||
|
|
||||||
|
To run the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
# or
|
||||||
|
pnpm install
|
||||||
|
# or
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Start development server
|
||||||
npm run dev
|
npm run dev
|
||||||
# or
|
# or
|
||||||
pnpm dev
|
pnpm dev
|
||||||
# or
|
# or
|
||||||
yarn dev
|
bun dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:3000 with your browser to see the result.
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the site.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `/src/app` - Next.js application routes
|
||||||
|
- `/src/content` - Documentation content in MDX format
|
||||||
|
- `/public` - Static assets
|
||||||
|
|
||||||
|
## Contributing to Documentation
|
||||||
|
|
||||||
|
To add or modify documentation:
|
||||||
|
|
||||||
|
1. Edit the appropriate MDX files in the `src/content` directory
|
||||||
|
2. Run the development server to preview your changes
|
||||||
|
3. Submit a pull request with your updates
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js and Fumadocs, take a look at the following
|
To learn more about the technologies used in this website:
|
||||||
resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
- [Next.js Documentation](https://nextjs.org/docs) - Next.js features and API
|
||||||
features and API.
|
- [Fumadocs](https://fumadocs.vercel.app) - The documentation framework used
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
- [Better-T-Stack](https://better-t-stack.pages.dev) - Main project site
|
||||||
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
|
|
||||||
const CodeContainer = () => {
|
const CodeContainer = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectedPM, setSelectedPM] = useState<"npm" | "yarn" | "pnpm" | "bun">(
|
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("npm");
|
||||||
"npm",
|
|
||||||
);
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const [typingComplete, setTypingComplete] = useState(false);
|
const [typingComplete, setTypingComplete] = useState(false);
|
||||||
@@ -24,12 +22,11 @@ const CodeContainer = () => {
|
|||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
npm: "npx create-better-t-stack@latest",
|
npm: "npx create-better-t-stack@latest",
|
||||||
yarn: "yarn dlx create-better-t-stack",
|
|
||||||
pnpm: "pnpm dlx create-better-t-stack",
|
pnpm: "pnpm dlx create-better-t-stack",
|
||||||
bun: "bunx create-better-t-stack",
|
bun: "bunx create-better-t-stack",
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyToClipboard = async (pm: "npm" | "yarn" | "pnpm" | "bun") => {
|
const copyToClipboard = async (pm: "npm" | "pnpm" | "bun") => {
|
||||||
await navigator.clipboard.writeText(commands[pm]);
|
await navigator.clipboard.writeText(commands[pm]);
|
||||||
setSelectedPM(pm);
|
setSelectedPM(pm);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
@@ -93,25 +90,23 @@ const CodeContainer = () => {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute right-0 mt-2 w-36 bg-black border border-blue-500/30 rounded-md shadow-lg z-50">
|
<div className="absolute right-0 mt-2 w-36 bg-black border border-blue-500/30 rounded-md shadow-lg z-50">
|
||||||
<ul>
|
<ul>
|
||||||
{(
|
{(Object.keys(commands) as Array<"npm" | "pnpm" | "bun">).map(
|
||||||
Object.keys(commands) as Array<
|
(pm) => (
|
||||||
"npm" | "yarn" | "pnpm" | "bun"
|
<li key={pm}>
|
||||||
>
|
<button
|
||||||
).map((pm) => (
|
type="button"
|
||||||
<li key={pm}>
|
className={`block w-full text-left px-4 py-2 text-sm ${
|
||||||
<button
|
selectedPM === pm
|
||||||
type="button"
|
? "bg-blue-900/30 text-blue-400"
|
||||||
className={`block w-full text-left px-4 py-2 text-sm ${
|
: "text-gray-300 hover:bg-blue-900/20"
|
||||||
selectedPM === pm
|
}`}
|
||||||
? "bg-blue-900/30 text-blue-400"
|
onClick={() => copyToClipboard(pm)}
|
||||||
: "text-gray-300 hover:bg-blue-900/20"
|
>
|
||||||
}`}
|
{pm}
|
||||||
onClick={() => copyToClipboard(pm)}
|
</button>
|
||||||
>
|
</li>
|
||||||
{pm}
|
),
|
||||||
</button>
|
)}
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,30 +7,6 @@ const PackageIcon = ({ pm, className }: { pm: string; className?: string }) => {
|
|||||||
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
|
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
case "yarn":
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={className}
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit="2"
|
|
||||||
>
|
|
||||||
<title>yarn</title>
|
|
||||||
<path
|
|
||||||
d="M256 0c141.344 0 256 114.656 256 256S397.344 512 256 512 0 397.344 0 256 114.656 0 256 0z"
|
|
||||||
fill="#2c8ebb"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M430.16 333.59c-1.78-14.035-13.641-23.721-28.863-23.524-22.733.297-41.81 12.06-54.461 19.868-4.943 3.064-9.193 5.337-12.85 7.017.79-11.465.099-26.49-5.832-42.996-7.215-19.768-16.901-31.926-23.82-38.943 8.006-11.664 18.977-28.665 24.117-54.956 4.448-22.437 3.064-57.329-7.117-76.9-2.075-3.953-5.535-6.82-9.884-8.005-1.779-.495-5.14-1.483-11.762.395-9.983-20.658-13.442-22.832-16.111-24.612-5.535-3.558-12.059-4.349-18.187-2.075-8.204 2.965-15.222 10.872-21.844 24.908-.988 2.075-1.878 4.052-2.669 6.03-12.553.889-32.321 5.435-49.025 23.523-2.076 2.274-6.128 3.954-10.379 5.536h.1c-8.699 3.064-12.653 10.18-17.496 23.03-6.721 17.989.198 35.682 7.018 47.147-9.291 8.303-21.646 21.548-28.17 37.066-8.105 19.175-8.994 37.955-8.698 48.136-6.919 7.314-17.594 21.053-18.78 36.472-1.581 21.548 6.227 36.176 9.687 41.514.988 1.581 2.075 2.866 3.261 4.151-.395 2.669-.494 5.535.1 8.5 1.284 6.92 5.633 12.553 12.256 16.112 13.047 6.919 31.234 9.884 45.27 2.866 5.04 5.338 14.232 10.477 30.937 10.477h.988c4.25 0 58.218-2.866 73.934-6.72 7.017-1.681 11.86-4.646 15.023-7.315 10.082-3.163 37.956-12.652 64.248-29.653 18.582-12.058 25.007-14.628 38.844-17.989 13.443-3.262 21.844-15.518 20.164-29.06zm-23.525 14.53c-15.815 3.756-23.821 7.216-43.392 19.966-30.542 19.769-63.95 28.961-63.95 28.961s-2.768 4.151-10.774 6.03c-13.838 3.36-65.927 6.226-70.672 6.325-12.75.1-20.559-3.261-22.733-8.5-6.623-15.815 9.488-22.734 9.488-22.734s-3.558-2.174-5.634-4.151c-1.878-1.878-3.854-5.634-4.448-4.25-2.47 6.03-3.756 20.757-10.378 27.379-9.093 9.192-26.292 6.128-36.473.79-11.169-5.93.791-19.866.791-19.866s-6.03 3.558-10.872-3.756c-4.35-6.722-8.402-18.187-7.315-32.322 1.186-16.11 19.176-31.728 19.176-31.728s-3.163-23.82 7.215-48.235c9.39-22.239 34.694-40.13 34.694-40.13s-21.251-23.524-13.344-44.676c5.14-13.838 7.215-13.739 8.896-14.332 5.93-2.273 11.663-4.744 15.913-9.39 21.251-22.931 48.334-18.582 48.334-18.582s12.85-39.043 24.71-31.432c3.657 2.372 16.803 31.63 16.803 31.63s14.036-8.204 15.617-5.14c8.5 16.506 9.49 48.037 5.733 67.212-6.326 31.63-22.14 48.63-28.466 59.305-1.483 2.471 17 10.28 28.664 42.601 10.774 29.554 1.186 54.363 2.866 57.13.297.495.396.692.396.692s12.355.989 37.164-14.332c13.245-8.204 28.96-17.396 46.851-17.593 17.297-.297 18.187 19.966 5.14 23.128z"
|
|
||||||
fill="#fff"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case "pnpm":
|
case "pnpm":
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ const techOptions: Record<string, TechOption[]> = {
|
|||||||
packageManager: [
|
packageManager: [
|
||||||
{ id: "npm", label: "NPM", category: "packageManager" },
|
{ id: "npm", label: "NPM", category: "packageManager" },
|
||||||
{ id: "pnpm", label: "PNPM", category: "packageManager" },
|
{ id: "pnpm", label: "PNPM", category: "packageManager" },
|
||||||
{ id: "yarn", label: "Yarn", category: "packageManager" },
|
|
||||||
{ id: "bun", label: "Bun", category: "packageManager" },
|
{ id: "bun", label: "Bun", category: "packageManager" },
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
|
|||||||
Reference in New Issue
Block a user