question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Configure package exports for browser build

See original GitHub issue

I’ve been trying out the browser build @antfu added recently.

There is a slight issue in that we only specify the following in package.json:

  "main": "dist/index.js",
  "module": "dist/index.esm.js",

which isn’t necessarily true. index.esm.js is valid ESM within nodejs. there is a separate index.browser.mjs which is valid ESM for browsers.

however, rollup and most other tools will resolve to the node one because of the module field in the package manifest.

im not entirely sure how we resolve this as thats the correct behaviour for node, but not for browsers.

its true we could just tell browser consumers to directly import index.browser.mjs but we then need to alter the typescript declarations somehow to strongly type imports from that path…

import {Highlighter} from 'shiki'; // works
import {Highlighter} from 'shiki/dist/index.browser.mjs'; // doesn't work, TS has no clue what this file is as there's no index.browser.d.ts

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:6 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
TheFedaikincommented, Jan 20, 2022

I’ve been trying out the browser build @antfu added recently.

There is a slight issue in that we only specify the following in package.json:

  "main": "dist/index.js",
  "module": "dist/index.esm.js",

This is a two part.

First is exports resolution which I couldn’t find docs for it in the browsers realm, but here are docs for node side of things. You can employ conditioning with exports which I will give you an example of at the end.

Second is typescript resolution - typescript doesn’t care about how files in your package are looking like in the file system. Generally it needs to find declaration files (d.ts files) for every single thing that it imports. You can stub that behavior with wildcard shims ie:

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

but that’s not going to work on the library end, because it would basically allow end user to import stuff that’s not here.

What you want to do on the library end is to do a clever hacking technique with usage of typesVersion that you can see here: https://github.com/microsoft/TypeScript/issues/33079#issuecomment-702617758 I can propose better package.json config so your typescript (and potentially browser) is going to be happier and better at resolution:

  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.esm.js",
      "browser": {
        /* I think this will work out of the box, 
          but I am not 100% sure it can be nested three times. 
          It definitely needs real-world testing. */
        "import": "./dist/index.browser.mjs",
        "default": "./dist/index.iife.js"
      },
      "default": "./dist/index.js"
    },
    "./browser": "./dist/index.browser.mjs"
  },
  "typesVersions": {
    "*": {
      ".": [
        "dist/index.d.ts"
      ],
      "browser": [
        "dist/index.d.ts"
      ]
    }
  },

This will allow you to do import in browser and if nested condition work in the ESM environment in browser it will grab correct file, and in other browser env it will default to iife. Alternatively you can do shiki/browser now that will for ensure you are importing browser.mjs file and correctly identify typescript typings because of typesVersions. You can add this locally and test for yourself in the monorepo, it actually correctly resolves typescript definitions. Hopefully that was helpful, but feel free to correct me on minor details.

I can also create PR for that change so people can control what kind of import they want and have better defaulting.

EDIT: spelling mostly

1reaction
43081jcommented, Mar 9, 2021

@octref the difference is interop. Node’s implementation of ESM comes with a boat load of interop & backwards compatibility with commonjs.

If we ever have a dependency anywhere in the dependency tree which is not ESM, it means the entire package (shiki) becomes incompatible with browsers. Meanwhile, Node will happily import it because of the interop layer their module system contains which understands how to import commonjs modules.

For example:

https://github.com/shikijs/shiki/blob/3c596f1c97f8a6af0ad672a9f83bea64f55959c5/packages/shiki/src/loader.ts#L1

This line in an ES module may correctly resolve via import maps (needed for bare specifiers), but the file it resolves to is this:

https://github.com/NeekSandhu/onigasm/blob/82c56c383334b0c502e805d8940e4b745b67eda2/package.json#L5

Which specifies only main, at this file:

https://unpkg.com/onigasm@2.2.5/lib/index.js

As you can see, it is a commonjs module and it doesn’t appear there is an es module equivalent.

At this point, the browser would fall over and complain that exports isn’t a thing (tried to set a property on undefined).

To get around this, there are basically two options:

  • Contribute back to onigasm the ability for them to publish an ES module we can pull in
  • Introduce a build step in shiki to bundle everything into one big module including dependencies (this is what has been done)

Obviously option 1 is the perfect place to be, but very difficult because the entire dependency tree has to be ESM. You would have to get onigasm, vscode-textmate and any of their dependencies to publish ESM before shiki can work in the browser “as is”.

Btw technically i think the browser bundle would also work in node.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Package exports - webpack
The exports field in the package.json of a package allows to declare which module should be used when using module requests like import...
Read more >
Node.JS (New) Package.json Exports Field | by Thomas Juster
The exports field (or “export map”) provides a way to expose your package modules for different environments and JavaScript flavors WHILE ...
Read more >
Modules: Packages | Node.js v19.3.0 Documentation
For new packages targeting the currently supported versions of Node.js, the "exports" field is recommended. For packages supporting Node.js 10 and below, the...
Read more >
How to universally export an NPM module (for the Browser)?
create a simplified module returnExport that has no dependency (function ... like Node. module.exports = factory(); } else { // Browser ...
Read more >
how to build modular applications with browserify - GitHub
Here is a handy configuration for using watchify and browserify with the package.json "scripts" field: { "build": "browserify browser.js -o ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found