Skip to content

Commit

Permalink
feat: SanFileParser
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Nov 5, 2020
1 parent 549531e commit 50c7387
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 3 deletions.
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@semantic-release/npm": "^7.0.5",
"@semantic-release/release-notes-generator": "^9.0.1",
"@types/acorn": "^4.0.5",
"@types/astring": "^1.3.0",
"@types/debug": "^4.1.5",
"@types/estree": "0.0.45",
"@types/jest": "^26.0.8",
Expand Down Expand Up @@ -88,6 +89,7 @@
"dependencies": {
"acorn": "^8.0.1",
"acorn-walk": "^8.0.0",
"astring": "^1.4.3",
"camelcase": "^6.0.0",
"chalk": "^4.1.0",
"debug": "^4.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/models/san-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class SanProject {
public parseSanSourceFile (input: CompileInput): SanSourceFile {
if (isComponentClass(input)) return new ComponentClassParser(input, '').parse()
if (isSanFileDescriptor(input)) {
return new SanFileParser(input.scriptContent, input.templateContent).parse()
return new SanFileParser(input.scriptContent, input.templateContent, input.filePath).parse()
}
const filePath = isFileDescriptor(input) ? input.filePath : input
const fileContent = isFileDescriptor(input) ? input.fileContent : undefined
Expand Down
107 changes: 107 additions & 0 deletions src/parsers/san-file-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ExpressionStatement, MethodDefinition, ObjectExpression, CallExpression, Node } from 'estree'
import { JavaScriptSanParser } from './javascript-san-parser'
import assert from 'assert'
import { isClass, getConstructor, addStringPropertyForObject, assertObjectExpression, isCallExpression, isObjectExpression, findDefaultExport } from '../utils/js-ast-util'
import { generate } from 'astring'

export class SanFileParser {
fileContent: string

private readonly parser: JavaScriptSanParser

constructor (
public readonly scriptContent: string,
public readonly templateContent: string,
private readonly filePath: string
) {
this.parser = new JavaScriptSanParser(filePath, scriptContent, 'module')
this.fileContent = 'not parsed yet'
}

parse () {
const expr = findDefaultExport(this.parser.root)
assert(expr, 'default export not found')

// export default { inited() {} }
if (isObjectExpression(expr)) this.expandToSanComponent(expr)

// defineComponent({}) -> defineComponent({ template: `${templateContent}` })
this.insertTemplate(expr)

this.fileContent = generate(this.parser.root)
return this.parser.parse()
}

expandToSanComponent (options: ObjectExpression) {
const opts = { ...options }
const defineComponent: CallExpression = {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'require' },
arguments: [{ type: 'Literal', value: 'san', raw: "'san'" }],
optional: false
},
property: { type: 'Identifier', name: 'defineComponent' },
computed: false,
optional: false
},
arguments: [opts],
optional: false
}
Object.assign(options, defineComponent)
}

private insertTemplate (expr: Node) {
if (isCallExpression(expr)) {
assert(expr.arguments[0], 'cannot parse san script')
assertObjectExpression(expr.arguments[0])
addStringPropertyForObject(expr.arguments[0], 'template', this.templateContent)
} else if (isClass(expr)) {
const fn = getConstructor(expr) || this.createEmptyConstructor()
fn.value.body.body.push(this.createTemplateAssignmentExpression())
}
}

private createEmptyConstructor (): MethodDefinition {
return {
type: 'MethodDefinition',
kind: 'constructor',
static: false,
computed: false,
key: { type: 'Identifier', name: 'constructor' },
value: {
type: 'FunctionExpression',
id: null,
generator: false,
async: false,
params: [],
body: { type: 'BlockStatement', body: [] }
}
}
}

private createTemplateAssignmentExpression (): ExpressionStatement {
return {
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
object: { type: 'ThisExpression' },
property: { type: 'Identifier', name: 'template' },
computed: false,
optional: false
},
right: {
type: 'Literal',
value: this.templateContent,
raw: JSON.stringify(this.templateContent)
}
}
}
}
}
39 changes: 38 additions & 1 deletion src/utils/js-ast-util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { simple } from 'acorn-walk'
import assert, { equal } from 'assert'
import { Node as AcornNode } from 'acorn'
import { 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'
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'

const OPERATORS = {
'+': (l: any, r: any) => l + r
Expand Down Expand Up @@ -143,6 +143,24 @@ export function * getMembersFromClassDeclaration (expr: Class): Generator<[strin
}
}

export function getConstructor (expr: Class): undefined | MethodDefinition {
for (const method of expr.body.body) {
if (method.kind === 'constructor') return method
}
}

export function addStringPropertyForObject (expr: ObjectExpression, key: string, value: string) {
expr.properties.push({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: key },
value: { type: 'Literal', value: value, raw: JSON.stringify(value) },
kind: 'init'
})
}

export function getPropertyFromObject (obj: ObjectExpression | ObjectPattern, propertyName: string): Node | undefined {
for (const [key, val] of getPropertiesFromObject(obj)) {
if (key === propertyName) return val
Expand Down Expand Up @@ -208,6 +226,20 @@ export function location (node: Node) {
return `[${node['start']},${node['end']})`
}

export function findDefaultExport (node: Program): undefined | Node {
let result
simple(node as any as AcornNode, {
ExportDefaultDeclaration (node) {
result = (node as any as ExportDefaultDeclaration).declaration
},
AssignmentExpression (node) {
const expr = node as any as AssignmentExpression
if (isModuleExports(expr.left)) result = expr.right
}
})
return result
}

export function isRequire (node: Node): node is CallExpression {
return isCallExpression(node) && node.callee['name'] === 'require'
}
Expand Down Expand Up @@ -283,6 +315,11 @@ export function isClassExpression (expr: Node): expr is ClassExpression {
export function isProperty (expr: Node): expr is Property {
return expr.type === 'Property'
}

export function isExportDefaultDeclaration (node: Node): node is ExportDefaultDeclaration {
return node.type === 'ExportDefaultDeclaration'
}

export function isArrayExpression (expr: Node): expr is ArrayExpression {
return expr.type === 'ArrayExpression'
}
Expand Down
18 changes: 17 additions & 1 deletion test/unit/models/san-project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('SanProject', function () {
expect(code).toContain('module.exports =')
})

it('should support TypeScriptFileDescriptor', function () {
it('should support TypeScript FileDescriptor', function () {
const proj = new SanProject()
const code = proj.compile({
filePath: resolve(stubRoot, './a.comp.ts'),
Expand Down Expand Up @@ -77,6 +77,22 @@ describe('SanProject', function () {
const componentClass = require(resolve(stubRoot, './a.comp.js'))
expect(() => proj.compile(componentClass, 'js')).not.toThrow()
})

it('should support SanFileDescriptor', function () {
const proj = new SanProject()
const code = proj.compile({
filePath: resolve(stubRoot, './a.san'),
templateContent: '<div>{{name}}</div>',
scriptContent: `
import { Component } from 'san'
export default { inited() { this.data.set('name', 'san') } }
`
})

expect(code).toContain('html += "<div>"')
expect(code).toContain('html += _.output(ctx.data.name, true)')
expect(code).toContain('html += "</div>"')
})
})

describe('#compileToRenderer()', function () {
Expand Down
43 changes: 43 additions & 0 deletions test/unit/parsers/san-file-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SanFileParser } from '../../../src/parsers/san-file-parser'

describe('SanFileParser', () => {
describe('#wireChildComponents()', () => {
it('should parse a single defineComponent', () => {
const script = `
import { defineComponent } from 'san'
export default defineComponent({
inited() {}
})`
const template = '<div>Foo</div>'
const parser = new SanFileParser(script, template, '/tmp/foo.san')
const sourceFile = parser.parse()

expect(sourceFile.getFilePath()).toEqual('/tmp/foo.san')
expect(sourceFile.getFileContent()).toEqual(script)
expect(sourceFile.componentInfos).toHaveLength(1)
expect(sourceFile.componentInfos[0]).toEqual(sourceFile.entryComponentInfo)
expect(sourceFile.componentInfos[0].hasMethod('inited')).toBeTruthy()
expect(sourceFile.componentInfos[0].root.children[0]).toMatchObject({
textExpr: { type: 1, value: 'Foo' }
})
})
it('should parse a shorthand component', () => {
const script = `
export default {
inited() {}
}`
const template = '<div>Foo</div>'
const parser = new SanFileParser(script, template, '/tmp/foo.san')
const sourceFile = parser.parse()

expect(sourceFile.getFilePath()).toEqual('/tmp/foo.san')
expect(sourceFile.getFileContent()).toEqual(script)
expect(sourceFile.componentInfos).toHaveLength(1)
expect(sourceFile.componentInfos[0]).toEqual(sourceFile.entryComponentInfo)
expect(sourceFile.componentInfos[0].hasMethod('inited')).toBeTruthy()
expect(sourceFile.componentInfos[0].root.children[0]).toMatchObject({
textExpr: { type: 1, value: 'Foo' }
})
})
})
})

0 comments on commit 50c7387

Please sign in to comment.