sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.78k stars 152 forks source link

ESM Support #230

Closed stuft2 closed 2 years ago

stuft2 commented 2 years ago

@sinclairzx81 Do you have plans to support ESM modules? I'd be happy to work on a PR if you'd like.

sinclairzx81 commented 2 years ago

@stuft2 Hi. TypeBox currently offers ESM support via esm.sh for Deno and Browser environments. You can pull TypeBox ESM modules in the following way.

<html>
    <script type="module">
        import { TypeCompiler } from 'https://esm.sh/@sinclair/typebox/compiler'
        import { Type } from 'https://esm.sh/@sinclair/typebox'

        const T = Type.Object({
            x: Type.Number(),
            y: Type.Number(),
            z: Type.Number()
        })

        const C = TypeCompiler.Compile(T)

        console.log(C.Code())

        console.log(C.Check({ x: 1, y: 2, z: 3 }))

    </script> 

</html>

However, TypeBox currently publishes NPM modules in CJS format to help reduce friction for Node devs trying to make the switch from legacy CJS over to ESM (as Node ESM supports both CJS require and import). The thinking here is that because CJS is supported by both legacy and ESM versions of Node, targeting CJS helps to reduce friction when getting setup in either environment, but this is just an assumption on my part.

Did you have any thoughts on approaching ESM publishing while retaining CJS for legacy users?

jessekrubin commented 2 years ago

@sinclairzx81 you might try bundling with tsup? I have only ever had a good time using tsup. https://github.com/egoist/tsup

sinclairzx81 commented 2 years ago

@stuft2 Thanks for the suggestion on ESM, but I think I'm going to close off this issue for now. The current proxied ESM setup via esm.sh is serviceable (though admittingly not entirely ideal), and the current CJS publishing to NPM should service legacy and newer .mjs / .mts Node ESM setups.

Would be happy to discuss further if you had some thoughts on perhaps publishing both ESM/CJS to NPM (I think a couple of libraries may do this), would be interested to hear what the standard way of approaching this is. If you have any thoughts of best practice in this regard, happy to discuss more.

But for now, will close. Many Thanks! S

stuft2 commented 2 years ago

@sinclairzx81 Thanks for your response. I might approach distributing ESM files along side the CJS files by specifying a different entrypoint for each in the package.json file. Webpack searches the package.json for entrypoints browser, module, main (in that order) when the target is web (or unspecified) and for module and then main when the target is set to node. See resolve.mainFields in the docs for more details.

Only the browser property is documented in the npm docs on package.json files. However the module property is used by major players in the industry such as the already mentioned webpack and the javascript aws-sdk.

The AWS SDK Javascript packages are an example of how I would go about distributing both ESM and CJS files simultaneously. Basically, you can specify both a main and a module property in the package.json file and point to the different entrypoints for the CJS and ESM files respectively. Then split the tsconfig into two: one targeting CJS and the other ESM. Finally, adding a build script that runs each build concurrently might be helpful but it isn't strictly necessary.

stuft2 commented 1 year ago

@sinclairzx81 I've been reading up more on ESM and CJS differences recently and ran into this nice tidbit from the TypeScript documentation.

TLDR; Separate entry-points for CommonJS and ESM is supported in Node's package.json file with the new exports property.

Node.js supports a new field for defining entry points in package.json called "exports". This field is a more powerful alternative to defining "main" in package.json, and can control what parts of your package are exposed to consumers.

Here’s an package.json that supports separate entry-points for CommonJS and ESM:

{
    "name": "my-package",
    "type": "module",
    "exports": {
        ".": {
            // Entry-point for `import "my-package"` in ESM
            "import": "./esm/index.js",
            // Entry-point for `require("my-package") in CJS
            "require": "./commonjs/index.cjs",
        },
    },
    // CJS fall-back for older versions of Node.js
    "main": "./commonjs/index.cjs",
}

Source: https://www.typescriptlang.org/docs/handbook/esm-node.html#packagejson-exports-imports-and-self-referencing

sinclairzx81 commented 1 year ago

@stuft2 Hi, thanks for the reference links. I will actually take another look at this in future

At this stage though, I'm fairly happy with the current CJS publishing as this is supported by both Node CJS and Node ESM (although I would be very interested to hear of any problems Node ESM users were experiencing with the current published package). However, in lieu of such problems, there doesn't seem like there's much urgency to provide both CJS and ESM at this time.

I am however quite interested in replacing esm.sh as the recommended way to pull through packages for ESM capable / non NPM dependent platforms (such as Deno). As Deno will be supporting the npm: import specifiers in later releases, I expect once that functionality becomes stable, It will be around then I'll take another look at ESM publishing.

In a perfect world, I wish I could just publish ESM only and have everything just work (such is the state of JavaScript module systems, legacy or otherwise :( )

Thanks again for those links and references!

sinclairzx81 commented 1 year ago

@stuft2 Hi,

I've actually updated TypeBox's package.json inline with your suggestion to use exports. This update was made mostly for the benefit of Deno which supports pulling NPM modules via npm: import protocol specifiers. So, it should be possible to import TypeBox in Deno in the following way with the --unstable flag.

import { Value } from `npm:@sinclair/typebox/value` // requires package.json 'exports' for submodule discovery
import { Type } from `npm:@sinclair/typebox`

I'm still holding out on ESM compiled modules as to avoid multiple build targets being published to NPM (and because most downstream tooling seems to be able to understand CJS modules embedded in ESM). I'm still interested to hear if there are users experiencing problems using TypeBox CJS specifically in ESM environments.

Cheers!

jfrconley commented 1 year ago

@sinclairzx81

I'm still interested to hear if there are users experiencing problems using TypeBox CJS specifically in ESM environments.

An issue we have run into is increased bundle size for our backend code. We use esbuild to bundle our typescript lambdas, and the bundle currently contains every part of each imported module from typebox. Not sure exactly how big the difference would be, but I don't think the conversion is that hard. My experience has been it as simple as building natively for esm, then providing an esbuild bundled .cjs module with the exports mapping. Has been simple and effective.

I'm open to providing a PR, if you are amenable