create-reactor 0.1.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/LICENSE +21 -0
- package/README.md +123 -0
- package/create-app.mjs +712 -0
- package/lib/build.mjs +434 -0
- package/lib/pm.mjs +85 -0
- package/lib/presets.mjs +122 -0
- package/lib/templates/ai-docs.mjs +80 -0
- package/lib/templates/app.mjs +961 -0
- package/lib/templates/backend.mjs +715 -0
- package/lib/templates/base.mjs +671 -0
- package/lib/templates/biome.mjs +107 -0
- package/lib/templates/extras.mjs +360 -0
- package/lib/templates/features.mjs +463 -0
- package/lib/templates/quality.mjs +159 -0
- package/lib/templates/readme.mjs +351 -0
- package/lib/templates/security.mjs +70 -0
- package/lib/templates/server.mjs +141 -0
- package/lib/templates/state.mjs +192 -0
- package/package.json +52 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
// Base project files: package.json, index.html, vite config, tsconfig, eslint,
|
|
2
|
+
// Tailwind v4 + shadcn theme CSS, env files, shadcn config.
|
|
3
|
+
import { linterLintStaged, linterScripts } from "./biome.mjs";
|
|
4
|
+
|
|
5
|
+
/** package.json for the generated project (deps are added later via the package manager). */
|
|
6
|
+
export function pkgJson(c) {
|
|
7
|
+
const scripts = {};
|
|
8
|
+
|
|
9
|
+
if (c.backend === "convex") {
|
|
10
|
+
scripts["dev"] = "npm-run-all --parallel dev:frontend dev:backend";
|
|
11
|
+
scripts["dev:frontend"] = "vite";
|
|
12
|
+
scripts["dev:backend"] = "convex dev";
|
|
13
|
+
scripts["setup"] = "convex dev --until-success";
|
|
14
|
+
} else if (c.backend === "hono") {
|
|
15
|
+
scripts["dev"] = "npm-run-all --parallel dev:web dev:server";
|
|
16
|
+
scripts["dev:web"] = "vite";
|
|
17
|
+
scripts["dev:server"] = "tsx watch server/index.ts";
|
|
18
|
+
} else {
|
|
19
|
+
scripts["dev"] = "vite";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scripts["build"] = "vite build && tsc -b";
|
|
23
|
+
scripts["preview"] = "vite preview";
|
|
24
|
+
Object.assign(scripts, linterScripts(c));
|
|
25
|
+
scripts["typecheck"] = "tsc -b";
|
|
26
|
+
if (c.extras.includes("knip")) {
|
|
27
|
+
scripts["knip"] = "knip";
|
|
28
|
+
}
|
|
29
|
+
if (c.extras.includes("fallow")) {
|
|
30
|
+
// Full codebase analysis (dead code + duplication + complexity + health)
|
|
31
|
+
scripts["quality"] = "fallow";
|
|
32
|
+
// Changed-code audit vs the main branch (use before opening a PR)
|
|
33
|
+
scripts["quality:audit"] = "fallow audit";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (c.extras.includes("testing")) {
|
|
37
|
+
scripts["test"] = "vitest run";
|
|
38
|
+
scripts["test:watch"] = "vitest";
|
|
39
|
+
}
|
|
40
|
+
if (c.extras.includes("e2e")) {
|
|
41
|
+
scripts["test:e2e"] = "playwright test";
|
|
42
|
+
scripts["test:e2e:ui"] = "playwright test --ui";
|
|
43
|
+
}
|
|
44
|
+
// Note: the husky "prepare" script is added by the generator AFTER dependencies
|
|
45
|
+
// are installed — otherwise package managers try to run `husky` mid-install
|
|
46
|
+
// before it exists and the install fails.
|
|
47
|
+
|
|
48
|
+
if (c.orm === "drizzle") {
|
|
49
|
+
scripts["db:generate"] = "drizzle-kit generate";
|
|
50
|
+
scripts["db:migrate"] = "drizzle-kit migrate";
|
|
51
|
+
scripts["db:push"] = "drizzle-kit push";
|
|
52
|
+
scripts["db:studio"] = "drizzle-kit studio";
|
|
53
|
+
}
|
|
54
|
+
if (c.orm === "prisma") {
|
|
55
|
+
scripts["db:push"] = "prisma db push";
|
|
56
|
+
scripts["db:generate"] = "prisma generate";
|
|
57
|
+
scripts["db:studio"] = "prisma studio";
|
|
58
|
+
}
|
|
59
|
+
const pkg = {
|
|
60
|
+
name: c.name,
|
|
61
|
+
private: true,
|
|
62
|
+
version: "0.0.0",
|
|
63
|
+
type: "module",
|
|
64
|
+
scripts,
|
|
65
|
+
};
|
|
66
|
+
if (c.extras.includes("husky")) {
|
|
67
|
+
pkg["lint-staged"] = linterLintStaged(c);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return JSON.stringify(pkg, null, 2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function indexHtml(c) {
|
|
74
|
+
return `<!doctype html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="UTF-8" />
|
|
78
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
80
|
+
<title>${c.name}</title>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<div id="root"></div>
|
|
84
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function favicon() {
|
|
91
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="oklch(0.6 0.2 260)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function viteConfig(c) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
if (c.extras.includes("testing")) {
|
|
98
|
+
lines.push(`/// <reference types="vitest/config" />`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(`import path from "node:path";`);
|
|
101
|
+
lines.push(`import { defineConfig } from "vite";`);
|
|
102
|
+
lines.push(`import react from "@vitejs/plugin-react";`);
|
|
103
|
+
lines.push(`import tailwindcss from "@tailwindcss/vite";`);
|
|
104
|
+
if (c.router === "tanstack") {
|
|
105
|
+
lines.push(`import { tanstackRouter } from "@tanstack/router-plugin/vite";`);
|
|
106
|
+
}
|
|
107
|
+
if (c.extras.includes("pwa")) {
|
|
108
|
+
lines.push(`import { VitePWA } from "vite-plugin-pwa";`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(``);
|
|
111
|
+
lines.push(`// https://vite.dev/config/`);
|
|
112
|
+
lines.push(`export default defineConfig({`);
|
|
113
|
+
lines.push(` plugins: [`);
|
|
114
|
+
if (c.router === "tanstack") {
|
|
115
|
+
lines.push(` // tanstackRouter must come before react()`);
|
|
116
|
+
lines.push(` tanstackRouter({ target: "react", autoCodeSplitting: true }),`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(` react(),`);
|
|
119
|
+
lines.push(` tailwindcss(),`);
|
|
120
|
+
if (c.extras.includes("pwa")) {
|
|
121
|
+
lines.push(` VitePWA({`);
|
|
122
|
+
lines.push(` registerType: "autoUpdate",`);
|
|
123
|
+
lines.push(` manifest: {`);
|
|
124
|
+
lines.push(` name: "${c.name}",`);
|
|
125
|
+
lines.push(` short_name: "${c.name}",`);
|
|
126
|
+
lines.push(` theme_color: "#0a0a0a",`);
|
|
127
|
+
lines.push(` background_color: "#ffffff",`);
|
|
128
|
+
lines.push(` icons: [{ src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }],`);
|
|
129
|
+
lines.push(` },`);
|
|
130
|
+
lines.push(` }),`);
|
|
131
|
+
}
|
|
132
|
+
lines.push(` ],`);
|
|
133
|
+
lines.push(` resolve: {`);
|
|
134
|
+
lines.push(` alias: {`);
|
|
135
|
+
lines.push(` "@": path.resolve(import.meta.dirname, "./src"),`);
|
|
136
|
+
lines.push(` },`);
|
|
137
|
+
lines.push(` },`);
|
|
138
|
+
if (c.extras.includes("testing")) {
|
|
139
|
+
lines.push(` test: {`);
|
|
140
|
+
lines.push(` environment: "jsdom",`);
|
|
141
|
+
lines.push(` setupFiles: ["./src/test/setup.ts"],`);
|
|
142
|
+
if (c.extras.includes("e2e")) {
|
|
143
|
+
lines.push(` // Playwright tests live in e2e/ and are run separately`);
|
|
144
|
+
lines.push(` exclude: ["e2e/**", "node_modules/**"],`);
|
|
145
|
+
}
|
|
146
|
+
lines.push(` },`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(`});`);
|
|
149
|
+
return lines.join("\n") + "\n";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function tsconfig() {
|
|
153
|
+
return JSON.stringify(
|
|
154
|
+
{
|
|
155
|
+
files: [],
|
|
156
|
+
references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }],
|
|
157
|
+
compilerOptions: {
|
|
158
|
+
paths: { "@/*": ["./src/*"] },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
2,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function tsconfigApp(c) {
|
|
167
|
+
// Note: the convex/ folder is intentionally NOT included here — it is
|
|
168
|
+
// typechecked separately by the Convex CLI using convex/tsconfig.json.
|
|
169
|
+
void c;
|
|
170
|
+
const include = ["src"];
|
|
171
|
+
return JSON.stringify(
|
|
172
|
+
{
|
|
173
|
+
compilerOptions: {
|
|
174
|
+
tsBuildInfoFile: "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
175
|
+
target: "ES2022",
|
|
176
|
+
useDefineForClassFields: true,
|
|
177
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
178
|
+
module: "ESNext",
|
|
179
|
+
skipLibCheck: true,
|
|
180
|
+
moduleResolution: "bundler",
|
|
181
|
+
allowImportingTsExtensions: true,
|
|
182
|
+
verbatimModuleSyntax: true,
|
|
183
|
+
moduleDetection: "force",
|
|
184
|
+
noEmit: true,
|
|
185
|
+
jsx: "react-jsx",
|
|
186
|
+
strict: true,
|
|
187
|
+
noUnusedLocals: true,
|
|
188
|
+
noUnusedParameters: true,
|
|
189
|
+
noFallthroughCasesInSwitch: true,
|
|
190
|
+
noUncheckedSideEffectImports: true,
|
|
191
|
+
paths: { "@/*": ["./src/*"] },
|
|
192
|
+
},
|
|
193
|
+
include,
|
|
194
|
+
},
|
|
195
|
+
null,
|
|
196
|
+
2,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function tsconfigNode(c) {
|
|
201
|
+
const include = ["vite.config.ts"];
|
|
202
|
+
if (c.orm === "drizzle") include.push("drizzle.config.ts", "db");
|
|
203
|
+
if (c.extras.includes("redis") && !include.includes("db")) include.push("db");
|
|
204
|
+
if (c.backend === "hono") include.push("server");
|
|
205
|
+
if (c.extras.includes("e2e")) include.push("playwright.config.ts", "e2e");
|
|
206
|
+
return JSON.stringify(
|
|
207
|
+
{
|
|
208
|
+
compilerOptions: {
|
|
209
|
+
tsBuildInfoFile: "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
210
|
+
target: "ES2023",
|
|
211
|
+
lib: ["ES2023"],
|
|
212
|
+
module: "ESNext",
|
|
213
|
+
skipLibCheck: true,
|
|
214
|
+
moduleResolution: "bundler",
|
|
215
|
+
allowImportingTsExtensions: true,
|
|
216
|
+
verbatimModuleSyntax: true,
|
|
217
|
+
moduleDetection: "force",
|
|
218
|
+
noEmit: true,
|
|
219
|
+
strict: true,
|
|
220
|
+
noUnusedLocals: true,
|
|
221
|
+
noUnusedParameters: true,
|
|
222
|
+
noFallthroughCasesInSwitch: true,
|
|
223
|
+
noUncheckedSideEffectImports: true,
|
|
224
|
+
types: ["node"],
|
|
225
|
+
},
|
|
226
|
+
include,
|
|
227
|
+
},
|
|
228
|
+
null,
|
|
229
|
+
2,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function eslintConfig(c) {
|
|
234
|
+
const ignores = [`"dist"`];
|
|
235
|
+
if (c.backend === "convex") ignores.push(`"convex/_generated"`);
|
|
236
|
+
if (c.router === "tanstack") ignores.push(`"src/routeTree.gen.ts"`);
|
|
237
|
+
if (c.orm === "prisma") ignores.push(`"src/generated"`);
|
|
238
|
+
if (c.extras.includes("e2e")) ignores.push(`"playwright-report"`, `"test-results"`);
|
|
239
|
+
if (c.extras.includes("storybook")) ignores.push(`"storybook-static"`, `".storybook"`);
|
|
240
|
+
|
|
241
|
+
// TanStack Router file routes export a `Route` object instead of components,
|
|
242
|
+
// so the fast-refresh rule doesn't apply to them (routes are code-split anyway).
|
|
243
|
+
const routesOverride =
|
|
244
|
+
c.router === "tanstack"
|
|
245
|
+
? `
|
|
246
|
+
{
|
|
247
|
+
files: ["src/routes/**"],
|
|
248
|
+
rules: {
|
|
249
|
+
"react-refresh/only-export-components": "off",
|
|
250
|
+
},
|
|
251
|
+
},`
|
|
252
|
+
: "";
|
|
253
|
+
|
|
254
|
+
return `import js from "@eslint/js";
|
|
255
|
+
import globals from "globals";
|
|
256
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
257
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
258
|
+
import tseslint from "typescript-eslint";
|
|
259
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
260
|
+
|
|
261
|
+
export default defineConfig([
|
|
262
|
+
globalIgnores([${ignores.join(", ")}]),
|
|
263
|
+
{
|
|
264
|
+
files: ["**/*.{ts,tsx}"],
|
|
265
|
+
extends: [
|
|
266
|
+
js.configs.recommended,
|
|
267
|
+
tseslint.configs.recommended,
|
|
268
|
+
// react-hooks v7 moved flat configs under .flat; fall back for v6
|
|
269
|
+
reactHooks.configs.flat?.["recommended-latest"] ?? reactHooks.configs["recommended-latest"],
|
|
270
|
+
reactRefresh.configs.vite,
|
|
271
|
+
],
|
|
272
|
+
languageOptions: {
|
|
273
|
+
ecmaVersion: 2022,
|
|
274
|
+
globals: globals.browser,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
// shadcn/ui components export variants and hooks alongside components by design;
|
|
279
|
+
// Storybook stories export meta objects — neither needs fast-refresh purity
|
|
280
|
+
files: ["src/components/ui/**", "**/*.stories.*"],
|
|
281
|
+
rules: {
|
|
282
|
+
"react-refresh/only-export-components": "off",
|
|
283
|
+
},
|
|
284
|
+
},${routesOverride}
|
|
285
|
+
]);
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function gitignore(c) {
|
|
290
|
+
const lines = [
|
|
291
|
+
"# Logs",
|
|
292
|
+
"logs",
|
|
293
|
+
"*.log",
|
|
294
|
+
"npm-debug.log*",
|
|
295
|
+
"yarn-debug.log*",
|
|
296
|
+
"yarn-error.log*",
|
|
297
|
+
"pnpm-debug.log*",
|
|
298
|
+
"lerna-debug.log*",
|
|
299
|
+
"",
|
|
300
|
+
"node_modules",
|
|
301
|
+
"dist",
|
|
302
|
+
"dist-ssr",
|
|
303
|
+
"*.local",
|
|
304
|
+
"",
|
|
305
|
+
"# Env files - never commit secrets",
|
|
306
|
+
".env",
|
|
307
|
+
".env.local",
|
|
308
|
+
".env.*.local",
|
|
309
|
+
"",
|
|
310
|
+
"# Editor directories and files",
|
|
311
|
+
".vscode/*",
|
|
312
|
+
"!.vscode/extensions.json",
|
|
313
|
+
".idea",
|
|
314
|
+
".DS_Store",
|
|
315
|
+
"*.suo",
|
|
316
|
+
"*.ntvs*",
|
|
317
|
+
"*.njsproj",
|
|
318
|
+
"*.sln",
|
|
319
|
+
"*.sw?",
|
|
320
|
+
];
|
|
321
|
+
if (c.router === "tanstack") {
|
|
322
|
+
lines.push("", "# TanStack Router generated route tree", "# (kept in git by convention; uncomment to ignore)", "# src/routeTree.gen.ts");
|
|
323
|
+
}
|
|
324
|
+
if (c.backend === "convex") {
|
|
325
|
+
lines.push("", "# Convex", ".convex");
|
|
326
|
+
}
|
|
327
|
+
if (c.orm === "prisma") {
|
|
328
|
+
lines.push("", "# Prisma generated client", "src/generated");
|
|
329
|
+
}
|
|
330
|
+
if (c.dbProvider === "turso") {
|
|
331
|
+
lines.push("", "# Local SQLite databases", "*.db", "*.db-journal");
|
|
332
|
+
}
|
|
333
|
+
if (c.extras.includes("e2e")) {
|
|
334
|
+
lines.push("", "# Playwright", "test-results", "playwright-report", "blob-report", "playwright/.cache");
|
|
335
|
+
}
|
|
336
|
+
if (c.extras.includes("storybook")) {
|
|
337
|
+
lines.push("", "# Storybook", "storybook-static");
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n") + "\n";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Tailwind v4 + shadcn/ui (new-york, neutral) theme. */
|
|
343
|
+
export function indexCss() {
|
|
344
|
+
return `@import "tailwindcss";
|
|
345
|
+
@import "tw-animate-css";
|
|
346
|
+
|
|
347
|
+
@custom-variant dark (&:is(.dark *));
|
|
348
|
+
|
|
349
|
+
@theme inline {
|
|
350
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
351
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
352
|
+
--radius-lg: var(--radius);
|
|
353
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
354
|
+
--color-background: var(--background);
|
|
355
|
+
--color-foreground: var(--foreground);
|
|
356
|
+
--color-card: var(--card);
|
|
357
|
+
--color-card-foreground: var(--card-foreground);
|
|
358
|
+
--color-popover: var(--popover);
|
|
359
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
360
|
+
--color-primary: var(--primary);
|
|
361
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
362
|
+
--color-secondary: var(--secondary);
|
|
363
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
364
|
+
--color-muted: var(--muted);
|
|
365
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
366
|
+
--color-accent: var(--accent);
|
|
367
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
368
|
+
--color-destructive: var(--destructive);
|
|
369
|
+
--color-border: var(--border);
|
|
370
|
+
--color-input: var(--input);
|
|
371
|
+
--color-ring: var(--ring);
|
|
372
|
+
--color-chart-1: var(--chart-1);
|
|
373
|
+
--color-chart-2: var(--chart-2);
|
|
374
|
+
--color-chart-3: var(--chart-3);
|
|
375
|
+
--color-chart-4: var(--chart-4);
|
|
376
|
+
--color-chart-5: var(--chart-5);
|
|
377
|
+
--color-sidebar: var(--sidebar);
|
|
378
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
379
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
380
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
381
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
382
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
383
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
384
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:root {
|
|
388
|
+
--radius: 0.625rem;
|
|
389
|
+
--background: oklch(1 0 0);
|
|
390
|
+
--foreground: oklch(0.145 0 0);
|
|
391
|
+
--card: oklch(1 0 0);
|
|
392
|
+
--card-foreground: oklch(0.145 0 0);
|
|
393
|
+
--popover: oklch(1 0 0);
|
|
394
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
395
|
+
--primary: oklch(0.205 0 0);
|
|
396
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
397
|
+
--secondary: oklch(0.97 0 0);
|
|
398
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
399
|
+
--muted: oklch(0.97 0 0);
|
|
400
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
401
|
+
--accent: oklch(0.97 0 0);
|
|
402
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
403
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
404
|
+
--border: oklch(0.922 0 0);
|
|
405
|
+
--input: oklch(0.922 0 0);
|
|
406
|
+
--ring: oklch(0.708 0 0);
|
|
407
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
408
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
409
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
410
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
411
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
412
|
+
--sidebar: oklch(0.985 0 0);
|
|
413
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
414
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
415
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
416
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
417
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
418
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
419
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.dark {
|
|
423
|
+
--background: oklch(0.145 0 0);
|
|
424
|
+
--foreground: oklch(0.985 0 0);
|
|
425
|
+
--card: oklch(0.205 0 0);
|
|
426
|
+
--card-foreground: oklch(0.985 0 0);
|
|
427
|
+
--popover: oklch(0.205 0 0);
|
|
428
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
429
|
+
--primary: oklch(0.922 0 0);
|
|
430
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
431
|
+
--secondary: oklch(0.269 0 0);
|
|
432
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
433
|
+
--muted: oklch(0.269 0 0);
|
|
434
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
435
|
+
--accent: oklch(0.269 0 0);
|
|
436
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
437
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
438
|
+
--border: oklch(1 0 0 / 10%);
|
|
439
|
+
--input: oklch(1 0 0 / 15%);
|
|
440
|
+
--ring: oklch(0.556 0 0);
|
|
441
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
442
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
443
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
444
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
445
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
446
|
+
--sidebar: oklch(0.205 0 0);
|
|
447
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
448
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
449
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
450
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
451
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
452
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
453
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
@layer base {
|
|
457
|
+
* {
|
|
458
|
+
@apply border-border outline-ring/50;
|
|
459
|
+
}
|
|
460
|
+
body {
|
|
461
|
+
@apply bg-background text-foreground;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** shadcn/ui CLI configuration. */
|
|
468
|
+
export function componentsJson() {
|
|
469
|
+
return JSON.stringify(
|
|
470
|
+
{
|
|
471
|
+
$schema: "https://ui.shadcn.com/schema.json",
|
|
472
|
+
style: "new-york",
|
|
473
|
+
rsc: false,
|
|
474
|
+
tsx: true,
|
|
475
|
+
tailwind: {
|
|
476
|
+
config: "",
|
|
477
|
+
css: "src/index.css",
|
|
478
|
+
baseColor: "neutral",
|
|
479
|
+
cssVariables: true,
|
|
480
|
+
prefix: "",
|
|
481
|
+
},
|
|
482
|
+
iconLibrary: "lucide",
|
|
483
|
+
aliases: {
|
|
484
|
+
components: "@/components",
|
|
485
|
+
utils: "@/lib/utils",
|
|
486
|
+
ui: "@/components/ui",
|
|
487
|
+
lib: "@/lib",
|
|
488
|
+
hooks: "@/hooks",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
null,
|
|
492
|
+
2,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** shadcn cn() helper. */
|
|
497
|
+
export function libUtils() {
|
|
498
|
+
return `import { clsx, type ClassValue } from "clsx";
|
|
499
|
+
import { twMerge } from "tailwind-merge";
|
|
500
|
+
|
|
501
|
+
export function cn(...inputs: ClassValue[]) {
|
|
502
|
+
return twMerge(clsx(inputs));
|
|
503
|
+
}
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Typed import.meta.env declarations based on selected features. */
|
|
508
|
+
export function viteEnvDts(c) {
|
|
509
|
+
const vars = [];
|
|
510
|
+
if (c.backend === "convex") vars.push("VITE_CONVEX_URL");
|
|
511
|
+
if (c.auth === "better-auth") vars.push("VITE_CONVEX_SITE_URL");
|
|
512
|
+
if (c.auth === "clerk") vars.push("VITE_CLERK_PUBLISHABLE_KEY");
|
|
513
|
+
if (c.backend === "supabase") vars.push("VITE_SUPABASE_URL", "VITE_SUPABASE_ANON_KEY");
|
|
514
|
+
if (c.extras.includes("sentry")) vars.push("VITE_SENTRY_DSN");
|
|
515
|
+
if (c.extras.includes("stripe")) vars.push("VITE_STRIPE_PUBLISHABLE_KEY");
|
|
516
|
+
if (c.extras.includes("posthog")) vars.push("VITE_POSTHOG_KEY", "VITE_POSTHOG_HOST");
|
|
517
|
+
|
|
518
|
+
let out = `/// <reference types="vite/client" />\n`;
|
|
519
|
+
if (vars.length > 0) {
|
|
520
|
+
out += `
|
|
521
|
+
interface ImportMetaEnv {
|
|
522
|
+
${vars.map((v) => ` readonly ${v}: string;`).join("\n")}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
interface ImportMeta {
|
|
526
|
+
readonly env: ImportMetaEnv;
|
|
527
|
+
}
|
|
528
|
+
`;
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** .env.local with placeholders for the selected features. */
|
|
534
|
+
export function envLocal(c) {
|
|
535
|
+
const lines = [];
|
|
536
|
+
if (c.backend === "convex") {
|
|
537
|
+
lines.push("# Convex - filled automatically when you run: " + c.pmRunLabel("setup"));
|
|
538
|
+
lines.push("# CONVEX_DEPLOYMENT=");
|
|
539
|
+
lines.push("# VITE_CONVEX_URL=");
|
|
540
|
+
lines.push("");
|
|
541
|
+
}
|
|
542
|
+
if (c.auth === "better-auth") {
|
|
543
|
+
lines.push("# Better Auth via Convex - same as VITE_CONVEX_URL but with .cloud replaced by .site");
|
|
544
|
+
lines.push("# e.g. https://adjective-animal-123.convex.site");
|
|
545
|
+
lines.push("VITE_CONVEX_SITE_URL=");
|
|
546
|
+
lines.push("");
|
|
547
|
+
}
|
|
548
|
+
if (c.auth === "clerk") {
|
|
549
|
+
lines.push("# Clerk - get from https://dashboard.clerk.com -> API Keys");
|
|
550
|
+
lines.push("VITE_CLERK_PUBLISHABLE_KEY=");
|
|
551
|
+
lines.push("");
|
|
552
|
+
}
|
|
553
|
+
if (c.backend === "supabase") {
|
|
554
|
+
lines.push("# Supabase - get from https://supabase.com/dashboard -> Project Settings -> API");
|
|
555
|
+
lines.push("VITE_SUPABASE_URL=");
|
|
556
|
+
lines.push("VITE_SUPABASE_ANON_KEY=");
|
|
557
|
+
lines.push("");
|
|
558
|
+
}
|
|
559
|
+
if (c.extras.includes("sentry")) {
|
|
560
|
+
lines.push("# Sentry (optional) - get a DSN from https://sentry.io -> Project Settings -> Client Keys");
|
|
561
|
+
lines.push("# Leave empty to disable error reporting locally");
|
|
562
|
+
lines.push("VITE_SENTRY_DSN=");
|
|
563
|
+
lines.push("");
|
|
564
|
+
}
|
|
565
|
+
if (c.extras.includes("stripe")) {
|
|
566
|
+
lines.push("# Stripe - https://dashboard.stripe.com/apikeys (publishable key is safe for the browser)");
|
|
567
|
+
lines.push("VITE_STRIPE_PUBLISHABLE_KEY=");
|
|
568
|
+
lines.push("");
|
|
569
|
+
}
|
|
570
|
+
if (c.extras.includes("posthog")) {
|
|
571
|
+
lines.push("# PostHog (optional) - https://app.posthog.com -> Project Settings -> API key");
|
|
572
|
+
lines.push("# Leave empty to disable analytics locally");
|
|
573
|
+
lines.push("VITE_POSTHOG_KEY=");
|
|
574
|
+
lines.push("VITE_POSTHOG_HOST=https://us.i.posthog.com");
|
|
575
|
+
lines.push("");
|
|
576
|
+
}
|
|
577
|
+
if (c.ai !== "none") {
|
|
578
|
+
const keyName = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY" }[c.ai];
|
|
579
|
+
if (c.backend === "convex") {
|
|
580
|
+
lines.push(`# AI SDK - set ${keyName} on your Convex deployment instead of here:`);
|
|
581
|
+
lines.push(`# npx convex env set ${keyName} <your-key>`);
|
|
582
|
+
} else {
|
|
583
|
+
lines.push("# AI SDK - server-side key, never expose to the browser");
|
|
584
|
+
lines.push(`${keyName}=`);
|
|
585
|
+
}
|
|
586
|
+
lines.push("");
|
|
587
|
+
}
|
|
588
|
+
return lines.length ? lines.join("\n") : "";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** .env.example mirrors .env.local (committed to git). */
|
|
592
|
+
export function envExample(c) {
|
|
593
|
+
let out = envLocal(c);
|
|
594
|
+
const hasServerEnv =
|
|
595
|
+
c.orm !== "none" || ["redis", "stripe", "resend"].some((e) => c.extras.includes(e));
|
|
596
|
+
if (hasServerEnv) out += envDatabase(c);
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** .env — server-side secrets: database connection (Drizzle/Prisma) and/or Upstash Redis. */
|
|
601
|
+
export function envDatabase(c) {
|
|
602
|
+
const lines = [];
|
|
603
|
+
if (c.orm !== "none") {
|
|
604
|
+
const ormName = c.orm === "drizzle" ? "Drizzle" : "Prisma";
|
|
605
|
+
if (c.dbProvider === "neon") {
|
|
606
|
+
lines.push(`# Neon serverless Postgres (used by ${ormName})`);
|
|
607
|
+
lines.push("# Create a free project at https://neon.tech, then copy the connection string");
|
|
608
|
+
lines.push("# Format: postgresql://user:pass@ep-xxx-xxx.region.aws.neon.tech/dbname?sslmode=require");
|
|
609
|
+
lines.push('DATABASE_URL=""');
|
|
610
|
+
} else if (c.dbProvider === "turso") {
|
|
611
|
+
lines.push("# Turso (libSQL) - create a database with: turso db create");
|
|
612
|
+
lines.push("# Or use a local SQLite file during development: TURSO_DATABASE_URL=\"file:./local.db\"");
|
|
613
|
+
lines.push('TURSO_DATABASE_URL="file:./local.db"');
|
|
614
|
+
lines.push('TURSO_AUTH_TOKEN=""');
|
|
615
|
+
} else if (c.dbProvider === "docker") {
|
|
616
|
+
lines.push(`# Local Postgres via Docker (used by ${ormName})`);
|
|
617
|
+
lines.push("# Start the database with: docker compose up -d");
|
|
618
|
+
lines.push(`DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${c.name.replace(/[^a-z0-9_]/gi, "_")}"`);
|
|
619
|
+
} else if (c.backend === "supabase") {
|
|
620
|
+
lines.push(`# Supabase Postgres (used by ${ormName})`);
|
|
621
|
+
lines.push("# Supabase -> Project Settings -> Database -> Connection string (use the pooler in production)");
|
|
622
|
+
lines.push('DATABASE_URL=""');
|
|
623
|
+
} else {
|
|
624
|
+
lines.push(`# Database connection string (used by ${ormName})`);
|
|
625
|
+
lines.push("# Point this at your Postgres database");
|
|
626
|
+
lines.push('DATABASE_URL=""');
|
|
627
|
+
}
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
if (c.extras.includes("redis")) {
|
|
631
|
+
lines.push("# Upstash Redis (server-side only) - create a database at https://console.upstash.com");
|
|
632
|
+
lines.push('UPSTASH_REDIS_REST_URL=""');
|
|
633
|
+
lines.push('UPSTASH_REDIS_REST_TOKEN=""');
|
|
634
|
+
lines.push("");
|
|
635
|
+
}
|
|
636
|
+
if (c.extras.includes("stripe")) {
|
|
637
|
+
lines.push("# Stripe secret key (server-side only - never expose to the browser)");
|
|
638
|
+
lines.push('STRIPE_SECRET_KEY=""');
|
|
639
|
+
lines.push("");
|
|
640
|
+
}
|
|
641
|
+
if (c.extras.includes("resend")) {
|
|
642
|
+
lines.push("# Resend (server-side only) - https://resend.com/api-keys");
|
|
643
|
+
lines.push('RESEND_API_KEY=""');
|
|
644
|
+
lines.push("");
|
|
645
|
+
}
|
|
646
|
+
return lines.join("\n");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function prettierrc() {
|
|
650
|
+
return JSON.stringify(
|
|
651
|
+
{
|
|
652
|
+
semi: true,
|
|
653
|
+
singleQuote: false,
|
|
654
|
+
trailingComma: "all",
|
|
655
|
+
plugins: ["prettier-plugin-tailwindcss"],
|
|
656
|
+
},
|
|
657
|
+
null,
|
|
658
|
+
2,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function prettierignore() {
|
|
663
|
+
return `dist
|
|
664
|
+
node_modules
|
|
665
|
+
convex/_generated
|
|
666
|
+
src/routeTree.gen.ts
|
|
667
|
+
bun.lock
|
|
668
|
+
pnpm-lock.yaml
|
|
669
|
+
package-lock.json
|
|
670
|
+
`;
|
|
671
|
+
}
|