/* * Copyright © 2021 Bonitasoft S.A. * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import {analyzeText, AnalyzeTextResult, transformAnalyzerResult} from "web-component-analyzer"; import {PropertiesInfo} from "./PropertiesInfo"; import {Property} from "./Property"; import * as fs from "fs"; import path, {sep} from "path"; import {PropertiesJsonGenerator} from "./PropertiesJsonGenerator"; import {FwkType, HtmlTemplatesGenerator} from "./HtmlTemplatesGenerator"; import {Bond} from "./Bond"; import {PropertyConstraint} from "./PropertyConstraint"; import {PropertyBuilder} from "./PropertyBuilder"; import {PropertyType} from "./PropertyType"; import {PropertiesInfoBuilder} from "./PropertiesInfoBuilder"; import * as pjson from "../package.json"; import * as archiver from "archiver"; import os from "os"; import {Asset} from "./Asset"; export class CustomWidgetBuilder { public generatePropertyFileFromWcFile(wcFile: string, outputDir: string) { let propInfo = this.getPropertiesInfoFromWebComponent(wcFile); new PropertiesJsonGenerator(propInfo, outputDir).generate(true); } public generatePropertyFileFromWcName(wcName: string, outputDir: string) { let propertyA = new PropertyBuilder("propertyA") .type(PropertyType.Text) .label("Property A") .help("") .defaultValue("initial value") .build(); let propertyB = new PropertyBuilder("propertyB") .type(PropertyType.Integer) .label("Property B") .help("") .defaultValue("0") .constraints(new PropertyConstraint("0", "100")) .bond(Bond.Variable) .build(); let properties = [propertyA, propertyB]; let propInfo = new PropertiesInfoBuilder(wcName) .description("") .properties(properties) .build(); new PropertiesJsonGenerator(propInfo, outputDir).generate(true); } public async generateWidgetAssets(propertiesFile: string, wcBundle: string, outputDir: string) { // This is typically used for standard widgets let propInfo = CustomWidgetBuilder.getPropertiesFromFile(propertiesFile); new HtmlTemplatesGenerator(propInfo).generate(outputDir, FwkType.AngularJS); CustomWidgetBuilder.updatePropertiesFile(propInfo, wcBundle, outputDir); new PropertiesJsonGenerator(propInfo, outputDir).generate(false); new HtmlTemplatesGenerator(propInfo).generate(outputDir, FwkType.Angular); console.log(`Widget assets have been generated in ${outputDir}`); } public async generateWidget(propertiesFile: string, wcBundle: string, outputDir: string) { // Generate a directory tree such as: // widgetWc.properties // └── resources // ├── assets // │ └── js // │ ├── my-input.es5.min.js // │ └── myInput.tpl.runtime.html // ├── myInput.tpl.html // └── widget.json let propInfo = CustomWidgetBuilder.getPropertiesFromFile(propertiesFile); // Generate widget files in a temp directory let tempDir = fs.mkdtempSync(`${os.tmpdir()}${sep}`); // root dir CustomWidgetBuilder.generateWidgetProperties(propInfo.name, tempDir); // resources dir let resourcesDir = `${tempDir}/resources`; fs.mkdirSync(resourcesDir); new HtmlTemplatesGenerator(propInfo).generate(resourcesDir, FwkType.AngularJS); CustomWidgetBuilder.updatePropertiesFile(propInfo, wcBundle, resourcesDir); new PropertiesJsonGenerator(propInfo, resourcesDir).generate(false, `widget.json`); // assets/js dir let jsDir = `${resourcesDir}/assets/js`; fs.mkdirSync(jsDir, {recursive: true}); CustomWidgetBuilder.copyFile(wcBundle, jsDir); new HtmlTemplatesGenerator(propInfo).generate(jsDir, FwkType.Angular); let zipFile = `${outputDir}/widget-${propInfo.name}.zip`; await CustomWidgetBuilder.generateZip(tempDir, zipFile); console.log(`Widget has been generated in ${zipFile}`); fs.rmSync(tempDir, {recursive: true}); } public getPropertiesInfoFromWebComponent(wcFile: string): PropertiesInfo { let analyzeResult = CustomWidgetBuilder.analyzeFile(wcFile); if (!analyzeResult) { throw new Error(CustomWidgetBuilder.getNoInformationMessage(wcFile)); } let resultJson = JSON.parse(analyzeResult); if (!resultJson || !resultJson.tags[0]) { throw new Error(CustomWidgetBuilder.getNoInformationMessage(wcFile)); } let info = resultJson.tags[0]; let wcName = info.name; let description = info.description; let properties = CustomWidgetBuilder.getProperties(info.properties); return new PropertiesInfoBuilder(wcName) .description(description) .properties(properties) .build(); } private static analyzeFile(wcFile: string): string { if (!fs.existsSync(wcFile)) { throw new Error(`File does not exist: ${wcFile}`); } let fileStr = fs.readFileSync(wcFile, "utf8").toString(); let result: AnalyzeTextResult = analyzeText(fileStr); return transformAnalyzerResult("json", result.results, result.program, {visibility: "public"}); } private static getProperties(props: any): Array { if (!props) { return []; } let properties: Array = []; for (let prop of props) { if (this.propToExclude(prop.name)) { continue; } let help; if (prop.description) { help = prop.description; } let name = prop.name; let type; if (prop.type) { type = CustomWidgetBuilder.getPropertyType(prop.type); } let defaultValue = CustomWidgetBuilder.getDefaultValue(prop.default); properties.push(new PropertyBuilder(name).type(type).defaultValue(defaultValue).help(help).build()); } return properties; } private static getAssets(wcId: string, wcBundle: string): Array { let assets: Array = []; // web component bundle let assetBundle: Asset = { id: CustomWidgetBuilder.getUUIDv4(), name: path.basename(wcBundle), type: "js" }; // web component runtime template let runtimeTplBundle: Asset = { id: CustomWidgetBuilder.getUUIDv4(), name: `${wcId}.${HtmlTemplatesGenerator.AngularFileExtension}`, type: "js", }; assets.push(assetBundle, runtimeTplBundle); return assets; } private static generateWidgetProperties(wcName: string, outputDir: string) { let buff = []; buff.push('#Generated by Bonita UI Designer SDK'); buff.push(`#${new Date().toString()}`); buff.push(`name=${wcName}`); buff.push(`designerSdkVersion=${pjson.version}`); buff.push('contentType=wc-widget'); let content = buff.join('\n'); let filePath = `${outputDir}/widget.properties`; fs.writeFileSync(filePath, content); } /** * Transform a string to boolean or number * e.g. "false" -> false * "4" -> 4 */ private static getDefaultValue(value: string): string | number | boolean { try { return JSON.parse(value); } catch (err) { return value; } } /** * Mapping from web component type to UID type * number -> integer * string -> text * number | undefined -> number */ private static getPropertyType(wcType: string): PropertyType { wcType = wcType.replace(" | undefined", ""); switch (wcType) { case 'number': return PropertyType.Integer; case 'string': return PropertyType.Text; case 'boolean': return PropertyType.Boolean; default: throw new Error(`Unsupported type: ${wcType}`); } } private static propToExclude(propName: string) { // "styles" is a reserves word for css with lit-element let toExclude = ["styles", "lang"]; return toExclude.indexOf(propName) > -1; } private static getPropertiesFromFile(propertiesFile: string): PropertiesInfo { let propertiesStr = fs.readFileSync(propertiesFile, "utf8").toString(); return JSON.parse(propertiesStr); } private static getNoInformationMessage(wcFile: string) { return `Cannot get any information from file ${wcFile}\nExiting...`; } private static copyFile(file: string, dir: string) { let sourceBuff = fs.readFileSync(file); let fileName = path.basename(file); fs.writeFileSync(`${dir}/${fileName}`, sourceBuff); } private static getUUIDv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } private static generateZip(dir: string, zipFile: string): Promise { const archive = archiver.create('zip'); const stream = fs.createWriteStream(zipFile); return new Promise((resolve, reject) => { archive .directory(dir, false) .on('error', err => reject(err)) .pipe(stream) ; archive.finalize().then(() => resolve(1)); }); } private static updatePropertiesFile(propInfo: PropertiesInfo, wcBundle: string, templatePath: string) { // Add assets to the json properties file propInfo.assets = CustomWidgetBuilder.getAssets(propInfo.id, wcBundle); propInfo.custom = !propInfo.id.startsWith("uid"); // Add bundles to the json properties file propInfo.jsBundle = `assets/js/${path.basename(wcBundle)}`; propInfo.htmlBundle = `assets/js/${propInfo.id}.${HtmlTemplatesGenerator.AngularFileExtension}`; // Embed template content in the json properties file let angularJSTemplatePath = `${templatePath}/${propInfo.id}.${HtmlTemplatesGenerator.AngularJsFileExtension}`; propInfo.template = fs.readFileSync(angularJSTemplatePath).toString(); } }