From b9f0f32cecaf6068d7ca188154ba37e1e0388d09 Mon Sep 17 00:00:00 2001 From: Xuguang Mei Date: Sun, 4 Jun 2023 23:08:56 +0800 Subject: [PATCH] fix: support sanReferenceInfo in ts files (#168) --- src/ast/js-ast-util.ts | 4 +- src/ast/ts-ast-util.ts | 26 ++++++--- src/compilers/renderer-options.ts | 14 ++++- src/models/san-project.ts | 40 +++++++++++-- src/parsers/javascript-san-parser.ts | 48 ++++++--------- src/parsers/san-file-parser.ts | 6 +- src/parsers/typescript-san-parser.ts | 5 +- .../parsers/javascript-san-parser.spec.ts | 58 +++++++++++-------- test/unit/parsers/san-file-parser.spec.ts | 18 ++++-- .../parsers/typescript-san-parser.spec.ts | 15 +++-- test/unit/target-js/index.spec.ts | 8 ++- test/unit/utils/ts-ast-util.spec.ts | 13 +++-- 12 files changed, 164 insertions(+), 91 deletions(-) diff --git a/src/ast/js-ast-util.ts b/src/ast/js-ast-util.ts index 0f999d4d..b9df6fc5 100644 --- a/src/ast/js-ast-util.ts +++ b/src/ast/js-ast-util.ts @@ -66,8 +66,8 @@ export function getRequireSpecifier (node: Node): string { // 是否是 require 了 spec 的语句。 // 例如:对于 node = ,isRequireSpecifier(node, 'san') === true -export function isRequireSpecifier (node: Expression, spec: string) { - return isRequire(node) && getRequireSpecifier(node) === spec +export function isRequireSpecifier (node: Expression, spec: string[]) { + return isRequire(node) && spec.includes(getRequireSpecifier(node)) } export function isModuleExports (node: Node) { diff --git a/src/ast/ts-ast-util.ts b/src/ast/ts-ast-util.ts index 7ebe5cf9..e461cbfd 100644 --- a/src/ast/ts-ast-util.ts +++ b/src/ast/ts-ast-util.ts @@ -8,15 +8,14 @@ import type { SourceFile, ObjectLiteralExpression } from 'ts-morph' import { TypeGuards, SyntaxKind } from 'ts-morph' -import debugFactory from 'debug' import { TagName } from '../models/component-info' import { componentID, ComponentReference } from '../models/component-reference' +import { strongParseSanSourceFileOptions } from '../compilers/renderer-options' -const debug = debugFactory('ts-ast-util') - -export function getSanImportDeclaration (sourceFile: SourceFile): ImportDeclaration | undefined { +export function getSanImportDeclaration (sourceFile: SourceFile, moduleNames: string[]): ImportDeclaration | undefined { + const moduleNameSet = new Set(moduleNames) return sourceFile.getImportDeclaration( - node => node.getModuleSpecifierValue() === 'san' + node => moduleNameSet.has(node.getModuleSpecifierValue()) ) } @@ -24,18 +23,27 @@ export function getSanImportDeclaration (sourceFile: SourceFile): ImportDeclarat * import {Component as OtherName} from 'san'; * 获取到 “OtherName” */ -export function getComponentClassIdentifier (sourceFile: SourceFile): string | undefined { - const declaration = getSanImportDeclaration(sourceFile) +export function getComponentClassIdentifier ( + sourceFile: SourceFile, + sanReferenceInfo: strongParseSanSourceFileOptions['sanReferenceInfo']): string | undefined { + const declaration = getSanImportDeclaration(sourceFile, sanReferenceInfo.moduleName) if (!declaration) return const namedImports = declaration.getNamedImports() + const classNameSet = new Set(sanReferenceInfo.className) for (const namedImport of namedImports) { const name = namedImport.getName() - if (name !== 'Component') continue + if (!classNameSet.has(name)) continue const alias = namedImport.getAliasNode() if (alias) return alias.getText() - return 'Component' + return name + } + + const defaultImport = declaration.getDefaultImport() + const text = defaultImport?.getText() + if (text && classNameSet.has(text)) { + return text } } diff --git a/src/compilers/renderer-options.ts b/src/compilers/renderer-options.ts index 84d63e02..c642af3a 100644 --- a/src/compilers/renderer-options.ts +++ b/src/compilers/renderer-options.ts @@ -31,9 +31,9 @@ export interface RenderOptions { removeModules?: RegExp[] sanReferenceInfo?: { - methodName?: string - moduleName?: string - className?: string + methodName?: string | string []; + moduleName?: string | string[]; + className?: string | string[]; } /** @@ -45,3 +45,11 @@ export interface RenderOptions { export interface parseSanSourceFileOptions { sanReferenceInfo?: RenderOptions['sanReferenceInfo'] } + +export interface strongParseSanSourceFileOptions { + sanReferenceInfo: { + methodName: string[]; + moduleName: string[]; + className: string[]; + } +} diff --git a/src/models/san-project.ts b/src/models/san-project.ts index e3743cb6..6171a619 100644 --- a/src/models/san-project.ts +++ b/src/models/san-project.ts @@ -14,7 +14,11 @@ import type { Component } from 'san' import type { TypedSanSourceFile, DynamicSanSourceFile, SanSourceFile, JSSanSourceFile } from '../models/san-source-file' -import type { parseSanSourceFileOptions, RenderOptions } from '../compilers/renderer-options' +import type { + parseSanSourceFileOptions, + RenderOptions, + strongParseSanSourceFileOptions +} from '../compilers/renderer-options' import type { Renderer } from './renderer' import type { CompileOptions } from '../target-js/compilers/compile-options' import type { TargetCodeGenerator } from '../models/target-code-generator' @@ -86,9 +90,17 @@ export class SanProject { public parseSanSourceFile (input: CompileInput, options?: parseSanSourceFileOptions): SanSourceFile public parseSanSourceFile (input: CompileInput, options?: parseSanSourceFileOptions): SanSourceFile { if (isComponentClass(input)) return new ComponentClassParser(input, '').parse() + + const formattedOptions = this.checkAndFormatParseSanSourceFileOptions(options) if (isSanFileDescriptor(input)) { - return new SanFileParser(input.scriptContent, input.templateContent, input.filePath).parse() + return new SanFileParser( + input.scriptContent, + input.templateContent, + input.filePath, + formattedOptions + ).parse() } + const filePath = isFileDescriptor(input) ? input.filePath : input const fileContent = isFileDescriptor(input) ? input.fileContent : undefined if (/\.ts$/.test(filePath)) { @@ -97,9 +109,29 @@ export class SanProject { ? this.tsProject.createSourceFile(filePath, fileContent, { overwrite: true }) : this.tsProject.addSourceFileAtPath(filePath) !fileContent && sourceFile.refreshFromFileSystemSync() - return new TypeScriptSanParser().parse(sourceFile) + return new TypeScriptSanParser().parse(sourceFile, formattedOptions) + } + return new JavaScriptSanParser(filePath, formattedOptions).parse() + } + + private checkAndFormatParseSanSourceFileOptions (options?: parseSanSourceFileOptions) + : strongParseSanSourceFileOptions { + const moduleName = options?.sanReferenceInfo?.moduleName + const methodName = options?.sanReferenceInfo?.methodName + const className = options?.sanReferenceInfo?.className + return { + sanReferenceInfo: { + moduleName: moduleName + ? Array.isArray(moduleName) ? moduleName : [moduleName] + : ['san'], + className: className + ? Array.isArray(className) ? className : [className] + : ['Component'], + methodName: methodName + ? Array.isArray(methodName) ? methodName : [methodName] + : ['defineComponent'] + } } - return new JavaScriptSanParser(filePath, undefined, 'script', options).parse() } /** diff --git a/src/parsers/javascript-san-parser.ts b/src/parsers/javascript-san-parser.ts index b4bc2c86..d3d64a3c 100644 --- a/src/parsers/javascript-san-parser.ts +++ b/src/parsers/javascript-san-parser.ts @@ -41,7 +41,7 @@ import { import { JSSanSourceFile } from '../models/san-source-file' import { componentID, ComponentReference } from '../models/component-reference' import { readFileSync } from 'fs' -import { parseSanSourceFileOptions } from '../compilers/renderer-options' +import { strongParseSanSourceFileOptions } from '../compilers/renderer-options' const debug = debugFactory('ts-component-parser') const DEFAULT_LOADER_CMP = 'SanSSRDefaultLoaderComponent' @@ -69,8 +69,6 @@ export class JavaScriptSanParser { componentInfos: JSComponentInfo[] = [] entryComponentInfo?: JSComponentInfo - private sanComponentIdentifier?: string - private defineComponentIdentifier: string private defineTemplateComponentIdentifier: string private defaultExport?: string private imports: Map = new Map() @@ -78,15 +76,14 @@ export class JavaScriptSanParser { private componentIDs: Map = new Map() private defaultPlaceholderComponent?: JSComponentInfo private id = 0 - private sanReferenceInfo?: parseSanSourceFileOptions['sanReferenceInfo'] + private sanReferenceInfo: strongParseSanSourceFileOptions['sanReferenceInfo'] constructor ( private readonly filePath: string, + options: strongParseSanSourceFileOptions, fileContent?: string, - sourceType: 'module' | 'script' = 'script', - options?: parseSanSourceFileOptions + sourceType: 'module' | 'script' = 'script' ) { - this.defineComponentIdentifier = 'defineComponent' this.defineTemplateComponentIdentifier = 'defineTemplateComponent' this.root = parse( fileContent === undefined ? readFileSync(filePath, 'utf8') : fileContent, @@ -223,19 +220,6 @@ export class JavaScriptSanParser { parseNames () { for (const [local, specifier, imported] of this.parseImportedNames()) { this.imports.set(local, [specifier, imported]) - if (imported === 'Component' && specifier === 'san') { - this.sanComponentIdentifier = local - } - if (imported === 'defineComponent' && specifier === 'san') { - this.defineComponentIdentifier = local - } - if (imported === 'defineTemplateComponent' && specifier === 'san') { - this.defineTemplateComponentIdentifier = local - } - } - if (this.sanReferenceInfo) { - this.sanComponentIdentifier = this.sanReferenceInfo.moduleName - this.defineComponentIdentifier = this.sanReferenceInfo.methodName || this.defineComponentIdentifier } for (const [local, exported] of findExportNames(this.root)) { @@ -315,7 +299,7 @@ export class JavaScriptSanParser { private getComponentType (node: ComponentDefinition): ComponentType { if ( isCallExpression(node) && - this.isImportedFromSanWithName(node.callee, this.defineTemplateComponentIdentifier) + this.isImportedFromSanWithName(node.callee, [this.defineTemplateComponentIdentifier]) ) { return 'template' } @@ -325,36 +309,38 @@ export class JavaScriptSanParser { private isDefineComponentCall (node: Node): node is CallExpression { return isCallExpression(node) && - (this.isImportedFromSanWithName(node.callee, this.defineComponentIdentifier) || - this.isImportedFromSanWithName(node.callee, this.defineTemplateComponentIdentifier)) + (this.isImportedFromSanWithName(node.callee, this.sanReferenceInfo.methodName) || + this.isImportedFromSanWithName(node.callee, [this.defineTemplateComponentIdentifier])) } private isCreateComponentLoaderCall (node: Node): node is CallExpression { - return isCallExpression(node) && this.isImportedFromSanWithName(node.callee, 'createComponentLoader') + return isCallExpression(node) && this.isImportedFromSanWithName(node.callee, ['createComponentLoader']) } private isComponentClass (node: Node): node is Class { - return isClass(node) && !!node.superClass && this.isImportedFromSanWithName(node.superClass, 'Component') + return isClass(node) && !!node.superClass && + this.isImportedFromSanWithName(node.superClass, this.sanReferenceInfo.className) } - private isImportedFromSanWithName (expr: Node, sanExport: string): boolean { + private isImportedFromSanWithName (expr: Node, sanExport: string[]): boolean { if (isIdentifier(expr)) { - return this.isImportedFrom(expr.name, this.sanReferenceInfo?.moduleName || 'san', sanExport) + return this.isImportedFrom(expr.name, this.sanReferenceInfo.moduleName, sanExport) } if (isMemberExpression(expr)) { - return this.isImportedFromSanWithName(expr.object, 'default') && getStringValue(expr.property) === sanExport + return this.isImportedFromSanWithName(expr.object, ['default']) && + sanExport.includes(getStringValue(expr.property)) } if (isCallExpression(expr)) { - return isRequireSpecifier(expr, this.sanReferenceInfo?.moduleName || 'san') && sanExport === 'default' + return isRequireSpecifier(expr, this.sanReferenceInfo.moduleName) && sanExport.includes('default') } return false } - private isImportedFrom (localName: string, packageSpec: string, importedName: string) { + private isImportedFrom (localName: string, packageSpec: string[], importedName: string[]) { if (!this.imports.has(localName)) return false const [spec, name] = this.imports.get(localName)! - return spec === packageSpec && name === importedName + return packageSpec.includes(spec) && importedName.includes(name) } private stringify (node: Node) { diff --git a/src/parsers/san-file-parser.ts b/src/parsers/san-file-parser.ts index 53775cc9..7c73aa46 100644 --- a/src/parsers/san-file-parser.ts +++ b/src/parsers/san-file-parser.ts @@ -12,6 +12,7 @@ import { findDefaultExport } from '../ast/js-ast-util' import { JSSanSourceFile } from '../models/san-source-file' +import { strongParseSanSourceFileOptions } from '../compilers/renderer-options' export class SanFileParser { private readonly parser: JavaScriptSanParser @@ -19,9 +20,10 @@ export class SanFileParser { constructor ( public readonly scriptContent: string, public readonly templateContent: string, - private readonly filePath: string + private readonly filePath: string, + options: strongParseSanSourceFileOptions ) { - this.parser = new JavaScriptSanParser(filePath, scriptContent, 'module') + this.parser = new JavaScriptSanParser(filePath, options, scriptContent, 'module') } parse (): JSSanSourceFile { diff --git a/src/parsers/typescript-san-parser.ts b/src/parsers/typescript-san-parser.ts index 95a843ba..b6dc11b0 100644 --- a/src/parsers/typescript-san-parser.ts +++ b/src/parsers/typescript-san-parser.ts @@ -18,6 +18,7 @@ import { TypedSanSourceFile } from '../models/san-source-file' import { parseAndNormalizeTemplate } from './parse-template' import { ComponentSSRType, TypedComponentInfo } from '../models/component-info' import { componentID } from '../models/component-reference' +import type { strongParseSanSourceFileOptions } from '../compilers/renderer-options' const debug = debugFactory('ts-component-parser') @@ -25,8 +26,8 @@ const debug = debugFactory('ts-component-parser') * 把包含 San 组件定义的 TypeScript 源码,通过静态分析(AST),得到组件信息。 */ export class TypeScriptSanParser { - parse (sourceFile: SourceFile) { - const componentClassIdentifier = getComponentClassIdentifier(sourceFile) + parse (sourceFile: SourceFile, options: strongParseSanSourceFileOptions) { + const componentClassIdentifier = getComponentClassIdentifier(sourceFile, options.sanReferenceInfo) if (!componentClassIdentifier) { return new TypedSanSourceFile([], sourceFile) } diff --git a/test/unit/parsers/javascript-san-parser.spec.ts b/test/unit/parsers/javascript-san-parser.spec.ts index b5c821d8..5121f475 100644 --- a/test/unit/parsers/javascript-san-parser.spec.ts +++ b/test/unit/parsers/javascript-san-parser.spec.ts @@ -1,5 +1,13 @@ import { JavaScriptSanParser } from '../../../src/parsers/javascript-san-parser' +const defaultOptions = { + sanReferenceInfo: { + moduleName: ['san'], + className: ['Component'], + methodName: ['defineComponent'] + } +} + describe('JavaScriptSanParser', () => { describe('#parseImportedNames()', () => { it('should parse imports', () => { @@ -7,7 +15,7 @@ describe('JavaScriptSanParser', () => { import { Component } from 'san' const define = require('san').defineComponent ` - const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module') + const parser = new JavaScriptSanParser('/tmp/foo.san', defaultOptions, script, 'module') const imports = [...parser.parseImportedNames()] expect(imports).toHaveLength(2) expect(imports[0]).toEqual(['Component', 'san', 'Component']) @@ -27,7 +35,7 @@ describe('JavaScriptSanParser', () => { inited() {} } ` - const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module') + const parser = new JavaScriptSanParser('/tmp/foo.san', defaultOptions, script, 'module') parser.parseNames() const [components, entry] = parser.parseComponents() expect(components).toHaveLength(1) @@ -44,7 +52,7 @@ describe('JavaScriptSanParser', () => { const YComponent = defineTemplateComponent({ template: '
' }) export default defineTemplateComponent({ template: '
' }) ` - const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module') + const parser = new JavaScriptSanParser('/tmp/foo.san', defaultOptions, script, 'module') parser.parseNames() const [components, entry] = parser.parseComponents() expect(components).toHaveLength(2) @@ -58,7 +66,7 @@ describe('JavaScriptSanParser', () => { export class XComponent extends san.Component {} export default class YComponent extends san.Component {} ` - const parser = new JavaScriptSanParser('/tmp/foo.san', script, 'module') + const parser = new JavaScriptSanParser('/tmp/foo.san', defaultOptions, script, 'module') parser.parseNames() const [components, entry] = parser.parseComponents() expect(entry).toHaveProperty('id', 'default') @@ -72,7 +80,7 @@ describe('JavaScriptSanParser', () => { const YComponent = defineComponent({ template: '' }) export default san.defineComponent({ template: '