更新
更旧
/**
*
* 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 queryPackage = require('../db/query-package.js')
const querySession = require('../db/query-session')
const dbEnum = require('../../src-shared/db-enum.js')

Timotej Ecimovic
已提交
const templateEngine = require('./template-engine.js')
const dbApi = require('../db/db-api.js')
const queryNotification = require('../db/query-package-notification.js')
/**
* Given a path, it will read generation template object into memory.
*
* @param {*} path
* @returns Object that contains: data, crc, templateData
async function loadGenTemplateFromFile(path) {
let ret = {}
ret.data = await fsPromise.readFile(path, 'utf8')
ret.crc = util.checksum(ret.data)
ret.templateData = JSON.parse(ret.data)
if ('requiredFeatureLevel' in ret.templateData) {
requiredFeatureLevel = ret.templateData.requiredFeatureLevel
let status = util.matchFeatureLevel(requiredFeatureLevel, path)

Timotej Ecimovic
已提交
db,
packagePath,
parentId,
packageType,

Timotej Ecimovic
已提交
) {
let pkg = await queryPackage.getPackageByPathAndParent(
db,
packagePath,
parentId
)
if (pkg == null) {
// doesn't exist
return queryPackage.insertPathCrc(
db,
packagePath,
null,
packageType,
parentId,
)
} else {
// Already exists
return pkg.id
}
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, it records the package into the packages table and adds the packageId field into the resolved context.
*
* @param {*} context
* @returns promise that resolves with the same context passed in, except packageId added to it
*/
async function recordTemplatesPackage(context) {
let topLevel = await queryPackage.registerTopLevelPackage(
context.db,
context.path,
context.crc,
dbEnum.packageType.genTemplatesJson,
context.templateData.version,
context.templateData.category,
context.templateData.description,
true
context.packageId = topLevel.id
if (topLevel.existedPreviously) return context
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,
).then((id) => {
// We loaded the individual file, now we add options
if (template.iterator) {
return queryPackage.insertOptionsKeyValues(
context.db,
id,
dbEnum.packageOptionCategory.outputOptions,
[
{
code: 'iterator',
label: template.iterator,
},
]
)
}
})
// 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(
context.db,
context.packageId,
)
)
} else {
// Treat this data as an object.
let codeLabelArray = []
for (const code of Object.keys(data)) {
codeLabelArray.push({
code: code,
label: data[code],
})

Timotej Ecimovic
已提交
promises.push(
context.db,
context.packageId,

Timotej Ecimovic
已提交
)
}
// 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
)
)
}
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
)
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
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
)
)
}
// 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,
}
// Deal with zcl extensions
if (context.templateData.zcl != null) {
let zclExtension = context.templateData.zcl
promises.push(
loadZclExtensions(
context.db,
context.packageId,
zclExtension,
context.path
)
)
}
/**
* 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,

Timotej Ecimovic
已提交
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
}
}

Timotej Ecimovic
已提交
/**
* 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) {
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) => {
`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)) {
`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)

Timotej Ecimovic
已提交
}
/**
* 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,

Bharat Raju
已提交
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
}

Bharat Raju
已提交
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 {*} genTemplatesJson Path to the JSON file or an array of paths to JSON file

Timotej Ecimovic
已提交
* @returns the loading context, contains: db, path, crc, packageId and templateData, or error
async function loadGenTemplatesJsonFile(db, genTemplatesJson) {

Timotej Ecimovic
已提交
if (genTemplatesJson == null) {
context.error = 'No templates file specified.'
env.logWarning(context.error)

Timotej Ecimovic
已提交
return Promise.resolve(context)
}
let isTransactionAlreadyExisting = dbApi.isTransactionActive()

Timotej Ecimovic
已提交
let file = path.resolve(genTemplatesJson)

Timotej Ecimovic
已提交
if (!fs.existsSync(file)) {
context.error = `Can't locate templates file: ${file}`
env.logWarning(context.error)

Timotej Ecimovic
已提交
}
context.path = file
if (!isTransactionAlreadyExisting) await dbApi.dbBeginTransaction(db)
Object.assign(context, await loadGenTemplateFromFile(file))
context = await recordTemplatesPackage(context)
return context
} catch (err) {
env.logInfo(`Can not read templates from: ${file}`)
throw err
} finally {
if (!isTransactionAlreadyExisting) await dbApi.dbCommit(db)
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
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
}

Timotej Ecimovic
已提交
/**
* 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.

Timotej Ecimovic
已提交
* @returns Promise that resolves with genResult, that contains all the generated templates, keyed by their 'output'
*/
genResult,
genTemplateJsonPkg,
options = {
generateOnly: null,
disableDeprecationWarnings: false,
generateSequentially: false,
let packages = await queryPackage.getPackageByParent(
genResult.db,
genTemplateJsonPkg.id
)
let generationTemplates = []
let helperPromises = []
let partialPromises = []
let overridePath = null
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
}
})
}
// 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
packages.forEach((singlePkg) => {
if (singlePkg.type == dbEnum.packageType.genPartial) {
partialPromises.push(
templateEngine.loadPartial(hb, singlePkg.category, singlePkg.path)
)
}
})
// 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) {
helperPromises.push(
templateEngine.loadHelper(hb, singlePkg.path, context)
)
}
})
// Next prepare the templates
packages.forEach((singlePkg) => {
if (singlePkg.type == dbEnum.packageType.genSingleTemplate) {
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)
}
}
})
// And finally go over the actual templates.
await Promise.all(helperPromises)
await Promise.all(partialPromises)
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)
}
genResult.partial = false
return genResult

Timotej Ecimovic
已提交
/**
* Function that generates a single package and adds it to the generation result.
*
* @param {*} genResult
* @param {*} singleTemplatePkg Single template package.

Timotej Ecimovic
已提交
* @returns promise that resolves with the genResult, with newly generated content added.
*/
hb,
genResult,
singleTemplatePkg,
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
}
hb,
genResult.db,
genResult.sessionId,
singleTemplatePkg,
for (let result of resultArray) {
genResult.content[result.key] = result.content
genResult.stats[result.key] = result.stats
}
let nsDuration = process.hrtime.bigint() - genStart
//console.log(`Finish generating from template: ${singleTemplatePkg?.path}: ${util.duration(nsDuration)}`)
`Finish generating from template: ${
singleTemplatePkg?.path
}: ${util.duration(nsDuration)}`
return genResult
} catch (err) {
genResult.errors[singleTemplatePkg.category] = err
genResult.hasErrors = true
}

Bharat Raju
已提交
* @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.
*/

Timotej Ecimovic
已提交
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('~')

Timotej Ecimovic
已提交
await fsPromise.rename(fileName, backupName)
return fsPromise.writeFile(fileName, content)

Timotej Ecimovic
已提交
// 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 = {}) {
writeTime: new Date().toString(),

Timotej Ecimovic
已提交
featureLevel: env.zapVersion().featureLevel,
creator: 'zap',
content: [],
for (const f of Object.keys(genResult.content).sort()) {
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.
*/

Timotej Ecimovic
已提交
templatePackageId,
options = {
logger: (msg) => {
// Empty logger is the default.
},
backup: false,
skipPostGeneration: false,
appendGenerationSubdirectory: false,
// 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),
}
}

Timotej Ecimovic
已提交
let genOptions = await queryPackage.selectAllOptionsValues(
db,
templatePackageId,
dbEnum.packageOptionCategory.generator
)

Timotej Ecimovic
已提交
// 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)
}

Timotej Ecimovic
已提交
if (!fs.existsSync(outputDirectory)) {
options.logger(`✅ Creating directory: ${outputDirectory}`)
fs.mkdirSync(outputDirectory, { recursive: true })
}

Timotej Ecimovic
已提交
options.logger('🤖 Generating files:')
let promises = []
for (const f of Object.keys(genResult.content)) {

Timotej Ecimovic
已提交
let content = genResult.content[f]
let fileName = path.join(outputDirectory, f)
options.logger(` ✍ ${fileName}`)
env.logDebug(`Preparing to write file: ${fileName}`)
promises.push(writeFileWithBackup(fileName, content, options.backup))
}
if (genResult.hasErrors) {
options.logger('⚠️ Errors:')
for (const f of Object.keys(genResult.errors)) {

Timotej Ecimovic
已提交
let err = genResult.errors[f]
let fileName = path.join(outputDirectory, f)
options.logger(` 👎 ${fileName}: ⛔ ${err}\nStack trace:\n`)
options.logger(err)
}
}
let nsDuration = process.hrtime.bigint() - hrstart
options.logger(`🕐 Generation time: ${util.duration(nsDuration)} `)
timing.generation = {
nsDuration: Number(nsDuration),
readableDuration: util.duration(nsDuration),
}

Timotej Ecimovic
已提交
promises.push(
generateGenerationContent(genResult, timing).then((generatedContent) => {

Timotej Ecimovic
已提交
if (options.genResultFile) {
let resultPath = path.join(outputDirectory, 'genResult.json')
options.logger(` ✍ Result: ${resultPath}`)
return writeFileWithBackup(resultPath, generatedContent, options.backup)

Timotej Ecimovic
已提交
} else {
return
}

Timotej Ecimovic
已提交
)
if (options.generationLog) {
let pkg = await queryPackage.getPackageByPackageId(db, templatePackageId)
let filePath = await querySession.getSessionKeyValue(
db,
sessionId,
dbEnum.sessionKey.filePath