-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
nodeJs: added TS declaration writer (#31)
- Loading branch information
Showing
2 changed files
with
653 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
// | ||
// Copyright (c) 2023, Brian Frank and Andy Frank | ||
// Licensed under the Academic Free License version 3.0 | ||
// | ||
// History: | ||
// 27 Apr 2023 Matthew Giannini Creation | ||
// 2 Jun 2023 Kiera O'Flynn Implemented | ||
// | ||
|
||
using compiler | ||
using compilerEs | ||
using fandoc | ||
|
||
** | ||
** Generate TypeScript declaration file for a pod | ||
** | ||
class CompileTsPlugin : CompilerStep | ||
{ | ||
new make(Compiler compiler) : super(compiler) | ||
{ | ||
this.c = compiler | ||
docParser = FandocParser() | ||
} | ||
|
||
private Compiler c | ||
private OutStream? out | ||
private FandocParser docParser | ||
private TsDocWriter? docWriter | ||
|
||
////////////////////////////////////////////////////////////////////////// | ||
// Main writing method | ||
////////////////////////////////////////////////////////////////////////// | ||
|
||
override Void run() | ||
{ | ||
buf := Buf() | ||
out = buf.out | ||
|
||
// Write dependencies | ||
deps := pod.depends.map |CDepend dep->Str| { dep.name } | ||
docWriter = TsDocWriter(out, deps) | ||
|
||
deps.each |dep| | ||
{ | ||
out.print("import * as ${dep} from './${dep}.js';\n") | ||
} | ||
if (pod.name == "sys") printJsObj | ||
out.write('\n') | ||
|
||
// Write declaration for each type | ||
pod.typeDefs.findAll { !it.isSynthetic }.each |type| | ||
{ | ||
// TODO: for now generate declaration for all types regardless of whether | ||
// they have the @Js facet or not | ||
// if (!type.hasFacet(jsFacet)) return | ||
if (type.isInternal) return | ||
if (type.signature == "sys::Func") return | ||
|
||
setupDoc(pod.name, type.name) | ||
|
||
// Parameterization of List & Map | ||
classParams := "" | ||
if (type.signature == "sys::List") | ||
classParams = "<V = unknown>" | ||
if (type.signature == "sys::Map") | ||
classParams = "<K = unknown, V = unknown>" | ||
|
||
abstr := type.isMixin ? "abstract " : "" | ||
extends := "" | ||
if (type.base != null) | ||
extends = "extends ${getNamespacedType(type.base.name, type.base.pod.name, pod)} " | ||
if (!type.mixins.isEmpty) | ||
{ | ||
implement := type.mixins.map { getNamespacedType(it.name, it.pod.name, this.pod) }.join(", ") | ||
extends += "implements $implement " | ||
} | ||
|
||
// Write class documentation & header | ||
printDoc(type.doc, 0) | ||
out.print("export ${abstr}class $type.name$classParams $extends{\n") | ||
|
||
hasItBlockCtor := type.ctors.any |CMethod m->Bool| { | ||
m.params.any |CParam p->Bool| { p.paramType.isFunc } | ||
} | ||
|
||
// Write fields | ||
fields := type.fields.findAll |field| | ||
{ | ||
field.isPublic && | ||
(field is FieldDef || | ||
(type.mixins.any |m| { m.slot(field.name)?.isPublic == true } && | ||
type.base?.slot(field.name) == null)) | ||
} | ||
fields.each |field| | ||
{ | ||
name := JsNode.pickleName(field.name, deps) | ||
staticStr := field.isStatic ? "static " : "" | ||
typeStr := getJsType(field.fieldType, pod, field.isStatic ? type : null) | ||
|
||
if (field is FieldDef) | ||
printDoc(field->doc, 2) | ||
|
||
out.print(" $staticStr$name(): $typeStr\n") | ||
if (!field.isConst) | ||
out.print(" $staticStr$name(it: $typeStr): void\n") | ||
else if (hasItBlockCtor) | ||
out.print(" ${staticStr}__$name(it: $typeStr): void\n") | ||
} | ||
|
||
// Write methods | ||
methods := type.methods.findAll |method| | ||
{ | ||
method.isPublic && | ||
(method is MethodDef || | ||
(type.mixins.any |m| { m.slot(method.name)?.isPublic == true } && | ||
type.base?.slot(method.name) == null)) | ||
} | ||
methods.each |method| | ||
{ | ||
isStatic := method.isStatic || method.isCtor || pmap.containsKey(type.signature) | ||
staticStr := isStatic ? "static " : "" | ||
name := JsNode.pickleName(method.name, deps) | ||
|
||
inputList := method.params.map |CParam p->Str| { | ||
paramName := JsNode.pickleName(p.name, deps) | ||
if (p.hasDefault) | ||
paramName += "?" | ||
paramType := getJsType(p.paramType, pod, isStatic ? type : null) | ||
return "$paramName: $paramType" | ||
} | ||
if (!method.isStatic && !method.isCtor && pmap.containsKey(type.signature)) | ||
inputList.insert(0, "self: ${pmap[type.signature]}") | ||
if (method.isCtor) | ||
inputList.add("...args: unknown[]") | ||
inputs := inputList.join(", ") | ||
|
||
output := method.isCtor ? type.name : getJsType(method.returnType, pod, pmap.containsKey(type.signature) ? type : null) | ||
if (method.qname == "sys::Obj.toImmutable" || | ||
method.qname == "sys::List.ro" || | ||
method.qname == "sys::Map.ro") | ||
output = "Readonly<$output>" | ||
|
||
if (method is MethodDef) | ||
printDoc(method->doc, 2) | ||
out.print(" $staticStr$name($inputs): $output\n") | ||
} | ||
|
||
out.print("}\n") | ||
} | ||
if (pod.name == "sys") printObjUtil | ||
|
||
buf.seek(0) | ||
c.tsDecl = buf.readAllStr | ||
} | ||
|
||
////////////////////////////////////////////////////////////////////////// | ||
// Utils | ||
////////////////////////////////////////////////////////////////////////// | ||
|
||
** Gets the name of the given type in JS. For example, a map type | ||
** could show up as Map, sys.Map, Map<string, string>, etc. | ||
** | ||
** 'thisPod' is the pod you are writing the type in; if 'type' is | ||
** from a different pod, it will have its pod name prepended to it, | ||
** e.g. sys.Map rather than just Map. | ||
** | ||
** 'thisType' should only be non-null if instances of sys::This should | ||
** be written as that type instead of "this". For example, Int methods | ||
** which are non-static in Fantom but static in JS cannot use the "this" | ||
** type. | ||
private Str getJsType(CType type, CPod thisPod, CType? thisType := null) | ||
{ | ||
// Built-in type | ||
if (pmap.containsKey(type.signature)) | ||
return pmap[type.signature] | ||
|
||
// Nullable type | ||
if (type.isNullable) | ||
return "${getJsType(type.toNonNullable, thisPod, thisType)} | null" | ||
|
||
// This | ||
if (type.isThis) | ||
return thisType == null ? "this" : thisType.name | ||
|
||
// Generic parameters | ||
if (type.isGenericParameter) | ||
switch (type.name) | ||
{ | ||
case "L": return "List<V>" | ||
case "M": return "Map<K,V>" | ||
} | ||
|
||
// List/map types | ||
if (type.isList || type.isMap) | ||
{ | ||
if (type is TypeRef) type = type.deref | ||
|
||
res := getNamespacedType(type.name, "sys", thisPod) | ||
if (!type.isGeneric) | ||
{ | ||
k := type is MapType ? "${getJsType(type->k, thisPod, thisType)}, " : "" | ||
v := getJsType(type->v, thisPod, thisType) | ||
res += "<$k$v>" | ||
} | ||
return res | ||
} | ||
|
||
// Function types | ||
if (type.isFunc) | ||
{ | ||
if (type is TypeRef) type = type.deref | ||
if (!(type is FuncType)) //isGeneric | ||
return "Function" | ||
|
||
CType[] args := type->params->dup | ||
inputs := args.map |CType t, Int i->Str| { "arg$i: ${getJsType(t, thisPod, thisType)}" } | ||
.join(", ") | ||
output := getJsType(type->ret, thisPod, thisType) | ||
return "(($inputs) => $output)" | ||
} | ||
|
||
// Obj | ||
if (type.signature == "sys::Obj") | ||
return getNamespacedType("JsObj", "sys", thisPod) | ||
|
||
// Regular types | ||
return getNamespacedType(type.name, type.pod.name, thisPod) | ||
} | ||
|
||
** Gets the name of the type with, when necessary, the pod name prepended to it. | ||
** e.g. could return "TimeZone" or "sys.TimeZone" based on the current pod. | ||
private Str getNamespacedType(Str typeName, Str typePod, CPod currentPod) | ||
{ | ||
if (typePod == currentPod.name) | ||
return typeName | ||
return "${typePod}.${typeName}" | ||
} | ||
|
||
private Void setupDoc(Str pod, Str type) | ||
{ | ||
docWriter.pod = pod | ||
docWriter.type = type | ||
} | ||
|
||
private Void printDoc(DocDef? doc, Int indent) | ||
{ | ||
if (doc == null) return | ||
|
||
docWriter.indent = indent | ||
docParser.parse("Doc", doc.lines.join("\n").in).write(docWriter) | ||
} | ||
|
||
private Void printJsObj() | ||
{ | ||
out.print("export type JsObj = Obj | number | string | boolean | Function\n") | ||
} | ||
|
||
private Void printObjUtil() | ||
{ | ||
out.print( """export class ObjUtil { | ||
static hash(obj: any): number | ||
static equals(a: any, b: JsObj | null): boolean | ||
static compare(a: any, b: JsObj | null, op?: boolean): number | ||
static compareNE(a: any, b: JsObj | null): boolean | ||
static compareLT(a: any, b: JsObj | null): boolean | ||
static compareLE(a: any, b: JsObj | null): boolean | ||
static compareGE(a: any, b: JsObj | null): boolean | ||
static compareGT(a: any, b: JsObj | null): boolean | ||
static is(obj: any, type: Type): boolean | ||
static as(obj: any, type: Type): any | ||
static coerce(obj: any, type: Type): any | ||
static typeof\$(obj: any): Type | ||
static trap(obj: any, name: string, args: List<JsObj | null> | null): JsObj | null | ||
static doTrap(obj: any, name: string, args: List<JsObj | null> | null, type: Type): JsObj | null | ||
static isImmutable(obj: any): boolean | ||
static toImmutable(obj: any): JsObj | null | ||
static with\$<T>(self: T, f: (() => T)): T | ||
static toStr(obj: any): string | ||
static echo(obj: any): void | ||
} | ||
""") | ||
} | ||
|
||
private const Str:Str pmap := | ||
[ | ||
"sys::Bool": "boolean", | ||
"sys::Decimal": "number", | ||
"sys::Float": "number", | ||
"sys::Int": "number", | ||
"sys::Num": "number", | ||
"sys::Str": "string", | ||
"sys::Void": "void" | ||
] | ||
|
||
} |
Oops, something went wrong.