- resources/skills/vue3-vite/SKILL.md: procedure, outputs, and atomic design conventions - resources/scripts/scaffold-vue3-vite.mjs: self-contained scaffold generating package.json, vite.config.js, index.html, router, pages, atomic component dirs, and SCSS foundations - resources/scripts/scaffold-vue3-vite.sh: Node.js-delegating shell wrapper - resources/prompts/scaffold-vue3-vite.prompt.md: chat-invocable prompt following shared pattern
326 lines
7.4 KiB
JavaScript
326 lines
7.4 KiB
JavaScript
#!/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 <path> Target project path.
|
|
--app-name <name> Application name used in package.json and index.html.
|
|
|
|
Optional:
|
|
--mode <dry-run|apply> Default: dry-run
|
|
--port <number> 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 `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>${appName}</title>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module" src="/src/main.js"></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
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 `<template>
|
|
<RouterView />
|
|
</template>
|
|
|
|
<script setup>
|
|
import { RouterView } from 'vue-router';
|
|
</script>
|
|
`;
|
|
}
|
|
|
|
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 `<template>
|
|
<main>
|
|
<h1>{{ title }}</h1>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
const title = '${appName}';
|
|
</script>
|
|
`;
|
|
}
|
|
|
|
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();
|