Skip to content

Commit

Permalink
feat: ACNA-1650 - Template Registry Api integration (#569)
Browse files Browse the repository at this point in the history
This connects with the Template Registry API so we can bootstrap App Builder apps with pre-defined templates available in the registry, and not with pre-packaged templates.

## Features changed:
- aio app init
  - be aware of this multiple flag oclif bug if you use --template or --extension. The appname argument must be before those flags: argument not read, if after a flag with multiple: true oclif/core#496
- aio app add extension
- aio app add action
- aio app add web-assets
- aio app delete extension
  • Loading branch information
shazron authored Sep 27, 2022
1 parent 35dd448 commit 805ee90
Show file tree
Hide file tree
Showing 20 changed files with 1,800 additions and 1,351 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"jsdoc": {
"ignorePrivate": true
}
},
"parserOptions": {
"ecmaVersion": "latest"
}
}
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module.exports = {
],
collectCoverageFrom: [
'src/commands/**/*.js',
'src/lib/*.js'
'src/lib/*.js',
'src/*.js'
],
coverageThreshold: {
global: {
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
"@adobe/aio-lib-env": "^2.0.0",
"@adobe/aio-lib-ims": "^6.0.0",
"@adobe/aio-lib-runtime": "^5.0.0",
"@adobe/aio-lib-templates": "^2.1.0",
"@adobe/aio-lib-web": "^6.0.1",
"@adobe/generator-aio-app": "^4.0.0",
"@adobe/generator-app-common-lib": "^0.3.3",
"@adobe/inquirer-table-checkbox": "^1.0.1",
"@oclif/core": "^1.15.0",
"@parcel/core": "^2.7.0",
"@parcel/reporter-cli": "^2.7.0",
Expand All @@ -38,6 +40,7 @@
"ora": "^5",
"pure-http": "^3",
"serve-static": "^1.14.1",
"term-size": "^2.2.1",
"upath": "^2",
"which": "^2.0.1",
"yeoman-environment": "^3.2.0"
Expand All @@ -60,6 +63,7 @@
"eslint-plugin-promise": "^6.0.0",
"jest": "^28",
"jest-plugin-fs": "^2.9.0",
"nock": "^13.2.9",
"oclif": "^3.2.0",
"stdout-stderr": "^0.1.9"
},
Expand Down
8 changes: 1 addition & 7 deletions src/AddCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ const { installPackages } = require('./lib/app-helper')

class AddCommand extends BaseCommand {
async runInstallPackages (flags, spinner) {
const doInstall = flags.install && !flags['skip-install']
if (doInstall) {
if (flags.install) {
await installPackages('.', { spinner, verbose: flags.verbose })
} else {
this.log('skipped installation, make sure to run \'npm install\' later on')
Expand All @@ -25,11 +24,6 @@ class AddCommand extends BaseCommand {
}

AddCommand.flags = {
'skip-install': Flags.boolean({
description: '[deprecated] Please use --no-install',
char: 's',
default: false
}),
install: Flags.boolean({
description: '[default: true] Run npm installation after files are created',
default: true,
Expand Down
246 changes: 246 additions & 0 deletions src/TemplatesCommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
/*
Copyright 2021 Adobe. All rights reserved.
This file is licensed to you 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 REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const AddCommand = require('./AddCommand')
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:TemplatesCommand', { provider: 'debug' })
const inquirerTableCheckbox = require('@adobe/inquirer-table-checkbox')
const inquirer = require('inquirer')
const TemplateRegistryAPI = require('@adobe/aio-lib-templates')
const hyperlinker = require('hyperlinker')
const ora = require('ora')
const terminalSize = require('term-size')

class TemplatesCommand extends AddCommand {
/**
* Gets a list of templates from the Template Registry API using the criteria provided.
*
* @param {object} searchCriteria the Template Registry API search criteria
* @param {object} orderByCriteria the Template Registry API orderBy criteria
* @param {object} [templateRegistryConfig={}] the optional Template Registry API config
* @returns {Array<object>} list of templates
*/
async getTemplates (searchCriteria, orderByCriteria, templateRegistryConfig = {}) {
const templateRegistryClient = TemplateRegistryAPI.init(templateRegistryConfig)
const templateList = []

const templatesIterator = templateRegistryClient.getTemplates(searchCriteria, orderByCriteria)

for await (const templates of templatesIterator) {
for (const template of templates) {
templateList.push(template)
}
}
aioLogger.debug('template list', JSON.stringify(templateList, null, 2))

return templateList
}

/**
* Select templates from the Template Registry API, via a cli table.
*
* @param {object} searchCriteria the Template Registry API search criteria
* @param {object} orderByCriteria the Template Registry API orderBy criteria
* @param {object} [templateRegistryConfig={}] the optional Template Registry API config
* @returns {Array<string>} an array of selected template module name(s)
*/
async selectTemplates (searchCriteria, orderByCriteria, templateRegistryConfig = {}) {
aioLogger.debug('searchCriteria', JSON.stringify(searchCriteria, null, 2))
aioLogger.debug('orderByCriteria', JSON.stringify(orderByCriteria, null, 2))

const spinner = ora()
spinner.start('Getting available templates')

const templateList = await this.getTemplates(searchCriteria, orderByCriteria, templateRegistryConfig)
aioLogger.debug('templateList', JSON.stringify(templateList, null, 2))
spinner.succeed('Downloaded the list of templates')

if (templateList.length === 0) {
throw new Error('There are no templates that match the query for selection')
}

const { columns: terminalColumns } = terminalSize()

const colPadding = 3
const colWidths = [
Math.round(0.3 * terminalColumns) - colPadding,
Math.round(0.3 * terminalColumns) - colPadding,
Math.round(0.2 * terminalColumns) - colPadding,
Math.round(0.2 * terminalColumns) - colPadding]

const COLUMNS = {
COL_TEMPLATE: 'Template',
COL_DESCRIPTION: 'Description',
COL_EXTENSION_POINT: 'Extension Point',
COL_CATEGORIES: 'Categories'
}

const rows = templateList.map(template => {
const extensionPoint = template.extensions ? template.extensions.map(ext => ext.extensionPointId).join(',') : 'N/A'
const name = template.adobeRecommended ? `${template.name} *` : template.name
return {
value: template.name,
[COLUMNS.COL_TEMPLATE]: name,
[COLUMNS.COL_DESCRIPTION]: template.description,
[COLUMNS.COL_EXTENSION_POINT]: extensionPoint,
[COLUMNS.COL_CATEGORIES]: template?.categories?.join(', ')
}
})
const promptName = 'select template'

inquirer.registerPrompt('table', inquirerTableCheckbox)
const answers = await inquirer
.prompt([
{
type: 'table',
name: promptName,
bottomContent: `* = recommended by Adobe; to learn more about the templates, go to ${hyperlinker('https://adobe.ly/templates', 'https://adobe.ly/templates')}`,
message: 'Choose the template(s) to install:',
style: { head: [], border: [] },
wordWrap: true,
wrapOnWordBoundary: false,
colWidths,
columns: [
{ name: COLUMNS.COL_TEMPLATE },
{ name: COLUMNS.COL_DESCRIPTION, wrapOnWordBoundary: true },
{ name: COLUMNS.COL_EXTENSION_POINT },
{ name: COLUMNS.COL_CATEGORIES, wrapOnWordBoundary: false }
],
rows
}
])

return answers[promptName]
}

/**
* Install the templates.
*
* @param {object} templateData the template data
* @param {boolean} [templateData.useDefaultValues=false] use default values when installing the template
* @param {boolean} [templateData.installConfig=true] process the install.yml of the template
* @param {boolean} [templateData.installNpm=true] run npm install after installing the template
* @param {object} [templateData.templateOptions=null] set the template options for installation
* @param {Array} templateData.templates the list of templates to install
*/
async installTemplates ({
useDefaultValues = false,
installConfig = true,
installNpm = true,
templateOptions = null,
templates = []
} = {}) {
const spinner = ora()

// install the templates in sequence
for (const template of templates) {
spinner.info(`Installing template ${template}`)
const installArgs = [template]
if (useDefaultValues) {
installArgs.push('--yes')
}
if (!installConfig) {
installArgs.push('--no-process-install-config')
}
if (!installNpm) {
installArgs.push('--no-install')
}

if (templateOptions) {
if (typeof templateOptions !== 'object' || Array.isArray(templateOptions)) { // must be a non-array object
aioLogger.debug('malformed templateOptions', templateOptions)
throw new Error('The templateOptions is not a JavaScript object.')
}
const jsonString = JSON.stringify(templateOptions)
installArgs.push(`--template-options=${Buffer.from(jsonString).toString('base64')}`)
}

await this.config.runCommand('templates:install', installArgs)
spinner.succeed(`Installed template ${template}`)
}
}

/** @private */
_uniqueArray (array) {
return Array.from(new Set(array))
}

/**
* Get templates by extension point ids.
*
* @param {Array<string>} extensionsToInstall an array of extension point ids to install.
* @param {object} [templateRegistryConfig={}] the optional Template Registry API config
* @returns {object} returns the result
*/
async getTemplatesByExtensionPointIds (extensionsToInstall, templateRegistryConfig = {}) {
const orderByCriteria = {
[TemplateRegistryAPI.ORDER_BY_CRITERIA_PUBLISH_DATE]: TemplateRegistryAPI.ORDER_BY_CRITERIA_SORT_DESC
}

const searchCriteria = {
[TemplateRegistryAPI.SEARCH_CRITERIA_STATUSES]: TemplateRegistryAPI.TEMPLATE_STATUS_APPROVED,
[TemplateRegistryAPI.SEARCH_CRITERIA_EXTENSIONS]: extensionsToInstall
}

const templates = await this.getTemplates(searchCriteria, orderByCriteria, templateRegistryConfig)
aioLogger.debug('templateList', JSON.stringify(templates, null, 2))

// check whether we got all extensions
const found = this._uniqueArray(templates
.map(t => t.extensions.map(e => e.extensionPointId)) // array of array of extensionPointIds
.filter(ids => extensionsToInstall.some(item => ids.includes(item)))
.flat()
)

const notFound = this._uniqueArray(extensionsToInstall).filter(ext => !found.includes(ext))

return {
found,
notFound,
templates
}
}

/**
* Install templates by extension point ids.
*
* @param {Array<string>} extensionsToInstall an array of extension point ids to install.
* @param {Array<string>} extensionsAlreadyImplemented an array of extension point ids that have already been implemented (to filter)
* @param {boolean} [useDefaultValues=false] use default values when installing the template
* @param {boolean} [installNpm=true] run npm install after installing the template
* @param {object} [templateRegistryConfig={}] the optional Template Registry API config
*/
async installTemplatesByExtensionPointIds (extensionsToInstall, extensionsAlreadyImplemented, useDefaultValues = false, installNpm = true, templateRegistryConfig = {}) {
// no prompt
const alreadyThere = extensionsToInstall.filter(i => extensionsAlreadyImplemented.includes(i))
if (alreadyThere.length > 0) {
throw new Error(`'${alreadyThere.join(', ')}' extension(s) are already implemented in this project.`)
}

const { found, notFound, templates } = await this.getTemplatesByExtensionPointIds(extensionsToInstall, templateRegistryConfig)

if (notFound.length > 0) {
this.error(`Extension(s) '${notFound.join(', ')}' not found in the Template Registry.`)
}

this.log(`Extension(s) '${found.join(', ')}' found in the Template Registry. Installing...`)
await this.installTemplates({
useDefaultValues,
installNpm,
templates: templates.map(t => t.name)
})
}
}

TemplatesCommand.flags = {
...AddCommand.flags
}

module.exports = TemplatesCommand
Loading

0 comments on commit 805ee90

Please sign in to comment.