Skip to content
This repository was archived by the owner on Jun 15, 2023. It is now read-only.

Implement new syntax for importing values from JavaScript modules #217

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add support for JS import declarations in interface files.
  • Loading branch information
Iwan committed Jan 3, 2021
commit cadca0405a170c0b2af1d0108c38ff1d7a09b90d
120 changes: 71 additions & 49 deletions src/res_core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5156,9 +5156,24 @@ and parseStructureItemRegion p =
let loc = mkLoc startPos p.prevEndPos in
Some (Ast_helper.Str.primitive ~loc externalDef)
| Import ->
let structureItem = parseJsImportDeclaration ~startPos ~attrs p in
let valueDescriptions = parseJsImportDeclaration ~startPos ~attrs p in
parseNewlineOrSemicolonStructure p;
let loc = mkLoc startPos p.prevEndPos in
let structureItem =
let primitives =
List.map (fun valueDescription ->
Ast_helper.Str.primitive ~loc:valueDescription.Parsetree.pval_loc valueDescription
) valueDescriptions
in
match primitives with
| [oneExternal] -> oneExternal
| clause ->
let attrs = (Location.mknoloc "ns.jsFfi", Parsetree.PStr [])::attrs in
clause
|> Ast_helper.Mod.structure ~loc
|> Ast_helper.Incl.mk ~attrs ~loc
|> Ast_helper.Str.include_ ~loc
in
Some {structureItem with pstr_loc = loc}
| Exception ->
let exceptionDef = parseExceptionDef ~attrs p in
Expand Down Expand Up @@ -5213,56 +5228,44 @@ and parseStructureItemRegion p =
end

(* import ImportClause FromClause *)
and parseJsImportDeclaration ~startPos ~attrs p =
and parseJsImportDeclaration ~startPos:_startPos ~attrs:_ p =
Parser.expect Token.Import p;
let importJsClause = parseJsImportClause p in
let fromClause = parseFromClause p in
let loc = mkLoc startPos p.prevEndPos in
(* let attrs = (Location.mknoloc "ns.jsFfi", Parsetree.PStr [])::attrs in *)
let clause =
List.map (fun jsImport ->
let valueDescription = match jsImport with
(* import schoolName: string from '/modules/school.js' *)
| DefaultImport {attrs; name; alias; typ} ->
(* module("/modules/school.js") external schoolName: string = "default" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(fromClause::attrs) ~prim:[alias] name typ

(* import * as leftPad: (string, int) => string from "left-pad" *)
| NameSpaceImport {attrs; name; typ} ->
let (_, moduleSpecifier) = fromClause in
let jsModuleName = match (moduleSpecifier: Parsetree.payload) with
(* extract "left-pad"*)
| PStr [
{Parsetree.pstr_desc = Pstr_eval (
{pexp_desc = Pexp_constant (Pconst_string (jsModuleName, None))},
_
)}
] -> jsModuleName
| _ -> ""
in
let moduleAttribute = (Location.mknoloc "module", Parsetree.PStr []) in
(* @module external leftPad: (string, int) => string = "left-pad" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(moduleAttribute::attrs) ~prim:[jsModuleName] name typ

(* import {dirname: string => string} from "path" *)
| NamedImport {attrs; name; alias; typ} ->
(* module("path") external dirname: (string, string) => string = "dirname" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(fromClause::attrs) ~prim:[alias] name typ
List.map (fun jsImport ->
let valueDescription = match jsImport with
(* import schoolName: string from '/modules/school.js' *)
| DefaultImport {attrs; name; alias; typ} ->
(* module("/modules/school.js") external schoolName: string = "default" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(fromClause::attrs) ~prim:[alias] name typ

(* import * as leftPad: (string, int) => string from "left-pad" *)
| NameSpaceImport {attrs; name; typ} ->
let (_, moduleSpecifier) = fromClause in
let jsModuleName = match (moduleSpecifier: Parsetree.payload) with
(* extract "left-pad"*)
| PStr [
{Parsetree.pstr_desc = Pstr_eval (
{pexp_desc = Pexp_constant (Pconst_string (jsModuleName, None))},
_
)}
] -> jsModuleName
| _ -> ""
in
(* TODO: nicer loc on primitive? *)
Ast_helper.Str.primitive ~loc:valueDescription.pval_loc valueDescription
) importJsClause
in match clause with
| [oneExternal] -> oneExternal
| clause ->
let attrs = (Location.mknoloc "ns.jsFfi", Parsetree.PStr [])::attrs in
clause
|> Ast_helper.Mod.structure ~loc
|> Ast_helper.Incl.mk ~attrs ~loc
|> Ast_helper.Str.include_ ~loc
let moduleAttribute = (Location.mknoloc "module", Parsetree.PStr []) in
(* @module external leftPad: (string, int) => string = "left-pad" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(moduleAttribute::attrs) ~prim:[jsModuleName] name typ

(* import {dirname: string => string} from "path" *)
| NamedImport {attrs; name; alias; typ} ->
(* module("path") external dirname: (string, string) => string = "dirname" *)
Ast_helper.Val.mk ~loc:name.loc
~attrs:(fromClause::attrs) ~prim:[alias] name typ
in
valueDescription
) importJsClause

(*
* ImportClause ::=
Expand Down Expand Up @@ -6131,8 +6134,27 @@ and parseSignatureItemRegion p =
let loc = mkLoc startPos p.prevEndPos in
Some (Ast_helper.Sig.extension ~attrs ~loc extension)
| Import ->
Parser.next p;
parseSignatureItemRegion p
let valueDescriptions = parseJsImportDeclaration ~startPos ~attrs p in
parseNewlineOrSemicolonStructure p;
let loc = mkLoc startPos p.prevEndPos in
let signatureItem =
let primitives =
List.map (fun valueDescription ->
Ast_helper.Sig.value ~loc:valueDescription.Parsetree.pval_loc valueDescription
) valueDescriptions
in
match primitives with
| [oneExternal] -> oneExternal
| clause ->
let attrs = (Location.mknoloc "ns.jsFfi", Parsetree.PStr [])::attrs in
clause
|> Ast_helper.Mty.signature ~loc
|> Ast_helper.Incl.mk ~attrs ~loc
|> Ast_helper.Sig.include_ ~loc
in
Some {signatureItem with psig_loc = loc}
(* Parser.next p; *)
(* parseSignatureItemRegion p *)
| _ ->
begin match attrs with
| (({Asttypes.loc = attrLoc}, _) as attr)::_ ->
Expand Down
14 changes: 14 additions & 0 deletions src/res_parsetree_viewer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,20 @@ let extractValueDescriptionFromModExpr modExpr =
| Pmod_structure structure -> loop structure []
| _ -> []

let extractValueDescriptionFromModType modType =
let rec loop signature acc =
match signature with
| [] -> List.rev acc
| signatureItem::signature ->
begin match signatureItem.Parsetree.psig_desc with
| Psig_value vd -> loop signature (vd::acc)
| _ -> loop signature acc
end
in
match modType.pmty_desc with
| Pmty_signature signature -> loop signature []
| _ -> []

type jsModuleFlavour =
(* import ceo: string from "company" *)
| JsDefaultImport of string
Expand Down
1 change: 1 addition & 0 deletions src/res_parsetree_viewer.mli
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ val isBracedExpr : Parsetree.expression -> bool
val isSinglePipeExpr : Parsetree.expression -> bool

val extractValueDescriptionFromModExpr: Parsetree.module_expr -> Parsetree.value_description list
val extractValueDescriptionFromModType: Parsetree.module_type -> Parsetree.value_description list

type jsModuleFlavour =
(* import ceo: string from "company" *)
Expand Down
25 changes: 20 additions & 5 deletions src/res_printer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -910,11 +910,26 @@ and printOpenDescription (openDescription : Parsetree.open_description) cmtTbl =
]

and printIncludeDescription (includeDescription: Parsetree.include_description) cmtTbl =
Doc.concat [
printAttributes includeDescription.pincl_attributes cmtTbl;
Doc.text "include ";
printModType includeDescription.pincl_mod cmtTbl;
]
let isJsFfiImport = List.exists (fun attr -> match attr with
| ({Location.txt = "ns.jsFfi"}, _) -> true
| _ -> false
) includeDescription.pincl_attributes
in
if isJsFfiImport then
let attrs = List.filter (fun attr ->
match attr with
| ({Location.txt = "ns.jsFfi"}, _) -> false
| _ -> true
) includeDescription.pincl_attributes
in
let imports = ParsetreeViewer.extractValueDescriptionFromModType includeDescription.pincl_mod in
printJsFfiImportDeclaration ~attrs ~imports cmtTbl
else
Doc.concat [
printAttributes includeDescription.pincl_attributes cmtTbl;
Doc.text "include ";
printModType includeDescription.pincl_mod cmtTbl;
]

and printIncludeDeclaration (includeDeclaration : Parsetree.include_declaration) cmtTbl =
let isJsFfiImport = List.exists (fun attr ->
Expand Down
168 changes: 168 additions & 0 deletions tests/printer/other/__snapshots__/render.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,174 @@ import * as copyToClipboard: string => unit from \\"copy-to-clipboard\\"
"
`;

exports[`import.resi 1`] = `
"// Import a single export from a module
import {dirname: string => string} from \\"path\\"

// old external
import {dirname: string => string} from \\"path\\"

// Import multiple exports from a module
import {
delimiter: string,
dirname: string => string,
relative: (~from: string, ~\\\\\\"to\\": string) => string,
} from \\"path\\"

// old external
import {delimiter: string} from \\"path\\"
import {dirname: (string, string) => string} from \\"path\\"
import {relative: (~from: string, ~\\\\\\"to\\": string) => string} from \\"path\\"

// Import an export with a more convenient alias
import {renameSync as rename: (string, string) => string} from \\"fs\\"

import {renameSync as rename: (string, string) => string} from \\"fs\\"

// import an export with an illegal ReScript identifier
import {\\"Fragment\\" as make: component<{\\"children\\": element}>} from \\"react\\"

// old external
import {\\"Fragment\\" as make: component<{\\"children\\": element}>} from \\"react\\"

// import an export with additional attributes
import {@splice join: array<string> => string} from \\"path\\"

// old external
import {
@splice
join: array<string> => string,
} from \\"path\\"

// Rename multiple exports during import
import {
renameSync as rename: (string, string) => string,
unlinkSync as unlink: string => unit,
} from \\"fs\\"

// old external
import {renameSync as rename: (string, string) => string} from \\"path\\"
import {unlinkSync as unlink: string => unit} from \\"path\\"

// import an entire module's contents (aka NameSpacedImport)
import * as leftPad: (string, int) => string from \\"leftPad\\"

// old external
import * as leftPad: (string, int) => string from \\"leftPad\\"

// Importing default export
import schoolName: string from \\"/modules/school.js\\"
// syntactic sugar for
import schoolName: string from \\"/modules/school.js\\"

// old external
import schoolName: string from \\"/modules/school.js\\"

// importing default export with named imports
import schoolName: string, {getGrade: student => int} from \\"/modules/school.js\\"

// old external
import schoolName: string from \\"/modules/school.js\\"
import {getGrade: student => int} from \\"/modules/school.js\\"

// importing default export with multiple named imports
import
schoolName: string,
{
getGrade: student => int,
getTeachers: unit => array<teacher>
}
from \\"/modules/school.js\\"

// old external
import schoolName: string from \\"/modules/school.js\\"
import {getGrade: student => int} from \\"/modules/school.js\\"
import {getTeachers: unit => array<teacher>} from \\"/modules/school.js\\"

// import values from JavaScript modules with @as and polyvariants
import {
openSync: (
path,
@bs.string
[
| @bs.as(\\"r\\") #Read
| @bs.as(\\"r+\\") #Read_write
| @bs.as(\\"rs+\\") #Read_write_sync
| @bs.as(\\"w\\") #Write
| @bs.as(\\"wx\\") #Write_fail_if_exists
| @bs.as(\\"w+\\") #Write_read
| @bs.as(\\"wx+\\") #Write_read_fail_if_exists
| @bs.as(\\"a\\") #Append
| @bs.as(\\"ax\\") #Append_fail_if_exists
| @bs.as(\\"a+\\") #Append_read
| @bs.as(\\"ax+\\") #Append_read_fail_if_exists
],
) => unit,
} from \\"fs\\"

// old external
import {
openSync: (
path,
@bs.string
[
| @bs.as(\\"r\\") #Read
| @bs.as(\\"r+\\") #Read_write
| @bs.as(\\"rs+\\") #Read_write_sync
| @bs.as(\\"w\\") #Write
| @bs.as(\\"wx\\") #Write_fail_if_exists
| @bs.as(\\"w+\\") #Write_read
| @bs.as(\\"wx+\\") #Write_read_fail_if_exists
| @bs.as(\\"a\\") #Append
| @bs.as(\\"ax\\") #Append_fail_if_exists
| @bs.as(\\"a+\\") #Append_read
| @bs.as(\\"ax+\\") #Append_read_fail_if_exists
],
) => unit,
} from \\"fs\\"

// import a react component from a module
import {
@react.component
\\"Circle\\" as make: (
~center: Leaflet.point,
~radius: int,
~opacity: float,
~stroke: bool,
~color: string,
~children: React.element=?,
) => React.element,
} from \\"react-leaflet\\"

module type JsComponent = {
import
@react.component
make: (
~id: string=?,
~onChange: Js.Nullable.t<Js.Date.t> => unit,
~selected: Js.Date.t=?,
) => React.element
from \\"./DatePicker\\"
}

import @attr * as leftPad: (string, int) => string from \\"leftPad\\"
import @attr * as leftPad: (string, int) => string from \\"leftPad\\"

import
@react.component
make: (
~id: string=?,
~onChange: Js.Nullable.t<Js.Date.t> => unit,
~selected: Js.Date.t=?,
) => React.element,
@attr
* as dateLib: 'a
from \\"./DatePicker\\"

import * as copyToClipboard: string => unit from \\"copy-to-clipboard\\"
"
`;

exports[`lor.js 1`] = `
"let lower = ch => lor(32, ch)
"
Expand Down
Loading