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.
Files changed (2) hide show
  1. package/index.mjs +220 -0
  2. 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
+ }