Skip to content

Commit

Permalink
Handle parsing more syntax in .d.ts files (#367)
Browse files Browse the repository at this point in the history
* Handle parsing more syntax in .d.ts files

* handle files that only contain comments

* Parse more .d.ts syntax
  • Loading branch information
kevinbarabash authored Sep 17, 2024
1 parent bc08e88 commit 3e212e7
Show file tree
Hide file tree
Showing 9 changed files with 604 additions and 133 deletions.
6 changes: 5 additions & 1 deletion src/Escalier.Codegen/Codegen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,11 @@ module rec Codegen =
let elemTypes: list<TsTupleElement> =
elems
|> List.map (buildType ctx)
|> List.map (fun t -> { Label = None; Type = t; Loc = None })
|> List.map (fun t ->
{ Label = None
Type = t
IsRest = false
Loc = None })

TsType.TsTupleType { ElemTypes = elemTypes; Loc = None }
| TypeKind.Array { Elem = elem; Length = length } ->
Expand Down
1 change: 1 addition & 0 deletions src/Escalier.Interop.Tests/Escalier.Interop.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Compile Include="Interfaces.fs"/>
<Compile Include="Classes.fs"/>
<Compile Include="Tests.fs"/>
<Compile Include="MergeLib.fs"/>
<Compile Include="Program.fs"/>
</ItemGroup>
</Project>
20 changes: 20 additions & 0 deletions src/Escalier.Interop.Tests/MergeLib.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Escalier.Interop.Tests.MergeLib

open FsToolkit.ErrorHandling
open Xunit

open Escalier.Interop.MergeLib

[<Fact>]
let testGetDependencies () =
let res =
result {
let projectRoot = __SOURCE_DIRECTORY__

let! deps = getDependencies projectRoot "@apollo/client"

printfn $"deps.Length = {deps.Length}"
}

printfn $"res = %A{res}"
Assert.True(Result.isOk res)
120 changes: 120 additions & 0 deletions src/Escalier.Interop.Tests/Migrate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,126 @@ let ParseAndInferPropertyKey () =
printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndMigrateExportDeclareFunction () =
let res =
result {
let input =
"""
type Point = {x: number, y: number};
export declare function add({x, y}: Point): number;
"""

let! ast =
match parseModule input with
| Success(ast, _, _) -> Result.Ok ast
| Failure(_, error, _) -> Result.Error(CompileError.ParseError error)

let ast = migrateModule ast

let! ctx, env = Prelude.getEnvAndCtx projectRoot

let! env =
Infer.inferModule ctx env ast |> Result.mapError CompileError.TypeError

Assert.Value(env, "add", "fn ({mut x, mut y}: Point) -> number")
}

printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndMigrateCommentOnly () =
let res =
result {
let input = "// This is a comment"

let! ast =
match parseModule input with
| Success(ast, _, _) -> Result.Ok ast
| Failure(_, error, _) -> Result.Error(CompileError.ParseError error)

let _ = migrateModule ast

()
}

printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndMigrateEnum () =
let res =
result {
let input =
"""
export declare const enum CacheWriteBehavior {
FORBID = 0,
OVERWRITE = 1,
MERGE = 2
}
"""

let! ast =
match parseModule input with
| Success(ast, _, _) -> Result.Ok ast
| Failure(_, error, _) -> Result.Error(CompileError.ParseError error)

let _ = migrateModule ast

()
}

printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndMigrateMappedTypeWithRenaming () =
let res =
result {
let input =
"""
type OnlyRequiredProperties<T> = {
[K in keyof T as {} extends Pick<T, K> ? never : K]: T[K];
};
"""


let! ast =
match parseModule input with
| Success(ast, _, _) -> Result.Ok ast
| Failure(_, error, _) -> Result.Error(CompileError.ParseError error)

let _ = migrateModule ast

()
}

printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndMigrateNamedTuples () =
let res =
result {
let input =
"""
type Point = [x: number, y?: number];
"""

let! ast =
match parseModule input with
| Success(ast, _, _) -> Result.Ok ast
| Failure(_, error, _) -> Result.Error(CompileError.ParseError error)

let _ = migrateModule ast

()
}

printfn "res = %A" res
Assert.True(Result.isOk res)

[<Fact>]
let ParseAndInferLibEs5 () =
let res =
Expand Down
11 changes: 4 additions & 7 deletions src/Escalier.Interop/Escalier.Interop.fsproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="TypeScript.fs"/>
<Compile Include="Parser.fs"/>
<Compile Include="Migrate.fs"/>
<Compile Include="MergeLib.fs"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="FParsec" Version="1.1.1"/>
<PackageReference Include="FSharp.Data" Version="6.4.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Escalier.Data\Escalier.Data.fsproj"/>
<ProjectReference Include="..\Escalier.TypeChecker\Escalier.TypeChecker.fsproj"/>
</ItemGroup>

</Project>
</Project>
173 changes: 173 additions & 0 deletions src/Escalier.Interop/MergeLib.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
namespace Escalier.Interop

open FParsec.Error
open FsToolkit.ErrorHandling
open FSharp.Data
open System.IO

open Escalier.Data.Syntax

module MergeLib =
let mutable memoizedNodeModulesDir: Map<string, string> = Map.empty

let rec findNearestAncestorWithNodeModules (currentDir: string) =
match memoizedNodeModulesDir.TryFind currentDir with
| Some(nodeModulesDir) -> nodeModulesDir
| None ->
let nodeModulesDir = Path.Combine(currentDir, "node_modules")

if Directory.Exists(nodeModulesDir) then
currentDir
else
let parentDir = Directory.GetParent(currentDir)

match parentDir with
| null ->
failwith "node_modules directory not found in any ancestor directory."
| _ -> findNearestAncestorWithNodeModules parentDir.FullName

let private packageJsonHasTypes (packageJsonPath: string) : bool =
if File.Exists packageJsonPath then
let packageJson = File.ReadAllText(packageJsonPath)
let packageJsonObj = JsonValue.Parse(packageJson)

match packageJsonObj.TryGetProperty("types") with
| None -> false
| Some _ -> true
else
false

let resolvePath
(projectRoot: string)
(currentPath: string)
(importPath: string)
: string =
if importPath.StartsWith "~" then
Path.GetFullPath(Path.Join(projectRoot, importPath.Substring(1)))
else if importPath.StartsWith "." then
let resolvedPath =
Path.GetFullPath(
Path.Join(Path.GetDirectoryName(currentPath), importPath)
)

if currentPath.EndsWith(".d.ts") then
Path.ChangeExtension(resolvedPath, ".d.ts")
else
resolvedPath
else
// TODO: once this is implemented, move it over to Escalier.Interop
// TODO: check if there's `/` in the import path, if so, the first
// part before the `/` is the name of the module and the rost is a
// path to a .d.ts file within the module.

// determine if importPath contains a '/' and split on it
// if it does, the first part is the module name and the second part is
// the path to the .d.ts file within the module

// It's possible that the module name is a scoped package, in which case
// the module name will be the first part of the second part of the split
// and the second part will be the path to the .d.ts file within the module.
let moduleName, subpath =
match importPath.Split('/') |> List.ofArray with
| [] -> failwith "This should never happen."
| [ name ] -> name, None
| name :: path ->
if name.StartsWith("@") then
let ns = name

match path with
| [] -> failwith "This should never happen."
| [ name ] ->
let moduleName = String.concat "/" [ ns; name ]
moduleName, None
| name :: path ->
let moduleName = String.concat "/" [ ns; name ]
moduleName, Some(String.concat "/" path)
else
name, Some(String.concat "/" path)

let rootDir = findNearestAncestorWithNodeModules projectRoot
let nodeModulesDir = Path.Combine(rootDir, "node_modules")

let pkgJsonPath1 =
Path.Combine(nodeModulesDir, moduleName, "package.json")

let pkgJsonPath2 =
Path.Combine(nodeModulesDir, "@types", moduleName, "package.json")

let pkgJsonPath =
if packageJsonHasTypes pkgJsonPath1 then
pkgJsonPath1
elif packageJsonHasTypes pkgJsonPath2 then
pkgJsonPath2
else
failwith
$"package.json not found for module {moduleName}, rootDir = {rootDir}, nodeModulesDir = {nodeModulesDir}."

// read package.json and parse it
let pkgJson = File.ReadAllText(pkgJsonPath)
let pkgJsonObj = JsonValue.Parse(pkgJson)

let combinedPath =
match subpath with
| None ->
match pkgJsonObj.TryGetProperty("types") with
| None -> failwith "Invalid package.json: missing `types` field."
| Some value ->
let types = value.InnerText()

if types.EndsWith(".d.ts") then
Path.Combine(Path.GetDirectoryName(pkgJsonPath), types)
else
Path.Combine(Path.GetDirectoryName(pkgJsonPath), $"{types}.d.ts")
// Path.Combine(Path.GetDirectoryName(pkgJsonPath), types)
| Some value ->
Path.Combine(Path.GetDirectoryName(pkgJsonPath), $"{value}.d.ts")

let di = DirectoryInfo(combinedPath)
di.FullName

let getDependencies
(projectRoot: string)
(importSrc: string)
: Result<list<string>, ParserError> =
let mutable visitedPaths: list<string> = []

let rec traverse (currentPath: string) (importSrc: string) =
result {
if not (importSrc.StartsWith ".") && visitedPaths.Length <> 0 then
return ()
else
let resolvedPath = resolvePath projectRoot currentPath importSrc

if List.contains resolvedPath visitedPaths then
return ()
else
visitedPaths <- resolvedPath :: visitedPaths

let contents = File.ReadAllText(resolvedPath)

let! ast =
match Parser.parseModule contents with
| FParsec.CharParsers.Success(value, _, _) -> Result.Ok(value)
| FParsec.CharParsers.Failure(_, parserError, _) ->
printfn $"parserError = {parserError}"
Result.Error(parserError)

let ast = Migrate.migrateModule ast

for item in ast.Items do
match item with
| Import { Path = path } -> do! traverse resolvedPath path
| Export(Export.NamedExport { Src = src }) ->
do! traverse resolvedPath src
| Export(Export.ExportAll { Src = src }) ->
do! traverse resolvedPath src
| _ -> ()

return ()
}

match traverse projectRoot importSrc with
| Ok _ -> Ok(visitedPaths)
| Error e -> Error(e)
2 changes: 1 addition & 1 deletion src/Escalier.Interop/Migrate.fs
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ module rec Migrate =
| None ->
// TODO: handle renaming named exports, e.g.
// export { setVerbosity as setLogVerbosity };
printfn $"namedExport = %A{namedExport}"
printfn "TODO: migrate named export without src"
[]
| ModuleDecl.ExportDefaultDecl _ ->
failwith "TODO: migrateModuleDecl - exportDefaultDecl"
Expand Down
Loading

0 comments on commit 3e212e7

Please sign in to comment.