#!/usr/bin/env node // @ts-nocheck import fs from 'node:fs'; import path from 'node:path'; const DEFAULTS = { mode: 'dry-run', port: '5173', }; function usage() { console.error(`Usage: resources/scripts/scaffold-vue3-vite.sh [options] Required: --project-root Target project path. --app-name Application name used in package.json and index.html. Optional: --mode Default: dry-run --port Vite dev server port. Default: 5173 --force Overwrite existing files. --help Show this help text. `); } function parseArgs(argv) { const options = { mode: DEFAULTS.mode, port: DEFAULTS.port, force: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help') { usage(); process.exit(0); } if (arg === '--project-root') { options.projectRoot = argv[index + 1]; index += 1; continue; } if (arg === '--app-name') { options.appName = argv[index + 1]; index += 1; continue; } if (arg === '--mode') { options.mode = argv[index + 1]; index += 1; continue; } if (arg === '--port') { options.port = argv[index + 1]; index += 1; continue; } if (arg === '--force') { options.force = true; continue; } throw new Error(`Unknown argument: ${arg}`); } if (!options.projectRoot) throw new Error('--project-root is required.'); if (!options.appName) throw new Error('--app-name is required.'); if (!['dry-run', 'apply'].includes(options.mode)) { throw new Error('--mode must be dry-run or apply.'); } options.projectRoot = path.resolve(options.projectRoot); options.slug = slugify(options.appName); return options; } function slugify(value) { return String(value) .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } function writeFileWithGuard(filePath, content, { mode, force }) { const exists = fs.existsSync(filePath); if (exists && !force) return { action: 'skipped', filePath }; const action = exists ? 'updated' : 'created'; if (mode === 'apply') { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); } return { action, filePath }; } function touchDir(dirPath, { mode }) { const keepPath = path.join(dirPath, '.gitkeep'); const exists = fs.existsSync(keepPath); if (mode === 'apply' && !exists) { fs.mkdirSync(dirPath, { recursive: true }); fs.writeFileSync(keepPath, '', 'utf8'); } return { action: exists ? 'skipped' : 'created', filePath: keepPath }; } // --- Renderers --- function renderPackageJson({ slug, appName, port }) { return JSON.stringify( { name: slug, version: '0.1.0', private: true, type: 'module', scripts: { dev: `vite --port ${port}`, build: 'vite build', preview: 'vite preview', }, dependencies: { vue: '^3.4.0', 'vue-router': '^4.3.0', }, devDependencies: { '@vitejs/plugin-vue': '^5.0.0', sass: '^1.77.0', vite: '^5.3.0', }, }, null, 2, ) + '\n'; } function renderViteConfig({ port }) { return `import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { fileURLToPath, URL } from 'node:url'; export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, server: { port: ${port}, }, }); `; } function renderIndexHtml({ appName }) { return ` ${appName}
`; } function renderMainJs() { return `import { createApp } from 'vue'; import App from './App.vue'; import router from './router/index.js'; import '@/assets/styles/global-reset.scss'; const app = createApp(App); app.use(router); app.mount('#app'); `; } function renderAppVue() { return ` `; } function renderRouter() { return `import { createRouter, createWebHistory } from 'vue-router'; import HomePage from '@/pages/HomePage.vue'; const routes = [ { path: '/', name: 'home', component: HomePage, }, ]; export default createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes, }); `; } function renderHomePage({ appName }) { return ` `; } function renderGlobalTokens() { return `// Global design tokens. // Add project-specific custom properties and SCSS variables here. :root { --color-bg: #ffffff; --color-text: #1a1a1a; --color-primary: #5865f2; --font-base: system-ui, sans-serif; --space-1: 0.25rem; --space-2: 0.5rem; --space-4: 1rem; --space-8: 2rem; } `; } function renderGlobalReset() { return `@use 'global-tokens' as *; *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { background-color: var(--color-bg); color: var(--color-text); font-family: var(--font-base); line-height: 1.5; } `; } function renderGitIgnore() { return `node_modules dist .env .env.* *.local `; } // --- Main --- function main() { const options = parseArgs(process.argv.slice(2)); if (!fs.existsSync(options.projectRoot)) { throw new Error(`Project root does not exist: ${options.projectRoot}`); } const src = (rel) => path.join(options.projectRoot, rel); const writes = [ writeFileWithGuard(src('package.json'), renderPackageJson(options), options), writeFileWithGuard(src('vite.config.js'), renderViteConfig(options), options), writeFileWithGuard(src('index.html'), renderIndexHtml(options), options), writeFileWithGuard(src('.gitignore'), renderGitIgnore(), options), writeFileWithGuard(src('src/main.js'), renderMainJs(), options), writeFileWithGuard(src('src/App.vue'), renderAppVue(), options), writeFileWithGuard(src('src/router/index.js'), renderRouter(), options), writeFileWithGuard(src('src/pages/HomePage.vue'), renderHomePage(options), options), writeFileWithGuard(src('src/assets/styles/global-tokens.scss'), renderGlobalTokens(), options), writeFileWithGuard(src('src/assets/styles/global-reset.scss'), renderGlobalReset(), options), touchDir(src('src/components/atoms'), options), touchDir(src('src/components/molecules'), options), touchDir(src('src/components/organisms'), options), touchDir(src('src/components/layouts'), options), ]; console.log(`Mode: ${options.mode}`); console.log(`Project root: ${options.projectRoot}`); console.log(`App name: ${options.appName}`); console.log(`Dev port: ${options.port}`); console.log(''); for (const entry of writes) { console.log(`${entry.action.toUpperCase()}: ${entry.filePath}`); } if (options.mode === 'dry-run') { console.log(''); console.log('No files were written. Re-run with --mode apply to persist changes.'); } else { console.log(''); console.log('Next: cd into the project root and run npm install, then npm run dev.'); } } main();