Skip to content

Commit

Permalink
feat: 删除原始组件定义代码中无用的子组件定义
Browse files Browse the repository at this point in the history
  • Loading branch information
meixg committed Dec 3, 2021
1 parent 3b84c0b commit 1fc6d47
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 25 deletions.
78 changes: 70 additions & 8 deletions src/ast/js-ast-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* 方便读取和操作 ESTree 结构,本文件里提供的功能和实现逻辑是和具体 SSR 逻辑无关的,只和 ESTree 有关。
*/

import { simple } from 'acorn-walk'
import { ancestor, simple } from 'acorn-walk'
import assert, { equal } from 'assert'
import { Node as AcornNode } from 'acorn'
import { MethodDefinition, ExportDefaultDeclaration, ImportDeclaration, Property, BinaryExpression, ClassExpression, ClassDeclaration, ThisExpression, ExpressionStatement, TemplateLiteral, Literal, Identifier, MemberExpression, ArrayExpression, CallExpression, ObjectExpression, Node, Program, Pattern, VariableDeclaration, ObjectPattern, Class, AssignmentExpression, Expression, ImportSpecifier, ImportDefaultSpecifier, VariableDeclarator } from 'estree'
Expand Down Expand Up @@ -61,43 +61,44 @@ export function isExportsMemberExpression (expr: Pattern) {
* 通过迭代器返回每一个 require 的 localName、moduleName、exportName。
* 例如:let foo = require('bar').coo,会 yield ["foo", "bar", "coo"]
*/
export function * findScriptRequires (node: Node): Generator<[string, string, string]> {
export function * findScriptRequires (node: Node): Generator<[string, string, string, VariableDeclaration]> {
for (const decl of filterByType(node, 'VariableDeclaration')) {
const { id, init } = decl.declarations[0]
if (!init) continue
if (isRequire(init)) {
const specifier = getRequireSpecifier(init)
if (isIdentifier(id)) yield [id.name, specifier, 'default']
if (isIdentifier(id)) yield [id.name, specifier, 'default', decl]
if (isObjectPattern(id)) {
for (const [key, value] of getPropertiesFromObject(id)) {
assertIdentifier(value)
yield [value.name, specifier, key]
yield [value.name, specifier, key, decl]
}
}
}
// const C = require('san').Component
if (isMemberExpression(init) && isRequire(init.object)) {
const specifier = getRequireSpecifier(init.object)
assertIdentifier(id)
yield [id.name, specifier, getStringValue(init.property)]
yield [id.name, specifier, getStringValue(init.property), decl]
}
}
}

/**
* 找到 root 下所有的 import 语句
*
* 通过迭代器返回每一个 require 的 localName、moduleName、exportName。
* 例如:import { coo as foo } from 'bar',会 yield ["foo", "bar", "coo"]
*/
export function * findESMImports (root: Node): Generator<[string, string, string]> {
export function * findESMImports (root: Node): Generator<[string, string, string, ImportDeclaration]> {
for (const node of filterByType(root, 'ImportDeclaration')) {
const relativeFile = node.source.value as string
for (const spec of node['specifiers']) {
if (isImportDefaultSpecifier(spec)) {
yield [spec.local.name, relativeFile, 'default']
yield [spec.local.name, relativeFile, 'default', node]
}
if (isImportSpecifier(spec)) {
yield [spec.local.name, relativeFile, spec.imported.name]
yield [spec.local.name, relativeFile, spec.imported.name, node]
}
}
}
Expand Down Expand Up @@ -286,6 +287,10 @@ export function findDefaultExport (node: Program): undefined | Node {
return result
}

export function isProgram (node: Node): node is Program {
return node.type === 'Program'
}

export function isRequire (node: Node): node is CallExpression {
return isCallExpression(node) && node.callee['name'] === 'require'
}
Expand Down Expand Up @@ -393,3 +398,60 @@ export function assertObjectExpression (expr: Node): asserts expr is ObjectExpre
export function assertVariableDeclarator (expr: Node): asserts expr is VariableDeclarator {
assert(isVariableDeclarator(expr))
}

export function deleteMembersFromClassDeclaration (expr: Class, name: string) {
for (const [, decl] of expr.body.body.entries()) {
if (decl['kind'] === 'constructor') {
const constructorDecl = decl
for (const [index, expr] of constructorDecl.value.body.body.entries()) {
if (isExpressionStatement(expr) &&
isAssignmentExpression(expr.expression) &&
isMemberAssignment(expr.expression.left) &&
getStringValue(expr.expression.left['property']) === name
) {
constructorDecl.value.body.body.splice(index, 1)
}
}
continue
}
}
}
export function deletePropertiesFromObject (obj: ObjectExpression | ObjectPattern, name: string) {
for (const [index, prop] of obj.properties.entries()) {
assertProperty(prop)
if (getStringValue(prop.key) === name) {
obj.properties.splice(index, 1)
}
}
}
export function deleteMemberAssignmentsTo (program: Program, objName: string, name: string) {
ancestor(program as any as AcornNode, {
AssignmentExpression (node: AcornNode, ancestors: AcornNode[]) {
const expr = node as any as AssignmentExpression
if (
isMemberExpression(expr.left) &&
isIdentifier(expr.left.object) &&
expr.left.object.name === objName &&
isIdentifier(expr.left.property) &&
expr.left.property.name === name
) {
const res = findValidParent(ancestors as Node[])
if (res && res.parent) {
res.parent.body.splice(res.index, 1)
}
}
}
})

function findValidParent (ancestors: Node[]) {
for (let i = ancestors.length - 1; i > -1; i--) {
const node = ancestors[i]
if (isProgram(node)) {
return {
parent: node,
index: node.body.indexOf(ancestors[i + 1] as ExpressionStatement)
}
}
}
}
}
70 changes: 67 additions & 3 deletions src/parsers/javascript-san-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,34 @@ import { Node as AcornNode, parse } from 'acorn'
import { CallExpression, Program, Node, Class } from 'estree'
import { generate } from 'astring'
import { JSComponentInfo } from '../models/component-info'
import { isVariableDeclarator, isProperty, isAssignmentExpression, isExportDefaultDeclaration, location, isMemberExpression, isObjectExpression, isCallExpression, isIdentifier, isLiteral, getMemberAssignmentsTo, getPropertyFromObject, getPropertiesFromObject, getMembersFromClassDeclaration, isClass, getClassName, getStringValue, isExportsMemberExpression, isRequireSpecifier, findExportNames, isModuleExports, findESMImports, findScriptRequires } from '../ast/js-ast-util'
import {
isVariableDeclarator,
isProperty,
isAssignmentExpression,
isExportDefaultDeclaration,
location,
isMemberExpression,
isObjectExpression,
isCallExpression,
isIdentifier,
isLiteral,
getMemberAssignmentsTo,
getPropertyFromObject,
getPropertiesFromObject,
getMembersFromClassDeclaration,
isClass,
getClassName,
getStringValue,
isExportsMemberExpression,
isRequireSpecifier,
findExportNames,
isModuleExports,
findESMImports,
findScriptRequires,
deleteMembersFromClassDeclaration,
deletePropertiesFromObject,
deleteMemberAssignmentsTo
} from '../ast/js-ast-util'
import { JSSanSourceFile } from '../models/san-source-file'
import { componentID, ComponentReference } from '../models/component-reference'
import { readFileSync } from 'fs'
Expand Down Expand Up @@ -59,6 +86,7 @@ export class JavaScriptSanParser {
this.parseNames()
this.parseComponents()
this.wireChildComponents()
this.deleteChildComponentRequires()
return new JSSanSourceFile(this.filePath, this.stringify(this.root), this.componentInfos, this.entryComponentInfo)
}

Expand Down Expand Up @@ -87,6 +115,27 @@ export class JavaScriptSanParser {
}
}

private deleteChildComponentRequires () {
const childComponentsSpecifier = new Set()
for (const component of this.componentInfos) {
for (const [, childComponent] of component.childComponents) {
childComponentsSpecifier.add(childComponent.specifier)
}
}

const a = [...findESMImports(this.root), ...findScriptRequires(this.root)]
for (const [, moduleName, , node] of a) {
if (!childComponentsSpecifier.has(moduleName)) {
continue
}

const index = this.root.body.indexOf(node)
if (index !== -1) {
this.root.body.splice(index, 1)
}
}
}

private createChildComponentReference (child: Node, selfId: string): ComponentReference {
if (isObjectExpression(child)) {
this.createComponent(child)
Expand Down Expand Up @@ -168,8 +217,8 @@ export class JavaScriptSanParser {
}

* parseImportedNames (): Generator<[string, string, string]> {
for (const entry of findESMImports(this.root)) yield entry
for (const entry of findScriptRequires(this.root)) yield entry
for (const [localName, moduleName, exportName] of findESMImports(this.root)) yield [localName, moduleName, exportName]
for (const [localName, moduleName, exportName] of findScriptRequires(this.root)) yield [localName, moduleName, exportName]
}

createComponent (node: Node, name: string = getClassName(node), isDefault = false) {
Expand All @@ -181,6 +230,9 @@ export class JavaScriptSanParser {
this.componentIDs.set(node, id)
const comp = new JSComponentInfo(id, name, properties, this.stringify(node), isObjectExpression(node))
this.componentInfos.push(comp)

// 删除掉子组件
this.deletePropertiesFromComponentDecalration(node, name, 'components')
return comp
}

Expand All @@ -192,6 +244,18 @@ export class JavaScriptSanParser {
return this.defaultPlaceholderComponent
}

private deletePropertiesFromComponentDecalration (node: Node, targetName: string, name: string) {
if (this.isComponentClass(node)) {
deleteMembersFromClassDeclaration(node, name)
} else if (isObjectExpression(node)) {
deletePropertiesFromObject(node, name)
} else {
deletePropertiesFromObject(node['arguments'][0], name)
}

deleteMemberAssignmentsTo(this.root, targetName, name)
}

private * getPropertiesFromComponentDeclaration (node: Node, name: string) {
if (this.isComponentClass(node)) yield * getMembersFromClassDeclaration(node as Class)
else if (isObjectExpression(node)) yield * getPropertiesFromObject(node)
Expand Down
34 changes: 20 additions & 14 deletions test/unit/utils/js-ast-util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,49 @@ describe('js-ast-util', () => {
import { Component } from 'san'
import { Component as c } from 'san'
`
const imports = [...findESMImports(pm(script))]
const tree = pm(script)
const imports = [...findESMImports(tree)]
expect(imports).toHaveLength(2)
expect(imports[0]).toEqual(['Component', 'san', 'Component'])
expect(imports[1]).toEqual(['c', 'san', 'Component'])
expect(imports[0]).toEqual(['Component', 'san', 'Component', tree.body[0]])
expect(imports[1]).toEqual(['c', 'san', 'Component', tree.body[1]])
})
it('should parse default import in ES module', () => {
const script = 'import XComponent from "./x-component"'
const imports = [...findESMImports(pm(script))]
const tree = pm(script)
const imports = [...findESMImports(tree)]
expect(imports).toHaveLength(1)
expect(imports[0]).toEqual(['XComponent', './x-component', 'default'])
expect(imports[0]).toEqual(['XComponent', './x-component', 'default', tree.body[0]])
})
})
describe('.findScriptRequires()', () => {
it('should parse const {Component, defineComponent} = require', () => {
const script = 'const {Component, defineComponent} = require("san")'
const imports = [...findScriptRequires(p(script))]
const tree = p(script)
const imports = [...findScriptRequires(tree)]
expect(imports).toHaveLength(2)
expect(imports[0]).toEqual(['Component', 'san', 'Component'])
expect(imports[1]).toEqual(['defineComponent', 'san', 'defineComponent'])
expect(imports[0]).toEqual(['Component', 'san', 'Component', tree.body[0]])
expect(imports[1]).toEqual(['defineComponent', 'san', 'defineComponent', tree.body[0]])
})
it('should parse const san = require("san")', () => {
const script = 'const san = require("san")'
const imports = [...findScriptRequires(p(script))]
const tree = p(script)
const imports = [...findScriptRequires(tree)]
expect(imports).toHaveLength(1)
expect(imports[0]).toEqual(['san', 'san', 'default'])
expect(imports[0]).toEqual(['san', 'san', 'default', tree.body[0]])
})
it('should parse const {defineComponent: def} = require("san")', () => {
const script = 'const {defineComponent: def} = require("san")'
const imports = [...findScriptRequires(p(script))]
const tree = p(script)
const imports = [...findScriptRequires(tree)]
expect(imports).toHaveLength(1)
expect(imports[0]).toEqual(['def', 'san', 'defineComponent'])
expect(imports[0]).toEqual(['def', 'san', 'defineComponent', tree.body[0]])
})
it('should parse const define = require("san").defineComponent', () => {
const script = 'const define = require("san").defineComponent'
const imports = [...findScriptRequires(p(script))]
const tree = p(script)
const imports = [...findScriptRequires(tree)]
expect(imports).toHaveLength(1)
expect(imports[0]).toEqual(['define', 'san', 'defineComponent'])
expect(imports[0]).toEqual(['define', 'san', 'defineComponent', tree.body[0]])
})
})
describe('.findExportNames()', () => {
Expand Down

0 comments on commit 1fc6d47

Please sign in to comment.