UNPKG

aem-clientlib-generator

Version:

Creates configuration files for AEM ClientLibs and synchronizes assets.

546 lines (458 loc) 17.2 kB
/* * Copyright (c) 2016 wcm.io and Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ "use strict"; var async = require("async"); var path = require("path"); var _ = require("lodash"); var fs = require("fs"); var fse = require("fs-extra"); var glob = require("glob"); /** * JSON serialization format * * @type {string} */ var SERIALIZATION_FORMAT_JSON = "json"; /** * XML serialization format for File Vault * * @type {string} */ var SERIALIZATION_FORMAT_XML = "xml"; /** * XML serialization format for Sling Initial Content * * @type {string} */ var SERIALIZATION_FORMAT_SLING_XML = "slingxml"; /** * List of fields to be evaluated for being added to the {@code cq:ClientLibraryFolder} file descriptor * @type {String[]} */ var defaultClientLibDirectoryFields = ["embed", "dependencies", "cssProcessor", "jsProcessor", "allowProxy", "longCacheKey", "replaces", "disableIfReplacing", "guideComponentType"]; var clientLibDirectoryFields = []; /** * List of source file extensions which AEM is able to merge and provide as `.js` or `.css` client-side library * @type {Object} */ var ALLOWED_EXTENSIONS = { css: [".css", ".less"], js: [".js"] } /** * @typedef {Object} ClientLibItem * @property {String} path - Clientlib root path (optional if `options.clientLibRoot` is set) * @property {String} name - Clientlib name * @property {String} [serializationFormat=json] - Type of the target archive for which the resources must be generated [json|xml|slingxml] (optional, default=json) * @property {boolean} [allowProxy] - Is the Clientlib meant to be used as a proxy * @property {Array<String>} [embed] - other Clientlib names that should be embedded * @property {Array<String>} [dependencies] - other Clientlib names that should be included * @property {Array<String>} [categories] - to set a category for the clientLib (optional), ovrrides the default that uses the name as category * @property {Array<String>} [customProperties] - by default only a set of known properties will be copied over the clientlib, using this field custom properties can be added * @property {Array<String>} [cssProcessor] - Clientlib processor specification for CSS * @property {Array<String>} [jsProcessor] - Clientlib processor specification for JS * @property {Array<Object>} assets - content that should be copied to the clientlib folder, more details below * @property {String} guideComponentType - property added for adaptive forms */ /** * Check if the given file exists * @param file * @returns {boolean} */ function fileExists(file) { try { fs.accessSync(file); return true; } catch (e) { return false; } } /** * Removes clientlib folder and configuration file (JSON) for the given * clientlib item. * @param {ClientLibItem} item - clientlib properties * @param {Object} [options] - further options * @param {Function} done - callback to be invoked after */ function removeClientLib(item, options, done) { var configJson = path.join(item.path, item.name + ".json"); var clientLibPath = path.join(item.path, item.name); var files = []; if (_.isFunction(options)) { done = options; options = {}; } if (fileExists(configJson)) { files.push(configJson); } if (fileExists(clientLibPath)) { files.push(clientLibPath); } if (files.length === 0) { return done(); } options.verbose && console.log("remove clientlib from " + clientLibPath); if (options.dry) { return done(); } async.eachSeries(files, function (file, doneClean) { fse.remove(file, doneClean); }, done); } /** * Write the clientlib asset TXT file (js or css) that describes the * base and contains all resource paths. * @param {String} clientLibPath - path to the clientlib folder * @param {Object} asset - asset object */ function writeAssetTxt(clientLibPath, asset, options) { if (!asset || !asset.type || !_.isArray(asset.files)) { return; } var outputFile = path.join(clientLibPath, asset.type + ".txt"); var basePath = path.posix.join(clientLibPath, asset.base); // determines file path relative to the base var filenames = []; options.verbose && console.log("write clientlib asset txt file (type: " + asset.type + "): " + outputFile); asset.files.forEach(function (file) { // inject only files that correspondents to the asset type if (ALLOWED_EXTENSIONS[asset.type].indexOf(path.extname(file.dest)) !== -1) { var rel = path.posix.relative(basePath, file.dest); filenames.push(rel); } }); var content = "#base=" + asset.base + "\n\n" + filenames.join("\n"); content.trim(); if (!options.dry) { fs.writeFileSync(outputFile, content); } } /** * Write a configuration JSON file for a clientlib * with the given properties in `item` * @param {ClientLibItem} item - clientlib configuration properties * @param {Array<Object>} properties - sorted list of properties to write * @param {Object} options - further options */ function writeClientLibJson(item, properties, options) { var content = { 'jcr:primaryType': 'cq:ClientLibraryFolder' }; properties.forEach(function (property) { content[property.key] = property.value; }); var jsonFile = path.join(item.path, item.name + ".json"); options.verbose && console.log("write clientlib json file: " + jsonFile); if (!options.dry) { fse.writeJsonSync(jsonFile, content, {spaces: 2}); } } /** * Write a configuration FileVault XML file for a clientlib * with the given properties in `item` * @param {ClientLibItem} item - clientlib configuration properties * @param {Array<Object>} properties - sorted list of properties to write * @param {Object} options - further options */ function writeClientLibXml(item, properties, options) { var content = '<?xml version="1.0" encoding="UTF-8"?>' + '\n<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"' + '\n jcr:primaryType="cq:ClientLibraryFolder"'; properties.forEach(function (property) { if (typeof property.value === 'boolean') { // Boolean value content += '\n ' + property.key + '="{Boolean}' + property.value + '"'; } else if (Array.isArray(property.value)) { // Array of strings var fieldValue = property.value.join(','); content += '\n ' + property.key + '="[' + fieldValue + ']"'; } else if (typeof property.value === 'string') { // String value content += '\n ' + property.key + '="' + property.value + '"'; } }); content += "/>\n"; var contentXml = getXmlOutputFile(item, SERIALIZATION_FORMAT_XML); options.verbose && console.log("write clientlib json file: " + contentXml); if (!options.dry) { fse.writeFileSync(contentXml, content); } } /** * Write a configuration Sling-Initial-Content XML file for a clientlib * with the given properties in `item` * @param {ClientLibItem} item - clientlib configuration properties * @param {Array<Object>} properties - sorted list of properties to write * @param {Object} options - further options */ function writeClientLibSlingXml(item, properties, options) { var content = '<?xml version="1.0" encoding="UTF-8"?>' + '\n<node>' + '\n <name>' + item.name + '</name>' + '\n <primaryNodeType>cq:ClientLibraryFolder</primaryNodeType>'; properties.forEach(function (property) { content += '\n <property>\n <name>' + property.key + '</name>'; if (typeof property.value === 'boolean') { // Boolean value content += '\n <value>' + property.value + '</value>' + '\n <type>Boolean</type>'; } else if (Array.isArray(property.value)) { // Array of strings content += '\n <values>'; property.value.forEach(value => { content += '\n <value>' + value + '</value>'; }); content += '\n </values>\n <type>String</type>'; } else if (typeof property.value === 'string') { // String value content += '\n <value>' + property.value + '</value>' + '\n <type>String</type>'; } content += '\n </property>'; }); content += '\n</node>\n' var contentXml = getXmlOutputFile(item, SERIALIZATION_FORMAT_SLING_XML); options.verbose && console.log("write clientlib Sling-Initial-Content XML file: " + contentXml); if (!options.dry) { fse.writeFileSync(contentXml, content); } } /** * Get the output file name and path for the specified XML serialization format * @param {ClientLibItem} item - clientlib configuration properties * @param {String} serializationFormat serialization format (xml|slingxml) */ function getXmlOutputFile(item, serializationFormat) { // Sling initial content: <name>.xml if (serializationFormat === SERIALIZATION_FORMAT_SLING_XML) { return item.outputPath || path.join(item.path, item.name + ".xml") } // FileVault: a folder with a .content.xml file in it var outputPath = item.outputPath || path.join(item.path, item.name); return path.join(outputPath + "/.content.xml"); } /** * Iterate through the given array of clientlib configuration objects and * process them asynchronously. * @param {Array<ClientLibItem>} itemList - array of clientlib configuration items * @param {Object} [options] - global configuration options * @param {Function} done - to be called if everything is done */ function start(itemList, options, done) { if (_.isFunction(options)) { done = options; options = {}; } if (!_.isArray(itemList)) { itemList = [itemList]; } if (options.context || options.cwd) { options.cwd = options.context || options.cwd; process.chdir(options.cwd); } if (options.verbose) { console.log("\nstart aem-clientlib-generator"); console.log(" working directory: " + process.cwd()); } options.dry && console.log("\nDRY MODE - without write options!"); async.eachSeries(itemList, function (item, processItemDone) { processItem(item, options, processItemDone); }, done); } /** * Normalize different asset configuration options. * @param {String} clientLibPath - clientlib subfolder * @param {Object} assets - asset configuration object * @returns {*} */ function normalizeAssets(clientLibPath, assets) { var list = assets; // transform object to array if (!_.isArray(assets)) { list = []; _.keys(assets).forEach(function (assetKey) { var assetItem = assets[assetKey]; // check/transform short version if (_.isArray(assetItem)) { assetItem = { files: assetItem }; } if (!assetItem.base) { assetItem.base = assetKey; } assetItem.type = assetKey; list.push(assetItem); }); } // transform files to scr-dest mapping list.forEach(function (asset) { var mapping = []; var flatName = typeof asset.flatten !== "boolean" ? true : asset.flatten; var assetPath = path.posix.join(clientLibPath, asset.base); var globOptions = {}; if (asset.cwd) { globOptions.cwd = asset.cwd; } if (asset.ignore) { globOptions.ignore = asset.ignore; } asset.files.forEach(function (file) { var fileItem = file; // convert simple syntax to object if (_.isString(file)) { fileItem = { src: file }; } // no magic pattern -> default behaviour if (!glob.hasMagic(fileItem.src)) { // determine default dest if (!fileItem.dest) { fileItem.dest = path.posix.basename(file); } // generate full path fileItem.dest = path.posix.join(assetPath, fileItem.dest); mapping.push(fileItem); } // resolve magic pattern else { var files = glob.sync(fileItem.src, globOptions); var hasCwd = !!globOptions.cwd; var dest = fileItem.dest ? path.posix.join(assetPath, fileItem.dest) : assetPath; files.forEach(function (resolvedFile) { // check 'flatten' option -> strip dir name var destFile = flatName ? path.posix.basename(resolvedFile) : resolvedFile; var item = { src: resolvedFile, dest: path.posix.join(dest, destFile) }; // check "cwd" option -> rebuild path, because it was stripped by glob.sync() if (hasCwd) { item.src = path.posix.join(globOptions.cwd, resolvedFile); } mapping.push(item); }); } }); mapping = removeDuplicates(mapping, 'src'); asset.files = mapping; }); return list; } /** * Removes duplicates in array of Objects based on the provided key * From: https://firstclassjs.com/remove-duplicate-objects-from-javascript-array-how-to-performance-comparison/ * @param {Array} array - array of Objects that will be filtered * @param {String} key - key that will be used for filter comparison */ function removeDuplicates(array, key) { let lookup = new Set(); return array.filter(obj => !lookup.has(obj[key]) && lookup.add(obj[key])); } /** * Prepares a sorted list of all properties that should be written to the clientlib descriptor. */ function prepareSortedProperties(item) { var properties = []; // if categories is a config entry append the values to the array, else use item.name if (item.hasOwnProperty('categories')) { properties.push({key: 'categories', value: item['categories']}); } else { properties.push({key: 'categories', value: [item.name]}); } clientLibDirectoryFields.forEach(function (nodeKey) { if (item.hasOwnProperty(nodeKey)) { properties.push({key: nodeKey, value: item[nodeKey]}); } }); // sort properties by key properties.sort(function (a, b) { if (a.key < b.key) { return -1; } if (a.key > b.key) { return 1; } return 0; }); return properties; } /** * Process the given clientlib configuration object. * @param {ClientLibItem} item - clientlib configuration object * @param {Object} options - configuration options * @param {Function} processDone - to be called if everything is done */ function processItem(item, options, processDone) { if (!item.path) { item.path = options.clientLibRoot; } clientLibDirectoryFields.length = 0; clientLibDirectoryFields = defaultClientLibDirectoryFields.concat(item.customProperties); options.verbose && console.log("\n\nprocessing clientlib: " + item.name); // remove current files if exists removeClientLib(item, function (err) { var clientLibPath = item.outputPath || path.join(item.path, item.name); // create clientlib directory fse.mkdirsSync(clientLibPath); var serializationFormat = item.serializationFormat || SERIALIZATION_FORMAT_JSON; options.verbose && console.log("Write node configuration using serialization format: " + serializationFormat); var properties = prepareSortedProperties(item); if (serializationFormat === SERIALIZATION_FORMAT_JSON) { // write configuration JSON writeClientLibJson(item, properties, options); } else if (serializationFormat === SERIALIZATION_FORMAT_SLING_XML) { // write Sling-Initial-Content configuration writeClientLibSlingXml(item, properties, options); } else { // write FileVault XML configuration writeClientLibXml(item, properties, options); } var assetList = normalizeAssets(clientLibPath, item.assets); // iterate through assets async.eachSeries(assetList, function (asset, assetDone) { // write clientlib creator files if (asset.type === "js" || asset.type === "css") { options.verbose && console.log(""); writeAssetTxt(clientLibPath, asset, options); } // copy files for given asset async.eachSeries(asset.files, function (fileItem, copyDone) { if (fileItem.src == fileItem.dest) { options.verbose && console.log(`${fileItem.src} already in output directory`); return copyDone(); } options.verbose && console.log("copy:", fileItem.src, fileItem.dest); if (options.dry) { return copyDone(); } // create directories separately or it will be copied recursively if (fs.lstatSync(fileItem.src).isDirectory()) { fs.mkdir(fileItem.dest, { recursive: true }, copyDone); } else { fse.copy(fileItem.src, fileItem.dest, copyDone); } }, assetDone); }, processDone); }); } module.exports = start; module.exports.removeClientLib = removeClientLib; module.exports.fileExists = fileExists;