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/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,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
|
+
}
|