| /** |
| * |
| * Copyright (c) 2020 Silicon Labs |
| * |
| * 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. |
| */ |
| |
| /** |
| * @module JS API: generator logic |
| */ |
| |
| const _ = require('lodash') |
| const fs = require('fs') |
| const fsPromise = fs.promises |
| const path = require('path') |
| const util = require('../util/util.js') |
| const queryPackage = require('../db/query-package.js') |
| const querySession = require('../db/query-session') |
| const dbEnum = require('../../src-shared/db-enum.js') |
| const env = require('../util/env') |
| const templateEngine = require('./template-engine.js') |
| const dbApi = require('../db/db-api.js') |
| const dbCache = require('../db/db-cache.js') |
| const queryNotification = require('../db/query-package-notification.js') |
| |
| /** |
| * Finds and reads JSON files referenced in a nested object. |
| * |
| * @param {Object} obj - The object to search for JSON file references. |
| * @param {string} basePath - The base directory to resolve relative paths. |
| * @returns {Promise<string>} - A promise that resolves to the concatenated content of all JSON files. |
| */ |
| async function findAndReadJsonFiles(obj, basePath) { |
| let additionalJsonContent = '' |
| for (const key in obj) { |
| if (typeof obj[key] === 'string' && obj[key].endsWith('.json')) { |
| // If the value is a JSON file reference, read its content |
| let jsonFilePath = path.resolve(path.join(basePath, obj[key])) |
| if (fs.existsSync(jsonFilePath)) { |
| additionalJsonContent += await fsPromise.readFile(jsonFilePath, 'utf8') |
| } |
| } else if (typeof obj[key] === 'object' && obj[key] !== null) { |
| // If the value is an object, recursively search for JSON file references |
| additionalJsonContent += await findAndReadJsonFiles(obj[key], basePath) |
| } |
| } |
| return additionalJsonContent |
| } |
| |
| /** |
| * Given a path, it will read generation template object into memory. |
| * |
| * @param {*} templatePath |
| * @returns Object that contains: data, crc, templateData |
| */ |
| async function loadGenTemplateFromFile(templatePath) { |
| let ret = {} |
| ret.data = await fsPromise.readFile(templatePath, 'utf8') |
| ret.templateData = JSON.parse(ret.data) |
| // Find the json files within the gen template json files and add them to the |
| // crc as well. |
| let allJsonContentInTemplatePath = await findAndReadJsonFiles( |
| ret.templateData, |
| path.dirname(templatePath) |
| ) |
| let checksumData = ret.data + allJsonContentInTemplatePath |
| ret.crc = util.checksum(checksumData) |
| let requiredFeatureLevel = 0 |
| if ('requiredFeatureLevel' in ret.templateData) { |
| requiredFeatureLevel = ret.templateData.requiredFeatureLevel |
| } |
| let status = util.matchFeatureLevel(requiredFeatureLevel, templatePath) |
| if (status.match) { |
| return ret |
| } else { |
| throw status.message |
| } |
| } |
| |
| /** |
| * Inserts the package details when they do not exist. |
| * |
| * @param {*} db |
| * @param {*} packagePath |
| * @param {*} parentId |
| * @param {*} packageType |
| * @param {*} version |
| * @param {*} category |
| * @param {*} description |
| * @returns Promise of package insertion |
| */ |
| async function recordPackageIfNonexistent( |
| db, |
| packagePath, |
| parentId, |
| packageType, |
| version, |
| category, |
| description |
| ) { |
| let pkg = await queryPackage.getPackageByPathAndParent( |
| db, |
| packagePath, |
| parentId |
| ) |
| |
| if (pkg == null) { |
| // doesn't exist |
| return queryPackage.insertPathCrc( |
| db, |
| packagePath, |
| null, |
| packageType, |
| parentId, |
| version, |
| category, |
| description |
| ) |
| } else { |
| // Already exists |
| return pkg.id |
| } |
| } |
| |
| /** |
| * Insert the template options from the json meta data file. |
| * |
| * @param {*} db |
| * @param {*} packageId |
| * @param {*} category |
| * @param {*} externalPath |
| * @returns Promise of inserted template options |
| */ |
| async function loadTemplateOptionsFromJsonFile( |
| db, |
| packageId, |
| category, |
| externalPath |
| ) { |
| let content = await fsPromise.readFile(externalPath, 'utf8') |
| let jsonData = JSON.parse(content) |
| let codeLabels = [] |
| for (const code of Object.keys(jsonData)) { |
| codeLabels.push({ code: code, label: jsonData[code] }) |
| } |
| |
| return queryPackage.insertOptionsKeyValues( |
| db, |
| packageId, |
| category, |
| codeLabels |
| ) |
| } |
| |
| /** |
| * Given a loading context and whether the package is in sync, it records the |
| * package into the packages table and adds the packageId field into the resolved context. |
| * |
| * @param {*} context |
| * @param {*} isTopLevelPackageInSync |
| * @returns promise that resolves with the same context passed in, except packageId added to it |
| */ |
| async function recordTemplatesPackage(context, isTopLevelPackageInSync) { |
| let topLevel = await queryPackage.registerTopLevelPackage( |
| context.db, |
| context.path, |
| context.crc, |
| dbEnum.packageType.genTemplatesJson, |
| context.templateData.version, |
| context.templateData.category, |
| context.templateData.description, |
| isTopLevelPackageInSync |
| ) |
| context.packageId = topLevel.id |
| if (topLevel.existedPreviously) return context |
| |
| let promises = [] |
| let allTemplates = context.templateData.templates |
| |
| env.logDebug(`Loading ${allTemplates.length} templates.`) |
| allTemplates.forEach((template) => { |
| let templatePath = path.resolve( |
| path.join(path.dirname(context.path), template.path) |
| ) |
| if (!template.ignore) { |
| promises.push( |
| recordPackageIfNonexistent( |
| context.db, |
| templatePath, |
| context.packageId, |
| dbEnum.packageType.genSingleTemplate, |
| 0, |
| template.output, |
| template.name |
| ).then((id) => { |
| // We loaded the individual file, now we add options |
| let optionPromises = [] |
| // Handle iterator option |
| if (template.iterator) { |
| optionPromises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| id, |
| dbEnum.packageOptionCategory.outputOptions, |
| [ |
| { |
| code: 'iterator', |
| label: template.iterator |
| } |
| ] |
| ) |
| ) |
| } |
| // Handle static option |
| // If static is defined and true, then this template is only generated when generateStaticTemplates is true |
| // If static doesn't exist, we treat it as false |
| if (template.static) { |
| optionPromises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| id, |
| dbEnum.packageOptionCategory.generator, |
| [ |
| { |
| code: 'static', |
| label: template.static |
| } |
| ] |
| ) |
| ) |
| } |
| return Promise.all(optionPromises) |
| }) |
| ) |
| } |
| }) |
| |
| // Add options to the list of promises |
| if (context.templateData.options != null) { |
| for (const category of Object.keys(context.templateData.options)) { |
| let data = context.templateData.options[category] |
| |
| if (_.isString(data)) { |
| // Data is a string, so we will treat it as a relative path to the JSON file. |
| let externalPath = path.resolve( |
| path.join(path.dirname(context.path), data) |
| ) |
| promises.push( |
| loadTemplateOptionsFromJsonFile( |
| context.db, |
| context.packageId, |
| category, |
| externalPath |
| ) |
| ) |
| } else { |
| // Treat this data as an object. |
| let codeLabelArray = [] |
| for (const code of Object.keys(data)) { |
| codeLabelArray.push({ |
| code: code, |
| label: data[code] |
| }) |
| } |
| promises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| context.packageId, |
| category, |
| codeLabelArray |
| ) |
| ) |
| } |
| } |
| } |
| |
| // Deal with categories |
| let helperCategories = [] |
| if (context.templateData.categories != null) { |
| context.templateData.categories.forEach((cat) => { |
| helperCategories.push({ |
| code: cat, |
| label: '' |
| }) |
| }) |
| } |
| if (helperCategories.length > 0) { |
| promises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| context.packageId, |
| dbEnum.packageOptionCategory.helperCategories, |
| helperCategories |
| ) |
| ) |
| } |
| |
| // Deal with helpers |
| let helperAliases = [] |
| if (context.templateData.helpers != null) { |
| context.templateData.helpers.forEach((helper) => { |
| let pkg = templateEngine.findHelperPackageByAlias(helper) |
| |
| if (pkg != null) { |
| // The helper listed is an alias to a built-in helper |
| // Put it in the array to write into DB later. |
| helperAliases.push({ |
| code: helper, |
| label: '' |
| }) |
| } else { |
| // We don't have an alias by that name, so we assume it's a path. |
| let helperPath = path.join(path.dirname(context.path), helper) |
| promises.push( |
| recordPackageIfNonexistent( |
| context.db, |
| helperPath, |
| context.packageId, |
| dbEnum.packageType.genHelper, |
| null, |
| null, |
| null |
| ) |
| ) |
| } |
| }) |
| } |
| if (helperAliases.length > 0) { |
| promises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| context.packageId, |
| dbEnum.packageOptionCategory.helperAliases, |
| helperAliases |
| ) |
| ) |
| } |
| |
| // Deal with resource references |
| let resources = [] |
| if (context.templateData.resources != null) { |
| for (let key of Object.keys(context.templateData.resources)) { |
| let resourcePath = path.join( |
| path.dirname(context.path), |
| context.templateData.resources[key] |
| ) |
| if (!fs.existsSync(resourcePath)) |
| throw new Error(`Resource not found: ${resourcePath}`) |
| resources.push({ |
| code: key, |
| label: resourcePath |
| }) |
| } |
| } |
| if (resources.length > 0) { |
| promises.push( |
| queryPackage.insertOptionsKeyValues( |
| context.db, |
| context.packageId, |
| dbEnum.packageOptionCategory.resources, |
| resources |
| ) |
| ) |
| } |
| |
| // Deal with overrides |
| if (context.templateData.override != null) { |
| let overridePath = path.join( |
| path.dirname(context.path), |
| context.templateData.override |
| ) |
| promises.push( |
| recordPackageIfNonexistent( |
| context.db, |
| overridePath, |
| context.packageId, |
| dbEnum.packageType.genOverride, |
| null, |
| null, |
| null |
| ) |
| ) |
| } |
| // Deal with partials |
| if (context.templateData.partials != null) { |
| context.templateData.partials.forEach((partial) => { |
| let partialPath = path.join(path.dirname(context.path), partial.path) |
| promises.push( |
| queryPackage.insertPathCrc( |
| context.db, |
| partialPath, |
| null, |
| dbEnum.packageType.genPartial, |
| context.packageId, |
| 0, |
| partial.name, |
| '' |
| ) |
| ) |
| }) |
| } |
| |
| // Deal with zcl extensions |
| if (context.templateData.zcl != null) { |
| let zclExtension = context.templateData.zcl |
| promises.push( |
| loadZclExtensions( |
| context.db, |
| context.packageId, |
| zclExtension, |
| context.path |
| ) |
| ) |
| } |
| await Promise.all(promises) |
| return context |
| } |
| |
| /** |
| * This method takes extension data in JSON, and converts it into |
| * an object that contains: |
| * entityCode, entityQualifier, parentCode, manufacturerCode and value |
| * @param {*} entityType |
| * @param {*} entity |
| * @returns object that can be used for database injection |
| */ |
| function decodePackageExtensionEntity(entityType, entity) { |
| switch (entityType) { |
| case dbEnum.packageExtensionEntity.cluster: |
| return { |
| entityCode: entity.clusterCode, |
| entityQualifier: entity.role, |
| manufacturerCode: null, |
| parentCode: null, |
| value: entity.value |
| } |
| case dbEnum.packageExtensionEntity.command: |
| return { |
| entityCode: parseInt(entity.commandCode), |
| entityQualifier: entity.source, |
| manufacturerCode: null, |
| parentCode: parseInt(entity.clusterCode), |
| value: entity.value |
| } |
| case dbEnum.packageExtensionEntity.event: |
| return { |
| entityCode: parseInt(entity.eventCode), |
| manufacturerCode: null, |
| parentCode: parseInt(entity.clusterCode), |
| value: entity.value |
| } |
| case dbEnum.packageExtensionEntity.attribute: |
| return { |
| entityCode: parseInt(entity.attributeCode), |
| entityQualifier: null, |
| manufacturerCode: null, |
| parentCode: parseInt(entity.clusterCode), |
| value: entity.value |
| } |
| case dbEnum.packageExtensionEntity.deviceType: |
| return { |
| entityCode: entity.device, |
| entityQualifier: null, |
| manufacturerCode: null, |
| parentCode: null, |
| value: entity.value |
| } |
| case dbEnum.packageExtensionEntity.attributeType: |
| return { |
| entityCode: null, |
| entityQualifier: entity.type, |
| manufacturerCode: null, |
| parentCode: null, |
| value: entity.value |
| } |
| default: |
| // We don't know how to process defaults otherwise |
| return null |
| } |
| } |
| |
| /** |
| * Returns a promise that will load the zcl extensions. |
| * |
| * @param {*} zclExt |
| * @returns Promise of loading the zcl extensions. |
| */ |
| async function loadZclExtensions(db, packageId, zclExt, defaultsPath) { |
| let promises = [] |
| for (const entity of Object.keys(zclExt)) { |
| let entityExtension = zclExt[entity] |
| let propertyArray = [] |
| let defaultArrayOfArrays = [] |
| for (const property of Object.keys(entityExtension)) { |
| let prop = entityExtension[property] |
| propertyArray.push({ |
| property: property, |
| type: prop.type, |
| configurability: prop.configurability, |
| label: prop.label, |
| globalDefault: prop.globalDefault |
| }) |
| if ('defaults' in prop) { |
| if ( |
| typeof prop.defaults === 'string' || |
| prop.defaults instanceof String |
| ) { |
| // Data is a string, so we will treat it as a relative path to the JSON file. |
| let externalPath = path.resolve( |
| path.join(path.dirname(defaultsPath), prop.defaults) |
| ) |
| let data = await fsPromise |
| .readFile(externalPath, 'utf8') |
| .then((content) => JSON.parse(content)) |
| .catch((err) => { |
| env.logWarning( |
| `Invalid file! Failed to load defaults from: ${prop.defaults}` |
| ) |
| queryNotification.setNotification( |
| db, |
| 'WARNING', |
| `Invalid file! Failed to load defaults from: ${prop.defaults}`, |
| packageId, |
| 2 |
| ) |
| }) |
| |
| if (data) { |
| if (!Array.isArray(data)) { |
| env.logWarning( |
| `Invalid file format! Failed to load defaults from: ${prop.defaults}` |
| ) |
| queryNotification.setNotification( |
| db, |
| 'WARNING', |
| `Invalid file format! Failed to load defaults from: ${prop.defaults}`, |
| packageId, |
| 2 |
| ) |
| } else { |
| defaultArrayOfArrays.push( |
| data.map((x) => decodePackageExtensionEntity(entity, x)) |
| ) |
| } |
| } |
| } else { |
| defaultArrayOfArrays.push( |
| prop.defaults.map((x) => decodePackageExtensionEntity(entity, x)) |
| ) |
| } |
| } else { |
| defaultArrayOfArrays.push(null) |
| } |
| } |
| promises.push( |
| queryPackage.insertPackageExtension( |
| db, |
| packageId, |
| entity, |
| propertyArray, |
| defaultArrayOfArrays |
| ) |
| ) |
| } |
| return Promise.all(promises) |
| } |
| |
| /** |
| * Api that loads an array of template JSON files or a single file if |
| * you just pass in one String. |
| * |
| * @param {*} db |
| * @param {*} genTemplatesJsonArray |
| */ |
| async function loadTemplates( |
| db, |
| genTemplatesJsonArray, |
| options = { |
| failOnLoadingError: true |
| } |
| ) { |
| if (Array.isArray(genTemplatesJsonArray)) { |
| let globalCtx = { |
| packageIds: [], |
| packageId: null, |
| templateData: [] |
| } |
| if (genTemplatesJsonArray != null && genTemplatesJsonArray.length > 0) { |
| for (let jsonFile of genTemplatesJsonArray) { |
| if (jsonFile == null || jsonFile == '') continue |
| let ctx = await loadGenTemplatesJsonFile(db, jsonFile) |
| if (ctx.error) { |
| if (options.failOnLoadingError) globalCtx.error = ctx.error |
| } else { |
| if (globalCtx.packageId == null) { |
| globalCtx.packageId = ctx.packageId |
| } |
| globalCtx.templateData.push(ctx.templateData) |
| globalCtx.packageIds.push(ctx.packageId) |
| } |
| } |
| } |
| return globalCtx |
| } else if (genTemplatesJsonArray != null) { |
| let ctx = await loadGenTemplatesJsonFile(db, genTemplatesJsonArray) |
| ctx.packageIds = [ctx.packageId] |
| return ctx |
| } else { |
| // We didn't load anything, we don't return anything. |
| return { |
| nop: true |
| } |
| } |
| } |
| |
| /** |
| * Main API async function to load templates from a gen-template.json file. |
| * |
| * @param {*} db Database |
| * @param {*} genTemplatesJson Path to the JSON file or an array of paths to JSON file |
| * @returns the loading context, contains: db, path, crc, packageId and templateData, or error |
| */ |
| async function loadGenTemplatesJsonFile(db, genTemplatesJson) { |
| let context = { |
| db: db |
| } |
| if (genTemplatesJson == null) { |
| context.error = 'No templates file specified.' |
| env.logWarning(context.error) |
| return Promise.resolve(context) |
| } |
| let isTransactionAlreadyExisting = dbApi.isTransactionActive() |
| |
| let file = path.resolve(genTemplatesJson) |
| if (!fs.existsSync(file)) { |
| context.error = `Can't locate templates file: ${file}` |
| env.logWarning(context.error) |
| return context |
| } |
| context.path = file |
| if (!isTransactionAlreadyExisting) await dbApi.dbBeginTransaction(db) |
| try { |
| Object.assign(context, await loadGenTemplateFromFile(file)) |
| let isTopLevelPackageInSync = true |
| // Check if that package already exist with the same crc |
| let existingPackage = await queryPackage.getPackageByPathAndType( |
| db, |
| file, |
| dbEnum.packageType.genTemplatesJson |
| ) |
| if (existingPackage && existingPackage.crc !== context.crc) { |
| // Package crc has changed so turning the old package out of sync(IN_SYNC=0) |
| await queryPackage.updatePackageIsInSync(db, existingPackage.id, 0) |
| isTopLevelPackageInSync = false |
| } |
| context = await recordTemplatesPackage(context, isTopLevelPackageInSync) |
| return context |
| } catch (err) { |
| env.logInfo(`Can not read templates from: ${file}`) |
| throw err |
| } finally { |
| if (!isTransactionAlreadyExisting) await dbApi.dbCommit(db) |
| } |
| } |
| |
| /** |
| * Get the package information from the given package ID. |
| * |
| * @param {*} db |
| * @param {*} genTemplatesPkgId |
| * @returns package information |
| */ |
| async function retrievePackageMetaInfo(db, genTemplatesPkgId) { |
| let metaInfo = { |
| aliases: [], |
| categories: [], |
| resources: {} |
| } |
| |
| let aliases = await queryPackage.selectAllOptionsValues( |
| db, |
| genTemplatesPkgId, |
| dbEnum.packageOptionCategory.helperAliases |
| ) |
| for (let a of aliases) { |
| metaInfo.aliases.push(a.optionCode) |
| } |
| |
| let categories = await queryPackage.selectAllOptionsValues( |
| db, |
| genTemplatesPkgId, |
| dbEnum.packageOptionCategory.helperCategories |
| ) |
| for (let c of categories) { |
| metaInfo.categories.push(c.optionCode) |
| } |
| |
| let resources = await queryPackage.selectAllOptionsValues( |
| db, |
| genTemplatesPkgId, |
| dbEnum.packageOptionCategory.resources |
| ) |
| for (let c of resources) { |
| metaInfo.resources[c.optionCode] = c.optionLabel |
| } |
| |
| return metaInfo |
| } |
| |
| /** |
| * Generates all the templates inside a toplevel package. |
| * |
| * @param {*} genResult |
| * @param {*} genTemplateJsonPkg Package that points to genTemplate.json file |
| * @param {*} generateOnly if NULL then generate all templates, else only generate template whose out file name matches this. |
| * @returns Promise that resolves with genResult, that contains all the generated templates, keyed by their 'output' |
| */ |
| async function generateAllTemplates( |
| genResult, |
| genTemplateJsonPkg, |
| options = { |
| generateOnly: null, |
| disableDeprecationWarnings: false, |
| generateSequentially: false |
| } |
| ) { |
| let packages = await queryPackage.getPackageByParent( |
| genResult.db, |
| genTemplateJsonPkg.id |
| ) |
| let generationTemplates = [] |
| let overridePath = null |
| |
| // generateStaticTemplates can be a session-key, if it exists then use it from session key otherwise use it from generatorOptions |
| let generateStaticTemplates = await querySession.getSessionKeyValue( |
| genResult.db, |
| genResult.sessionId, |
| dbEnum.sessionKey.generateStaticTemplates |
| ) |
| if (generateStaticTemplates == null) { |
| generateStaticTemplates = genResult.generatorOptions.generateStaticTemplates |
| } |
| // Convert string values to proper boolean - generateStaticTemplates defaults to true unless explicitly set to false |
| generateStaticTemplates = |
| generateStaticTemplates !== 'false' && generateStaticTemplates !== false |
| |
| let hb = templateEngine.hbInstance() |
| let context = { |
| db: genResult.db, |
| sessionId: genResult.sessionId, |
| hb: hb |
| } |
| |
| for (let pkg of packages) { |
| let outputOptions = await queryPackage.selectAllOptionsValues( |
| genResult.db, |
| pkg.id, |
| dbEnum.packageOptionCategory.outputOptions |
| ) |
| outputOptions.forEach((opt) => { |
| if (opt.optionCode == 'iterator') { |
| pkg.iterator = opt.optionLabel |
| } |
| }) |
| let generatorOptions = await queryPackage.selectAllOptionsValues( |
| genResult.db, |
| pkg.id, |
| dbEnum.packageOptionCategory.generator |
| ) |
| generatorOptions.forEach((opt) => { |
| if (opt.optionCode == 'static') { |
| pkg.static = opt.optionLabel |
| } |
| }) |
| } |
| // First extract overridePath if one exists, as we need to |
| // pass it to the generation. |
| packages.forEach((singlePkg) => { |
| if (singlePkg.type == dbEnum.packageType.genOverride) { |
| overridePath = singlePkg.path |
| } |
| }) |
| |
| // Next load the partials |
| const partialPackages = packages.filter( |
| (pkg) => pkg.type === dbEnum.packageType.genPartial |
| ) |
| const partialDataArray = await Promise.all( |
| partialPackages.map((pkg) => fsPromise.readFile(pkg.path, 'utf8')) |
| ) |
| partialDataArray.forEach((data, index) => { |
| const singlePkg = partialPackages[index] |
| templateEngine.loadPartial(hb, singlePkg.category, data) |
| }) |
| |
| // Let's collect the required list of helpers. |
| let metaInfo = await retrievePackageMetaInfo( |
| genResult.db, |
| genTemplateJsonPkg.id |
| ) |
| |
| // Initialize helpers package. This is based on the specific |
| // list that was calculated above in the `metaInfo` |
| templateEngine.initializeBuiltInHelpersForPackage(hb, metaInfo) |
| |
| // Next load the addon helpers which were not yet initialized earlier. |
| packages.forEach((singlePkg) => { |
| if (singlePkg.type == dbEnum.packageType.genHelper) { |
| templateEngine.loadHelper(hb, singlePkg.path, context) |
| } |
| }) |
| |
| // Next prepare the templates |
| packages.forEach((singlePkg) => { |
| if (singlePkg.type == dbEnum.packageType.genSingleTemplate) { |
| // Skip static templates if generateStaticTemplates is false |
| if (!generateStaticTemplates && singlePkg.static === 'true') { |
| return |
| } |
| if (options.generateOnly == null) { |
| generationTemplates.push(singlePkg) |
| } else if ( |
| Array.isArray(options.generateOnly) && |
| options.generateOnly.includes(singlePkg.category) |
| ) { |
| // If generateOnly is an array that contains the name |
| generationTemplates.push(singlePkg) |
| } else if (options.generateOnly == singlePkg.category) { |
| // If the generate Only contains the name, then we include it |
| generationTemplates.push(singlePkg) |
| } |
| } |
| }) |
| |
| if (options.generateSequentially) { |
| await util.executePromisesSequentially(generationTemplates, (t) => |
| generateSingleTemplate(hb, metaInfo, genResult, t, genTemplateJsonPkg, { |
| overridePath: overridePath, |
| disableDeprecationWarnings: options.disableDeprecationWarnings |
| }) |
| ) |
| } else { |
| let templates = generationTemplates.map((pkg) => |
| generateSingleTemplate(hb, metaInfo, genResult, pkg, genTemplateJsonPkg, { |
| overridePath: overridePath, |
| disableDeprecationWarnings: options.disableDeprecationWarnings |
| }) |
| ) |
| await Promise.all(templates) |
| } |
| |
| if (genResult.hasErrors) { |
| for (const [key, value] of Object.entries(genResult.errors)) { |
| console.error(`${key}: ${value}`) |
| } |
| } |
| genResult.partial = false |
| return genResult |
| } |
| |
| /** |
| * Function that generates a single package and adds it to the generation result. |
| * |
| * @param {*} genResult |
| * @param {*} singleTemplatePkg Single template package. |
| * @returns promise that resolves with the genResult, with newly generated content added. |
| */ |
| async function generateSingleTemplate( |
| hb, |
| metaInfo, |
| genResult, |
| singleTemplatePkg, |
| genTemplateJsonPackage, |
| options = { |
| overridePath: null, |
| disableDeprecationWarnings: false |
| } |
| ) { |
| let genStart = process.hrtime.bigint() |
| //console.log(`Start generating from template: ${singleTemplatePkg?.path}`) |
| env.logInfo(`Start generating from template: ${singleTemplatePkg?.path}`) |
| let genFunction |
| if (singleTemplatePkg.iterator != null) { |
| genFunction = templateEngine.produceIterativeContent |
| } else { |
| genFunction = templateEngine.produceContent |
| } |
| try { |
| let resultArray = await genFunction( |
| hb, |
| metaInfo, |
| genResult.db, |
| genResult.sessionId, |
| singleTemplatePkg, |
| genTemplateJsonPackage, |
| options |
| ) |
| for (let result of resultArray) { |
| if (!result.content) { |
| console.error(`No content generated for ${result.key}`) |
| } |
| genResult.content[result.key] = result.content |
| genResult.stats[result.key] = result.stats |
| } |
| genResult.partial = true |
| let nsDuration = process.hrtime.bigint() - genStart |
| //console.log(`Finish generating from template: ${singleTemplatePkg?.path}: ${util.duration(nsDuration)}`) |
| env.logInfo( |
| `Finish generating from template: ${ |
| singleTemplatePkg?.path |
| }: ${util.duration(nsDuration)}` |
| ) |
| return genResult |
| } catch (err) { |
| genResult.errors[singleTemplatePkg.category] = err |
| genResult.hasErrors = true |
| } |
| } |
| |
| /** |
| * Main API async function to generate stuff. |
| * |
| * @param {*} db Database |
| * @param {*} sessionId |
| * @param {*} templatePackageId packageId Template package id. It can be either single template or gen template json. |
| * @returns Promise that resolves into a generation result. |
| * @param {*} templateGeneratorOptions |
| * @param {*} options |
| * @returns Promise that resolves into a generation result. |
| */ |
| async function generate( |
| db, |
| sessionId, |
| templatePackageId, |
| templateGeneratorOptions = {}, |
| options = { |
| generateOnly: null, |
| disableDeprecationWarnings: false |
| } |
| ) { |
| let pkg = await queryPackage.getPackageByPackageId(db, templatePackageId) |
| if (pkg == null) throw new Error(`Invalid packageId: ${templatePackageId}`) |
| let genResult = { |
| db: db, |
| sessionId: sessionId, |
| content: {}, |
| stats: {}, |
| errors: {}, |
| hasErrors: false, |
| generatorOptions: templateGeneratorOptions, |
| templatePath: path.dirname(pkg.path) |
| } |
| if (pkg.type === dbEnum.packageType.genTemplatesJson) { |
| return generateAllTemplates(genResult, pkg, options) |
| } else { |
| throw new Error(`Invalid package type: ${pkg.type}`) |
| } |
| } |
| |
| /** |
| * Promise to write out a file, optionally creating a backup. |
| * |
| * @param {*} fileName |
| * @param {*} content |
| * @param {*} doBackup |
| * @returns promise of a written file. |
| */ |
| async function writeFileWithBackup(fileName, content, doBackup) { |
| if (doBackup && fs.existsSync(fileName)) { |
| let backupName = fileName.concat('~') |
| await fsPromise.rename(fileName, backupName) |
| return fsPromise.writeFile(fileName, content) |
| } else { |
| // we need to ensure that directories exist. |
| await fsPromise.mkdir(path.dirname(fileName), { recursive: true }) |
| return fsPromise.writeFile(fileName, content) |
| } |
| } |
| |
| /** |
| * Returns a promise that resolves into a content that should be written out to gen result file. |
| * |
| * @param {*} genResult |
| */ |
| async function generateGenerationContent(genResult, timing = {}) { |
| let out = { |
| writeTime: new Date().toString(), |
| featureLevel: env.zapVersion().featureLevel, |
| creator: 'zap', |
| content: [], |
| timing: timing, |
| stats: {} |
| } |
| for (const f of Object.keys(genResult.content).sort((a, b) => |
| a.localeCompare(b) |
| )) { |
| out.content.push(f) |
| } |
| return Promise.resolve(JSON.stringify(out, null, 2)) |
| } |
| |
| /** |
| * Generate files and write them into the given directory. |
| * |
| * @param {*} db |
| * @param {*} sessionId |
| * @param {*} packageId |
| * @param {*} outputDirectory |
| * @returns a promise which will resolve when all the files are written. |
| */ |
| async function generateAndWriteFiles( |
| db, |
| sessionId, |
| templatePackageId, |
| outputDirectory, |
| options = { |
| logger: (msg) => { |
| // Empty logger is the default. |
| }, |
| backup: false, |
| genResultFile: false, |
| skipPostGeneration: false, |
| appendGenerationSubdirectory: false, |
| generationLog: null |
| } |
| ) { |
| // in case user customization has invalidated the cache |
| dbCache.clear() |
| |
| let timing = {} |
| if (options.fileLoadTime) { |
| timing.fileLoad = { |
| nsDuration: Number(options.fileLoadTime), |
| readableDuration: util.duration(options.fileLoadTime) |
| } |
| } |
| let hrstart = process.hrtime.bigint() |
| let genOptions = await queryPackage.selectAllOptionsValues( |
| db, |
| templatePackageId, |
| dbEnum.packageOptionCategory.generator |
| ) |
| |
| // Reduce the long array from query into a single object |
| let templateGeneratorOptions = genOptions.reduce((acc, current) => { |
| acc[current.optionCode] = current.optionLabel |
| return acc |
| }, {}) |
| |
| let genResult = await generate( |
| db, |
| sessionId, |
| templatePackageId, |
| templateGeneratorOptions |
| ) |
| |
| // The path we append, assuming you specify the --appendGenerationSubdirectory, and a |
| // appendDirectory generator option is present in the genTemplates.json file |
| let appendedPath = null |
| if ( |
| templateGeneratorOptions.appendDirectory != null && |
| options.appendGenerationSubdirectory |
| ) { |
| appendedPath = templateGeneratorOptions.appendDirectory |
| } |
| |
| if (appendedPath != null) { |
| outputDirectory = path.join(outputDirectory, appendedPath) |
| } |
| |
| if (!fs.existsSync(outputDirectory)) { |
| options.logger( |
| env.formatEmojiMessage('✅', `Creating directory: ${outputDirectory}`) |
| ) |
| fs.mkdirSync(outputDirectory, { recursive: true }) |
| } |
| |
| options.logger(env.formatEmojiMessage('🤖', 'Generating files:')) |
| let promises = [] |
| for (const f of Object.keys(genResult.content)) { |
| let content = genResult.content[f] |
| let fileName = path.join(outputDirectory, f) |
| options.logger(env.formatEmojiMessage('✍', fileName)) |
| env.logDebug(`Preparing to write file: ${fileName}`) |
| promises.push(writeFileWithBackup(fileName, content, options.backup)) |
| } |
| if (genResult.hasErrors) { |
| options.logger(env.formatEmojiMessage('⚠️', 'Errors:')) |
| for (const f of Object.keys(genResult.errors)) { |
| let err = genResult.errors[f] |
| let fileName = path.join(outputDirectory, f) |
| options.logger( |
| `${env.formatEmojiMessage('👎', `${fileName}:`)} ${env.formatEmojiMessage('⛔', err)}\nStack trace:\n` |
| ) |
| options.logger(err) |
| } |
| } |
| let nsDuration = process.hrtime.bigint() - hrstart |
| options.logger( |
| env.formatEmojiMessage( |
| '🕐', |
| `Generation time: ${util.duration(nsDuration)} ` |
| ) |
| ) |
| timing.generation = { |
| nsDuration: Number(nsDuration), |
| readableDuration: util.duration(nsDuration) |
| } |
| promises.push( |
| generateGenerationContent(genResult, timing).then((generatedContent) => { |
| if (options.genResultFile) { |
| let resultPath = path.join(outputDirectory, 'genResult.json') |
| options.logger(env.formatEmojiMessage('✍', `Result: ${resultPath}`)) |
| return writeFileWithBackup(resultPath, generatedContent, options.backup) |
| } else { |
| return |
| } |
| }) |
| ) |
| |
| if (options.generationLog) { |
| let pkg = await queryPackage.getPackageByPackageId(db, templatePackageId) |
| let filePath = await querySession.getSessionKeyValue( |
| db, |
| sessionId, |
| dbEnum.sessionKey.filePath |
| ) |
| let zclPkg = await queryPackage.getSessionPackagesByType( |
| db, |
| sessionId, |
| dbEnum.packageType.zclProperties |
| ) |
| promises.push( |
| createGenerationLog(options.generationLog, { |
| zapFile: filePath, |
| output: outputDirectory, |
| templatePath: pkg.path, |
| zclPath: zclPkg.path |
| }) |
| ) |
| } |
| |
| await Promise.all(promises) |
| |
| if (options.skipPostGeneration) { |
| return genResult |
| } else { |
| return postProcessGeneratedFiles(outputDirectory, genResult, options.logger) |
| } |
| } |
| |
| /** |
| * Create a generation log. |
| * |
| * @param {*} logFile |
| * @param {*} genData |
| */ |
| async function createGenerationLog(logFile, genData) { |
| try { |
| let jsonData |
| if (fs.existsSync(logFile)) { |
| let data = await fsPromise.readFile(logFile) |
| jsonData = JSON.parse(data) |
| } else { |
| jsonData = [] |
| } |
| jsonData.push(genData) |
| await fsPromise.writeFile(logFile, JSON.stringify(jsonData, null, 2)) |
| } catch (err) { |
| console.log(err) |
| console.log(`Could not write log: ${logFile}`) |
| } |
| } |
| |
| /** |
| * Executes post processing actions as defined by the gen-templates.json |
| * |
| * @param {*} outputDirectory |
| * @param {*} genResult |
| * @returns promise of a dealt-with post processing actions |
| */ |
| async function postProcessGeneratedFiles( |
| outputDirectory, |
| genResult, |
| logger = (msg) => { |
| // Empty logger is the default. |
| } |
| ) { |
| let doExecute = true |
| let isEnabledS = genResult.generatorOptions[dbEnum.generatorOptions.enabled] |
| let f = |
| genResult.generatorOptions[ |
| dbEnum.generatorOptions.postProcessConditionalFile |
| ] |
| |
| let isEnabled = true |
| if (isEnabledS == 'false' || isEnabledS == '0') isEnabled = false |
| if (!isEnabled) { |
| // If `enabled` is false, then we do nothing. |
| doExecute = false |
| } else if (f != null) { |
| // If `postProcessConditionalFile' doesn't exist, we also do nothing. |
| f = path.join(genResult.templatePath, f) |
| if (!fs.existsSync(f)) doExecute = false |
| } |
| |
| // Now we deal with postProcessing |
| let postProcessPromises = [] |
| if ( |
| doExecute && |
| dbEnum.generatorOptions.postProcessMulti in genResult.generatorOptions |
| ) { |
| let cmd = |
| genResult.generatorOptions[dbEnum.generatorOptions.postProcessMulti] |
| for (const genFile of Object.keys(genResult.content)) { |
| let fileName = path.join(outputDirectory, genFile) |
| cmd = cmd + ' ' + fileName |
| } |
| postProcessPromises.push( |
| util.executeExternalProgram(cmd, genResult.templatePath, { |
| rejectOnFail: false, |
| routeErrToOut: |
| genResult.generatorOptions[dbEnum.generatorOptions.routeErrToOut] |
| }) |
| ) |
| } |
| if ( |
| doExecute && |
| dbEnum.generatorOptions.postProcessSingle in genResult.generatorOptions |
| ) { |
| let cmd = |
| genResult.generatorOptions[dbEnum.generatorOptions.postProcessSingle] |
| for (const genFile of Object.keys(genResult.content)) { |
| let fileName = path.join(outputDirectory, genFile) |
| let singleCmd = cmd + ' ' + fileName |
| postProcessPromises.push( |
| util.executeExternalProgram(singleCmd, genResult.templatePath, { |
| rejectOnFail: false |
| }) |
| ) |
| } |
| } |
| if (postProcessPromises.length > 0) |
| logger(env.formatEmojiMessage('🤖', 'Executing post-processing actions:')) |
| return Promise.all(postProcessPromises).then(() => genResult) |
| } |
| |
| /** |
| * This async function takes a string, and resolves a preview object out of it. |
| * |
| * @param {*} content String to form into preview. |
| */ |
| async function contentIndexer(content, linesPerIndex = 2000) { |
| let index = 0 |
| let indexedResult = {} |
| let code = content.split(/\n/) |
| let loc = code.length |
| |
| if (content == null || content.length == 0) { |
| return Promise.resolve(indexedResult) |
| } |
| |
| // Indexing the generation result for faster preview pane generation |
| for (let i = 0; i < loc; i++) { |
| if (i % linesPerIndex === 0) { |
| index++ |
| indexedResult[index] = '' |
| } |
| indexedResult[index] = indexedResult[index].concat(code[i]).concat('\n') |
| } |
| return Promise.resolve(indexedResult) |
| } |
| |
| /** |
| * Generates a single file and feeds it back for preview. |
| * |
| * @param {*} db |
| * @param {*} sessionId |
| * @param {*} fileName |
| * @returns promise that resolves into a preview object. |
| */ |
| async function generateSingleFileForPreview(db, sessionId, outFileName) { |
| // in case user customization has invalidated the cache |
| dbCache.clear() |
| |
| return queryPackage |
| .getSessionPackagesByType( |
| db, |
| sessionId, |
| dbEnum.packageType.genTemplatesJson |
| ) |
| .then((pkgs) => { |
| let promises = [] |
| pkgs.forEach((pkg) => { |
| promises.push( |
| generate( |
| db, |
| sessionId, |
| pkg.id, |
| {}, |
| { |
| generateOnly: outFileName, |
| disableDeprecationWarnings: true |
| } |
| ) |
| ) |
| }) |
| return Promise.all(promises) |
| }) |
| .then((genResultArrays) => { |
| let content = '' |
| genResultArrays.forEach((gr) => { |
| if (outFileName in gr.content) { |
| content = gr.content[outFileName] |
| } |
| }) |
| return content |
| }) |
| .then((content) => contentIndexer(content)) |
| } |
| |
| exports.loadTemplates = loadTemplates |
| exports.generateAndWriteFiles = generateAndWriteFiles |
| exports.generateSingleFileForPreview = generateSingleFileForPreview |
| exports.contentIndexer = contentIndexer |
| exports.generate = generate |