Skip to content

Commit

Permalink
feat(tsc): implement ts only visitors
Browse files Browse the repository at this point in the history
  • Loading branch information
fathyb committed Mar 6, 2018
1 parent 847a59e commit c511bd6
Show file tree
Hide file tree
Showing 28 changed files with 885 additions and 174 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store

.vscode/
build/
node_modules/

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parcel-plugin-typescript",
"version": "0.8.0-next.0",
"version": "0.6.0-next.2",
"description": "Enhanced TypeScript support for Parcel bundler",
"author": "Fathy Boundjadj <fathy.boundjadj@gmail.com>",
"license": "MIT",
Expand Down
13 changes: 13 additions & 0 deletions src/backend/ast-utils/create-literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as ts from 'typescript'

export function createLiteral(value: any) {
if(typeof value === 'number') {
return ts.createNumericLiteral(value.toString(10))
}

if(typeof value === 'string') {
return ts.createLiteral(value)
}

throw new Error('node not supported')
}
70 changes: 70 additions & 0 deletions src/backend/ast-utils/eval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as ts from 'typescript'

export class NodeEvaluationError extends Error {
public start: number
public end: number

constructor(node: ts.Node) {
super('Cannot evaluate node')

this.start = node.pos
this.end = node.end
}
}

export function evaluateNode(node: ts.Node, scope: {[k: string]: any}): any {
if(ts.isStringLiteral(node)) {
return node.text
}

if(ts.isNumericLiteral(node)) {
return parseInt(node.text, undefined)
}

if(ts.isIdentifier(node)) {
return scope[node.text]
}

if(ts.isBinaryExpression(node)) {
const {left, right} = node

switch(node.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
return evaluateNode(left, scope) + evaluateNode(right, scope)
case ts.SyntaxKind.MinusToken:
return evaluateNode(left, scope) - evaluateNode(right, scope)
default:
throw new NodeEvaluationError(node)
}
}

if(ts.isObjectLiteralExpression(node)) {
const obj: any = {}

node.properties.forEach(property => {
let key: any|null = null

if(!ts.isPropertyAssignment(property)) {
throw new NodeEvaluationError(property)
}

const {initializer, name} = property

if(ts.isComputedPropertyName(name)) {
key = evaluateNode(name.expression, scope)
}
else if(ts.isIdentifier(name)) {
key = name.text
}
else {
throw new NodeEvaluationError(name)
}

obj[key] = evaluateNode(initializer, scope)
})

return obj
}

throw new NodeEvaluationError(node)
}
117 changes: 117 additions & 0 deletions src/backend/ast-utils/scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as ts from 'typescript'

import {ImportDependency} from '../transformers/paths'

export interface ImportedVariable {
type: 'import'
from: string
name?: string
}

export interface Variable {
type: 'variable'
}

export type AnyVariable = Variable | ImportedVariable

export class Scope {
public collectModules: boolean
private readonly scope: Map<string, AnyVariable>

constructor(
private readonly dependencies: ImportDependency[],
previous?: Scope
) {
if(previous) {
this.collectModules = previous.collectModules
this.scope = new Map(previous.scope)
}
else {
this.collectModules = true
this.scope = new Map()
}
}

public get(variable: string): AnyVariable | undefined {
return this.scope.get(variable)
}

public has(variable: string) {
return this.scope.has(variable)
}

public hasModule(source: string) {
return this.dependencies.find(dep => dep.source === source)
}

public getRequiredModule(node: ts.Node): string|null {
if(ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
const {expression, arguments: [modulePath]} = node

if(expression.text === 'require' && modulePath && ts.isStringLiteral(modulePath)) {
return modulePath.text
}
}

return null
}

public collect(node: ts.Node) {
const {collectModules, dependencies, scope} = this

if(ts.isVariableDeclaration(node)) {
const {name, initializer} = node
const required = initializer && this.getRequiredModule(initializer)

if(required) {
dependencies.push({
source: required,
position: initializer!.pos
})
}

if(ts.isIdentifier(name)) {
if(required) {
scope.set(name.text, {
type: 'import',
from: required,
name: name.text
})
}
else {
scope.set(name.text, {
type: 'variable'
})
}
}
}
else if(
collectModules && ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) &&
node.importClause && node.importClause.namedBindings
) {
const {namedBindings} = node.importClause
const {text: from} = node.moduleSpecifier
const type = 'import'

if(ts.isNamespaceImport(namedBindings)) {
scope.set(namedBindings.name.text, {type, from})
}
else {
namedBindings.elements.forEach(({propertyName, name}) => {
const prop = propertyName
? propertyName.text
: name.text

dependencies.push({
source: from,
position: node.moduleSpecifier.pos
})
scope.set(name.text, {
type, from,
name: prop
})
})
}
}
}
}
42 changes: 42 additions & 0 deletions src/backend/ast-utils/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as ts from 'typescript'

import {createLiteral} from './create-literal'

export interface Vars {
[key: string]: any
}

export function astTemplate<T extends (ts.Statement | ReadonlyArray<ts.Statement>)>(
text: string
): (vars: Vars) => T {
const source = ts.createSourceFile('template.tsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
const transformers = (vars: Vars) => [(context: ts.TransformationContext) =>
(root: ts.SourceFile) => {
function visit(node: ts.Node): ts.Node {
if(ts.isIdentifier(node) && vars.hasOwnProperty(node.text)) {
node = createLiteral(vars[node.text])
}

// clear the position to prevent scrambled input when .getText is used
node.pos = -1
node.end = -1

return ts.visitEachChild(node, visit, context)
}

return ts.visitNode(root, visit)
}
]

return (vars: Vars): T => {
const {transformed} = ts.transform(source, transformers(vars))
const expression = transformed[0].statements

if(expression.length === 1) {
return expression[0] as T
}
else {
return expression as any as T
}
}
}
33 changes: 33 additions & 0 deletions src/backend/ast-utils/traverse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as ts from 'typescript'

import {ImportDependency} from '../transformers/paths'
import {Scope} from './scope'

/// traverse and build a scope without the type-checker
export function traverse<N extends ts.Node>(
node: N, visitor: (node: ts.Node, scope: Scope) => ts.VisitResult<ts.Node>,
ctx: ts.TransformationContext, dependencies: ImportDependency[],
parentScope?: Scope, collectModules = true
): N {
const scope = new Scope(dependencies, parentScope)

if(!parentScope) {
scope.collect(node)
}

return ts.visitEachChild(node, child => {
scope.collect(child)

if(collectModules && scope.hasModule('fs')) {
scope.collectModules = collectModules = false
}

const replacement = visitor(child, scope)

if(replacement !== child) {
return replacement
}

return traverse(child, visitor, ctx, dependencies, scope, collectModules)
}, ctx)
}
61 changes: 43 additions & 18 deletions src/backend/compiler/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ import * as ts from 'typescript'

import {FileStore} from './store'

export class CompilerHost implements ts.CompilerHost {
public readonly store = FileStore.shared()
private readonly host: ts.CompilerHost
export class Host {
protected readonly host: ts.CompilerHost
private readonly setParentNodes = true

constructor(
options: ts.CompilerOptions,
public readonly store = FileStore.shared()
) {
this.host = ts.createCompilerHost(options, this.setParentNodes)
}

private setParentNodes = true
public fileExists(path: string) {
return this.store.exists(path) || this.host.fileExists(path)
}

constructor(options: ts.CompilerOptions) {
this.host = ts.createCompilerHost(options, this.setParentNodes)
public readFile(fileName: string) {
return this.store.readFile(fileName)
}
}

export class CompilerHost extends Host implements ts.CompilerHost {
public useCaseSensitiveFileNames(): boolean {
return this.host.useCaseSensitiveFileNames()
}

public getSourceFile(
Expand All @@ -34,10 +49,6 @@ export class CompilerHost implements ts.CompilerHost {
return this.host.getDefaultLibFileName(options)
}

public readFile(fileName: string) {
return this.store.readFile(fileName)
}

public writeFile(fileName: string, data: string) {
this.store.writeFile(fileName, data)
}
Expand All @@ -54,19 +65,33 @@ export class CompilerHost implements ts.CompilerHost {
return this.host.getCanonicalFileName(this.resolve(fileName))
}

public useCaseSensitiveFileNames(): boolean {
return this.host.useCaseSensitiveFileNames()
}

public getNewLine(): string {
return this.host.getNewLine()
}

public fileExists(path: string) {
return this.store.exists(path) || this.host.fileExists(path)
}

private resolve(path: string) {
return path
}
}

export class ConfigHost extends Host implements ts.ParseConfigHost {
public useCaseSensitiveFileNames = this.host.useCaseSensitiveFileNames()

constructor() {
super({}, new FileStore())
}

public getDeepFiles(): string[] {
return this.store.getFiles()
}

public readDirectory(
rootDir: string,
extensions: ReadonlyArray<string>,
excludes: ReadonlyArray<string> | undefined,
includes: ReadonlyArray<string>,
depth?: number
) {
return ts.sys.readDirectory(rootDir, extensions, excludes, includes, depth)
}
}
Loading

0 comments on commit c511bd6

Please sign in to comment.