kofi-stack-template-generator 2.0.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/.turbo/turbo-build.log +20 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +744 -0
- package/package.json +29 -0
- package/scripts/generate-templates.js +104 -0
- package/src/core/index.ts +7 -0
- package/src/core/template-processor.ts +127 -0
- package/src/core/virtual-fs.ts +189 -0
- package/src/generator.ts +429 -0
- package/src/index.ts +19 -0
- package/src/templates.generated.ts +39 -0
- package/templates/base/_gitignore.hbs +45 -0
- package/templates/base/biome.json.hbs +34 -0
- package/templates/convex/_env.local.hbs +52 -0
- package/templates/convex/convex/auth.ts.hbs +7 -0
- package/templates/convex/convex/http.ts.hbs +8 -0
- package/templates/convex/convex/schema.ts.hbs +15 -0
- package/templates/convex/convex/users.ts.hbs +13 -0
- package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +17 -0
- package/templates/monorepo/package.json.hbs +29 -0
- package/templates/monorepo/pnpm-workspace.yaml.hbs +3 -0
- package/templates/monorepo/turbo.json.hbs +42 -0
- package/templates/packages/config-biome/biome.json.hbs +4 -0
- package/templates/packages/config-biome/package.json.hbs +6 -0
- package/templates/packages/config-typescript/base.json.hbs +17 -0
- package/templates/packages/config-typescript/nextjs.json.hbs +7 -0
- package/templates/packages/config-typescript/package.json.hbs +10 -0
- package/templates/packages/ui/components.json.hbs +20 -0
- package/templates/packages/ui/package.json.hbs +34 -0
- package/templates/packages/ui/src/index.ts.hbs +3 -0
- package/templates/packages/ui/src/lib/utils.ts.hbs +6 -0
- package/templates/packages/ui/tsconfig.json.hbs +22 -0
- package/templates/web/components.json.hbs +20 -0
- package/templates/web/next.config.ts.hbs +9 -0
- package/templates/web/package.json.hbs +62 -0
- package/templates/web/postcss.config.mjs.hbs +5 -0
- package/templates/web/src/app/globals.css.hbs +122 -0
- package/templates/web/src/app/layout.tsx.hbs +55 -0
- package/templates/web/src/app/page.tsx.hbs +74 -0
- package/templates/web/src/components/providers/convex-provider.tsx.hbs +18 -0
- package/templates/web/src/lib/auth.ts.hbs +23 -0
- package/templates/web/src/lib/utils.ts.hbs +6 -0
- package/templates/web/tsconfig.json.hbs +23 -0
- package/tsconfig.json +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
// src/core/virtual-fs.ts
|
|
2
|
+
import { createFsFromVolume, Volume } from "memfs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var VirtualFileSystem = class {
|
|
5
|
+
volume;
|
|
6
|
+
fs;
|
|
7
|
+
binarySourcePaths = /* @__PURE__ */ new Map();
|
|
8
|
+
constructor() {
|
|
9
|
+
this.volume = new Volume();
|
|
10
|
+
this.fs = createFsFromVolume(this.volume);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Write a file to the virtual filesystem
|
|
14
|
+
*/
|
|
15
|
+
writeFile(filePath, content) {
|
|
16
|
+
const dir = path.dirname(filePath);
|
|
17
|
+
this.mkdir(dir);
|
|
18
|
+
this.fs.writeFileSync(filePath, content);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read a file from the virtual filesystem
|
|
22
|
+
*/
|
|
23
|
+
readFile(filePath) {
|
|
24
|
+
return this.fs.readFileSync(filePath);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a path exists
|
|
28
|
+
*/
|
|
29
|
+
exists(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
this.fs.statSync(filePath);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if path is a file
|
|
39
|
+
*/
|
|
40
|
+
fileExists(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
return this.fs.statSync(filePath).isFile();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if path is a directory
|
|
49
|
+
*/
|
|
50
|
+
directoryExists(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
return this.fs.statSync(filePath).isDirectory();
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create directory recursively
|
|
59
|
+
*/
|
|
60
|
+
mkdir(dirPath) {
|
|
61
|
+
if (!this.exists(dirPath)) {
|
|
62
|
+
this.fs.mkdirSync(dirPath, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Delete a file
|
|
67
|
+
*/
|
|
68
|
+
deleteFile(filePath) {
|
|
69
|
+
if (this.fileExists(filePath)) {
|
|
70
|
+
this.fs.unlinkSync(filePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List directory contents
|
|
75
|
+
*/
|
|
76
|
+
listDir(dirPath) {
|
|
77
|
+
if (!this.directoryExists(dirPath)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return this.fs.readdirSync(dirPath);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Track source path for binary files (for later copying)
|
|
84
|
+
*/
|
|
85
|
+
setBinarySourcePath(virtualPath, sourcePath) {
|
|
86
|
+
this.binarySourcePaths.set(virtualPath, sourcePath);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get source path for binary file
|
|
90
|
+
*/
|
|
91
|
+
getBinarySourcePath(virtualPath) {
|
|
92
|
+
return this.binarySourcePaths.get(virtualPath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convert the virtual filesystem to a tree structure
|
|
96
|
+
*/
|
|
97
|
+
toTree(config) {
|
|
98
|
+
const root = this.buildTree("/");
|
|
99
|
+
const stats = this.countNodes(root);
|
|
100
|
+
return {
|
|
101
|
+
root,
|
|
102
|
+
fileCount: stats.files,
|
|
103
|
+
directoryCount: stats.directories,
|
|
104
|
+
config
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
buildTree(dirPath) {
|
|
108
|
+
const name = dirPath === "/" ? "/" : path.basename(dirPath);
|
|
109
|
+
const children = [];
|
|
110
|
+
const entries = this.listDir(dirPath);
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const fullPath = path.join(dirPath, entry);
|
|
113
|
+
const stat = this.fs.statSync(fullPath);
|
|
114
|
+
if (stat.isDirectory()) {
|
|
115
|
+
children.push(this.buildTree(fullPath));
|
|
116
|
+
} else {
|
|
117
|
+
const content = this.fs.readFileSync(fullPath);
|
|
118
|
+
const file = {
|
|
119
|
+
type: "file",
|
|
120
|
+
path: fullPath,
|
|
121
|
+
name: entry,
|
|
122
|
+
content,
|
|
123
|
+
extension: path.extname(entry),
|
|
124
|
+
sourcePath: this.binarySourcePaths.get(fullPath)
|
|
125
|
+
};
|
|
126
|
+
children.push(file);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
type: "directory",
|
|
131
|
+
path: dirPath,
|
|
132
|
+
name,
|
|
133
|
+
children
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
countNodes(node) {
|
|
137
|
+
if (node.type === "file") {
|
|
138
|
+
return { files: 1, directories: 0 };
|
|
139
|
+
}
|
|
140
|
+
let files = 0;
|
|
141
|
+
let directories = 1;
|
|
142
|
+
for (const child of node.children) {
|
|
143
|
+
const counts = this.countNodes(child);
|
|
144
|
+
files += counts.files;
|
|
145
|
+
directories += counts.directories;
|
|
146
|
+
}
|
|
147
|
+
return { files, directories };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the raw memfs instance for advanced operations
|
|
151
|
+
*/
|
|
152
|
+
getRawFs() {
|
|
153
|
+
return this.fs;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/core/template-processor.ts
|
|
158
|
+
import Handlebars from "handlebars";
|
|
159
|
+
import path2 from "path";
|
|
160
|
+
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
161
|
+
Handlebars.registerHelper("ne", (a, b) => a !== b);
|
|
162
|
+
Handlebars.registerHelper("and", (...args) => {
|
|
163
|
+
const values = args.slice(0, -1);
|
|
164
|
+
return values.every(Boolean);
|
|
165
|
+
});
|
|
166
|
+
Handlebars.registerHelper("or", (...args) => {
|
|
167
|
+
const values = args.slice(0, -1);
|
|
168
|
+
return values.some(Boolean);
|
|
169
|
+
});
|
|
170
|
+
Handlebars.registerHelper("includes", (array, value) => {
|
|
171
|
+
if (!Array.isArray(array)) return false;
|
|
172
|
+
return array.includes(value);
|
|
173
|
+
});
|
|
174
|
+
Handlebars.registerHelper("not", (value) => !value);
|
|
175
|
+
Handlebars.registerHelper("json", (value) => JSON.stringify(value, null, 2));
|
|
176
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
177
|
+
".png",
|
|
178
|
+
".jpg",
|
|
179
|
+
".jpeg",
|
|
180
|
+
".gif",
|
|
181
|
+
".ico",
|
|
182
|
+
".webp",
|
|
183
|
+
".svg",
|
|
184
|
+
".woff",
|
|
185
|
+
".woff2",
|
|
186
|
+
".ttf",
|
|
187
|
+
".eot",
|
|
188
|
+
".otf",
|
|
189
|
+
".mp3",
|
|
190
|
+
".mp4",
|
|
191
|
+
".webm",
|
|
192
|
+
".pdf",
|
|
193
|
+
".zip",
|
|
194
|
+
".tar",
|
|
195
|
+
".gz"
|
|
196
|
+
]);
|
|
197
|
+
function isBinaryFile(filename) {
|
|
198
|
+
const ext = path2.extname(filename).toLowerCase();
|
|
199
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
200
|
+
}
|
|
201
|
+
function processTemplateString(template, config) {
|
|
202
|
+
try {
|
|
203
|
+
const compiled = Handlebars.compile(template, { noEscape: true });
|
|
204
|
+
return compiled(config);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Template processing error:", error);
|
|
207
|
+
return template;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function transformFilename(filename, config) {
|
|
211
|
+
let result = filename;
|
|
212
|
+
if (result.endsWith(".hbs")) {
|
|
213
|
+
result = result.slice(0, -4);
|
|
214
|
+
}
|
|
215
|
+
if (result.startsWith("_")) {
|
|
216
|
+
result = "." + result.slice(1);
|
|
217
|
+
}
|
|
218
|
+
if (result.includes("{{")) {
|
|
219
|
+
result = processTemplateString(result, config);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
function shouldIncludeFile(templatePath, config) {
|
|
224
|
+
if (templatePath.includes("/if-monorepo/") && config.structure !== "monorepo") {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (templatePath.includes("/if-standalone/") && config.structure !== "standalone") {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (templatePath.includes("/if-payload/") && config.marketingSite !== "payload") {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (templatePath.includes("/if-posthog/") && config.integrations.analytics !== "posthog") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (templatePath.includes("/if-uploadthing/") && config.integrations.uploads !== "uploadthing") {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/templates.generated.ts
|
|
243
|
+
var EMBEDDED_TEMPLATES = {
|
|
244
|
+
"base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
|
|
245
|
+
"base/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "organizeImports": {\n "enabled": true\n },\n "linter": {\n "enabled": true,\n "rules": {\n "recommended": true\n }\n },\n "formatter": {\n "enabled": true,\n "indentStyle": "space",\n "indentWidth": 2\n },\n "javascript": {\n "formatter": {\n "quoteStyle": "single",\n "semicolons": "asNeeded"\n }\n },\n "files": {\n "ignore": [\n "node_modules",\n ".next",\n "dist",\n ".turbo",\n "coverage",\n ".vercel",\n "_generated"\n ]\n }\n}\n',
|
|
246
|
+
"convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Arcjet\nARCJET_KEY=\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n\n# Email (Resend)\nRESEND_API_KEY=\n",
|
|
247
|
+
"convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [GitHub, Google],\n})\n",
|
|
248
|
+
"convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\nauth.addHttpRoutes(http)\n\nexport default http\n",
|
|
249
|
+
"convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { authTables } from '@convex-dev/auth/server'\nimport { v } from 'convex/values'\n\nexport default defineSchema({\n ...authTables,\n // Add your custom tables here\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // authorId: v.id('users'),\n // createdAt: v.number(),\n // }).index('by_author', ['authorId']),\n})\n",
|
|
250
|
+
"convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
|
|
251
|
+
"integrations/posthog/src/components/providers/posthog-provider.tsx.hbs": "'use client'\n\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PHProvider } from 'posthog-js/react'\nimport { useEffect } from 'react'\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',\n person_profiles: 'identified_only',\n capture_pageview: false, // We capture pageviews manually\n })\n }, [])\n\n return <PHProvider client={posthog}>{children}</PHProvider>\n}\n",
|
|
252
|
+
"monorepo/package.json.hbs": '{\n "name": "{{projectName}}",\n "version": "0.1.0",\n "private": true,\n "scripts": {\n "dev": "node scripts/dev.mjs",\n "dev:turbo": "turbo dev",\n "build": "turbo build",\n "lint": "turbo lint",\n "lint:fix": "turbo lint:fix",\n "format": "turbo format",\n "typecheck": "turbo typecheck",\n "test": "turbo test",\n "test:e2e": "turbo test:e2e",\n "clean": "turbo clean && rm -rf node_modules",\n "prepare": "husky",\n "setup:convex": "node apps/web/scripts/setup-convex.mjs"\n },\n "devDependencies": {\n "turbo": "^2.0.0",\n "husky": "^9.0.0",\n "lint-staged": "^15.0.0"\n },\n "packageManager": "pnpm@9.0.0",\n "lint-staged": {\n "*.{js,ts,jsx,tsx}": ["biome check --apply"],\n "*.{json,md}": ["biome format --write"]\n }\n}\n',
|
|
253
|
+
"monorepo/pnpm-workspace.yaml.hbs": 'packages:\n - "apps/*"\n - "packages/*"\n',
|
|
254
|
+
"monorepo/turbo.json.hbs": '{\n "$schema": "https://turbo.build/schema.json",\n "ui": "tui",\n "tasks": {\n "build": {\n "dependsOn": ["^build"],\n "inputs": ["$TURBO_DEFAULT$", ".env*"],\n "outputs": [".next/**", "!.next/cache/**", "dist/**"]\n },\n "lint": {\n "dependsOn": ["^lint"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "lint:fix": {\n "dependsOn": ["^lint:fix"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "format": {\n "dependsOn": ["^format"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "typecheck": {\n "dependsOn": ["^typecheck"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "dev": {\n "cache": false,\n "persistent": true\n },\n "test": {\n "dependsOn": ["^build"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "test:e2e": {\n "dependsOn": ["build"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "clean": {\n "cache": false\n }\n }\n}\n',
|
|
255
|
+
"packages/config-biome/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "extends": ["../../biome.json"]\n}\n',
|
|
256
|
+
"packages/config-biome/package.json.hbs": '{\n "name": "@repo/config-biome",\n "version": "0.1.0",\n "private": true,\n "files": ["biome.json"]\n}\n',
|
|
257
|
+
"packages/config-typescript/base.json.hbs": '{\n "compilerOptions": {\n "target": "ES2020",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "incremental": true\n },\n "exclude": ["node_modules"]\n}\n',
|
|
258
|
+
"packages/config-typescript/nextjs.json.hbs": '{\n "extends": "./base.json",\n "compilerOptions": {\n "jsx": "react-jsx",\n "plugins": [{ "name": "next" }]\n }\n}\n',
|
|
259
|
+
"packages/config-typescript/package.json.hbs": '{\n "name": "@repo/config-typescript",\n "version": "0.1.0",\n "private": true,\n "exports": {\n "./base.json": "./base.json",\n "./nextjs.json": "./nextjs.json"\n },\n "files": ["base.json", "nextjs.json"]\n}\n',
|
|
260
|
+
"packages/ui/components.json.hbs": '{\n "$schema": "https://ui.shadcn.com/schema.json",\n "style": "{{shadcn.componentLibrary}}-{{shadcn.styleVariant}}",\n "rsc": false,\n "tsx": true,\n "tailwind": {\n "config": "",\n "css": "",\n "baseColor": "{{shadcn.baseColor}}",\n "cssVariables": true\n },\n "iconLibrary": "{{shadcn.iconLibrary}}",\n "aliases": {\n "components": "@/components",\n "utils": "@/lib/utils",\n "ui": "@/components/ui",\n "lib": "@/lib",\n "hooks": "@/hooks"\n }\n}\n',
|
|
261
|
+
"packages/ui/package.json.hbs": '{\n "name": "@repo/ui",\n "version": "0.1.0",\n "private": true,\n "main": "./src/index.ts",\n "types": "./src/index.ts",\n "exports": {\n ".": "./src/index.ts",\n "./components/*": "./src/components/*.tsx",\n "./lib/*": "./src/lib/*.ts"\n },\n "scripts": {\n "lint": "biome check .",\n "lint:fix": "biome check --write .",\n "typecheck": "tsc --noEmit"\n },\n "dependencies": {\n "@hugeicons/react": "^0.3.0",\n "class-variance-authority": "^0.7.0",\n "clsx": "^2.1.0",\n "tailwind-merge": "^2.5.0"\n },\n "devDependencies": {\n "@repo/config-typescript": "workspace:*",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "react": "^19.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0"\n },\n "peerDependencies": {\n "react": "^19.0.0"\n }\n}\n',
|
|
262
|
+
"packages/ui/src/index.ts.hbs": "export { cn } from './lib/utils'\n// Export components as they are added\n// export * from './components/ui/button'\n",
|
|
263
|
+
"packages/ui/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
264
|
+
"packages/ui/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2020",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["src/**/*"],\n "exclude": ["node_modules"]\n}\n',
|
|
265
|
+
"web/components.json.hbs": '{\n "$schema": "https://ui.shadcn.com/schema.json",\n "style": "{{shadcn.componentLibrary}}-{{shadcn.styleVariant}}",\n "rsc": true,\n "tsx": true,\n "tailwind": {\n "config": "",\n "css": "src/app/globals.css",\n "baseColor": "{{shadcn.baseColor}}",\n "cssVariables": true\n },\n "iconLibrary": "{{shadcn.iconLibrary}}",\n "aliases": {\n "components": "@/components",\n "utils": "@/lib/utils",\n "ui": "@/components/ui",\n "lib": "@/lib",\n "hooks": "@/hooks"\n }\n}\n',
|
|
266
|
+
"web/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n{{#if (eq structure 'monorepo')}}\n transpilePackages: ['@repo/ui', '@repo/backend'],\n{{/if}}\n}\n\nexport default nextConfig\n",
|
|
267
|
+
"web/package.json.hbs": `{
|
|
268
|
+
"name": "{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}",
|
|
269
|
+
"version": "0.1.0",
|
|
270
|
+
"private": true,
|
|
271
|
+
"type": "module",
|
|
272
|
+
"scripts": {
|
|
273
|
+
"dev": "{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}",
|
|
274
|
+
"dev:next": "next dev --turbopack",
|
|
275
|
+
"build": "next build",
|
|
276
|
+
"start": "next start",
|
|
277
|
+
"lint": "biome check .",
|
|
278
|
+
"lint:fix": "biome check --write .",
|
|
279
|
+
"typecheck": "tsc --noEmit",
|
|
280
|
+
"test": "vitest run",
|
|
281
|
+
"test:watch": "vitest",
|
|
282
|
+
"test:e2e": "playwright test",
|
|
283
|
+
"setup:convex": "node scripts/setup-convex.mjs"
|
|
284
|
+
},
|
|
285
|
+
"dependencies": {
|
|
286
|
+
"next": "^16.0.0",
|
|
287
|
+
"react": "^19.0.0",
|
|
288
|
+
"react-dom": "^19.0.0",
|
|
289
|
+
"convex": "^1.25.0",
|
|
290
|
+
"@convex-dev/auth": "^0.0.90",
|
|
291
|
+
"@auth/core": "^0.37.0",
|
|
292
|
+
"@hugeicons/react": "^0.3.0",
|
|
293
|
+
"class-variance-authority": "^0.7.0",
|
|
294
|
+
"clsx": "^2.1.0",
|
|
295
|
+
"tailwind-merge": "^2.5.0",
|
|
296
|
+
"tw-animate-css": "^1.3.0",
|
|
297
|
+
"resend": "^4.0.0",
|
|
298
|
+
"react-email": "^3.0.0",
|
|
299
|
+
"@react-email/components": "^0.0.36"{{#if (eq integrations.analytics 'posthog')}},
|
|
300
|
+
"posthog-js": "^1.200.0",
|
|
301
|
+
"posthog-node": "^5.0.0"{{/if}}{{#if (eq integrations.analytics 'vercel')}},
|
|
302
|
+
"@vercel/analytics": "^1.4.0",
|
|
303
|
+
"@vercel/speed-insights": "^1.1.0"{{/if}}{{#if (eq integrations.uploads 'uploadthing')}},
|
|
304
|
+
"uploadthing": "^7.0.0",
|
|
305
|
+
"@uploadthing/react": "^7.0.0"{{/if}}{{#if (eq integrations.uploads 's3')}},
|
|
306
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
307
|
+
"@aws-sdk/s3-request-presigner": "^3.700.0"{{/if}}{{#if (eq integrations.uploads 'vercel-blob')}},
|
|
308
|
+
"@vercel/blob": "^2.0.0"{{/if}}{{#if (includes addons 'rate-limiting')}},
|
|
309
|
+
"@arcjet/next": "^1.0.0-beta.16"{{/if}}{{#if (includes addons 'monitoring')}},
|
|
310
|
+
"@sentry/nextjs": "^8.0.0"{{/if}}
|
|
311
|
+
},
|
|
312
|
+
"devDependencies": {
|
|
313
|
+
{{#if (eq structure 'monorepo')}} "@repo/config-typescript": "workspace:*",
|
|
314
|
+
{{/if}} "@types/node": "^20.0.0",
|
|
315
|
+
"@types/react": "^19.0.0",
|
|
316
|
+
"@types/react-dom": "^19.0.0",
|
|
317
|
+
"tailwindcss": "^4.0.0",
|
|
318
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
319
|
+
"postcss": "^8.4.0",
|
|
320
|
+
"typescript": "^5.0.0",
|
|
321
|
+
"vitest": "^3.0.0",
|
|
322
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
323
|
+
"@testing-library/react": "^16.0.0",
|
|
324
|
+
"jsdom": "^26.0.0",
|
|
325
|
+
"playwright": "^1.50.0",
|
|
326
|
+
"@playwright/test": "^1.50.0"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
`,
|
|
330
|
+
"web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
331
|
+
"web/src/app/globals.css.hbs": '@import "tailwindcss";\n@import "tw-animate-css";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n',
|
|
332
|
+
"web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
|
|
333
|
+
"web/src/app/page.tsx.hbs": `'use client'
|
|
334
|
+
|
|
335
|
+
import { useAuth } from '@/lib/auth'
|
|
336
|
+
|
|
337
|
+
export default function HomePage() {
|
|
338
|
+
const { isAuthenticated, isLoading, signIn, signOut, user } = useAuth()
|
|
339
|
+
|
|
340
|
+
if (isLoading) {
|
|
341
|
+
return (
|
|
342
|
+
<main className="min-h-screen flex items-center justify-center">
|
|
343
|
+
<div className="animate-pulse">Loading...</div>
|
|
344
|
+
</main>
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<main className="min-h-screen flex flex-col items-center justify-center p-8">
|
|
350
|
+
<div className="max-w-2xl text-center space-y-8">
|
|
351
|
+
<h1 className="text-4xl font-bold">{{projectName}}</h1>
|
|
352
|
+
<p className="text-xl text-muted-foreground">
|
|
353
|
+
Built with Next.js, Convex, Better-Auth, and shadcn/ui
|
|
354
|
+
</p>
|
|
355
|
+
|
|
356
|
+
{isAuthenticated ? (
|
|
357
|
+
<div className="space-y-4">
|
|
358
|
+
<p className="text-lg">
|
|
359
|
+
Welcome, <span className="font-semibold">{user?.name || user?.email}</span>!
|
|
360
|
+
</p>
|
|
361
|
+
<button
|
|
362
|
+
onClick={() => signOut()}
|
|
363
|
+
className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
|
|
364
|
+
>
|
|
365
|
+
Sign Out
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
) : (
|
|
369
|
+
<div className="space-y-4">
|
|
370
|
+
<p className="text-muted-foreground">
|
|
371
|
+
Sign in to get started
|
|
372
|
+
</p>
|
|
373
|
+
<div className="flex gap-4 justify-center">
|
|
374
|
+
<button
|
|
375
|
+
onClick={() => signIn('github')}
|
|
376
|
+
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition"
|
|
377
|
+
>
|
|
378
|
+
Sign in with GitHub
|
|
379
|
+
</button>
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => signIn('google')}
|
|
382
|
+
className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
|
|
383
|
+
>
|
|
384
|
+
Sign in with Google
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
<div className="pt-8 border-t border-border">
|
|
391
|
+
<p className="text-sm text-muted-foreground">
|
|
392
|
+
Created with{' '}
|
|
393
|
+
<a
|
|
394
|
+
href="https://github.com/theodenanyoh11/create-kofi-stack"
|
|
395
|
+
className="text-primary hover:underline"
|
|
396
|
+
target="_blank"
|
|
397
|
+
rel="noopener noreferrer"
|
|
398
|
+
>
|
|
399
|
+
create-kofi-stack
|
|
400
|
+
</a>
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</main>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
`,
|
|
408
|
+
"web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs'\nimport { ConvexReactClient } from 'convex/react'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsProvider client={convex}>\n {children}\n </ConvexAuthNextjsProvider>\n )\n}\n",
|
|
409
|
+
"web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth, useMutation, useQuery } from 'convex/react'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../../convex/_generated/api{{/if}}'\n\nexport function useAuth() {\n const { isAuthenticated, isLoading } = useConvexAuth()\n const { signIn, signOut } = useAuthActions()\n const user = useQuery(api.users.current)\n\n return {\n isAuthenticated,\n isLoading,\n user,\n signIn: (provider: 'github' | 'google') => {\n void signIn(provider)\n },\n signOut: () => {\n void signOut()\n },\n }\n}\n",
|
|
410
|
+
"web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
|
|
411
|
+
"web/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n'
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/generator.ts
|
|
415
|
+
import path3 from "path";
|
|
416
|
+
async function generateVirtualProject(config) {
|
|
417
|
+
const vfs = new VirtualFileSystem();
|
|
418
|
+
const errors = [];
|
|
419
|
+
try {
|
|
420
|
+
await processBaseTemplates(vfs, config);
|
|
421
|
+
await processWebAppTemplates(vfs, config);
|
|
422
|
+
await processConvexTemplates(vfs, config);
|
|
423
|
+
await processBetterAuthTemplates(vfs, config);
|
|
424
|
+
if (config.structure === "monorepo") {
|
|
425
|
+
await processMonorepoTemplates(vfs, config);
|
|
426
|
+
if (config.marketingSite === "payload") {
|
|
427
|
+
await processPayloadTemplates(vfs, config);
|
|
428
|
+
} else if (config.marketingSite === "nextjs") {
|
|
429
|
+
await processMarketingTemplates(vfs, config);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
await processIntegrationTemplates(vfs, config);
|
|
433
|
+
await processAddonTemplates(vfs, config);
|
|
434
|
+
await postProcess(vfs, config);
|
|
435
|
+
const tree = vfs.toTree(config);
|
|
436
|
+
return {
|
|
437
|
+
tree,
|
|
438
|
+
success: true
|
|
439
|
+
};
|
|
440
|
+
} catch (error) {
|
|
441
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
442
|
+
return {
|
|
443
|
+
tree: vfs.toTree(config),
|
|
444
|
+
success: false,
|
|
445
|
+
errors
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function processTemplatesFromPrefix(vfs, prefix, outputPrefix, config) {
|
|
450
|
+
for (const [templatePath, content] of Object.entries(EMBEDDED_TEMPLATES)) {
|
|
451
|
+
if (!templatePath.startsWith(prefix)) continue;
|
|
452
|
+
if (!shouldIncludeFile(templatePath, config)) continue;
|
|
453
|
+
const relativePath = templatePath.slice(prefix.length);
|
|
454
|
+
const outputPath = path3.join(outputPrefix, relativePath);
|
|
455
|
+
const dir = path3.dirname(outputPath);
|
|
456
|
+
const filename = path3.basename(outputPath);
|
|
457
|
+
const transformedFilename = transformFilename(filename, config);
|
|
458
|
+
const finalPath = path3.join(dir, transformedFilename);
|
|
459
|
+
if (isBinaryFile(filename)) {
|
|
460
|
+
vfs.writeFile(finalPath, Buffer.from(content, "base64"));
|
|
461
|
+
} else {
|
|
462
|
+
const processedContent = processTemplateString(content, config);
|
|
463
|
+
vfs.writeFile(finalPath, processedContent);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function processBaseTemplates(vfs, config) {
|
|
468
|
+
processTemplatesFromPrefix(vfs, "base/", "/", config);
|
|
469
|
+
}
|
|
470
|
+
async function processWebAppTemplates(vfs, config) {
|
|
471
|
+
const appPath = config.structure === "monorepo" ? "/apps/web" : "/";
|
|
472
|
+
processTemplatesFromPrefix(vfs, "web/", appPath, config);
|
|
473
|
+
}
|
|
474
|
+
async function processConvexTemplates(vfs, config) {
|
|
475
|
+
const convexPath = config.structure === "monorepo" ? "/packages/backend" : "/";
|
|
476
|
+
processTemplatesFromPrefix(vfs, "convex/", convexPath, config);
|
|
477
|
+
}
|
|
478
|
+
async function processBetterAuthTemplates(vfs, config) {
|
|
479
|
+
const webPath = config.structure === "monorepo" ? "/apps/web" : "/";
|
|
480
|
+
processTemplatesFromPrefix(vfs, "auth/", webPath, config);
|
|
481
|
+
}
|
|
482
|
+
async function processMonorepoTemplates(vfs, config) {
|
|
483
|
+
processTemplatesFromPrefix(vfs, "monorepo/", "/", config);
|
|
484
|
+
processTemplatesFromPrefix(vfs, "packages/ui/", "/packages/ui", config);
|
|
485
|
+
processTemplatesFromPrefix(vfs, "packages/config/", "/packages/config", config);
|
|
486
|
+
}
|
|
487
|
+
async function processPayloadTemplates(vfs, config) {
|
|
488
|
+
processTemplatesFromPrefix(vfs, "marketing/payload/", "/apps/marketing", config);
|
|
489
|
+
}
|
|
490
|
+
async function processMarketingTemplates(vfs, config) {
|
|
491
|
+
processTemplatesFromPrefix(vfs, "marketing/nextjs/", "/apps/marketing", config);
|
|
492
|
+
}
|
|
493
|
+
async function processIntegrationTemplates(vfs, config) {
|
|
494
|
+
const webPath = config.structure === "monorepo" ? "/apps/web" : "/";
|
|
495
|
+
if (config.integrations.analytics === "posthog") {
|
|
496
|
+
processTemplatesFromPrefix(vfs, "integrations/posthog/", webPath, config);
|
|
497
|
+
}
|
|
498
|
+
if (config.integrations.analytics === "vercel") {
|
|
499
|
+
processTemplatesFromPrefix(vfs, "integrations/vercel-analytics/", webPath, config);
|
|
500
|
+
}
|
|
501
|
+
if (config.integrations.uploads === "uploadthing") {
|
|
502
|
+
processTemplatesFromPrefix(vfs, "integrations/uploadthing/", webPath, config);
|
|
503
|
+
}
|
|
504
|
+
if (config.integrations.uploads === "s3") {
|
|
505
|
+
processTemplatesFromPrefix(vfs, "integrations/s3/", webPath, config);
|
|
506
|
+
}
|
|
507
|
+
if (config.integrations.uploads === "vercel-blob") {
|
|
508
|
+
processTemplatesFromPrefix(vfs, "integrations/vercel-blob/", webPath, config);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function processAddonTemplates(vfs, config) {
|
|
512
|
+
for (const addon of config.addons) {
|
|
513
|
+
processTemplatesFromPrefix(vfs, `addons/${addon}/`, "/", config);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function postProcess(vfs, config) {
|
|
517
|
+
const scriptsPath = config.structure === "monorepo" ? "/scripts" : "/scripts";
|
|
518
|
+
const webScriptsPath = config.structure === "monorepo" ? "/apps/web/scripts" : "/scripts";
|
|
519
|
+
generateDevScript(vfs, scriptsPath, config);
|
|
520
|
+
generateSetupConvexScript(vfs, webScriptsPath, config);
|
|
521
|
+
generateReadme(vfs, config);
|
|
522
|
+
}
|
|
523
|
+
function generateDevScript(vfs, scriptsPath, config) {
|
|
524
|
+
const isMonorepo = config.structure === "monorepo";
|
|
525
|
+
const webAppDir = isMonorepo ? "apps/web" : ".";
|
|
526
|
+
const devScript = `#!/usr/bin/env node
|
|
527
|
+
/**
|
|
528
|
+
* Dev Script - Starts Next.js and Convex dev servers
|
|
529
|
+
*/
|
|
530
|
+
|
|
531
|
+
import { spawn, execSync } from 'child_process'
|
|
532
|
+
import { existsSync, readFileSync } from 'fs'
|
|
533
|
+
import { resolve, dirname } from 'path'
|
|
534
|
+
import { fileURLToPath } from 'url'
|
|
535
|
+
|
|
536
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
537
|
+
const rootDir = resolve(__dirname, '..')
|
|
538
|
+
const webAppDir = resolve(rootDir, '${webAppDir}')
|
|
539
|
+
|
|
540
|
+
function loadEnvFile(dir) {
|
|
541
|
+
const envPath = resolve(dir, '.env.local')
|
|
542
|
+
if (!existsSync(envPath)) return {}
|
|
543
|
+
|
|
544
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
545
|
+
const env = {}
|
|
546
|
+
|
|
547
|
+
for (const line of content.split('\\n')) {
|
|
548
|
+
const trimmed = line.trim()
|
|
549
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
550
|
+
const eqIndex = trimmed.indexOf('=')
|
|
551
|
+
if (eqIndex === -1) continue
|
|
552
|
+
const key = trimmed.slice(0, eqIndex)
|
|
553
|
+
let value = trimmed.slice(eqIndex + 1)
|
|
554
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
555
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
556
|
+
value = value.slice(1, -1)
|
|
557
|
+
}
|
|
558
|
+
if (value) env[key] = value
|
|
559
|
+
}
|
|
560
|
+
return env
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function checkAndInstall() {
|
|
564
|
+
if (!existsSync(resolve(rootDir, 'node_modules'))) {
|
|
565
|
+
console.log('\u{1F4E6} Installing dependencies...\\n')
|
|
566
|
+
execSync('pnpm install', { cwd: rootDir, stdio: 'inherit' })
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function startDevServers() {
|
|
571
|
+
const localEnv = loadEnvFile(webAppDir)
|
|
572
|
+
|
|
573
|
+
if (!localEnv.CONVEX_DEPLOYMENT) {
|
|
574
|
+
console.log('\u26A0\uFE0F Convex not configured. Run: pnpm dev:setup\\n')
|
|
575
|
+
console.log('Starting Next.js only...\\n')
|
|
576
|
+
spawn('pnpm', ['${isMonorepo ? "dev:turbo" : "dev:next"}'], {
|
|
577
|
+
cwd: rootDir, stdio: 'inherit', shell: true
|
|
578
|
+
})
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log('\u{1F680} Starting development servers...\\n')
|
|
583
|
+
|
|
584
|
+
const nextProcess = spawn('pnpm', ['${isMonorepo ? "dev:turbo" : "dev:next"}'], {
|
|
585
|
+
cwd: rootDir, stdio: 'inherit', shell: true
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
const convexProcess = spawn('npx', ['convex', 'dev'], {
|
|
589
|
+
cwd: webAppDir, stdio: 'inherit', shell: true
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const cleanup = () => {
|
|
593
|
+
nextProcess.kill()
|
|
594
|
+
convexProcess.kill()
|
|
595
|
+
process.exit(0)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
process.on('SIGINT', cleanup)
|
|
599
|
+
process.on('SIGTERM', cleanup)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function main() {
|
|
603
|
+
await checkAndInstall()
|
|
604
|
+
startDevServers()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
main()
|
|
608
|
+
`;
|
|
609
|
+
vfs.writeFile(`${scriptsPath}/dev.mjs`, devScript);
|
|
610
|
+
}
|
|
611
|
+
function generateSetupConvexScript(vfs, scriptsPath, config) {
|
|
612
|
+
const setupScript = `#!/usr/bin/env node
|
|
613
|
+
/**
|
|
614
|
+
* Setup Convex - Interactive setup wizard for Convex
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
import { execSync, spawnSync } from 'child_process'
|
|
618
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
619
|
+
import { resolve, dirname } from 'path'
|
|
620
|
+
import { fileURLToPath } from 'url'
|
|
621
|
+
import * as readline from 'readline'
|
|
622
|
+
|
|
623
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
624
|
+
const projectDir = resolve(__dirname, '..')
|
|
625
|
+
|
|
626
|
+
function prompt(question) {
|
|
627
|
+
const rl = readline.createInterface({
|
|
628
|
+
input: process.stdin,
|
|
629
|
+
output: process.stdout
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
return new Promise((resolve) => {
|
|
633
|
+
rl.question(question, (answer) => {
|
|
634
|
+
rl.close()
|
|
635
|
+
resolve(answer.trim())
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function main() {
|
|
641
|
+
console.log('\\n\u{1F527} Convex Setup Wizard\\n')
|
|
642
|
+
|
|
643
|
+
// Check if already configured
|
|
644
|
+
const envPath = resolve(projectDir, '.env.local')
|
|
645
|
+
if (existsSync(envPath)) {
|
|
646
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
647
|
+
if (content.includes('CONVEX_DEPLOYMENT=') && !content.includes('CONVEX_DEPLOYMENT=\\n')) {
|
|
648
|
+
console.log('\u2705 Convex is already configured!')
|
|
649
|
+
console.log(' Run "pnpm dev" to start development.\\n')
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log('This wizard will help you set up Convex for your project.\\n')
|
|
655
|
+
|
|
656
|
+
// Run convex dev to trigger authentication and project creation
|
|
657
|
+
console.log('Running Convex setup (this will open your browser if needed)...\\n')
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
spawnSync('npx', ['convex', 'dev', '--once'], {
|
|
661
|
+
cwd: projectDir,
|
|
662
|
+
stdio: 'inherit',
|
|
663
|
+
shell: true
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
console.log('\\n\u2705 Convex setup complete!')
|
|
667
|
+
console.log(' Run "pnpm dev" to start development.\\n')
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.error('\\n\u274C Convex setup failed:', error.message)
|
|
670
|
+
console.error(' Try running "npx convex dev" manually.\\n')
|
|
671
|
+
process.exit(1)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
main()
|
|
676
|
+
`;
|
|
677
|
+
vfs.writeFile(`${scriptsPath}/setup-convex.mjs`, setupScript);
|
|
678
|
+
}
|
|
679
|
+
function generateReadme(vfs, config) {
|
|
680
|
+
const readme = `# ${config.projectName}
|
|
681
|
+
|
|
682
|
+
Built with [create-kofi-stack](https://github.com/theodenanyoh11/create-kofi-stack)
|
|
683
|
+
|
|
684
|
+
## Tech Stack
|
|
685
|
+
|
|
686
|
+
- **Framework**: Next.js 16 with App Router
|
|
687
|
+
- **Backend**: Convex (reactive backend-as-a-service)
|
|
688
|
+
- **Auth**: Better-Auth with Convex adapter
|
|
689
|
+
- **UI**: shadcn/ui with ${config.shadcn.componentLibrary}
|
|
690
|
+
- **Styling**: Tailwind CSS v4
|
|
691
|
+
|
|
692
|
+
## Getting Started
|
|
693
|
+
|
|
694
|
+
\`\`\`bash
|
|
695
|
+
cd ${config.projectName}
|
|
696
|
+
pnpm dev
|
|
697
|
+
\`\`\`
|
|
698
|
+
|
|
699
|
+
This will:
|
|
700
|
+
- Install dependencies (if needed)
|
|
701
|
+
- Set up Convex (if not configured)
|
|
702
|
+
- Start Next.js and Convex dev servers
|
|
703
|
+
|
|
704
|
+
## Project Structure
|
|
705
|
+
|
|
706
|
+
${config.structure === "monorepo" ? `
|
|
707
|
+
\`\`\`
|
|
708
|
+
\u251C\u2500\u2500 apps/
|
|
709
|
+
\u2502 \u251C\u2500\u2500 web/ # Main Next.js application
|
|
710
|
+
\u2502 ${config.marketingSite !== "none" ? "\u251C\u2500\u2500 marketing/ # Marketing site" : ""}
|
|
711
|
+
\u2502 \u2514\u2500\u2500 design-system/ # Component showcase
|
|
712
|
+
\u251C\u2500\u2500 packages/
|
|
713
|
+
\u2502 \u251C\u2500\u2500 backend/ # Convex functions
|
|
714
|
+
\u2502 \u2514\u2500\u2500 ui/ # Shared UI components
|
|
715
|
+
\u2514\u2500\u2500 ...
|
|
716
|
+
\`\`\`
|
|
717
|
+
` : `
|
|
718
|
+
\`\`\`
|
|
719
|
+
\u251C\u2500\u2500 src/
|
|
720
|
+
\u2502 \u251C\u2500\u2500 app/ # Next.js App Router
|
|
721
|
+
\u2502 \u251C\u2500\u2500 components/ # React components
|
|
722
|
+
\u2502 \u2514\u2500\u2500 lib/ # Utilities
|
|
723
|
+
\u251C\u2500\u2500 convex/ # Convex functions
|
|
724
|
+
\u2514\u2500\u2500 ...
|
|
725
|
+
\`\`\`
|
|
726
|
+
`}
|
|
727
|
+
|
|
728
|
+
## Documentation
|
|
729
|
+
|
|
730
|
+
- [Convex](https://docs.convex.dev)
|
|
731
|
+
- [Better-Auth](https://www.better-auth.com)
|
|
732
|
+
- [shadcn/ui](https://ui.shadcn.com)
|
|
733
|
+
- [Next.js](https://nextjs.org/docs)
|
|
734
|
+
`;
|
|
735
|
+
vfs.writeFile("/README.md", readme);
|
|
736
|
+
}
|
|
737
|
+
export {
|
|
738
|
+
VirtualFileSystem,
|
|
739
|
+
generateVirtualProject,
|
|
740
|
+
isBinaryFile,
|
|
741
|
+
processTemplateString,
|
|
742
|
+
shouldIncludeFile,
|
|
743
|
+
transformFilename
|
|
744
|
+
};
|