Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rescript: support rescript-nodejs as the binding for Node API #412

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion build/build.fs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,10 @@ module Test =
// "safe" package which depends on another "safe" package
"safe", !! "node_modules/@types/yargs-parser/index.d.ts", [];
"safe", !! "node_modules/@types/yargs/index.d.ts", [];

"minimal", !! "node_modules/@types/vscode/index.d.ts", ["--readable-names"];

// #404: package with mutually recursive files (requires --merge)
"minimal", !! "node_modules/playwright-core/index.d.ts" ++ "node_modules/playwright-core/types/*.d.ts", ["--merge"];
]

for preset, package, additionalOptions in packages do
Expand All @@ -194,6 +196,15 @@ module Test =
$"--preset {preset}"; $"-o {outputDir}"] @ additionalOptions)
package

// patches for playwright-core
Shell.replaceInFiles [
"Readable.t", "Readable.t<'t>"
"URL.t", "NodeJs.Url.t"
] [
outputDir </> "playwright_core.resi"
outputDir </> "playwright_core.res"
]

let build () =
Shell.mkdir srcGeneratedDir
for file in outputDir |> Shell.copyRecursiveTo true srcGeneratedDir do
Expand Down
2 changes: 2 additions & 0 deletions dist/res/src/ts2ocaml.res
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ type true_ = bool
type false_ = bool
type intrinsic = private string
type object = Type.Classify.object
module Object = { type t = object }
type function = Type.Classify.function
module Function = { type t = function }

module Union = {
type container<+'cases>
Expand Down
20 changes: 20 additions & 0 deletions lib/Extensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ type StringBuilder (s: string) =
type StringBuilder = System.Text.StringBuilder
#endif

type OptionBuilder() =
member inline _.Bind (v,f) = Option.bind f v
member inline _.Return v = Some v
member inline _.BindReturn (v, f) = Option.map f v
member inline _.Bind2Return (v1, v2, f) = Option.map2 f v1 v2
member inline _.Bind3Return (v1, v2, v3, f) = Option.map3 f v1 v2 v3
member _.MergeSources (v1, v2) =
match v1, v2 with
| Some x1, Some x2 -> Some (x1, x2)
| _, _ -> None
member _.MergeSources3 (v1, v2, v3) =
match v1, v2, v3 with
| Some x1, Some x2, Some x3 -> Some (x1, x2, x3)
| _, _, _ -> None
member inline _.For (xs, f) = Seq.tryPick f xs
member inline _.ReturnFrom o = o
member inline _.Zero () = None

let option = new OptionBuilder()

open Fable.Core
open Fable.Core.JsInterop

Expand Down
82 changes: 82 additions & 0 deletions lib/JsHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,85 @@ let resolveRelativeImportPath (info: Syntax.PackageInfo option) (currentFile: Pa
getJsModuleName info targetPath
else
Valid path

[<StringEnum(CaseRules.SnakeCase); RequireQualifiedAccess>]
type NodeBuiltin =
| Assert
| AsyncHooks
| Buffer
| ChildProcess
| Cluster
| Console
| Constants
| Crypto
| Dgram
| DiagnosticsChannel
| Dns
| Domain
| Events
| Fs
| Http
| [<CompiledName("http2")>] Http2
| Https
| Inspector
| Module
| Net
| Os
| Path
| PerfHooks
| Process
| Punycode
| Querystring
| Readline
| Repl
| Stream
| StringDecoder
| Timers
| Tls
| TraceEvents
| Tty
| Url
| Util
| [<CompiledName("v8")>] V8
| Vm
| Wasi
| WorkerThreads
| Zlib

let nodeBuiltins : Set<NodeBuiltin> =
set [
NodeBuiltin.Assert; NodeBuiltin.AsyncHooks; NodeBuiltin.Buffer; NodeBuiltin.ChildProcess;
NodeBuiltin.Cluster; NodeBuiltin.Console; NodeBuiltin.Constants; NodeBuiltin.Crypto;
NodeBuiltin.Dgram; NodeBuiltin.DiagnosticsChannel; NodeBuiltin.Dns; NodeBuiltin.Domain;
NodeBuiltin.Events; NodeBuiltin.Fs; NodeBuiltin.Http; NodeBuiltin.Http2; NodeBuiltin.Https;
NodeBuiltin.Inspector; NodeBuiltin.Module; NodeBuiltin.Net; NodeBuiltin.Net; NodeBuiltin.Os;
NodeBuiltin.Path; NodeBuiltin.PerfHooks; NodeBuiltin.Process; NodeBuiltin.Punycode;
NodeBuiltin.Querystring; NodeBuiltin.Readline; NodeBuiltin.Repl; NodeBuiltin.Stream;
NodeBuiltin.StringDecoder; NodeBuiltin.Timers; NodeBuiltin.Tls; NodeBuiltin.TraceEvents;
NodeBuiltin.Tty; NodeBuiltin.Url; NodeBuiltin.Util; NodeBuiltin.V8; NodeBuiltin.Vm;
NodeBuiltin.Wasi; NodeBuiltin.WorkerThreads; NodeBuiltin.Zlib;
]

type NodeBuiltinResult = {
name: NodeBuiltin;
subpath: string option;
}

open System.Text.RegularExpressions

let nodeBuiltinPattern = new Regex("^(?:node:)?(\\w+)(?:\\/(.+))?$")

let getNodeBuiltin (specifier: string) : NodeBuiltinResult option =
let res = nodeBuiltinPattern.Match(specifier)
if isNull res || res.Groups.Count < 2 then None
else
let name : NodeBuiltin = !!res.Groups[1].Value
if nodeBuiltins |> Set.contains name |> not then None
else
let subpath =
if res.Groups.Count < 3 then None
else
let subpath = res.Groups[2].Value
if System.String.IsNullOrWhiteSpace subpath then None
else Some subpath
Some { name = name; subpath = subpath }
4 changes: 4 additions & 0 deletions lib/Parser.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,10 @@ let createDependencyGraph (sourceFiles: Ts.SourceFile[]) =
let n = n :?> Ts.ImportDeclaration
n.moduleSpecifier
|> handleModuleSpecifier sourceFile
| Ts.SyntaxKind.ExportDeclaration ->
let n = n :?> Ts.ExportDeclaration
n.moduleSpecifier
|> Option.iter (handleModuleSpecifier sourceFile)
| _ -> ()
ns |> Array.collect (fun n -> n.getChildren(sourceFile))

Expand Down
7 changes: 7 additions & 0 deletions lib/Syntax.fs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,13 @@ and ImportClause =
/// import name = identifier
/// ```
| LocalImport of {| name: string; kind: Set<Kind> option; target: Ident |}
member this.importedName =
match this with
| NamespaceImport i -> Some i.name
| ES6WildcardImport _ -> None
| ES6Import i -> i.renameAs |> Option.orElse (Some i.name)
| ES6DefaultImport i -> Some i.name
| LocalImport i -> Some i.name
member this.kind =
match this with
| NamespaceImport i -> i.kind
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"cassandra-driver": "^4.6.3",
"cdk8s": "^2.2.41",
"monaco-editor": "0.47.0",
"playwright": "^1.43.1",
"react-player": "2.16.0",
"rescript": "11.1.0",
"ts2fable": "0.8.0-build.723",
Expand Down
164 changes: 163 additions & 1 deletion src/Targets/ReScript/ReScriptHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ module Naming =

let reservedModuleNames =
Set.ofList [
"Export"; "Default"; "Types"
"Export"; "Default"; "Types"; "NodeJs"
] |> Set.union keywords

let moduleNameReserved (name: string) =
Expand Down Expand Up @@ -588,3 +588,165 @@ module Binding =
| Binding.Ext x -> yield Statement.external x.attrs x.name x.ty x.target
| Binding.Unknown x -> match x.msg with Some msg -> yield comment msg | None -> ()
]

module Import =
open Fable.Core.JsInterop
open JsHelper
type Node = NodeBuiltin

let isModule (ctx: Typer.TyperContext<_, _>) (name: string) (kind: Set<Kind> option) =
kind |> Option.map Kind.generatesReScriptModule
|> Option.defaultValue false
|| ctx |> Typer.TyperContext.tryCurrentSourceInfo (fun i -> i.unknownIdentTypes |> Trie.containsKey [name])
|> Option.defaultValue false
|| name |> Naming.isCase Naming.PascalCase

[<RequireQualifiedAccess>]
type Statement =
/// `module Name = Target`
| Alias of name:string * target:string
/// `open Name`
| Open of name:string
/// `module Name = { type t = typeName }`
| Type of name:string * typeName:string * typeArgs:string list
/// `module Name = Y.Make()`
| FunctorInstance of name:string * expr:string
| Comment of text

let alias name target = Statement.Alias (name, target)
let open_ name = Statement.Open name
let type_ name typeName typeArgs = Statement.Type (name, typeName, typeArgs)
let instance name expr = Statement.FunctorInstance (name, expr)
let comment text = Statement.Comment text

let private getDedicatedImportStatementForNodeBuiltin (ctx: Typer.TyperContext<_, _>) (c: ImportClause) =
option {
let! specifier = c.moduleSpecifier
let! builtin = getNodeBuiltin specifier
let moduleName =
match builtin.name with
| Node.Vm -> "VM"
| _ -> Naming.toCase Naming.PascalCase !!builtin.name
let importedName = c.importedName |? "" // this becomes None only when ES6WildcardImport is used

match builtin.name, builtin.subpath, c with
// special cases
| Node.ChildProcess, None, ES6Import x ->
match x.name with
| "ChildProcess" ->
return alias (Naming.moduleName importedName) "NodeJs.ChildProcess"
| name when name.StartsWith("ExecOptions") ->
return type_ (Naming.moduleName importedName) "NodeJs.ChildProcess.execOptions" []
| name when name.StartsWith("ExecFileOptions") ->
return type_ (Naming.moduleName importedName) "NodeJs.ChildProcess.execFileOptions" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.ChildProcess.{x.name}"
| Node.Console, None, ES6Import x when x.name = "ConsoleConstructorOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Console.consoleOptions" []
| Node.Dns, None, ES6Import x when x.name.StartsWith("Lookup") && x.name.EndsWith("Options") ->
return type_ (Naming.moduleName importedName) "NodeJs.Dns.options" []
| Node.Events, None, ES6Import x when x.name = "EventEmitter" ->
return instance (Naming.moduleName importedName) "NodeJs.EventEmitter.Make()"
| Node.Fs, _, ES6Import x ->
match builtin.subpath, x.name with
| None, "ReadStreamOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Fs.createReadStreamOptions" []
| None, "WriteStreamOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Fs.createWriteStreamOptions" ["'t"]
| None, "WriteFileOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Fs.writeFileOptions" []
| _, _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Fs.{x.name}"
| Node.Http, None, ES6Import x ->
match x.name with
| "IncomingHttpHeaders" | "OutgoingHttpHeaders" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http.headersObject" []
| "ServerOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http.createServerOptions" []
| "RequestOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http.requestOptions" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Http.{x.name}"
| Node.Http2, None, ES6Import x when x.name = "Settings" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http2.settingsObject" []
| Node.Https, None, ES6Import x ->
match x.name with
| "Server" ->
return alias (Naming.moduleName importedName) "NodeJs.Https.HttpsServer"
| "Agent" ->
return alias (Naming.moduleName importedName) "NodeJs.Https.Agent"
// rescript-nodejs doesn't have these, but it has fallback types
| "ServerOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http.createServerOptions" []
| "RequestOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Http.requestOptions" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Https.{x.name}"
| Node.Net, None, ES6Import x when x.name = "AddressInfo" ->
return type_ (Naming.moduleName importedName) "NodeJs.Net.address" []
| Node.Os, None, ES6Import x when x.name = "CpuInfo" ->
return type_ (Naming.moduleName importedName) "NodeJs.Os.cpu" []
| Node.Os, None, ES6Import x when x.name = "ParsedPath" || x.name = "FormatInputPathObject" ->
return type_ (Naming.moduleName importedName) "NodeJs.Path.t" []
| Node.Tls, None, ES6Import x ->
match x.name with
| "TLSSocket" ->
return alias (Naming.moduleName importedName) "NodeJs.Tls.TlsSocket"
| "Server" ->
return alias (Naming.moduleName importedName) "NodeJs.Tls.TlsServer"
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Tls.{x.name}"
| Node.Url, None, ES6Import x ->
match x.name with
| "UrlObject" | "Url" | "UrlWithParsedQuery" | "UrlWithStringQuery" | "URL" ->
return alias (Naming.moduleName importedName) "NodeJs.Url"
| "URLSearchParams" ->
return alias (Naming.moduleName importedName) "NodeJs.Url.SearchParams"
| "URLFormatOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Url.urlFormatOptions" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Url.{x.name}"
| Node.Util, None, ES6Import x ->
match x.name with
| "InspectOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.Util.inspectOptions" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.Util.{x.name}"
| Node.V8, None, ES6Import x ->
match x.name with
| "HeapSpaceInfo" ->
return type_ (Naming.moduleName importedName) "NodeJs.V8.heapSpaceStats" []
| "HeapInfo" ->
return type_ (Naming.moduleName importedName) "NodeJs.V8.heapStats" []
| "HeapCodeStatistics" ->
return type_ (Naming.moduleName importedName) "NodeJs.V8.heapCodeStats" []
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.V8.{x.name}"
| Node.Vm, None, ES6Import x ->
match x.name with
| "CreateContextOptions" ->
return type_ (Naming.moduleName importedName) "NodeJs.VM.createContextOptions" []
| "Context" ->
return type_ (Naming.moduleName importedName) "NodeJs.VM.contextifiedObject" ["'t"]
| _ ->
return alias (Naming.moduleName importedName) $"NodeJs.VM.{x.name}"

// general cases
// 1. the `t` type right under the module
| (Node.Buffer | Node.ChildProcess | Node.Console | Node.Module | Node.Stream | Node.StringDecoder), None, ES6Import x when x.name = moduleName ->
return alias (Naming.moduleName importedName) $"NodeJs.{moduleName}"
// 2. the module contains submodules with their `t` type
| _, _, _ ->
match c with
| (NamespaceImport _ | ES6DefaultImport _) ->
return alias (Naming.moduleName importedName) $"NodeJs.{moduleName}"
| ES6Import x when isModule ctx x.name x.kind ->
return alias (Naming.moduleName importedName) $"NodeJs.{moduleName}.{x.name}"
| ES6WildcardImport _ ->
return open_ $"NodeJs.{moduleName}"
| _ -> ()
}

let getDedicatedImportStatement (ctx: Typer.TyperContext<_, _>) (c: ImportClause) =
getDedicatedImportStatementForNodeBuiltin ctx c
// TODO: add more dedicated imports?
Loading
Loading