Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit 6b70fec

Browse files
committed
simplify compilation of compositions, improving error reporting
Fixes #776
1 parent 648074a commit 6b70fec

File tree

5 files changed

+69
-162
lines changed

5 files changed

+69
-162
lines changed

app/plugins/modules/composer/lib/create-from-source.js

Lines changed: 64 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717
const vm = require('vm'),
1818
fs = require('fs'),
1919
path = require('path'),
20-
mod = require('module'),
2120
expandHomeDir = require('expand-home-dir'),
2221
openwhiskComposer = require('@ibm-functions/composer'),
23-
{ isValidFSM, handleError } = require('./composer')
22+
{ isValidFSM } = require('./composer')
2423

2524
//
2625
// just in case, block any use of wsk from within sandboxed compilations
@@ -87,184 +86,95 @@ exports.compileToFSM = (src, opts={}) => new Promise((resolve, reject) => {
8786
return reject({ message: 'No code to compile', type: 'EMPTY_FILE'})
8887
}
8988

90-
/**
91-
* The Composer constructor tries to initialize
92-
* wsk. But we may not have a wskprops, e.g. if the
93-
* user is previewing apps without any AUTH
94-
* configuration. See
95-
* tests/passes/07/composer-viz-no-auth.js
96-
*
97-
*/
98-
99-
// check to see if the source already requires the openwhisk-composer library
100-
function bootstrapWithRequire() {
101-
lineOffset = 1
102-
return `const composer = require('@ibm-functions/composer');` + originalCode
103-
}
104-
function bootstrapWithRequireForModule() {
105-
lineOffset = 1
106-
return `const composer = require('@ibm-functions/composer')const process = module.process; const console = module.console;\n` + originalCode
107-
}
108-
function bootstrapWithModuleExports() {
109-
lineOffset = 0
110-
return "const process = module.process; const console = module.console; module.exports=" + originalCode
111-
}
112-
function bootstrapWithModuleExportsAndRequire() {
113-
lineOffset = 0
114-
return `const composer = require('@ibm-functions/composer'); const process = module.process; const console = module.console; module.exports=` + originalCode
115-
}
116-
function bootstrapWithModuleExportsAndRequireAndTrim() {
117-
lineOffset = 0
118-
const code = originalCode.trim().replace(/^([;\s]+)/, '') // trim leading semicolons
119-
return `const composer = require('@ibm-functions/composer'); const process = module.process; const console = module.console; module.exports=` + code
120-
}
121-
function bootstrapWithConstMain() {
122-
lineOffset = 1
123-
return "const process = module.process; const console = module.console;" + originalCode + "\n;module.exports=main"
124-
}
125-
function bootstrapWithConstMainAndRequire() {
126-
lineOffset = 1
127-
return `const composer = require('@ibm-functions/composer'); const process = module.process; const console = module.console;\n` + originalCode + "\n;module.exports=main"
128-
}
129-
130-
const retryA = [bootstrapWithRequireForModule,
131-
bootstrapWithModuleExports,
132-
bootstrapWithModuleExportsAndRequire,
133-
bootstrapWithModuleExportsAndRequireAndTrim,
134-
bootstrapWithConstMain,
135-
bootstrapWithConstMainAndRequire]
136-
13789
let errorMessage = '',
13890
logMessage = '' // TODO this isn't flowing through, yet
139-
const doLog = msg => logMessage += msg + '\n',
140-
doExit = () => reject({
141-
fsm: errorMessage,
142-
code: originalCode
143-
})
14491
const errors = []
145-
const compile = (code, retries=retryA) => {
92+
const compile = code => {
14693
errorMessage = ''
14794
logMessage = ''
14895
try {
149-
const module = { exports: {},
150-
process: { env: opts.env || process.env, exit: doExit },
151-
console: { error: msg => errorMessage += msg + '\n',
152-
log: doLog }
153-
},
154-
my_require = m => {
155-
if (m === '@ibm-functions/composer') {
156-
return openwhiskComposer
157-
} else {
158-
return require(path.resolve(dir, m))
96+
const doExit = () => reject({
97+
fsm: errorMessage,
98+
code: originalCode
99+
})
100+
101+
const my = {
102+
process: Object.assign(process, {
103+
env: Object.assign({}, process.env, opts.env), // merge -e from the command line
104+
exit: doExit // override process.exit()
105+
}),
106+
console: {
107+
error: msg => errorMessage += msg + '\n',
108+
log: msg => logMessage += msg + '\n'
109+
},
110+
require: m => {
111+
if (m === '@ibm-functions/composer') {
112+
return openwhiskComposer
113+
} else {
114+
return require(path.resolve(dir, m))
115+
116+
}
117+
}
118+
}
119+
120+
const module = {
121+
exports: {}
122+
}
123+
const sandbox = {
124+
module,
125+
exports: module.exports,
126+
filename,
127+
lineOffset,
128+
console: my.console,
129+
process: my.process,
130+
require: my.require
131+
}
132+
const sandboxWithComposer = Object.assign(sandbox, { composer: openwhiskComposer })
159133

160-
}
161-
}
134+
let res = vm.runInNewContext(code, sandboxWithComposer)
135+
debug('res', typeof res, res)
162136

163-
const sandbox = {}
164-
module.exports = {}
165-
let res = vm.runInNewContext(mod.wrap(code), { filename, lineOffset, console: module.console, process: module.process })(module.exports, my_require, module, filename, dir) || module.exports.main || module.exports || res.main
166-
//console.error(code)
167137
if (typeof res === 'function') {
168138
res = res()
169139
}
170140

171141
if (isValidFSM(res)) {
172142
return res
143+
173144
} else {
145+
let err = ''
174146
try {
175147
// maybe the code did a console.log?
176148
const maybe = openwhiskComposer.deserialize(JSON.parse(logMessage))
177149
if (isValidFSM(maybe)) {
178150
return maybe
179151
}
180-
} catch (e) { }
181-
182-
throw new Error('Unable to compile your composition')
183-
}
184-
} catch (e) {
185-
console.error(e)
186-
errors.push(e)
187-
if (retries.length > 0) {
188-
return compile(retries.pop()(), retries)
189-
}
190-
191-
const log = console.log, exit = process.exit
192-
console.log = doLog
193-
process.exit = doExit
194-
try {
195-
errorMessage = ''
196-
const tmp = save(process.env, opts.env)
197-
try {
198-
const json = eval(originalCode)
199-
if (isValidFSM(json)) {
200-
return json
201-
} else {
202-
const maybe = json
203-
console.log = log
204-
process.exit = exit
205-
return maybe
206-
}
207-
} finally {
208-
restore(process.env, tmp)
152+
} catch (e) {
153+
err = e
209154
}
210-
} catch (e2) {
211-
console.log = log
212-
process.exit = exit
213-
try {
214-
// maybe the user logged a compiled fsm?
215-
const maybe = JSON.parse(logMessage)
216-
if (isValidFSM(maybe)) {
217-
return maybe
218-
}
219-
} catch (e3) {
220-
try {
221-
console.log = doLog
222-
process.exit = doExit
223-
errorMessage = ''
224-
const tmp = save(process.env, opts.env)
225-
try {
226-
const composition = eval(bootstrapWithRequire(originalCode))
227-
console.log = log
228-
process.exit = exit
229-
return composition
230-
} finally {
231-
restore(process.env, tmp)
232-
}
233-
234-
} catch (e4) {
235-
console.log = log
236-
process.exit = exit
237-
// some sort of parse or runtime error with the composer source file
238-
// note that we take care to elide our junk on any error stacks (junkMatch)
239-
//console.error(mod.wrap(code))
240-
//console.error(errorMessage)
241155

242-
const goodMsg = e => e.message.indexOf('has already been declared') < 0
243-
&& e.message.indexOf('composer is not defined') < 0
244-
&& e
245-
const err = errors.find(goodMsg) || goodMsg(e2) || goodMsg(e3) || e4
246-
247-
const junkMatch = err.stack.match(/\s+at Object\.exports\.runInNewContext/)
248-
|| err.stack.match(/\s+at Object\.runInNewContext/)
249-
|| err.stack.match(/\s+at fs\.readFile/),
250-
_message = err.message.indexOf('Invalid argument to compile') >= 0? 'Your source code did not produce a valid app.' : (!junkMatch ? e.stack : err.stack.substring(0, junkMatch.index).replace(/\s+.*create-from-source([^\n])*/g, '\n').replace(/(evalmachine.<anonymous>)/g, filename).replace(/\s+at createScript([^\n])*/g, '\n').trim()),
251-
message = _message.replace(/\s+\(.*plugins\/modules\/composer\/node_modules\/@ibm-functions\/composer\/composer\.js:[^\s]*/, '')
156+
throw new Error(`Unable to compile your composition
157+
${err}
158+
${errorMessage}`)
159+
}
160+
} catch (err) {
161+
const junkMatch = err.stack.match(/\s+at Object\.exports\.runInNewContext/)
162+
|| err.stack.match(/\s+at Object\.runInNewContext/)
163+
|| err.stack.match(/\s+at fs\.readFile/),
164+
_message = err.message.indexOf('Invalid argument to compile') >= 0? 'Your source code did not produce a valid app.' : (!junkMatch ? e.stack : err.stack.substring(0, junkMatch.index).replace(/\s+.*create-from-source([^\n])*/g, '\n').replace(/(evalmachine.<anonymous>)/g, filename).replace(/\s+at createScript([^\n])*/g, '\n').trim()),
165+
message = _message
166+
.replace(/\s+\(.*plugins\/modules\/composer\/node_modules\/@ibm-functions\/composer\/composer\.js:[^\s]*/, '')
167+
.replace(/\s+at ContextifyScript[^\n]*/g, '')
252168

253-
console.error('All composer create/preview errors are here', errors)
254-
console.error('Selected error', err)
255-
console.error('Selected message', message, junkMatch)
256169

257-
// for parse error, error message is shown in the fsm (JSON) tab, and user code in the source (code) tab
258-
// reject now returns {fsm:errMsg, code:originalCode}
259-
reject(
260-
{
261-
fsm: message,
262-
code: originalCode
263-
}
264-
)
265-
}
170+
// for parse error, error message is shown in the fsm (JSON) tab, and user code in the source (code) tab
171+
// reject now returns {fsm:errMsg, code:originalCode}
172+
reject(
173+
{
174+
fsm: message,
175+
code: originalCode
266176
}
267-
}
177+
)
268178
}
269179
}
270180
let fsm
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
function main() {
2-
return composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))
3-
}
1+
module.exports = composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
const main = () => composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))
1+
exports.main = composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
return composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))
1+
composer.try('RandomError', /* catch */ args => ({ message: args.error + ' is caught' }))

tests/tests/passes/07/composer-create-error-handling.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,9 @@ describe('app create error handling and app create --dry-run', function() {
7878
const dryRunOk = 'data/composer-source/if.js',
7979
badDir = 'data/composer-source-expect-errors',
8080
dryRunBad = [ { input: `${badDir}/error1.js`, err: `SLACK_TOKEN required in environment.` },
81-
{ input: `${badDir}/nofsm.js`, err: `Your code could not be composed` },
81+
{ input: `${badDir}/nofsm.js`, err: `Error: Unable to compile your composition` },
8282
{ input: `${badDir}/t2s.js`, err: `ReferenceError: slackConfig is not defined
83-
at t2s.js:17:19
84-
at t2s.js:22:3` },
83+
at t2s.js:16:19` },
8584
{ input: `${badDir}/if-bad.js`, err: `if-bad.js:2
8685
/* cond */ 'authenticate',, /* double comma, expect parse error */
8786
^

0 commit comments

Comments
 (0)