//
// 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
**
** JsPod
**
class JsPod : JsNode
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(CompileEsPlugin plugin) : super(plugin)
{
this.pod = plugin.pod
// map native files by name
baseDir := c.input.baseDir
c.jsFiles?.each |f| {
// get the relative uri to this js file from the baseDir.
// For example, consider pod "podName" in this location:
// /path/to/podName/js/Foo.js
// The baseDir is /path/to/podName/
// When we relativie it it becomes js/Foo.js
// We then strip the first directory from the path and change it to es.
// Therefore we look for the file in /path/to/podName/es/Foo.js
//
// This supports deeper paths also:
// /path/to/podName/js/dir1/Bar.js => /path/to/podname/es/dir1/Bar.js
relUri := f.uri.relTo(baseDir.uri)[1..-1]
esFile := baseDir.plus(`es/${relUri}`)
if (esFile.exists)
natives[f.name] = esFile
}
// find types to emit
c.types.findAll { isJsType(it) }.each { types.add(JsType(plugin, it)) }
}
private PodDef pod
private JsType[] types := [,]
private [Str:File] natives := [:]
private [Str:Bool] peers := [:]{ it.def = false }
//////////////////////////////////////////////////////////////////////////
// JsPod
//////////////////////////////////////////////////////////////////////////
override Void write()
{
writeRequire
writeTypes
writeTypeInfo
writeProps
writeClosureFields
writeNatives
writeExports
js.wl("}).call(this);")
}
private Void writeRequire()
{
js.wl("// cjs require begin")
CommonJs.moduleStart.splitLines.each |line| { js.wl(line) }
js.wl("const fan = __require('fan.js');")
js.wl("const fantom = __require('fantom.js');")
js.wl("const sys = fantom ? fantom.sys : __require('sys.js');")
// we need to require full dependency chain
pods := (CPod[])pod.depends.mapNotNull |p->CPod?|
{
if (p.name.startsWith("[java]")) return null
return c.ns.resolvePod(p.name, null)
}
c.ns.flattenAndOrderByDepends(pods).each |depend|
{
if (depend.name == "sys") return
if (!c.ns.resolvePod(depend.name, null).hasJs && !c.input.forceJs) return
// NOTE if we change sys to fan we need to update JNode.qnameToJs
// js.wl("import * as ${depend.name} from './${depend.name}.js';")
js.wl("const ${plugin.podAlias(depend.name)} = __require('${depend.name}.js');")
}
js.wl("// cjs require end")
js.wl("const js = (typeof window !== 'undefined') ? window : global;")
}
private Void writeTypes()
{
types.each |JsType t|
{
plugin.curType = t.def
if (t.def.isNative) writePeer(t, null)
else
{
t.write
if (t.hasNatives) writePeer(t, t.peer)
}
js.nl
plugin.curType = null
}
}
private Void writePeer(JsType t, CType? peer)
{
key := "${t.name}.js"
if (peer != null)
{
key = "${peer.name}Peer.js"
this.peers[t.name] = true
}
file := natives.remove(key)
if (file == null || !file.exists)
{
warn("Missing native impl for ${t.def.signature}", Loc("${t.name}.fan"))
// Do not export peer types that we don't have implementations for
this.peers[t.name] = false
}
else
{
in := file.in
js.minify(in)
}
}
private Void writeTypeInfo()
{
// add the pod to the type system
js.wl("const p = sys.Pod.add\$('${pod.name}');")
js.wl("const xp = sys.Param.noParams\$();")
// general use map variable
js.wl("let m;")
// filter out synthetic types from reflection
reflect := types.findAll |t| { !t.def.isSynthetic }
// write all types first
reflect.each |t|
{
adder := t.def.isMixin ? "p.am\$" : "p.at\$"
base := "${t.base.pod}::${t.base.name}"
mixins := t.mixins.join(",") |m| { "'${m.pod}::${m.name}'" }
facets := toFacets(t.facets)
flags := t.def.flags
js.wl("${t.name}.type\$ = ${adder}('${t.name}','${base}',[${mixins}],{${facets}},${flags},${t.name});")
}
// then write slot info
reflect.each |JsType t|
{
if (t.fields.isEmpty && t.methods.isEmpty) return
js.w("${t.name}.type\$")
t.fields.each |FieldDef f|
{
// don't write for FFI
if (f.isForeign || f.fieldType.isForeign) return
facets := toFacets(f.facets)
js.w(".af\$('${f.name}',${f->flags},'${f.fieldType.signature}',{${facets}})")
}
t.methods.each |MethodDef m|
{
if (m.isFieldAccessor) return
if (m.params.any |CParam p->Bool| { p.paramType.isForeign }) return
params := m.params.join(",") |p| { "new sys.Param('${p.name}','${p.paramType.signature}',${p.hasDefault})"}
paramList := m.params.isEmpty
? "xp"
: "sys.List.make(sys.Param.type\$,[${params}])"
facets := toFacets(m.facets)
js.w(".am\$('${m.name}',${m.flags},'${m.ret.signature}',${paramList},{${facets}})")
}
js.wl(";")
}
// pod meta
js.nl.wl("m=sys.Map.make(sys.Str.type\$,sys.Str.type\$);")
pod.meta.each |v, k|
{
js.wl("m.set(${k.toCode}, ${v.toCode});")
}
js.wl("p.__meta(m);").nl
}
private static Str toFacets(FacetDef[]? facets)
{
facets == null ? "" : facets.join(",") |f| { "'${f.type.qname}':${f.serialize.toCode}" }
}
private Void writeProps()
{
baseDir := c.input.baseDir
if (baseDir != null)
{
c.jsPropsFiles?.each |file|
{
if (file.ext != "props") return
uri := file.uri.relTo(baseDir.uri)
key := "${pod.name}:${uri}"
try
{
props := file.in.readProps
js.wl("m=sys.Map.make(sys.Str.type\$, sys.Str.type\$);")
props.each |v,k| { js.wl("m.set(${k.toCode},${v.toCode});") }
js.wl("sys.Env.cur().__props(${key.toCode}, m);").nl
}
catch (ArgErr err)
{
// some props files aren't actually valid props files, so we ignore those
// e.g. they have duplicate keys which is not allowed
warn("Invalid props file: ${uri}. ${err.msg}")
}
}
js.nl
}
}
private Void writeClosureFields()
{
plugin.closureSupport.write
}
private Void writeNatives()
{
natives.each |f| { js.minify(f.in) }
}
private Void writeExports()
{
// only export public types
js.wl("// cjs exports begin")
js.wl("const __${pod.name} = {").indent
types.findAll { it.def.isPublic }.each |t| {
js.wl("${t.name},")
if (this.peers[t.name]) js.wl("${t.peer.name}Peer,")
}
js.unindent.wl("};")
js.wl("fan.${pod.name} = __${pod.name};")
js.wl("if (typeof exports !== 'undefined') module.exports = __${pod.name};")
js.wl("// cjs exports end")
}
}