-
Notifications
You must be signed in to change notification settings - Fork 40
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
fix: use conditional exports #134
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
A single JSDOM test is still failing because it uses the browser module resolution despite Jest config telling it not to: FAIL src/demos/fetch/AddonOnNode.test.jsx
● Test suite failed to run
Invariant Violation: [MSW] Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.
31 | var api;
32 | function initialize(options, initialHandlers = []) {
> 33 | const worker = (0, import_browser.setupWorker)(...initialHandlers);
| ^
34 | worker.start(options);
35 | api = worker;
36 | return worker;
at setupWorker (../../node_modules/msw/lib/browser/index.js:754:10)
at initialize (../msw-addon/dist/index.browser.js:[33](https://github.com/mswjs/msw-storybook-addon/actions/runs/7570077990/job/20614712950?pr=134#step:5:34):48)
at Object.<anonymous> (src/demos/fetch/AddonOnNode.test.jsx:12:11) I may be missing some context around this test to understand why this happens. I'm quite confuse as to what resolves |
target: 'node18', | ||
format: 'cjs', | ||
esbuildOptions(options) { | ||
options.alias = { |
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 can technically keep a single initialize.ts
and then use build-time condition via define
to strip away any conditional imports. But I find it risky because we are opening the doors to accidentally mixing environments if we forget to check for them.
Our functionality here is tiny. There's no harm in repeating.
packages/msw-addon/src/loader.ts
Outdated
api.resetHandlers() | ||
|
||
if (msw) { | ||
if (Array.isArray(msw) && msw.length > 0) { |
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'd much like to see reuse is this logic. It's identical between the decorator and the loader and moving it away looks like a good direction to take.
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 understanding is we can just get rid of the decorator entirely. That it was the original implementation but has timing issues that are solved by the loader.
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.
@yannbf, can you please confirm that?
If we can drop mswDecorator
, we can simplify our addon tremendously. Now is also the right time to drop it, since the MSW 2.0 introduction is a breaking change anyway.
I'd also like to discuss the general DX with you. I feel that we can move the initialize
call to the MSW loader and have a better end developer-experience for everyone. My assumption is that we are asking the users to call initialize
in Storybook's config to kick off MSW earlier. Is that the case?
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.
Yup I had plans to remove the mswdecorator, but it's not super simple.
The problem is that it makes it much more difficult for people who reuse stories in tests (especially because portable stories do not support loaders), and this might be a big change for them, and thus requires a proper migration guide.
Regarding initialize, we could provide the code internally but then we remove the customizing aspect of it, which is needed in many cases. We could also make it optional, and call the internal code only if initialize is not called by the user. The side effect to this, again, is that it will break the story reuse scenario (because portable stories won't access such presets)
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.
Got it. Thanks for providing the much needed context here!
Regarding initialize, we could provide the code internally but then we remove the customizing aspect of it, which is needed in many cases.
Can't we provide the customization on the loader level though?
export function mswLoader(options) {
return theActualLoader
}
{
loaders: [mswLoader({ onUnhandledRequest: 'warn' })
}
packages/msw-addon/src/index.new.ts
Outdated
@@ -0,0 +1,3 @@ | |||
export { initialize, InitializeOptions } from '@build-time/initialize' |
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.
🤯 That's really cool...
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.
Note: it's just a fancy name to make it clear where this import will be resolved to. This import identifier is an alias in tsup.confug.ts
that resolves it to different modules (initialize.browser.ts
, initialize.node.ts
, etc) on build time.
Added the right TypeScript configuration to support the right types depending on where you import import { RequestHandler } from 'msw';
import { SetupWorker } from 'msw/browser';
type InitializeOptions = Parameters<SetupWorker['start']>[0];
declare function initialize(options?: InitializeOptions, initialHandlers?: RequestHandler[]): SetupWorker; While import { RequestHandler } from 'msw';
import { SetupServer } from 'msw/node';
type InitializeOptions = Parameters<SetupServer['listen']>[0];
declare function initialize(options?: InitializeOptions, initialHandlers?: RequestHandler[]): SetupServer; |
@@ -19,6 +19,9 @@ module.exports = { | |||
setupFiles: ['./src/fetch-polyfill.js'], | |||
setupFilesAfterEnv: ['./src/setupTests.js'], | |||
testEnvironment: 'jsdom', | |||
testEnvironmentOptions: { | |||
customExportConditions: [''], |
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 must use node-like module resolution in JSDOM. JSDOM uses a browser module resolution by default and that's a rather big oversight on their part.
"types": "./dist/index.browser.d.ts", | ||
"exports": { | ||
"browser": { | ||
"types": "./dist/index.browser.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.
Also providing the right types since depending on the environment, you will be importing either msw/browser
or msw/node
when using initialize
.
| Parameters<SetupServer['listen']>[0] | ||
| Parameters<SetupWorker['start']>[0] | ||
|
||
export declare function initialize( |
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 is really an ambiguous placeholder module used for local development, where we don't yet know which environment the code will compile to. This must never be shipped. This is why explicit environment TypeScript configs never even include this file.
import { applyRequestHandlers } from './applyRequestHandlers' | ||
|
||
export const mswLoader = async (context: Context) => { | ||
applyRequestHandlers(context.parameters.msw) |
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.
@yannbf, I think you will love how simple these functions become! I've also moved the singleton api
access to applyRequestHandlers
, which means we keep a single point of entry when we decide to refactor something.
"compilerOptions": { | ||
"baseUrl": ".", | ||
"paths": { | ||
"@build-time/initialize": ["./src/initialize.browser.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.
The idea of environment-specific TypeScript configs is really simple:
- Each build in
tsup.config.ts
points to a differenttsconfig.<env>.json
. - Each different
tsconfig.<env>.json
resolves the@build-time/initialize
import differently on type-level (that's why we provide a standalone import alias inesbuildOptions
for build-level module resolution too.
This way each compiled .d.ts
file comes with the right types and msw/*
imports.
@mattcosta7, hi, Matt 👋 May I ask your opinion on the direction we are taking with this pull request? If you have a moment, of course. |
A quick glance and this makes sense, but I don't know too much about the build/runtimes of storybook addons offhand to be sure this makes sense. Conceptually breaking this in the way described looks reasonable though nit: 1 test seems to be failing due to environment
It's not super clear what/if any configuration will be needed for end users, compared to the current version which is minimal? |
@mattcosta7, I think the test is failing because it runs Storybook and somehow the test (jest?)(thinks it should use the browser resolution strategy). I assume this is what's happening:
Strangely, Jest config to opt-out from this in JSDOM doesn't seem to have any effect. Perhaps @yannbf knows a bit more. |
Hey there! 👋 I am not sure what you mean by I think the path forward in this repo is to migrate the example code to use Vitest (which is in this PR), but it's important to know whether this change would work with Jest as well I'll take a look at this as soon as I get some time. I just got back from holidays and there are tons of stuff to catch up to. |
@yannbf, happy to have you back! I think we should fix the existing test (it's the only one failing), and I also suspect that the issue lies in how that test is configured. We can migrate the entire repo to use Vitest later, I wouldn't add it on top of an already long-pending changes. Let's bring MSW 2.0 to the Storybook users as soon as we can. This 99% has to do with export conditions. I'm just confused why the general Jest config that fixes this doesn't do anything for that particular test. |
Awesome stuff here @kettanaito ! thank you so much for your help <3 I fixed the last bits and brought the |
moduleNameMapper: { '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy' }, | ||
moduleNameMapper: { | ||
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', | ||
'^msw-storybook-addon$': '<rootDir>/../msw-addon/dist/index.node.js', |
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.
@yannbf this makes sense just make sure this isn't needed for the end developer who will be using Storybook and the add-on for integration testing.
* Breaking change: Support MSW 2.0 Co-authored-by: Gert Hengeveld <info@ghengeveld.nl> * update example to use MSW 2.0 handlers * temporarily disable tests in release workflow * update jest config to pass tests * fix path * worker * alias msw/native * update peer dependency range * upgrade example storybook * fix: use conditional exports (#134) * fix: use conditional exports * chore: keep "dist" as build directory * chore: remove custom webpackFinal config * chore: prevent browser-style resolution in jsdom tests * chore: remove old code * chore: use "index.ts" * fix: use environment-based tsconfig.json * chore: remove unused imports * fix: abstract "api.use()" to "applyRequestHandlers" * read getWorker export * add types for getWorker --------- Co-authored-by: Yann Braga <yannbf@gmail.com> * bring back tests * Filter requests that are not relevant to the user * make sure to reset the handlers * feat!(msw-addon): fix package exports and generate esm build (#138) this updates conditional exports so that both types and dependencies can be resolved. this also migrates build to generating esm by default for a more standards based packaging approach with strong support in modern tooling like webpack and vite. resolves #137 * fix(loader.ts): fix possible undefined (#141) If msw is running in fallback mode, navigator.serviceWorker may not be defined. * update auto * update to yarn 4 * upgrade chromatic * upgrade CI actions * fix return types * add yarn binary * fix jest script * fix chromatic script * fix chromatic script * update package.json files * refactor tsup config * remove unnecessary dependency * temp: downgrade to yarn1 * export applyRequestHandlers * update docs [skip ci] * update tests * upgrade to Storybook 8 * improve documentation on portable stories * deprecate mswDecorator * deprecate msw parameter as an array * migrate docs example to vite (#128) * migrate docs example to vite * migrate from jest to vitest * add rollup workaround * migrate example to Typescript --------- Co-authored-by: Gert Hengeveld <info@ghengeveld.nl> Co-authored-by: Matthew Costabile <mattcosta7@github.com> Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com> Co-authored-by: Christian Murphy <christian.murphy.42@gmail.com> Co-authored-by: Alex Muñoz <AlexMunoz@users.noreply.github.com>
📦 Published PR as canary version:
1.10.1--canary.134.37dfc82.0
✨ Test out this PR locally via:
npm install msw-storybook-addon@1.10.1--canary.134.37dfc82.0 # or yarn add msw-storybook-addon@1.10.1--canary.134.37dfc82.0
The fundamental issue is that we have a single module that does three requires to different environments. We think we make those requires conditional by introducing
if
checks but those are runtime conditions, not build-time.The simplest route to take is to split the
initialize
module into three, environment0-dependent modules, and resolve them conditionally on build-time, since we know during the build the target we're building to (browser/node/react native).