#!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; function toSnakeCase(name: string): string { return name.replace(/[-]/g, '_'); } function toPascalCase(name: string): string { return name .split(/[-_]/) .map(s => s.charAt(0).toUpperCase() + s.slice(1)) .join(' '); } function humanLabel(name: string): string { return name .split(/[-_]/) .map(s => s.charAt(0).toUpperCase() + s.slice(1)) .join(' '); } interface FileTemplate { relativePath: string; content: string; } function buildTemplates(name: string, port: number): FileTemplate[] { const snake = toSnakeCase(name); const label = humanLabel(name); const routePath = `/${name}`; return [ { relativePath: 'manifest.json', content: JSON.stringify({ id: name, name: snake, label: { en: label, uk: label }, icon: 'Puzzle', routePath, remoteEntry: `http://localhost:${port}/remoteEntry.js`, exposedModule: './App', roles: ['admin'], }, null, 2) + '\n', }, { relativePath: 'k8s-plugin.yaml', content: `apiVersion: apps/v1 kind: Deployment metadata: name: ${name} namespace: crm spec: replicas: 1 selector: matchLabels: app: ${name} template: metadata: labels: app: ${name} spec: containers: - name: backend image: crm-plugins/${name}:latest ports: - containerPort: ${port} env: - name: CORE_BACKEND_URL value: "http://core-backend.crm.svc.cluster.local:3001" - name: PUBLIC_URL value: "https://crm.yourdomain.com/modules/${name}" --- apiVersion: v1 kind: Service metadata: name: ${name} namespace: crm spec: selector: app: ${name} ports: - port: 80 targetPort: ${port} `, }, { relativePath: 'backend/package.json', content: JSON.stringify({ name: `${name}-backend`, version: '0.0.1', description: `CRM Plugin Backend - ${label}`, private: true, scripts: { build: 'nest build', start: 'nest start', 'start:dev': 'nest start --watch', 'start:prod': 'node dist/main', }, dependencies: { '@nestjs/common': '^10.0.0', '@nestjs/core': '^10.0.0', '@nestjs/microservices': '^10.0.0', '@nestjs/platform-express': '^10.0.0', '@nestjs/serve-static': '^4.0.2', 'amqp-connection-manager': '^4.1.14', amqplib: '^0.10.3', 'erp-plugin-sdk': 'git+https://gitea.ss.3w.com.ua/erp/erp-plugin-sdk.git', 'reflect-metadata': '^0.1.13', rxjs: '^7.8.1', uuid: '^9.0.1', }, devDependencies: { '@nestjs/cli': '^10.0.0', '@nestjs/schematics': '^10.0.0', '@types/node': '^20.3.1', typescript: '^5.1.3', }, }, null, 2) + '\n', }, { relativePath: 'backend/tsconfig.json', content: JSON.stringify({ compilerOptions: { module: 'commonjs', declaration: true, removeComments: true, emitDecoratorMetadata: true, experimentalDecorators: true, allowSyntheticDefaultImports: true, target: 'ES2021', sourceMap: true, outDir: './dist', baseUrl: './', incremental: true, skipLibCheck: true, strictNullChecks: false, noImplicitAny: false, strictBindCallApply: false, forceConsistentCasingInFileNames: false, noFallthroughCasesInSwitch: false, }, }, null, 2) + '\n', }, { relativePath: 'backend/nest-cli.json', content: JSON.stringify({ collection: '@nestjs/schematics', sourceRoot: 'src', compilerOptions: { deleteOutDir: true, }, }, null, 2) + '\n', }, { relativePath: 'backend/Dockerfile', content: `FROM node:18-alpine WORKDIR /app COPY backend/package*.json ./backend/ COPY frontend/package*.json ./frontend/ RUN apk add --no-cache git RUN cd backend && npm install RUN cd frontend && npm install COPY . . RUN cd backend && npm run build RUN cd frontend && npm run build EXPOSE ${port} WORKDIR /app/backend CMD ["npm", "run", "start:prod"] `, }, { relativePath: 'backend/src/main.ts', content: `import { getRabbitMQOptions, CoreService, RequestContextInterceptor } from 'erp-plugin-sdk'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger } from '@nestjs/common'; import * as path from 'path'; async function bootstrap() { const logger = new Logger('PluginBootstrap'); const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new RequestContextInterceptor()); const express = require('express'); const distPath = path.join(process.cwd(), '..', 'frontend', 'dist'); app.use('/', express.static(distPath, { setHeaders: (res: any) => { res.set('Access-Control-Allow-Origin', '*'); } })); app.use((req: any, res: any, next: any) => { logger.log(\`Incoming request: \${req.method} \${req.url}\`); res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); app.connectMicroservice(getRabbitMQOptions('plugin_${snake}_queue')); app.enableCors(); await app.startAllMicroservices(); const port = process.env.PORT || ${port}; await app.listen(port); const coreService = app.get(CoreService); coreService.register(); logger.log(\`Plugin Service is running on: http://localhost:\${port}\`); } bootstrap(); `, }, { relativePath: 'backend/src/app.module.ts', content: `import { CoreService } from 'erp-plugin-sdk'; import { Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { AppController } from './app.controller'; import { PingController } from './ping.controller'; @Module({ imports: [ ClientsModule.register([ { name: 'RABBITMQ_CLIENT', transport: Transport.RMQ, options: { urls: [process.env.RABBITMQ_URL || 'amqp://localhost:5672'], queue: 'core_queue', queueOptions: { durable: true, }, }, }, ]), ], controllers: [AppController, PingController], providers: [CoreService], exports: [CoreService], }) export class AppModule {} `, }, { relativePath: 'backend/src/app.controller.ts', content: `import { Controller, Get } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; @Controller('debug') export class AppController { @Get('files') listFiles() { const distPath = path.join(process.cwd(), '..', 'frontend', 'dist'); try { const files = fs.readdirSync(distPath); return { path: distPath, exists: fs.existsSync(distPath), files }; } catch (e: any) { return { error: e.message, path: distPath }; } } } `, }, { relativePath: 'backend/src/ping.controller.ts', content: `import { Controller, Get } from '@nestjs/common'; @Controller('ping') export class PingController { @Get() ping() { return { status: 'ok', timestamp: Date.now() }; } } `, }, { relativePath: 'frontend/package.json', content: JSON.stringify({ name: `${name}-ui`, private: true, version: '0.1.0', type: 'module', scripts: { dev: 'vite', build: 'tsc && vite build', preview: 'vite preview', }, dependencies: { react: '^18.3.1', 'react-dom': '^18.3.1', 'react-router-dom': '^6.23.1', 'lucide-react': '^0.378.0', 'framer-motion': '^11.2.10', }, devDependencies: { '@originjs/vite-plugin-federation': '^1.3.5', '@types/react': '^18.3.3', '@types/react-dom': '^18.3.0', '@vitejs/plugin-react': '^4.3.1', typescript: '^5.2.2', vite: '^5.3.1', }, }, null, 2) + '\n', }, { relativePath: 'frontend/tsconfig.json', content: JSON.stringify({ compilerOptions: { target: 'ESNext', useDefineForClassFields: true, lib: ['DOM', 'DOM.Iterable', 'ESNext'], allowJs: false, skipLibCheck: true, esModuleInterop: false, allowSyntheticDefaultImports: true, strict: true, forceConsistentCasingInFileNames: true, module: 'ESNext', moduleResolution: 'Node', resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: 'react-jsx', }, include: ['src'], references: [{ path: './tsconfig.node.json' }], }, null, 2) + '\n', }, { relativePath: 'frontend/tsconfig.node.json', content: JSON.stringify({ compilerOptions: { composite: true, module: 'ESNext', moduleResolution: 'Node', allowSyntheticDefaultImports: true, }, include: ['vite.config.ts'], }, null, 2) + '\n', }, { relativePath: 'frontend/vite.config.ts', content: `import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import federation from '@originjs/vite-plugin-federation'; export default defineConfig({ plugins: [ react(), federation({ name: '${snake}', filename: 'remoteEntry.js', remotes: { host: 'http://localhost:3000/assets/remoteEntry.js' }, exposes: { './App': './src/App.tsx', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true }, 'react-router-dom': { singleton: true }, 'lucide-react': { singleton: true }, 'framer-motion': { singleton: true } } }) ], build: { modulePreload: false, target: 'esnext', minify: false, cssCodeSplit: false, assetsDir: '' }, server: { port: ${port}, cors: true } }); `, }, { relativePath: 'frontend/index.html', content: `