Private NPM package in rescript with support for TypeScript and optionally vanilla JS

Hello rescript community!

I’m a rescript beginner but I have quite some experience with TypeScript (mostly Angular but also some React). I came here after being introduced to the language by an ex colleague and I thought to try it out in a small library to be consumed by my existing Angular app. And maybe then after that move on to writing React apps in rescript as well.

So the rescript app would be a GraphQL client that uses @reasonml-community/graphql-ppx to generate the necessary client code from a GQL schema that I will then wrap and expose an API to use by my Angular app. It will expose this API with TypeScript types.

So far I’ve checked out genType and that doesn’t quite do it. I have used yarn link to use my lib in my other project and even though it can find the *.gen.ts files (through import { SomeAPI_fetchSomeResource } from 'my-rescript-client/src/SomeAPI.gen') it isn’t bundled properly. This made me think that this setup usually is meant to be used when you have rescript and TypeScript code living in the same project.

Because of that, the examples I’ve seen so far hasn’t quite fit what I need, from what I understand. While they have used rescript inside their typescript projects but I am trying to expose an API through an NPM package. I will consume this library through GitHub though, so I will not publish it.

So, the goal is to add this library to my package.json of my Angular project and be able to install it from GitHub. It will expose a TypeScript API (preferably such that it just explains to TypeScript what the *.bs.js files are) that can be imported as import { ClientFactory, UserAPI } from 'my-rescript-client' or import { UserAPI } from 'my-rescript-client/user', or something like that. And down the road I’d want to also consume this in other projects, thus the approach of having a separate package for the client and being able to test the client separately from the view layer.

Would it be possible to accomplish this using esbuild to bundle up all the different modules (all the bs.js files) into an index file and then maintaining an index.d.ts that would somehow tie in with the generated types from genType to provide an accurate package API? Specifically the TypeScript versions of the GraphQL inputs and outputs.

I have very little experience with writing NPM packages from scratch so that’s a topic I’ll be researching separately, but I’m asking here to see if somebody has experience with the setup in general, whether or not the bundling is done with esbuild or something else.

Hi, to be honest, that sounds overly complicated :slight_smile: IMHO, to use a GraphQL client in a TypeScript app, just generate it directly for TypeScript. If you have an actual ReScript app, then use the ReScript PPX and generate the client. They will integrate much better and provide the ReScript level of type safety. It just won’t be the same if you generate a ReScript GQL client, then export it and use it from TypeScript. Much better to do that directly.

While on the surface I agree with @yawaramin, the graphql ppx is most useful inside the rescript project, this overall scenario still seems like something that should work. The .gen.ts files import the .bs.js files that are next to them, so there shouldn’t be a problem with having all the files it needs.

What errors are generated in the Angular app? Can you post a small sample of the ReScript project to a public repo?

Hi, to be honest, that sounds overly complicated …

Sure, I understand that it sounds that way to you. But there are reasons for me landing in this conclusion and that’s besides the point of this thread. I am not satisfied with the options on the TypeScript side of things (Apollo and urql…?) for various reasons.

If you have an actual ReScript app, then use the ReScript PPX and generate the client.

As I mentioned, this is meant to be a little gateway project to see if I will consider ReScript for React down the road. So there’s that too.

It just won’t be the same if you generate a ReScript GQL client, then export it and use it from TypeScript. Much better to do that directly.

Given the reasoning above about resting ReScript out by consuming this is a package from Angular, why not? Can you give me some examples on how it won’t be the same? Other than it would be better to write it all in one language because if I could I would.

While on the surface I agree with @yawaramin, the graphql ppx is most useful inside the rescript project, this overall scenario still seems like something that should work. The .gen.ts files import the .bs.js files that are next to them, so there shouldn’t be a problem with having all the files it needs.

As far as my Angular project was concerned, it could find the generated TypeScript files. It however could not import them. The error is an import/bundling issue in webpack.

The error I get from yarn start in my iTerm:

$ yarn start
...
./src/app/core/services/user.service.ts:1:0-71 - Error: Module not found: Error: Package path ./src/UserApi.gen is not exported from package /<project-dir>/angular/node_modules/lpe-rescript-client (see exports field in /<project-dir>/angular/node_modules/lpe-rescript-client/package.json)

This error is new, since it does not match the error I had before. This had to do with me trying to explicitly state which files to export in the package’s package.json.

The other error (that I think is about bundling/importing) is this one:

ERROR TypeError: (0 , lpe_rescript_client_src_UserApi_gen__WEBPACK_IMPORTED_MODULE_0__.fetchCurrentUser) is not a function
    at UserService.getUser (main.js:sourcemap:381:94)
    at HomeComponent.ngOnInit (main.js:sourcemap:308:27)

So this webpack error is why I feel like what I need to figure out is how to setup the package.json and the build chain in the package to produce some kind of bundled version of all the files, and an index.d.ts to go with it. It would also help with the the fact that currently importing anything from the package is done like this: import { fetchCurrentUser } from 'lpe-client/src/UserApi.gen'. This is not what I want.

Got any ideas on how to approach this one? Any packages I’ve maintained before has either already been setup or were automatically generated with Angular’s ng new, which I don’t think is applicable here. I’m looking to use esbuild if I need to build and bundle something instead of webpack as well. I’ve used esbuild as part of an Elixir Phoenix LiveView bundling thing and it worked great, but it is the NPM specific parts that I ain’t got covered right now.

Well, apart from the fact that it’s a super niche setup that seems like it will break easily (as seen from the errors you are getting), two reasons:

  1. One of the main value propositions of GraphQL is the flexibility it gives clients. You are able to adjust the query quickly and easily to get exactly the data you need. If you put the generated client in a separate package this adds unnecessary friction to your workflow and will be a frustrating experience to work with in future.
  2. Take a look at an example ReScript client usage here: https://github.com/jeddeloh/rescript-apollo-client/blob/master/EXAMPLES/src/docs/Docs.res
module TodosQuery = %graphql(`
    query TodosQuery {
      todos: allTodos {
        id
        text
        completed
      }
    }
  `)

module Basics = {
  @react.component
  let make = () =>
    switch TodosQuery.use() {
    | {loading: true} => "Loading..."->React.string
    | {error: Some(_error)} => "Error loading data"->React.string
    | {data: Some({todos})} =>
      <div>
        {"There are "->React.string} {todos->Belt.Array.length->React.int} {" To-Dos"->React.string}
      </div>
    | {data: None, error: None, loading: false} =>
      "I hope this is impossible, but sometimes it's not!"->React.string
    }
}

After getting exported by genType, how would the query be used? It would look something like this:

const result = TodosQuery.use()
if (result.error != null) ...handle error...
if (result.loading) ...show spinner...
if (result.data != null) ...show data...

In other words, the ReScript pattern matching benefit is gone. TypeScript supports a limited form of exhaustiveness checking, but only when the object has a specific tag field. ReScript’s checking is much more powerful, it would tell you if you were to miss checking some combination of the {loading, error, data} record.

It’s possible to achieve the same with typescript, calling exhaustiveCheck(result ) in default case.

function exhaustiveCheck( param: never ) { }

It doesn’t really check it in the same way that ReScript can. E.g. look at this example. Forget sophisticated exchaustiveness checking for a second, TypeScript is erroring because there’s no final return statement even though all branches of the function end in a return statement.

But there is no else, so not all branches end in a return statement? So I’m not sure what you mean. Or am I missing something here?

You have one if without any other branches, and it proceeds to an if which is coupled with an else if, but there’s no else. So of course it is missing one?

Anyway, this doesn’t worry me. I don’t see the problem.

If you can produce a GitHub repo with this problem for me so I can actually see and feel it, then I have a higher chance of understanding the issue.

I feel like this is taking away from what I’m trying to talk about and instead focuses on the wrong topic. And all you’ll accomplish is to discourage me from ever trying ReScript in this case. I’m doing this as an experiment to try it out. If not I will just find another option that solves my problem and ReScript will probably never pass me by again as I don’t like webdev and do my best to pick the things that interest me and give me the most value (ReScript being a candidate here). So far those things has been Angular + TypeScript, but that’s mostly due to experience with both, but I’m open to changing my mind. It’s just I will not dive in without testing the waters. That seems sane, right?

And I know all the hazards of mixing languages. I’ve mixed ObjectiveC and Swift, UIKit and SwiftUI, Java And Kotlin, Jetpack Compose with… whatever the first ui framework on Android is called? I know the hazards and I’ve contemplated it all. So I’d like to focus on the actual question of the problem here :slight_smile: I do appreciate the heads up though!

This I have considered, and I understand. But it is a very small project and for the scope it is totally fine.

Again, I’ve thought about this for a while and weighed options.