Compare commits
No commits in common. "90d6298f1c58b7ce76082e2f56dab6ee11663c89" and "0a2319dbef3336ec7ae52680572b1a340e156037" have entirely different histories.
90d6298f1c
...
0a2319dbef
|
|
@ -8,20 +8,16 @@
|
||||||
"name": "erp-plugin-sdk",
|
"name": "erp-plugin-sdk",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
|
||||||
"create-erp-plugin": "dist/cli/index.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/common": "^11.1.23",
|
"@nestjs/common": "^11.1.23",
|
||||||
"@nestjs/core": "^11.1.23",
|
"@nestjs/core": "^11.1.23",
|
||||||
"@nestjs/microservices": "^11.1.23",
|
"@nestjs/microservices": "^11.1.23",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/uuid": "^9.0.8",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@borewit/text-codec": {
|
"node_modules/@borewit/text-codec": {
|
||||||
|
|
@ -240,13 +236,6 @@
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/uuid": {
|
|
||||||
"version": "9.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
|
||||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/consola": {
|
"node_modules/consola": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
|
|
@ -485,10 +474,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "9.0.1",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
|
||||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
|
||||||
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
"https://github.com/sponsors/broofa",
|
"https://github.com/sponsors/broofa",
|
||||||
|
|
@ -496,7 +484,7 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist-node/bin/uuid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"bin": {
|
|
||||||
"create-erp-plugin": "./dist/cli/index.js"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
|
|
@ -25,10 +22,9 @@
|
||||||
"@nestjs/microservices": "^11.1.23",
|
"@nestjs/microservices": "^11.1.23",
|
||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/uuid": "^9.0.8",
|
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^14.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,15 +212,4 @@ export class CoreService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendNotification(payload: { userId: number | null; title: string; message: string; icon?: string; image?: string; details?: any }): Promise<void> {
|
|
||||||
await this.emit('system.notification.send', {
|
|
||||||
userId: payload.userId,
|
|
||||||
title: payload.title,
|
|
||||||
message: payload.message,
|
|
||||||
icon: payload.icon,
|
|
||||||
image: payload.image,
|
|
||||||
details: payload.details,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
515
src/cli/index.ts
515
src/cli/index.ts
|
|
@ -1,515 +0,0 @@
|
||||||
#!/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: `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>${label}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/App.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
relativePath: 'frontend/src/App.tsx',
|
|
||||||
content: `import React from 'react';
|
|
||||||
import { Puzzle } from 'lucide-react';
|
|
||||||
// @ts-ignore
|
|
||||||
import { Card, Typography } from 'host/UIKit';
|
|
||||||
|
|
||||||
interface AppProps {
|
|
||||||
language?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const App: React.FC<AppProps> = ({ language = 'en' }) => {
|
|
||||||
return (
|
|
||||||
<Card className="plugin-card" title={language === 'uk' ? '${label}' : '${label}'}>
|
|
||||||
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
|
||||||
<Puzzle size={48} color="var(--accent)" style={{ marginBottom: '20px' }} />
|
|
||||||
<Typography.H2>${label}</Typography.H2>
|
|
||||||
<Typography.P>
|
|
||||||
{language === 'uk'
|
|
||||||
? 'Плагін успішно створено та підключено до CRM.'
|
|
||||||
: 'Plugin successfully created and connected to CRM.'}
|
|
||||||
</Typography.P>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
|
|
||||||
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
||||||
console.log(`Usage: create-erp-plugin <plugin-name> [--port <port>]
|
|
||||||
|
|
||||||
Create a new ERP plugin scaffold in the current directory.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
plugin-name Plugin name in kebab-case (e.g., "my-custom-plugin")
|
|
||||||
--port, -p Port number for the plugin server (default: 3002)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
create-erp-plugin my-plugin --port 3007
|
|
||||||
`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = args[0];
|
|
||||||
const portIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
|
||||||
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3002;
|
|
||||||
|
|
||||||
if (port < 1024 || port > 65535) {
|
|
||||||
console.error('Error: Port must be between 1024 and 65535');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
||||||
console.error('Error: Plugin name must be in kebab-case (e.g., "my-plugin")');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetDir = path.join(process.cwd(), name);
|
|
||||||
|
|
||||||
if (fs.existsSync(targetDir)) {
|
|
||||||
console.error(`Error: Directory "${name}" already exists`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const templates = buildTemplates(name, port);
|
|
||||||
|
|
||||||
for (const tpl of templates) {
|
|
||||||
const filePath = path.join(targetDir, tpl.relativePath);
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
fs.writeFileSync(filePath, tpl.content, 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Plugin "${name}" created successfully in ./${name}/`);
|
|
||||||
console.log();
|
|
||||||
console.log('Next steps:');
|
|
||||||
console.log(` cd ${name}/backend && npm install`);
|
|
||||||
console.log(` cd ${name}/frontend && npm install`);
|
|
||||||
console.log(` cd ${name}/backend && npm run start:dev`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
console.error('Error:', err.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue