• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

baidu / san-ssr / 3824729513

pending completion
3824729513

Pull #163

github

GitHub
Merge e05e440a2 into 5feaebca6
Pull Request #163: chore(deps): bump json5 and tsconfig-paths

1105 of 1107 branches covered (99.82%)

Branch coverage included in aggregate %.

1853 of 1854 relevant lines covered (99.95%)

4190.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.59
/src/parsers/javascript-san-parser.ts
1
/**
2
 * JavaScript 文件解析器
3
 *
4
 * 从 JavaScript 文件源码,得到它里面的 San 信息,产出 JSSanSourceFile。
5
 * JSSanSourceFile 包含了若干个 JSComponentInfo 和一个 entryComponentInfo。
6
 */
7
import debugFactory from 'debug'
8
import { ancestor } from 'acorn-walk'
9
import { Node as AcornNode, parse } from 'acorn'
10
import { CallExpression, Program, Node, Class, ObjectExpression } from 'estree'
11
import { generate } from 'astring'
12
import { ComponentType, JSComponentInfo } from '../models/component-info'
13
import {
14
    isVariableDeclarator,
15
    isProperty,
16
    isAssignmentExpression,
17
    isExportDefaultDeclaration,
18
    location,
19
    isMemberExpression,
20
    isObjectExpression,
21
    isCallExpression,
22
    isIdentifier,
23
    isLiteral,
24
    getMemberAssignmentsTo,
25
    getPropertyFromObject,
26
    getPropertiesFromObject,
27
    getMembersFromClassDeclaration,
28
    isClass,
29
    getClassName,
30
    getStringValue,
31
    isExportsMemberExpression,
32
    isRequireSpecifier,
33
    findExportNames,
34
    isModuleExports,
35
    findESMImports,
36
    findScriptRequires,
37
    deleteMembersFromClassDeclaration,
38
    deletePropertiesFromObject,
39
    deleteMemberAssignmentsTo
40
} from '../ast/js-ast-util'
41
import { JSSanSourceFile } from '../models/san-source-file'
42
import { componentID, ComponentReference } from '../models/component-reference'
43
import { readFileSync } from 'fs'
44
import { parseSanSourceFileOptions } from '../compilers/renderer-options'
45

46
const debug = debugFactory('ts-component-parser')
8✔
47
const DEFAULT_LOADER_CMP = 'SanSSRDefaultLoaderComponent'
8✔
48

49
type LocalName = string
50
type ImportName = string
51
type ExportName = string
52
type ImportSpecifier = string
53

54
/**
55
 * 组件定义可能的 node 类型
56
 */
57
export type ComponentDefinition = CallExpression | Class | ObjectExpression
58

59
/**
60
 * 子组件(components 属性中)可能的 node 类型
61
 */
62
export type ChildComponentDefinition = ObjectExpression
63

64
/**
65
 * 把包含 San 组件定义的 JavaScript 源码,通过静态分析(AST),得到组件信息。
66
 */
67
export class JavaScriptSanParser {
68
    root: Program
69
    componentInfos: JSComponentInfo[] = []
172✔
70
    entryComponentInfo?: JSComponentInfo
71

72
    private sanComponentIdentifier?: string
73
    private defineComponentIdentifier: string
74
    private defineTemplateComponentIdentifier: string
75
    private defaultExport?: string
76
    private imports: Map<LocalName, [ImportSpecifier, ImportName]> = new Map()
172✔
77
    private exports: Map<LocalName, ExportName> = new Map()
172✔
78
    private componentIDs: Map<Node | undefined, string> = new Map()
172✔
79
    private defaultPlaceholderComponent?: JSComponentInfo
80
    private id = 0
172✔
81
    private sanReferenceInfo?: parseSanSourceFileOptions['sanReferenceInfo']
82

83
    constructor (
84
        private readonly filePath: string,
85
        fileContent?: string,
86
        sourceType: 'module' | 'script' = 'script',
1✔
87
        options?: parseSanSourceFileOptions
88
    ) {
89
        this.defineComponentIdentifier = 'defineComponent'
172✔
90
        this.defineTemplateComponentIdentifier = 'defineTemplateComponent'
172✔
91
        this.root = parse(
172✔
92
            fileContent === undefined ? readFileSync(filePath, 'utf8') : fileContent,
172✔
93
            { ecmaVersion: 2020, sourceType }
94
        ) as any as Program
95
        this.sanReferenceInfo = options?.sanReferenceInfo
172✔
96
    }
97

98
    parse (): JSSanSourceFile {
99
        this.parseNames()
154✔
100
        this.parseComponents()
154✔
101
        this.wireChildComponents()
154✔
102
        this.deleteChildComponentRequires()
154✔
103
        return new JSSanSourceFile(
154✔
104
            this.filePath,
105
            this.stringify(this.root),
106
            this.componentInfos,
107
            this.entryComponentInfo
108
        )
109
    }
110

111
    parseComponents (): [JSComponentInfo[], JSComponentInfo | undefined] {
112
        const visitor = (node: AcornNode, ancestors: AcornNode[]) => {
170✔
113
            const parent = ancestors[ancestors.length - 2] as Node
506✔
114
            const n = node as Node
506✔
115
            if (this.isComponent(n)) {
506✔
116
                const component = this.parseComponentFromNode(n, parent)
265✔
117
                if (component.className === this.defaultExport) {
265✔
118
                    this.entryComponentInfo = component
146✔
119
                }
120
            }
121
        }
122
        ancestor(this.root as any as AcornNode, {
170✔
123
            CallExpression: visitor,
124
            ClassExpression: visitor,
125
            ClassDeclaration: visitor
126
        })
127
        return [this.componentInfos, this.entryComponentInfo]
170✔
128
    }
129

130
    wireChildComponents () {
131
        for (const info of this.componentInfos) {
158✔
132
            for (const [key, value] of info.getComponentsDelcarations()) {
249✔
133
                info.childComponents.set(key, this.createChildComponentReference(value, info.id))
92✔
134
            }
135
        }
136
    }
137

138
    private deleteChildComponentRequires () {
139
        const childComponentsSpecifier = new Set()
154✔
140
        for (const component of this.componentInfos) {
154✔
141
            for (const [, childComponent] of component.childComponents) {
243✔
142
                childComponentsSpecifier.add(childComponent.specifier)
86✔
143
            }
144
        }
145

146
        const a = [...findESMImports(this.root), ...findScriptRequires(this.root)]
154✔
147
        for (const [, moduleName, , node] of a) {
154✔
148
            if (!childComponentsSpecifier.has(moduleName)) {
157✔
149
                continue
153✔
150
            }
151

152
            const index = this.root.body.indexOf(node)
4✔
153
            if (index !== -1) {
4✔
154
                this.root.body.splice(index, 1)
3✔
155
            }
156
        }
157
    }
158

159
    private createChildComponentReference (child: Node, selfId: string): ComponentReference {
160
        if (isObjectExpression(child)) {
99✔
161
            this.createComponent(child)
1✔
162
        }
163
        if (this.componentIDs.has(child)) {
99✔
164
            return new ComponentReference('.', this.componentIDs.get(child)!)
2✔
165
        }
166
        // 'self' 指定组件为自身
167
        // 用法见 https://baidu.github.io/san/tutorial/component/#components
168
        if (isLiteral(child) && child.value === 'self') {
97✔
169
            return new ComponentReference('.', selfId)
1✔
170
        }
171
        if (isIdentifier(child)) {
96✔
172
            if (this.imports.has(child.name)) {
85✔
173
                const [specifier, id] = this.imports.get(child.name)!
5✔
174
                return new ComponentReference(specifier, id)
5✔
175
            }
176
            return new ComponentReference('.', child.name)
80✔
177
        }
178
        if (this.isCreateComponentLoaderCall(child)) {
11✔
179
            const options = child.arguments[0]
10✔
180
            const placeholder = isObjectExpression(options) && getPropertyFromObject(options, 'placeholder')
10✔
181

182
            // placeholder 是一个组件声明或组件的引用
183
            if (placeholder) return this.createChildComponentReference(placeholder, selfId)
10✔
184

185
            // placeholder 未定义,生成一个默认的组件
186
            const cmpt = this.getOrCreateDefaultLoaderComponent()
3✔
187
            return new ComponentReference('.', cmpt.id)
3✔
188
        }
189
        throw new Error(`${location(child)} cannot parse components`)
1✔
190
    }
191

192
    private parseComponentFromNode (node: ComponentDefinition, parent: Node) {
193
        // export default Component
194
        if (isExportDefaultDeclaration(parent)) {
265✔
195
            return (this.entryComponentInfo = this.createComponent(node, undefined, true))
12✔
196
        }
197
        // module.exports = Component
198
        if (isAssignmentExpression(parent) && isModuleExports(parent.left)) {
253✔
199
            return (this.entryComponentInfo = this.createComponent(node, undefined, true))
7✔
200
        }
201
        // exports.Foo = Component
202
        if (isAssignmentExpression(parent) && isExportsMemberExpression(parent.left)) {
246✔
203
            return this.createComponent(node, getStringValue(parent.left['property']))
2✔
204
        }
205
        // const Foo = Component
206
        if (isVariableDeclarator(parent)) {
244✔
207
            return this.createComponent(node, parent.id['name'])
221✔
208
        }
209
        // Foo = Component
210
        if (isAssignmentExpression(parent) && isIdentifier(parent.left)) {
23✔
211
            return this.createComponent(node, parent.left.name)
3✔
212
        }
213
        // { 'x-list': san.defineComponent() }
214
        if (isProperty(parent) && this.isComponent(parent.value)) {
20✔
215
            return this.createComponent(node)
1✔
216
        }
217
        return this.createComponent(node)
19✔
218
    }
219

220
    /**
221
     * 解析文件中出现的名字:找到重要的类名、方法名以及它们的来源
222
     */
223
    parseNames () {
224
        for (const [local, specifier, imported] of this.parseImportedNames()) {
170✔
225
            this.imports.set(local, [specifier, imported])
179✔
226
            if (imported === 'Component' && specifier === 'san') {
179✔
227
                this.sanComponentIdentifier = local
17✔
228
            }
229
            if (imported === 'defineComponent' && specifier === 'san') {
179✔
230
                this.defineComponentIdentifier = local
12✔
231
            }
232
            if (imported === 'defineTemplateComponent' && specifier === 'san') {
179✔
233
                this.defineTemplateComponentIdentifier = local
2✔
234
            }
235
        }
236
        if (this.sanReferenceInfo) {
170✔
237
            this.sanComponentIdentifier = this.sanReferenceInfo.moduleName
2✔
238
            this.defineComponentIdentifier = this.sanReferenceInfo.methodName || this.defineComponentIdentifier
2!
239
        }
240

241
        for (const [local, exported] of findExportNames(this.root)) {
170✔
242
            if (exported === 'default') this.defaultExport = local
151✔
243
            this.exports.set(local, exported)
151✔
244
        }
245
    }
246

247
    * parseImportedNames (): Generator<[string, string, string]> {
248
        for (const [localName, moduleName, exportName] of findESMImports(this.root)) {
171✔
249
            yield [localName, moduleName, exportName]
21✔
250
        }
251
        for (const [localName, moduleName, exportName] of findScriptRequires(this.root)) {
171✔
252
            yield [localName, moduleName, exportName]
160✔
253
        }
254
    }
255

256
    createComponent (node: ComponentDefinition, name: string = getClassName(node), isDefault = false) {
287✔
257
        const properties = new Map(this.getPropertiesFromComponentDeclaration(node, name))
266✔
258
        const id = componentID(isDefault, (name
266✔
259
            ? (this.exports.get(name) || name)
348✔
260
            : ('SanSSRAnonymousComponent' + this.id++)
261
        ))
262
        this.componentIDs.set(node, id)
266✔
263
        const comp = new JSComponentInfo(
266✔
264
            id,
265
            name,
266
            properties,
267
            this.stringify(node),
268
            this.getComponentType(node),
269
            isObjectExpression(node)
270
        )
271
        this.componentInfos.push(comp)
266✔
272

273
        // 删除掉子组件
274
        this.deletePropertiesFromComponentDecalration(node, name, 'components')
266✔
275
        return comp
266✔
276
    }
277

278
    private getOrCreateDefaultLoaderComponent (): JSComponentInfo {
279
        if (!this.defaultPlaceholderComponent) {
3✔
280
            this.defaultPlaceholderComponent = new JSComponentInfo(
2✔
281
                DEFAULT_LOADER_CMP,
282
                '',
283
                new Map(),
284
                'function(){}',
285
                'template'
286
            )
287
            this.componentInfos.push(this.defaultPlaceholderComponent)
2✔
288
        }
289
        return this.defaultPlaceholderComponent
3✔
290
    }
291

292
    private deletePropertiesFromComponentDecalration (node: Node, targetName: string, name: string) {
293
        if (this.isComponentClass(node)) {
266✔
294
            deleteMembersFromClassDeclaration(node, name)
25✔
295
        } else if (isObjectExpression(node)) {
241✔
296
            deletePropertiesFromObject(node, name)
1✔
297
        } else {
298
            deletePropertiesFromObject(node['arguments'][0], name)
240✔
299
        }
300

301
        deleteMemberAssignmentsTo(this.root, targetName, name)
266✔
302
    }
303

304
    private * getPropertiesFromComponentDeclaration (node: Node, name: string) {
305
        if (this.isComponentClass(node)) yield * getMembersFromClassDeclaration(node as Class)
266✔
306
        else if (isObjectExpression(node)) yield * getPropertiesFromObject(node)
241✔
307
        else yield * getPropertiesFromObject(node['arguments'][0])
240✔
308
        yield * getMemberAssignmentsTo(this.root, name)
266✔
309
    }
310

311
    private isComponent (node: Node): node is ComponentDefinition {
312
        return this.isDefineComponentCall(node) || this.isComponentClass(node)
507✔
313
    }
314

315
    private getComponentType (node: ComponentDefinition): ComponentType {
316
        if (
266✔
317
            isCallExpression(node) &&
506✔
318
            this.isImportedFromSanWithName(node.callee, this.defineTemplateComponentIdentifier)
319
        ) {
320
            return 'template'
11✔
321
        }
322

323
        return 'normal'
255✔
324
    }
325

326
    private isDefineComponentCall (node: Node): node is CallExpression {
327
        return isCallExpression(node) &&
507✔
328
            (this.isImportedFromSanWithName(node.callee, this.defineComponentIdentifier) ||
329
                this.isImportedFromSanWithName(node.callee, this.defineTemplateComponentIdentifier))
330
    }
331

332
    private isCreateComponentLoaderCall (node: Node): node is CallExpression {
333
        return isCallExpression(node) && this.isImportedFromSanWithName(node.callee, 'createComponentLoader')
11✔
334
    }
335

336
    private isComponentClass (node: Node): node is Class {
337
        return isClass(node) && !!node.superClass && this.isImportedFromSanWithName(node.superClass, 'Component')
798✔
338
    }
339

340
    private isImportedFromSanWithName (expr: Node, sanExport: string): boolean {
341
        if (isIdentifier(expr)) {
1,743✔
342
            return this.isImportedFrom(expr.name, this.sanReferenceInfo?.moduleName || 'san', sanExport)
960✔
343
        }
344
        if (isMemberExpression(expr)) {
783✔
345
            return this.isImportedFromSanWithName(expr.object, 'default') && getStringValue(expr.property) === sanExport
683✔
346
        }
347
        if (isCallExpression(expr)) {
100✔
348
            return isRequireSpecifier(expr, this.sanReferenceInfo?.moduleName || 'san') && sanExport === 'default'
12✔
349
        }
350
        return false
88✔
351
    }
352

353
    private isImportedFrom (localName: string, packageSpec: string, importedName: string) {
354
        if (!this.imports.has(localName)) return false
960✔
355

356
        const [spec, name] = this.imports.get(localName)!
593✔
357
        return spec === packageSpec && name === importedName
593✔
358
    }
359

360
    private stringify (node: Node) {
361
        return generate(node, { indent: '    ' })
420✔
362
    }
363
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc