src/lib/transformers/ScriptTransformer.js
import { extname } from 'path';
import { parseString as parseXML, Builder as XMLBuilder } from 'xml2js';
import { replaceExtension, log, colors } from 'gulp-util';
import Transformer from './Transformer';
import NodeType from '../def/NodeType';
/**
* Extracts JavaScript code from atvise (serverside) scripts. A config file is used to describe
* parameters.
*/
export default class ScriptTransformer extends Transformer {
/**
* Creates a new script transformer. Currently no options are applied.
*/
constructor() {
super();
/**
* The XMLBuilder to use when writing back to the database. Uses atvise specific defaults.
* @type {XMLBuilder}
*/
this.outputXMLBuilder = new XMLBuilder({
renderOpts: {
pretty: true,
indent: ' ',
newline: '\r\n', // TODO: Replace with require('os').EOL once atvise is cross-platform
},
xmldec: {
version: '1.0',
encoding: 'UTF-8',
},
cdata: true,
});
}
/**
* Parses script XML files and splits them a JavaScript and a configuration file.
* @param {NodeStream} stream The stream to use.
* @param {Node} node The node to split.
* @param {String} enc The encoding used.
* @param {function(err: Error, node: Node)} cb Callback to call after transformation.
*/
transformFromDB(stream, node, enc, cb) {
parseXML(node.contents, (err, results) => {
let document = {};
if (err || !results || !results.script) {
if (err) {
log(colors.red('Error parsing script'), colors.cyan(node.relative));
log(colors.red(err.message.split('\n').join(' ')));
} else if (!results) {
log(colors.red('Invalid script XML'), colors.cyan(node.relative));
log(colors.red('Empty document'));
} else if (!results.script) {
log(colors.red('Invalid script XML'), colors.cyan(node.relative));
log(colors.red('Tag <script> not found'));
}
} else {
document = results.script;
}
const config = {};
let code = '';
// Extract metadata
if (document.metadata && document.metadata.length === 1) {
const meta = document.metadata[0];
if (meta.icon && meta.icon.length === 1) {
const icon = meta.icon[0];
config.icon = icon.$ || {};
config.icon.content = icon._ || '';
}
if (meta.visible && meta.visible.length === 1) {
config.visible = Boolean(meta.visible[0]);
}
if (meta.title && meta.title.length === 1) {
config.title = meta.title[0];
}
if (meta.description && meta.description.length === 1) {
config.description = meta.description[0];
}
}
// Extract parameters
if (document.parameter) {
config.parameters = [];
document.parameter.forEach(param => config.parameters.push(param.$));
}
// Extract JavaScript
if (document.code && document.code.length === 1) {
code = document.code[0];
}
// Write files
stream.push(Transformer.encapsuledFile(node, '.json', JSON.stringify(config, null, ' ')));
stream.push(Transformer.encapsuledFile(node, '.js', code));
cb();
});
}
/**
* Buffers the contents of the files a script was split into.
* @param {Stream} stream The stream used.
* @param {AtviseFile} file The file to buffer.
* @param {String} enc The encoding used.
* @param {function(err: Error, file: AtviseFile)} cb Callback to call after transformation.
*/
transformFromFilesystem(stream, file, enc, cb) {
// Buffer contents
const script = replaceExtension(file.relative, '');
if (!stream._scripts) { stream._scripts = {}; }
if (!stream._scripts[script]) { stream._scripts[script] = {}; }
const ext = extname(file.path);
stream._scripts[script][ext] = file;
cb();
}
/**
* Called after all script files were buffered. Creates script XMLs and pushes them to the stream.
* @param {Stream} stream The stream used.
* @param {function(err: Error)} cb Callback to call after flushing.
*/
flushFromFilesystem(stream, cb) {
for (const key in stream._scripts) {
if (stream._scripts[key]['.js']) { // FIXME: Error if Script file doesn't exist
this.createScriptCode(stream, stream._scripts[key]);
}
}
cb();
}
/**
* Creates an XML script file from buffered JavaScript and configuration code.
* @param {Stream} stream The stream used.
* @param {{}} script The buffered script content.
*/
createScriptCode(stream, script) {
const paramsFile = script['.json'];
const jsFile = script['.js'];
const config = JSON.parse(paramsFile.contents);
const code = jsFile.contents.toString();
const result = {
script: {},
};
if (paramsFile.rc.typeDefinition === NodeType.QuickDynamic) {
const icon = config.icon.content;
delete config.icon.content;
result.script.metadata = {
icon: {
$: config.icon,
_: icon || '',
},
visible: config.visible ? 1 : 0,
title: config.title,
description: config.description,
};
}
result.script.parameter = config.parameters ?
config.parameters.map(param => ({ $: param })) :
[];
result.script.code = code;
const scriptFile = paramsFile.clone({ contents: false });
scriptFile.stat.mtime = new Date(Math.max(jsFile.stat.mtime, paramsFile.stat.mtime));
scriptFile.path = scriptFile.dirname;
scriptFile.contents = Buffer.from(this.outputXMLBuilder.buildObject(result));
stream.push(scriptFile);
}
/**
* This transformer is only applied to {@link NodeType.Script} and {@link NodeType.QuickDynamic}
* node types.
* @type {Map<NodeType, Boolean>}
*/
static get applyToTypes() {
return {
[NodeType.Script]: true,
[NodeType.QuickDynamic]: true,
};
}
}