/*
* 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();
}
}