@codingcat2202/walter_white_cli
Version:
This is a project, where i try to create an template, where you can create easy and (very) fast custom HTML elements
626 lines (539 loc) • 20.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { exec } = require('child_process');
const { log } = require('console');
// Utility-Funktion: create dir
function createFolder(folderPath) {
if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}
}
// Utility-Funktion: create data
function createFile(filePath, content = '') {
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, content, 'utf8');
console.log(`created "${filePath}" `);
}
}
// wanna use routing?
function askUser(question) {
const rl = readline.createInterface({
input: process.stdin, output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === 'y');
});
});
}
// project-setup
async function setupProject() {
const basePath = path.join(process.cwd(), projectName);
// create base structur
const folders = [
path.join(basePath, 'src'),
path.join(basePath, 'src', 'app'),
path.join(basePath, 'src', 'app', 'components'),
path.join(basePath, 'src', 'app', 'model'),
path.join(basePath, 'src', 'app', 'service'),
];
const files = {
[path.join(basePath, 'webpack.config.js')]: `const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = env => ({
entry: './src/index.ts',
mode: "development",
module: {
rules: [
{
test: /\\.ts$/, // Testet auf TypeScript-Dateien
use: [
{
loader: "ts-loader"
}
],
exclude: /node_modules/,
},
{test:/\\.css$/, use:'css-loader'}
]
},
devtool: "cheap-source-map",
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bundle-[fullhash].js',
path: path.resolve(__dirname, './dist'),
publicPath: '/', // Wichtig: Root-Path setzen
},
plugins: [
new HtmlWebpackPlugin({
template: "index.html"
})
],
devServer: {
static: {
directory: path.join(__dirname, '/'),
},
compress: true,
historyApiFallback: {
rewrites: [
{ from: /^\\/[^.]*$/, to: '/index.html' }, // Nur "clean URLs" an index.html umleiten
],
},
proxy: {
'/api': 'http://localhost:8080',
},
port: 4200,
},
})`,
[path.join(basePath, 'package.json')]: `
{
"name": "${projectName}",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate": "node generate-component.js",
"start": "webpack serve",
"test": "echo \\"Error: no test specified\\" && exit 1",
"build": "webpack build && gulp optimize"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@testdeck/mocha": "^0.3.3",
"@types/chai": "^4.3.4",
"chai": "^4.3.7",
"file-loader": "^6.2.0",
"gulp": "^5.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-cssnano": "^2.1.3",
"gulp-htmlmin": "^5.0.1",
"gulp-imagemin": "^9.1.0",
"gulp-terser": "^2.1.0",
"gulp-uglify": "^3.0.2",
"html-webpack-plugin": "^5.5.0",
"htmlmin": "^0.0.7",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"sass": "^1.81.0",
"sass-loader": "^16.0.3",
"ts-loader": "^9.4.1",
"ts-mockito": "^2.6.1",
"ts-node": "^10.9.1",
"typescript": "^4.8.4",
"web-cli": "^1.0.0-prealpha",
"webpack": "^5.96.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1"
},
"bin": {
"ww": "./bin/ww.js"
},
"dependencies": {
"css-loader": "^7.1.2",
"immer": "^9.0.16",
"lit": "^3.2.1",
"lit-html": "^2.4.0",
"rxjs": "^7.5.7",
"style-loader": "^4.0.0"
}
}
`,
[path.join(basePath, 'tsconfig.json')]: `{
"compilerOptions": {
"outDir": "dist",
"module": "esnext",
"target": "es2018",
"lib": ["es2017", "dom"],
"moduleResolution": "node",
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"importHelpers": true,
"sourceMap": true,
"inlineSources": true,
"rootDir": "./"
},
"exclude": [],
"include": ["**/*.ts"]
}`,
[path.join(basePath, 'register.js')]: `/**
* Overrides the tsconfig used for the app.
* In the test environment we need some tweaks.
*/
const tsNode = require('ts-node')
const testTSConfig = require('./test/tsconfig.json')
tsNode.register({
files: true,
transpileOnly: true,
project: './test/tsconfig.json'
})
`,
[path.join(basePath, 'README.md')]: '# Projekt README',
[path.join(basePath, 'index.html')]: `<!DOCTYPE html>
<html lang="en">
<head>
<script>
document.write(\`<base href="\${location.pathname}\${location.pathname.endsWith('/') ? '' : '/'}"/>\`);
</script>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Title</title>
</head>
<body>
<app-component></app-component>
</body>
</html>`,
[path.join(basePath, 'generate-component.js')]: `const fs = require('fs');
const path = require('path');
const componentName = process.argv[2];
if (!componentName) {
console.error('Usage: npm run generate <componentName>');
process.exit(1);
}
const componentFolderPath = path.join(__dirname, 'src', 'app', 'components', componentName);
try {
// Erstelle den Ordner für die Komponente
fs.mkdirSync(componentFolderPath + "-component");
// Erstelle die Dateien in der Komponente
const indexContent = \`import "./\${componentName}-component";\`;
fs.writeFileSync(path.join(componentFolderPath + "-component", 'index.ts'), indexContent);
const cssContent = \`/* Styles for \${componentName} component */\`;
fs.writeFileSync(path.join(componentFolderPath + "-component", 'style.css'), cssContent);
const componentContent = \`
import { html, render } from "lit-html";
import style from './style.css'
const template = html\\\`
<div class="\${componentName}">
</div>
\\\`;
class \${capitalizeFirstLetter(componentName)} extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
}
connectedCallback() {
this.render();
}
private render() {
const styleTag = document.createElement('style');
styleTag.textContent = style; // Hier wird der CSS-Inhalt eingefügt
this.shadowRoot.appendChild(styleTag);
render(template, this.shadowRoot);
}
}
customElements.define("\${componentName}-component", \${capitalizeFirstLetter(componentName)});
\`;
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
fs.writeFileSync(path.join(componentFolderPath + "-component", \`\${componentName}-component.ts\`), componentContent);
console.log(\`\${componentName} was generated successfully.\`);
} catch (error) {
console.error('Error generating the component:', error);
}
`,
[path.join(basePath, '.gitignore')]: `# Node modules
node_modules/
# Alles unter ./src/app/components/
/src/app/components/
# Betriebssystem-spezifische Dateien
.DS_Store
Thumbs.db
# Log-Dateien
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Umgebungskonfigurationsdateien
.env
.env.local
.env.*.local
# Abhängigkeiten für den Paketmanager
package-lock.json
yarn.lock
# Build-Ordner
dist/
build/
www/
# IDE-/Editor-spezifische Dateien
.vscode/
.idea/
*.iml
# Temporäre Dateien
*.tmp
*.swp
*.bak
*.log
*.out
# Sonstige Dateien
coverage/
`,
[path.join(basePath, 'src', 'index.ts')]: `
import "./app/app-component"
`,
[path.join(basePath, 'gulpfile.js')]: `const gulp = require('gulp');
async function optimizeImages() {
const imagemin = await import('gulp-imagemin');
return gulp.src('dist/*.{jpg,jpeg,png,webp}')
.pipe(imagemin.default())
.pipe(gulp.dest('www'));
}
async function optimizeJS() {
const terser = require('gulp-terser');
try {
return gulp.src('dist/*.js')
.pipe(terser())
.pipe(gulp.dest('www'));
} catch (error) {
console.error("Error optimizing JS:", error);
// Optional: Force a stream to end
return Promise.resolve();
}
}
async function optimizeCSS() {
const cleanCSS = await import('gulp-clean-css');
return gulp.src('dist/*.css')
.pipe(cleanCSS.default())
.pipe(gulp.dest('www')); // Zielordner bleibt www
}
async function optimizeHTML() {
const htmlmin = await import('gulp-htmlmin');
return gulp.src('dist/index.html') // Index-HTML-Datei aus dist
.pipe(htmlmin.default({ collapseWhitespace: true, removeComments: true })) // HTML optimieren
.pipe(gulp.dest('www')); // Zielordner bleibt www
}
// Optimierungs-Aufgabe
gulp.task('optimize', gulp.series(optimizeJS, optimizeCSS, optimizeHTML, optimizeImages)); // optimizeImages hinzugefügt
// Watch-Task
gulp.task('watch', () => {
gulp.watch('dist/*.js', optimizeJS);
gulp.watch('dist/*.css', optimizeCSS);
gulp.watch('dist/index.html', optimizeHTML); // HTML überwachen
gulp.watch('dist/*.{jpg,jpeg,png,webp}', optimizeImages); // Bilder überwachen
});
// Standard-Task
gulp.task('default', gulp.series('optimize', 'watch'));
`,
[path.join(basePath, 'robots.txt')]: `User-agent: *
Disallow:
Sitemap: https://deinehandwerker.net/sitemap.xml
`,
[path.join(basePath, 'sitemap.xml')]: `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://deinehandwerker.net/</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://deinehandwerker.net/gartenpflege</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://deinehandwerker.net/reinigung</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://deinehandwerker.net/hausmeisterdienst</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://deinehandwerker.net/kontakt</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>yearly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://deinehandwerker.net/impressum</loc>
<lastmod>2025-02-13</lastmod>
<changefreq>yearly</changefreq>
<priority>0.6</priority>
</url>
</urlset>
`
};
// create directory
folders.forEach(createFolder);
// create files
for (const [filePath, content] of Object.entries(files)) {
createFile(filePath, content);
}
// optional: add Routing-files
const useRouting = await askUser('Do you wannt to add routing? (y/n): ');
if (useRouting) {
createFile(path.join(basePath, 'src', 'app', 'app-component.ts'), `import {html, render} from 'lit-html';
import './router-outlet';
class AppComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
}
connectedCallback() {
const template = html\`<router-outlet></router-outlet>\`;
render(template, this.shadowRoot!);
}
}
customElements.define('app-component', AppComponent);
`);
createFile(path.join(basePath, 'src', 'app', 'router-outlet.ts'), `import {LitElement, html} from 'lit';
import {property} from 'lit/decorators.js';
interface Route {
path: string;
component: string;
}
class RouterOutlet extends LitElement {
currentRoute: string = window.location.pathname;
static routes: Route[] = [
{path: '/', component: 'home-component'},
];
constructor() {
super();
this.setupInitialRoute();
window.addEventListener('popstate', () => {
this.currentRoute = window.location.pathname;
this.requestUpdate();
});
}
private setupInitialRoute() {
const initialPath = window.location.pathname;
const route = RouterOutlet.routes.find(r => r.path === initialPath);
if (!route) {
// Wenn die initiale Route nicht "/" ist und nicht existiert, navigiere zu "/"
if (initialPath !== '/') {
this.navigate('/');
} else {
// Wenn es "/" ist und nicht explizit definiert, aber eine home-component erwartet wird.
this.currentRoute = initialPath; // Korrekte Initialroute setzen
}
} else {
this.currentRoute = initialPath; // Korrekte Initialroute setzen
}
}
navigate(path: string) {
if (this.currentRoute !== path) {
window.history.pushState({}, '', path);
this.currentRoute = path;
this.requestUpdate(); // Fordert ein Update an, updated() wird aufgerufen
}
window.scrollTo(0, 0);
}
private onNavigate(event: Event) {
event.preventDefault();
const target = event.target as HTMLElement;
const link = target.closest('a');
const path = link?.getAttribute('href');
if (path) {
this.navigate(path);
}
}
render() {
// Die Navigationslogik ist jetzt in onNavigate, hier wird nur das Menü gerendert.
// Das dynamische Laden des Inhalts geschieht in updated().
return html\`
<nav =\${this.onNavigate}>
<a href="/">Home</a>
</nav>
<div id="outlet"></div>
\`;
}
updated(changedProperties) { // changedProperties ist ein Map-Objekt
super.updated(changedProperties); // Wichtig für Lifecycle-Hooks von LitElement
// Prüfen, ob sich currentRoute geändert hat oder es das erste Rendering ist
if (changedProperties.has('currentRoute') || !this.shadowRoot?.querySelector('#outlet')?.hasChildNodes()) {
const outlet = this.shadowRoot?.querySelector('#outlet');
if (outlet) {
outlet.innerHTML = ''; // Alten Inhalt leeren
const route = RouterOutlet.routes.find(r => r.path === this.currentRoute);
const ComponentTag = route ? route.component : 'notfound-component'; // Fallback zu not-found
// Dynamischer Import und Erstellung der Komponente
if (!customElements.get(ComponentTag)) {
// Wichtig: Der Pfad zum Komponenten-Bundle muss korrekt sein.
// Annahme: Komponenten liegen in './components/' und werden als 'component-name.ts' oder ähnlich gebündelt.
// Der Import-Pfad muss ggf. angepasst werden, je nachdem wie Webpack die Komponenten bündelt.
// Normalerweise würde man hier direkt den Dateinamen importieren, z.B. './components/home-component.js'
// Da generate-component.js Ordner mit "-component" am Ende erstellt, muss dies berücksichtigt werden.
import(\`./components/\${ComponentTag}/\${ComponentTag}.js\`) // Pfad anpassen, falls nötig
.then(() => {
const element = document.createElement(ComponentTag);
outlet.appendChild(element);
})
.catch(err => {
console.error(\`Failed to load component \${ComponentTag} for route \${this.currentRoute}\`, err);
// Fallback, wenn Komponente nicht geladen werden kann
if (ComponentTag !== 'notfound-component' && customElements.get('notfound-component')) {
const notFoundElement = document.createElement('notfound-component');
outlet.appendChild(notFoundElement);
} else if (ComponentTag !== 'notfound-component') {
outlet.innerHTML = '<p>Error loading page content. Not found component is also missing.</p>';
}
});
} else {
const element = document.createElement(ComponentTag);
outlet.appendChild(element);
}
}
}
}
}
customElements.define('router-outlet', RouterOutlet);
`);
} else {
console.log("creating with no routing")
createFile(path.join(basePath, 'src', 'app', 'app-component.ts'), `import {html, render} from "lit-html"
// Stellen Sie sicher, dass home-component einen Standardexport hat oder hier korrekt importiert wird
// z.B. wenn home-component.ts dies exportiert: export class HomeComponent extends HTMLElement { ... }
// und in src/app/components/home-component/index.ts steht: import './home-component';
import "./components/home-component/index"; // Korrekter Import basierend auf der Struktur von generate-component.js
const template = html\`
<home-component></home-component>
\`
class AppComponent extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: "open"})
}
connectedCallback() {
console.log("app component connected")
this.render()
}
private render() {
render(template, this.shadowRoot)
}
}
customElements.define("app-component", AppComponent)`);
}
createFile(path.join(basePath, 'src', 'app', 'global.d.ts'), `declare module '*.css' {
const content: string;
export default content;
}
`);
console.log('Done creating project structure');
console.log('To get started:');
console.log(`cd ${projectName}`);
console.log('npm install');
console.log('npm start');
console.log('To generate a new component: npm run generate your-component-name');
}
//
const command = process.argv[2];
const projectName = process.argv[3];
if (command === 'new' && projectName) {
const basePath = process.cwd();
setupProject(basePath).catch((error) => console.error('Fehler:', error));
} else {
console.log('Usage: ww new "projectname"');
}