release: v4

This commit is contained in:
gaoshuaixing
2025-01-13 18:03:16 +08:00
parent ab88523ec8
commit fab91a9247
162 changed files with 9166 additions and 0 deletions

2
ee-bin-ts/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
package-lock.json

49
ee-bin-ts/package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "ee-bin-ts",
"version": "0.1.0-beta.4",
"description": "ee bin for ts",
"main": "./dist/index.js",
"type": "commonjs",
"exports": {
".": {
"require": {
"default": "./dist/index.js"
},
"import": {
"default": "./ems/index.mjs"
},
"default": "./dist/index.js"
}
},
"scripts": {
"check": "npm run check:type",
"check:type": "npm run check:type:js && npm run check:type:ts",
"check:type:ts": "tsd && tsc -p tsconfig.ts.json",
"check:type:js": "tsc -p tsconfig.js.json"
},
"author": "",
"license": "ISC",
"bin": {
"ee-bin-m": "./ems/index.mjs",
"ee-bin-c": "./dist/index.js"
},
"dependencies": {
"@types/node": "^20.9.0",
"adm-zip": "^0.4.11",
"bytenode": "^1.3.6",
"chalk": "^4.1.2",
"commander": "^11.0.0",
"config-file-ts": "^0.2.8-rc1",
"cross-spawn": "^7.0.3",
"fs-extra": "^10.0.0",
"globby": "^10.0.0",
"is-type-of": "^1.2.1",
"javascript-obfuscator": "^4.0.2",
"json5": "^2.2.3",
"mkdirp": "^2.1.3"
},
"devDependencies": {
"tsd": "^0.31.2",
"typescript": "^5.4.2"
}
}

1
ee-bin-ts/readme.md Normal file
View File

@@ -0,0 +1 @@
## ee-bin

118
ee-bin-ts/src/index.ts Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env node
import { program } from 'commander';
import moveScript from './tools/move';
import encrypt from './tools/encrypt';
import serve from './tools/serve';
import updater from './tools/incrUpdater';
import iconGen from './tools/iconGen';
/**
* move - Moves resources
*/
program
.command('move')
.description('Move multip resources')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.option('--flag <flag>', 'Custom flag')
.action(function () {
moveScript.run(this.opts());
});
/**
* encrypt - Code encryption
*/
program
.command('encrypt')
.description('Code encryption')
.option('--config <folder>', 'config file')
.option('--out <folder>', 'output directory', './public')
.action(function () {
encrypt.run(this.opts());
});
/**
* clean - Clear the encrypted code
*/
program
.command('clean')
.description('Clear the encrypted code')
.option('-d, --dir <folder>', 'clean directory')
.action(function () {
encrypt.clean(this.opts());
});
/**
* icon
*/
program
.command('icon')
.description('Generate logo')
.option('-i, --input <file>', 'image file default /public/images/logo.png')
.option('-o, --output <folder>', 'output directory default /build/icons/')
.action(function () {
iconGen.run(this.opts());
});
/**
* dev
*/
program
.command('dev')
.description('create frontend-serve and electron-serve')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.option('--serve <mode>', 'serve mode')
.action(function () {
serve.dev(this.opts());
});
/**
* build
*/
program
.command('build')
.description('building multiple resources')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.option('--cmds <flag>', 'custom commands')
.action(function () {
serve.build(this.opts());
});
/**
* start
*/
program
.command('start')
.description('preview effect')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.action(function () {
serve.start(this.opts());
});
/**
* exec
*/
program
.command('exec')
.description('create frontend-serve and electron-serve')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.option('--command <command>', 'Custom command')
.option('--cmds <flag>', 'custom commands')
.action(function () {
// command 选项是关键字,不再使用,改为 cmds
serve.exec(this.opts());
});
/**
* updater
*/
program
.command('updater')
.description('updater commands')
.option('--config <folder>', 'config file', './electron/config/bin.js')
.option('--asar-file <file>', 'asar file path')
.option('--platform <flag>', 'platform')
.action(function () {
updater.run(this.opts());
});
program.parse();

208
ee-bin-ts/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,208 @@
import chalk from 'chalk';
import is from 'is-type-of';
import { loadTsConfig } from 'config-file-ts';
import JsonLib from 'json5';
import mkdirp from 'mkdirp';
import * as fs from 'fs';
import * as path from 'path';
import OS from 'os';
const _basePath = process.cwd();
function checkConfig(prop: string): boolean {
const filepath = path.join(_basePath, prop);
return fs.existsSync(filepath);
}
function loadConfig(prop: string): any {
const configFile = path.join(_basePath, prop);
if (!fs.existsSync(configFile)) {
const errorTips = `config file ${chalk.blue(`${configFile}`)} does not exist !`;
throw new Error(errorTips);
}
let result: any;
if (configFile.endsWith(".json5") || configFile.endsWith(".json")) {
const data = fs.readFileSync(configFile, 'utf8');
return JsonLib.parse(data);
}
if (configFile.endsWith(".js") || configFile.endsWith(".cjs")) {
result = require(configFile);
if (result.default != null) {
result = result.default;
}
} else if (configFile.endsWith(".ts")) {
result = loadTsConfig(configFile);
}
if (is.function(result) && !is.class(result)) {
result = result();
}
return result || {};
}
function loadEncryptConfig(): any {
const configFile = './electron/config/encrypt.js';
const filepath = path.join(_basePath, configFile);
if (!fs.existsSync(filepath)) {
const errorTips = `config file ${chalk.blue(`${filepath}`)} does not exist !`;
throw new Error(errorTips);
}
const obj = require(filepath);
if (!obj) return obj;
let ret = obj;
if (is.function(obj) && !is.class(obj)) {
ret = obj();
}
return ret || {};
}
function getElectronProgram(): string {
let electronPath: any;
const electronModulePath = path.dirname(require.resolve('electron'));
const pathFile = path.join(electronModulePath, 'path.txt');
const executablePath = fs.readFileSync(pathFile, 'utf-8');
if (executablePath) {
electronPath = path.join(electronModulePath, 'dist', executablePath);
} else {
throw new Error('Check that electron is installed!');
}
return electronPath;
}
function compareVersion(v1: string, v2: string): number {
const v1Arr: string[] = v1.split('.');
const v2Arr: string[] = v2.split('.');
const len = Math.max(v1Arr.length, v2Arr.length);
while (v1Arr.length < len) {
v1Arr.push('0');
}
while (v2Arr.length < len) {
v2Arr.push('0');
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1Arr[i]);
const num2 = parseInt(v2Arr[i]);
if (num1 > num2) {
return 1;
} else if (num1 < num2) {
return -1;
}
}
return 0;
}
function isWindows(): boolean {
return process.platform === 'win32';
}
function isOSX(): boolean {
return process.platform === 'darwin';
}
function isMacOS(): boolean {
return isOSX();
}
function isLinux(): boolean {
return process.platform === 'linux';
}
function isx86(): boolean {
return process.arch === 'ia32';
}
function isx64(): boolean {
return process.arch === 'x64';
}
function rm(name: string): void {
if (!fs.existsSync(name)) {
return;
}
const nodeVersion = (process.versions && process.versions.node) || null;
if (nodeVersion && compareVersion(nodeVersion, '14.14.0') === 1) {
fs.rmSync(name, { recursive: true });
} else {
fs.rmdirSync(name, { recursive: true });
}
}
function getPackage(): any {
const homeDir = process.cwd();
const content = readJsonSync(path.join(homeDir, 'package.json'));
return content;
}
function readJsonSync(filepath: string): any {
if (!fs.existsSync(filepath)) {
throw new Error(`${filepath} is not found`);
}
return JSON.parse(fs.readFileSync(filepath, 'utf8'));
}
function writeJsonSync(filepath: string, str: any, options?: { replacer?: any; space?: number }) {
options = options || {};
if (!('space' in options)) {
options.space = 2;
}
mkdirp.sync(path.dirname(filepath));
if (typeof str === 'object') {
str = JSON.stringify(str, options.replacer, options.space) + '\n';
}
fs.writeFileSync(filepath, str);
}
function getPlatform(delimiter: string = "_", isDiffArch: boolean = false): string {
let os = "";
if (isWindows()) {
os = "windows";
if (isDiffArch) {
const arch = isx64() ? "64" : "32";
os += delimiter + arch;
}
} else if (isMacOS()) {
let isAppleSilicon = false;
const cpus = OS.cpus();
for (let cpu of cpus) {
if (cpu.model.includes('Apple')) {
isAppleSilicon = true;
break;
}
}
const core = isAppleSilicon ? "apple" : "intel";
os = "macos" + delimiter + core;
} else if (isLinux()) {
os = "linux";
}
return os;
}
export {
loadConfig,
checkConfig,
loadEncryptConfig,
getElectronProgram,
compareVersion,
isWindows,
isOSX,
isMacOS,
isLinux,
isx86,
isx64,
getPlatform,
rm,
getPackage,
readJsonSync,
writeJsonSync
};

View File

@@ -0,0 +1,269 @@
import path from 'path';
import fs from 'fs';
import fsPro from 'fs-extra';
import bytenode from 'bytenode';
import crypto from 'crypto';
import JavaScriptObfuscator from 'javascript-obfuscator';
import globby from 'globby';
import chalk from 'chalk';
import Utils from '../lib/utils';
type Config = {
fileExt?: string[];
type?: string;
bytecodeOptions?: Record<string, any>;
confusionOptions?: Record<string, any>;
cleanFiles?: string[];
files?: string[];
directory?: string[];
};
class Encrypt {
basePath: string;
encryptCodeDir: string;
config: Config;
filesExt: string[];
type: string;
bOpt: Record<string, any>;
cOpt: Record<string, any>;
cleanFiles: string[];
patterns: string[] | null;
specificFiles: string[];
dirs: string[];
codefiles: string[];
constructor(options: { out?: string; config?: string } = {}) {
const outputFolder = options.out || './public';
const configFile = options.config || './electron/config/bin.js';
this.basePath = process.cwd();
this.encryptCodeDir = path.join(this.basePath, outputFolder);
const hasConfig = Utils.checkConfig(configFile);
if (hasConfig) {
const cfg = Utils.loadConfig(configFile);
this.config = cfg.encrypt;
}
if (!this.config) {
this.config = Utils.loadEncryptConfig();
}
this.filesExt = this.config.fileExt || ['.js'];
this.type = this.config.type || 'confusion';
this.bOpt = this.config.bytecodeOptions || {};
this.cOpt = this.config.confusionOptions || {};
this.cleanFiles = this.config.cleanFiles || ['./public/electron'];
this.patterns = this.config.files || null;
this.specificFiles = ['electron/preload/bridge.js'];
this.dirs = [];
const directory = this.config.directory || ['electron'];
for (let i = 0; i < directory.length; i++) {
let codeDirPath = path.join(this.basePath, directory[i]);
if (fs.existsSync(codeDirPath)) {
this.dirs.push(directory[i]);
}
}
this.codefiles = this._initCodeFiles();
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'cleanFiles:' + this.cleanFiles);
}
_initCodeFiles(): string[] {
if (!this.patterns) return [];
const files = globby.sync(this.patterns, { cwd: this.basePath });
return files;
}
backup(): boolean {
this.cleanCode();
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'backup start');
if (this.patterns) {
this.codefiles.forEach((filepath) => {
let source = path.join(this.basePath, filepath);
if (fs.existsSync(source)) {
let target = path.join(this.encryptCodeDir, filepath);
fsPro.copySync(source, target);
}
});
} else {
for (let i = 0; i < this.dirs.length; i++) {
let codeDirPath = path.join(this.basePath, this.dirs[i]);
if (!fs.existsSync(codeDirPath)) {
console.log('[ee-bin] [encrypt] ERROR: backup %s is not exist', codeDirPath);
return false;
}
let targetDir = path.join(this.encryptCodeDir, this.dirs[i]);
console.log('[ee-bin] [encrypt] backup target Dir:', targetDir);
if (!fs.existsSync(targetDir)) {
this.mkdir(targetDir);
this.chmodPath(targetDir, '777');
}
fsPro.copySync(codeDirPath, targetDir);
}
}
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'backup end');
return true;
}
cleanCode(): void {
this.cleanFiles.forEach((file) => {
let tmpFile = path.join(this.basePath, file);
this.rmBackup(tmpFile);
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'clean up tmp files:' + chalk.magenta(`${tmpFile}`));
});
}
encrypt(): void {
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'start ciphering');
if (this.patterns) {
for (const file of this.codefiles) {
const fullpath = path.join(this.encryptCodeDir, file);
if (!fs.statSync(fullpath).isFile()) continue;
if (this.specificFiles.includes(file)) {
this.generate(fullpath, 'confusion');
continue;
}
this.generate(fullpath);
}
} else {
console.log('[ee-bin] [encrypt] !!!!!! please use the new encryption method !!!!!!');
for (let i = 0; i < this.dirs.length; i++) {
let codeDirPath = path.join(this.encryptCodeDir, this.dirs[i]);
this.loop(codeDirPath);
}
console.log('[ee-bin] [encrypt] !!!!!! please use the new encryption method !!!!!!');
}
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'end ciphering');
}
loop(dirPath: string): void {
let files = [];
if (fs.existsSync(dirPath)) {
files = fs.readdirSync(dirPath);
files.forEach((file, index) => {
let curPath = dirPath + '/' + file;
if (fs.statSync(curPath).isDirectory()) {
this.loop(curPath);
} else {
const extname = path.extname(curPath);
if (this.filesExt.indexOf(extname) !== -1) {
this.generate(curPath);
}
}
});
}
}
generate(curPath: string, type?: string): void {
let encryptType = type ? type : this.type;
let tips = chalk.blue('[ee-bin] [encrypt] ') + 'file: ' + chalk.green(`${curPath}`) + ' ' + chalk.cyan(`(${encryptType})`);
console.log(tips);
if (encryptType == 'bytecode') {
this.generateBytecodeFile(curPath);
} else if (encryptType == 'confusion') {
this.generateJSConfuseFile(curPath);
} else {
this.generateJSConfuseFile(curPath);
this.generateBytecodeFile(curPath);
}
}
generateJSConfuseFile(file: string): void {
let opt = Object.assign({
compact: true,
stringArray: true,
stringArrayThreshold: 1,
}, this.cOpt);
let code = fs.readFileSync(file, "utf8");
let result = JavaScriptObfuscator.obfuscate(code, opt);
fs.writeFileSync(file, result.getObfuscatedCode(), "utf8");
}
generateBytecodeFile(curPath: string): void {
if (path.extname(curPath) !== '.js') {
return
}
let jscFile = curPath + 'c';
let opt = Object.assign({
filename: curPath,
output: jscFile,
electron: true
}, this.bOpt);
bytenode.compileFile(opt);
fsPro.removeSync(curPath);
}
rmBackup(file: string): void {
if (fs.existsSync(file)) {
fsPro.removeSync(file);
}
}
fileExist(filePath: string): boolean {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
};
mkdir(dirpath: string, dirname?: string): void {
if (typeof dirname === 'undefined') {
if (fs.existsSync(dirpath)) {
return;
}
this.mkdir(dirpath, path.dirname(dirpath));
} else {
if (dirname !== path.dirname(dirpath)) {
this.mkdir(dirpath);
return;
}
if (fs.existsSync(dirname)) {
fs.mkdirSync(dirpath);
} else {
this.mkdir(dirname, path.dirname(dirname));
fs.mkdirSync(dirpath);
}
}
};
chmodPath(path: string, mode: string): void {
let files = [];
if (fs.existsSync(path)) {
files = fs.readdirSync(path);
files.forEach((file, index) => {
const curPath = path + '/' + file;
if (fs.statSync(curPath).isDirectory()) {
this.chmodPath(curPath, mode); // 递归删除文件夹
} else {
fs.chmodSync(curPath, mode);
}
});
fs.chmodSync(path, mode);
}
};
md5(file: string): string {
const buffer = fs.readFileSync(file);
const hash = crypto.createHash('md5');
hash.update(buffer, 'utf8');
const str = hash.digest('hex');
return str;
}
}
const run = (options: { out?: string; config?:

View File

@@ -0,0 +1,177 @@
import fs from 'fs';
import path from 'path';
interface IconOptions {
report: boolean;
ico: {
name: string;
sizes: number[];
};
favicon: {
name: string;
pngSizes: number[];
};
}
interface Params {
input: string;
output: string;
size: string;
clear: boolean;
imagesDir: string;
}
class IconGen {
private params: Params;
private input: string;
private output: string;
private imagesDir: string;
private iconOptions: IconOptions;
constructor() {
this._init();
}
_init(): void {
const args = process.argv.slice(3);
this.params = {
input: "/public/images/logo.png",
output: "/build/icons/",
size: "16,32,64,256,512",
clear: false,
imagesDir: "/public/images/",
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.match(/^-i/) || arg.match(/^-input/)) {
this.params.input = args[i + 1];
i++;
continue;
}
if (arg.match(/^-o/) || arg.match(/^-output/)) {
this.params.output = args[i + 1];
i++;
continue;
}
if (arg.match(/^-s/) || arg.match(/^-size/)) {
this.params.size = args[i + 1];
i++;
continue;
}
if (arg.match(/^-c/) || arg.match(/^-clear/)) {
this.params.clear = true;
continue;
}
if (arg.match(/^-img/) || arg.match(/^-images/)) {
this.params.imagesDir = args[i + 1];
i++;
continue;
}
}
this.input = path.join(process.cwd(), this.params.input);
this.output = path.join(process.cwd(), this.params.output);
this.imagesDir = path.join(process.cwd(), this.params.imagesDir);
const sizeList = this.params.size.split(",").map((item) => parseInt(item));
this.iconOptions = {
report: false,
ico: {
name: "icon",
sizes: [256],
},
favicon: {
name: "logo-",
pngSizes: sizeList,
},
};
}
generateIcons(): void {
console.log("[ee-bin] [icon-gen] iconGen 开始处理生成logo图片");
if (!fs.existsSync(this.input)) {
console.error("[ee-bin] [icon-gen] input: ", this.input);
throw new Error("输入的图片不存在或路径错误");
}
if (!fs.existsSync(this.output)) {
fs.mkdirSync(this.output, { recursive: true });
} else {
if (this.params.clear) this.deleteGenFile(this.output);
}
if (!fs.existsSync(this.imagesDir)) {
fs.mkdirSync(this.imagesDir, { recursive: true });
}
const icongen = require("icon-gen");
icongen(this.input, this.output, this.iconOptions)
.then((results) => {
console.log("[ee-bin] [icon-gen] iconGen 已生成下方图片资源");
console.log(results);
this._renameForEE(results);
})
.catch((err) => {
console.error(err);
throw new Error("[ee-bin] [icon-gen] iconGen 生成失败!");
});
}
deleteGenFile(dirPath: string): void {
if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const curPath = path.join(dirPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
this.deleteGenFile(curPath);
} else {
if ([".ico", ".png"].includes(path.extname(curPath))) {
fs.unlinkSync(curPath);
}
}
});
}
}
_renameForEE(filesPath: string[]): void {
console.log("[ee-bin] [icon-gen] iconGen 开始重新命名logo图片资源");
try {
for (let i = 0; i < filesPath.length; i++) {
const filePath = filesPath[i];
const extname = path.extname(filePath);
if ([".png"].includes(extname)) {
const filename = path.basename(filePath, extname);
const basename = filename.split("-")[1];
const dirname = path.dirname(filePath);
if ("16" === basename) {
const newName = "tray" + extname;
fs.copyFileSync(filePath, path.join(this.imagesDir, newName));
console.log(`${filename}${extname} --> ${this.params.imagesDir}/${newName} 复制成功!`);
fs.unlinkSync(filePath);
continue;
}
if ("32" === basename) {
const newName = filename + extname;
fs.copyFileSync(filePath, path.join(this.imagesDir, newName));
console.log(`${filename}${extname} --> ${this.params.imagesDir}/${newName} 复制成功!`);
}
const newName = basename + "x" + basename + extname;
const newPath = path.join(dirname, newName);
fs.renameSync(filePath, newPath);
console.log(`${filename}${extname} --> ${newName} 重命名成功!`);
}
}
console.log("[ee-bin] [icon-gen] iconGen 资源处理完成!");
} catch (e) {
console.error("[ee-bin] [icon-gen] ERROR: ", e);
throw new Error("重命名logo图片资源失败!!");
}
}
}
const run = (): void => {
const i = new IconGen();
i.generateIcons();
}
export const IconGenModule = {
run,
};

View File

@@ -0,0 +1,163 @@
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import chalk from 'chalk';
import * as Utils from '../lib/utils';
import admZip from 'adm-zip';
type Config = {
asarFile?: string | string[];
output: {
zip: string;
file: string;
directory: string;
};
extraResources?: string[];
cleanCache?: boolean;
};
type PlatformConfig = {
[key: string]: any; // 使用索引签名作为示例,根据实际结构定义类型
};
class Updater {
/**
* 执行
*/
static run(options: { config: string; asarFile?: string; platform: string }) {
console.log('[ee-bin] [updater] Start');
const { config, asarFile, platform } = options;
const binCfg = Utils.loadConfig(config);
const cfg: PlatformConfig = binCfg.updater;
if (!cfg) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${cfg} config does not exist`));
return;
}
this.generateFile(cfg, asarFile, platform);
console.log('[ee-bin] [updater] End');
}
/**
* 生成增量升级文件
*/
static generateFile(config: PlatformConfig, asarFile: string | undefined, platform: string) {
const cfg = config[platform];
let latestVersionInfo: any = {}; // 根据实际结构定义类型
const homeDir = process.cwd();
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.green(`${platform} config:`), cfg);
let asarFilePath = "";
if (asarFile) {
asarFilePath = path.normalize(path.join(homeDir, asarFile));
} else if (Array.isArray(cfg.asarFile)) {
for (const filep of cfg.asarFile) {
asarFilePath = path.normalize(path.join(homeDir, filep));
if (fs.existsSync(asarFilePath)) {
break;
}
}
} else {
asarFilePath = path.normalize(path.join(homeDir, cfg.asarFile as string));
}
if (!fs.existsSync(asarFilePath)) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${asarFilePath} does not exist`));
return;
}
const packageJson = Utils.getPackage();
const version = packageJson.version;
let platformForFilename = platform;
if (platform.indexOf("_") !== -1) {
const platformArr = platform.split("_");
platformForFilename = platformArr.join("-");
}
// 生成 zip
let zipName = "";
zipName = path.basename(cfg.output.zip, '.zip') + `-${platformForFilename}-${version}.zip`;
const asarZipPath = path.join(homeDir, cfg.output.directory, zipName);
if (fs.existsSync(asarZipPath)) {
Utils.rm(asarZipPath);
}
const zip = new admZip();
zip.addLocalFile(asarFilePath);
if (cfg.extraResources && cfg.extraResources.length > 0) {
for (const extraRes of cfg.extraResources) {
const extraResPath = path.normalize(path.join(homeDir, extraRes));
if (fs.existsSync(extraResPath)) {
zip.addLocalFile(extraResPath, "extraResources");
}
}
}
zip.writeZip(asarZipPath, (err) => {
if (err) {
console.log(chalk.blue('[ee-bin] [updater] create zip ') + chalk.red(`Error: ${err}`));
}
});
// 生成 latest.json
const sha1 = this.generateSha1(asarFilePath);
const date = this._getFormattedDate();
const fileStat = fs.statSync(asarFilePath);
const item = {
version: version,
file: zipName,
size: fileStat.size,
sha1: sha1,
releaseDate: date,
};
const jsonName = path.basename(cfg.output.file, '.json') + `-${platformForFilename}.json`;
latestVersionInfo = item;
const updaterJsonFilePath = path.join(homeDir, cfg.output.directory, jsonName);
Utils.writeJsonSync(updaterJsonFilePath, latestVersionInfo);
// 删除缓存文件,防止生成的 zip 是旧版本
if (cfg.cleanCache) {
Utils.rm(path.join(homeDir, cfg.output.directory, 'mac'));
Utils.rm(path.join(homeDir, cfg.output.directory, 'mac-arm64'));
Utils.rm(path.join(homeDir, cfg.output.directory, 'win-unpacked'));
Utils.rm(path.join(homeDir, cfg.output.directory, 'win-ia32-unpacked'));
Utils.rm(path.join(homeDir, cfg.output.directory, 'linux-unpacked'));
}
}
static generateSha1(filepath: string = ""): string {
let sha1 = '';
if (filepath.length === 0) {
return sha1;
}
if (!fs.existsSync(filepath)) {
return sha1;
}
console.log(chalk.blue('[ee-bin] [updater] ') + `generate sha1 for filepath:${filepath}`);
try {
const buffer = fs.readFileSync(filepath);
const fsHash = crypto.createHash('sha1');
fsHash.update(buffer);
sha1 = fsHash.digest('hex');
return sha1;
} catch (error) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: generate sha1 error!`));
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${error}`));
}
return sha1;
}
static _getFormattedDate(): string {
const date = new Date();
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
}
export = Updater;

View File

@@ -0,0 +1,92 @@
import path from 'path';
import fs from 'fs';
import fsPro from 'fs-extra';
import chalk from 'chalk';
import * as Utils from '../lib/utils';
interface MoveConfig {
dist: string;
target: string;
}
interface BinConfig {
move: Record<string, MoveConfig>;
}
/**
* 移动资源
*/
const moveResources = {
/**
* 执行
*/
run(options: { config: string; flag: string } = {}) {
console.log('[ee-bin] [move] Start moving resources');
const homeDir = process.cwd();
const { config, flag } = options;
const binCfg: BinConfig = Utils.loadConfig(config);
let flags: string[];
const flagString = flag.trim();
if (flagString.indexOf(',') !== -1) {
flags = flagString.split(',');
} else {
flags = [flagString];
}
for (let i = 0; i < flags.length; i++) {
let f = flags[i];
let cfg: MoveConfig | undefined = binCfg.move[f];
if (!cfg) {
console.log(chalk.blue('[ee-bin] [move] ') + chalk.red(`Error: ${f} config does not exist`));
return;
}
console.log(chalk.blue('[ee-bin] [move] ') + chalk.green(`Move flag: ${f}`));
console.log(chalk.blue('[ee-bin] [move] ') + chalk.green('config:'), cfg);
const distResource = path.join(homeDir, cfg.dist);
if (!fs.existsSync(distResource)) {
const errorTips = chalk.bgRed('Error') + ` ${cfg.dist} resource does not exist !`;
console.error(errorTips);
return;
}
// clear the historical resource and copy it to the ee resource directory
const targetResource = path.join(homeDir, cfg.target);
if (fs.statSync(distResource).isDirectory() && !fs.existsSync(targetResource)) {
fs.mkdirSync(targetResource, { recursive: true, mode: 0o777 });
} else {
this._rm(targetResource);
console.log('[ee-bin] [move] Clear history resources:', targetResource);
}
fsPro.copySync(distResource, targetResource);
// [todo] go project, special treatment of package.json, reserved only necessary
console.log(`[ee-bin] [move] Copy ${distResource} to ${targetResource}`);
}
console.log('[ee-bin] [move] End');
},
/**
* Delete a file or folder
*/
_rm(name: string) {
// check
if (!fs.existsSync(name)) {
return;
}
const nodeVersion = (process.versions && process.versions.node) || null;
if (nodeVersion && Utils.compareVersion(nodeVersion, '14.14.0') === 1) {
fs.rmSync(name, { recursive: true });
} else {
fs.rmdirSync(name, { recursive: true });
}
},
};
export = moveResources;

View File

@@ -0,0 +1,175 @@
import path from 'path';
import * as Utils from '../lib/utils';
import is from 'is-type-of';
import chalk from 'chalk';
import crossSpawn from 'cross-spawn';
interface CommandConfig {
cmd: string;
args?: string | string[];
directory: string;
stdio?: string | string[];
sync?: boolean;
}
interface BinCmdConfig {
[key: string]: CommandConfig;
}
interface Options {
config?: string;
serve?: string;
cmds?: string;
command?: string;
}
const execProcess: Record<string, any> = {}; // 使用索引签名作为示例,根据实际结构定义类型
const moduleExports = {
/**
* 启动前端、主进程服务
*/
dev(options: Options = {}) {
const { config, serve } = options;
const binCmd = 'dev';
const binCfg = Utils.loadConfig(config);
const binCmdConfig: BinCmdConfig = binCfg[binCmd];
let command = serve;
if (!command) {
command = Object.keys(binCmdConfig).join();
}
const opt = {
binCmd,
binCmdConfig,
command,
};
moduleExports.multiExec(opt);
},
/**
* 启动主进程服务
*/
start(options: Options = {}) {
const { config } = options;
const binCmd = 'start';
const binCfg = Utils.loadConfig(config);
const binCmdConfig: BinCmdConfig = {
start: binCfg[binCmd],
};
const opt = {
binCmd,
binCmdConfig,
command: binCmd,
};
moduleExports.multiExec(opt);
},
sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
/**
* 构建
*/
build(options: Options = {}) {
const { config, cmds } = options;
const binCmd = 'build';
const binCfg = Utils.loadConfig(config);
const binCmdConfig: BinCmdConfig = binCfg[binCmd];
if (!cmds || cmds === "") {
let tip = chalk.bgYellow('Warning') + ' Please modify the ' + chalk.blue('build') + ' config, See: ';
tip += chalk.underline('https://www.kaka996.com/pages/c492f8/');
console.log(tip);
return;
}
const opt = {
binCmd,
binCmdConfig,
command: cmds,
};
moduleExports.multiExec(opt);
},
/**
* 执行自定义命令
*/
exec(options: Options = {}) {
let { config, command, cmds } = options;
const binCmd = 'exec';
const binCfg = Utils.loadConfig(config);
const binCmdConfig: BinCmdConfig = binCfg[binCmd];
if (typeof command === "string") {
cmds = command;
}
const opt = {
binCmd,
binCmdConfig,
command: cmds,
};
moduleExports.multiExec(opt);
},
/**
* 支持多个命令
*/
multiExec(opt: { binCmd: string; binCmdConfig: BinCmdConfig; command: string }) {
const { binCmd, binCmdConfig, command } = opt;
let cmds: string[];
const cmdString = command.trim();
if (cmdString.indexOf(',') !== -1) {
cmds = cmdString.split(',');
} else {
cmds = [cmdString];
}
for (let i = 0; i < cmds.length; i++) {
let cmd = cmds[i];
let cfg: CommandConfig | undefined = binCmdConfig[cmd];
if (!cfg) {
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.red(`Error: [${binCmd} ${cmd}] config does not exist`));
continue;
}
if (cmd === 'frontend' && cfg.protocol === 'file://') {
continue;
}
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + "Run " + chalk.green(`[${binCmd} ${cmd}]`) + " command");
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.green('config:'), JSON.stringify(cfg));
let execDir = path.join(process.cwd(), cfg.directory);
let execArgs = is.string(cfg.args) ? [cfg.args] : cfg.args;
let stdio = cfg.stdio ? cfg.stdio : 'inherit';
const handler = cfg.sync ? crossSpawn.sync : crossSpawn;
execProcess[cmd] = handler(
cfg.cmd,
execArgs,
{ stdio: stdio, cwd: execDir, maxBuffer: 1024 * 1024 * 1024 },
);
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + 'The ' + chalk.green(`[${binCmd} ${cmd}]`) + ` command is ${cfg.sync ? 'run completed' : 'running'}`);
if (!cfg.sync) {
execProcess[cmd].on('exit', () => {
if (cmd === 'electron') {
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.green('Press "CTRL+C" to exit'));
return;
}
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + 'The ' + chalk.green(`[${binCmd} ${cmd}]`) + ' command is executed and exits');
});
}
}
},
};
export = moduleExports;

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"include": [
/* All JavaScript targets from tsconfig.json include. */
// "*.js",
// "*.mjs",
// "lib/**/*.js",
// "tools/**/*.js",
"src/**/*.js",
"src/**/*.mjs"
],
}

41
ee-bin-ts/tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"lib": ["es2021"],
"module": "node16",
"target": "es2021",
"allowJs": true,
"checkJs": true,
/* Strict by default, but dial it down to reduce churn in our JavaScript code. */
"strict": true,
"noImplicitAny": false,
"strictNullChecks": false,
"useUnknownInCatchVariables": false,
"noImplicitThis": false,
//"noUnusedLocals": true, /* 是否允许未使用的局部变量 */
//"noUnusedParameters": true, /* 是否允许未使用的函数参数 */
// "noFallthroughCasesInSwitch": true, /* 没有break语句的 switch 分支报错 */
"types": ["node"],
// "noEmit": true /* just type checking and not emitting transpiled files */,
"skipLibCheck": false /* we want to check our hand crafted definitions */,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true /* common TypeScript config */,
"resolveJsonModule": true /* needed for globals in node_modules?! */,
"outDir": "./dist"
},
"include": [
"src/**/*"
],
// "include": [
// "src/**/*.js",
// "src/**/*.mjs",
// "src/**/*.ts",
// "src/**/*.mts"
// ],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
/* Full strict is fine for the TypeScript files, so turn back on the checks we turned off for mixed-use. */
"noImplicitAny": true,
"strictNullChecks": true,
"useUnknownInCatchVariables": true,
},
"include": [
/* All TypeScript targets from tsconfig.json include. */
"src/**/*.ts",
"src/**/*.mts",
],
}

2
ee-bin/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
package-lock.json

View File

@@ -0,0 +1,181 @@
/**
* ee-bin 配置
* 仅适用于开发环境
*/
module.exports = {
/**
* development serve ("frontend" "electron" )
* ee-bin dev
*/
dev: {
frontend: {
directory: './frontend',
cmd: 'npm',
args: ['run', 'dev'],
protocol: 'http://',
hostname: 'localhost',
port: 8080,
indexPath: 'index.html',
force: false,
sync: false,
},
electron: {
directory: './',
cmd: 'electron',
args: ['.', '--env=local'],
loadingPage: '/public/html/loading.html',
watch: false,
sync: false,
},
},
/**
* 构建
* ee-bin build
*/
build: {
frontend: {
directory: './frontend',
cmd: 'npm',
args: ['run', 'build'],
},
electron: {
type: 'javascript',
bundler: 'esbuild',
bundleType: 'bundle', // copy | bundle
javascript: {
entryPoints: ['./electron/**/*.js'],
platform: 'node',
bundle: false,
minify: false,
outdir: 'public/electron',
packages: 'external',
sourcemap:false,
sourcesContent: false
},
typescript: {
entryPoints: ['./electron/**/*.ts'],
tsconfig: './tsconfig.json',
platform: 'node',
format: 'cjs',
bundle: false,
minify: false,
outdir: 'public/electron',
packages: 'external',
sourcemap:false,
sourcesContent: false
}
},
win32: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=nsis', '--ia32'],
},
win64: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=nsis', '--x64'],
},
win_e: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=portable', '--x64'],
},
win_7z: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=7z', '--x64'],
},
mac: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder-mac.json', '-m'],
},
mac_arm64: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder-mac-arm64.json', '-m', '--arm64'],
},
linux: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder-linux.json', '-l=deb', '--x64'],
},
linux_arm64: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder-linux.json', '-l=deb', '--arm64'],
},
},
/**
* 移动资源
* ee-bin move
*/
move: {
frontend_dist: {
src: './frontend/dist',
dest: './public/dist'
}
},
/**
* 预发布模式prod
* ee-bin start
*/
start: {
directory: './',
cmd: 'electron',
args: ['.', '--env=prod']
},
/**
* 加密
*/
encrypt: {
frontend: {
type: 'none',
files: [
'./public/dist/**/*.(js|json)',
],
fileExt: ['.js'],
cleanFiles: ['./public/dist'],
specificFiles: [],
encryptDir: './',
confusionOptions: {
compact: true,
stringArray: true,
stringArrayEncoding: ['none'],
deadCodeInjection: false,
stringArrayCallsTransform: true,
numbersToExpressions: true,
target: 'browser',
}
},
electron: {
type: 'none',
files: [
'./public/electron/**/*.(js|json)',
],
fileExt: ['.js'],
cleanFiles: ['./public/electron'],
specificFiles: ['./public/electron/preload/bridge.js'],
encryptDir: './',
confusionOptions: {
compact: true,
stringArray: true,
stringArrayEncoding: ['rc4'],
deadCodeInjection: false,
stringArrayCallsTransform: true,
numbersToExpressions: true,
target: 'node',
}
}
},
/**
* 执行自定义命令
* ee-bin exec
*/
exec: {},
};

118
ee-bin/index.js Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env node
const { program } = require('commander');
const { move } = require('./tools/move');
const { encrypt, cleanEncrypt } = require('./tools/encrypt');
const { serveProcess } = require('./tools/serve');
const { incrUpdater } = require('./tools/incrUpdater');
/**
* dev
*/
program
.command('dev')
.description('create frontend-serve and electron-serve')
.option('--config <folder>', 'config file')
.option('--serve <mode>', 'serve mode')
.action(function() {
serveProcess.dev(this.opts());
});
/**
* build
*/
program
.command('build')
.description('building multiple resources')
.option('--config <folder>', 'config file')
.option('--cmds <flag>', 'custom commands')
.option('--env <env>', 'environment')
.action(function() {
serveProcess.build(this.opts());
});
/**
* start
*/
program
.command('start')
.description('preview effect')
.option('--config <folder>', 'config file')
.action(function() {
serveProcess.start(this.opts());
});
/**
* exec
*/
program
.command('exec')
.description('create frontend-serve and electron-serve')
.option('--config <folder>', 'config file')
.option('--cmds <flag>', 'custom commands')
.action(function() {
serveProcess.exec(this.opts());
});
/**
* move - Moves resources
*/
program
.command('move')
.description('Move multip resources')
.option('--config <folder>', 'config file')
.option('--flag <flag>', 'Custom flag')
.action(function() {
move(this.opts());
});
/**
* encrypt - Code encryption
*/
program
.command('encrypt')
.description('Code encryption')
.option('--config <folder>', 'config file')
.option('--out <folder>', 'output directory')
.action(function() {
encrypt(this.opts());
});
/**
* clean - Clear the encrypted code
*/
program
.command('clean')
.description('Clear the encrypted code')
.option('-d, --dir <folder>', 'clean directory')
.action(function() {
cleanEncrypt(this.opts());
});
/**
* icon
*/
program
.command('icon')
.description('Generate logo')
.option('-i, --input <file>', 'image file default /public/images/logo.png')
.option('-o, --output <folder>', 'output directory default /build/icons/')
.action(function() {
const iconGen = require('./tools/iconGen');
iconGen.run();
});
/**
* updater
*/
program
.command('updater')
.description('updater commands')
.option('--config <folder>', 'config file')
.option('--asar-file <file>', 'asar file path')
.option('--platform <flag>', 'platform')
.action(function() {
incrUpdater.run(this.opts());
});
program.parse();

78
ee-bin/lib/extend.js Normal file
View File

@@ -0,0 +1,78 @@
'use strict';
const hasOwn = Object.prototype.hasOwnProperty;
const toStr = Object.prototype.toString;
function isPlainObject(obj) {
if (!obj || toStr.call(obj) !== '[object Object]') {
return false;
}
var hasOwnConstructor = hasOwn.call(obj, 'constructor');
var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for (key in obj) { /**/ }
return typeof key === 'undefined' || hasOwn.call(obj, key);
};
function extend() {
var options, name, src, copy, clone;
var target = arguments[0];
var i = 1;
var length = arguments.length;
var deep = false;
// Handle a deep copy situation
if (typeof target === 'boolean') {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
} else if ((typeof target !== 'object' && typeof target !== 'function') || target == null) {
target = {};
}
for (; i < length; ++i) {
options = arguments[i];
// Only deal with non-null/undefined values
if (options == null) continue;
// Extend the base object
for (name in options) {
if (name === '__proto__') continue;
src = target[name];
copy = options[name];
// Prevent never-ending loop
if (target === copy) continue;
// Recurse if we're merging plain objects
if (deep && copy && isPlainObject(copy)) {
clone = src && isPlainObject(src) ? src : {};
// Never move original objects, clone them
target[name] = extend(deep, clone, copy);
// Don't bring in undefined values
} else if (typeof copy !== 'undefined') {
target[name] = copy;
}
}
}
// Return the modified object
return target;
};
module.exports = {
extend,
isPlainObject,
};

263
ee-bin/lib/pargv.js Normal file
View File

@@ -0,0 +1,263 @@
'use strict';
function hasKey(obj, keys) {
var o = obj;
keys.slice(0, -1).forEach(function (key) {
o = o[key] || {};
});
var key = keys[keys.length - 1];
return key in o;
}
function isNumber(x) {
if (typeof x === 'number') { return true; }
if ((/^0x[0-9a-f]+$/i).test(x)) { return true; }
return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x);
}
function isConstructorOrProto(obj, key) {
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
}
module.exports = function (args, opts) {
if (!opts) { opts = {}; }
var flags = {
bools: {},
strings: {},
unknownFn: null,
};
if (typeof opts.unknown === 'function') {
flags.unknownFn = opts.unknown;
}
if (typeof opts.boolean === 'boolean' && opts.boolean) {
flags.allBools = true;
} else {
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
flags.bools[key] = true;
});
}
var aliases = {};
function aliasIsBoolean(key) {
return aliases[key].some(function (x) {
return flags.bools[x];
});
}
Object.keys(opts.alias || {}).forEach(function (key) {
aliases[key] = [].concat(opts.alias[key]);
aliases[key].forEach(function (x) {
aliases[x] = [key].concat(aliases[key].filter(function (y) {
return x !== y;
}));
});
});
[].concat(opts.string).filter(Boolean).forEach(function (key) {
flags.strings[key] = true;
if (aliases[key]) {
[].concat(aliases[key]).forEach(function (k) {
flags.strings[k] = true;
});
}
});
var defaults = opts.default || {};
var argv = { _: [] };
function argDefined(key, arg) {
return (flags.allBools && (/^--[^=]+$/).test(arg))
|| flags.strings[key]
|| flags.bools[key]
|| aliases[key];
}
function setKey(obj, keys, value) {
var o = obj;
for (var i = 0; i < keys.length - 1; i++) {
var key = keys[i];
if (isConstructorOrProto(o, key)) { return; }
if (o[key] === undefined) { o[key] = {}; }
if (
o[key] === Object.prototype
|| o[key] === Number.prototype
|| o[key] === String.prototype
) {
o[key] = {};
}
if (o[key] === Array.prototype) { o[key] = []; }
o = o[key];
}
var lastKey = keys[keys.length - 1];
if (isConstructorOrProto(o, lastKey)) { return; }
if (
o === Object.prototype
|| o === Number.prototype
|| o === String.prototype
) {
o = {};
}
if (o === Array.prototype) { o = []; }
if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') {
o[lastKey] = value;
} else if (Array.isArray(o[lastKey])) {
o[lastKey].push(value);
} else {
o[lastKey] = [o[lastKey], value];
}
}
function setArg(key, val, arg) {
if (arg && flags.unknownFn && !argDefined(key, arg)) {
if (flags.unknownFn(arg) === false) { return; }
}
var value = !flags.strings[key] && isNumber(val)
? Number(val)
: val;
setKey(argv, key.split('.'), value);
(aliases[key] || []).forEach(function (x) {
setKey(argv, x.split('.'), value);
});
}
Object.keys(flags.bools).forEach(function (key) {
setArg(key, defaults[key] === undefined ? false : defaults[key]);
});
var notFlags = [];
if (args.indexOf('--') !== -1) {
notFlags = args.slice(args.indexOf('--') + 1);
args = args.slice(0, args.indexOf('--'));
}
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var key;
var next;
if ((/^--.+=/).test(arg)) {
// Using [\s\S] instead of . because js doesn't support the
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
key = m[1];
var value = m[2];
if (flags.bools[key]) {
value = value !== 'false';
}
setArg(key, value, arg);
} else if ((/^--no-.+/).test(arg)) {
key = arg.match(/^--no-(.+)/)[1];
setArg(key, false, arg);
} else if ((/^--.+/).test(arg)) {
key = arg.match(/^--(.+)/)[1];
next = args[i + 1];
if (
next !== undefined
&& !(/^(-|--)[^-]/).test(next)
&& !flags.bools[key]
&& !flags.allBools
&& (aliases[key] ? !aliasIsBoolean(key) : true)
) {
setArg(key, next, arg);
i += 1;
} else if ((/^(true|false)$/).test(next)) {
setArg(key, next === 'true', arg);
i += 1;
} else {
setArg(key, flags.strings[key] ? '' : true, arg);
}
} else if ((/^-[^-]+/).test(arg)) {
var letters = arg.slice(1, -1).split('');
var broken = false;
for (var j = 0; j < letters.length; j++) {
next = arg.slice(j + 2);
if (next === '-') {
setArg(letters[j], next, arg);
continue;
}
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
setArg(letters[j], next.slice(1), arg);
broken = true;
break;
}
if (
(/[A-Za-z]/).test(letters[j])
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
) {
setArg(letters[j], next, arg);
broken = true;
break;
}
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
setArg(letters[j], arg.slice(j + 2), arg);
broken = true;
break;
} else {
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
}
}
key = arg.slice(-1)[0];
if (!broken && key !== '-') {
if (
args[i + 1]
&& !(/^(-|--)[^-]/).test(args[i + 1])
&& !flags.bools[key]
&& (aliases[key] ? !aliasIsBoolean(key) : true)
) {
setArg(key, args[i + 1], arg);
i += 1;
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
setArg(key, args[i + 1] === 'true', arg);
i += 1;
} else {
setArg(key, flags.strings[key] ? '' : true, arg);
}
}
} else {
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
}
if (opts.stopEarly) {
argv._.push.apply(argv._, args.slice(i + 1));
break;
}
}
}
Object.keys(defaults).forEach(function (k) {
if (!hasKey(argv, k.split('.'))) {
setKey(argv, k.split('.'), defaults[k]);
(aliases[k] || []).forEach(function (x) {
setKey(argv, x.split('.'), defaults[k]);
});
}
});
if (opts['--']) {
argv['--'] = notFlags.slice();
} else {
notFlags.forEach(function (k) {
argv._.push(k);
});
}
return argv;
};

209
ee-bin/lib/utils.js Normal file
View File

@@ -0,0 +1,209 @@
'use strict';
const debug = require('debug')('ee-bin:lib:utils');
const path = require('path');
const fs = require('fs');
const chalk = require('chalk');
const is = require('is-type-of');
const { loadTsConfig } = require('config-file-ts');
const JsonLib = require('json5');
const mkdirp = require('mkdirp');
const OS = require('os');
const defaultConfig = require('../config/bin_default');
const { extend } = require('./extend');
const _basePath = process.cwd();
const userBin = './cmd/bin.js';
function loadConfig(binFile) {
const binPath = binFile ? binFile : userBin;
const userConfig = loadFile(binPath);
const result = extend(true, defaultConfig, userConfig);
debug('[loadConfig] bin:%j', result)
return result
};
function loadFile(filepath) {
const configFile = path.join(_basePath, filepath);
if (!fs.existsSync(configFile)) {
const errorTips = 'file ' + chalk.blue(`${configFile}`) + ' does not exist !';
throw new Error(errorTips)
}
let result;
if (configFile.endsWith(".json5") || configFile.endsWith(".json")) {
const data = fs.readFileSync(configFile, 'utf8');
return JsonLib.parse(data);
}
if (configFile.endsWith(".js") || configFile.endsWith(".cjs")) {
result = require(configFile);
if (result.default != null) {
result = result.default;
}
} else if (configFile.endsWith(".ts")) {
result = loadTsConfig(configFile);
}
if (is.function(result) && !is.class(result)) {
result = result();
}
return result || {}
};
/**
* get electron program
*/
function getElectronProgram() {
let electronPath
const electronModulePath = path.dirname(require.resolve('electron'))
const pathFile = path.join(electronModulePath, 'path.txt')
const executablePath = fs.readFileSync(pathFile, 'utf-8')
if (executablePath) {
electronPath = path.join(electronModulePath, 'dist', executablePath)
} else {
throw new Error('Check that electron is installed!')
}
return electronPath;
};
/**
* 版本号比较
*/
function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i])
const num2 = parseInt(v2[i])
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
function isWindows() {
return process.platform === 'win32'
}
function isOSX() {
return process.platform === 'darwin'
}
function isMacOS() {
return isOSX()
}
function isLinux() {
return process.platform === 'linux'
}
function isx86() {
return process.arch === 'ia32'
}
function isx64() {
return process.arch === 'x64'
}
/**
* Delete a file or folder
*/
function rm(name) {
// check
if (!fs.existsSync(name)) {
return
}
const nodeVersion = (process.versions && process.versions.node) || null;
if (nodeVersion && compareVersion(nodeVersion, '14.14.0') == 1) {
fs.rmSync(name, {recursive: true});
} else {
fs.rmdirSync(name, {recursive: true});
}
}
/**
* 获取项目根目录package.json
*/
function getPackage () {
const content = readJsonSync(path.join(_basePath, 'package.json'));
return content;
}
function readJsonSync (filepath) {
if (!fs.existsSync(filepath)) {
throw new Error(filepath + ' is not found');
}
return JSON.parse(fs.readFileSync(filepath));
}
function writeJsonSync (filepath, str, options) {
options = options || {};
if (!('space' in options)) {
options.space = 2;
}
mkdirp.sync(path.dirname(filepath));
if (typeof str === 'object') {
str = JSON.stringify(str, options.replacer, options.space) + '\n';
}
fs.writeFileSync(filepath, str);
};
function getPlatform(delimiter = "_", isDiffArch = false) {
let os = "";
if (isWindows()) {
os = "windows";
if (isDiffArch) {
const arch = isx64() ? "64" : "32";
os += delimiter + arch;
}
} else if (isMacOS()) {
let isAppleSilicon = false;
const cpus = OS.cpus();
for (let cpu of cpus) {
if (cpu.model.includes('Apple')) {
isAppleSilicon = true;
break;
}
}
const core = isAppleSilicon? "apple" : "intel";
os = "macos" + delimiter + core;
} else if (isLinux()) {
os = "linux";
}
return os;
}
module.exports = {
loadConfig,
getElectronProgram,
compareVersion,
isWindows,
isOSX,
isMacOS,
isLinux,
isx86,
isx64,
getPlatform,
rm,
getPackage,
readJsonSync,
writeJsonSync
}

31
ee-bin/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "ee-bin",
"version": "4.0.0-beta.1",
"description": "ee bin",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"bin": {
"ee-bin": "index.js"
},
"dependencies": {
"adm-zip": "^0.4.11",
"bytenode": "^1.3.6",
"chalk": "^4.1.2",
"chokidar": "^4.0.3",
"commander": "^11.0.0",
"config-file-ts": "^0.2.8-rc1",
"cross-spawn": "^7.0.3",
"debug": "^4.4.0",
"esbuild": "0.24.2",
"fs-extra": "^10.0.0",
"globby": "^10.0.0",
"is-type-of": "^1.2.1",
"javascript-obfuscator": "^4.0.2",
"json5": "^2.2.3",
"mkdirp": "^2.1.3"
}
}

1
ee-bin/readme.md Normal file
View File

@@ -0,0 +1 @@
## ee-bin

179
ee-bin/tools/encrypt.js Normal file
View File

@@ -0,0 +1,179 @@
'use strict';
const path = require('path');
const fs = require('fs');
const fsPro = require('fs-extra');
const is = require('is-type-of');
const bytenode = require('bytenode');
const JavaScriptObfuscator = require('javascript-obfuscator');
const globby = require('globby');
const chalk = require('chalk');
const { loadConfig } = require('../lib/utils');
const { extend } = require('../lib/extend');
const EncryptTypes = ['bytecode', 'confusion', 'strict'];
class Encrypt {
constructor(options = {}) {
// cli args
const { config, out, target } = options;
this.basePath = process.cwd();
this.target = target;
const conf = loadConfig(config).encrypt;
this.config = conf[target];
const outputFolder = out || this.config.encryptDir;
this.encryptDir = path.join(this.basePath, outputFolder);
this.filesExt = this.config.fileExt;
this.type = this.config.type;
this.bOpt = this.config.bytecodeOptions;
this.cOpt = this.config.confusionOptions;
this.cleanFiles = this.config.cleanFiles;
this.patterns = this.config.files || null;
this.specFiles = this.config.specificFiles;
this.codefiles = this._initCodeFiles();
}
/**
* 初始化需要加密的文件列表
*/
_initCodeFiles() {
if (!this.patterns) return;
const files = globby.sync(this.patterns, { cwd: this.basePath });
return files;
}
/**
* 加密代码
*/
encrypt() {
if (EncryptTypes.indexOf(this.type) == -1) return;
if (this.target == 'frontend' && (this.type == 'bytecode' || this.type == 'strict')) return;
console.log(chalk.blue('[ee-bin] [encrypt] ') + `start ciphering ${this.target}`);
for (const file of this.codefiles) {
const fullpath = path.join(this.encryptDir, file);
if (!fs.statSync(fullpath).isFile()) continue;
// 特殊文件处理
if (this.specFiles.includes(file)) {
this.generate(fullpath, 'confusion');
continue;
}
this.generate(fullpath);
}
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'end ciphering');
};
/**
* 递归
*/
loop(dirPath) {
let files = [];
if (fs.existsSync(dirPath)) {
files = fs.readdirSync(dirPath);
files.forEach((file, index) => {
let curPath = dirPath + '/' + file;
if (fs.statSync(curPath).isDirectory()) {
this.loop(curPath);
} else {
const extname = path.extname(curPath);
if (this.filesExt.indexOf(extname) !== -1) {
this.generate(curPath);
}
}
});
}
}
/**
* 生成文件
*/
generate(curPath, type) {
let encryptType = type ? type : this.type;
let tips = chalk.blue('[ee-bin] [encrypt] ') + 'file: ' + chalk.green(`${curPath}`) + ' ' + chalk.cyan(`(${encryptType})`);
console.log(tips);
if (encryptType == 'strict') {
this.generateJSConfuseFile(curPath);
this.generateBytecodeFile(curPath);
} else if (encryptType == 'bytecode') {
this.generateBytecodeFile(curPath);
} else if (encryptType == 'confusion') {
this.generateJSConfuseFile(curPath);
} else {
// none
}
}
/**
* 使用 javascript-obfuscator 生成压缩/混淆文件
*/
generateJSConfuseFile(file) {
let opt = Object.assign({
compact: true,
stringArray: true,
stringArrayThreshold: 1,
}, this.cOpt);
let code = fs.readFileSync(file, "utf8");
let result = JavaScriptObfuscator.obfuscate(code, opt);
fs.writeFileSync(file, result.getObfuscatedCode(), "utf8");
}
/**
* 生成字节码文件
*/
generateBytecodeFile(curPath) {
if (path.extname(curPath) !== '.js') {
return
}
//let jscFile = curPath.replace(/.js/g, '.jsc');
let jscFile = curPath + 'c';
let opt = Object.assign({
filename: curPath,
output: jscFile,
electron: true
}, this.bOpt);
bytenode.compileFile(opt);
fsPro.removeSync(curPath);
}
}
function encrypt(options = {}) {
const electronOpt = extend(true, {
target: 'electron',
}, options);
const electronEpt = new Encrypt(electronOpt);
electronEpt.encrypt();
const frontendOpt = extend(true, {
target: 'frontend',
}, options);
const frontendEpt = new Encrypt(frontendOpt);
frontendEpt.encrypt();
}
function cleanEncrypt(options = {}) {
// [todo] 删除前端和主进程代码
return;
let files = options.dir !== undefined ? options.dir : ['./public/electron'];
files = is.string(files) ? [files] : files;
files.forEach((file) => {
const tmpFile = path.join(process.cwd(), file);
if (fs.existsSync(tmpFile)) {
fsPro.removeSync(tmpFile);
console.log(chalk.blue('[ee-bin] [encrypt] ') + 'clean up tmp files: ' + chalk.magenta(`${tmpFile}`));
}
})
}
module.exports = {
encrypt,
cleanEncrypt,
};

183
ee-bin/tools/iconGen.js Normal file
View File

@@ -0,0 +1,183 @@
'use strict';
const fs = require("fs");
const path = require("path");
const icongen = require("icon-gen");
class IconGen {
constructor() {
this._init();
}
/**
* _init
*/
_init() {
// ---> 处理参数
const args = process.argv.splice(3);
let params = {
input: "/public/images/logo.png",
output: "/build/icons/",
size: "16,32,64,256,512",
clear: false,
imagesDir: "/public/images/",
};
try {
const len = args.length;
for (let i = 0; i < len; i++) {
const arg = args[i];
if (arg.match(/^-i/) || arg.match(/^-input/)) {
params["input"] = args[i + 1];
i++;
continue;
}
if (arg.match(/^-o/) || arg.match(/^-output/)) {
params["output"] = args[i + 1];
i++;
continue;
}
if (arg.match(/^-s/) || arg.match(/^-size/)) {
params["size"] = args[i + 1];
i++;
continue;
}
if (arg.match(/^-c/) || arg.match(/^-clear/)) {
params["clear"] = true;
continue;
}
if (arg.match(/^-img/) || arg.match(/^-images/)) {
params["imagesDir"] = args[i + 1];
i++;
continue;
}
}
} catch (e) {
console.error("[ee-bin] [icon-gen] args: ", args);
console.error("[ee-bin] [icon-gen] ERROR: ", e);
throw new Error("参数错误!!");
}
this.params = params;
// ---> 组装参数
console.log("[ee-bin] [icon-gen] icon 当前路径: ", process.cwd());
this.input = path.join(process.cwd(), params.input);
this.output = path.join(process.cwd(), params.output);
this.imagesDir = path.join(process.cwd(), params.imagesDir);
const sizeList = params.size.split(",").map((item) => parseInt(item));
this.iconOptions = {
report: false,
ico: {
name: "icon",
sizes: [256],
},
favicon: {
name: "logo-",
pngSizes: sizeList,
},
};
}
/**
* 生成图标
*/
generateIcons() {
console.log("[ee-bin] [icon-gen] iconGen 开始处理生成logo图片");
if (!fs.existsSync(this.input)) {
console.error("[ee-bin] [icon-gen] input: ", this.input);
throw new Error("输入的图片不存在或路径错误");
}
if (!fs.existsSync(this.output)) {
fs.mkdirSync(this.output, { recursive: true });
} else {
// 清空目录
this.params.clear && this.deleteGenFile(this.output);
}
if (!fs.existsSync(this.imagesDir)) {
fs.mkdirSync(this.imagesDir, { recursive: true });
}
icongen(this.input, this.output, this.iconOptions)
.then((results) => {
console.log("[ee-bin] [icon-gen] iconGen 已生成下方图片资源");
console.log(results);
this._renameForEE(results);
})
.catch((err) => {
console.error(err);
throw new Error("[ee-bin] [icon-gen] iconGen 生成失败!");
});
}
/**
* 删除生成的文件(.ico .png)
*/
deleteGenFile(dirPath) {
if (fs.existsSync(dirPath)) {
// 读取文件夹下的文件目录
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const curPath = path.join(dirPath, file);
// 判断是不是文件夹,如果是,继续递归
if (fs.lstatSync(curPath).isDirectory()) {
this.deleteGenFile(curPath);
} else {
// 删除文件
if ([".ico", ".png"].includes(path.extname(curPath))) {
fs.unlinkSync(curPath);
}
}
});
}
}
/**
* 为生成的资源重命名 (logo-32.png -> 32x32.png)
*/
_renameForEE(filesPath) {
console.log("[ee-bin] [icon-gen] iconGen 开始重新命名logo图片资源");
try {
const len = filesPath.length;
for (let i = 0; i < len; i++) {
const filePath = filesPath[i];
const extname = path.extname(filePath);
if ([".png"].includes(extname)) {
const filename = path.basename(filePath, extname);
const basename = filename.split("-")[1];
const dirname = path.dirname(filePath);
// 处理 tray 图标 --> 复制到 public/images 目录下
if ("16" === basename) {
const newName = "tray" + extname;
fs.copyFileSync(filePath, path.join(this.imagesDir, newName));
console.log(`${filename}${extname} --> ${this.params.imagesDir}/${newName} 复制成功!`);
fs.unlinkSync(filePath);
continue;
}
// 处理 win 窗口图标 --> 复制到 public/images 目录下
if ("32" === basename) {
const newName = filename + extname;
fs.copyFileSync(filePath, path.join(this.imagesDir, newName));
console.log(`${filename}${extname} --> ${this.params.imagesDir}/${newName} 复制成功!`);
}
// 重命名 --> 32x32.png
const newName = basename + "x" + basename + extname;
const newPath = path.join(dirname, newName);
fs.renameSync(filePath, newPath);
console.log(`${filename}${extname} --> ${newName} 重命名成功!`);
}
}
console.log("[ee-bin] [icon-gen] iconGen 资源处理完成!");
} catch (e) {
console.error("[ee-bin] [icon-gen] ERROR: ", e);
throw new Error("重命名logo图片资源失败!!");
}
}
}
const run = () => {
const i = new IconGen();
i.generateIcons();
}
module.exports = {
run,
};

168
ee-bin/tools/incrUpdater.js Normal file
View File

@@ -0,0 +1,168 @@
'use strict';
const path = require('path');
const fs = require('fs');
const crypto = require('crypto')
const chalk = require('chalk');
const { loadConfig, rm, getPackage, writeJsonSync } = require('../lib/utils');
const admZip = require('adm-zip')
/**
* 增量升级
* @class
*/
class IncrUpdater {
/**
* 执行
*/
run(options = {}) {
console.log('[ee-bin] [updater] Start');
const { config, asarFile, platform } = options;
const binCfg = loadConfig(config);
const cfg = binCfg.updater;
if (!cfg) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${cfg} config does not exist`));
return;
}
this.generateFile(cfg, asarFile, platform);
console.log('[ee-bin] [updater] End');
}
/**
* 生成增量升级文件
*/
generateFile(config, asarFile, platform) {
const cfg = config[platform];
if (!cfg) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${platform} config does not exist`));
return;
}
let latestVersionInfo = {}
const homeDir = process.cwd();
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.green(`${platform} config:`), cfg);
let asarFilePath = "";
if (asarFile) {
asarFilePath = path.normalize(path.join(homeDir, asarFile));
} else if (Array.isArray(cfg.asarFile)) {
// 检查文件列表,如果存在就跳出
for (const filep of cfg.asarFile) {
asarFilePath = path.normalize(path.join(homeDir, filep));
if (fs.existsSync(asarFilePath)) {
break;
}
}
} else {
asarFilePath = path.normalize(path.join(homeDir, cfg.asarFile));
}
if (!fs.existsSync(asarFilePath)) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${asarFilePath} does not exist`));
return;
}
const packageJson = getPackage();
const version = packageJson.version;
let platformForFilename = platform;
if (platform.indexOf("_") !== -1) {
const platformArr = platform.split("_");
platformForFilename = platformArr.join("-");
}
// 生成 zip
let zipName = "";
zipName = path.basename(cfg.output.zip, '.zip') + `-${platformForFilename}-${version}.zip`;
const asarZipPath = path.join(homeDir, cfg.output.directory, zipName);
if (fs.existsSync(asarZipPath)) {
rm(asarZipPath);
}
const zip = new admZip();
// 添加 asar 文件
zip.addLocalFile(asarFilePath);
// 添加 extraResources
if (cfg.extraResources && cfg.extraResources.length > 0) {
for (const extraRes of cfg.extraResources) {
const extraResPath = path.normalize(path.join(homeDir, extraRes));
if (fs.existsSync(extraResPath)) {
zip.addLocalFile(extraResPath, "extraResources");
}
}
}
zip.writeZip(asarZipPath, (err) => {
if (err) {
console.log(chalk.blue('[ee-bin] [updater] create zip ') + chalk.red(`Error: ${err}`));
}
});
// 生成 latest.json
const sha1 = this.generateSha1(asarFilePath);
const date = this._getFormattedDate();
const fileStat = fs.statSync(asarFilePath);
const item = {
version: version,
file: zipName,
size: fileStat.size,
sha1: sha1,
releaseDate: date,
};
const jsonName = path.basename(cfg.output.file, '.json') + `-${platformForFilename}.json`;
latestVersionInfo = item;
const updaterJsonFilePath = path.join(homeDir, cfg.output.directory, jsonName);
writeJsonSync(updaterJsonFilePath, latestVersionInfo);
// 删除缓存文件,防止生成的 zip 是旧版本
if (cfg.cleanCache) {
rm(path.join(homeDir, cfg.output.directory, 'mac'));
rm(path.join(homeDir, cfg.output.directory, 'mac-arm64'));
rm(path.join(homeDir, cfg.output.directory, 'win-unpacked'));
rm(path.join(homeDir, cfg.output.directory, 'win-ia32-unpacked'));
rm(path.join(homeDir, cfg.output.directory, 'linux-unpacked'));
}
}
generateSha1(filepath = "") {
let sha1 = '';
if (filepath.length == 0) {
return sha1;
}
if (!fs.existsSync(filepath)) {
return sha1;
}
console.log(chalk.blue('[ee-bin] [updater] ') + `generate sha1 for filepath:${filepath}`);
try {
const buffer = fs.readFileSync(filepath);
const fsHash = crypto.createHash('sha1');
fsHash.update(buffer);
sha1 = fsHash.digest('hex');
return sha1;
} catch (error) {
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: generate sha1 error!`));
console.log(chalk.blue('[ee-bin] [updater] ') + chalk.red(`Error: ${error}`));
}
return sha1;
}
_getFormattedDate() {
const date = new Date(); // 获取当前日期
const year = date.getFullYear(); // 获取年份
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 获取月份月份从0开始计数
const day = date.getDate().toString().padStart(2, '0'); // 获取日
return `${year}-${month}-${day}`;
}
}
module.exports = {
IncrUpdater,
incrUpdater: new IncrUpdater()
}

69
ee-bin/tools/move.js Normal file
View File

@@ -0,0 +1,69 @@
'use strict';
const path = require('path');
const fs = require('fs');
const fsPro = require('fs-extra');
const chalk = require('chalk');
const { loadConfig, rm } = require('../lib/utils');
const homeDir = process.cwd();
// 移动资源
function move(options = {}) {
console.log('[ee-bin] [move] Start moving resources');
const { config, flag } = options;
const binCfg = loadConfig(config);
const moveConfig = binCfg.move;
let flags;
const flagString = flag.trim();
if (flagString.indexOf(',') !== -1) {
flags = flagString.split(',');
} else {
flags = [flagString];
}
for (let i = 0; i < flags.length; i++) {
let f = flags[i];
let cfg = moveConfig[f];
if (!cfg) {
console.log(chalk.blue('[ee-bin] [move] ') + chalk.red(`Error: ${f} config does not exist` ));
return;
}
const { src, dest, dist, target } = cfg;
const source = dist ? dist : src;
const destination = target ? target : dest;
console.log(chalk.blue('[ee-bin] [move] ') + chalk.green(`Move flag: ${f}`));
console.log(chalk.blue('[ee-bin] [move] ') + chalk.green('config:'), cfg);
const srcResource = path.join(homeDir, source);
if (!fs.existsSync(srcResource)) {
const errorTips = chalk.bgRed('Error') + ` ${source} resource does not exist !`;
console.error(errorTips);
return
}
// clear the historical resource and copy it to the ee resource directory
const destResource = path.join(homeDir, destination);
if (fs.statSync(srcResource).isDirectory() && !fs.existsSync(destResource)) {
fs.mkdirSync(destResource, {recursive: true, mode: 0o777});
} else {
rm(destResource);
console.log('[ee-bin] [move] Clear history resources:', destResource);
}
fsPro.copySync(srcResource, destResource);
// [todo] go project, special treatment of package.json, reserved only necessary
console.log(`[ee-bin] [move] Copy ${srcResource} to ${destResource}`);
}
console.log('[ee-bin] [move] End');
}
module.exports = {
move
}

240
ee-bin/tools/serve.js Normal file
View File

@@ -0,0 +1,240 @@
'use strict';
const debug = require('debug')('ee-bin:serve');
const path = require('path');
const fsPro = require('fs-extra');
const { loadConfig } = require('../lib/utils');
const is = require('is-type-of');
const chalk = require('chalk');
const crossSpawn = require('cross-spawn');
const { buildSync } = require('esbuild');
const chokidar = require('chokidar');
class ServeProcess {
constructor() {
process.env.NODE_ENV = 'prod'; // dev / prod
this.execProcess = {};
this.electronDir = './electron';
this.defaultBundleDir = './public/electron';
}
/**
* 启动前端、主进程服务
*/
dev(options = {}) {
// 设置一个环境变量
process.env.NODE_ENV = 'dev';
const { config, serve } = options;
const binCfg = loadConfig(config);
const binCmd = 'dev';
const binCmdConfig = binCfg[binCmd];
let command = serve;
if (!command) {
command = Object.keys(binCmdConfig).join();
}
const opt = {
binCmd,
binCmdConfig,
command,
}
// build electron main code
const cmds = this._formatCmds(command);
if (cmds.indexOf("electron") !== -1) {
// watche electron main code
const electronConfig = binCmdConfig.electron;
if (electronConfig.watch) {
const cmd = 'electron';
const watcher = chokidar.watch([this.electronDir], {
persistent: true
});
watcher.on('change', async (f) => {
console.log(chalk.blue('[ee-bin] [dev] ') + `File ${f} has been changed`);
console.log(chalk.blue('[ee-bin] [dev] ') + `Restart ${cmd}`);
// rebuild code
this.bundle(binCfg.build.electron);
let subPorcess = this.execProcess[cmd];
subPorcess.kill();
delete this.execProcess[cmd];
// restart electron command
let onlyElectronOpt = {
binCmd,
binCmdConfig,
command: cmd,
}
this.multiExec(onlyElectronOpt);
});
}
// When starting for the first time, build the code for the electron directory
this.bundle(binCfg.build.electron);
}
this.multiExec(opt);
}
/**
* 启动主进程服务
*/
start(options = {}) {
const { config } = options;
const binCfg = loadConfig(config);
const binCmd = 'start';
const binCmdConfig = {
start: binCfg[binCmd]
};
const opt = {
binCmd,
binCmdConfig,
command: binCmd,
}
this.multiExec(opt);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 构建
*/
build(options = {}) {
const { config, cmds, env } = options;
process.env.NODE_ENV = env;
const binCfg = loadConfig(config);
const binCmd = 'build';
const binCmdConfig = binCfg[binCmd];
if (!cmds || cmds == "") {
const tip = chalk.bgYellow('Warning') + ' Please modify the ' + chalk.blue('build') + ' property in the bin file';
console.log(tip);
return
}
if (cmds.indexOf("electron") !== -1) {
this.bundle(binCmdConfig.electron);
return;
}
const opt = {
binCmd,
binCmdConfig,
command: cmds,
}
this.multiExec(opt);
}
/**
* 执行自定义命令
*/
exec(options = {}) {
const { config, cmds } = options;
const binCfg = loadConfig(config);
const binCmd = 'exec';
const binCmdConfig = binCfg[binCmd];
const opt = {
binCmd,
binCmdConfig,
command: cmds,
}
this.multiExec(opt);
}
/**
* 支持多个命令
*/
multiExec(opt = {}) {
//console.log('multiExec opt:', opt)
const { binCmd, binCmdConfig, command } = opt;
const cmds = this._formatCmds(command);
for (let i = 0; i < cmds.length; i++) {
let cmd = cmds[i];
const cfg = binCmdConfig[cmd];
if (!cfg) {
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.red(`Error: [${binCmd} ${cmd}] config does not exist` ));
continue;
}
// frontend 如果是 file:// 协议,则不启动
if (binCmd == 'dev' && cmd == 'frontend' && cfg.protocol == 'file://') {
continue;
}
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + "Run " + chalk.green(`[${binCmd} ${cmd}]` + " command"));
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.green('config:'), JSON.stringify(cfg));
const execDir = path.join(process.cwd(), cfg.directory);
const execArgs = is.string(cfg.args) ? [cfg.args] : cfg.args;
const stdio = cfg.stdio ? cfg.stdio: 'inherit';
const handler = cfg.sync ? crossSpawn.sync : crossSpawn;
this.execProcess[cmd] = handler(
cfg.cmd,
execArgs,
{ stdio: stdio, cwd: execDir, maxBuffer: 1024 * 1024 * 1024 },
);
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + 'The ' + chalk.green(`[${binCmd} ${cmd}]`) + ` command is ${cfg.sync ? 'run completed' : 'running'}`);
if(!cfg.sync) {
this.execProcess[cmd].on('exit', () => {
if (cmd == 'electron') {
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + chalk.green('Press "CTRL+C" to exit'));
return
}
console.log(chalk.blue(`[ee-bin] [${binCmd}] `) + 'The ' + chalk.green(`[${binCmd} ${cmd}]`) + ' command has been executed and exited');
});
}
}
}
// esbuild
bundle(bundleConfig) {
const { bundleType } = bundleConfig;
if (bundleType == 'copy') {
const srcResource = path.join(process.cwd(), this.electronDir);
const destResource = path.join(process.cwd(), this.defaultBundleDir);
fsPro.removeSync(destResource);
fsPro.copySync(srcResource, destResource);
} else {
const esbuildOptions = bundleConfig[bundleConfig.type];
// todo 不压缩
// if (this.isDev()) {
// esbuildOptions.minify = false;
// }
debug('esbuild options:%O', esbuildOptions);
buildSync(esbuildOptions);
}
}
// format commands
_formatCmds(command) {
let cmds;
const cmdString = command.trim();
if (cmdString.indexOf(',') !== -1) {
cmds = cmdString.split(',');
} else {
cmds = [cmdString];
}
return cmds;
}
// env
isDev() {
return process.env.NODE_ENV === 'dev';
}
}
module.exports = {
ServeProcess,
serveProcess: new ServeProcess()
}

2
ee-core/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
package-lock.json

21
ee-core/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Wallace Gao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
ee-core/README.md Normal file
View File

@@ -0,0 +1,66 @@
### introduction
Powerful electron third party module, offering **100+ API**
[introduction](https://www.kaka996.com/pages/85c531/)
[中文文档](https://www.kaka996.com/pages/85c531/)
### module list
#### addon
plug-in module
#### bin
Command line module.
#### config
Configuration Module
#### const
Define the universal constants used in the framework.
#### controller
Controller module
#### ee
EE module
#### electron
The electric function encapsulated by the framework will provide APIs in this module.
#### exception
Capture exception modules.
#### httpclient
HTTP client.
#### jobs
Jobs module.
#### loader
loader module
#### log
log module
#### message
Message module. Sending messages between the main process and child processes.
#### ps
Process tool class module.
#### services
services module
#### socket
Provide socket communication function.
#### storage
Storage module. Provide JSON database and sqlite database.
#### tools
Script module. Encrypt and move resources.
#### utils
Tool library module.

5
ee-core/app/application.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export declare class Appliaction {
register(eventName: string, handler: Function): void;
run(): void;
}
export declare const app: Appliaction;

View File

@@ -0,0 +1,31 @@
'use strict';
const debug = require('debug')('ee-core:app:appliaction');
const { loadController } = require('../controller');
const { eventBus, Ready } = require('./events');
const { loadSocket } = require('../socket');
const { loadElectron } = require('../electron');
class Appliaction {
constructor() {
}
register(eventName, handler) {
return eventBus.register(eventName, handler);
}
run() {
loadController();
loadSocket();
eventBus.emitLifecycle(Ready);
loadElectron();
}
}
const app = new Appliaction();
module.exports = {
Appliaction,
app,
};

5
ee-core/app/boot.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export declare class ElectronEgg {
init(): void;
register(eventName: string, handler: Function): void;
run(): void;
}

74
ee-core/app/boot.js Normal file
View File

@@ -0,0 +1,74 @@
'use strict';
const debug = require('debug')('ee-core:app:boot');
const path = require('path');
const { loadException } = require('../exception');
const { electronApp } = require('../electron/app');
const { getArgumentByName, getBundleDir } = require('../ps');
const { loadConfig } = require('../config');
const { loadLog } = require('../log');
const { app } = require('./application');
const { loadDir } = require('./dir');
class ElectronEgg {
constructor() {
const baseDir = electronApp.getAppPath();
const { env } = process;
const environmet = getArgumentByName('env') || 'prod';
const options = {
env: environmet,
baseDir,
electronDir: getBundleDir(baseDir),
appName: electronApp.getName(),
userHome: electronApp.getPath('home'),
appData: electronApp.getPath('appData'),
appUserData: electronApp.getPath('userData'),
appVersion: electronApp.getVersion(),
isPackaged: electronApp.isPackaged,
execDir: baseDir,
}
// exec directory (exe dmg dep) for prod
if (environmet == 'prod' && options.isPackaged) {
options.execDir = path.dirname(electronApp.getPath('exe'));
}
// normalize env
env.EE_ENV = environmet;
env.EE_APP_NAME = options.appName;
env.EE_APP_VERSION = options.appVersion;
env.EE_BASE_DIR = options.baseDir;
env.EE_ELECTRON_DIR = options.electronDir;
env.EE_USER_HOME = options.userHome;
env.EE_APP_DATA = options.appData;
env.EE_APP_USER_DATA = options.appUserData;
env.EE_EXEC_DIR = options.execDir;
env.EE_IS_PACKAGED = options.isPackaged;
env.EE_SOCKET_PORT = null;
env.EE_HTTP_PORT = null;
debug('[constructor] options:%j', options)
this.init();
}
init() {
// basic functions
loadException();
loadConfig();
loadDir();
loadLog();
}
register(eventName, handler) {
return app.register(eventName, handler);
}
run() {
app.run();
}
}
module.exports = {
ElectronEgg,
};

1
ee-core/app/dir.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare function loadDir(): void;

27
ee-core/app/dir.js Normal file
View File

@@ -0,0 +1,27 @@
'use strict';
const fs = require('fs');
const { getUserHomeHiddenAppDir, getLogDir, getDataDir } = require('../ps');
const { mkdir } = require('../utils/helper');
function loadDir() {
initDir();
}
function initDir() {
const homeHiddenAppDir = getUserHomeHiddenAppDir();
if (!fs.existsSync(homeHiddenAppDir)) {
mkdir(homeHiddenAppDir, { mode: 0o755 });
}
const dataDir = getDataDir();
if (!fs.existsSync(dataDir)) {
mkdir(dataDir, { mode: 0o755 });
}
const logDir = getLogDir();
if (!fs.existsSync(logDir)) {
mkdir(logDir, { mode: 0o755 });
}
}
module.exports = {
loadDir
};

14
ee-core/app/events.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export declare class EventBus {
lifecycleEvents: {};
eventsMap: {};
register(eventName: string, handler: Function): void;
emitLifecycle(eventName: string, ...args: any[]): void;
on(eventName: string, handler: Function): void;
emit(eventName: string, ...args: any[]): void;
}
export declare const eventBus: EventBus;
export declare const Ready: "ready";
export declare const ElectronAppReady: "electron-app-ready";
export declare const WindowReady: "window-ready";
export declare const Preload: "preload";
export declare const BeforeClose: "before-close";

56
ee-core/app/events.js Normal file
View File

@@ -0,0 +1,56 @@
'use strict';
const Ready = "ready";
const ElectronAppReady = "electron-app-ready";
const WindowReady = "window-ready";
const BeforeClose = "before-close";
const Preload = "preload";
class EventBus {
constructor() {
this.lifecycleEvents = {};
this.eventsMap = {};
}
// add lifecycle event
register(eventName, handler) {
if (!this.lifecycleEvents[eventName]) {
this.lifecycleEvents[eventName] = handler;
}
}
// call lifecycle event
emitLifecycle(eventName, ...args) {
const eventFn = this.lifecycleEvents[eventName];
if (eventFn) {
eventFn(...args);
}
}
// add listener
on(eventName, handler) {
if (!this.eventsMap[eventName]) {
this.eventsMap[eventName] = handler;
}
}
// emit listener
emit(eventName, ...args) {
const eventFn = this.eventsMap[eventName];
if (eventFn) {
eventFn(...args);
}
}
}
const eventBus = new EventBus();
module.exports = {
EventBus,
eventBus,
Ready,
ElectronAppReady,
WindowReady,
Preload,
BeforeClose
};

2
ee-core/app/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { ElectronEgg };
import { ElectronEgg } from "./boot";

7
ee-core/app/index.js Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
const { ElectronEgg } = require('./boot');
module.exports = {
ElectronEgg,
};

13
ee-core/config/config_loader.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Timing } from "../core/utils/timing";
import { EEConfig } from "./default_config";
export declare class ConfigLoader {
timing: Timing;
/**
* Load config/config.xxx.js
*/
load(): EEConfig;
_AppConfig(): EEConfig;
_loadConfig(dirpath: string, filename: string): EEConfig;
}

View File

@@ -0,0 +1,66 @@
'use strict';
const debug = require('debug')('ee-core:config:config_loader');
const path = require('path');
const { appName, env, getElectronDir, getBaseDir, getRootDir } = require('../ps');
const { extend } = require('../utils/extend');
const { loadFile } = require('../loader');
const { Timing } = require('../core/utils/timing');
const defaultConfig = require('./default_config');
class ConfigLoader {
constructor() {
this.timing = new Timing();
}
/**
* Load config/config.xxx.js
*/
load() {
this.timing.start('Load Config');
// Load Application config
const appConfig = this._AppConfig();
// debug("[load] appConfig: %O", appConfig);
const defaultConf = defaultConfig();
const config = extend(true, defaultConf, appConfig);
debug("[load] config: %o", config);
this.timing.end('Load Config');
return config;
}
_AppConfig() {
const names = [
'config.default',
`config.${env()}`,
];
const target = {};
for (const filename of names) {
const config = this._loadConfig(getElectronDir(), filename);
extend(true, target, config);
}
return target;
}
_loadConfig(dirpath, filename) {
const appInfo = {
name: appName(),
baseDir: getBaseDir(),
electronDir: getElectronDir(),
env: env(),
root: getRootDir(),
}
const filepath = path.join(dirpath, 'config', filename);
const config = loadFile(filepath, appInfo);
debug("[_loadConfig] filepath: %s", filepath);
if (!config) return null;
return config;
}
}
module.exports = {
ConfigLoader
};

95
ee-core/config/default_config.d.ts vendored Normal file
View File

@@ -0,0 +1,95 @@
import { BrowserWindowConstructorOptions } from 'electron';
export declare interface AppConfig {
openDevTools?: boolean | Object;
singleLock?: boolean;
windowsOption?: BrowserWindowConstructorOptions;
logger?: LoggerConfig;
socketServer?: SocketConfig;
httpServer?: HttpConfig;
remote?: RemoteConfig;
mainServer?: MainConfig;
exception?: ExceptionConfig;
job?: JobConfig;
cross?: CrossConfig;
}
export declare interface LoggerConfig {
type?: string;
dir?: string;
encoding?: string;
env?: string;
level?: string;
consoleLevel?: string;
disableConsoleAfterReady?: boolean;
outputJSON?: boolean;
buffer?: boolean;
appLogName: string;
coreLogName?: string;
agentLogName?: string;
errorLogName: string;
coreLogger?: {};
allowDebugAtProd?: boolean;
enablePerformanceTimer?: boolean;
rotator?: string;
}
export declare interface SocketConfig {
enable: boolean;
port: number;
path?: string;
connectTimeout?: number;
pingTimeout?: number;
pingInterval?: number;
maxHttpBufferSize?: number;
transports?: string[];
cors?: {
origin?: boolean;
};
channel?: string;
}
export declare interface HttpConfig {
enable: boolean;
https?: {
enable: boolean;
key: string;
cert: string;
};
protocol?: string;
host?: string;
port: number;
cors?: {
origin?: string;
};
body?: {
multipart?: boolean;
formidable?: {
keepExtensions?: boolean;
};
};
filterRequest?: {
uris?: string[];
returnData?: string;
};
}
export declare interface RemoteConfig {
enable: boolean;
url: string;
}
export declare interface MainConfig {
protocol?: string;
indexPath: string;
options?: {};
takeover?: string;
loadingPage?: string;
channelSeparator?: string;
}
export declare interface ExceptionConfig {
mainExit?: boolean;
childExit?: boolean;
rendererExit?: boolean;
}
export declare interface JobConfig {
messageLog: boolean;
}
export declare interface CrossConfig {}
declare function config(): AppConfig;
export = config;

View File

@@ -0,0 +1,111 @@
'use strict';
const path = require('path');
const { env, getBaseDir, getLogDir } = require('../ps');
const { SocketIO } = require('../const/channel');
/**
* default
*/
module.exports = () => {
return {
openDevTools: false,
singleLock: true,
windowsOption: {
title: 'electron-egg',
width: 980,
height: 650,
minWidth: 400,
minHeight: 300,
webPreferences: {
//webSecurity: false,
contextIsolation: false, // false -> 可在渲染进程中使用electron的apitrue->需要bridge.js(contextBridge)
nodeIntegration: true,
//preload: path.join(appInfo.electronDir, 'preload', 'bridge.js'),
},
frame: true,
show: false,
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
},
logger: {
type: 'application',
dir: getLogDir(),
encoding: 'utf8',
env: env(),
level: 'INFO',
consoleLevel: 'INFO',
disableConsoleAfterReady: env() !== 'local',
outputJSON: false,
buffer: true,
appLogName: `ee.log`,
coreLogName: 'ee-core.log',
agentLogName: 'ee-agent.log',
errorLogName: `ee-error.log`,
coreLogger: {},
allowDebugAtProd: false,
enablePerformanceTimer: false,
rotator: 'day',
},
socketServer: {
enable: false, // is it enabled
port: 7070, // default port (if the port is in use, randomly select one)
path: "/socket.io/", // path
connectTimeout: 45000, // client connection timeout
pingTimeout: 30000, // heartbeat detection timeout
pingInterval: 25000, // heartbeat detection interval
maxHttpBufferSize: 1e8, // the data size of each message 1M
transports: ["polling", "websocket"], // http polling or websocket
cors: {
origin: true, // http协议时要设置跨域 类型 Boolean String RegExp Array Function
},
channel: SocketIO.partySoftware
},
httpServer: {
enable: false, // Is it enabled
https: {
enable: false,
key: '/public/ssl/localhost+1.key',
cert: '/public/ssl/localhost+1.pem'
},
protocol: 'http://',
host: '127.0.0.1',
port: 7071, // Default port (if the port is in use, randomly select one)
cors: {
origin: "*"
},
body: {
multipart: true,
formidable: {
keepExtensions: true
}
},
filterRequest: {
uris: [
'favicon.ico'
],
returnData: ''
}
},
remote: {
enable: false,
url: ''
},
mainServer: {
protocol: 'file://', // file://
indexPath: '/public/dist/index.html',
options: {},
takeover: '',
loadingPage: '',
channelSeparator: '/',
},
exception: {
mainExit: false,
childExit: false,
rendererExit: true,
},
jobs: {
messageLog: false
},
cross: {}
}
}

28
ee-core/config/index.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
import {
AppConfig,
WindowsConfig,
LoggerConfig,
SocketConfig,
HttpConfig,
RemoteConfig,
MainConfig,
ExceptionConfig,
JobConfig,
CrossConfig
} from "./default_config";
declare function loadConfig(): AppConfig;
declare function getConfig(): AppConfig;
export {
AppConfig,
WindowsConfig,
LoggerConfig,
SocketConfig,
HttpConfig,
RemoteConfig,
MainConfig,
ExceptionConfig,
JobConfig,
CrossConfig,
loadConfig,
getConfig
}

25
ee-core/config/index.js Normal file
View File

@@ -0,0 +1,25 @@
'use strict';
const { ConfigLoader } = require('./config_loader');
const Instance = {
config: null,
};
function loadConfig() {
const configLoader = new ConfigLoader();
Instance["config"] = configLoader.load();
return Instance["config"];
}
function getConfig() {
if (!Instance["config"]) {
loadConfig();
};
return Instance["config"];
}
module.exports = {
loadConfig,
getConfig,
};

16
ee-core/const/channel.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
export declare namespace Processes {
let showException: string;
let sendToMain: string;
}
export declare namespace SocketIO {
let partySoftware: string;
}
export declare namespace Events {
let childProcessExit: string;
let childProcessError: string;
}
export declare namespace Receiver {
let childJob: string;
let forkProcess: string;
let all: string;
}

28
ee-core/const/channel.js Normal file
View File

@@ -0,0 +1,28 @@
'use strict';
const Processes = {
showException: 'ee#showException',
sendToMain: 'ee#sendToMain'
}
const SocketIO = {
partySoftware: 'socket-channel',
}
const Events = {
childProcessExit: 'ee#childProcess#exit',
childProcessError: 'ee#childProcess#error',
}
const Receiver = {
childJob: 'job',
forkProcess: 'task',
all: 'all'
}
module.exports = {
Processes,
SocketIO,
Events,
Receiver
};

View File

@@ -0,0 +1,9 @@
import { Timing } from "../core/utils/timing";
export declare class ControllerLoader {
timing: Timing;
/**
* Load controller/xxx.js
*/
load(): any;
}

View File

@@ -0,0 +1,78 @@
'use strict';
const debug = require('debug')('ee-core:controller:controller_loader');
const path = require('path');
const is = require('is-type-of');
const { getElectronDir } = require('../ps');
const { Timing } = require('../core/utils/timing');
const { FileLoader, FULLPATH } = require('../core/loader/file_loader');
const { isBytecodeClass, callFn } = require('../core/utils');
class ControllerLoader {
constructor() {
this.timing = new Timing();
}
/**
* Load controller/xxx.js
*/
load() {
this.timing.start('Load Controller');
const opt = {
caseStyle: 'lower',
directory: path.join(getElectronDir(), 'controller'),
initializer: (obj, opt) => {
if (is.class(obj) || isBytecodeClass(obj)) {
obj.prototype.pathName = opt.pathName;
obj.prototype.fullPath = opt.path;
return wrapClass(obj);
}
return obj;
},
};
const target = new FileLoader(opt).load();
debug("[load] controllers: %o", target);
this.timing.end('Load Controller');
return target;
}
}
// wrap the class, yield a object with middlewares
function wrapClass(Controller) {
let proto = Controller.prototype;
const ret = {};
// tracing the prototype chain
while (proto !== Object.prototype) {
const keys = Object.getOwnPropertyNames(proto);
// debug("[wrapClass] keys:", keys);
for (const key of keys) {
// getOwnPropertyNames will return constructor
// that should be ignored
if (key === 'constructor') {
continue;
}
// skip getter, setter & non-function properties
const d = Object.getOwnPropertyDescriptor(proto, key);
// prevent to override sub method
if (is.function(d.value) && !ret.hasOwnProperty(key)) {
ret[key] = methodToMiddleware(Controller, key);
ret[key][FULLPATH] = Controller.prototype.fullPath + '#' + Controller.name + '.' + key + '()';
}
}
proto = Object.getPrototypeOf(proto);
}
return ret;
}
function methodToMiddleware(Controller, key) {
return function classControllerMiddleware(...args) {
const controller = new Controller();
return callFn(controller[key], args, controller);
};
}
module.exports = {
ControllerLoader
};

2
ee-core/controller/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare function loadController(): void;
export declare function getController(): any;

View File

@@ -0,0 +1,24 @@
'use strict'
const { ControllerLoader } = require('./controller_loader');
const Instance = {
controller: null,
};
function loadController() {
const controllerLoader = new ControllerLoader();
Instance.controller = controllerLoader.load();
}
function getController() {
if (!Instance.controller) {
loadController();
}
return Instance.controller;
}
module.exports = {
loadController,
getController
};

2
ee-core/core/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
import utils = require("./utils");
export { EeLoader, BaseContextClass, utils };

12
ee-core/core/index.js Normal file
View File

@@ -0,0 +1,12 @@
'use strict';
const EeLoader = require('./loader/ee_loader');
const BaseContextClass = require('./utils/base_context_class');
const utils = require('./utils');
module.exports = {
EeLoader,
BaseContextClass,
utils,
};

66
ee-core/core/loader/file_loader.d.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
/**
* Load files from directory to target object.
*/
export declare class FileLoader {
/**
* @class
* @param {Object} options - options
* @param {String|Array} options.directory - directories to be loaded
* @param {Object} options.target - attach the target object from loaded files
* @param {String} options.match - match the files when load, support glob, default to all js files
* @param {Function} options.initializer - custom file exports, receive two parameters, first is the inject object(if not js file, will be content buffer), second is an `options` object that contain `path`
* @param {Boolean} options.call - determine whether invoke when exports is function
* @param {Object} options.inject - an object that be the argument when invoke the function
* @param {String|Function} options.caseStyle - set property's case when converting a filepath to property list.
*/
constructor(options: {
directory: string | any[];
target: any;
match: string;
initializer: Function;
call: boolean;
inject: any;
caseStyle: string | Function;
});
options: {
directory: any;
target: any;
match: any;
caseStyle: string;
initializer: any;
call: boolean;
inject: any;
} & {
directory: string | any[];
target: any;
match: string;
initializer: Function;
call: boolean;
inject: any;
caseStyle: string | Function;
};
/**
* attach items to target object. Mapping the directory to properties.
* `xxx/group/repository.js` => `target.group.repository`
* @return {Object} target
*/
load(): any;
/**
* Parse files from given directories, then return an items list, each item contains properties and exports.
* For example, parse `controller/group/repository.js`
* It returns a item
* ```
* {
* fullpath: '',
* properties: [ 'group', 'repository' ],
* exports: { ... },
* }
* ```
* `Properties` is an array that contains the directory of a filepath.
* `Exports` depends on type, if exports is a function, it will be called. if initializer is specified, it will be called with exports for customizing.
* @return {Array} items
*/
parse(): any[];
}
export declare const EXPORTS: unique symbol;
export declare const FULLPATH: unique symbol;

View File

@@ -0,0 +1,212 @@
'use strict';
const debug = require('debug')('ee-core:core:loader:file_loader');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const globby = require('globby');
const is = require('is-type-of');
const { isBytecodeClass, loadFile, filePatterns } = require('../utils');
const FULLPATH = Symbol('LOADER_ITEM_FULLPATH');
const EXPORTS = Symbol('LOADER_ITEM_EXPORTS');
const defaults = {
directory: null,
target: null,
match: undefined,
caseStyle: 'camel',
initializer: null,
call: true,
inject: undefined,
};
/**
* Load files from directory to target object.
*/
class FileLoader {
/**
* @class
* @param {Object} options - options
* @param {String|Array} options.directory - directories to be loaded
* @param {Object} options.target - attach the target object from loaded files
* @param {String} options.match - match the files when load, support glob, default to all js files
* @param {Function} options.initializer - custom file exports, receive two parameters, first is the inject object(if not js file, will be content buffer), second is an `options` object that contain `path`
* @param {Boolean} options.call - determine whether invoke when exports is function
* @param {Object} options.inject - an object that be the argument when invoke the function
* @param {String|Function} options.caseStyle - set property's case when converting a filepath to property list.
*/
constructor(options) {
assert(options.directory, 'options.directory is required');
this.options = Object.assign({}, defaults, options);
debug("[constructor] options: %o", this.options);
}
/**
* attach items to target object. Mapping the directory to properties.
* `xxx/group/repository.js` => `target.group.repository`
* @return {Object} target
*/
load() {
const items = this.parse();
const target = {};
for (const item of items) {
// item { fullpath, properties: [ 'a', 'b', 'c'], exports }
item.properties.reduce((target, property, index) => {
let obj;
// properties is a path slice, only the last value is the file name
if (index === item.properties.length - 1) {
obj = item.exports;
if (obj && !is.primitive(obj)) {
obj[FULLPATH] = item.fullpath;
obj[EXPORTS] = true;
}
} else {
obj = target[property] || {};
}
target[property] = obj;
// const properties = item.properties.slice(0, index + 1).join('.');
// debug('[load] properties: %s', properties);
return obj;
}, target);
}
//debug('[load] target: %O', target);
return target;
}
/**
* Parse files from given directories, then return an items list, each item contains properties and exports.
* For example, parse `controller/group/repository.js`
* It returns a item
* ```
* {
* fullpath: '',
* properties: [ 'group', 'repository' ],
* exports: { ... },
* }
* ```
* `Properties` is an array that contains the directory of a filepath.
* `Exports` depends on type, if exports is a function, it will be called. if initializer is specified, it will be called with exports for customizing.
* @return {Array} items
*/
parse() {
let files = this.options.match;
if (!files) {
files = filePatterns();
} else {
files = Array.isArray(files) ? files : [ files ];
}
let directories = this.options.directory;
if (!Array.isArray(directories)) {
directories = [ directories ];
}
const items = [];
debug('[parse] directories %o', directories);
for (const directory of directories) {
const filepaths = globby.sync(files, { cwd: directory });
debug('[parse] filepaths %o', filepaths);
for (const filepath of filepaths) {
const fullpath = path.join(directory, filepath);
if (!fs.statSync(fullpath).isFile()) continue;
// get properties
// controller/foo/bar.js => [ 'foo', 'bar' ]
const properties = getProperties(filepath, this.options);
// debug('[parse] properties %o', properties);
// controller/foo/bar.js => controller.foo.bar
const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
// debug('[parse] pathName %s', pathName);
// get exports from the file
let exports = getExports(fullpath, this.options, pathName);
// ignore exports when it's null or false returned by filter function
if (exports == null) continue;
// set properties of class
if (is.class(exports) || isBytecodeClass(exports)) {
exports.prototype.pathName = pathName;
exports.prototype.fullPath = fullpath;
}
items.push({ fullpath, properties, exports });
//debug('[parse] fullpath %s, properties %o, export %o', fullpath, properties, exports);
}
}
//debug('[parse] items %O', items);
return items;
}
}
// convert file path to an array of properties
// a/b/c.js => ['a', 'b', 'c']
function getProperties(filepath, { caseStyle }) {
// if caseStyle is function, return the result of function
if (is.function(caseStyle)) {
const result = caseStyle(filepath);
assert(is.array(result), `caseStyle expect an array, but got ${result}`);
return result;
}
// use default camelize
return defaultCamelize(filepath, caseStyle);
}
// Get exports from filepath
// If exports is null/undefined, it will be ignored
function getExports(fullpath, { initializer, call, inject }, pathName) {
let exports = loadFile(fullpath);
//debug('[getExports] exports %o', exports);
if (initializer) {
// exports type is Class or Object
exports = initializer(exports, { path: fullpath, pathName });
}
if (is.class(exports) || is.generatorFunction(exports) || is.asyncFunction(exports) || isBytecodeClass(exports)) {
return exports;
}
// whether to execute the function
if (call && is.function(exports)) {
exports = exports(inject);
if (exports != null) {
return exports;
}
}
return exports;
}
function defaultCamelize(filepath, caseStyle) {
const properties = filepath.substring(0, filepath.lastIndexOf('.')).split('/');
return properties.map(property => {
if (!/^[a-z][a-z0-9_-]*$/i.test(property)) {
throw new Error(`${property} is not match 'a-z0-9_-' in ${filepath}`);
}
// use default camelize, will capitalize the first letter
// foo_bar.js > FooBar
// fooBar.js > FooBar
// FooBar.js > FooBar
// FooBar.js > FooBar
// FooBar.js > fooBar
property = property.replace(/[_-][a-z]/ig, s => s.substring(1).toUpperCase());
let first = property[0];
switch (caseStyle) {
case 'lower':
first = first.toLowerCase();
break;
case 'upper':
first = first.toUpperCase();
break;
case 'camel':
default:
}
return first + property.substring(1);
});
}
module.exports = {
FileLoader,
EXPORTS,
FULLPATH,
};

12
ee-core/core/utils/index.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
export declare const extensions: any;
export declare function loadFile(filepath: any): any;
export declare function callFn(fn: any, args: any, ctx: any): Promise<any>;
export declare function getResolvedFilename(filepath: any, baseDir: any): any;
/**
* 字节码类
*/
export declare function isBytecodeClass(exports: any): boolean;
/**
* 文件类型
*/
export declare function filePatterns(): string[];

View File

@@ -0,0 +1,83 @@
'use strict';
require('bytenode');
const is = require('is-type-of');
const path = require('path');
const fs = require('fs');
const BuiltinModule = require('module');
// Guard against poorly mocked module constructors.
const Module = module.constructor.length > 1
? module.constructor
/* istanbul ignore next */
: BuiltinModule;
// Module._extensions:
// '.js': [Function (anonymous)],
// '.json': [Function (anonymous)],
// '.node': [Function: func],
// '.jsc': [Function (anonymous)]
const extensions = Module._extensions;
function loadFile(filepath) {
try {
// if not js module, just return content buffer
const extname = path.extname(filepath);
if (extname && !Module._extensions[extname]) {
return fs.readFileSync(filepath);
}
// require js module
const obj = require(filepath);
if (!obj) return obj;
// it's es module
if (obj.__esModule) return 'default' in obj ? obj.default : obj;
return obj;
} catch (err) {
err.message = `[ee-core] load file: ${filepath}, error: ${err.message}`;
throw err;
}
}
async function callFn(fn, args, ctx) {
args = args || [];
if (!is.function(fn)) return;
return ctx ? fn.call(ctx, ...args) : fn(...args);
}
function getResolvedFilename(filepath, baseDir) {
const reg = /[/\\]/g;
return filepath.replace(baseDir + path.sep, '').replace(reg, '/');
}
/**
* 字节码类
*/
function isBytecodeClass(exports) {
let isClass = false;
// 标识
if (exports.toString().indexOf('[class') != -1) {
isClass = true;
}
return isClass;
}
/**
* 文件类型
*/
function filePatterns() {
const files = [ '**/*.js','**/*.jsc' ];
return files;
}
module.exports = {
extensions,
loadFile,
callFn,
getResolvedFilename,
isBytecodeClass,
filePatterns,
};

22
ee-core/core/utils/timing.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
export declare class Timing {
_enable: boolean;
init(): void;
start(name: any, start: any): {
name: any;
start: any;
end: any;
duration: any;
pid: number;
index: number;
};
end(name: any): any;
enable(): void;
disable(): void;
clear(): void;
toJSON(): any[];
[MAP]: Map<any, any>;
[LIST]: any[];
}
declare const MAP: unique symbol;
declare const LIST: unique symbol;
export {};

View File

@@ -0,0 +1,78 @@
'use strict';
const assert = require('assert');
const MAP = Symbol('Timing#map');
const LIST = Symbol('Timing#list');
class Timing {
constructor() {
this._enable = true;
this[MAP] = new Map();
this[LIST] = [];
this.init();
}
init() {
// process start time
this.start('Process Start', Date.now() - Math.floor((process.uptime() * 1000)));
this.end('Process Start');
if (typeof process.scriptStartTime === 'number') {
// js script start execute time
this.start('Script Start', process.scriptStartTime);
this.end('Script Start');
}
}
start(name, start) {
if (!name || !this._enable) return;
if (this[MAP].has(name)) this.end(name);
start = start || Date.now();
const item = {
name,
start,
end: undefined,
duration: undefined,
pid: process.pid,
index: this[LIST].length,
};
this[MAP].set(name, item);
this[LIST].push(item);
return item;
}
end(name) {
if (!name || !this._enable) return;
assert(this[MAP].has(name), `should run timing.start('${name}') first`);
const item = this[MAP].get(name);
item.end = Date.now();
item.duration = item.end - item.start;
return item;
}
enable() {
this._enable = true;
}
disable() {
this._enable = false;
}
clear() {
this[MAP].clear();
this[LIST] = [];
}
toJSON() {
return this[LIST];
}
}
module.exports = {
Timing
};

19
ee-core/cross/cross.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import { EventEmitter } from 'node:events';
import { CrossProcess } from "./crossProcess";
import internal = require("stream");
export declare class Cross {
emitter: EventEmitter<[never]>;
children: {};
childrenMap: {};
create(): Promise<void>;
run(service: string, opt?: {}): Promise<CrossProcess>;
killAll(): void;
kill(pid: string|number): void;
killByName(name: string): void;
getUrl(name: string): string;
getProcByName(name: string): CrossProcess;
getProc(pid: string|number): CrossProcess;
getPids(): string[];
_initEventEmitter(): void;
}
export declare let cross: Cross;

152
ee-core/cross/cross.js Normal file
View File

@@ -0,0 +1,152 @@
'use strict';
const EventEmitter = require('events');
const { getConfig } = require('../config');
const { sleep, getValueFromArgv, replaceArgsValue } = require('../utils/helper');
const { CrossProcess } = require('./crossProcess');
const { Events } = require('../const/channel');
const { extend } = require('../utils/extend');
const { getPort } = require('../utils/port');
class Cross {
constructor() {
this.emitter = undefined;
// pid唯一
// {pid:{name,entity}, pid:{name,entity}, ...}
this.children = {};
// name唯一
// {name:pid, name:pid, ...}
this.childrenMap = {};
// eventEmitter
this._initEventEmitter();
}
async create() {
// boot services
const crossCfg = getConfig().cross;
//await sleep(5 * 1000);
for (let key of Object.keys(crossCfg)) {
let val = crossCfg[key];
if (val.enable == true) {
this.run(key)
}
}
}
// run
async run(service, opt = {}) {
const crossConf = getConfig().cross;
const defaultOpt = crossConf[service] || {};
const targetConf = extend(true, {}, defaultOpt, opt);
if (Object.keys(targetConf).length == 0) {
throw new Error(`[ee-core] [cross] The service [${service}] config does not exit`);
}
// format params
let tmpArgs = targetConf.args;
let confPort = parseInt(getValueFromArgv(tmpArgs, 'port'));
// 某些程序给它传入不存在的参数时会报错
if (isNaN(confPort) && targetConf.port > 0) {
confPort = targetConf.port;
}
if (confPort > 0) {
// 动态生成port传入的端口必须为int
confPort = await getPort({ port: confPort });
// 替换port
targetConf.args = replaceArgsValue(tmpArgs, "port", String(confPort));
}
// 创建进程
const subProcess = new CrossProcess(this, { targetConf, port: confPort });
let uniqueName = targetConf.name;
if (this.childrenMap.hasOwnProperty(uniqueName)) {
uniqueName = uniqueName + "-" + String(subProcess.pid);
}
this.childrenMap[uniqueName] = subProcess.pid;
subProcess.name = uniqueName;
this.children[subProcess.pid] = {
name: uniqueName,
entity: subProcess
};
return subProcess;
}
killAll() {
Object.keys(this.children).forEach(pid => {
this.kill(pid)
});
}
kill(pid) {
const entity = this.getProc(pid);
if (entity) {
entity.kill();
}
}
killByName(name) {
const entity = this.getProcByName(name);
if (entity) {
entity.kill();
}
}
getUrl(name) {
const entity = this.getProcByName(name);
const url = entity.getUrl();
return url;
}
// 获取 proc
getProcByName(name) {
const pid = this.childrenMap[name];
if (!pid) {
throw new Error(`[ee-core] [cross] The process named [${name}] does not exit`);
}
const entity = this.getProc(pid);
return entity;
}
// 获取 proc
getProc(pid) {
const child = this.children[pid];
if (!pid) {
throw new Error(`[ee-core] [cross] The process pid [${pid}] does not exit`);
}
return child.entity;
}
// 获取pids
getPids() {
let pids = Object.keys(this.children);
return pids;
}
_initEventEmitter() {
this.emitter = new EventEmitter();
this.emitter.on(Events.childProcessExit, (data) => {
const child = this.children[data.pid];
delete this.childrenMap[child.name];
delete this.children[data.pid];
});
this.emitter.on(Events.childProcessError, (data) => {
const child = this.children[data.pid];
delete this.childrenMap[child.name];
delete this.children[data.pid];
});
}
}
module.exports = {
Cross,
cross: new Cross()
};

29
ee-core/cross/crossProcess.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
import EventEmitter = require("events");
export declare class CrossProcess {
constructor(host: any, opt?: {});
emitter: EventEmitter<[never]>;
host: any;
child: any;
pid: number;
port: number;
name: string;
config: {};
/**
* 初始化子进程
*/
_init(options?: {}): void;
/**
* kill
*/
kill(timeout?: number): void;
getUrl(): string;
getArgsObj(): {
_: any[];
};
setPort(port: string|number): void;
_generateId(): string;
/**
* exit electron
*/
_exitElectron(timeout?: number): void;
}

View File

@@ -0,0 +1,168 @@
'use strict';
const EventEmitter = require('events');
const path = require('path');
const crossSpawn = require('cross-spawn');
const { coreLogger } = require('../log');
const { getExtraResourcesDir, isPackaged, isDev, getBaseDir } = require('../ps');
const { Events } = require('../const/channel');
const { getRandomString, getValueFromArgv } = require('../utils/helper');
const { is } = require('../utils');
const { parseArgv } = require('../utils/pargv');
const { app: electronApp } = require('electron');
class CrossProcess {
constructor(host, opt = {}) {
this.emitter = new EventEmitter();
this.host = host;
this.child = undefined;
this.pid = 0;
this.port = 0;
this.name = "";
this.config = {};
this._init(opt);
}
/**
* 初始化子进程
*/
_init(options = {}) {
const { targetConf, port } = options;
this.config = targetConf;
this.port = port;
// 该名称如果在childrenMap重复会被重写
this.name = targetConf.name;
// Launch executable program
let cmdPath = '';
let cmdArgs = targetConf.args;
let execDir = getExtraResourcesDir();
let standardOutput = ['inherit', 'inherit', 'inherit', 'ipc'];
if (isPackaged()) {
standardOutput = ['ignore', 'ignore', 'ignore', 'ipc'];
}
if (targetConf.stdio) {
standardOutput = targetConf.stdio;
}
const { cmd, directory } = targetConf;
// use cmd first
if (cmd) {
if (!directory) {
throw new Error(`[ee-core] [cross] The config [directory] attribute does not exist`);
}
cmdPath = cmd;
if (!path.isAbsolute(cmd) && !isDev()) {
cmdPath = path.join(getExtraResourcesDir(), cmd);
}
} else {
cmdPath = path.join(getExtraResourcesDir(), targetConf.name);
}
// windows
if (is.windows() && path.extname(cmdPath) != '.exe') {
// Complete the executable program extension
// notice: python.exe may bring up the App Store
if (targetConf.windowsExtname === true || !isDev()) {
cmdPath += ".exe";
}
}
// executable program directory
if (directory && path.isAbsolute(directory)) {
execDir = directory;
} else if (directory && !path.isAbsolute(directory)) {
if (isDev()) {
execDir = path.join(getBaseDir(), directory);
} else {
execDir = path.join(getExtraResourcesDir(), directory);
}
} else {
execDir = getExtraResourcesDir();
}
coreLogger.info(`[ee-core] [cross/run] cmd: ${cmdPath}, args: ${cmdArgs}`);
const coreProcess = crossSpawn(cmdPath, cmdArgs, {
stdio: standardOutput,
detached: false,
cwd: execDir,
maxBuffer: 1024 * 1024 * 1024
});
this.child = coreProcess;
this.pid = coreProcess.pid;
coreProcess.on('exit', (code, signal) => {
let data = {
pid: this.pid
}
this.host.emitter.emit(Events.childProcessExit, data);
// Child process closed: The child process was killed externally or an internal error caused the application to stop, resulting in the application exiting
coreLogger.info(`[ee-core] [corss/process] received a exit from child-process, code:${code}, signal:${signal}, pid:${this.pid}, cmd:${cmdPath}, args: ${cmdArgs}`);
this._exitElectron();
});
coreProcess.on('error', (err) => {
let data = {
pid: this.pid
}
this.host.emitter.emit(Events.childProcessError, data);
coreLogger.error(`[ee-core] [corss/process] received a error from child-process, error: ${err}, pid:${this.pid}`);
this._exitElectron();
});
}
/**
* kill
*/
kill(timeout = 1000) {
this.child.kill('SIGINT');
setTimeout(() => {
if (this.child.killed) return;
this.child.kill('SIGKILL');
}, timeout)
}
getUrl() {
const ssl = getValueFromArgv(this.config.args, 'ssl');
let hostname = getValueFromArgv(this.config.args, 'hostname')
let protocol = 'http://';
if (ssl && (ssl == 'true' || ssl == '1')) {
protocol = 'https://';
}
hostname = hostname ? hostname : '127.0.0.1';
const url = protocol + hostname + ":" + this.port;
return url;
}
getArgsObj() {
const obj = parseArgv(this.config.args);
return obj;
}
setPort(port) {
this.port = parseInt(port);
}
_generateId() {
const rid = getRandomString();
return `node:${this.pid}:${rid}`;
}
/**
* exit electron
*/
_exitElectron(timeout = 1000) {
if (this.config.appExit) {
setTimeout(() => {
// 主进程退出
electronApp.quit();
}, timeout)
}
}
}
module.exports = {
CrossProcess
};

2
ee-core/cross/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
import { Cross, cross } from "./cross";
export { Cross, cross };

8
ee-core/cross/index.js Normal file
View File

@@ -0,0 +1,8 @@
'use strict';
const { Cross, cross} = require('./cross');
module.exports = {
Cross,
cross
};

5
ee-core/electron/app/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/**
* 创建electron应用
*/
export declare function createElectron(): void;
export { electronApp };

View File

@@ -0,0 +1,48 @@
'use strict';
const debug = require('debug')('ee-core:electron:app');
const { app: electronApp } = require('electron');
const { coreLogger } = require('../../log');
const { is } = require('../../utils');
const { cross } = require('../../cross');
const { createMainWindow, setCloseAndQuit, loadServer } = require('../window');
const { eventBus, ElectronAppReady, BeforeClose, Preload } = require('../../app/events');
const { getConfig } = require('../../config');
/**
* 创建electron应用
*/
function createElectron() {
const { singleLock } = getConfig();
// 允许多个实例
const gotTheLock = electronApp.requestSingleInstanceLock();
if (singleLock && !gotTheLock) {
electronApp.quit();
}
electronApp.whenReady().then(() => {
createMainWindow();
eventBus.emitLifecycle(Preload);
loadServer();
})
electronApp.on('window-all-closed', () => {
if (!is.macOS()) {
coreLogger.info('[ee-core] [lib/eeApp] window-all-closed quit');
electronApp.quit();
}
})
electronApp.on('before-quit', () => {
setCloseAndQuit(true);
eventBus.emitLifecycle(BeforeClose);
cross.killAll();
})
eventBus.emitLifecycle(ElectronAppReady);
}
module.exports = {
electronApp,
createElectron,
};

3
ee-core/electron/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import { getMainWindow, setCloseAndQuit, getCloseAndQuit } from "./window";
export declare function loadElectron(): void;
export { getMainWindow, setCloseAndQuit, getCloseAndQuit };

16
ee-core/electron/index.js Normal file
View File

@@ -0,0 +1,16 @@
'use strict';
const { createElectron } = require("./app");
const { getMainWindow, setCloseAndQuit, getCloseAndQuit } = require("./window");
// load socket server
function loadElectron() {
createElectron();
}
module.exports = {
loadElectron,
getMainWindow,
setCloseAndQuit,
getCloseAndQuit
};

6
ee-core/electron/window/index.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export declare function getMainWindow(): any;
export declare function createMainWindow(): any;
export declare function restoreMainWindow(): void;
export declare function setCloseAndQuit(flag: any): void;
export declare function getCloseAndQuit(): boolean;
export declare function loadServer(): Promise<void>;

View File

@@ -0,0 +1,269 @@
'use strict';
const debug = require('debug')('ee-core:electron:window');
const is = require('is-type-of');
const path = require('path');
const axios = require('axios');
const { BrowserWindow } = require('electron');
const { getConfig } = require('../../config');
const { eventBus, WindowReady } = require('../../app/events');
const { env, isDev, getBaseDir } = require('../../ps');
const { loadFile } = require('../../loader');
const { isFileProtocol } = require('../../utils');
const { getHtmlFilepath } = require('../../html');
const { fileIsExist, sleep } = require('../../utils/helper');
const { coreLogger } = require('../../log');
const { extend } = require('../../utils/extend');
const { cross } = require('../../cross');
const Instance = {
mainWindow: null,
closeAndQuit: true,
};
// getMainWindow
function getMainWindow() {
return Instance.mainWindow;
}
// Create the main application window
function createMainWindow() {
const { openDevTools, windowsOption } = getConfig();
const win = new BrowserWindow(windowsOption);
Instance.mainWindow = win;
// DevTools
if (is.object(openDevTools)) {
win.webContents.openDevTools(openDevTools);
} else if (openDevTools === true) {
win.webContents.openDevTools({
mode: 'bottom'
});
}
eventBus.emitLifecycle(WindowReady);
return win;
}
// restored window
function restoreMainWindow() {
if (Instance.mainWindow) {
if (Instance.mainWindow.isMinimized()) {
Instance.mainWindow.restore();
}
Instance.mainWindow.show();
Instance.mainWindow.focus();
}
}
// Set the flag for exiting after close all windows
function setCloseAndQuit(flag) {
Instance.closeAndQuit = flag;
}
function getCloseAndQuit() {
return Instance.closeAndQuit;
}
// load server
// type: remote | single
async function loadServer() {
let type = 'spa';
let url = '';
const { remote, mainServer } = getConfig();
const win = getMainWindow();
// remote model
if (remote.enable == true) {
type = 'remote';
url = remote.url;
loadMainUrl(type, url);
return;
}
// 开发环境
if (isDev()) {
let url;
let load = 'url';
const binFile = path.join(getBaseDir(), "./cmd/bin.js");
const binConfig = loadFile(binFile);
const { dev } = binConfig;
// tips: match with ee-bin
const frontendConf = extend(true, {
protocol: 'http://',
hostname: 'localhost',
port: 8080,
indexPath: 'index.html',
}, dev.frontend);
const electronConf = extend(true, {
loadingPage: '/public/html/loading.html',
}, dev.electron);
url = frontendConf.protocol + frontendConf.hostname + ':' + frontendConf.port;
if (isFileProtocol(frontendConf.protocol)) {
url = path.join(getBaseDir(), frontendConf.directory, frontendConf.indexPath);
load = 'file';
}
// Check if UI serve is started, load a boot page first
if (load == 'url') {
// loading page
let lp = getHtmlFilepath('boot.html');
if (electronConf.hasOwnProperty('loadingPage') && electronConf.loadingPage != '') {
lp = path.join(getBaseDir(), electronConf.loadingPage);
}
_loadingPage(lp);
// check frontend is ready
const retryTimes = frontendConf.force === true ? 3 : 60;
let count = 0;
let frontendReady = false;
while(!frontendReady && count < retryTimes){
await sleep(1 * 1000);
try {
await axios({
method: 'get',
url,
timeout: 1000,
proxy: false,
headers: {
'Accept': 'text/html, application/json, text/plain, */*',
},
//responseType: 'text',
});
frontendReady = true;
} catch(err) {
// console.warn(err.stack)
}
count++;
}
debug('it takes %d seconds to start the frontend', count);
if (frontendReady == false && frontendConf.force !== true) {
const bootFailurePage = getHtmlFilepath('failure.html');
win.loadFile(bootFailurePage);
coreLogger.error(`[ee-core] Please check the ${url} !`);
return;
}
}
loadMainUrl(type, url, load);
return;
}
// 生产环境
// cross takeover web
if (mainServer.takeover.length > 0) {
await crossTakeover()
return
}
// 主进程
url = path.join(getBaseDir(), mainServer.indexPath);
loadMainUrl(type, url, 'file');
}
/**
* 主服务
* @params load <string> value: "url" 、 "file"
*/
function loadMainUrl(type, url, load = 'url') {
const { mainServer } = getConfig();
const mainWindow = getMainWindow();
coreLogger.info('[ee-core] Env: %s, Type: %s', env(), type);
coreLogger.info('[ee-core] App running at: %s', url);
if (load == 'file') {
mainWindow.loadFile(url, mainServer.options)
.then()
.catch((err)=>{
coreLogger.error(`[ee-core] Please check the ${url} !`);
});
} else {
mainWindow.loadURL(url, mainServer.options)
.then()
.catch((err)=>{
coreLogger.error(`[ee-core] Please check the ${url} !`);
});
}
}
// loading page
function _loadingPage(name) {
if (!fileIsExist(name)) {
return
}
const win = getMainWindow();
win.loadFile(name);
}
/**
* cross takeover web
*/
async function crossTakeover() {
const crossConf = getConfig().cross;
const mainConf = getConfig().mainServer;
// loading page
if (mainConf.loadingPage.length > 0) {
const lp = path.join(getBaseDir, mainConf.loadingPage);
_loadingPage(lp);
}
// cross service url
const service = mainConf.takeover;
if (!crossConf.hasOwnProperty(service)) {
throw new Error(`[ee-core] Please Check the value of mainServer.takeover in the config file !`);
}
// check service
if (crossConf[service].enable != true) {
throw new Error(`[ee-core] Please Check the value of cross.${service} enable is true !`);
}
const entityName = crossConf[service].name;
const url = cross.getUrl(entityName);
// 循环检查
let count = 0;
let serviceReady = false;
const times = isDev() ? 20 : 100;
const sleeptime = isDev() ? 1000 : 200;
while(!serviceReady && count < times){
await sleep(sleeptime);
try {
await axios({
method: 'get',
url,
timeout: 100,
proxy: false,
headers: {
'Accept': 'text/html, application/json, text/plain, */*',
},
});
serviceReady = true;
} catch(err) {
// console.warn(err.stack)
}
count++;
}
debug('it takes %d seconds to start the cross serivce', count * sleeptime);
if (serviceReady == false) {
const bootFailurePage = getHtmlFilepath('cross-failure.html');
const mainWindow = getMainWindow();
mainWindow.loadFile(bootFailurePage);
throw new Error(`[ee-core] Please check cross service [${service}] ${url} !`)
}
coreLogger.info(`[ee-core] cross service [${service}] is started successfully`);
loadMainUrl('spa', url);
}
module.exports = {
getMainWindow,
createMainWindow,
restoreMainWindow,
setCloseAndQuit,
getCloseAndQuit,
loadServer
};

9
ee-core/exception/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// Capture exceptions
export declare function loadException(): void;
// When an exception is thrown on a process without being caught, trigger the event and silence the exception
export declare function uncaughtExceptionHandler(): void;
// When the reject exception in the promise is not caught using catch in the synchronization task, it will trigger the event
// Even if catch is used in asynchronous situations, it will trigger the event
export declare function unhandledRejectionHandler(): void;
// This event is triggered when an exception is thrown on the process without being caught.
export declare function uncaughtExceptionMonitorHandler(): void;

101
ee-core/exception/index.js Normal file
View File

@@ -0,0 +1,101 @@
'use strict';
const { coreLogger } = require('../log');
const { isForkedChild, isRenderer, isDev, isMain } = require('../ps');
const { getConfig } = require('../config');
const { childMessage } = require('../message');
// 捕获异常
function loadException() {
uncaughtExceptionHandler();
unhandledRejectionHandler();
}
// 当进程上抛出异常而没有被捕获时触发该事件,并且使异常静默。
function uncaughtExceptionHandler() {
process.on('uncaughtException', function(err) {
if (!(err instanceof Error)) {
err = new Error(String(err));
}
if (err.name === 'Error') {
err.name = 'unhandledExceptionError';
}
coreLogger.error(err);
_devError(err);
_exit();
});
}
// 当进程上抛出异常而没有被捕获时触发该事件。
function uncaughtExceptionMonitorHandler() {
process.on('uncaughtExceptionMonitor', function(err, origin) {
if (!(err instanceof Error)) {
err = new Error(String(err));
}
coreLogger.error('uncaughtExceptionMonitor:',err);
});
}
// 当promise中reject的异常在同步任务中没有使用catch捕获就会触发该事件
// 即便是在异步情况下使用了catch也会触发该事件
function unhandledRejectionHandler() {
process.on('unhandledRejection', function(err) {
if (!(err instanceof Error)) {
const newError = new Error(String(err));
// err maybe an object, try to copy the name, message and stack to the new error instance
if (err) {
if (err.name) newError.name = err.name;
if (err.message) newError.message = err.message;
if (err.stack) newError.stack = err.stack;
}
err = newError;
}
if (err.name === 'Error') {
err.name = 'unhandledRejectionError';
}
coreLogger.error(err);
_devError(err);
_exit();
});
}
// 如果是子进程,发送错误到主进程控制台
function _devError (err) {
if (isForkedChild() && isDev()) {
childMessage.sendErrorToTerminal(err);
}
}
// 捕获异常后是否退出
function _exit () {
const { mainExit, childExit, rendererExit } = getConfig().exception;
if (isMain() && mainExit == true) {
_delayExit();
} else if (isForkedChild() && childExit == true) {
_delayExit();
} else if (isRenderer() && rendererExit == true) {
_delayExit();
} else {
// other
}
}
// 捕获异常后是否退出
function _delayExit() {
// 等待日志等异步写入完成
setTimeout(() => {
process.exit();
}, 1500)
}
module.exports = {
loadException,
uncaughtExceptionHandler,
unhandledRejectionHandler,
uncaughtExceptionMonitorHandler
};

98
ee-core/html/boot.html Normal file
View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<style>
#loadingPage {
background-color: #dedede;
font-size: 12px;
}
.base {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.desc {
margin: 0, 0, 20px, 0;
}
.loading,
.loading > div {
position: relative;
box-sizing: border-box;
}
.loading {
display: block;
font-size: 0;
color: #06b359;
}
.loading.la-dark {
color: #07C160;
}
.loading > div {
display: inline-block;
float: none;
background-color: currentColor;
border: 0 solid currentColor;
}
.loading {
width: 92px;
height: 92px;
}
.loading > div {
position: absolute;
top: 50%;
left: 50%;
background: transparent;
border-style: solid;
border-width: 2px;
border-radius: 100%;
animation: ball-clip-rotate-multiple-rotate 1s ease-in-out infinite;
}
.loading > div:first-child {
position: absolute;
width: 92px;
height: 92px;
border-right-color: transparent;
border-left-color: transparent;
}
.loading > div:last-child {
width: 16px;
height: 16px;
border-top-color: transparent;
border-bottom-color: transparent;
animation-duration: 0.5s;
animation-direction: reverse;
}
@keyframes ball-clip-rotate-multiple-rotate {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
50% {
transform: translate(-50%, -50%) rotate(180deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>
</head>
<body>
<div id="boot">
<div class='base'>
<!-- Booting the frontend service ... -->
<!-- <div class='desc'>
Booting frontend
</div> -->
<div class="loading">
<div></div>
<div></div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title>Booting failure</title>
<style>
#failure {
background-color: #dedede;
font-size: 14px;
}
.base {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div id="failure">
<div class='base'>
Startup failed, please check cross service is runing !
</div>
</div>
</body>
</html>

28
ee-core/html/failure.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title>Booting failure</title>
<style>
#failure {
background-color: #dedede;
font-size: 14px;
}
.base {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>
<body>
<div id="failure">
<div class='base'>
Startup failed, please check frontend service is runing !
</div>
</div>
</body>
</html>

1
ee-core/html/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare function getHtmlFilepath(name: string): string;

11
ee-core/html/index.js Normal file
View File

@@ -0,0 +1,11 @@
const path = require('path');
// Html
function getHtmlFilepath(name){
const pagePath = path.join(__dirname, name);
return pagePath;
}
module.exports = {
getHtmlFilepath
};

2
ee-core/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { ElectronEgg };
import { ElectronEgg } from "./app";

7
ee-core/index.js Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
const { ElectronEgg } = require('./app');
module.exports = {
ElectronEgg
}

26
ee-core/jobs/child-pool/index.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
import EventEmitter = require("events");
import LoadBalancer = require("../load-balancer");
export declare class ChildPoolJob extends EventEmitter<[never]> {
constructor(opt?: {});
config: any;
boundMap: Map<any, any>;
children: {};
min: number;
max: number;
strategy: string;
weights: any[];
LB: LoadBalancer;
_initEvents(): void;
_removeChild(pid: any): void;
create(number?: number): Promise<string[]>;
_childCreated(childProcess: any): void;
run(filepath: any, params?: {}): any;
runPromise(filepath: any, params?: {}): Promise<any>;
getBoundChild(boundId: any): any;
getChildByPid(pid: any): any;
getChild(): any;
getPids(): string[];
// kill all
// type: sequence | parallel
killAll(type?: string): void;
}

View File

@@ -0,0 +1,190 @@
const EventEmitter = require('events');
const LoadBalancer = require('../load-balancer');
const { getFullpath } = require('../../loader');
const { JobProcess } = require('../child/jobProcess');
const { Events } = require('../../const/channel');
const { validValue } = require('../../utils/helper');
const { getConfig } = require('../../config');
class ChildPoolJob extends EventEmitter {
constructor(opt = {}) {
super();
let options = Object.assign({
weights: [],
}, opt);
this.config = {};
this.boundMap = new Map();
this.children = {};
this.min = 3;
this.max = 6;
this.strategy = 'polling';
this.weights = new Array(this.max).fill().map((v, i) => {
let w = validValue(options.weights[i]) ? options.weights[i] : 1
return w;
});
let lbOpt = {
algorithm: LoadBalancer.Algorithm.polling,
targets: [],
}
this.LB = new LoadBalancer(lbOpt);
const cfg = getConfig().jobs;
if (cfg) {
this.config = cfg;
}
this._initEvents();
}
_initEvents() {
this.on(Events.childProcessExit, (data) => {
this._removeChild(data.pid);
});
this.on(Events.childProcessError, (data) => {
this._removeChild(data.pid);
});
}
_removeChild(pid) {
const length = Object.keys(this.children).length;
const lbOpt = {
id: pid,
weight: this.weights[length - 1],
}
this.LB.del(lbOpt);
delete this.children[pid];
}
async create(number = 3) {
if (number < 0 || number > this.max) {
throw new Error(`[ee-core] [jobs/child-pool] The number is invalid !`);
}
let currentNumber = this.children.length;
if (currentNumber > this.max) {
throw new Error(`[ee-core] [jobs/child-pool] The number of current processes number: ${currentNumber} is greater than the maximum: ${this.max} !`);
}
if (number + currentNumber > this.max) {
number = this.max - currentNumber;
}
// args
let options = Object.assign({
processArgs: {
type: 'childPoolJob'
}
}, {});
for (let i = 1; i <= number; i++) {
let task = new JobProcess(this, options);
this._childCreated(task);
}
let pids = Object.keys(this.children);
return pids;
}
// Post creation processing of child processes
_childCreated(childProcess) {
let pid = childProcess.pid;
this.children[pid] = childProcess;
const length = Object.keys(this.children).length;
let lbTask = {
id: pid,
weight: this.weights[length - 1],
}
this.LB.add(lbTask);
}
// Execute a job file
run(filepath, params = {}) {
const jobPath = getFullpath(filepath);
const childProcess = this.getChild();
childProcess.dispatch('run', jobPath, params);
return childProcess;
}
// Asynchronous execution of a job file
async runPromise(filepath, params = {}) {
return this.run(filepath, params);
}
// Get the bound process object
getBoundChild(boundId) {
let proc;
const boundPid = this.boundMap.get(boundId);
if (boundPid) {
proc = this.children[boundPid];
return proc;
}
// 获取进程并绑定
proc = this.getChild();
this.boundMap.set(boundId, proc.pid);
return proc;
}
// Retrieve a sub process object through PID
getChildByPid(pid) {
let proc = this.children[pid] || null;
return proc;
}
// Get a sub process object
getChild() {
let proc;
const currentPids = Object.keys(this.children);
// 没有则创建
if (currentPids.length == 0) {
let subIds = this.create(1);
proc = this.children[subIds[0]];
} else {
// 从池子中获取一个
let onePid = this.LB.pickOne().id;
proc = this.children[onePid];
}
if (!proc) {
let errorMessage = `[ee-core] [jobs/child-pool] Failed to obtain the child process !`
throw new Error(errorMessage);
}
return proc;
}
// Get current pigs
getPids() {
let pids = Object.keys(this.children);
return pids;
}
// kill all
// type: sequence | parallel
killAll(type = 'parallel') {
let i = 1;
Object.keys(this.children).forEach(key => {
let proc = this.children[key];
if (proc) {
if (type == 'sequence') {
setTimeout(()=>{
proc.kill();
}, i * 1000)
i++;
} else {
proc.kill();
}
}
});
}
}
module.exports = {
ChildPoolJob
};

1
ee-core/jobs/child/app.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

65
ee-core/jobs/child/app.js Normal file
View File

@@ -0,0 +1,65 @@
const is = require('is-type-of');
const { loadException } = require('ee-core/exception');
const { requireFile } = require('ee-core/loader');
const { coreLogger } = require('ee-core/log');
const { isBytecodeClass } = require('ee-core/core/utils');
loadException();
const commands = ['run'];
class ChildApp {
constructor() {
this._initEvents();
this.jobMap = new Map();
}
/**
* 初始化事件监听
*/
_initEvents() {
process.on('message', this._handleMessage.bind(this));
process.once('exit', (code) => {
coreLogger.info(`[ee-core] [jobs/child] received a exit from main-process, code:${code}, pid:${process.pid}`);
});
}
/**
* 监听消息
*/
_handleMessage(m) {
if (commands.indexOf(m.cmd) == -1) {
return
}
switch (m.cmd) {
case 'run':
this.run(m);
break;
default:
}
coreLogger.info(`[ee-core] [jobs/child] received a message from main-process, message: ${JSON.stringify(m)}`);
}
/**
* 运行脚本
*/
run(msg = {}) {
const {jobPath, jobParams, jobFunc, jobFuncParams} = msg;
let mod = requireFile(jobPath);
if (is.class(mod) || isBytecodeClass(mod)) {
if (!this.jobMap.has(jobPath)) {
const instance = new mod(...jobParams);
instance.handle(...jobParams);
this.jobMap.set(jobPath, instance);
} else {
const instance = this.jobMap.get(jobPath);
instance[jobFunc](...jobFuncParams);
}
} else if (is.function(mod)) {
mod(jobParams);
}
}
}
new ChildApp();

13
ee-core/jobs/child/index.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import EventEmitter = require("events");
import { JobProcess } from "./jobProcess";
export declare class ChildJob extends EventEmitter<[never]> {
constructor();
jobs: {};
config: any;
_initEvents(): void;
exec(filepath: string, params?: {}, opt?: {}): JobProcess;
createProcess(opt?: {}): JobProcess;
getPids(): string[];
execPromise(filepath: string, params?: {}, opt?: {}): Promise<JobProcess>;
}

View File

@@ -0,0 +1,85 @@
const EventEmitter = require('events');
const { JobProcess } = require('./jobProcess');
const { getFullpath } = require('../../loader');
const { Events } = require('../../const/channel');
const { getConfig } = require('../../config');
const { extend } = require('../../utils/extend');
class ChildJob extends EventEmitter {
constructor() {
super();
this.jobs = {};
this.config = {};
const cfg = getConfig().jobs;
if (cfg) {
this.config = cfg;
}
this._initEvents();
}
/**
* 初始化监听
*/
_initEvents() {
this.on(Events.childProcessExit, (data) => {
delete this.jobs[data.pid];
});
this.on(Events.childProcessError, (data) => {
delete this.jobs[data.pid];
});
}
/**
* 执行一个job文件
*/
exec(filepath, params = {}, opt = {}) {
const jobPath = getFullpath(filepath);
const proc = this.createProcess(opt);
const cmd = 'run';
proc.dispatch(cmd, jobPath, params);
return proc;
}
/**
* 创建子进程
*/
createProcess(opt = {}) {
const options = extend(true, {
processArgs: {
type: 'childJob'
}
}, opt);
const proc = new JobProcess(this, options);
if (!proc) {
let errorMessage = `[ee-core] [jobs/child] Failed to obtain the child process !`
throw new Error(errorMessage);
}
this.jobs[proc.pid] = proc;
return proc;
}
/**
* 获取当前pids
*/
getPids() {
let pids = Object.keys(this.jobs);
return pids;
}
/**
* 异步执行一个job文件 todo this指向
*/
async execPromise(filepath, params = {}, opt = {}) {
return this.exec(filepath, params, opt);
}
}
module.exports = {
ChildJob
};

16
ee-core/jobs/child/jobProcess.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
import { EventEmitter } from "events";
import { ChildProcess } from "child_process";
declare export class JobProcess {
constructor(host: any, opt?: {});
emitter: EventEmitter<[never]>;
host: any;
args: string[];
sleeping: boolean;
child: ChildProcess;
pid: number;
_init(): void;
_eventEmit(m: any): void;
dispatch(cmd: string, jobPath?: string, ...params: any[]): void;
callFunc(jobPath?: string, funcName?: string, ...params: any[]): void;
kill(timeout?: number): void;
}

View File

@@ -0,0 +1,134 @@
const path = require('path');
const EventEmitter = require('events');
const serialize = require('serialize-javascript');
const { fork } = require('child_process');
const { coreLogger } = require('../../log');
const { getBaseDir, isPackaged, allEnv } = require('../../ps');
const { Processes, Events, Receiver } = require('../../const/channel');
const { getRandomString } = require('../../utils/helper');
const { getFullpath } = require('../../loader');
const { extend } = require('../../utils/extend');
class JobProcess {
constructor(host, opt = {}) {
let cwd = getBaseDir();
const appPath = path.join(__dirname, 'app.js');
if (isPackaged()) {
// todo fork的cwd目录为什么要在app.asar外
cwd = path.join(getBaseDir(), '..');
}
const options = extend(true, {
processArgs: {},
processOptions: {
cwd: cwd,
env: allEnv(),
stdio: 'ignore' // pipe
}
}, opt);
this.emitter = new EventEmitter();
this.host = host;
this.args = [];
this.sleeping = false;
// 传递给子进程的参数
this.args.push(JSON.stringify(options.processArgs));
this.child = fork(appPath, this.args, options.processOptions);
this.pid = this.child.pid;
this._init();
}
_init() {
const { messageLog } = this.host.config;
this.child.on('message', (m) => {
if (messageLog == true) {
coreLogger.info(`[ee-core] [jobs/child] received a message from child-process, message: ${serialize(m)}`);
}
if (m.channel == Processes.showException) {
coreLogger.error(`${m.data}`);
}
// 收到子进程消息,转发到 event
if (m.channel == Processes.sendToMain) {
this._eventEmit(m);
}
});
this.child.on('exit', (code, signal) => {
let data = {
pid: this.pid
}
this.host.emit(Events.childProcessExit, data);
coreLogger.info(`[ee-core] [jobs/child] received a exit from child-process, code:${code}, signal:${signal}, pid:${this.pid}`);
});
this.child.on('error', (err) => {
let data = {
pid: this.pid
}
this.host.emit(Events.childProcessError, data);
coreLogger.error(`[ee-core] [jobs/child] received a error from child-process, error: ${err}, pid:${this.pid}`);
});
}
_eventEmit(m) {
switch (m.eventReceiver) {
case Receiver.forkProcess:
this.emitter.emit(m.event, m.data);
break;
case Receiver.childJob:
this.host.emit(m.event, m.data);
break;
default:
this.host.emit(m.event, m.data);
this.emitter.emit(m.event, m.data);
break;
}
}
dispatch(cmd, jobPath = '', ...params) {
// 消息对象
const mid = getRandomString();
let msg = {
mid,
cmd,
jobPath,
jobParams: params
}
// todo 是否会发生监听未完成时,接收不到消息?
// 发消息到子进程
this.child.send(msg);
}
callFunc(jobPath = '', funcName = '', ...params) {
jobPath = getFullpath(jobPath);
// 消息对象
const mid = getRandomString();
let msg = {
mid,
cmd:'run',
jobPath,
jobFunc: funcName,
jobFuncParams: params
}
this.child.send(msg);
}
kill(timeout = 1000) {
this.child.kill('SIGINT');
setTimeout(() => {
if (this.child.killed) return;
this.child.kill('SIGKILL');
}, timeout)
}
}
module.exports = {
JobProcess
};

3
ee-core/jobs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import { ChildJob } from "./child";
import { ChildPoolJob } from "./child-pool";
export { ChildJob, ChildPoolJob };

7
ee-core/jobs/index.js Normal file
View File

@@ -0,0 +1,7 @@
const { ChildJob } = require('./child');
const { ChildPoolJob } = require('./child-pool');
module.exports = {
ChildJob,
ChildPoolJob
};

View File

@@ -0,0 +1,4 @@
declare const _exports: {
[x: string]: (tasks: any, weightIndex: any, weightTotal: any, context: any) => any;
};
export = _exports;

View File

@@ -0,0 +1,12 @@
const Consts = require("../consts");
module.exports = {
[Consts.polling]: require('./polling'),
[Consts.weights]: require('./weights'),
[Consts.random]: require('./random'),
[Consts.specify]: require('./specify'),
[Consts.minimumConnection]: require('./minimumConnection'),
[Consts.weightsPolling]: require('./weightsPolling'),
[Consts.weightsRandom]: require('./weightsRandom'),
[Consts.weightsMinimumConnection]: require('./weightsMinimumConnection'),
};

View File

@@ -0,0 +1,2 @@
declare function _exports(tasks: any, conMap?: {}): any;
export = _exports;

View File

@@ -0,0 +1,19 @@
/**
* 最小连接数
*/
module.exports = function (tasks, conMap={}) {
if (tasks.length < 2) return tasks[0] || null;
let min = conMap[tasks[0].id];
let minIndex = 0;
for (let i = 1; i < tasks.length; i++) {
const con = conMap[tasks[i].id] || 0;
if (con <= min) {
min = con;
minIndex = i;
}
}
return tasks[minIndex] || null;
};

View File

@@ -0,0 +1,2 @@
declare function _exports(tasks: any, currentIndex: any, context: any): any;
export = _exports;

View File

@@ -0,0 +1,12 @@
/**
* 轮询
*/
module.exports = function (tasks, currentIndex, context) {
if (!tasks.length) return null;
const task = tasks[currentIndex];
context.currentIndex ++;
context.currentIndex %= tasks.length;
return task || null;
};

View File

@@ -0,0 +1,2 @@
declare function _exports(tasks: any): any;
export = _exports;

View File

@@ -0,0 +1,10 @@
/**
* 随机
*/
module.exports = function (tasks) {
const length = tasks.length;
const target = tasks[Math.floor(Math.random() * length)];
return target || null;
};

View File

@@ -0,0 +1,2 @@
declare function _exports(tasks: any, id: any): any;
export = _exports;

View File

@@ -0,0 +1,15 @@
/**
* 声明绑定
*/
module.exports = function (tasks, id) {
let task;
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === id) {
task = tasks[i];
break;
}
}
return task || null;
};

Some files were not shown because too many files have changed in this diff Show More