Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active September 20, 2025 14:48
Show Gist options
  • Select an option

  • Save markerikson/42d55b0fd970449fc5d84263f4775cbd to your computer and use it in GitHub Desktop.

Select an option

Save markerikson/42d55b0fd970449fc5d84263f4775cbd to your computer and use it in GitHub Desktop.

Revisions

  1. markerikson revised this gist Mar 18, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion tk-esm-ts-notes-2023-02-28.md
    Original file line number Diff line number Diff line change
    @@ -31,4 +31,4 @@
    - Mark: sounds like pre-bundling our own output is a good idea. Should also pre-bundle the TS types
    - Andrew: apparently `api-extractor` doesn't work with TS 5.0. Asked teammates, got conflicting answers on whether it's a dead project or not. May want to try a Rollup plugin? You may want to look at `tsup` in general - it does
    - Mark: RTKQ relies on multiple entry points. 1.9.x uses nested `package.json` files, tried adding `exports` subpaths, _seems_ to mostly work.
    - Mark: would be fantastic if we had more automated tools for checking ESM compat, like "here's my app code, generate and build a dozen projects with different tooling", or enhanced versions of `publint` and `wherearethetypes`
    - Mark: would be fantastic if we had more automated tools for checking ESM compat, like "here's my app code, generate and build a dozen projects with different tooling", or enhanced versions of `publint` and `arethetypeswrong`
  2. markerikson revised this gist Feb 28, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion tk-esm-ts-notes-2023-02-28.md
    Original file line number Diff line number Diff line change
    @@ -19,7 +19,7 @@
    - Mark: trying to remember why I chose to pre-bundle all these different bundle output formats in the first place
    - Mark: Did try switching to individual files briefly with `2.0-alpha`, but saw import errors with `type: "module"`, gave up and went back to bundling
    - Andrew: users want to use TSC for varous output formats. Node CJS is relatively loose for imports, ESM is strict and wants extensions. But writing with extensions doesn't always work - default import/exports can cause problems.
    - Andrew: amazing how fast ESM _syntax_ got adopted - added in ES2015, everyone used it immediately via Babel. Assumptions were made about behavior, and that's not what Node chose to do. Default imports can behave differently in Node CJS, ESM, and a bundler. TS needs to know about this because the _type_ will be different. So, it might be possible to write a file once, have TSC emit it, and have it work in both CJS/ESM. By bundling, you can get rid of a lot of those issues. So, bundling per entry point seems like a good idea.
    - Andrew: amazing how fast ESM _syntax_ got adopted - added in ES2015, everyone used it immediately via Babel. Assumptions were made about behavior, and that's not what Node chose to do. Default imports can behave differently in Node CJS, ESM, and a bundler. TS needs to know about this because the _type_ will be different. So, it might not be possible to write a file once, have TSC emit it, and have it work in both CJS/ESM. By bundling, you can get rid of a lot of those issues. So, bundling per entry point seems like a good idea.
    - Mark: supposedly shipping individual JS files is better for tree shaking, but anecdotally I don't see a problem with shipping bundles - our exports seem to get shaken okay
    - Andrew: if you _want_ to ship multiple individual files, you could run TSC for each separate module mode. Can't just type-check once, emit two different outputs - you'd really need to type-check _every_ time.
    - Andrew: can of course get into trouble with things like ESM-only dependencies
  3. markerikson revised this gist Feb 28, 2023. 2 changed files with 34 additions and 0 deletions.
    File renamed without changes.
    34 changes: 34 additions & 0 deletions tk-esm-ts-notes-2023-02-28.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    ## RTK ESM/TS Discussion with the TS Team

    ### Attendees

    - Mark Erikson
    - Andrew Branch

    ### Notes

    - Andrew: any opening questions?
    - Mark: _long_ rant about the state of package publishing and lack of public documentation
    - Andrew: yep. I worked on `moduleResolution: "bundler"`, and I have a private repo instrumenting a half-dozen bundlers to understand what's going on
    - Andrew: let's look at the RTK published output on `unpkg`:
    - Andrew: `2.0-alpha` does pre-bundling of files per output format
    - Andrew: it's important that import specifiers match between TS and JS. Node can't handle ESM files without an extension. So, TS has to make assumptions about what the _runtime_ will do.
    - Andrew: in this case, RTK's `.d.ts` files are actually a lie - the runtime part works fine because you've pre-bundled, but TS can't know that
    - Andrew: looking at transcript of discussion with Andarist, it may actually be a good idea to pre-bundle your `.d.ts` files too.
    - Andrew: the "don't change the module specifiers" issue is the deadest horse in the TS repo. But, at least in _this_ case for RTK, it's ESBuild that matters for consuming the imports. This is actually what `moduleResolution: "bundler"` (coming in TS 5.0)
    - Mark: trying to remember why I chose to pre-bundle all these different bundle output formats in the first place
    - Mark: Did try switching to individual files briefly with `2.0-alpha`, but saw import errors with `type: "module"`, gave up and went back to bundling
    - Andrew: users want to use TSC for varous output formats. Node CJS is relatively loose for imports, ESM is strict and wants extensions. But writing with extensions doesn't always work - default import/exports can cause problems.
    - Andrew: amazing how fast ESM _syntax_ got adopted - added in ES2015, everyone used it immediately via Babel. Assumptions were made about behavior, and that's not what Node chose to do. Default imports can behave differently in Node CJS, ESM, and a bundler. TS needs to know about this because the _type_ will be different. So, it might be possible to write a file once, have TSC emit it, and have it work in both CJS/ESM. By bundling, you can get rid of a lot of those issues. So, bundling per entry point seems like a good idea.
    - Mark: supposedly shipping individual JS files is better for tree shaking, but anecdotally I don't see a problem with shipping bundles - our exports seem to get shaken okay
    - Andrew: if you _want_ to ship multiple individual files, you could run TSC for each separate module mode. Can't just type-check once, emit two different outputs - you'd really need to type-check _every_ time.
    - Andrew: can of course get into trouble with things like ESM-only dependencies
    - Mark: we have to assume our code runs everywhere - browser, Node, etc
    - Andrew: been cringing every time I see advice about `type: "module"`
    - Andrew: There's nothing at the _package_ level that says "this is an ESM package". `type: "module"` does _only_ two things: tells Node how to interpret `.js` extensions, and also tells TS how to interpret `.d.ts`
    - Andrew: could ship `type: "module"`, every file with `.cjs` - all files are CJS, that's what your extensions said, so `type: "module"` did nothing. Could flip it, omit `type: "module"` and every file is `.mjs`, again that field didn't matter. So, either you specify the format via file extension, or use `type: "module"` to say how `.js` files are interpreted. TS interprets `.d.ts` files the same way, because the compiler has to know what kind of module the types represent.
    - Mark: actionable steps?
    - Mark: sounds like pre-bundling our own output is a good idea. Should also pre-bundle the TS types
    - Andrew: apparently `api-extractor` doesn't work with TS 5.0. Asked teammates, got conflicting answers on whether it's a dead project or not. May want to try a Rollup plugin? You may want to look at `tsup` in general - it does
    - Mark: RTKQ relies on multiple entry points. 1.9.x uses nested `package.json` files, tried adding `exports` subpaths, _seems_ to mostly work.
    - Mark: would be fantastic if we had more automated tools for checking ESM compat, like "here's my app code, generate and build a dozen projects with different tooling", or enhanced versions of `publint` and `wherearethetypes`
  4. markerikson created this gist Feb 27, 2023.
    51 changes: 51 additions & 0 deletions rtk-esm-ts-notes.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,51 @@
    ## RTK ESM/TS Discussion

    ### Attendees:
    - Nathan Bierema
    - Mateusz Burzynski
    - Mark Erikson

    ### Notes

    - Mateusz: what do you want besides "ship widely compat code?" What preferences?
    - Mark: don't care about `.mjs` as an output, really. Mostly just don't want to have to rewrite all imports to be `"createSlice.js"`
    - Mateusz: _Could_ do some changes like rewriting imports as a post-build script
    - Mateusz: TS team kinda caved and _will_ allow `.ts` extensions if you add a flag. Not sure what the output will be. Original reason for `.js` extensions was not wanting to transform source paths - "write what will be executed". So, in 5.0 they'll allow you to use `.ts`, question is whether they're going to rewrite that or not.
    - Mateusz: tried switching XState repo over, Jest had trouble with the `.js` extension due to mappings. Had to write a resolver plugin, and still had issues with `.jsx/tsx` too. So, not all tools might understand this depending on what the tool is looking for.
    - Mateusz: "what does it _mean_ to ship ESM today?"
    - Baseline: Node should be able to load files with `require()` and `import`, but you can even `import` CJS files (as long as it has named exports). Point is, you technically don't _have_ to ship ESM for node, it can get loaded, and by shipping _both_ formats you can run into the "dual-package hazard". (Possible to isolate shared code, but it has to be CJS. But, not every project suffers from that concern. Other issue is that files/deps _can_ get double-imported, like two copies of Lodash.) So, shipping dual packages _does_ make it more hazardous.
    - Mateusz: Probably _do_ want to ship ESM files for bundlers, and that's where "export conditions" comes in. `exports.module` mostly understood by bundlers. ESM technically "async", but irelevant with bundlers because they're in control of the module graph and flattening things anyway. Some tools _might_ even deduplicate `require/module` conditions? Not sure. Webpack 5 tries to be strict and follow Node semantics. So, by using `exports.module`, I as package author am being more clear about what I want tools to use.
    - Mateusz: So, technically, _don't_ have to ship ESM just to get Node to work. So, to a certain extent, shipping ESM is about "purity". For now, Emotion is sticking with `require`, and bundlers will load ESM files.
    - Mark: RTK ships pre-bundled versions in various formats (CJS/ESM/UMD). 1.9.x uses ESBuild+TSC (to compile to ES5), v2.0 sticks with `esnext` (although may transpile `package.module` to ES2017 for Webpack 4 parsing support). Redux core uses Rollup, other packages use Babel and output separate files.
    - Mateusz: TS doesn't even try to interpret `require()` as far as types. Lot of complexity around `.cdts` files, etc.
    - Mark: I'M LOST! :)
    - Mateusz: if you mark the whole package as `type: "module"`, it gets a lot harder to have types for CJS consumers
    - Mark: I'm fine removing `type: "module"`
    - Mateusz: yeah, most important thing is the `exports` conditions.
    - Mark: so the order of fields in `exports` matters?
    - Mateusz: yes, matched from top to bottom. So, put `types` first (mentioned in the TS docs)
    - Mateusz: Andrew Branch (TS team) prefers to use "sibling files", but that's hard because TSC and other tools might be outputting in different ames/locations.
    - Mateusz: you're using `typesVersions` over in Reselect, right? TS will ignore `typesVersions` if you supply `exports`, but supports an equivalent version selection _inside_ of `exports`.
    - Mark: we do stupid build shenanigans in Reselect with a renamed `package.json` to select between a nested type in TS 4.6 vs 4.7
    - Mateusz: this might not work with TS reading `exports`, TS might ignore it.
    - Mateusz: a lot of packages are misconfigured - they've started to use `exports`, but don't happen to change TS's `moduleResolution` field, so they don't see the issues
    - Mark: shipped `[email protected]` to switch from default export to only named; Reselect is only named. Would _prefer_ to avoid shipping a React-Redux major _just_ to alter packaging.
    - Mateusz: we did ship `exports` in an Emotion minor
    - Mark: everyone's warned me that shipping `exports` is a "breaking change"
    - Mateusz: we've decided that internal file structure is not a "public API". Yeah, some people can import from `dist` or `src`, but that's on them.
    - Mark: been trying to set up a battery of RTK example apps (CRA, Next, Vite, Parcel, RN, etc), and build+run them in CI. Maybe they should live in a separate repo so that we can run the jobs in _all_ the Redux repo CIs? (Would be _really_ nice if this was generalized into a SAAS somehow...)
    - Mateusz: yeah, hard to do because of all the different flags and options, and also if external dependencies come into play.
    - Mark: we've got 3 entry points, `@reduxjs/toolkit`, `@reduxjs/toolkit/query`, and `@reduxjs/toolkit/query/react`.
    - Mateusz: Yeah, Emotion has a couple as well. Nested `package.json` files is probably right. For `exports`, have multiple entries for `"."` and each sub-entry.
    - Mark: What actions should we take?
    - Mateusz: I think it's easiest to only ship ESM for bundlers, which avoids the dual-package issue.
    - Mateusz: I provide ESM files for bundlers (see Emotion files):
    - `exports.default` -> CJS
    - `exports.module` -> ESM
    - Can combine exports conditions via nesting
    - Mark: What about TS `"node16"`?
    - Mateusz: Put `"types"` condition before `"module"`
    - Mark: any advantage to rolling up TS typedefs into one file?
    - Mateusz: not really. Save a couple FS reads, but no benefit there.
    - Mark: is it worth switching to a premade tool like `preconstruct` or `tsup`?
    - Mateusz: `preconstruct` isn't being marketed. Smart wrapper around Rollup, doesn't allow customization. Can do autofixes. Ran into trouble when Node shipped ESM support, hard to support dual packages out of the box.