feat: distribute the library for browser and node in ESM and CJS#313
feat: distribute the library for browser and node in ESM and CJS#313kettanaito merged 16 commits intomswjs:mainfrom cdimitroulas:feat/bundle-the-library
Conversation
@cdimitroulas, This is fantastic! While we're at it, folks, what is your opinion on #52? I think it would be great to be able to import interceptors directly from the root of the package like so: import { ClientRequestInterecptor } from '@mswjs/interceptors/ClientRequestInterceptor'This is possible via |
I think this should work everywhere that We may need to add fake folders/package.json for older versions of typescript and node to handle, but otherwise i think this is the path we should be on here. we should add a block like this to the package.json (once we configure both esm and cjs builds from tsup) then we'll probably also need a folder for each of these (empty, just with a package.json) |
|
@mattcosta7 I've added the "exports" field to package.json, please take a look and maybe also test out the build locally to see if it matches your expectations.
I think this should work fine based on the current build output ( |
|
@mattcosta7 @kettanaito when you have a moment I would appreciate if we could finalize some of these discussions so I can finalize the code and mark the PR as ready for review |
package.json
Outdated
| "import": "./lib/interceptors/ClientRequest/index.mjs", | ||
| "types": "./lib/interceptors/ClientRequest/index.d.ts" | ||
| }, | ||
| "./lib/interceptors/fetch": { |
There was a problem hiding this comment.
Should we drop ./lib/interceptors/ from the exports paths? So that you could import interceptors like so:
import X from '@mswjsw/interceptors/ClientRequest'The more we work on this, the more I think it needs to be a separate change. I wouldn't want to ask you to restructure all of your work, so if you feel it's already under substantial progress, we can ship this together with the remix-run change. If not, I think we're safer releasing the exports changes separately.
There was a problem hiding this comment.
I've moved it to a separate PR. Let's tackle that once we get the new bundling merged since I'm not sure if it depends on this work or not.
I've opened a draft PR on my fork just to keep a record of the work: https://github.com/cdimitroulas/interceptors/pull/1
There was a problem hiding this comment.
I don't think these changes are dependent. Thanks for moving it away 👍
|
Hey, @cdimitroulas. I was having a couple of busy weeks at work. I've updated all the discussions, summarizing them below:
I wouldn't want to introduce empty placeholder directories like @mattcosta7 mentions but I think we need that for full compatibility. Do you know if there's a support map for |
I'm not sure such a thing exists, I did find this helpful SO answer which is relevant: Not sure about bundler support but I imagine it's as you say and they all support it (at least on more recent versions) |
|
I've added Now, seems like all browser tests are failing because some Node code ends up in @cdimitroulas, do you have some suspicion as to why this happens? |
|
No idea, I'll try to figure it out |
|
It's not all the browser tests that are failing, the It seems that a chunk that is split off by the code-splitting that is used by both the This suggests that it's somehow related to |
|
It seems remix is configured to automatically deal with this stuff... https://remix.run/docs/en/v1/guides/constraints#server-code-pruning |
OOof hmm. I think since we're bundling this with a node target, we're probably only getting the node imports for remix-run/fetch? I wonder if we actually need a 'browser' build that's separate from the node build, which could potentially be a separate tsup config? the docs specificy - https://tsup.egoist.dev/#target-environment that target environment is node14 by default? do we need one that targets browsers to get the browser build of that library? (just guessing) |
|
I think the correct flag is esbuild's |
|
I'm going to see if there's a way to exclude |
|
I was able to get the tests to pass, but I'm not sure how the build output should look like when building for node/browser separately. Here's a gist showing the changes I made as I don't feel like this is something I want to commit here: |
This is a fantastic find.
This may be the case. We use I think we do need to ship Node and browser builds separately, which means that XMLHttpRequest and Fetch interceptors should be compiled to both Node (depends on web-fetch) and browser versions (doesn't need web-fetch, uses browser API). I believe a separate config is the way here, as we mustn't compile Something like this should work? import { defineConfig } from "tsup";
export default defineConfig([
{
name: "node",
entry: [
"src/index.ts",
"src/interceptors/ClientRequest",
"src/interceptors/XMLHttpRequest",
"src/interceptors/fetch",
],
platform: "node",
outDir: "./lib",
format: ["cjs", "esm"],
},
{
name: "browser",
entry: [
"src/index.ts",
"src/interceptors/XMLHttpRequest",
"src/interceptors/fetch",
],
platform: "browser",
outDir: "./lib",
format: ["cjs", "esm"],
},
]);The fact that we depend on // some-module.ts
// Imagine a module (e.g. an interceptor) that imports
// some fetch api utils.
import { Request } from './fetch-api'// fetch-api.ts
// Define those fetch api utils for Node.
// Here, depend on the polyfill.
export * from '@remix-run/web-fetch'// fetch-api.browser.ts
// Then, create a browser-side version of these utils,
// reusing the native browser API.
export {
Request,
Response,
// ...
}Lastly, instruct the code to import different {
"browser": {
"./lib/fetch-api.js": "./lib/fetch-api.browser.js"
},
}There may be a better way of doing this since I'm not a fan of re-exporting browser globals from // Reference globalThis.Request which is:
// A. A regular browser global if run in the browser;
// B. The same export from @remix-run/web-fetch in Node.js.
new Request('/foo')Maybe there are other options you have in mind, please share them. |
This sounds better and simpler to me than the alternative you proposed |
|
Unfortunately I don't think the config you proposed will work as both node/browser configs are outputting the same files to the same place (e.g. "lib/interceptors/fetch.js" is an output from both configs). |
|
@mattcosta7 I reordered the keys in the exports as per your suggestion |
|
@mattcosta7, thanks for the review! And @cdimitroulas, thank you so much for addressing those points so quickly! I've rebased this branch, and solved the package.json conflict. Will continue with testing in MSW, let's see if the order of those fields solved the issue for the IIFE build. |
|
@kettanaito did the latest changes fix the problems you saw when testing in MSW? |
|
Hey, @cdimitroulas. I haven't tested that yet. Planning on returning to open source this/next week, need to arrange a few other things. The changes to Interceptors look good from what I can gather. The order sensitivity that @mattcosta7 mentioned was a great addition to this, now to see what exactly gets bundled in when tsup builds into IIFE. For some reason, I suspect it doesn't respect the |
|
The IIFE integration test passes in MSW with these changes to Interceptors linked: 🎉 |
|
There are, however, failing unit tests because somewhere in Interceptors we rely on This happens because
"main": "./src/lib.js",
"module": "./src/lib.mjs",
"browser": "./src/lib.js",
"types": "./src/lib.d.ts",
"exports": {
".": {
"import": "./src/lib.mjs",
"require": "./src/lib.js"
}
},// src/lib.mjs
// In node `export { TextEncoder }` throws:
// "Export 'TextEncoder' is not defined in module"
// To workaround we first define constants and then export with as.
const Encoder = TextEncoder
const Decoder = TextDecoder
export { Encoder as TextEncoder, Decoder as TextDecoder }Those globals won't be defined in Jest. They are defined in Node.js. This may have something to do with how we require/bundle // lib.js
"use strict"
exports.TextEncoder =
typeof TextEncoder !== "undefined" ? TextEncoder : require("util").TextEncoder
exports.TextDecoder =
typeof TextDecoder !== "undefined" ? TextDecoder : require("util").TextDecoder
|
Solution to
|
Issue: Jest doesn't understand the
|
|
Jumping from Jest v26 -> v27 doesn't seem too bad https://jestjs.io/blog/2021/05/25/jest-27#features-coming-with-breaking-changes Jumping from v27 -> v28 requires working through this guide though |
|
I wonder if Jest 27 brings in ESM support... The references I find mention Jest 28 if not 29. All and all, this looks like an endless loop of testing setup update that I've been postponing for forever. I don't mind doing it in MSW's repo, it just takes time. Everything does. Working on that mswjs/msw#1394 |
|
I wonder if we just used |
offhand, i'm not sure of node support, would we need to lose backward support? (ie, would we only support versions where unidici already exists? and referenceable on globalThis?) the less we need to bundle, the better I think - so if so, it sounds worth exploring |
Eventually, yes! That's the plan for this autumn when Node 16 goes to pass and we only have the versions of Node that ship global fetch throughout the board. Until then, we need to resort to some polyfill to support Node 16. |
Trying this with the Fetch API branch✅ Build All the errors seem to have the same origin: they fail to resolve the "exports" to the interceptors. This is likely caused by Jest@26 missing support for "exports". But since we provide backward-compatibility with the |
kettanaito
left a comment
There was a problem hiding this comment.
@cdimitroulas @mattcosta7 thank you so much for all the work on this. I think the changes are good to go and I'm going to merge them once the CI passes. I still cannot fully test them in MSW due to the old Jest being used there but I've decided to slowly make our way towards that, as updating its test dependencies is a time-consuming endeavor and I don't wish to stall this change any longer.
Released: v0.21.0 🎉This has been released in v0.21.0! Make sure to always update to the latest version ( Predictable release automation by @ossjs/release. |
|
Is it just me, or does it appear that this change made the presets unreachable now? This sample code no longer works for importing the import { BatchInterceptor } from '@mswjs/interceptors'
import browserInterceptors from '@mswjs/interceptors/lib/presets/browser'
const interceptor = new BatchInterceptor({
name: 'my-interceptor',
interceptors: browserInterceptors,
})
interceptor.on('request', listener) |
|
Most probably yes @blaenk - we'll need to add these exports explicitly at the top-level somewhere I think and update the docs to reflect this. Do you have capacity to open a PR on that? |
|
Sorry, I don't have the bandwidth at the moment. For now I pinned to the previous release. |


re #283
Bundle the library, including
remix-runand other polyfill libs, so that consumers of the lib can consume it without having to bundle/transpile it themselves.