UNPKG

7.89 kBJavaScriptView Raw
1/*
2Copyright 2019 Adobe. All rights reserved.
3This file is licensed to you under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License. You may obtain a copy
5of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7Unless required by applicable law or agreed to in writing, software distributed under
8the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9OF ANY KIND, either express or implied. See the License for the specific language
10governing permissions and limitations under the License.
11*/
12
13const yaml = require('js-yaml')
14const path = require('path')
15const upath = require('upath')
16const { EOL } = require('os')
17
18const Generator = require('yeoman-generator')
19
20const { manifestPackagePlaceholder, dotenvFilename, actionsDirname } = require('./constants')
21
22const utils = require('./utils')
23
24class ActionGenerator extends Generator {
25 constructor (args, opts) {
26 super(args, opts)
27 this.option('skip-prompt', { default: false })
28 }
29
30 async promptForActionName (actionPurpose, defaultValue) {
31 // todo check args
32
33 let actionName = this.getDefaultActionName(defaultValue)
34 if (!this.options['skip-prompt']) {
35 const promptProps = await this.prompt([
36 {
37 type: 'input',
38 name: 'actionName',
39 message: `We are about to create a new sample action that ${actionPurpose}.\nHow would you like to name this action?`,
40 default: actionName,
41 when: !this.options['skip-prompt'],
42 validate (input) {
43 // must be a valid openwhisk action name, this is a simplified set see:
44 // https://github.com/apache/openwhisk/blob/master/docs/reference.md#entity-names
45 const valid = /^[a-zA-Z0-9][a-zA-Z0-9-]{2,31}$/
46 if (valid.test(input)) {
47 return true
48 }
49 return `'${input}' is not a valid action name, please use a name that matches "^[a-zA-Z0-9][a-zA-Z0-9-]{2,31}$"`
50 }
51 }
52 ])
53 actionName = promptProps.actionName
54 }
55
56 return actionName
57 }
58
59 /**
60 * Adds a new action to the project
61 *
62 * @param {string} actionName
63 * @param {string} tplActionPath
64 * @param {object} [options={}]
65 * @param {object} [options.testFile]
66 * @param {object} [options.e2eTestFile]
67 * @param {object} [options.sharedLibFile]
68 * @param {object} [options.sharedLibTestFile]
69 * @param {object} [options.tplContext]
70 * @param {object} [options.dotenvStub]
71 * @param {string} options.dotenvStub.label
72 * @param {Array<string>} options.dotenvStub.vars
73 * @param {object} [options.dependencies]
74 * @param {object} [options.devDependencies]
75 * @param {object} [options.actionManifestConfig]
76 * @memberof ActionGenerator
77 */
78 addAction (actionName, tplActionPath, options = {}) {
79 options.tplContext = options.tplContext || {}
80
81 const destPath = this.writeTplAction(actionName, tplActionPath, options.tplContext)
82 this.writeActionManifest(actionName, destPath, options.actionManifestConfig)
83 if (options.testFile) {
84 this.writeActionTest(actionName, options.testFile, destPath, options.tplContext)
85 }
86 if (options.sharedLibFile) {
87 this.fs.copyTpl(this.templatePath(options.sharedLibFile), this.destinationPath(path.join(actionsDirname, path.basename(options.sharedLibFile))), options.tplContext)
88 }
89 if (options.sharedLibFile && options.sharedLibTestFile) {
90 this.fs.copyTpl(this.templatePath(options.sharedLibTestFile), this.destinationPath('test', actionsDirname, path.basename(options.sharedLibTestFile)), options.tplContext)
91 }
92 if (options.e2eTestFile) {
93 this.writeActionE2ETest(actionName, options.e2eTestFile, options.tplContext)
94 }
95 if (options.dotenvStub) {
96 this.appendStubVarsToDotenv(options.dotenvStub.label, options.dotenvStub.vars)
97 }
98 if (options.dependencies) {
99 utils.addDependencies(this, options.dependencies)
100 }
101 // make sure wskdebug is there
102 utils.addDependencies(this, { '@openwhisk/wskdebug': '^1.3.0', ...options.devDependencies }, true)
103 // make sure the node engines are added
104 this.addPackageJsonNodeEngines()
105 }
106
107 /** @private */
108 getDefaultActionName (defaultActionName) {
109 const manifestPath = this.destinationPath('manifest.yml')
110 const manifest = this.loadManifest(manifestPath)
111 let actionName = defaultActionName
112 let defaultIndex = 1
113
114 while (actionName in manifest.packages[manifestPackagePlaceholder].actions) {
115 actionName = defaultActionName + '-' + defaultIndex
116 defaultIndex++
117 }
118 return actionName
119 }
120
121 /** @private */
122 loadManifest (manifestPath) {
123 if (!this.fs.exists(manifestPath)) {
124 // stub manifest content
125 return {
126 packages: {
127 [manifestPackagePlaceholder]: {
128 license: 'Apache-2.0',
129 actions: {}
130 }
131 }
132 }
133 } else {
134 return yaml.safeLoad(this.fs.read(manifestPath))
135 }
136 }
137
138 /** @private */
139 writeTplAction (actionName, tplActionPath, tplContext) {
140 // always actions/actionName/index.js
141 // option for custom action destination path
142 const actionDestinationPath = tplContext.actionDestPath ? tplContext.actionDestPath : this.destinationPath(actionsDirname, actionName, 'index.js')
143
144 // should we support other options of copyTpl (templateOptions && copyOptions) ?
145 this.fs.copyTpl(this.templatePath(tplActionPath), actionDestinationPath, tplContext, {}, {})
146
147 return actionDestinationPath
148 }
149
150 /** @private */
151 writeActionManifest (actionName, actionPath, actionManifestConfig = {}) {
152 const manifestPath = this.destinationPath('manifest.yml')
153 const manifest = this.loadManifest(manifestPath)
154
155 manifest.packages[manifestPackagePlaceholder].actions[actionName] = {
156 function: path.relative(path.dirname(manifestPath), actionPath),
157 web: 'yes',
158 runtime: 'nodejs:12',
159 ...actionManifestConfig,
160 annotations: { 'require-adobe-auth': true, ...actionManifestConfig.annotations }
161 }
162
163 this.fs.write(manifestPath, yaml.safeDump(manifest))
164 }
165
166 /** @private */
167 writeActionTest (actionName, tplActionTest, actionDestPath, tplContext) {
168 // test/actions/actionName.test.js
169 const testDestPath = this.destinationPath('test', actionsDirname, actionName + '.test.js')
170 // enriches tplContext with actionRelPath to be required from test file
171 // tplContext = { actionRelPath: path.relative(path.dirname(testDestPath), actionDestPath), ...tplContext }
172 tplContext = { actionRelPath: upath.toUnix(path.relative(path.dirname(testDestPath), actionDestPath)), ...tplContext }
173 // should we support other options of copyTpl (templateOptions && copyOptions) ?
174 this.fs.copyTpl(this.templatePath(tplActionTest), testDestPath, tplContext, {}, {})
175
176 return testDestPath
177 }
178
179 /** @private */
180 writeActionE2ETest (actionName, tplActionTest, tplContext) {
181 // test/actions/actionName.test.js
182 const testDestPath = this.destinationPath('e2e', actionsDirname, actionName + '.e2e.js')
183 // should we support other options of copyTpl (templateOptions && copyOptions) ?
184 this.fs.copyTpl(this.templatePath(tplActionTest), testDestPath, tplContext, {}, {})
185
186 return testDestPath
187 }
188
189 /** @private */
190 appendStubVarsToDotenv (label, vars) {
191 const content = `## ${label}${EOL}${vars.map(v => `#${v}=`).join(EOL)}${EOL}`
192 utils.appendOrWrite(this, this.destinationPath(dotenvFilename), content, label)
193 }
194
195 /** @private */
196 addPackageJsonNodeEngines () {
197 const content = utils.readPackageJson(this)
198 const engines = content.engines || {}
199 if (!engines.node) {
200 // do not overwrite existing node engines
201 engines.node = '^10 || ^12'
202 utils.writePackageJson(this, { ...content, engines })
203 }
204 }
205}
206
207module.exports = ActionGenerator