Skip to content

Commit

Permalink
fix: support sanReferenceInfo in ts files (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
meixg authored Jun 4, 2023
1 parent fdaff53 commit b9f0f32
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 91 deletions.
4 changes: 2 additions & 2 deletions src/ast/js-ast-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export function getRequireSpecifier (node: Node): string {

// 是否是 require 了 spec 的语句。
// 例如:对于 node = <require('san')>,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) {
Expand Down
26 changes: 17 additions & 9 deletions src/ast/ts-ast-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,42 @@ 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())
)
}

/**
* 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
}
}

Expand Down
14 changes: 11 additions & 3 deletions src/compilers/renderer-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand All @@ -45,3 +45,11 @@ export interface RenderOptions {
export interface parseSanSourceFileOptions {
sanReferenceInfo?: RenderOptions['sanReferenceInfo']
}

export interface strongParseSanSourceFileOptions {
sanReferenceInfo: {
methodName: string[];
moduleName: string[];
className: string[];
}
}
40 changes: 36 additions & 4 deletions src/models/san-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)) {
Expand All @@ -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()
}

/**
Expand Down
48 changes: 17 additions & 31 deletions src/parsers/javascript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -69,24 +69,21 @@ export class JavaScriptSanParser {
componentInfos: JSComponentInfo[] = []
entryComponentInfo?: JSComponentInfo

private sanComponentIdentifier?: string
private defineComponentIdentifier: string
private defineTemplateComponentIdentifier: string
private defaultExport?: string
private imports: Map<LocalName, [ImportSpecifier, ImportName]> = new Map()
private exports: Map<LocalName, ExportName> = new Map()
private componentIDs: Map<Node | undefined, string> = 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,
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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'
}
Expand All @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/parsers/san-file-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@ 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

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 {
Expand Down
5 changes: 3 additions & 2 deletions src/parsers/typescript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ 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')

/**
* 把包含 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)
}
Expand Down
Loading

0 comments on commit b9f0f32

Please sign in to comment.