create-better-t-stack 3.27.5 → 3.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/dist/{src-VGvTc2ik.mjs → src-CHenuE55.mjs} +610 -25
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,21 +29,21 @@ Follow the prompts to configure your project or use the `--yes` flag for default
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
| Category | Options
|
|
33
|
-
| ------------------------ |
|
|
34
|
-
| **TypeScript** | End-to-end type safety across all parts of your application
|
|
35
|
-
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None
|
|
36
|
-
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None
|
|
37
|
-
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None
|
|
38
|
-
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None
|
|
39
|
-
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None
|
|
40
|
-
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None
|
|
41
|
-
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup)
|
|
42
|
-
| **Authentication** | • Better Auth<br>• Clerk
|
|
43
|
-
| **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps
|
|
44
|
-
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
|
|
45
|
-
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK)
|
|
46
|
-
| **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation
|
|
32
|
+
| Category | Options |
|
|
33
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| **TypeScript** | End-to-end type safety across all parts of your application |
|
|
35
|
+
| **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
|
|
36
|
+
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
|
|
37
|
+
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
|
|
38
|
+
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
|
|
39
|
+
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
|
|
40
|
+
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
|
|
41
|
+
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
|
|
42
|
+
| **Authentication** | • Better Auth<br>• Clerk |
|
|
43
|
+
| **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
|
|
44
|
+
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
|
|
45
|
+
| **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
|
|
46
|
+
| **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
@@ -60,7 +60,7 @@ Options:
|
|
|
60
60
|
--auth <provider> Authentication (better-auth, clerk, none)
|
|
61
61
|
--payments <provider> Payments provider (polar, none)
|
|
62
62
|
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, astro, native-bare, native-uniwind, native-unistyles, none)
|
|
63
|
-
--addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, none)
|
|
63
|
+
--addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
|
|
64
64
|
--examples <types...> Examples to include (todo, ai, none)
|
|
65
65
|
--git Initialize git repository
|
|
66
66
|
--no-git Skip git initialization
|
|
@@ -239,7 +239,7 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
|
|
|
239
239
|
- **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
|
|
240
240
|
- **Runtime 'none'**: Only available with Convex backend, backend `none`, or backend `self`.
|
|
241
241
|
- **Cloudflare Workers runtime**: Only compatible with Hono backend. If a database is used, MongoDB is not supported.
|
|
242
|
-
- **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, and `astro`.
|
|
242
|
+
- **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, and `astro`.
|
|
243
243
|
- **Addons 'none'**: Skips all addons.
|
|
244
244
|
- **Examples 'none'**: Skips all example implementations (todo, AI chat).
|
|
245
245
|
- **Nuxt, Svelte, SolidJS, and Astro** frontends are only compatible with oRPC API layer
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-
|
|
2
|
+
import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-CHenuE55.mjs";
|
|
3
3
|
import z from "zod";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
package/dist/index.d.mts
CHANGED
|
@@ -174,7 +174,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
174
174
|
auth?: "none" | "better-auth" | "clerk" | undefined;
|
|
175
175
|
payments?: "none" | "polar" | undefined;
|
|
176
176
|
frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
|
|
177
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
|
|
177
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
178
178
|
examples?: ("none" | "todo" | "ai")[] | undefined;
|
|
179
179
|
git?: boolean | undefined;
|
|
180
180
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
@@ -241,7 +241,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
241
241
|
scope?: "project" | "global" | undefined;
|
|
242
242
|
agents?: ("antigravity" | "cline" | "cursor" | "claude-code" | "codex" | "opencode" | "gemini-cli" | "goose" | "github-copilot" | "windsurf" | "roo" | "kilo" | "openhands" | "trae" | "amp" | "pi" | "qoder" | "qwen-code" | "kiro-cli" | "droid" | "command-code" | "clawdbot" | "zencoder" | "neovate" | "mcpjam")[] | undefined;
|
|
243
243
|
selections?: {
|
|
244
|
-
source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite";
|
|
244
|
+
source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
|
|
245
245
|
skills: string[];
|
|
246
246
|
}[] | undefined;
|
|
247
247
|
} | undefined;
|
|
@@ -273,7 +273,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
273
273
|
auth?: "none" | "better-auth" | "clerk" | undefined;
|
|
274
274
|
payments?: "none" | "polar" | undefined;
|
|
275
275
|
frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
|
|
276
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
|
|
276
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
277
277
|
examples?: ("none" | "todo" | "ai")[] | undefined;
|
|
278
278
|
git?: boolean | undefined;
|
|
279
279
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
@@ -316,7 +316,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
316
316
|
}>;
|
|
317
317
|
add: _$_trpc_server0.TRPCMutationProcedure<{
|
|
318
318
|
input: {
|
|
319
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
|
|
319
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
320
320
|
install?: boolean | undefined;
|
|
321
321
|
packageManager?: "bun" | "npm" | "pnpm" | undefined;
|
|
322
322
|
projectDir?: string | undefined;
|
|
@@ -326,7 +326,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
326
326
|
}>;
|
|
327
327
|
addJson: _$_trpc_server0.TRPCMutationProcedure<{
|
|
328
328
|
input: {
|
|
329
|
-
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
|
|
329
|
+
addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
|
|
330
330
|
addonOptions?: {
|
|
331
331
|
wxt?: {
|
|
332
332
|
template: "svelte" | "solid" | "vanilla" | "vue" | "react";
|
|
@@ -351,7 +351,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
|
|
|
351
351
|
scope?: "project" | "global" | undefined;
|
|
352
352
|
agents?: ("antigravity" | "cline" | "cursor" | "claude-code" | "codex" | "opencode" | "gemini-cli" | "goose" | "github-copilot" | "windsurf" | "roo" | "kilo" | "openhands" | "trae" | "amp" | "pi" | "qoder" | "qwen-code" | "kiro-cli" | "droid" | "command-code" | "clawdbot" | "zencoder" | "neovate" | "mcpjam")[] | undefined;
|
|
353
353
|
selections?: {
|
|
354
|
-
source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite";
|
|
354
|
+
source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
|
|
355
355
|
skills: string[];
|
|
356
356
|
}[] | undefined;
|
|
357
357
|
} | undefined;
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-
|
|
2
|
+
import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-CHenuE55.mjs";
|
|
3
3
|
export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
|
|
@@ -86,6 +86,7 @@ const ADDON_COMPATIBILITY = {
|
|
|
86
86
|
opentui: [],
|
|
87
87
|
wxt: [],
|
|
88
88
|
skills: [],
|
|
89
|
+
evlog: [],
|
|
89
90
|
none: []
|
|
90
91
|
};
|
|
91
92
|
//#endregion
|
|
@@ -757,7 +758,7 @@ function splitFrontends(values = []) {
|
|
|
757
758
|
}
|
|
758
759
|
function ensureSingleWebAndNative(frontends) {
|
|
759
760
|
const { web, native } = splitFrontends(frontends);
|
|
760
|
-
if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
761
|
+
if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid, astro");
|
|
761
762
|
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
|
|
762
763
|
return Result.ok(void 0);
|
|
763
764
|
}
|
|
@@ -765,18 +766,36 @@ const FULLSTACK_FRONTENDS$1 = [
|
|
|
765
766
|
"next",
|
|
766
767
|
"tanstack-start",
|
|
767
768
|
"nuxt",
|
|
769
|
+
"svelte",
|
|
768
770
|
"astro"
|
|
769
771
|
];
|
|
772
|
+
const EVLOG_SERVER_BACKENDS = [
|
|
773
|
+
"hono",
|
|
774
|
+
"express",
|
|
775
|
+
"fastify",
|
|
776
|
+
"elysia"
|
|
777
|
+
];
|
|
778
|
+
const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
|
|
779
|
+
const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
|
|
780
|
+
function supportsEvlogAddon(frontend = [], backend, _runtime) {
|
|
781
|
+
if (!backend) return true;
|
|
782
|
+
if (EVLOG_SERVER_BACKENDS.includes(backend)) return true;
|
|
783
|
+
if (backend === "self") {
|
|
784
|
+
if (frontend.length === 0) return true;
|
|
785
|
+
return frontend.some((f) => EVLOG_FULLSTACK_FRONTENDS.includes(f));
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
770
789
|
function validateSelfBackendCompatibility(providedFlags, options, config) {
|
|
771
790
|
const backend = config.backend || options.backend;
|
|
772
791
|
const frontends = config.frontend || options.frontend || [];
|
|
773
792
|
if (backend === "self") {
|
|
774
793
|
const { web, native } = splitFrontends(frontends);
|
|
775
|
-
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, or --frontend astro.
|
|
794
|
+
if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.");
|
|
776
795
|
if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
|
|
777
796
|
}
|
|
778
797
|
const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
|
|
779
|
-
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend astro, or choose a different backend.
|
|
798
|
+
if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend astro, or choose a different backend.");
|
|
780
799
|
return Result.ok(void 0);
|
|
781
800
|
}
|
|
782
801
|
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
@@ -849,7 +868,11 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
|
|
|
849
868
|
if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
|
|
850
869
|
return Result.ok(void 0);
|
|
851
870
|
}
|
|
852
|
-
function validateAddonCompatibility(addon, frontend, _auth) {
|
|
871
|
+
function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
|
|
872
|
+
if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
|
|
873
|
+
isCompatible: false,
|
|
874
|
+
reason: evlogCompatibilityMessage
|
|
875
|
+
};
|
|
853
876
|
const compatibleFrontends = ADDON_COMPATIBILITY[addon];
|
|
854
877
|
if (compatibleFrontends.length > 0) {
|
|
855
878
|
if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
|
|
@@ -859,23 +882,26 @@ function validateAddonCompatibility(addon, frontend, _auth) {
|
|
|
859
882
|
}
|
|
860
883
|
return { isCompatible: true };
|
|
861
884
|
}
|
|
862
|
-
function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
|
|
885
|
+
function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, backend, runtime) {
|
|
863
886
|
return allAddons.filter((addon) => {
|
|
864
887
|
if (existingAddons.includes(addon)) return false;
|
|
865
888
|
if (addon === "none") return false;
|
|
866
|
-
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
|
|
889
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
|
|
867
890
|
return isCompatible;
|
|
868
891
|
});
|
|
869
892
|
}
|
|
870
|
-
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
|
|
893
|
+
function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
|
|
871
894
|
if (addons.includes("turborepo") && addons.includes("nx")) return validationErr$1("Cannot combine 'turborepo' and 'nx' addons. Choose one monorepo tool.");
|
|
872
895
|
for (const addon of addons) {
|
|
873
896
|
if (addon === "none") continue;
|
|
874
|
-
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
|
|
897
|
+
const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
|
|
875
898
|
if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
|
|
876
899
|
}
|
|
877
900
|
return Result.ok(void 0);
|
|
878
901
|
}
|
|
902
|
+
function validateAddonsAgainstConfig(addons = [], config) {
|
|
903
|
+
return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
|
|
904
|
+
}
|
|
879
905
|
function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
|
|
880
906
|
if (!payments || payments === "none") return Result.ok(void 0);
|
|
881
907
|
if (payments === "polar") {
|
|
@@ -1207,6 +1233,10 @@ function getAddonDisplay(addon) {
|
|
|
1207
1233
|
label = "MCP";
|
|
1208
1234
|
hint = "Install MCP servers, including Better T Stack, via add-mcp";
|
|
1209
1235
|
break;
|
|
1236
|
+
case "evlog":
|
|
1237
|
+
label = "evlog";
|
|
1238
|
+
hint = "Request logging with Better Auth context and AI SDK telemetry";
|
|
1239
|
+
break;
|
|
1210
1240
|
default:
|
|
1211
1241
|
label = addon;
|
|
1212
1242
|
hint = `Add ${addon}`;
|
|
@@ -1233,6 +1263,7 @@ const ADDON_GROUPS = {
|
|
|
1233
1263
|
"opentui",
|
|
1234
1264
|
"wxt"
|
|
1235
1265
|
],
|
|
1266
|
+
Observability: ["evlog"],
|
|
1236
1267
|
"AI & Agent Tools": ["skills", "mcp"]
|
|
1237
1268
|
};
|
|
1238
1269
|
function createGroupedOptions() {
|
|
@@ -1259,13 +1290,13 @@ function sortAndPruneGroupedOptions(groupedOptions) {
|
|
|
1259
1290
|
function validateAddonSelection(selected) {
|
|
1260
1291
|
if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
|
|
1261
1292
|
}
|
|
1262
|
-
async function getAddonsChoice(addons, frontends, auth) {
|
|
1293
|
+
async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
|
|
1263
1294
|
if (addons !== void 0) return addons;
|
|
1264
1295
|
const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
|
|
1265
1296
|
const groupedOptions = createGroupedOptions();
|
|
1266
1297
|
const frontendsArray = frontends || [];
|
|
1267
1298
|
for (const addon of allAddons) {
|
|
1268
|
-
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
|
|
1299
|
+
const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth, backend, runtime);
|
|
1269
1300
|
if (!isCompatible) continue;
|
|
1270
1301
|
const { label, hint } = getAddonDisplay(addon);
|
|
1271
1302
|
addOptionToGroup(groupedOptions, {
|
|
@@ -1285,10 +1316,10 @@ async function getAddonsChoice(addons, frontends, auth) {
|
|
|
1285
1316
|
if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
|
|
1286
1317
|
return response;
|
|
1287
1318
|
}
|
|
1288
|
-
async function getAddonsToAdd(
|
|
1319
|
+
async function getAddonsToAdd(config) {
|
|
1289
1320
|
const groupedOptions = createGroupedOptions();
|
|
1290
|
-
const frontendArray = frontend || [];
|
|
1291
|
-
const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray,
|
|
1321
|
+
const frontendArray = config.frontend || [];
|
|
1322
|
+
const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, config.addons, config.auth, config.backend, config.runtime);
|
|
1292
1323
|
for (const addon of compatibleAddons) {
|
|
1293
1324
|
const { label, hint } = getAddonDisplay(addon);
|
|
1294
1325
|
addOptionToGroup(groupedOptions, {
|
|
@@ -1381,6 +1412,541 @@ const addPackageDependency = async (opts) => {
|
|
|
1381
1412
|
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
1382
1413
|
};
|
|
1383
1414
|
//#endregion
|
|
1415
|
+
//#region src/helpers/addons/evlog-setup.ts
|
|
1416
|
+
const evlogBackends = [
|
|
1417
|
+
"hono",
|
|
1418
|
+
"express",
|
|
1419
|
+
"fastify",
|
|
1420
|
+
"elysia"
|
|
1421
|
+
];
|
|
1422
|
+
const evlogWebFrontends = [
|
|
1423
|
+
"next",
|
|
1424
|
+
"nuxt",
|
|
1425
|
+
"svelte",
|
|
1426
|
+
"tanstack-start",
|
|
1427
|
+
"astro"
|
|
1428
|
+
];
|
|
1429
|
+
function isEvlogBackend(backend) {
|
|
1430
|
+
return evlogBackends.includes(backend);
|
|
1431
|
+
}
|
|
1432
|
+
function getEvlogWebFrontend(frontends) {
|
|
1433
|
+
return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
|
|
1434
|
+
}
|
|
1435
|
+
function shouldIdentifyWebAuth(config) {
|
|
1436
|
+
return config.auth === "better-auth" && config.backend === "self";
|
|
1437
|
+
}
|
|
1438
|
+
function prependMissingImports(content, imports) {
|
|
1439
|
+
const missingImports = imports.filter((line) => !content.includes(line));
|
|
1440
|
+
if (missingImports.length === 0) return content;
|
|
1441
|
+
const importBlock = `${missingImports.join("\n")}\n`;
|
|
1442
|
+
const referenceMatch = content.match(/^(?:\/\/\/ <reference[^\n]*>\n)+/);
|
|
1443
|
+
if (referenceMatch) return `${referenceMatch[0]}${importBlock}${content.slice(referenceMatch[0].length)}`;
|
|
1444
|
+
return `${importBlock}${content}`;
|
|
1445
|
+
}
|
|
1446
|
+
function addNamedImport(content, moduleName, names) {
|
|
1447
|
+
const importRegex = new RegExp(`import \\{([^}]+)\\} from "${moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";`);
|
|
1448
|
+
const match = content.match(importRegex);
|
|
1449
|
+
if (!match) return prependMissingImports(content, [`import { ${names.join(", ")} } from "${moduleName}";`]);
|
|
1450
|
+
const nextNames = [...match[1].split(",").map((name) => name.trim()).filter(Boolean)];
|
|
1451
|
+
for (const name of names) if (!nextNames.includes(name)) nextNames.push(name);
|
|
1452
|
+
return content.replace(match[0], `import { ${nextNames.join(", ")} } from "${moduleName}";`);
|
|
1453
|
+
}
|
|
1454
|
+
function insertBeforeOnce(content, marker, snippet, alreadyPresent) {
|
|
1455
|
+
if (content.includes(alreadyPresent)) return content;
|
|
1456
|
+
if (!content.includes(marker)) return content;
|
|
1457
|
+
return content.replace(marker, `${snippet}${marker}`);
|
|
1458
|
+
}
|
|
1459
|
+
function insertAfterOnce(content, marker, snippet, alreadyPresent) {
|
|
1460
|
+
if (content.includes(alreadyPresent)) return content;
|
|
1461
|
+
if (!content.includes(marker)) return content;
|
|
1462
|
+
return content.replace(marker, `${marker}${snippet}`);
|
|
1463
|
+
}
|
|
1464
|
+
async function writeFileIfChanged(filePath, content) {
|
|
1465
|
+
if ((await fs.pathExists(filePath) ? await fs.readFile(filePath, "utf-8") : void 0) === content) return;
|
|
1466
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
1467
|
+
await fs.writeFile(filePath, content);
|
|
1468
|
+
}
|
|
1469
|
+
async function updateFileIfExists(filePath, update) {
|
|
1470
|
+
if (!await fs.pathExists(filePath)) return;
|
|
1471
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1472
|
+
const nextContent = update(content);
|
|
1473
|
+
if (nextContent !== content) await fs.writeFile(filePath, nextContent);
|
|
1474
|
+
}
|
|
1475
|
+
function usesCreateAuthFactory(config) {
|
|
1476
|
+
return config.runtime === "workers" || config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare";
|
|
1477
|
+
}
|
|
1478
|
+
function getAuthImportLine(config) {
|
|
1479
|
+
return usesCreateAuthFactory(config) ? `import { createAuth } from "@${config.projectName}/auth";` : `import { auth } from "@${config.projectName}/auth";`;
|
|
1480
|
+
}
|
|
1481
|
+
function getAuthExpression(config) {
|
|
1482
|
+
return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
|
|
1483
|
+
}
|
|
1484
|
+
function addAiSdkEvlogTelemetry(content, loggerExpression) {
|
|
1485
|
+
let nextContent = addNamedImport(content, "evlog/ai", ["createAILogger", "createEvlogIntegration"]);
|
|
1486
|
+
if (!nextContent.includes("const ai = createAILogger(")) nextContent = nextContent.replace(/^(\s*)const model = wrapLanguageModel\({/m, (_match, indent) => `${indent}const ai = createAILogger(${loggerExpression});\n${indent}const model = wrapLanguageModel({`);
|
|
1487
|
+
if (!nextContent.includes("model: ai.wrap(model)")) nextContent = nextContent.replace(/(const result = streamText\({\n\s*)model,/, "$1model: ai.wrap(model),");
|
|
1488
|
+
if (!nextContent.includes("createEvlogIntegration(ai)")) nextContent = nextContent.replace(/(messages:\s*await convertToModelMessages\([^)]+\),?)/, (match) => `${match.endsWith(",") ? match : `${match},`}\n\t\texperimental_telemetry: {\n\t\t\tisEnabled: true,\n\t\t\tintegrations: [createEvlogIntegration(ai)],\n\t\t},`);
|
|
1489
|
+
return nextContent;
|
|
1490
|
+
}
|
|
1491
|
+
function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
|
|
1492
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1493
|
+
const usesAuthFactory = authExpression.endsWith("()");
|
|
1494
|
+
const evlogAuthExpression = `${authExpression} as BetterAuthInstance`;
|
|
1495
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1496
|
+
const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
|
|
1497
|
+
const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
|
|
1498
|
+
if (backend === "hono") {
|
|
1499
|
+
nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
|
|
1500
|
+
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use("*", async (c, next) => {${identifyUserSetup}\n\tawait identifyUser(c.get("log"), c.req.raw.headers, c.req.path);\n\tawait next();\n});`, "identifyUser(c.get(\"log\")");
|
|
1501
|
+
}
|
|
1502
|
+
if (backend === "express") {
|
|
1503
|
+
nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
|
|
1504
|
+
return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(req.log, req.headers, req.path);\n\tnext();\n});`, "identifyUser(req.log");
|
|
1505
|
+
}
|
|
1506
|
+
if (backend === "fastify") {
|
|
1507
|
+
nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
|
|
1508
|
+
nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
|
|
1509
|
+
return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
|
|
1510
|
+
}
|
|
1511
|
+
nextContent = insertBeforeOnce(nextContent, "new Elysia", identifySnippet, "createAuthMiddleware(");
|
|
1512
|
+
return insertAfterOnce(nextContent, ".use(evlog())", `\n\t.derive(async ({ request, log }) => {${identifyUserSetup.replace(/\n\t/g, "\n ")}\n\t\tawait identifyUser(log, request.headers, new URL(request.url).pathname);\n\t\treturn {};\n\t})`, "identifyUser(log");
|
|
1513
|
+
}
|
|
1514
|
+
function addEvlogServerSetup(content, backend, serviceName) {
|
|
1515
|
+
const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
|
|
1516
|
+
if (backend === "hono") {
|
|
1517
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
|
|
1518
|
+
nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
|
|
1519
|
+
nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
|
|
1520
|
+
nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
|
|
1521
|
+
return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
|
|
1522
|
+
}
|
|
1523
|
+
if (backend === "express") {
|
|
1524
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
|
|
1525
|
+
nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
|
|
1526
|
+
return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
|
|
1527
|
+
}
|
|
1528
|
+
if (backend === "fastify") {
|
|
1529
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
|
|
1530
|
+
nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
|
|
1531
|
+
return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
|
|
1532
|
+
}
|
|
1533
|
+
let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
|
|
1534
|
+
nextContent = insertBeforeOnce(nextContent, "new Elysia", initSnippet, "initLogger({");
|
|
1535
|
+
for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
|
|
1536
|
+
return nextContent;
|
|
1537
|
+
}
|
|
1538
|
+
function addNuxtEvlogSetup(content, serviceName) {
|
|
1539
|
+
let nextContent = content;
|
|
1540
|
+
if (!nextContent.includes("\"evlog/nuxt\"") && !nextContent.includes("'evlog/nuxt'")) nextContent = nextContent.replace(/modules:\s*\[/, (match) => `${match}\n "evlog/nuxt",`);
|
|
1541
|
+
if (!nextContent.includes("evlog:")) nextContent = nextContent.replace(/\n\}\)\s*$/, (match) => {
|
|
1542
|
+
const contentBeforeConfigClose = nextContent.slice(0, -match.length);
|
|
1543
|
+
return `${!/[,{]\s*$/.test(contentBeforeConfigClose) ? "," : ""}\n evlog: {\n env: { service: "${serviceName}" },\n },\n})`;
|
|
1544
|
+
});
|
|
1545
|
+
return nextContent;
|
|
1546
|
+
}
|
|
1547
|
+
function addSvelteViteEvlogSetup(content, serviceName) {
|
|
1548
|
+
let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
|
|
1549
|
+
if (nextContent.includes("evlog({")) return nextContent;
|
|
1550
|
+
return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
|
|
1551
|
+
}
|
|
1552
|
+
function addSvelteHooksEvlogSetup(content) {
|
|
1553
|
+
let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
|
|
1554
|
+
if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
|
|
1555
|
+
if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
|
|
1556
|
+
return nextContent;
|
|
1557
|
+
}
|
|
1558
|
+
nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
|
|
1559
|
+
if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
|
|
1560
|
+
nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
|
|
1561
|
+
if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
|
|
1562
|
+
return nextContent;
|
|
1563
|
+
}
|
|
1564
|
+
function addSvelteLocalsType(content) {
|
|
1565
|
+
let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
|
|
1566
|
+
if (nextContent.includes("log: RequestLogger")) return nextContent;
|
|
1567
|
+
if (nextContent.includes("// interface Locals {}")) return nextContent.replace("// interface Locals {}", "interface Locals {\n log: RequestLogger;\n }");
|
|
1568
|
+
return nextContent.replace("namespace App {", "namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
|
|
1569
|
+
}
|
|
1570
|
+
function addTanstackStartRootEvlogSetup(content) {
|
|
1571
|
+
let nextContent = prependMissingImports(content, ["import { createMiddleware } from \"@tanstack/react-start\";", "import { evlogErrorHandler } from \"evlog/nitro/v3\";"]);
|
|
1572
|
+
const middlewareEntry = "createMiddleware().server(evlogErrorHandler)";
|
|
1573
|
+
if (nextContent.includes(`middleware: [${middlewareEntry}]`)) return nextContent;
|
|
1574
|
+
if (nextContent.includes("middleware: [")) return nextContent.replace("middleware: [", `middleware: [${middlewareEntry}, `);
|
|
1575
|
+
if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
|
|
1576
|
+
return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
|
|
1577
|
+
}
|
|
1578
|
+
function addAstroMiddlewareEvlogSetup(content, serviceName) {
|
|
1579
|
+
let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
|
|
1580
|
+
const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
|
|
1581
|
+
nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
|
|
1582
|
+
if (nextContent.includes("createRequestLogger({")) return nextContent;
|
|
1583
|
+
const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
|
|
1584
|
+
if (nextContent.includes(contextMarker)) {
|
|
1585
|
+
nextContent = insertAfterOnce(nextContent, contextMarker, `\n const url = new URL(context.request.url);\n const log = createRequestLogger({\n method: context.request.method,\n path: url.pathname,\n });\n\n context.locals.log = log;\n`, "const log = createRequestLogger({");
|
|
1586
|
+
return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
|
|
1587
|
+
}
|
|
1588
|
+
const localsMarker = "export const onRequest = defineMiddleware(async ({ request, locals }, next) => {";
|
|
1589
|
+
if (nextContent.includes(localsMarker)) {
|
|
1590
|
+
nextContent = insertAfterOnce(nextContent, localsMarker, `\n const url = new URL(request.url);\n const log = createRequestLogger({\n method: request.method,\n path: url.pathname,\n });\n\n locals.log = log;\n`, "const log = createRequestLogger({");
|
|
1591
|
+
return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
|
|
1592
|
+
}
|
|
1593
|
+
return nextContent;
|
|
1594
|
+
}
|
|
1595
|
+
function addAstroLocalsType(content) {
|
|
1596
|
+
let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
|
|
1597
|
+
if (nextContent.includes("log: RequestLogger")) return nextContent;
|
|
1598
|
+
if (nextContent.includes("interface Locals {")) return nextContent.replace("interface Locals {", "interface Locals {\n log: RequestLogger;");
|
|
1599
|
+
if (nextContent.includes("declare namespace App {")) return nextContent.replace("declare namespace App {", "declare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
|
|
1600
|
+
return `${nextContent.trimEnd()}\n\ndeclare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n}\n`;
|
|
1601
|
+
}
|
|
1602
|
+
function addNextRouteWrappers(content) {
|
|
1603
|
+
let nextContent = prependMissingImports(content, ["import { withEvlog } from \"@/lib/evlog\";"]);
|
|
1604
|
+
if (nextContent.includes("withEvlog(handler)") || nextContent.includes("withEvlog(handleRequest)")) return nextContent;
|
|
1605
|
+
nextContent = nextContent.replace("export { handler as GET, handler as POST };", "export const GET = withEvlog(handler);\nexport const POST = withEvlog(handler);");
|
|
1606
|
+
for (const method of [
|
|
1607
|
+
"GET",
|
|
1608
|
+
"POST",
|
|
1609
|
+
"PUT",
|
|
1610
|
+
"PATCH",
|
|
1611
|
+
"DELETE"
|
|
1612
|
+
]) nextContent = nextContent.replace(`export const ${method} = handleRequest;`, `export const ${method} = withEvlog(handleRequest);`);
|
|
1613
|
+
return nextContent;
|
|
1614
|
+
}
|
|
1615
|
+
function addNextAiEvlogSetup(content) {
|
|
1616
|
+
let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
|
|
1617
|
+
if (!nextContent.includes("withEvlog(async (req: Request)")) {
|
|
1618
|
+
nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
|
|
1619
|
+
if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
|
|
1620
|
+
}
|
|
1621
|
+
return nextContent;
|
|
1622
|
+
}
|
|
1623
|
+
function addNuxtAiEvlogSetup(content) {
|
|
1624
|
+
return addAiSdkEvlogTelemetry(content, "useLogger(event)");
|
|
1625
|
+
}
|
|
1626
|
+
function addSvelteAiEvlogSetup(content) {
|
|
1627
|
+
return addAiSdkEvlogTelemetry(content.replace("export const POST: RequestHandler = async ({ request }) => {", "export const POST: RequestHandler = async ({ request, locals }) => {"), "locals.log");
|
|
1628
|
+
}
|
|
1629
|
+
function addTanstackStartAiEvlogSetup(content) {
|
|
1630
|
+
return addAiSdkEvlogTelemetry(prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";", "import { useRequest } from \"nitro/context\";"]), "useRequest().context.log as RequestLogger");
|
|
1631
|
+
}
|
|
1632
|
+
function addBackendAiEvlogSetup(content, backend) {
|
|
1633
|
+
if (backend === "hono") return addAiSdkEvlogTelemetry(content, "c.get(\"log\")");
|
|
1634
|
+
if (backend === "express") return addAiSdkEvlogTelemetry(content, "req.log");
|
|
1635
|
+
if (backend === "fastify") return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/fastify", ["useLogger"]), "useLogger()");
|
|
1636
|
+
return addAiSdkEvlogTelemetry(content, "context.log");
|
|
1637
|
+
}
|
|
1638
|
+
function addNextBetterAuthToRoute(content) {
|
|
1639
|
+
let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
|
|
1640
|
+
nextContent = nextContent.replace("function handler(req:", "async function handler(req:");
|
|
1641
|
+
for (const marker of [
|
|
1642
|
+
"async function handler(req: NextRequest) {",
|
|
1643
|
+
"async function handleRequest(req: NextRequest) {",
|
|
1644
|
+
"export const POST = withEvlog(async (req: Request) => {"
|
|
1645
|
+
]) nextContent = insertAfterOnce(nextContent, marker, "\n await identifyEvlogUser(req);", "identifyEvlogUser(req)");
|
|
1646
|
+
return nextContent;
|
|
1647
|
+
}
|
|
1648
|
+
function addSvelteBetterAuthEvlogSetup(content, config) {
|
|
1649
|
+
if (!content.includes("authHandle") || content.includes("evlogAuthHandle")) return content;
|
|
1650
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1651
|
+
if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
|
|
1652
|
+
if (usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" && !nextContent.includes(`@${config.projectName}/env/server`)) nextContent = prependMissingImports(nextContent, [`import { env as localEnv } from "@${config.projectName}/env/server";`]);
|
|
1653
|
+
const authExpression = getAuthExpression(config);
|
|
1654
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1655
|
+
const authHandleSnippet = usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" ? `const evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tif (building) {\n\t\treturn resolve(event);\n\t}\n\n\tconst authEnv = event.platform?.env ?? localEnv;\n\tconst identifyUser = createAuthMiddleware(createAuth(authEnv) as BetterAuthInstance, ${authOptions});\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n` : `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\nconst evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n`;
|
|
1656
|
+
nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
|
|
1657
|
+
return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
|
|
1658
|
+
}
|
|
1659
|
+
function addAstroBetterAuthEvlogSetup(content, config) {
|
|
1660
|
+
if (content.includes("createAuthMiddleware(")) return content;
|
|
1661
|
+
let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
|
|
1662
|
+
if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
|
|
1663
|
+
const authExpression = getAuthExpression(config);
|
|
1664
|
+
const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
|
|
1665
|
+
const usesFactory = usesCreateAuthFactory(config);
|
|
1666
|
+
if (!usesFactory) nextContent = insertBeforeOnce(nextContent, "export const onRequest", `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\n`, "const identifyUser = createAuthMiddleware(");
|
|
1667
|
+
for (const marker of ["context.locals.log = log;", "locals.log = log;"]) {
|
|
1668
|
+
if (!nextContent.includes(marker)) continue;
|
|
1669
|
+
const requestExpression = marker.startsWith("context") ? "context.request" : "request";
|
|
1670
|
+
const identifySnippet = usesFactory ? `\n\n const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n await identifyUser(log, ${requestExpression}.headers, url.pathname);` : `\n\n await identifyUser(log, ${requestExpression}.headers, url.pathname);`;
|
|
1671
|
+
return insertAfterOnce(nextContent, marker, identifySnippet, "identifyUser(log");
|
|
1672
|
+
}
|
|
1673
|
+
return nextContent;
|
|
1674
|
+
}
|
|
1675
|
+
function getNextEvlogFile(serviceName) {
|
|
1676
|
+
return `import { createEvlog } from "evlog/next";
|
|
1677
|
+
import { createInstrumentation } from "evlog/next/instrumentation";
|
|
1678
|
+
|
|
1679
|
+
export const { withEvlog, useLogger, log, createError } = createEvlog({
|
|
1680
|
+
service: "${serviceName}",
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
export const { register, onRequestError } = createInstrumentation({
|
|
1684
|
+
service: "${serviceName}",
|
|
1685
|
+
});
|
|
1686
|
+
`;
|
|
1687
|
+
}
|
|
1688
|
+
function getNextInstrumentationFile() {
|
|
1689
|
+
return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
|
|
1690
|
+
|
|
1691
|
+
export const { register, onRequestError } = defineNodeInstrumentation(() => import("./src/lib/evlog"));
|
|
1692
|
+
`;
|
|
1693
|
+
}
|
|
1694
|
+
function getNextProxyFile() {
|
|
1695
|
+
return `import { evlogMiddleware } from "evlog/next";
|
|
1696
|
+
|
|
1697
|
+
export const proxy = evlogMiddleware();
|
|
1698
|
+
|
|
1699
|
+
export const config = {
|
|
1700
|
+
matcher: ["/api/:path*"],
|
|
1701
|
+
};
|
|
1702
|
+
`;
|
|
1703
|
+
}
|
|
1704
|
+
function getNextEvlogAuthFile(config) {
|
|
1705
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1706
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1707
|
+
import { useLogger } from "@/lib/evlog";
|
|
1708
|
+
|
|
1709
|
+
export async function identifyEvlogUser(request: Request) {
|
|
1710
|
+
const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1711
|
+
exclude: ["/api/auth/**"],
|
|
1712
|
+
maskEmail: true,
|
|
1713
|
+
});
|
|
1714
|
+
await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
|
|
1715
|
+
}
|
|
1716
|
+
`;
|
|
1717
|
+
return `${getAuthImportLine(config)}
|
|
1718
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1719
|
+
import { useLogger } from "@/lib/evlog";
|
|
1720
|
+
|
|
1721
|
+
const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1722
|
+
exclude: ["/api/auth/**"],
|
|
1723
|
+
maskEmail: true,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
export async function identifyEvlogUser(request: Request) {
|
|
1727
|
+
await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
|
|
1728
|
+
}
|
|
1729
|
+
`;
|
|
1730
|
+
}
|
|
1731
|
+
function getNitroEvlogAuthPluginFile(config) {
|
|
1732
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1733
|
+
import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
|
|
1734
|
+
|
|
1735
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
1736
|
+
nitroApp.hooks.hook("request", async (event) => {
|
|
1737
|
+
const identify = createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1738
|
+
exclude: ["/api/auth/**"],
|
|
1739
|
+
maskEmail: true,
|
|
1740
|
+
});
|
|
1741
|
+
await identify(event);
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
`;
|
|
1745
|
+
return `${getAuthImportLine(config)}
|
|
1746
|
+
import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
|
|
1747
|
+
|
|
1748
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
1749
|
+
nitroApp.hooks.hook(
|
|
1750
|
+
"request",
|
|
1751
|
+
createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1752
|
+
exclude: ["/api/auth/**"],
|
|
1753
|
+
maskEmail: true,
|
|
1754
|
+
}),
|
|
1755
|
+
);
|
|
1756
|
+
});
|
|
1757
|
+
`;
|
|
1758
|
+
}
|
|
1759
|
+
function getNuxtEvlogAuthMiddlewareFile(config) {
|
|
1760
|
+
if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
|
|
1761
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1762
|
+
|
|
1763
|
+
export default defineEventHandler(async (event) => {
|
|
1764
|
+
if (!event.context.log) return;
|
|
1765
|
+
const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1766
|
+
exclude: ["/api/auth/**"],
|
|
1767
|
+
maskEmail: true,
|
|
1768
|
+
});
|
|
1769
|
+
await identify(event.context.log, event.headers, event.path);
|
|
1770
|
+
});
|
|
1771
|
+
`;
|
|
1772
|
+
return `${getAuthImportLine(config)}
|
|
1773
|
+
import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
|
|
1774
|
+
|
|
1775
|
+
const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
|
|
1776
|
+
exclude: ["/api/auth/**"],
|
|
1777
|
+
maskEmail: true,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
export default defineEventHandler(async (event) => {
|
|
1781
|
+
if (!event.context.log) return;
|
|
1782
|
+
await identify(event.context.log, event.headers, event.path);
|
|
1783
|
+
});
|
|
1784
|
+
`;
|
|
1785
|
+
}
|
|
1786
|
+
function getTanstackNitroConfigFile(serviceName) {
|
|
1787
|
+
return `import { defineConfig } from "nitro";
|
|
1788
|
+
import evlog from "evlog/nitro/v3";
|
|
1789
|
+
|
|
1790
|
+
export default defineConfig({
|
|
1791
|
+
experimental: {
|
|
1792
|
+
asyncContext: true,
|
|
1793
|
+
},
|
|
1794
|
+
modules: [
|
|
1795
|
+
evlog({
|
|
1796
|
+
env: { service: "${serviceName}" },
|
|
1797
|
+
}),
|
|
1798
|
+
],
|
|
1799
|
+
});
|
|
1800
|
+
`;
|
|
1801
|
+
}
|
|
1802
|
+
function getAstroMiddlewareFile(serviceName) {
|
|
1803
|
+
return `import { defineMiddleware } from "astro:middleware";
|
|
1804
|
+
import { createRequestLogger, initLogger } from "evlog";
|
|
1805
|
+
|
|
1806
|
+
initLogger({
|
|
1807
|
+
env: { service: "${serviceName}" },
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
|
|
1811
|
+
const url = new URL(request.url);
|
|
1812
|
+
const log = createRequestLogger({
|
|
1813
|
+
method: request.method,
|
|
1814
|
+
path: url.pathname,
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
locals.log = log;
|
|
1818
|
+
|
|
1819
|
+
try {
|
|
1820
|
+
const response = await next();
|
|
1821
|
+
log.emit();
|
|
1822
|
+
return response;
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
log.error(error instanceof Error ? error : new Error(String(error)));
|
|
1825
|
+
log.emit();
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
function getAstroEnvFile() {
|
|
1832
|
+
return `/// <reference types="astro/client" />
|
|
1833
|
+
|
|
1834
|
+
import type { RequestLogger } from "evlog";
|
|
1835
|
+
|
|
1836
|
+
declare namespace App {
|
|
1837
|
+
interface Locals {
|
|
1838
|
+
log: RequestLogger;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
`;
|
|
1842
|
+
}
|
|
1843
|
+
async function setupNextEvlog(config, serviceName) {
|
|
1844
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1845
|
+
const evlogPath = path.join(webDir, "src/lib/evlog.ts");
|
|
1846
|
+
if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
|
|
1847
|
+
const identifyWebAuth = shouldIdentifyWebAuth(config);
|
|
1848
|
+
if (identifyWebAuth) {
|
|
1849
|
+
const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
|
|
1850
|
+
if (!await fs.pathExists(evlogAuthPath)) await writeFileIfChanged(evlogAuthPath, getNextEvlogAuthFile(config));
|
|
1851
|
+
}
|
|
1852
|
+
const instrumentationPath = path.join(webDir, "instrumentation.ts");
|
|
1853
|
+
if (!await fs.pathExists(instrumentationPath)) await writeFileIfChanged(instrumentationPath, getNextInstrumentationFile());
|
|
1854
|
+
const proxyPath = path.join(webDir, "src/proxy.ts");
|
|
1855
|
+
const rootProxyPath = path.join(webDir, "proxy.ts");
|
|
1856
|
+
if (!await fs.pathExists(proxyPath) && !await fs.pathExists(rootProxyPath)) await writeFileIfChanged(proxyPath, getNextProxyFile());
|
|
1857
|
+
const updateNextApiRoute = (content) => {
|
|
1858
|
+
let nextContent = addNextRouteWrappers(content);
|
|
1859
|
+
if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
|
|
1860
|
+
return nextContent;
|
|
1861
|
+
};
|
|
1862
|
+
await updateFileIfExists(path.join(webDir, "src/app/api/trpc/[trpc]/route.ts"), updateNextApiRoute);
|
|
1863
|
+
await updateFileIfExists(path.join(webDir, "src/app/api/rpc/[[...rest]]/route.ts"), updateNextApiRoute);
|
|
1864
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/app/api/ai/route.ts"), (content) => {
|
|
1865
|
+
let nextContent = addNextAiEvlogSetup(content);
|
|
1866
|
+
if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
|
|
1867
|
+
return nextContent;
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
async function setupNuxtEvlog(config, serviceName) {
|
|
1871
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1872
|
+
await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
|
|
1873
|
+
if (shouldIdentifyWebAuth(config)) {
|
|
1874
|
+
const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
|
|
1875
|
+
if (await fs.pathExists(oldAuthPluginPath)) {
|
|
1876
|
+
if ((await fs.readFile(oldAuthPluginPath, "utf-8")).includes("evlog/better-auth")) await fs.remove(oldAuthPluginPath);
|
|
1877
|
+
}
|
|
1878
|
+
const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
|
|
1879
|
+
if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
|
|
1880
|
+
}
|
|
1881
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "server/api/ai.post.ts"), addNuxtAiEvlogSetup);
|
|
1882
|
+
}
|
|
1883
|
+
async function setupSvelteEvlog(config, serviceName) {
|
|
1884
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1885
|
+
await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
|
|
1886
|
+
const hooksPath = path.join(webDir, "src/hooks.server.ts");
|
|
1887
|
+
if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
|
|
1888
|
+
else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
|
|
1889
|
+
|
|
1890
|
+
export const { handle, handleError } = createEvlogHooks();
|
|
1891
|
+
`);
|
|
1892
|
+
await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
|
|
1893
|
+
if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
|
|
1894
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/+server.ts"), addSvelteAiEvlogSetup);
|
|
1895
|
+
}
|
|
1896
|
+
async function setupTanstackStartEvlog(config, serviceName) {
|
|
1897
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1898
|
+
const nitroConfigPath = path.join(webDir, "nitro.config.ts");
|
|
1899
|
+
if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
|
|
1900
|
+
await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
|
|
1901
|
+
if (shouldIdentifyWebAuth(config)) {
|
|
1902
|
+
const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
|
|
1903
|
+
if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
|
|
1904
|
+
}
|
|
1905
|
+
if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/$.ts"), addTanstackStartAiEvlogSetup);
|
|
1906
|
+
}
|
|
1907
|
+
async function setupAstroEvlog(config, serviceName) {
|
|
1908
|
+
const webDir = path.join(config.projectDir, "apps/web");
|
|
1909
|
+
const middlewarePath = path.join(webDir, "src/middleware.ts");
|
|
1910
|
+
if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
|
|
1911
|
+
else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
|
|
1912
|
+
const envPath = path.join(webDir, "src/env.d.ts");
|
|
1913
|
+
if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
|
|
1914
|
+
else await updateFileIfExists(envPath, addAstroLocalsType);
|
|
1915
|
+
if (shouldIdentifyWebAuth(config)) await updateFileIfExists(middlewarePath, (content) => addAstroBetterAuthEvlogSetup(content, config));
|
|
1916
|
+
}
|
|
1917
|
+
async function setupEvlogWeb(config) {
|
|
1918
|
+
const frontend = getEvlogWebFrontend(config.frontend);
|
|
1919
|
+
if (!frontend) return;
|
|
1920
|
+
const serviceName = `${config.projectName}-web`;
|
|
1921
|
+
if (frontend === "next") await setupNextEvlog(config, serviceName);
|
|
1922
|
+
else if (frontend === "nuxt") await setupNuxtEvlog(config, serviceName);
|
|
1923
|
+
else if (frontend === "svelte") await setupSvelteEvlog(config, serviceName);
|
|
1924
|
+
else if (frontend === "tanstack-start") await setupTanstackStartEvlog(config, serviceName);
|
|
1925
|
+
else if (frontend === "astro") await setupAstroEvlog(config, serviceName);
|
|
1926
|
+
}
|
|
1927
|
+
async function setupEvlog(config) {
|
|
1928
|
+
return Result.tryPromise({
|
|
1929
|
+
try: async () => {
|
|
1930
|
+
if (isEvlogBackend(config.backend)) {
|
|
1931
|
+
const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
|
|
1932
|
+
if (await fs.pathExists(serverIndexPath)) {
|
|
1933
|
+
const content = await fs.readFile(serverIndexPath, "utf-8");
|
|
1934
|
+
let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
|
|
1935
|
+
if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
|
|
1936
|
+
if (config.examples.includes("ai")) nextContent = addBackendAiEvlogSetup(nextContent, config.backend);
|
|
1937
|
+
if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
await setupEvlogWeb(config);
|
|
1941
|
+
},
|
|
1942
|
+
catch: (error) => new AddonSetupError({
|
|
1943
|
+
addon: "evlog",
|
|
1944
|
+
message: `Failed to set up evlog: ${error instanceof Error ? error.message : String(error)}`,
|
|
1945
|
+
cause: error
|
|
1946
|
+
})
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
//#endregion
|
|
1384
1950
|
//#region src/prompts/navigable-group.ts
|
|
1385
1951
|
/**
|
|
1386
1952
|
* Navigable group - a group of prompts that allows going back
|
|
@@ -2149,7 +2715,8 @@ const SKILL_SOURCES = {
|
|
|
2149
2715
|
"elysiajs/skills": { label: "ElysiaJS" },
|
|
2150
2716
|
"waynesutton/convexskills": { label: "Convex" },
|
|
2151
2717
|
"msmps/opentui-skill": { label: "OpenTUI Platform" },
|
|
2152
|
-
"haydenbleasel/ultracite": { label: "Ultracite" }
|
|
2718
|
+
"haydenbleasel/ultracite": { label: "Ultracite" },
|
|
2719
|
+
"https://www.evlog.dev": { label: "evlog" }
|
|
2153
2720
|
};
|
|
2154
2721
|
const AVAILABLE_AGENTS = [
|
|
2155
2722
|
{
|
|
@@ -2289,6 +2856,7 @@ function getRecommendedSourceKeys(config) {
|
|
|
2289
2856
|
if (backend === "convex") sources.push("waynesutton/convexskills");
|
|
2290
2857
|
if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
|
|
2291
2858
|
if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
|
|
2859
|
+
if (addons.includes("evlog")) sources.push("https://www.evlog.dev");
|
|
2292
2860
|
return sources;
|
|
2293
2861
|
}
|
|
2294
2862
|
const CURATED_SKILLS_BY_SOURCE = {
|
|
@@ -2335,7 +2903,6 @@ const CURATED_SKILLS_BY_SOURCE = {
|
|
|
2335
2903
|
"building-native-ui",
|
|
2336
2904
|
"native-data-fetching",
|
|
2337
2905
|
"expo-deployment",
|
|
2338
|
-
"upgrading-expo",
|
|
2339
2906
|
"expo-cicd-workflows"
|
|
2340
2907
|
];
|
|
2341
2908
|
if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
|
|
@@ -2360,7 +2927,8 @@ const CURATED_SKILLS_BY_SOURCE = {
|
|
|
2360
2927
|
"convex-security-check"
|
|
2361
2928
|
],
|
|
2362
2929
|
"msmps/opentui-skill": () => ["opentui"],
|
|
2363
|
-
"haydenbleasel/ultracite": () => ["ultracite"]
|
|
2930
|
+
"haydenbleasel/ultracite": () => ["ultracite"],
|
|
2931
|
+
"https://www.evlog.dev": () => ["review-logging-patterns", "analyze-logs"]
|
|
2364
2932
|
};
|
|
2365
2933
|
function getCuratedSkillNamesForSourceKey(sourceKey, config) {
|
|
2366
2934
|
return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
|
|
@@ -3083,6 +3651,7 @@ async function setupAddons(config) {
|
|
|
3083
3651
|
if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
|
|
3084
3652
|
if (addons.includes("skills")) await runSetup(() => setupSkills(config));
|
|
3085
3653
|
if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
|
|
3654
|
+
if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
|
|
3086
3655
|
}
|
|
3087
3656
|
async function setupBiome(projectDir) {
|
|
3088
3657
|
await addPackageDependency({
|
|
@@ -3246,7 +3815,7 @@ async function addHandlerInternal(input) {
|
|
|
3246
3815
|
} else if (isSilent()) return Result.err(new CLIError({ message: "Addons are required in silent mode. Provide them via add() or add-json." }));
|
|
3247
3816
|
else {
|
|
3248
3817
|
const promptResult = await Result.tryPromise({
|
|
3249
|
-
try: () => getAddonsToAdd(existingConfig
|
|
3818
|
+
try: () => getAddonsToAdd(existingConfig),
|
|
3250
3819
|
catch: (e) => {
|
|
3251
3820
|
if (UserCancelledError.is(e)) return e;
|
|
3252
3821
|
return new CLIError({
|
|
@@ -3270,6 +3839,8 @@ async function addHandlerInternal(input) {
|
|
|
3270
3839
|
}
|
|
3271
3840
|
addonsToAdd = selectedAddons;
|
|
3272
3841
|
}
|
|
3842
|
+
const addonsValidationResult = validateAddonsAgainstConfig(addonsToAdd, existingConfig);
|
|
3843
|
+
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
3273
3844
|
if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
|
|
3274
3845
|
const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
|
|
3275
3846
|
const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
|
|
@@ -3297,7 +3868,12 @@ async function addHandlerInternal(input) {
|
|
|
3297
3868
|
};
|
|
3298
3869
|
if (!isSilent()) log.info(pc.dim("Installing addon files..."));
|
|
3299
3870
|
const vfs = new VirtualFileSystem();
|
|
3300
|
-
for (const pkgPath of [
|
|
3871
|
+
for (const pkgPath of [
|
|
3872
|
+
"package.json",
|
|
3873
|
+
"apps/web/package.json",
|
|
3874
|
+
"apps/server/package.json",
|
|
3875
|
+
"apps/native/package.json"
|
|
3876
|
+
]) {
|
|
3301
3877
|
const fullPath = path.join(projectDir, pkgPath);
|
|
3302
3878
|
if (await fs.pathExists(fullPath)) {
|
|
3303
3879
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
@@ -3452,6 +4028,7 @@ const FULLSTACK_FRONTENDS = [
|
|
|
3452
4028
|
"next",
|
|
3453
4029
|
"tanstack-start",
|
|
3454
4030
|
"nuxt",
|
|
4031
|
+
"svelte",
|
|
3455
4032
|
"astro"
|
|
3456
4033
|
];
|
|
3457
4034
|
async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
@@ -3988,7 +4565,7 @@ function validateFullConfig(config, providedFlags, options) {
|
|
|
3988
4565
|
if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
|
|
3989
4566
|
if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
|
|
3990
4567
|
if (config.addons && config.addons.length > 0) {
|
|
3991
|
-
yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
4568
|
+
yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
3992
4569
|
config.addons = [...new Set(config.addons)];
|
|
3993
4570
|
}
|
|
3994
4571
|
yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
@@ -4002,7 +4579,7 @@ function validateConfigForProgrammaticUse(config) {
|
|
|
4002
4579
|
if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
|
|
4003
4580
|
yield* validateApiFrontendCompatibility(config.api, config.frontend);
|
|
4004
4581
|
yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
4005
|
-
if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
|
|
4582
|
+
if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
4006
4583
|
yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
|
|
4007
4584
|
return Result.ok(void 0);
|
|
4008
4585
|
});
|
|
@@ -4197,7 +4774,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
4197
4774
|
api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
|
|
4198
4775
|
auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
|
|
4199
4776
|
payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
|
|
4200
|
-
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth),
|
|
4777
|
+
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
|
|
4201
4778
|
examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
|
|
4202
4779
|
dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
|
|
4203
4780
|
webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
|
|
@@ -6177,7 +6754,10 @@ async function displayPostInstallInstructions(config) {
|
|
|
6177
6754
|
output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
|
|
6178
6755
|
if (api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
|
|
6179
6756
|
}
|
|
6180
|
-
if (isBackendSelf && api === "orpc")
|
|
6757
|
+
if (isBackendSelf && api === "orpc") {
|
|
6758
|
+
const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
|
|
6759
|
+
output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
|
|
6760
|
+
}
|
|
6181
6761
|
if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
|
|
6182
6762
|
if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
|
|
6183
6763
|
}
|
|
@@ -6205,7 +6785,8 @@ async function displayPostInstallInstructions(config) {
|
|
|
6205
6785
|
}
|
|
6206
6786
|
function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
|
|
6207
6787
|
const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
|
|
6208
|
-
const
|
|
6788
|
+
const selfBackendPort = frontend.includes("svelte") ? "5173" : frontend.includes("astro") ? "4321" : "3001";
|
|
6789
|
+
const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
|
|
6209
6790
|
const envFileName = ".env";
|
|
6210
6791
|
const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
|
|
6211
6792
|
let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
|
|
@@ -6352,8 +6933,8 @@ function getPolarInstructions(backend) {
|
|
|
6352
6933
|
function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
|
|
6353
6934
|
const instructions = [];
|
|
6354
6935
|
const isBackendSelf = backend === "self";
|
|
6355
|
-
if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare") instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${
|
|
6356
|
-
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${
|
|
6936
|
+
if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6937
|
+
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6357
6938
|
else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
|
|
6358
6939
|
return instructions.length ? `\n${instructions.join("\n")}` : "";
|
|
6359
6940
|
}
|
|
@@ -6633,6 +7214,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
|
|
|
6633
7214
|
...config,
|
|
6634
7215
|
dbSetupOptions: effectiveDbSetupOptions
|
|
6635
7216
|
};
|
|
7217
|
+
if (!input.yolo) {
|
|
7218
|
+
const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
|
|
7219
|
+
if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
|
|
7220
|
+
}
|
|
6636
7221
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
6637
7222
|
if (input.dryRun) {
|
|
6638
7223
|
const elapsedTimeMs = Date.now() - startTime;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-auth",
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"prepublishOnly": "npm run build"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
|
-
"@better-t-stack/template-generator": "^3.
|
|
74
|
-
"@better-t-stack/types": "^3.
|
|
73
|
+
"@better-t-stack/template-generator": "^3.28.0",
|
|
74
|
+
"@better-t-stack/types": "^3.28.0",
|
|
75
75
|
"@clack/core": "^1.2.0",
|
|
76
76
|
"@clack/prompts": "^1.2.0",
|
|
77
77
|
"@modelcontextprotocol/sdk": "1.29.0",
|