diff --git a/src/models/component-info.ts b/src/models/component-info.ts index da96cfe1..51077a83 100644 --- a/src/models/component-info.ts +++ b/src/models/component-info.ts @@ -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,都需要实现如下接口 */ @@ -57,7 +59,8 @@ abstract class ComponentInfoImpl + public readonly childComponents: Map, + public readonly componentType: ComponentType ) {} abstract hasMethod (name: string): boolean @@ -88,9 +91,10 @@ export class DynamicComponentInfo extends ComponentInfoImpl, + componentType: ComponentType, public readonly componentClass: Component ) { - super(id, root, childComponents) + super(id, root, childComponents, componentType) this.proto = Object.assign(componentClass.prototype, componentClass) } @@ -117,6 +121,7 @@ export class JSComponentInfo extends ComponentInfoImpl { className: string, properties: Map, sourceCode: string, + componentType: ComponentType = 'normal', isRawObject: boolean = false ) { const template = properties.has('template') ? getLiteralValue(properties.get('template')!) as string : '' @@ -126,7 +131,7 @@ export class JSComponentInfo extends ComponentInfoImpl { ? 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 @@ -167,9 +172,10 @@ export class TypedComponentInfo extends ComponentInfoImpl implements ComponentIn id: string, root: ANode, childComponents: Map, - 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') } diff --git a/src/parsers/component-class-parser.ts b/src/parsers/component-class-parser.ts index 0df6b95f..98b741f9 100644 --- a/src/parsers/component-class-parser.ts +++ b/src/parsers/component-class-parser.ts @@ -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' @@ -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' } /** diff --git a/src/parsers/javascript-san-parser.ts b/src/parsers/javascript-san-parser.ts index 84d64810..b4bc2c86 100644 --- a/src/parsers/javascript-san-parser.ts +++ b/src/parsers/javascript-san-parser.ts @@ -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, @@ -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),得到组件信息。 */ @@ -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] } @@ -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)) @@ -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) // 删除掉子组件 @@ -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 @@ -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) || diff --git a/src/parsers/typescript-san-parser.ts b/src/parsers/typescript-san-parser.ts index e33e5a47..1c40adcc 100644 --- a/src/parsers/typescript-san-parser.ts +++ b/src/parsers/typescript-san-parser.ts @@ -91,7 +91,10 @@ export class TypeScriptSanParser { trimWhitespace, delimiters }), childComponents, - classDeclaration + classDeclaration, + + // TypeScript 目前只支持 class 方式定义组件,还不支持 TemplateComponent + 'normal' ) } diff --git a/test/unit/models/component-info.spec.ts b/test/unit/models/component-info.spec.ts index abb519e5..7be111d2 100644 --- a/test/unit/models/component-info.spec.ts +++ b/test/unit/models/component-info.spec.ts @@ -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 @@ -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([]) }) }) diff --git a/test/unit/parsers/component-class-parser.spec.ts b/test/unit/parsers/component-class-parser.spec.ts index a8d831d7..092dd763 100644 --- a/test/unit/parsers/component-class-parser.spec.ts +++ b/test/unit/parsers/component-class-parser.spec.ts @@ -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', () => { @@ -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: '
' }) + const { componentInfos } = new ComponentClassParser(foo, '').parse() + expect(componentInfos).toHaveLength(1) + expect(componentInfos[0]).toHaveProperty('componentType', 'template') + }) }) diff --git a/test/unit/parsers/javascript-san-parser.spec.ts b/test/unit/parsers/javascript-san-parser.spec.ts index 14c72bec..b5c821d8 100644 --- a/test/unit/parsers/javascript-san-parser.spec.ts +++ b/test/unit/parsers/javascript-san-parser.spec.ts @@ -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: '
' }) + export default defineTemplateComponent({ template: '
' }) + ` + 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' @@ -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: '
' }) + module.exports = defineTemplateComponent({ template: '
' }) + ` + 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 = `