create-bingstack-app 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/index.mjs +220 -0
- package/package.json +17 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as p from '@clack/prompts'
|
|
4
|
+
import { execa } from 'execa'
|
|
5
|
+
import fs from 'fs/promises'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import { existsSync } from 'fs'
|
|
8
|
+
|
|
9
|
+
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Your GitHub template repo in the format "username/repo"
|
|
11
|
+
const TEMPLATE_REPO = 'GITHUB_USERNAME/TEMPLATE_REPO'
|
|
12
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2)
|
|
15
|
+
const argProjectName = args.find((a) => !a.startsWith('--'))
|
|
16
|
+
const argTemplate = args.find((a) => a.startsWith('--template='))?.split('=')[1]
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
console.log()
|
|
20
|
+
p.intro('create-bingstack-app')
|
|
21
|
+
|
|
22
|
+
// ── Step 1: Project name ────────────────────────────────────────────────────
|
|
23
|
+
let projectName = argProjectName
|
|
24
|
+
|
|
25
|
+
if (!projectName) {
|
|
26
|
+
projectName = await p.text({
|
|
27
|
+
message: 'Project name',
|
|
28
|
+
placeholder: 'my-app',
|
|
29
|
+
validate: validateSlug,
|
|
30
|
+
})
|
|
31
|
+
if (p.isCancel(projectName)) return cancel()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Step 2: Protected route name ────────────────────────────────────────────
|
|
35
|
+
const protectedRoute = await p.text({
|
|
36
|
+
message: 'Main protected page name (shown after sign-in)',
|
|
37
|
+
placeholder: 'dashboard',
|
|
38
|
+
initialValue: 'dashboard',
|
|
39
|
+
validate: validateSlug,
|
|
40
|
+
})
|
|
41
|
+
if (p.isCancel(protectedRoute)) return cancel()
|
|
42
|
+
|
|
43
|
+
// ── Step 3: Package manager ─────────────────────────────────────────────────
|
|
44
|
+
const pkgManager = await p.select({
|
|
45
|
+
message: 'Package manager',
|
|
46
|
+
options: [
|
|
47
|
+
{ value: 'npm', label: 'npm' },
|
|
48
|
+
{ value: 'bun', label: 'bun' },
|
|
49
|
+
{ value: 'pnpm', label: 'pnpm' },
|
|
50
|
+
],
|
|
51
|
+
initialValue: 'npm',
|
|
52
|
+
})
|
|
53
|
+
if (p.isCancel(pkgManager)) return cancel()
|
|
54
|
+
|
|
55
|
+
// ── Confirm ─────────────────────────────────────────────────────────────────
|
|
56
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
57
|
+
|
|
58
|
+
if (existsSync(targetDir)) {
|
|
59
|
+
const overwrite = await p.confirm({
|
|
60
|
+
message: `"${projectName}" already exists. Overwrite?`,
|
|
61
|
+
initialValue: false,
|
|
62
|
+
})
|
|
63
|
+
if (p.isCancel(overwrite) || !overwrite) return cancel()
|
|
64
|
+
const s = p.spinner()
|
|
65
|
+
s.start('Removing existing directory...')
|
|
66
|
+
await fs.rm(targetDir, { recursive: true })
|
|
67
|
+
s.stop('Removed')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Clone template ───────────────────────────────────────────────────────────
|
|
71
|
+
{
|
|
72
|
+
const s = p.spinner()
|
|
73
|
+
s.start('Cloning template...')
|
|
74
|
+
try {
|
|
75
|
+
const templateSource = argTemplate ?? TEMPLATE_REPO
|
|
76
|
+
const degit = (await import('degit')).default
|
|
77
|
+
const emitter = degit(templateSource, { cache: false, force: true })
|
|
78
|
+
await emitter.clone(targetDir)
|
|
79
|
+
s.stop('Template cloned')
|
|
80
|
+
} catch (err) {
|
|
81
|
+
s.stop('Failed to clone template')
|
|
82
|
+
p.log.error(String(err.message ?? err))
|
|
83
|
+
if (!argTemplate) {
|
|
84
|
+
p.log.info(
|
|
85
|
+
`Make sure you've set TEMPLATE_REPO in index.mjs to your GitHub "username/repo".\n` +
|
|
86
|
+
`Or pass a local path: npx create-bingstack-app --template=../my-template`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Apply replacements ───────────────────────────────────────────────────────
|
|
94
|
+
{
|
|
95
|
+
const s = p.spinner()
|
|
96
|
+
s.start('Configuring project...')
|
|
97
|
+
await applyReplacements(targetDir, projectName, protectedRoute)
|
|
98
|
+
s.stop('Project configured')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Install dependencies ─────────────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
const s = p.spinner()
|
|
104
|
+
s.start(`Installing dependencies with ${pkgManager}...`)
|
|
105
|
+
await execa(pkgManager, ['install'], { cwd: targetDir })
|
|
106
|
+
s.stop('Dependencies installed')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
110
|
+
p.outro(
|
|
111
|
+
`Done! Next steps:\n\n` +
|
|
112
|
+
` cd ${projectName}\n` +
|
|
113
|
+
` cp .env.example .env # fill in your secrets\n` +
|
|
114
|
+
` ${pkgManager} run dev`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Replacements ────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
async function applyReplacements(dir, appName, protectedRoute) {
|
|
121
|
+
const oldRoute = 'workspace'
|
|
122
|
+
|
|
123
|
+
// 1. Rename the protected route file if needed
|
|
124
|
+
if (protectedRoute !== oldRoute) {
|
|
125
|
+
const oldFile = path.join(dir, `src/routes/_protected/${oldRoute}.tsx`)
|
|
126
|
+
const newFile = path.join(dir, `src/routes/_protected/${protectedRoute}.tsx`)
|
|
127
|
+
if (existsSync(oldFile)) {
|
|
128
|
+
await fs.rename(oldFile, newFile)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Walk all text files and do string replacements
|
|
133
|
+
const files = await walkFiles(dir, ['node_modules', '.git', 'dist', '.cache', '.turbo'])
|
|
134
|
+
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
// Only process text files
|
|
137
|
+
if (!isTextFile(file)) continue
|
|
138
|
+
|
|
139
|
+
let content = await fs.readFile(file, 'utf-8').catch(() => null)
|
|
140
|
+
if (content === null) continue
|
|
141
|
+
|
|
142
|
+
const original = content
|
|
143
|
+
|
|
144
|
+
// Replace package name
|
|
145
|
+
if (file.endsWith('package.json')) {
|
|
146
|
+
content = content.replace(/"name":\s*"app"/, `"name": "${appName}"`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Replace page title
|
|
150
|
+
content = content.replaceAll('TanStack Start Starter', toTitleCase(appName))
|
|
151
|
+
|
|
152
|
+
// Replace protected route references (case-sensitive variants)
|
|
153
|
+
if (protectedRoute !== oldRoute) {
|
|
154
|
+
content = content
|
|
155
|
+
.replaceAll(`/_protected/${oldRoute}`, `/_protected/${protectedRoute}`)
|
|
156
|
+
.replaceAll(`/${oldRoute}`, `/${protectedRoute}`)
|
|
157
|
+
.replaceAll(`"${oldRoute}"`, `"${protectedRoute}"`)
|
|
158
|
+
.replaceAll(`'${oldRoute}'`, `'${protectedRoute}'`)
|
|
159
|
+
.replaceAll(toTitleCase(oldRoute), toTitleCase(protectedRoute))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (content !== original) {
|
|
163
|
+
await fs.writeFile(file, content, 'utf-8')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async function walkFiles(dir, ignore = []) {
|
|
171
|
+
const results = []
|
|
172
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (ignore.includes(entry.name)) continue
|
|
175
|
+
const fullPath = path.join(dir, entry.name)
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
results.push(...(await walkFiles(fullPath, ignore)))
|
|
178
|
+
} else {
|
|
179
|
+
results.push(fullPath)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return results
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const TEXT_EXTENSIONS = new Set([
|
|
186
|
+
'.ts', '.tsx', '.js', '.mjs', '.cjs', '.jsx',
|
|
187
|
+
'.json', '.md', '.txt', '.html', '.css', '.env',
|
|
188
|
+
'.example', '.gitignore', '.prettierignore', '.eslintignore',
|
|
189
|
+
'.sql', '.toml', '.yaml', '.yml',
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
function isTextFile(filePath) {
|
|
193
|
+
const ext = path.extname(filePath)
|
|
194
|
+
const base = path.basename(filePath)
|
|
195
|
+
// Handle extension-less dotfiles like .env
|
|
196
|
+
return TEXT_EXTENSIONS.has(ext) || TEXT_EXTENSIONS.has(base)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toTitleCase(slug) {
|
|
200
|
+
return slug
|
|
201
|
+
.split('-')
|
|
202
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
203
|
+
.join(' ')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function validateSlug(value) {
|
|
207
|
+
if (!value || value.trim() === '') return 'This field is required'
|
|
208
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value))
|
|
209
|
+
return 'Use lowercase letters, numbers, and hyphens only (e.g. my-app)'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cancel() {
|
|
213
|
+
p.cancel('Cancelled')
|
|
214
|
+
process.exit(0)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch((err) => {
|
|
218
|
+
console.error(err)
|
|
219
|
+
process.exit(1)
|
|
220
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-bingstack-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Bingstack app with auth, routing, and database pre-configured",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-bingstack-app": "./index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@clack/prompts": "^0.9.1",
|
|
11
|
+
"degit": "^2.8.4",
|
|
12
|
+
"execa": "^9.5.2"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
17
|
+
}
|