BREAKING(shared): Revamp package to use subpaths#1898
Conversation
| return { | ||
| entry: ['./src/**/*.{ts,tsx}', '!./src/**/*.test.{ts,tsx}'], | ||
| format: ['cjs', 'esm'], | ||
| bundle: true, |
There was a problem hiding this comment.
We need to have .mjs file extensions in imports of .mjs files - there's no way around it.
And for that we need to enable bundle, see egoist/tsup#953
There's a papertrail of related issues for that:
There was a problem hiding this comment.
On a separate but related note, .mjs extensions used to break CRA apps and this is the reason we didn't follow the ESM spec completely up until now and also the reason we used legacyOutput here. I will need time to find old support requests for this, but I'm torn if this is something we want to treat as a breaking change or not at this point.
We need to discuss whether a build/bundle change on our side can be considered a breaking change. If the public API of our package has not changed, and the issue is caused by an outdated bundler on the user's side, I think we should consider this a non breaking change.
There was a problem hiding this comment.
Have we considered the approach outlined egoist/tsup#953 (comment)?
For that we'd need to add type: "module" to our package.json which could have larger implications. Because the workaround then renames CJS .js files to .cjs because .js will be treated as ESM by default then
There was a problem hiding this comment.
What I've seen from e.g. https://github.com/sindresorhus is that when they made packages ESM-only, it was a breaking change. Us adjusting the way how we output ESM here, I wouldn't consider that a breaking change.
Anyways, we're already publishing this as a new major, so it's fine?
| export { createWorkerTimers } from './workerTimers'; | ||
| export * from './browser'; | ||
| export * from './color'; | ||
| export * from './date'; | ||
| export * from './deprecated'; | ||
| export * from './error'; | ||
| export * from './file'; | ||
| export { handleValueOrFn } from './handleValueOrFn'; | ||
| export { isomorphicAtob } from './isomorphicAtob'; | ||
| export * from './keys'; | ||
| export { loadScript } from './loadScript'; | ||
| export { LocalStorageBroadcastChannel } from './localStorageBroadcastChannel'; | ||
| export * from './poller'; | ||
| export * from './proxy'; | ||
| export * from './underscore'; | ||
| export * from './url'; |
There was a problem hiding this comment.
❓ Why are we retaining the index file here if we're shifting everything to use the exported subpaths?
There was a problem hiding this comment.
(Also see the first bulletpoint in the the "Other changes" PR description)
With this PR I don't want to completely kill productivity for the sake of diminishing returns on optimizations. The purpose of the PR is to allow subpaths so that we (the engineers) can use them where appropriate.
If I'd remove this index file, some files would have 10+ imports without giving any actual benefit - because all these functions exported from this file here are fine to be not completely separated. They don't use any heavy libraries, they don't assume a completely different environment (like react). So they won't cause any problems if they might be analyzed before being tree-shaken.
Hope that makes sense :)
There was a problem hiding this comment.
With the proposed setup, is there any difference between * exports and named exports as far as tree-shaking is concerned?
There was a problem hiding this comment.
No, no difference. Important bit for this file is to not export certain stuff to keep it separate
| sourcemap: true, | ||
| legacyOutput: true, | ||
| external: ['@testing-library', 'react', 'jest', 'jest-environment-jsdom', 'react-dom'], | ||
| dts: true, |
There was a problem hiding this comment.
❓ Any idea how much slower this is than generating the declarations separately?
There was a problem hiding this comment.
tsup + tsc : npm run build 2.64s user 0.26s system 195% cpu 1.485 total
tsup : npm run build 4.53s user 0.60s system 184% cpu 3.486 total
So it is slower (egoist/tsup#945)
I'm fine with revisiting this is a follow-up, the relative speed loss is large, the absolute not so much (at least for this package)
There was a problem hiding this comment.
Thanks, agreed the absolute difference is reasonable 😃
| "moduleResolution": "NodeNext", | ||
| "module": "NodeNext", |
There was a problem hiding this comment.
See the PR description, section "Other changes"
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", |
There was a problem hiding this comment.
See the PR description, section "Other changes"
|
!snapshot |
|
Hey @LekoArts - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/backend@0.31.2-snapshot.vf9232b3 --save-exact
npm i @clerk/chrome-extension@0.4.9-snapshot.vf9232b3 --save-exact
npm i @clerk/clerk-js@4.62.1-snapshot.vf9232b3 --save-exact
npm i @clerk/clerk-expo@0.19.11-snapshot.vf9232b3 --save-exact
npm i @clerk/fastify@0.6.16-snapshot.vf9232b3 --save-exact
npm i gatsby-plugin-clerk@4.4.17-snapshot.vf9232b3 --save-exact
npm i @clerk/localizations@1.26.6-snapshot.vf9232b3 --save-exact
npm i @clerk/nextjs@4.25.6-snapshot.vf9232b3 --save-exact
npm i @clerk/clerk-react@4.26.6-snapshot.vf9232b3 --save-exact
npm i @clerk/remix@3.0.8-snapshot.vf9232b3 --save-exact
npm i @clerk/clerk-sdk-node@4.12.15-snapshot.vf9232b3 --save-exact
npm i @clerk/shared@1.0.0-snapshot.vf9232b3 --save-exact
npm i @clerk/types@3.56.1-snapshot.vf9232b3 --save-exact |
There was a problem hiding this comment.
🙃 rename files to packages/shared/src/workerTimers/types.ts and packages/shared/src/workerTimers/worker.ts since the workerTimers exists as folder in path
There was a problem hiding this comment.
We rely on the naming here:
I didn't want to break anything so I just kept it as-is :)
| "name": "@clerk/shared", | ||
| "version": "0.24.4", | ||
| "description": "Internal package utils used by the Clerk SDKs", | ||
| "types": "./dist/types/index.d.ts", |
There was a problem hiding this comment.
What about users still on previous TS versions? Are there types exported from clerk/shared that could affect our public types? Probably not, asking to make sure.
| "types": "./dist/types/index.d.ts", | ||
| "import": "./dist/esm/index.js", | ||
| "require": "./dist/cjs/index.js" | ||
| "import": { |
There was a problem hiding this comment.
Correct - sharing some extra context:
Older Clerk packages used ^ to reference other Clerk dependencies. We are not using pinned versions but we should be careful and make this a major bump as we don't want to break apps depending on a fixed-version Clerk package (in the user package.json), that in turns depends on clerk/shared@^0
| export { createWorkerTimers } from './workerTimers'; | ||
| export * from './browser'; | ||
| export * from './color'; | ||
| export * from './date'; | ||
| export * from './deprecated'; | ||
| export * from './error'; | ||
| export * from './file'; | ||
| export { handleValueOrFn } from './handleValueOrFn'; | ||
| export { isomorphicAtob } from './isomorphicAtob'; | ||
| export * from './keys'; | ||
| export { loadScript } from './loadScript'; | ||
| export { LocalStorageBroadcastChannel } from './localStorageBroadcastChannel'; | ||
| export * from './poller'; | ||
| export * from './proxy'; | ||
| export * from './underscore'; | ||
| export * from './url'; |
There was a problem hiding this comment.
With the proposed setup, is there any difference between * exports and named exports as far as tree-shaking is concerned?
| return { | ||
| entry: ['./src/**/*.{ts,tsx}', '!./src/**/*.test.{ts,tsx}'], | ||
| format: ['cjs', 'esm'], | ||
| bundle: true, |
| return { | ||
| entry: ['./src/**/*.{ts,tsx}', '!./src/**/*.test.{ts,tsx}'], | ||
| format: ['cjs', 'esm'], | ||
| bundle: true, |
There was a problem hiding this comment.
On a separate but related note, .mjs extensions used to break CRA apps and this is the reason we didn't follow the ESM spec completely up until now and also the reason we used legacyOutput here. I will need time to find old support requests for this, but I'm torn if this is something we want to treat as a breaking change or not at this point.
We need to discuss whether a build/bundle change on our side can be considered a breaking change. If the public API of our package has not changed, and the issue is caused by an outdated bundler on the user's side, I think we should consider this a non breaking change.
| outDir: './dist/cjs', | ||
| }; | ||
|
|
||
| return runAfterLast(['npm run build:declarations', shouldPublish && 'npm run publish:local'])(esm, cjs); |
There was a problem hiding this comment.
We're losing yalc publishing here but let's revisit it after this is released
There was a problem hiding this comment.
My assumption is that we'll move to secco sooner than later, so I removed it 😅
|
This PR has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Description
This is a bigger PR, so buckle up for this description and reviewing the code.
First, I've created two separate PRs that enabled this PR:
testUtilsfromsharedand cleaned up the output a bit alreadymoduleResolution: "Bundler"in this PROverview
Now onto what this PR this.
The TL;DR is: This PR enables module subpaths for
@clerk/shared. You'll be able to write this:You can read the Why section on why we do this change. This PR fixes #1883
Fixes SDK-509
This PR is deemed a breaking change because we're removing every React-related import from the main import, moving it to the
/reactsubpath.Other Changes
If you go through the changed files list, you'll see that in most files only the imports were changed, from
@clerk/sharedto@clerk/shared/<name>. You'll also notice that I didn't do this for every single module, sometimes I left a biggerimport { stuff } from "@clerk/shared"in there. My rule of thumb was to have maximum 3-4 separate imports, otherwise it gets a bit too wild. The separate imports are specifically for functions where we care about splitting them out, e.g.reactordeprecated.The next change you'll see often is inside
tsconfig.json. Most/all of our config files defined themoduleResolutionasnode. To quote:I also referenced the TSConfig Cheat Sheet a lot. I think in theory I could have changed most things to use
Bundlerbut I want to be cautious in this PR here and only changed it toNodeNext. This allows using module subpaths. When usingmoduleResolution: "NodeNext"you also need to setmoduletoNodeNext. I usedBundleronce as I was forced to use it, see GitHub inline comment.Sidenote: Our
tsconfig.jsonsetup is a mess, but I already created an internal ticket to clean that upAs smaller change I did: The functionality of
inClientSideandinBrowserwas the same. So I removedinClientSideand replaced its imports withinBrowserThe webpack configuration of
clerk-jsneeded an update, too. Since@clerk/sharedwill use.mjsfile extensions now, webpack needed an update for itsextensionsconfig and thets-loaderWhere appropriate, I changed
export * from ""toexport { stuff } from ""as this is clearer what gets exportedI removed the
sharedfile insidesdk-nodeas we now fully rely on@clerk/shared@clerk/sharedChangesSee GitHub inline comments
Why
Building a bundler is hard, so we shouldn't expect that they can all "automagically" handle the weirdness we live in right now: CJS + ESM, Barrel files, Tree-Shaking of optional parts, etc.
We can help bundlers/Node to decide what to parse, load, analyze, and bundle by defining module subpaths. By using them we can make
@clerk/sharedcompletely isomorphic by separating out specific code that is only relevant in certain contexts (e.g. React only in React context).Another benefit this now gives us is that we can shape the public API and force users/us into the right patterns. Since
reactisn't exported from the rootindexfile of@clerk/shared, you're forced to use@clerk/shared/reactinstead. So React is completely isolated from the rest of the package.Checklist
npm testruns as expected.npm run buildruns as expected.Type of change
Packages affected
@clerk/clerk-js@clerk/clerk-react@clerk/nextjs@clerk/remix@clerk/types@clerk/themes@clerk/localizations@clerk/clerk-expo@clerk/backend@clerk/clerk-sdk-node@clerk/shared@clerk/fastify@clerk/chrome-extensiongatsby-plugin-clerkbuild/tooling/chore