Skip to content

Commit

Permalink
feat: support object literal as component
Browse files Browse the repository at this point in the history
  • Loading branch information
shirookie authored and harttle committed Mar 22, 2021
1 parent e5ba9d5 commit dbcce56
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 29 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"jest": "^26.2.2",
"mustache": "^4.0.1",
"san": "^3.10.0",
"san-html-cases": "^3.10.9",
"san-html-cases": "^3.10.10",
"san-ssr-target-fake-cmd": "^1.0.0",
"san-ssr-target-fake-esm": "^1.0.0",
"semantic-release": "^17.1.1",
Expand Down
23 changes: 15 additions & 8 deletions src/ast/ts-ast-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Node, MethodDeclaration, ShorthandPropertyAssignment, PropertyAssignment, ImportDeclaration, ClassDeclaration, SourceFile } from 'ts-morph'
import type { Node, MethodDeclaration, ShorthandPropertyAssignment, PropertyAssignment, ImportDeclaration, ClassDeclaration, SourceFile, ObjectLiteralExpression } from 'ts-morph'
import { TypeGuards, SyntaxKind } from 'ts-morph'
import debugFactory from 'debug'
import { TagName } from '../models/component-info'
Expand Down Expand Up @@ -37,12 +37,6 @@ export function isChildClassOf (clazz: ClassDeclaration, parentClass: string) {
return true
}

export function getComponentDeclarations (sourceFile: SourceFile) {
const componentClassIdentifier = getComponentClassIdentifier(sourceFile)
if (!componentClassIdentifier) return []
return sourceFile.getClasses().filter(clazz => isChildClassOf(clazz, componentClassIdentifier))
}

export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string, defaultValue: T): T;
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string): T | undefined;
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string, defaultValue?: T): T | undefined {
Expand Down Expand Up @@ -91,7 +85,11 @@ function getLiteralText (expr: Node) {
}
}

export function getChildComponents (clazz: ClassDeclaration, defaultClassDeclaration?: ClassDeclaration): Map<TagName, ComponentReference> {
export function getChildComponents (
clazz: ClassDeclaration,
defaultClassDeclaration: ClassDeclaration | undefined,
getComponentClassFromObjectLiteral: (rawObjectExpr: ObjectLiteralExpression) => ClassDeclaration
): Map<TagName, ComponentReference> {
const member = clazz.getProperty('components')
const ret: Map<TagName, ComponentReference> = new Map()
if (!member) return ret
Expand Down Expand Up @@ -138,6 +136,15 @@ export function getChildComponents (clazz: ClassDeclaration, defaultClassDeclara
))
continue
}
const propObjectValue = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)
if (propObjectValue) {
const clazz = getComponentClassFromObjectLiteral(propObjectValue)
ret.set(propName, new ComponentReference(
'.',
clazz.getName()!
))
continue
}
const childComponentClassName = prop.getInitializerIfKindOrThrow(SyntaxKind.Identifier).getText()
if (importedNames.has(childComponentClassName)) { // 子组件来自外部源文件
const { specifier, named } = importedNames.get(childComponentClassName)!
Expand Down
5 changes: 4 additions & 1 deletion src/models/component-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,14 @@ export class DynamicComponentInfo extends ComponentInfoImpl<DynamicComponentRefe
export class JSComponentInfo extends ComponentInfoImpl<ComponentReference> {
public readonly className: string
public readonly sourceCode: string
public readonly isRawObject: boolean
private readonly properties: Map<string, Node>
constructor (
id: string,
className: string,
properties: Map<string, Node>,
sourceCode: string
sourceCode: string,
isRawObject: boolean = false
) {
const template = properties.has('template') ? getLiteralValue(properties.get('template')!) as string : ''
const trimWhitespace: TrimWhitespace = properties.has('trimWhitespace') ? getLiteralValue(properties.get('trimWhitespace')!) : undefined
Expand All @@ -113,6 +115,7 @@ export class JSComponentInfo extends ComponentInfoImpl<ComponentReference> {
this.className = className
this.properties = properties
this.sourceCode = sourceCode
this.isRawObject = isRawObject
}

hasMethod (name: string) {
Expand Down
3 changes: 3 additions & 0 deletions src/parsers/component-class-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export class ComponentClassParser {
if (isComponentLoader(componentClass)) {
componentClass = componentClass.placeholder
}
if (typeof componentClass === 'object') {
componentClass = defineComponent(componentClass)
}
if (!componentClass) componentClass = defineComponent({ template: '' })

const template = getMember(componentClass, 'template', '')
Expand Down
6 changes: 5 additions & 1 deletion src/parsers/javascript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export class JavaScriptSanParser {
}

private createChildComponentReference (child: Node, selfId: string): ComponentReference {
if (isObjectExpression(child)) {
this.createComponent(child)
}
if (this.componentIDs.has(child)) {
return new ComponentReference('.', this.componentIDs.get(child)!)
}
Expand Down Expand Up @@ -166,7 +169,7 @@ export class JavaScriptSanParser {
: ('SanSSRAnonymousComponent' + this.id++)
))
this.componentIDs.set(node, id)
const comp = new JSComponentInfo(id, name, properties, this.stringify(node))
const comp = new JSComponentInfo(id, name, properties, this.stringify(node), isObjectExpression(node))
this.componentInfos.push(comp)
return comp
}
Expand All @@ -181,6 +184,7 @@ export class JavaScriptSanParser {

private * getPropertiesFromComponentDeclaration (node: Node, name: string) {
if (this.isComponentClass(node)) yield * getMembersFromClassDeclaration(node as Class)
else if (isObjectExpression(node)) yield * getPropertiesFromObject(node)
else yield * getPropertiesFromObject(node['arguments'][0])
yield * getMemberAssignmentsTo(this.root, name)
}
Expand Down
59 changes: 53 additions & 6 deletions src/parsers/typescript-san-parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SourceFile, ClassDeclaration } from 'ts-morph'
import type { SourceFile, ClassDeclaration, ObjectLiteralExpression } from 'ts-morph'
import { TypeGuards } from 'ts-morph'
import debugFactory from 'debug'
import { getChildComponents, getPropertyStringArrayValue, getComponentDeclarations, getPropertyStringValue } from '../ast/ts-ast-util'
import { getChildComponents, getPropertyStringArrayValue, getComponentClassIdentifier, isChildClassOf, getPropertyStringValue } from '../ast/ts-ast-util'
import { normalizeComponentClass } from './normalize-component'
import { TypedSanSourceFile } from '../models/san-source-file'
import { parseAndNormalizeTemplate } from './parse-template'
Expand All @@ -14,17 +15,46 @@ const debug = debugFactory('ts-component-parser')
*/
export class TypeScriptSanParser {
parse (sourceFile: SourceFile) {
const classDeclarations = getComponentDeclarations(sourceFile).map(normalizeComponentClass)
const componentClassIdentifier = getComponentClassIdentifier(sourceFile)
if (!componentClassIdentifier) {
return new TypedSanSourceFile([], sourceFile)
}
const componentInfos: TypedComponentInfo[] = []

// 初始声明的组件
const classDeclarations = sourceFile.getClasses().filter(clazz => isChildClassOf(clazz, componentClassIdentifier))
const defaultClassDeclaration = classDeclarations.find(clazz => clazz.isDefaultExport())
const componentInfos: TypedComponentInfo[] = classDeclarations.map(decl => this.parseComponentClassDeclaration(decl, defaultClassDeclaration))

// ObjectLiteral 作为匿名组件
// 在 parse 组件前难以解析,需要先遍历找出所有的 ObjectLiteral 并反向向上判断是否属于父级组件的 components
// 所以在 parse components 时遇到再换转成 class 然后 push 到 classDeclarations 中参与遍历 parse
let anonymousComponentId = 0
const createClassFromObjectLiteral = (obj: ObjectLiteralExpression) => {
const clazz = this.convertObjectLiteralToClassDeclaration(obj, 'SanSSRAnonymousComponent' + anonymousComponentId++)
clazz.setExtends(componentClassIdentifier)
classDeclarations.push(clazz)
return clazz
}

// forEach 时再向数组中 push 元素不会被遍历到,所以改成 for of 循环
for (const decl of classDeclarations) {
const clazz = normalizeComponentClass(decl)
const info = this.parseComponentClassDeclaration(clazz, defaultClassDeclaration, createClassFromObjectLiteral)
componentInfos.push(info)
}

return new TypedSanSourceFile(componentInfos, sourceFile, componentInfos.find(info => info.classDeclaration.isDefaultExport()))
}

private parseComponentClassDeclaration (classDeclaration: ClassDeclaration, defaultClassDeclaration?: ClassDeclaration): TypedComponentInfo {
private parseComponentClassDeclaration (
classDeclaration: ClassDeclaration,
defaultClassDeclaration: ClassDeclaration | undefined,
createClassFromObjectLiteral: (obj: ObjectLiteralExpression) => ClassDeclaration
): TypedComponentInfo {
const template = getPropertyStringValue(classDeclaration, 'template', '')
const trimWhitespace = getPropertyStringValue<'none' | 'blank' | 'all'>(classDeclaration, 'trimWhitespace')
const delimiters = getPropertyStringArrayValue<[string, string]>(classDeclaration, 'delimiters')
const childComponents = getChildComponents(classDeclaration, defaultClassDeclaration)
const childComponents = getChildComponents(classDeclaration, defaultClassDeclaration, createClassFromObjectLiteral)

for (const constructorDelcaration of classDeclaration.getConstructors()) {
constructorDelcaration.remove()
Expand All @@ -39,4 +69,21 @@ export class TypeScriptSanParser {
classDeclaration
)
}

private convertObjectLiteralToClassDeclaration (rawObjectExpr: ObjectLiteralExpression, className: string) {
const sourceFile = rawObjectExpr.getSourceFile()
const classDeclaration = sourceFile.addClass({
name: className
})
for (const prop of rawObjectExpr.getProperties()) {
if (!TypeGuards.isPropertyAssignment(prop)) throw new Error(`${JSON.stringify(prop.getText())} not supported`)
const init = prop.getInitializer()
const propDecl = classDeclaration.addProperty({
isStatic: true,
name: prop.getText()
})
propDecl.setInitializer(init!.getText())
}
return classDeclaration
}
}
6 changes: 4 additions & 2 deletions src/target-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ export default class ToJSCompiler implements Compiler {
emitter.writeLines(sourceFile.getFileContent())

for (const info of sourceFile.componentInfos) {
const proto = info.className ? info.className : info.sourceCode
emitter.writeLine(`sanSSRResolver.setPrototype("${info.id}", sanSSRHelpers._.createInstanceFromClass(${proto}));`)
const proto = info.isRawObject
? info.sourceCode
: `sanSSRHelpers._.createInstanceFromClass(${info.className || info.sourceCode})`
emitter.writeLine(`sanSSRResolver.setPrototype("${info.id}", ${proto});`)
}
}

Expand Down
31 changes: 31 additions & 0 deletions test/unit/parsers/typescript-san-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,35 @@ describe('.parseFromTypeScript()', () => {
const [info] = sourceFile.componentInfos
expect(info.classDeclaration.getConstructors()).toHaveLength(0)
})

it('should return empty component infos if san.Component not imported', function () {
const file = proj.createSourceFile('foo.ts', `
import { foo } from 'foo'
export class Foo extends Component {
foo = 'bar'
constructor() {
foo()
}
}
`)
const sourceFile = new TypeScriptSanParser().parse(file)
expect(sourceFile.componentInfos).toHaveLength(0)
})

it('should throw if prop of literal object is not property assignment', function () {
const file = proj.createSourceFile('foo.ts', `
import { Component } from 'san'
const bar = {
template: '<div>bar</div>'
}
export class MyComponent extends Component {
static components = {
'v-bar': { ...bar }
}
}
`)
expect(() => {
new TypeScriptSanParser().parse(file)
}).toThrow()
})
})
8 changes: 1 addition & 7 deletions test/unit/utils/ts-ast-util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getPropertyStringArrayValue, getComponentDeclarations, getObjectLiteralPropertyKeys, getChildComponents, getPropertyStringValue, getComponentClassIdentifier, isChildClassOf } from '../../../src/ast/ts-ast-util'
import { getPropertyStringArrayValue, getObjectLiteralPropertyKeys, getChildComponents, getPropertyStringValue, getComponentClassIdentifier, isChildClassOf } from '../../../src/ast/ts-ast-util'
import { Project } from 'ts-morph'

describe('utils/ts-ast-util', function () {
Expand Down Expand Up @@ -145,12 +145,6 @@ describe('utils/ts-ast-util', function () {
expect(getComponentClassIdentifier(file)).toEqual('SanComponent')
})
})
describe('.getComponentDeclarations()', function () {
it('should return [] if san.Component not imported', () => {
const file = proj.createSourceFile('foo.ts', 'import { resolve } from \'path\'')
expect(getComponentDeclarations(file)).toEqual([])
})
})
describe('.isChildClassOf()', function () {
it('should return true if is lhs is child class of rhs', () => {
const file = proj.createSourceFile('foo.ts', `
Expand Down

0 comments on commit dbcce56

Please sign in to comment.