braindrops.nicwands.com/bin/createDrop.js
2026-07-23 15:27:12 -04:00

234 lines
6.6 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import readline from 'readline'
import { spawn } from 'child_process'
const NO_COPY = [
'node_modules',
'dist',
'package-lock.json',
'.git',
'config.js',
'backend', // Don't copy backend by default, we'll handle it separately
]
const templateDir = path.join(process.cwd(), 'template')
const backendTemplateDir = path.join(process.cwd(), 'template/backend')
const dropsDir = path.join(process.cwd(), 'drops')
const kebabCase = (str) =>
str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase()
const pascalCase = (str) => {
const camel = str.replace(/[-_\s]+(.)?/g, (_, c) =>
c ? c.toUpperCase() : '',
)
return camel.charAt(0).toUpperCase() + camel.slice(1)
}
const titleCase = (str) => {
const words = str.split('-')
return words
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const copyDir = (src, dest) => {
fs.mkdirSync(dest, { recursive: true })
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (NO_COPY.includes(entry.name)) continue
if (entry.isDirectory()) {
copyDir(srcPath, destPath)
} else {
fs.copyFileSync(srcPath, destPath)
}
}
}
const updatePackageJson = (dir, name) => {
const pkgPath = path.join(dir, 'package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
pkg.name = name
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
}
const replaceFileString = (
dir,
filePath,
oldString,
newString,
newFilePath,
) => {
const fullPath = path.join(dir, filePath)
const fileContent = fs.readFileSync(fullPath, 'utf-8')
const updatedContent = fileContent.replace(oldString, newString)
if (newFilePath) {
fs.writeFileSync(path.join(dir, newFilePath), updatedContent)
} else {
fs.writeFileSync(fullPath, updatedContent)
}
}
const updateViteConfig = (dir, name) => {
replaceFileString(dir, 'vite.config.js', /\$NAME/g, name)
}
const updateVue = (dir, kebabName, pascalName) => {
replaceFileString(dir, 'src/App.vue', /Name/g, pascalName)
replaceFileString(
dir,
'src/components/Name.vue',
/\$name/g,
kebabName,
`src/components/${pascalName}.vue`,
)
fs.unlinkSync(path.join(dir, 'src/components/Name.vue'))
}
const updateIndexHtml = (dir, name) => {
replaceFileString(dir, 'index.html', /\$NAME/g, name)
}
const updateMetaJson = (dir, kebabName, titleName) => {
const metaPath = path.join(dir, 'public/meta.json')
if (fs.existsSync(metaPath)) {
let content = fs.readFileSync(metaPath, 'utf-8')
content = content.replace(/\$name/g, kebabName)
content = content.replace(/\$NAME/g, titleName)
// Update the date to today's date
const today = new Date().toISOString().split('T')[0]
content = content.replace(/"date":\s*"[^"]*"/, `"date": "${today}"`)
fs.writeFileSync(metaPath, content)
}
}
const setupBackend = (destDir, kebabName) => {
const backendDestDir = path.join(destDir, 'backend')
if (!fs.existsSync(backendTemplateDir)) {
console.warn(
'Warning: Backend template not found, skipping backend setup',
)
return
}
console.log('Setting up backend...')
// Copy backend template
copyDir(backendTemplateDir, backendDestDir)
// Replace DROPNAME in all backend files
const backendFiles = ['api.js', 'schema.sql', 'websocket.js', 'README.md']
for (const file of backendFiles) {
const filePath = path.join(backendDestDir, file)
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf-8')
// Replace all instances of DROPNAME with the actual drop name
content = content.replace(/DROPNAME/g, kebabName.replace(/-/g, '_'))
fs.writeFileSync(filePath, content)
}
}
console.log('Backend setup complete!')
}
const askQuestion = (query) => {
return new Promise((resolve) => {
rl.question(query, (answer) => {
resolve(answer.trim().toLowerCase())
})
})
}
const runCommand = (command, args, cwd = process.cwd()) => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
shell: true,
})
child.on('close', (code) => {
if (code === 0) resolve()
else reject(new Error(`Command failed with code ${code}`))
})
child.on('error', reject)
})
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const main = async () => {
const name = await askQuestion('Enter drop name: ')
const trimmedName = name.trim()
const kebabName = kebabCase(trimmedName)
const pascalName = pascalCase(trimmedName)
const titleName = titleCase(kebabName)
if (!kebabName) {
console.error('Error: drop name cannot be empty')
rl.close()
process.exit(1)
}
const destDir = path.join(dropsDir, kebabName)
if (fs.existsSync(destDir)) {
console.error(`Error: drop "${kebabName}" already exists`)
rl.close()
process.exit(1)
}
// Ask if user wants backend
const wantsBackend = await askQuestion(
'Do you want to add a backend? (y/N): ',
)
const includeBackend = wantsBackend === 'y' || wantsBackend === 'yes'
try {
console.log(`\nCreating drop "${kebabName}"...`)
copyDir(templateDir, destDir)
// Update template files
updatePackageJson(destDir, kebabName)
updateViteConfig(destDir, kebabName)
updateVue(destDir, kebabName, pascalName)
updateIndexHtml(destDir, titleName)
updateMetaJson(destDir, kebabName, titleName)
console.log(`Drop "${kebabName}" created successfully`)
// Setup backend if requested
if (includeBackend) {
setupBackend(destDir, kebabName)
}
console.log('\nRunning npm install...')
await runCommand('npm', ['install'])
console.log('npm install complete!')
console.log(`\nStarting dev environment for "${kebabName}"...\n`)
await runCommand('npm', ['run', 'dev:drops', kebabName])
} catch (err) {
console.error('Error:', err.message)
process.exit(1)
}
rl.close()
}
main()