You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

547 lines
14 KiB

"""
Vue.js/Quasar project generator for BadGUI.
"""
import os
import json
import shutil
from typing import List, Dict, Any, Optional
from .core import App, Component
class VueGenerator:
"""Generates Vue.js/Quasar project files from BadGUI components."""
def __init__(self, app: App):
self.app = app
def generate_project(self, output_dir: str, project_name: str = "badgui-app", **kwargs):
"""Generate the complete Vue.js/Quasar project."""
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Generate project structure
self._create_project_structure(output_dir, project_name)
self._generate_package_json(output_dir, project_name)
self._generate_quasar_config(output_dir)
self._generate_main_layout(output_dir)
self._generate_pages(output_dir)
self._generate_router(output_dir)
self._generate_app_vue(output_dir)
self._generate_boot_files(output_dir)
self._generate_css_files(output_dir)
self._copy_static_files(output_dir)
def _create_project_structure(self, output_dir: str, project_name: str):
"""Create the basic project directory structure."""
directories = [
'src',
'src/components',
'src/layouts',
'src/pages',
'src/router',
'src/boot',
'src/css',
'src/assets',
'public'
]
for directory in directories:
os.makedirs(os.path.join(output_dir, directory), exist_ok=True)
def _generate_package_json(self, output_dir: str, project_name: str):
"""Generate package.json file."""
package_json = {
"name": project_name,
"version": "0.0.1",
"description": "A Vue.js/Quasar app generated by BadGUI",
"productName": project_name.title(),
"author": "BadGUI Generator",
"private": True,
"scripts": {
"dev": "quasar dev",
"build": "quasar build",
"build:pwa": "quasar build -m pwa"
},
"dependencies": {
"@quasar/extras": "^1.16.4",
"quasar": "^2.12.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
"@types/node": "^20.4.5",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0",
"typescript": "^5.1.6"
},
"engines": {
"node": "^20 || ^18 || ^16 || ^14.19",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}
with open(os.path.join(output_dir, 'package.json'), 'w') as f:
json.dump(package_json, f, indent=2)
def _generate_quasar_config(self, output_dir: str):
"""Generate quasar.config.js file."""
config_content = '''const { configure } = require('quasar/wrappers');
module.exports = configure(function (ctx) {
return {
eslint: {
fix: false,
warnings: false,
errors: false,
},
boot: [
'badgui'
],
css: [
'app.scss'
],
extras: [
'roboto-font',
'material-icons',
],
build: {
target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16'
},
vueRouterMode: 'hash',
},
devServer: {
open: true
},
framework: {
config: {},
plugins: [
'Notify'
]
},
animations: [],
ssr: {
pwa: false,
prodPort: 3000,
},
pwa: {
workboxMode: 'injectManifest',
injectManifest: {
swSrc: 'src-pwa/sw.js'
},
manifestFilename: 'manifest.json',
useCredentialsForManifestTag: false,
},
cordova: {},
capacitor: {
hideSplashscreen: true
},
electron: {
inspectPort: 5858,
bundler: 'packager',
packager: {},
builder: {
appId: 'badgui-app'
}
},
bex: {
contentScripts: [
'my-content-script'
],
}
}
});
'''
with open(os.path.join(output_dir, 'quasar.config.js'), 'w') as f:
f.write(config_content)
def _generate_main_layout(self, output_dir: str):
"""Generate the main layout file."""
layout_content = '''<template>
<q-layout view="lHh Lpr lFf">
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup>
// Layout setup
</script>
'''
layout_path = os.path.join(output_dir, 'src', 'layouts', 'MainLayout.vue')
with open(layout_path, 'w') as f:
f.write(layout_content)
def _generate_pages(self, output_dir: str):
"""Generate all page components from BadGUI pages."""
# If no pages defined, create default from old components structure
if not self.app.pages:
# Backward compatibility: create default page from root components
if hasattr(self.app, 'components'):
self.app._ensure_default_page()
self.app.current_page.components = getattr(self.app, 'components', [])
# Generate each page
for path, page in self.app.pages.items():
self._generate_page_component(output_dir, page)
def _generate_page_component(self, output_dir: str, page):
"""Generate a single page component."""
# Generate the template from page components
template_content = self._generate_vue_template_for_page(page)
page_content = f'''<template>
<q-page class="row items-center justify-evenly">
{template_content}
</q-page>
</template>
<script setup>
import {{ ref }} from 'vue'
// Component state
const state = ref({{}})
// Event handlers
const handleClick = (event) => {{
console.log('Button clicked:', event)
}}
</script>
<style scoped>
.row {{
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
}}
.column {{
display: flex;
flex-direction: column;
gap: 16px;
}}
</style>
'''
page_path = os.path.join(output_dir, 'src', 'pages', f'{page.name}.vue')
with open(page_path, 'w') as f:
f.write(page_content)
def _generate_vue_template_for_page(self, page) -> str:
"""Generate Vue template from page components."""
if not page.components:
return " <div>No components defined</div>"
template = ""
for component in page.components:
template += component.to_vue_template(2) # 2 spaces indent for page content
return template
def _generate_router(self, output_dir: str):
"""Generate Vue Router configuration."""
router_content = '''import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
history: createHistory(process.env.VUE_ROUTER_BASE)
})
return Router
})
'''
# Generate routes from pages
page_routes = []
if not self.app.pages:
# Default route for backward compatibility
page_routes.append(" { path: '', component: () => import('pages/HomePage.vue') }")
else:
for path, page in self.app.pages.items():
if path == '/':
page_routes.append(f" {{ path: '', component: () => import('pages/{page.name}.vue') }}")
else:
page_routes.append(f" {{ path: '{path.lstrip('/')}', component: () => import('pages/{page.name}.vue') }}")
routes_content = f'''const routes = [
{{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{",".join([chr(10) + route for route in page_routes])}
]
}},
// Always leave this as last one,
// but you can also remove it
{{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}}
]
export default routes
'''
router_path = os.path.join(output_dir, 'src', 'router', 'index.js')
routes_path = os.path.join(output_dir, 'src', 'router', 'routes.js')
with open(router_path, 'w') as f:
f.write(router_content)
with open(routes_path, 'w') as f:
f.write(routes_content)
def _generate_app_vue(self, output_dir: str):
"""Generate the main App.vue file."""
app_content = '''<template>
<div id="q-app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
'''
app_path = os.path.join(output_dir, 'src', 'App.vue')
with open(app_path, 'w') as f:
f.write(app_content)
def _generate_boot_files(self, output_dir: str):
"""Generate boot files for Quasar."""
boot_content = '''import { boot } from 'quasar/wrappers'
// BadGUI boot file
export default boot(async ({ app, router, store }) => {
// Initialize BadGUI specific functionality
console.log('BadGUI initialized')
})
'''
boot_path = os.path.join(output_dir, 'src', 'boot', 'badgui.js')
with open(boot_path, 'w') as f:
f.write(boot_content)
def _generate_css_files(self, output_dir: str):
"""Generate CSS files."""
css_content = '''// app global css
@import './quasar.variables.scss';
body {
font-family: 'Roboto', sans-serif;
}
.row {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
}
.column {
display: flex;
flex-direction: column;
gap: 16px;
}
'''
variables_content = '''// Quasar SCSS Variables
$primary : #1976D2;
$secondary : #26A69A;
$accent : #9C27B0;
$dark : #1D1D1D;
$dark-page : #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;
'''
css_path = os.path.join(output_dir, 'src', 'css', 'app.scss')
variables_path = os.path.join(output_dir, 'src', 'css', 'quasar.variables.scss')
with open(css_path, 'w') as f:
f.write(css_content)
with open(variables_path, 'w') as f:
f.write(variables_content)
def _copy_static_files(self, output_dir: str):
"""Copy static files and create additional necessary files."""
# Create index.html in root directory
index_html = '''<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.theme && ctx.theme.capacitor || ctx.mode && ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>
'''
# Create main.js
main_js = '''import { createApp } from 'vue'
import { Quasar } from 'quasar'
// Import icon libraries
import '@quasar/extras/material-icons/material-icons.css'
// Import Quasar css
import 'quasar/src/css/index.sass'
// Assumes your root component is App.vue
// and placed in same folder as main.js
import App from './App.vue'
import router from './router'
const myApp = createApp(App)
myApp.use(Quasar, {
plugins: {}, // import Quasar plugins and add here
})
myApp.use(router)
// Assumes you have a <div id="app"></div> in your index.html
myApp.mount('#q-app')
'''
# Create index.html in root directory for Quasar
index_path = os.path.join(output_dir, 'index.html')
main_path = os.path.join(output_dir, 'src', 'main.js')
with open(index_path, 'w') as f:
f.write(index_html)
with open(main_path, 'w') as f:
f.write(main_js)
# Create error page
error_page = '''<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script>
export default {
name: 'ErrorNotFound'
}
</script>
'''
error_path = os.path.join(output_dir, 'src', 'pages', 'ErrorNotFound.vue')
with open(error_path, 'w') as f:
f.write(error_page)
# Create basic ESLint config to avoid errors
eslint_config = '''{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"@vue/eslint-config-typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"vue"
],
"rules": {},
"overrides": [
{
"files": ["*.vue"],
"parser": "vue-eslint-parser"
}
]
}
'''
eslint_path = os.path.join(output_dir, '.eslintrc.json')
with open(eslint_path, 'w') as f:
f.write(eslint_config)