Skip to content

Commit

Permalink
compilerEs: compiler for generating new ES JavaScript
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Giannini authored and Matthew Giannini committed Aug 14, 2023
1 parent 036e315 commit a2051d8
Show file tree
Hide file tree
Showing 14 changed files with 3,225 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/compilerEs/build.fan
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#! /usr/bin/env fan
//
// Copyright (c) 2023, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 31 Mar 23 Matthew Giannini Creation
//

using build

**
** Build: compilerJsx
**
class Build : BuildPod
{
new make()
{
podName = "compilerEs"
summary = "Fantom to ECMAScript Compiler"
meta = ["org.name": "Fantom",
"org.uri": "https://fantom.org/",
"proj.name": "Fantom Core",
"proj.uri": "https://fantom.org/",
"license.name": "Academic Free License 3.0",
"vcs.name": "Git",
"vcs.uri": "https://github.com/fantom-lang/fantom"]
depends = ["sys 1.0", "compiler 1.0",]
srcDirs = [`fan/`,
`fan/ast/`,
`fan/util/`,
]
docSrc = true
}
}
156 changes: 156 additions & 0 deletions src/compilerEs/fan/CompileEsPlugin.fan
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//
// Copyright (c) 2023, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 27 Apr 2023 Matthew Giannini Creation
//

using compiler

**
** Fantom source to JavaScript source compiler - this class is
** plugged into the compiler pipeline by the compiler::CompileJs step.
**
class CompileEsPlugin : CompilerStep
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

new make(Compiler c) : super(c)
{
this.sourcemap = SourceMap(this)
this.js = JsWriter(buf.out, sourcemap)
pod.depends.each |depend| { dependOnNames[depend.name] = true }
readJsProps
}

private StrBuf buf := StrBuf()
SourceMap sourcemap { private set }
JsWriter js { private set }
private [Str:Str] usingAs := [:]

//////////////////////////////////////////////////////////////////////////
// Emit State
//////////////////////////////////////////////////////////////////////////

** The variable name that refers to "this" in the current method context
Str thisName := "this"

** next unique id
private Int uid := 0
Int nextUid() { uid++; }

[Str:Bool] dependOnNames := [:] { def = false }

//////////////////////////////////////////////////////////////////////////
// js.props
//////////////////////////////////////////////////////////////////////////

private Void readJsProps()
{
f := compiler.input.baseDir.plus(`js.props`)
if (!f.exists) return
f.readProps.each |val, key|
{
if (key.startsWith("using."))
this.usingAs[key["using.".size..-1]] = val
}
}

** Get the alias for this pod if one was defined in js.props, otherwise
** return the pod name.
Str podAlias(Str podName) { usingAs.get(podName, podName) }

//////////////////////////////////////////////////////////////////////////
// Pipeline
//////////////////////////////////////////////////////////////////////////

override Void run()
{
if (pod.name.contains("fwt")) return

try {
JsPod(this).write
// echo(buf.toStr)
compiler.cjs = buf.toStr
compiler.esm = toEsm(compiler.cjs)

} catch (Err e) { echo(buf.toStr); throw e }

buf.clear
sourcemap.write(js.line, buf.out)
// echo(buf.toStr)
compiler.cjsSourceMap = buf.toStr
}

//////////////////////////////////////////////////////////////////////////
// ESM
//////////////////////////////////////////////////////////////////////////

** Converts CommonJs emitted code to ESM
private Str toEsm(Str cjs)
{
buf := StrBuf()
lines := cjs.splitLines
inRequire := false
inExport := false
i := 0
while (true)
{
line := lines[i++]
buf.add("${line}\n")
if (line.startsWith("// cjs require begin")) i = requireToImport(buf, lines, i)
else if (line.startsWith("// cjs exports begin"))
{
// we assume this is the very last thing in the file and stop once
// we convert to ESM export statement
toExports(buf, lines, i)
break
}
}
return buf.toStr
}

private Int requireToImport(StrBuf buf, Str[] lines, Int i)
{
// this regex matches statements that require a pod in the cjs
// and creates a group for the pod name/alias (1) and the file name (2).
regex := Regex<|^const ([^_].*)? =.*__require\('(.*)?.js'\);|>

while (true)
{
line := lines[i++]
m := regex.matcher(line)
if (m.matches)
{
pod := m.group(1)
if (pod == "fantom") { buf.addChar('\n'); continue; }
file := m.group(2)
buf.add("import * as ${pod} from './${file}.js'\n")
}
else if (line.startsWith("// cjs require end")) { buf.add("${line}\n"); break }
else buf.addChar('\n')
}
return i
}

private Int toExports(StrBuf buf, Str[] lines, Int i)
{
// skip: const <pod> = {
line := lines[i++]

buf.add("export {\n")
while(true)
{
line = lines[i++]
buf.add("${line}\n")
if (line == "};") break
}
while (!(line = lines[i++]).startsWith("// cjs exports end")) continue;

return i
}
}
130 changes: 130 additions & 0 deletions src/compilerEs/fan/JsWriter.fan
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// Copyright (c) 2023, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 03 May 2023 Matthew Giannini Creation
//

using compiler

**
** JsWriter
**
class JsWriter
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

new make(OutStream out, SourceMap? sourcemap := null)
{
this.out = out
this.sourcemap = sourcemap
}

private OutStream out
private SourceMap? sourcemap := null
Int line := 0 { private set }
Int col := 0 { private set }
private Bool needIndent := false
private Int indentation := 0
@NoDoc Bool trace := false

//////////////////////////////////////////////////////////////////////////
// JsWriter
//////////////////////////////////////////////////////////////////////////

** Write and then return this. If loc is not null, the text will be
** added to the generated source map.
JsWriter w(Obj o, Loc? loc := null, Str? name := null)
{
if (needIndent)
{
spaces := indentation * 2
out.writeChars(Str.spaces(spaces))
col += spaces
needIndent = false
}
str := o.toStr
if (str.containsChar('\n')) throw Err("cannot w() str with newline: ${str}")
if (loc != null) sourcemap?.add(str, Loc(loc.file, line, col), loc, name)
if (trace) Env.cur.out.print(str)
out.writeChars(str)
col += str.size
return this
}

** Convenience for 'w(o,loc,name).nl'.
JsWriter wl(Obj o, Loc? loc := null, Str? name := null)
{
this.w(o, loc, name).nl
}

** Write newline and then return this.
JsWriter nl()
{
if (trace) Env.cur.out.printLine
out.writeChar('\n')
++line
col = 0
needIndent = true
out.flush
return this
}


** Increment the indentation
JsWriter indent() { indentation++; return this }

** Decrement the indentation
JsWriter unindent()
{
indentation--
if (indentation < 0) indentation = 0
return this
}

JsWriter minify(InStream in, Bool close := true)
{
inBlock := false
in.readAllLines.each |line|
{
// TODO: temp hack for inlining already minified js
if (line.size > 1024) { w(line).nl; return }

s := line
// line comments
if (s.size > 1 && (s[0] == '/' && s[1] == '/')) return
// need to check if inside str
// i := s.index("//")
// if (i != null) s = s[0..<i]
// block comments
temp := s
a := temp.index("/*")
if (a != null)
{
s = temp[0..<a]
inBlock = true
}
if (inBlock)
{
b := temp.index("*/")
if (b != null)
{
s = (a == null) ? temp[b+2..-1] : s + temp[b+2..-1]
inBlock = false
}
}
// trim and print
s = s.trimEnd
if (inBlock) return
// if (s.size == 0) return
w(s).nl
}
if (close) in.close
return this
}

}
Loading

0 comments on commit a2051d8

Please sign in to comment.