Skip to content

Commit

Permalink
Merge a37e9e4 into c68233d
Browse files Browse the repository at this point in the history
  • Loading branch information
meixg committed Aug 12, 2021
2 parents c68233d + a37e9e4 commit c42f94f
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 16 deletions.
3 changes: 2 additions & 1 deletion jest.config.js
Expand Up @@ -4,7 +4,8 @@ module.exports = {
],
testMatch: [
'<rootDir>/test/unit/**/*.ts',
'<rootDir>/test/e2e.spec.ts'
'<rootDir>/test/e2e.spec.ts',
'<rootDir>/test/error.spec.ts'
],
transform: {
'^.+\\.ts$': 'babel-jest'
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -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"
},
Expand Down
19 changes: 18 additions & 1 deletion src/ast/renderer-ast-dfn.ts
Expand Up @@ -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 = '+' | '-' | '*' | '/' | '.' | '===' | '!==' | '||' | '&&' | '[]' | '+=' | '!=' | '=='

Expand Down Expand Up @@ -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 (
Expand Down
6 changes: 5 additions & 1 deletion src/ast/renderer-ast-util.ts
Expand Up @@ -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)))
Expand All @@ -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)
}
Expand Down
5 changes: 5 additions & 0 deletions src/ast/renderer-ast-walker.ts
Expand Up @@ -94,6 +94,11 @@ export function * walk (node: Expression | Statement): Iterable<Expression | Sta
yield * walk(node.iterable)
for (const stmt of node.body) yield * walk(stmt)
break
case SyntaxKind.TryStatement:
for (const stmt of node.block) yield * walk(stmt)
yield * walk(node.handler.param)
for (const stmt of node.handler.body) yield * walk(stmt)
break
default: assertNever(node)
}
}
27 changes: 19 additions & 8 deletions src/compilers/renderer-compiler.ts
Expand Up @@ -8,7 +8,7 @@ import { ANodeCompiler } from './anode-compiler'
import { ComponentInfo } from '../models/component-info'
import { RenderOptions } from './renderer-options'
import { FunctionDefinition, ComputedCall, Foreach, FunctionCall, MapLiteral, If, CreateComponentInstance, ImportHelper, ComponentReferenceLiteral, ConditionalExpression } from '../ast/renderer-ast-dfn'
import { EMPTY_MAP, STATEMENT, NEW, BINARY, ASSIGN, DEF, RETURN, createDefaultValue, L, I, NULL, UNDEFINED } from '../ast/renderer-ast-util'
import { EMPTY_MAP, STATEMENT, NEW, BINARY, ASSIGN, DEF, RETURN, createDefaultValue, L, I, NULL, UNDEFINED, createTryStatement } from '../ast/renderer-ast-util'
import { IDGenerator } from '../utils/id-generator'
import { mergeLiteralAdd } from '../optimizers/merge-literal-add'

Expand Down Expand Up @@ -52,10 +52,15 @@ export class RendererCompiler {

// call inited
if (info.hasMethod('inited')) {
body.push(STATEMENT(new FunctionCall(
BINARY(I('instance'), '.', I('inited')),
[]
)))
body.push(createTryStatement(
[STATEMENT(new FunctionCall(BINARY(I('instance'), '.', I('inited')), []))],
I('e'),
[STATEMENT(new FunctionCall(BINARY(I('sanSSRHelpers'), '.', I('handleError')), [
I('e'),
I('instance'),
L('hook:inited')
]))]
))
}

// calc computed
Expand Down Expand Up @@ -104,9 +109,15 @@ export class RendererCompiler {
const item = BINARY(BINARY(I('ctx'), '.', I('data')), '[]', I('key'))

return [
DEF(
'initData',
new FunctionCall(BINARY(I('instance'), '.', I('initData')), [])
DEF('initData', undefined),
createTryStatement(
[ASSIGN(I('initData'), new FunctionCall(BINARY(I('instance'), '.', I('initData')), []))],
I('e'),
[STATEMENT(new FunctionCall(BINARY(I('sanSSRHelpers'), '.', I('handleError')), [
I('e'),
I('instance'),
L('initData')
]))]
),
createDefaultValue(I('initData'), new MapLiteral([])),
new Foreach(I('key'), I('value'), I('initData'), [
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/create-helpers.ts
Expand Up @@ -7,6 +7,7 @@ import { createResolver } from './resolver'
import { SanSSRData } from './san-ssr-data'
import { JSEmitter } from '../target-js/js-emitter'
import { readStringSync } from '../utils/fs'
import { handleError } from './handle-error'

/**
* 编译成源代码时,需要包含的运行时文件
Expand All @@ -30,6 +31,8 @@ export interface SanSSRHelpers {
* 组件 render、Class 解析器
*/
createResolver: typeof createResolver

handleError: typeof handleError
}

/**
Expand Down Expand Up @@ -60,5 +63,5 @@ export function emitHelpers (emitter: JSEmitter) {
* 编译成 render 函数时,使用的 helper
*/
export function createHelpers (): SanSSRHelpers {
return { _, SanSSRData, createResolver }
return { _, SanSSRData, createResolver, handleError }
}
14 changes: 14 additions & 0 deletions src/runtime/handle-error.ts
@@ -0,0 +1,14 @@
import type { SanComponent } from 'san'

export function handleError (e: Error, instance: SanComponent<{}>, 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
}
1 change: 1 addition & 0 deletions 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'
19 changes: 17 additions & 2 deletions src/runtime/underscore.ts
@@ -1,4 +1,5 @@
import { ComponentClass } from '../models/component'
import { handleError } from './handle-error'

const BASE_PROPS = {
class: 1,
Expand Down Expand Up @@ -96,11 +97,25 @@ 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) {
/* istanbul ignore next */
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) {
/* istanbul ignore next */
handleError(e, ctx.instance, 'computed:' + name)
}
return value
}

function iterate (val: any[] | object) {
Expand Down
8 changes: 8 additions & 0 deletions src/target-js/js-emitter.ts
Expand Up @@ -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)
}
}
Expand Down
165 changes: 165 additions & 0 deletions 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: '<h1>test</h1>',
inited: function () {
throw new Error('error')
}
})
const MyComponent = san.defineComponent({
template: '<div><x-child /></div>',
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: '<h1>test</h1>',
initData: function () {
throw new Error('error')
}
})
const MyComponent = san.defineComponent({
template: '<div><x-child /></div>',
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: '<h1>{{ message }}</h1>',
computed: {
message: function () {
throw new Error('error')
}
}
})
const MyComponent = san.defineComponent({
template: '<div><x-child /></div>',
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: '<h1>{{ msg | add }}</h1>',
filters: {
add: function () {
throw new Error('error')
}
},
initData: function () {
return {
msg: 'test'
}
}
})
const MyComponent = san.defineComponent({
template: '<div><x-child /></div>',
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: '<span>test</span>',
inited: function () {
throw new Error('error')
}
})
const Child = san.defineComponent({
template: '<h1><slot /></h1>'
})
const MyComponent = san.defineComponent({
template: '<div><x-child><x-slot-child /></x-child></div>',
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')
})

0 comments on commit c42f94f

Please sign in to comment.