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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/dist/index.d.ts +94 -0
  3. package/dist/index.js +744 -0
  4. package/package.json +29 -0
  5. package/scripts/generate-templates.js +104 -0
  6. package/src/core/index.ts +7 -0
  7. package/src/core/template-processor.ts +127 -0
  8. package/src/core/virtual-fs.ts +189 -0
  9. package/src/generator.ts +429 -0
  10. package/src/index.ts +19 -0
  11. package/src/templates.generated.ts +39 -0
  12. package/templates/base/_gitignore.hbs +45 -0
  13. package/templates/base/biome.json.hbs +34 -0
  14. package/templates/convex/_env.local.hbs +52 -0
  15. package/templates/convex/convex/auth.ts.hbs +7 -0
  16. package/templates/convex/convex/http.ts.hbs +8 -0
  17. package/templates/convex/convex/schema.ts.hbs +15 -0
  18. package/templates/convex/convex/users.ts.hbs +13 -0
  19. package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +17 -0
  20. package/templates/monorepo/package.json.hbs +29 -0
  21. package/templates/monorepo/pnpm-workspace.yaml.hbs +3 -0
  22. package/templates/monorepo/turbo.json.hbs +42 -0
  23. package/templates/packages/config-biome/biome.json.hbs +4 -0
  24. package/templates/packages/config-biome/package.json.hbs +6 -0
  25. package/templates/packages/config-typescript/base.json.hbs +17 -0
  26. package/templates/packages/config-typescript/nextjs.json.hbs +7 -0
  27. package/templates/packages/config-typescript/package.json.hbs +10 -0
  28. package/templates/packages/ui/components.json.hbs +20 -0
  29. package/templates/packages/ui/package.json.hbs +34 -0
  30. package/templates/packages/ui/src/index.ts.hbs +3 -0
  31. package/templates/packages/ui/src/lib/utils.ts.hbs +6 -0
  32. package/templates/packages/ui/tsconfig.json.hbs +22 -0
  33. package/templates/web/components.json.hbs +20 -0
  34. package/templates/web/next.config.ts.hbs +9 -0
  35. package/templates/web/package.json.hbs +62 -0
  36. package/templates/web/postcss.config.mjs.hbs +5 -0
  37. package/templates/web/src/app/globals.css.hbs +122 -0
  38. package/templates/web/src/app/layout.tsx.hbs +55 -0
  39. package/templates/web/src/app/page.tsx.hbs +74 -0
  40. package/templates/web/src/components/providers/convex-provider.tsx.hbs +18 -0
  41. package/templates/web/src/lib/auth.ts.hbs +23 -0
  42. package/templates/web/src/lib/utils.ts.hbs +6 -0
  43. package/templates/web/tsconfig.json.hbs +23 -0
  44. package/tsconfig.json +15 -0
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "kofi-stack-template-generator",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "pnpm run prebuild && tsup src/index.ts --format esm --dts",
15
+ "dev": "tsup src/index.ts --format esm --dts --watch",
16
+ "prebuild": "node scripts/generate-templates.js",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "kofi-stack-types": "^2.0.0",
21
+ "handlebars": "^4.7.8",
22
+ "memfs": "^4.9.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.0.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.0.0"
28
+ }
29
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate templates.generated.ts from template files
4
+ *
5
+ * This script reads all template files from the templates/ directory
6
+ * and embeds them as a TypeScript object for browser/Node.js compatibility.
7
+ */
8
+
9
+ import { readdirSync, readFileSync, writeFileSync, statSync, existsSync } from 'fs'
10
+ import { join, relative, extname } from 'path'
11
+ import { fileURLToPath } from 'url'
12
+ import { dirname } from 'path'
13
+
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = dirname(__filename)
16
+
17
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates')
18
+ const OUTPUT_FILE = join(__dirname, '..', 'src', 'templates.generated.ts')
19
+
20
+ // Binary extensions that should be base64 encoded
21
+ const BINARY_EXTENSIONS = new Set([
22
+ '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp', '.svg',
23
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
24
+ '.mp3', '.mp4', '.webm', '.pdf', '.zip', '.tar', '.gz'
25
+ ])
26
+
27
+ function isBinaryFile(filename) {
28
+ const ext = extname(filename).toLowerCase()
29
+ return BINARY_EXTENSIONS.has(ext)
30
+ }
31
+
32
+ function getAllFiles(dir, baseDir = dir) {
33
+ const files = []
34
+
35
+ if (!existsSync(dir)) {
36
+ console.warn(`Warning: Templates directory not found at ${dir}`)
37
+ return files
38
+ }
39
+
40
+ const entries = readdirSync(dir)
41
+
42
+ for (const entry of entries) {
43
+ const fullPath = join(dir, entry)
44
+ const stat = statSync(fullPath)
45
+
46
+ if (stat.isDirectory()) {
47
+ files.push(...getAllFiles(fullPath, baseDir))
48
+ } else {
49
+ const relativePath = relative(baseDir, fullPath)
50
+ files.push({
51
+ path: relativePath.replace(/\\/g, '/'), // Normalize to forward slashes
52
+ fullPath
53
+ })
54
+ }
55
+ }
56
+
57
+ return files
58
+ }
59
+
60
+ function generateTemplates() {
61
+ console.log('Generating templates.generated.ts...')
62
+
63
+ const files = getAllFiles(TEMPLATES_DIR)
64
+
65
+ if (files.length === 0) {
66
+ // Create empty templates file if no templates exist yet
67
+ const output = `// Auto-generated file. Do not edit manually.
68
+ // Run 'pnpm prebuild' to regenerate.
69
+
70
+ export const EMBEDDED_TEMPLATES: Record<string, string> = {}
71
+ `
72
+ writeFileSync(OUTPUT_FILE, output)
73
+ console.log('Created empty templates.generated.ts (no templates found)')
74
+ return
75
+ }
76
+
77
+ const templates = {}
78
+
79
+ for (const file of files) {
80
+ const content = readFileSync(file.fullPath)
81
+
82
+ if (isBinaryFile(file.path)) {
83
+ // Base64 encode binary files
84
+ templates[file.path] = content.toString('base64')
85
+ } else {
86
+ // Store text files as-is
87
+ templates[file.path] = content.toString('utf-8')
88
+ }
89
+ }
90
+
91
+ // Generate the TypeScript file
92
+ const output = `// Auto-generated file. Do not edit manually.
93
+ // Run 'pnpm prebuild' to regenerate.
94
+ // Generated: ${new Date().toISOString()}
95
+ // Template count: ${files.length}
96
+
97
+ export const EMBEDDED_TEMPLATES: Record<string, string> = ${JSON.stringify(templates, null, 2)}
98
+ `
99
+
100
+ writeFileSync(OUTPUT_FILE, output)
101
+ console.log(`Generated templates.generated.ts with ${files.length} templates`)
102
+ }
103
+
104
+ generateTemplates()
@@ -0,0 +1,7 @@
1
+ export { VirtualFileSystem } from './virtual-fs.js'
2
+ export {
3
+ processTemplateString,
4
+ transformFilename,
5
+ isBinaryFile,
6
+ shouldIncludeFile,
7
+ } from './template-processor.js'
@@ -0,0 +1,127 @@
1
+ import Handlebars from 'handlebars'
2
+ import type { ProjectConfig } from 'kofi-stack-types'
3
+ import path from 'path'
4
+
5
+ // Register custom Handlebars helpers
6
+ Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b)
7
+ Handlebars.registerHelper('ne', (a: unknown, b: unknown) => a !== b)
8
+ Handlebars.registerHelper('and', (...args: unknown[]) => {
9
+ // Remove the options object (last argument)
10
+ const values = args.slice(0, -1)
11
+ return values.every(Boolean)
12
+ })
13
+ Handlebars.registerHelper('or', (...args: unknown[]) => {
14
+ const values = args.slice(0, -1)
15
+ return values.some(Boolean)
16
+ })
17
+ Handlebars.registerHelper('includes', (array: unknown[], value: unknown) => {
18
+ if (!Array.isArray(array)) return false
19
+ return array.includes(value)
20
+ })
21
+ Handlebars.registerHelper('not', (value: unknown) => !value)
22
+ Handlebars.registerHelper('json', (value: unknown) => JSON.stringify(value, null, 2))
23
+
24
+ // Binary file extensions that shouldn't be processed as templates
25
+ const BINARY_EXTENSIONS = new Set([
26
+ '.png',
27
+ '.jpg',
28
+ '.jpeg',
29
+ '.gif',
30
+ '.ico',
31
+ '.webp',
32
+ '.svg',
33
+ '.woff',
34
+ '.woff2',
35
+ '.ttf',
36
+ '.eot',
37
+ '.otf',
38
+ '.mp3',
39
+ '.mp4',
40
+ '.webm',
41
+ '.pdf',
42
+ '.zip',
43
+ '.tar',
44
+ '.gz',
45
+ ])
46
+
47
+ /**
48
+ * Check if a file is binary based on extension
49
+ */
50
+ export function isBinaryFile(filename: string): boolean {
51
+ const ext = path.extname(filename).toLowerCase()
52
+ return BINARY_EXTENSIONS.has(ext)
53
+ }
54
+
55
+ /**
56
+ * Process a Handlebars template string with the given config context
57
+ */
58
+ export function processTemplateString(
59
+ template: string,
60
+ config: ProjectConfig
61
+ ): string {
62
+ try {
63
+ const compiled = Handlebars.compile(template, { noEscape: true })
64
+ return compiled(config)
65
+ } catch (error) {
66
+ console.error('Template processing error:', error)
67
+ return template
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Transform a template filename to the final output filename
73
+ * - Remove .hbs extension
74
+ * - Convert _gitignore to .gitignore
75
+ * - Process filename through Handlebars if it contains {{ }}
76
+ */
77
+ export function transformFilename(
78
+ filename: string,
79
+ config: ProjectConfig
80
+ ): string {
81
+ let result = filename
82
+
83
+ // Remove .hbs extension
84
+ if (result.endsWith('.hbs')) {
85
+ result = result.slice(0, -4)
86
+ }
87
+
88
+ // Convert underscore prefix to dot prefix (for hidden files)
89
+ if (result.startsWith('_')) {
90
+ result = '.' + result.slice(1)
91
+ }
92
+
93
+ // Process filename through Handlebars if it contains template syntax
94
+ if (result.includes('{{')) {
95
+ result = processTemplateString(result, config)
96
+ }
97
+
98
+ return result
99
+ }
100
+
101
+ /**
102
+ * Check if a template file should be processed
103
+ * Some files are conditionally included based on config
104
+ */
105
+ export function shouldIncludeFile(
106
+ templatePath: string,
107
+ config: ProjectConfig
108
+ ): boolean {
109
+ // Files in conditional directories
110
+ if (templatePath.includes('/if-monorepo/') && config.structure !== 'monorepo') {
111
+ return false
112
+ }
113
+ if (templatePath.includes('/if-standalone/') && config.structure !== 'standalone') {
114
+ return false
115
+ }
116
+ if (templatePath.includes('/if-payload/') && config.marketingSite !== 'payload') {
117
+ return false
118
+ }
119
+ if (templatePath.includes('/if-posthog/') && config.integrations.analytics !== 'posthog') {
120
+ return false
121
+ }
122
+ if (templatePath.includes('/if-uploadthing/') && config.integrations.uploads !== 'uploadthing') {
123
+ return false
124
+ }
125
+
126
+ return true
127
+ }
@@ -0,0 +1,189 @@
1
+ import { createFsFromVolume, Volume } from 'memfs'
2
+ import type {
3
+ VirtualFile,
4
+ VirtualDirectory,
5
+ VirtualNode,
6
+ VirtualFileTree,
7
+ ProjectConfig,
8
+ } from 'kofi-stack-types'
9
+ import path from 'path'
10
+
11
+ /**
12
+ * Virtual filesystem using memfs for in-memory file operations.
13
+ * This allows us to generate the entire project tree without disk I/O,
14
+ * enabling browser compatibility and faster generation.
15
+ */
16
+ export class VirtualFileSystem {
17
+ private volume: InstanceType<typeof Volume>
18
+ private fs: ReturnType<typeof createFsFromVolume>
19
+ private binarySourcePaths: Map<string, string> = new Map()
20
+
21
+ constructor() {
22
+ this.volume = new Volume()
23
+ this.fs = createFsFromVolume(this.volume)
24
+ }
25
+
26
+ /**
27
+ * Write a file to the virtual filesystem
28
+ */
29
+ writeFile(filePath: string, content: string | Buffer): void {
30
+ const dir = path.dirname(filePath)
31
+ this.mkdir(dir)
32
+ this.fs.writeFileSync(filePath, content)
33
+ }
34
+
35
+ /**
36
+ * Read a file from the virtual filesystem
37
+ */
38
+ readFile(filePath: string): string | Buffer {
39
+ return this.fs.readFileSync(filePath)
40
+ }
41
+
42
+ /**
43
+ * Check if a path exists
44
+ */
45
+ exists(filePath: string): boolean {
46
+ try {
47
+ this.fs.statSync(filePath)
48
+ return true
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Check if path is a file
56
+ */
57
+ fileExists(filePath: string): boolean {
58
+ try {
59
+ return this.fs.statSync(filePath).isFile()
60
+ } catch {
61
+ return false
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if path is a directory
67
+ */
68
+ directoryExists(filePath: string): boolean {
69
+ try {
70
+ return this.fs.statSync(filePath).isDirectory()
71
+ } catch {
72
+ return false
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Create directory recursively
78
+ */
79
+ mkdir(dirPath: string): void {
80
+ if (!this.exists(dirPath)) {
81
+ this.fs.mkdirSync(dirPath, { recursive: true })
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Delete a file
87
+ */
88
+ deleteFile(filePath: string): void {
89
+ if (this.fileExists(filePath)) {
90
+ this.fs.unlinkSync(filePath)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * List directory contents
96
+ */
97
+ listDir(dirPath: string): string[] {
98
+ if (!this.directoryExists(dirPath)) {
99
+ return []
100
+ }
101
+ return this.fs.readdirSync(dirPath) as string[]
102
+ }
103
+
104
+ /**
105
+ * Track source path for binary files (for later copying)
106
+ */
107
+ setBinarySourcePath(virtualPath: string, sourcePath: string): void {
108
+ this.binarySourcePaths.set(virtualPath, sourcePath)
109
+ }
110
+
111
+ /**
112
+ * Get source path for binary file
113
+ */
114
+ getBinarySourcePath(virtualPath: string): string | undefined {
115
+ return this.binarySourcePaths.get(virtualPath)
116
+ }
117
+
118
+ /**
119
+ * Convert the virtual filesystem to a tree structure
120
+ */
121
+ toTree(config: ProjectConfig): VirtualFileTree {
122
+ const root = this.buildTree('/')
123
+ const stats = this.countNodes(root)
124
+
125
+ return {
126
+ root,
127
+ fileCount: stats.files,
128
+ directoryCount: stats.directories,
129
+ config,
130
+ }
131
+ }
132
+
133
+ private buildTree(dirPath: string): VirtualDirectory {
134
+ const name = dirPath === '/' ? '/' : path.basename(dirPath)
135
+ const children: VirtualNode[] = []
136
+
137
+ const entries = this.listDir(dirPath)
138
+ for (const entry of entries) {
139
+ const fullPath = path.join(dirPath, entry)
140
+ const stat = this.fs.statSync(fullPath)
141
+
142
+ if (stat.isDirectory()) {
143
+ children.push(this.buildTree(fullPath))
144
+ } else {
145
+ const content = this.fs.readFileSync(fullPath)
146
+ const file: VirtualFile = {
147
+ type: 'file',
148
+ path: fullPath,
149
+ name: entry,
150
+ content: content as string | Buffer,
151
+ extension: path.extname(entry),
152
+ sourcePath: this.binarySourcePaths.get(fullPath),
153
+ }
154
+ children.push(file)
155
+ }
156
+ }
157
+
158
+ return {
159
+ type: 'directory',
160
+ path: dirPath,
161
+ name,
162
+ children,
163
+ }
164
+ }
165
+
166
+ private countNodes(node: VirtualNode): { files: number; directories: number } {
167
+ if (node.type === 'file') {
168
+ return { files: 1, directories: 0 }
169
+ }
170
+
171
+ let files = 0
172
+ let directories = 1 // Count this directory
173
+
174
+ for (const child of node.children) {
175
+ const counts = this.countNodes(child)
176
+ files += counts.files
177
+ directories += counts.directories
178
+ }
179
+
180
+ return { files, directories }
181
+ }
182
+
183
+ /**
184
+ * Get the raw memfs instance for advanced operations
185
+ */
186
+ getRawFs(): ReturnType<typeof createFsFromVolume> {
187
+ return this.fs
188
+ }
189
+ }