Skip to content

Commit

Permalink
feat: parse templateComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
meixg committed May 20, 2022
1 parent 9c3a4a5 commit 883cd24
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 27 deletions.
16 changes: 11 additions & 5 deletions src/models/component-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { isATextNode } from '../ast/san-ast-type-guards'
export type TagName = string
type TrimWhitespace = 'none' | 'blank' | 'all' | undefined

export type ComponentType = 'normal' | 'template'

/**
* 所有类型的 ComponentInfo,都需要实现如下接口
*/
Expand Down Expand Up @@ -57,7 +59,8 @@ abstract class ComponentInfoImpl<R extends ComponentReference = ComponentReferen
*/
public readonly id: string,
public readonly root: ANode,
public readonly childComponents: Map<TagName, R>
public readonly childComponents: Map<TagName, R>,
public readonly componentType: ComponentType
) {}

abstract hasMethod (name: string): boolean
Expand Down Expand Up @@ -88,9 +91,10 @@ export class DynamicComponentInfo extends ComponentInfoImpl<DynamicComponentRefe
id: string,
root: ANode,
childComponents: Map<TagName, DynamicComponentReference>,
componentType: ComponentType,
public readonly componentClass: Component
) {
super(id, root, childComponents)
super(id, root, childComponents, componentType)
this.proto = Object.assign(componentClass.prototype, componentClass)
}

Expand All @@ -117,6 +121,7 @@ export class JSComponentInfo extends ComponentInfoImpl<ComponentReference> {
className: string,
properties: Map<string, Node>,
sourceCode: string,
componentType: ComponentType = 'normal',
isRawObject: boolean = false
) {
const template = properties.has('template') ? getLiteralValue(properties.get('template')!) as string : ''
Expand All @@ -126,7 +131,7 @@ export class JSComponentInfo extends ComponentInfoImpl<ComponentReference> {
? getStringArrayValue(properties.get('delimiters')!) as [string, string] : undefined
const root = parseAndNormalizeTemplate(template, { trimWhitespace, delimiters })

super(id, root, new Map())
super(id, root, new Map(), componentType)
this.className = className
this.properties = properties
this.sourceCode = sourceCode
Expand Down Expand Up @@ -167,9 +172,10 @@ export class TypedComponentInfo extends ComponentInfoImpl implements ComponentIn
id: string,
root: ANode,
childComponents: Map<TagName, ComponentReference>,
public readonly classDeclaration: ClassDeclaration
public readonly classDeclaration: ClassDeclaration,
componentType: ComponentType = 'normal'
) {
super(id, root, childComponents)
super(id, root, childComponents, componentType)
this.computedNames = getObjectLiteralPropertyKeys(this.classDeclaration, 'computed')
this.filterNames = getObjectLiteralPropertyKeys(this.classDeclaration, 'filters')
}
Expand Down
17 changes: 15 additions & 2 deletions src/parsers/component-class-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { Component, defineComponent, DefinedComponentClass } from 'san'
import { DynamicSanSourceFile } from '../models/san-source-file'
import { DynamicComponentInfo } from '../models/component-info'
import { ComponentType, DynamicComponentInfo } from '../models/component-info'
import { getMemberFromClass } from '../utils/lang'
import { isComponentLoader } from '../models/component'
import { parseAndNormalizeTemplate } from './parse-template'
Expand Down Expand Up @@ -72,7 +72,20 @@ export class ComponentClassParser {
const rootANode = parseAndNormalizeTemplate(template, { trimWhitespace, delimiters })
const childComponents = this.getChildComponentClasses(componentClass, id)

return new DynamicComponentInfo(id, rootANode, childComponents, componentClass as Component)
return new DynamicComponentInfo(
id,
rootANode,
childComponents,
this.getComponentType(componentClass as Component), componentClass as Component
)
}

getComponentType (component: Component): ComponentType {
if (component.prototype.watch) {
return 'normal'
}

return 'template'
}

/**
Expand Down
66 changes: 51 additions & 15 deletions src/parsers/javascript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import debugFactory from 'debug'
import { ancestor } from 'acorn-walk'
import { Node as AcornNode, parse } from 'acorn'
import { CallExpression, Program, Node, Class } from 'estree'
import { CallExpression, Program, Node, Class, ObjectExpression } from 'estree'
import { generate } from 'astring'
import { JSComponentInfo } from '../models/component-info'
import { ComponentType, JSComponentInfo } from '../models/component-info'
import {
isVariableDeclarator,
isProperty,
Expand Down Expand Up @@ -51,6 +51,16 @@ type ImportName = string
type ExportName = string
type ImportSpecifier = string

/**
* 组件定义可能的 node 类型
*/
export type ComponentDefinition = CallExpression | Class | ObjectExpression

/**
* 子组件(components 属性中)可能的 node 类型
*/
export type ChildComponentDefinition = ObjectExpression

/**
* 把包含 San 组件定义的 JavaScript 源码,通过静态分析(AST),得到组件信息。
*/
Expand Down Expand Up @@ -99,18 +109,20 @@ export class JavaScriptSanParser {
}

parseComponents (): [JSComponentInfo[], JSComponentInfo | undefined] {
const parseComponentFromNode = (node: AcornNode, ancestors: AcornNode[]) => {
const visitor = (node: AcornNode, ancestors: AcornNode[]) => {
const parent = ancestors[ancestors.length - 2] as Node
if (!this.isComponent(node as Node)) return
const component = this.parseComponentFromNode(node as Node, parent)
if (component.className === this.defaultExport) {
this.entryComponentInfo = component
const n = node as Node
if (this.isComponent(n)) {
const component = this.parseComponentFromNode(n, parent)
if (component.className === this.defaultExport) {
this.entryComponentInfo = component
}
}
}
ancestor(this.root as any as AcornNode, {
CallExpression: parseComponentFromNode,
ClassExpression: parseComponentFromNode,
ClassDeclaration: parseComponentFromNode
CallExpression: visitor,
ClassExpression: visitor,
ClassDeclaration: visitor
})
return [this.componentInfos, this.entryComponentInfo]
}
Expand Down Expand Up @@ -177,7 +189,7 @@ export class JavaScriptSanParser {
throw new Error(`${location(child)} cannot parse components`)
}

private parseComponentFromNode (node: Node, parent: Node) {
private parseComponentFromNode (node: ComponentDefinition, parent: Node) {
// export default Component
if (isExportDefaultDeclaration(parent)) {
return (this.entryComponentInfo = this.createComponent(node, undefined, true))
Expand Down Expand Up @@ -241,14 +253,21 @@ export class JavaScriptSanParser {
}
}

createComponent (node: Node, name: string = getClassName(node), isDefault = false) {
createComponent (node: ComponentDefinition, name: string = getClassName(node), isDefault = false) {
const properties = new Map(this.getPropertiesFromComponentDeclaration(node, name))
const id = componentID(isDefault, (name
? (this.exports.get(name) || name)
: ('SanSSRAnonymousComponent' + this.id++)
))
this.componentIDs.set(node, id)
const comp = new JSComponentInfo(id, name, properties, this.stringify(node), isObjectExpression(node))
const comp = new JSComponentInfo(
id,
name,
properties,
this.stringify(node),
this.getComponentType(node),
isObjectExpression(node)
)
this.componentInfos.push(comp)

// 删除掉子组件
Expand All @@ -258,7 +277,13 @@ export class JavaScriptSanParser {

private getOrCreateDefaultLoaderComponent (): JSComponentInfo {
if (!this.defaultPlaceholderComponent) {
this.defaultPlaceholderComponent = new JSComponentInfo(DEFAULT_LOADER_CMP, '', new Map(), 'function(){}')
this.defaultPlaceholderComponent = new JSComponentInfo(
DEFAULT_LOADER_CMP,
'',
new Map(),
'function(){}',
'template'
)
this.componentInfos.push(this.defaultPlaceholderComponent)
}
return this.defaultPlaceholderComponent
Expand All @@ -283,10 +308,21 @@ export class JavaScriptSanParser {
yield * getMemberAssignmentsTo(this.root, name)
}

private isComponent (node: Node) {
private isComponent (node: Node): node is ComponentDefinition {
return this.isDefineComponentCall(node) || this.isComponentClass(node)
}

private getComponentType (node: ComponentDefinition): ComponentType {
if (
isCallExpression(node) &&
this.isImportedFromSanWithName(node.callee, this.defineTemplateComponentIdentifier)
) {
return 'template'
}

return 'normal'
}

private isDefineComponentCall (node: Node): node is CallExpression {
return isCallExpression(node) &&
(this.isImportedFromSanWithName(node.callee, this.defineComponentIdentifier) ||
Expand Down
5 changes: 4 additions & 1 deletion src/parsers/typescript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ export class TypeScriptSanParser {
trimWhitespace, delimiters
}),
childComponents,
classDeclaration
classDeclaration,

// TypeScript 目前只支持 class 方式定义组件,还不支持 TemplateComponent
'normal'
)
}

Expand Down
6 changes: 3 additions & 3 deletions test/unit/models/component-info.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { JSComponentInfo, TypedComponentInfo, DynamicComponentInfo } from '../..
import { getPropertiesFromObject } from '../../../src/ast/js-ast-util'
import { parse } from 'acorn'
import { Project } from 'ts-morph'
import { ANode, defineComponent } from 'san'
import { ANode, Component, defineComponent } from 'san'

describe('TypedComponentInfo', function () {
let proj
Expand Down Expand Up @@ -36,12 +36,12 @@ describe('DynamicComponentInfo', function () {
b
}
})
const info = new DynamicComponentInfo('id', null as ANode, new Map(), component)
const info = new DynamicComponentInfo('id', null as ANode, new Map(), 'normal', component)
expect(info.getFilterNames()).toEqual(['a', 'x-b', 'b'])
})
it('should return empty array if filters not defined', () => {
const component = defineComponent({})
const info = new DynamicComponentInfo('id', null as ANode, new Map(), component)
const info = new DynamicComponentInfo('id', null as ANode, new Map(), 'normal', component)
expect(info.getFilterNames()).toEqual([])
})
})
Expand Down
10 changes: 9 additions & 1 deletion test/unit/parsers/component-class-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ComponentClassParser } from '../../../src/parsers/component-class-parser'
import { defineComponent } from 'san'
import { defineComponent, defineTemplateComponent } from 'san'

describe('ComponentClassParser', () => {
it('should parse one single component', () => {
const foo = defineComponent({ template: 'FOO' })
const { componentInfos } = new ComponentClassParser(foo, '').parse()
expect(componentInfos).toHaveLength(1)
expect(componentInfos[0]).toHaveProperty('componentType', 'normal')
expect(componentInfos[0].proto).toHaveProperty('template', 'FOO')
})
it('should parse recursively', () => {
Expand All @@ -14,7 +15,14 @@ describe('ComponentClassParser', () => {
const coo = defineComponent({ template: 'COO', components: { foo, bar } })
const { componentInfos } = new ComponentClassParser(coo, '').parse()
expect(componentInfos).toHaveLength(3)
expect(componentInfos[0]).toHaveProperty('componentType', 'normal')
expect(componentInfos.map(x => x.proto.template)).toEqual(['COO', 'BAR', 'FOO'])
expect(componentInfos.find(item => item.id === 'foo')).toBeTruthy()
})
it('should parse defineTemplateComponent', () => {
const foo = defineTemplateComponent({ template: '<div></div>' })
const { componentInfos } = new ComponentClassParser(foo, '').parse()
expect(componentInfos).toHaveLength(1)
expect(componentInfos[0]).toHaveProperty('componentType', 'template')
})
})
30 changes: 30 additions & 0 deletions test/unit/parsers/javascript-san-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,26 @@ describe('JavaScriptSanParser', () => {
const [components, entry] = parser.parseComponents()
expect(components).toHaveLength(1)
expect(components[0]).toEqual(entry)
expect(components[0]).toHaveProperty('componentType', 'normal')
expect(entry).toHaveProperty('id', 'default')
expect(entry.getComputedNames()).toEqual(['foo'])
expect(entry.hasMethod('inited')).toBeTruthy()
expect(entry.hasMethod('created')).toBeFalsy()
})
it('should parse defineTemplateComponent', () => {
const script = `
import { defineTemplateComponent } from 'san'
const YComponent = defineTemplateComponent({ template: '<div></div>' })
export default defineTemplateComponent({ template: '<div></div>' })
`
const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module')
parser.parseNames()
const [components, entry] = parser.parseComponents()
expect(components).toHaveLength(2)
expect(components[0]).toHaveProperty('componentType', 'template')
expect(components[1]).toHaveProperty('componentType', 'template')
expect(components[1]).toEqual(entry)
})
it('should parse multiple components from ESM', () => {
const script = `
import san from 'san'
Expand Down Expand Up @@ -106,6 +121,21 @@ describe('JavaScriptSanParser', () => {
expect(entry).toHaveProperty('id', 'default')
expect(components).toHaveLength(1)
expect(components[0]).toEqual(entry)
expect(components[0]).toHaveProperty('componentType', 'normal')
})
it('should parse defineTemplateComponent', () => {
const script = `
const { defineTemplateComponent } = require('san')
exports.YComponent = defineTemplateComponent({ template: '<div></div>' })
module.exports = defineTemplateComponent({ template: '<div></div>' })
`
const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module')
parser.parseNames()
const [components, entry] = parser.parseComponents()
expect(components).toHaveLength(2)
expect(components[0]).toHaveProperty('componentType', 'template')
expect(components[1]).toHaveProperty('componentType', 'template')
expect(components[1]).toEqual(entry)
})
it('should parse multiple components from script', () => {
const script = `
Expand Down

0 comments on commit 883cd24

Please sign in to comment.