Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I configure a TypeScript project that uses JavaScript modules compiled from PureScript?

TL;DR I want to create TypeScript typings for compiled PureScript modules, and distribute them in my npm package. I'm more than happy to maintain these typings manually, I just can't figure out what I need to put in tsconfig.json (up and downstream) and package.json.


I have a project where the core functionality is implemented in PureScript with a TypeScript CLI, and all of this is ultimately distributed as JavaScript via npm. I've created a similar project with a simpler layout:

.
├── cli                   # TypeScript CLI project
│   ├── main.ts
│   └── tsconfig.json
│
├── output                # Compiled PureScript modules
│   └── People
│       ├── externs.json
│       └── index.js
│
├── src                   # PureScript source
│   └── People.purs
│
└── types                 # TypeScript types for PureScript modules
    └── People.d.ts

In src/People.purs I define the core functionality:

module People where

type Person = { name :: String }

david :: Person
david = { name: "David "}

In types/People.d.ts I define TypeScript types for PureScript modules so I can safely use them from TypeScript:

declare module "People" {
    export interface Person {
        name: string;
    }

    export const david: Person;
}

Finally, in cli/main.ts, I want to import the compiled PureScript modules with the TypeScript types I've defined:

import * as People from "People";

console.log(People.david);

I can get this to build, but when I try to run the ultimate JavaScript (node cli/main.js), the require calls generated by tsc fail because they don't use absolute paths--require("People") should be require("./People") in this case. Making the import statements in TypeScript relative disassociates the typings.

I'm having a hard time meeting these goals:

  • Maintain TypeScript typings for all PureScript modules imported from TypeScript.
  • Ensure that TypeScript typechecks.
  • Ensure that require calls for PureScript modules resolve at runtime.
  • Distribute TypeScript typings with npm package, so that TypeScript consumers can use these typings as well.

If you have experience requiring PureScript modules from TypeScript, and distributing all of this via npm, I would really appreciate some guidance!

like image 694
David Siegel Avatar asked Sep 30 '17 18:09

David Siegel


2 Answers

Your Purescript module is called "Person", but should be "People". I think with relative path importing you might get partially there, but i don't think there's such thing as relative path typing in d.ts files.

I checked out your project and renamed the module, then I set allowJs to true and typed the module inline:

import * as OutputPeople from "../output/People";

type Person = {
  name: string;
};

type PeopleModule = {
  david: Person
}

const People: PeopleModule = OutputPeople

console.log(People.david);

I put my modifications up on a branch here: https://github.com/justinwoo/ps-js-ts-interop/commit/fc7c1239f9d1bac1cf2bea35f7a07d30c46a5f64

like image 178
kakigoori Avatar answered Oct 22 '22 06:10

kakigoori


If you definitely need to use the absolute path to require your PureScript packages (e.g. import * as People from 'People'), I think you will need to hack around the node module resolution algorithm (by placing your PureScript output in a node_modules subdirectory of ./output, for example).

But if that is not as important and you're fine with relative imports (like import * as People from './People'), you can do the following:

  1. Use relative imports for PureScript modules:

    cli/main.ts:

    import * as People from './People';
    
  2. Place your PureScript types side by side with your TypeScript sources:

    .
    ├── People
    │   └── index.d.ts
    ├── main.ts
    └── tsconfig.json
    
  3. Change your PureScript types to be local module declarations instead of global:

    cli/People/index.d.ts:

    export interface Person {
        name: string;
    }
    
    export const david: Person;
    
  4. Now, to make it work with Node module resolution, you need to place your JS output from both compilers in the same directory. We also want for TypeScript to emit type declarations for our TS files (to be consumable when published on npm). We're also removing the paths TS compiler option (no longer needed), and adding global type reference for Node types.

    cli/tsconfig.json:

    {
      "compilerOptions": {
        "target": "es5",
        "lib": ["es2015"],
        "moduleResolution": "node",
        "baseUrl": ".",
        "outDir": "./../output",
        "types": [
          "node"
        ],
        "declaration": true
      },
      "include": [
          "*.ts"
      ]
    }
    
  5. Almost done. Now, we need to make sure that type definitions for PureScript modules end up in the output directory to be available for consumption. TypeScript will not copy .d.ts files in its output when generating type definition output (since these were not created by the TS compiler) – we need to compensate for that. I don't know whether this is the best (or even a good?) way to achieve this, but the following worked for me:

    package.json:

    "scripts": {
      "build": "pulp build && tsc -P cli/tsconfig.json && cd cli && rsync -am --include='*.d.ts' -f 'hide,! */' . ./../output"
    },
    ...
    
  6. Last but not least, we need to specify the entry point for JS and for type definitions for npm consumers:

    package.json:

    "main": "output/main.js",
    "typings": "output/main.d.ts",
    "files": [
      "output/"
    ]
    ...
    

I believe this does not cover all the details of creating a mixed-language and mixed-type npm package, but hopefully this can serve as a starting point.

like image 40
MisterMetaphor Avatar answered Oct 22 '22 05:10

MisterMetaphor