diff --git a/jest.config.js b/jest.config.js index 04195b09..94718fa0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,8 @@ module.exports = { ], testMatch: [ '/test/unit/**/*.ts', - '/test/e2e.spec.ts' + '/test/e2e.spec.ts', + '/test/error.spec.ts' ], transform: { '^.+\\.ts$': 'babel-jest' diff --git a/package.json b/package.json index 3e9df927..18652cca 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "e2e": "jest test/e2e.spec.ts", "perf": "node ./test/perf/index.js", "check": "npm test && npm run lint", - "test": "jest test/unit test/e2e.spec.ts", - "coverage": "jest test/unit test/e2e.spec.ts --coverage", + "test": "jest", + "coverage": "jest --coverage", "version": "npm run build && npm run docs", "semantic-release": "semantic-release" }, diff --git a/src/ast/renderer-ast-dfn.ts b/src/ast/renderer-ast-dfn.ts index d0dc0ffa..741a9c57 100644 --- a/src/ast/renderer-ast-dfn.ts +++ b/src/ast/renderer-ast-dfn.ts @@ -63,11 +63,13 @@ export enum SyntaxKind { SlotRendererDefinition = 33, SlotRenderCall = 34, Undefined = 35, + TryStatement = 36, + CatchClause = 37 } export type Expression = Identifier | FunctionDefinition | Literal | BinaryExpression | UnaryExpression | CreateComponentInstance | NewExpression | MapLiteral | ComponentRendererReference | FunctionCall | Null | Undefined | MapAssign | ArrayIncludes | ConditionalExpression | FilterCall | HelperCall | EncodeURIComponent | ArrayLiteral | RegexpReplace | JSONStringify | ComputedCall | GetRootCtxCall | ComponentReferenceLiteral | SlotRendererDefinition | SlotRenderCall -export type Statement = ReturnStatement | ImportHelper | VariableDefinition | AssignmentStatement | If | ElseIf | Else | Foreach | ExpressionStatement +export type Statement = ReturnStatement | ImportHelper | VariableDefinition | AssignmentStatement | If | ElseIf | Else | Foreach | ExpressionStatement | TryStatement export type BinaryOperator = '+' | '-' | '*' | '/' | '.' | '===' | '!==' | '||' | '&&' | '[]' | '+=' | '!=' | '==' @@ -139,6 +141,21 @@ export class Undefined implements SyntaxNode { } } +export class TryStatement implements SyntaxNode { + public readonly kind = SyntaxKind.TryStatement + constructor ( + public block: Statement[], + public handler: CatchClause + ) {} +} +export class CatchClause implements SyntaxNode { + public readonly kind = SyntaxKind.CatchClause + constructor ( + public param: Identifier, + public body: Statement[] + ) {} +} + export class CreateComponentInstance implements SyntaxNode { public readonly kind = SyntaxKind.CreateComponentInstance constructor ( diff --git a/src/ast/renderer-ast-util.ts b/src/ast/renderer-ast-util.ts index e1fe0fc6..5ab464fb 100644 --- a/src/ast/renderer-ast-util.ts +++ b/src/ast/renderer-ast-util.ts @@ -5,7 +5,7 @@ * 例如:new AssignmentStatement(new Identifier('html'), new Literal('foo')) 可以简写为 ASSIGN(I('html), L('foo)) */ -import { SyntaxKind, SyntaxNode, Block, MapLiteral, UnaryOperator, UnaryExpression, NewExpression, VariableDefinition, ReturnStatement, BinaryOperator, If, Null, Undefined, AssignmentStatement, Statement, Expression, Identifier, ExpressionStatement, BinaryExpression, Literal } from './renderer-ast-dfn' +import { SyntaxKind, SyntaxNode, Block, MapLiteral, UnaryOperator, UnaryExpression, NewExpression, VariableDefinition, ReturnStatement, BinaryOperator, If, Null, Undefined, AssignmentStatement, Statement, Expression, Identifier, ExpressionStatement, BinaryExpression, Literal, TryStatement, CatchClause } from './renderer-ast-dfn' export function createHTMLLiteralAppend (html: string) { return STATEMENT(BINARY(I('html'), '+=', L(html))) @@ -31,6 +31,10 @@ export function createIfStrictEqual (lhs: Expression, rhs: Expression, statement return new If(BINARY(lhs, '===', rhs), statements) } +export function createTryStatement (block: Statement[], param: Identifier, body: Statement[]) { + return new TryStatement(block, new CatchClause(param, body)) +} + export function L (val: any) { return Literal.create(val) } diff --git a/src/ast/renderer-ast-walker.ts b/src/ast/renderer-ast-walker.ts index 2e6c48ec..c596f327 100644 --- a/src/ast/renderer-ast-walker.ts +++ b/src/ast/renderer-ast-walker.ts @@ -94,6 +94,11 @@ export function * walk (node: Expression | Statement): Iterable, info: string) { + let current: SanComponent<{}> | undefined = instance + while (current) { + if (typeof current.error === 'function') { + current.error(e, instance, info) + return + } + current = current.parentComponent + } + + throw e +} diff --git a/src/runtime/helpers.ts b/src/runtime/helpers.ts index dd33e8e0..1059503e 100644 --- a/src/runtime/helpers.ts +++ b/src/runtime/helpers.ts @@ -1,3 +1,4 @@ export { _ } from './underscore' export { createResolver } from './resolver' export { SanSSRData } from './san-ssr-data' +export { handleError } from './handle-error' diff --git a/src/runtime/underscore.ts b/src/runtime/underscore.ts index 4da2cb6b..c509e5f5 100644 --- a/src/runtime/underscore.ts +++ b/src/runtime/underscore.ts @@ -1,4 +1,5 @@ import { ComponentClass } from '../models/component' +import { handleError } from './handle-error' const BASE_PROPS = { class: 1, @@ -96,11 +97,23 @@ function boolAttrFilter (name: string, value: string) { } function callFilter (ctx: Context, name: string, ...args: any[]) { - return ctx.instance.filters[name].call(ctx.instance, ...args) + let value + try { + value = ctx.instance.filters[name].call(ctx.instance, ...args) + } catch (e) { + handleError(e, ctx.instance, 'filter:' + name) + } + return value } function callComputed (ctx: Context, name: string) { - return ctx.instance.computed[name].apply(ctx.instance) + let value + try { + value = ctx.instance.computed[name].apply(ctx.instance) + } catch (e) { + handleError(e, ctx.instance, 'computed:' + name) + } + return value } function iterate (val: any[] | object) { diff --git a/src/target-js/js-emitter.ts b/src/target-js/js-emitter.ts index 5e21e392..dd0b0dcf 100644 --- a/src/target-js/js-emitter.ts +++ b/src/target-js/js-emitter.ts @@ -169,6 +169,14 @@ export class JSEmitter extends Emitter { break case SyntaxKind.Foreach: return this.writeForeachStatement(node) + case SyntaxKind.TryStatement: + this.nextLine('try ') + this.writeBlockStatements(node.block) + this.nextLine('catch (') + this.writeSyntaxNode(node.handler.param) + this.write(') ') + this.writeBlockStatements(node.handler.body) + break default: assertNever(node) } } diff --git a/test/error.spec.ts b/test/error.spec.ts new file mode 100644 index 00000000..102af145 --- /dev/null +++ b/test/error.spec.ts @@ -0,0 +1,165 @@ +import { SanProject } from '../dist/index' +import san from 'san' + +it('lifecycle hook: inited', function () { + const spy = jest.fn() + const Child = san.defineComponent({ + template: '

test

', + inited: function () { + throw new Error('error') + } + }) + const MyComponent = san.defineComponent({ + template: '
', + components: { + 'x-child': Child + }, + error: spy + }) + + const project = new SanProject() + const renderer = project.compileToRenderer(MyComponent) + + renderer({}) + + expect(spy).toHaveBeenCalled() + + const args = spy.mock.calls[0] + expect(args[2]).toBe('hook:inited') + expect(args[1] instanceof Child).toBe(true) + expect(args[0] instanceof Error).toBe(true) + expect(args[0].message).toBe('error') +}) + +it('initData', function () { + const spy = jest.fn() + const Child = san.defineComponent({ + template: '

test

', + initData: function () { + throw new Error('error') + } + }) + const MyComponent = san.defineComponent({ + template: '
', + components: { + 'x-child': Child + }, + error: spy + }) + + const project = new SanProject() + const renderer = project.compileToRenderer(MyComponent) + + renderer({}) + + expect(spy).toHaveBeenCalled() + + const args = spy.mock.calls[0] + expect(args[2]).toBe('initData') + expect(args[1] instanceof Child).toBe(true) + expect(args[0] instanceof Error).toBe(true) + expect(args[0].message).toBe('error') +}) + +it('computed', function () { + const spy = jest.fn() + const Child = san.defineComponent({ + template: '

{{ message }}

', + computed: { + message: function () { + throw new Error('error') + } + } + }) + const MyComponent = san.defineComponent({ + template: '
', + components: { + 'x-child': Child + }, + error: spy + }) + + const project = new SanProject() + const renderer = project.compileToRenderer(MyComponent) + + renderer({}) + + expect(spy).toHaveBeenCalled() + + const args = spy.mock.calls[0] + expect(args[2]).toBe('computed:message') + expect(args[1] instanceof Child).toBe(true) + expect(args[0] instanceof Error).toBe(true) + expect(args[0].message).toBe('error') +}) + +it('filter', function () { + const spy = jest.fn() + const Child = san.defineComponent({ + template: '

{{ msg | add }}

', + filters: { + add: function () { + throw new Error('error') + } + }, + initData: function () { + return { + msg: 'test' + } + } + }) + const MyComponent = san.defineComponent({ + template: '
', + components: { + 'x-child': Child + }, + error: spy + }) + + const project = new SanProject() + const renderer = project.compileToRenderer(MyComponent) + + renderer({}) + + expect(spy).toHaveBeenCalled() + + const args = spy.mock.calls[0] + expect(args[2]).toBe('filter:add') + expect(args[1] instanceof Child).toBe(true) + expect(args[0] instanceof Error).toBe(true) + expect(args[0].message).toBe('error') +}) + +it('slot children', function () { + const spy = jest.fn() + const slotChild = san.defineComponent({ + template: 'test', + inited: function () { + throw new Error('error') + } + }) + const Child = san.defineComponent({ + template: '

' + }) + const MyComponent = san.defineComponent({ + template: '
', + components: { + 'x-child': Child, + 'x-slot-child': slotChild + }, + error: spy + }) + + const project = new SanProject() + const renderer = project.compileToRenderer(MyComponent) + + renderer({}) + + expect(spy).toHaveBeenCalled() + + const args = spy.mock.calls[0] + expect(args[2]).toBe('hook:inited') + expect(args[1] instanceof slotChild).toBe(true) + expect(args[0] instanceof Error).toBe(true) + expect(args[0].message).toBe('error') +}) diff --git a/types/san.d.ts b/types/san.d.ts index eb59a57c..84ba45c6 100644 --- a/types/san.d.ts +++ b/types/san.d.ts @@ -110,6 +110,7 @@ declare namespace San { attach(container: Element): void; detach(): void; dispose(): void; + error(e: Error, instance: SanComponent<{}>, info: string): void; } interface ComponentConstructor {