Skip to content
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

Merged
merged 11 commits into from
Jan 29, 2024
Merged

fix: use conditional exports #134

merged 11 commits into from
Jan 29, 2024

Conversation

kettanaito
Copy link
Member

@kettanaito kettanaito commented Jan 18, 2024

📦 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).

Copy link

vercel bot commented Jan 18, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
msw-storybook-addon ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 29, 2024 11:11am

@kettanaito kettanaito changed the base branch from main to feat/support-msw-2.0 January 18, 2024 12:23
@kettanaito kettanaito added the skip-release Preserve the current version when merged label Jan 18, 2024
@kettanaito
Copy link
Member Author

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 msw-storybook-addon in that test at all, there's no alias in Jest or on the monorepo level. Tool magic.

target: 'node18',
format: 'cjs',
esbuildOptions(options) {
options.alias = {
Copy link
Member Author

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.

api.resetHandlers()

if (msw) {
if (Array.isArray(msw) && msw.length > 0) {
Copy link
Member Author

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.

Copy link

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.

Copy link
Member Author

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?

Copy link
Collaborator

@yannbf yannbf Jan 18, 2024

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)

Copy link
Member Author

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' })
}

@@ -0,0 +1,3 @@
export { initialize, InitializeOptions } from '@build-time/initialize'
Copy link

@shilman shilman Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯 That's really cool...

Copy link
Member Author

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.

@kettanaito
Copy link
Member Author

Added the right TypeScript configuration to support the right types depending on where you import msw-storybook-addon. For example, the ./dist/index.browser.d.ts will contain msw/browser types:

import { RequestHandler } from 'msw';
import { SetupWorker } from 'msw/browser';

type InitializeOptions = Parameters<SetupWorker['start']>[0];
declare function initialize(options?: InitializeOptions, initialHandlers?: RequestHandler[]): SetupWorker;

While ./dist/index.node.d.ts will refer to msw/node:

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: [''],
Copy link
Member Author

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",
Copy link
Member Author

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(
Copy link
Member Author

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)
Copy link
Member Author

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"]
Copy link
Member Author

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 different tsconfig.<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 in esbuildOptions for build-level module resolution too.

This way each compiled .d.ts file comes with the right types and msw/* imports.

@kettanaito
Copy link
Member Author

@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.

@mattcosta7
Copy link
Contributor

@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

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/7587019471/job/20666449469?pr=134#step:5:34):48)
      at Object.<anonymous> (src/demos/fetch/AddonOnNode.test.jsx:12:11)

It's not super clear what/if any configuration will be needed for end users, compared to the current version which is minimal?

@kettanaito
Copy link
Member Author

@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:

  • Test -> Story -> import msw-storybook-addon - (browser resolution) -> msw/browser -> throws because we are still in Node.js.

Strangely, Jest config to opt-out from this in JSDOM doesn't seem to have any effect. Perhaps @yannbf knows a bit more.

@yannbf
Copy link
Collaborator

yannbf commented Jan 22, 2024

@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:

  • Test -> Story -> import msw-storybook-addon - (browser resolution) -> msw/browser -> throws because we are still in Node.js.

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 running Storybook, but the tests which we run in Node, just use an API from Storybook (nothing more than a javascript function), it doesn't do anything with Storybook's binary or the browser.

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.

@kettanaito
Copy link
Member Author

@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.

@yannbf yannbf added patch Increment the patch version when merged and removed skip-release Preserve the current version when merged labels Jan 29, 2024
@yannbf
Copy link
Collaborator

yannbf commented Jan 29, 2024

Awesome stuff here @kettanaito ! thank you so much for your help <3 I fixed the last bits and brought the getWorker function back that was removed (probably by mistake). I'll continue the work on the other PR!

@yannbf yannbf marked this pull request as ready for review January 29, 2024 11:37
@yannbf yannbf merged commit 37dfc82 into feat/support-msw-2.0 Jan 29, 2024
6 of 8 checks passed
@yannbf yannbf deleted the fix/exports branch January 29, 2024 11:37
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',
Copy link
Member Author

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.

yannbf added a commit that referenced this pull request Apr 17, 2024
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patch Increment the patch version when merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants