-
Notifications
You must be signed in to change notification settings - Fork 280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have we considered the approach outlined here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, no difference. Important bit for this file is to not export certain stuff to keep it separate
clean: true, | ||
minify: false, | ||
sourcemap: true, | ||
legacyOutput: true, | ||
external: ['@testing-library', 'react', 'jest', 'jest-environment-jsdom', 'react-dom'], | ||
dts: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Any idea how much slower this is than generating the declarations separately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, agreed the absolute difference is reasonable 😃
"moduleResolution": "NodeNext", | ||
"module": "NodeNext", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ why the change here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the PR description, section "Other changes"
"module": "NodeNext", | ||
"moduleResolution": "NodeNext", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Why the change here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i [email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact
npm i @clerk/[email protected] --save-exact |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙃 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We rely on the naming here:
I didn't want to break anything so I just kept it as-is :)
@@ -2,25 +2,58 @@ | |||
"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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have we considered the approach outlined here?
return { | ||
entry: ['./src/**/*.{ts,tsx}', '!./src/**/*.test.{ts,tsx}'], | ||
format: ['cjs', 'esm'], | ||
bundle: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're losing yalc publishing here but let's revisit it after this is released
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My assumption is that we'll move to secco
sooner than later, so I removed it 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚢
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:
testUtils
fromshared
and 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
/react
subpath.Other Changes
If you go through the changed files list, you'll see that in most files only the imports were changed, from
@clerk/shared
to@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.react
ordeprecated
.The next change you'll see often is inside
tsconfig.json
. Most/all of our config files defined themoduleResolution
asnode
. To quote:I also referenced the TSConfig Cheat Sheet a lot. I think in theory I could have changed most things to use
Bundler
but 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 setmodule
toNodeNext
. I usedBundler
once as I was forced to use it, see GitHub inline comment.Sidenote: Our
tsconfig.json
setup is a mess, but I already created an internal ticket to clean that upAs smaller change I did: The functionality of
inClientSide
andinBrowser
was the same. So I removedinClientSide
and replaced its imports withinBrowser
The webpack configuration of
clerk-js
needed an update, too. Since@clerk/shared
will use.mjs
file extensions now, webpack needed an update for itsextensions
config and thets-loader
Where appropriate, I changed
export * from ""
toexport { stuff } from ""
as this is clearer what gets exportedI removed the
shared
file insidesdk-node
as we now fully rely on@clerk/shared
@clerk/shared
ChangesSee 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/shared
completely 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
react
isn't exported from the rootindex
file of@clerk/shared
, you're forced to use@clerk/shared/react
instead. So React is completely isolated from the rest of the package.Checklist
npm test
runs as expected.npm run build
runs 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-extension
gatsby-plugin-clerk
build/tooling/chore