diff --git a/package-lock.json b/package-lock.json index 63404eb8..567c0051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11737,9 +11737,9 @@ "dev": true }, "san-html-cases": { - "version": "3.10.9", - "resolved": "http://registry.npm.baidu-int.com/san-html-cases/-/san-html-cases-3.10.9.tgz", - "integrity": "sha1-RH6641Uv9+OxE7R3P54JdogTB8Q=", + "version": "3.10.10", + "resolved": "https://registry.npmjs.org/san-html-cases/-/san-html-cases-3.10.10.tgz", + "integrity": "sha512-aBEtfOnXMgrp92aO3zMwRE4HftsJq7S0+GoV45+9pY0p0xFzbTZfVfp34Rmcbaxgn4PbWwY6UV1WfFzv7QqGIA==", "dev": true }, "san-ssr-target-fake-cmd": { diff --git a/package.json b/package.json index 38e90490..ef1f9a1a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ast/ts-ast-util.ts b/src/ast/ts-ast-util.ts index 96cbed8c..0a7bc2ad 100644 --- a/src/ast/ts-ast-util.ts +++ b/src/ast/ts-ast-util.ts @@ -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' @@ -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 (clazz: ClassDeclaration, memberName: string, defaultValue: T): T; export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string): T | undefined; export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string, defaultValue?: T): T | undefined { @@ -91,7 +85,11 @@ function getLiteralText (expr: Node) { } } -export function getChildComponents (clazz: ClassDeclaration, defaultClassDeclaration?: ClassDeclaration): Map { +export function getChildComponents ( + clazz: ClassDeclaration, + defaultClassDeclaration: ClassDeclaration | undefined, + getComponentClassFromObjectLiteral: (rawObjectExpr: ObjectLiteralExpression) => ClassDeclaration +): Map { const member = clazz.getProperty('components') const ret: Map = new Map() if (!member) return ret @@ -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)! diff --git a/src/models/component-info.ts b/src/models/component-info.ts index 00405be6..a2aacf1d 100644 --- a/src/models/component-info.ts +++ b/src/models/component-info.ts @@ -97,12 +97,14 @@ export class DynamicComponentInfo extends ComponentInfoImpl { public readonly className: string public readonly sourceCode: string + public readonly isRawObject: boolean private readonly properties: Map constructor ( id: string, className: string, properties: Map, - 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 @@ -113,6 +115,7 @@ export class JSComponentInfo extends ComponentInfoImpl { this.className = className this.properties = properties this.sourceCode = sourceCode + this.isRawObject = isRawObject } hasMethod (name: string) { diff --git a/src/parsers/component-class-parser.ts b/src/parsers/component-class-parser.ts index b3d1cf91..6ecb0611 100644 --- a/src/parsers/component-class-parser.ts +++ b/src/parsers/component-class-parser.ts @@ -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', '') diff --git a/src/parsers/javascript-san-parser.ts b/src/parsers/javascript-san-parser.ts index c2025d14..0be8c94b 100644 --- a/src/parsers/javascript-san-parser.ts +++ b/src/parsers/javascript-san-parser.ts @@ -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)!) } @@ -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 } @@ -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) } diff --git a/src/parsers/typescript-san-parser.ts b/src/parsers/typescript-san-parser.ts index 85352603..e9dbac54 100644 --- a/src/parsers/typescript-san-parser.ts +++ b/src/parsers/typescript-san-parser.ts @@ -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' @@ -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() @@ -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 + } } diff --git a/src/target-js/index.ts b/src/target-js/index.ts index 497f60e7..a6a8f92a 100644 --- a/src/target-js/index.ts +++ b/src/target-js/index.ts @@ -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});`) } } diff --git a/test/unit/parsers/typescript-san-parser.spec.ts b/test/unit/parsers/typescript-san-parser.spec.ts index 21bb790b..101b7acc 100644 --- a/test/unit/parsers/typescript-san-parser.spec.ts +++ b/test/unit/parsers/typescript-san-parser.spec.ts @@ -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: '
bar
' + } + export class MyComponent extends Component { + static components = { + 'v-bar': { ...bar } + } + } + `) + expect(() => { + new TypeScriptSanParser().parse(file) + }).toThrow() + }) }) diff --git a/test/unit/utils/ts-ast-util.spec.ts b/test/unit/utils/ts-ast-util.spec.ts index 0a7da415..6f74f020 100644 --- a/test/unit/utils/ts-ast-util.spec.ts +++ b/test/unit/utils/ts-ast-util.spec.ts @@ -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 () { @@ -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', `