diff --git a/packages/core/src/babel-plugin-inline-hbs.ts b/packages/core/src/babel-plugin-inline-hbs.ts index 7184b040f..28e4b6bda 100644 --- a/packages/core/src/babel-plugin-inline-hbs.ts +++ b/packages/core/src/babel-plugin-inline-hbs.ts @@ -1,7 +1,6 @@ import { TaggedTemplateExpression, CallExpression, - isStringLiteral, templateLiteral, templateElement, ExpressionStatement, @@ -10,6 +9,8 @@ import { Program, functionExpression, blockStatement, + throwStatement, + newExpression, } from '@babel/types'; import { NodePath } from '@babel/traverse'; import { join } from 'path'; @@ -119,19 +120,44 @@ function handleTagged(path: NodePath, state: State) { } function handleCalled(path: NodePath, state: State) { - if (path.node.arguments.length !== 1) { - throw path.buildCodeFrameError('hbs accepts exactly one argument'); - } - let arg = path.node.arguments[0]; - if (!isStringLiteral(arg)) { - throw path.buildCodeFrameError('hbs accepts only a string literal argument'); - } - let template = arg.value; + let { template, insertRuntimeErrors } = getCallArguments(path); + let compilerInstance = compiler(state); + if (state.opts.stage === 1) { - let compiled = compiler(state).applyTransforms(state.file.opts.filename, template); + let compiled: string; + try { + compiled = compilerInstance.applyTransforms(state.file.opts.filename, template); + } catch (err) { + if (insertRuntimeErrors) { + // in stage 1 we just leave the bad template in place (we were only + // trying to run transforms and re-emit hbs), so that it will be handled + // at stage3 instead. + return; + } + throw err; + } (path.get('arguments')[0] as NodePath).replaceWith(stringLiteral(compiled)); } else { - let { compiled, dependencies } = compiler(state).precompile(state.file.opts.filename, template); + let result: ReturnType; + try { + result = compilerInstance.precompile(state.file.opts.filename, template); + } catch (err) { + if (insertRuntimeErrors) { + path.replaceWith( + callExpression( + functionExpression( + null, + [], + blockStatement([throwStatement(newExpression(identifier('Error'), [stringLiteral(err.message)]))]) + ), + [] + ) + ); + return; + } + throw err; + } + let { compiled, dependencies } = result; for (let dep of dependencies) { state.dependencies.set(dep.runtimeName, dep); } @@ -179,3 +205,28 @@ function amdDefine(runtimeName: string, importCounter: number) { ]) ); } + +function getCallArguments(path: NodePath): { template: string; insertRuntimeErrors: boolean } { + let [template, options] = path.node.arguments; + + if (template?.type !== 'StringLiteral') { + throw path.buildCodeFrameError('hbs accepts only a string literal argument'); + } + + let insertRuntimeErrors = + options?.type === 'ObjectExpression' && + options.properties.some( + prop => + prop.type === 'ObjectProperty' && + prop.computed === false && + prop.key.type === 'Identifier' && + prop.key.name === 'insertRuntimeErrors' && + prop.value.type === 'BooleanLiteral' && + prop.value.value + ); + + return { + template: template.value, + insertRuntimeErrors, + }; +} diff --git a/packages/core/tests/inline-hbs.test.ts b/packages/core/tests/inline-hbs.test.ts index 803806ec2..8361f213a 100644 --- a/packages/core/tests/inline-hbs.test.ts +++ b/packages/core/tests/inline-hbs.test.ts @@ -27,6 +27,17 @@ function stage1Tests(transform: (code: string) => string) { expect(code).toMatch(/return hbs\("
{ + let code = transform(` + import hbs from 'htmlbars-inline-precompile'; + export default function() { + return hbs("
", { insertRuntimeErrors: true }); + } + `); + expect(code).toMatch(/import hbs from 'htmlbars-inline-precompile'/); + expect(code).toMatch(/return hbs\("
",\s*\{\s*insertRuntimeErrors: true\s*\}\)/); + }); } function stage3Tests(transform: (code: string) => string) { @@ -50,6 +61,16 @@ function stage3Tests(transform: (code: string) => string) { expect(code).not.toMatch(/import hbs from 'htmlbars-inline-precompile'/); expect(code).toMatch(/return Ember.HTMLBars.template\({/); }); + test('runtime errors become exceptions in stage 3', () => { + let code = transform(` + import hbs from 'htmlbars-inline-precompile'; + export default function() { + return hbs("
", { insertRuntimeErrors: true }); + } + `); + expect(code).not.toMatch(/import hbs from 'htmlbars-inline-precompile'/); + expect(code).toMatch(/throw new Error\("Unclosed element `div`/); + }); } describe('inline-hbs', () => {