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

feat(webpack-dev-server): Allow using custom, react-scripts-compati… #25865

Closed

Conversation

bencergazda
Copy link

…ble libraries in loadWebpackConfig()

Additional details

This PR makes it possible to use custom, react-scripts-compatible packages to be used as a source for webpack.config.js

Steps to test

  • Set the following config in cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
      scriptsPackageName: 'react-app-rewired',
    },
  },
});

How has the user experience changed?

3rd party, react-scripts compatible* packages should be loaded automatically, without requiring additional configuration.

* Compatible means that the package exposes a config/webpack.config.js file with a function exported that accepts an env parameter and returns the webpack configuration.

PR Tasks

@CLAassistant
Copy link

CLAassistant commented Feb 17, 2023

CLA assistant check
All committers have signed the CLA.

@cypress
Copy link

cypress bot commented Feb 17, 2023

40 flaky tests on run #44211 ↗︎

0 26843 1281 0 Flakiness 40

Details:

feat(webpack-dev-server): Allow using custom, `react-scripts`-compatible librari...
Project: cypress Commit: c5bfbe9a57
Status: Passed Duration: 19:29 💡
Started: Feb 17, 2023 9:57 AM Ended: Feb 17, 2023 10:16 AM
Flakiness  create-from-component.cy.ts • 2 flaky tests • app-e2e

View Output Video

Test
... > runs generated spec Screenshot
... > runs generated spec Screenshot
Flakiness  e2e/origin/cookie_behavior.cy.ts • 4 flaky tests • 5x-driver-electron

View Output Video

Test
... > same site / cross origin > XMLHttpRequest > sets cookie on same-site request if withCredentials is true, and attaches to same-site request if withCredentials is true
... > same site / cross origin > fetch > sets same-site cookies if "include" credentials option is specified from request, but does not attach same-site cookies to request by default (same-origin)
... > same site / cross origin > XMLHttpRequest > sets cookie on same-site request if withCredentials is true, and attaches to same-site request if withCredentials is true
... > same site / cross origin > fetch > sets same-site cookies if "include" credentials option is specified from request, but does not attach same-site cookies to request by default (same-origin)
Flakiness  specs_list_latest_runs.cy.ts • 2 flaky tests • app-e2e

View Output Video

Test
App/Cloud Integration - Latest runs and Average duration > when no runs are recorded > shows placeholders for all visible specs Screenshot
App/Cloud Integration - Latest runs and Average duration > when offline > shows offline alert then hides it after coming online Screenshot
Flakiness  cypress-origin-communicator.cy.ts • 1 flaky test • app-e2e

View Output Video

Test
Cypress In Cypress Origin Communicator > cy.origin passivity with app interactions > passes upon test reload mid test execution Screenshot
Flakiness  e2e/origin/navigation.cy.ts • 1 flaky test • 5x-driver-electron

View Output Video

Test
delayed navigation > errors > redirects to an unexpected cross-origin and calls another command in the cy.origin command

The first 5 flaky specs are shown, see all 18 specs in Cypress Cloud.

This comment has been generated by cypress-bot as a result of this project's GitHub integration settings.

@lmiller1990
Copy link
Contributor

Will continue conversation is https://github.com/cypress-io/cypress/pull/25865/files

@ZachJW34
Copy link
Contributor

I had two ideas that would change the flow of this but result in the same outcome. Before I do I'd like to state the motivation behind this change from my understanding:

Motivation: The ability to source a custom CRA-related webpack configuration that will be modified in the same fashion as a normal CRA webpack config.

We already support the webpackConfig option where a user can supply their own webpackConfig rather than use a preset, but certain configurations require modifications to get it to work with Cypress CT. If it's a CRA modifier, it will most likely require the same modifications we make to the sourced CRA config (allow importing files outside of source, eslint modifications for Cypress, CRA 5 tweaks etc..). These can be pretty verbose and since we don't export these webpack modifiers so as to make them composable, using a fully custom CT setup utilizing the webpackConfig option is non-trivial. This PR aims to tap into the existing CRA sourcing, tweak it to allow sourcing from a custom location and receive the same standard CRA modifications that we require.

My two ideas on how we tweak this:

  1. Add first class support for react-app-rewired

    We already have full support for ejected cra apps:

    // Might be an ejected cra app, search for common webpack configs

    We could modify this section to do another try catch and try to source from react-app-rewired. It's a fairly popular package so the motivating factor (usage) could be enough to go down this route. We wouldn't need to adjust the API at all in this scenario.

    The downside of this approach is the commitment to first class support for react-app-rewired. Lately, we've taken a more hands off approach towards adding support for a package in favor of making things "pluggable". We can't support every framework/permutation, but we can expose APIs to make this easier. Which leads me to by next suggestion:

  2. Add an API for custom cra sourcing specific to configs specifying framework: "create-react-app"

    This is more inline with the code changes suggested so far in this PR. A problem I see with the current suggestion is the convention put forth e.g. 3rd party package have to conform to having their webpack config sourcer in < scriptsPackageName>/config/webpack.config.js. Craco is another popular CRA modifier that doesn't conform to this standard. Though this change solves the problem for react-app-rewired, it doesn't do it broadly enough.

    We could adjust the API to include a customCRAWebpackConfig option that would share the same API as our webpackConfig API (an object in the form of a webpack config or a function that returns a promise). This could look like:

    export defineConfig({
      component: {
        devServer: {
          framework: 'create-react-app',
          bundler: 'webpack',
          options: {
            customCRAWebpackConfig: require('react-app-rewired/config/webpack.config.js')
          }
        }
      }
    })

    This could be extended to work with craco which utilizes the const { createWebpackDevConfig } = require('@craco/craco'); function to source a config.

I think the second option is more inline with our current 3rd party support plans. It also gives a better signal to the user that this isn't first class support and that if problems arise, it's probably due to an incompatibility on the users end and not on Cypress.

@lmiller1990 @bencergazda WDYT?

@lmiller1990
Copy link
Contributor

My concern with

options: {
        customCRAWebpackConfig: require('react-app-rewired/config/webpack.config.js')
      }

Is is still requires a change in Cypress core to support it... which is not very "hands off". It's also a very specific option. If Next.js decided to have something similar (like a "next rewired") we'd need a similar thing for that, and so on and so forth.

I feel like the only real general solution is to lean more heavily into #25881 and make a single API that can do everything, including all the weird things we need to do for Next.js, Angular, etc. This really seems like the only truly scalable solution - otherwise every time a new tool gains popularity, the community needs to wait for a handful of individuals at Cypress to have bandwidth to implement some new code path.

The Qwik team is having a similar issue integrating their dev server - we've got a better public API for basic libraries and mount functions like in Vue, React etc as of #25638 - looks like the Dev Server API is also pretty important.

@bencergazda
Copy link
Author

bencergazda commented Feb 24, 2023

According to the related github issues, the most complicated thing from the dev's perspective is to find the good place in the cypress config file for the require() call, and to find out the correct NODE_ENV values, to get a working webpack config. In case of CRA, Cypress sets process.env.NODE_ENV and process.env.BABEL_ENV to 'test' before requiring the CRA webpack config, and then it requires it with "develpoment" env.

So it might be also a good option to have the suggested webpackConfig callback. Cypress should set the required process.env variables to test before calling it, and pass the "recommended" webpack config environment name (development) to the callback function. This would avoid confiusions.

import { defineConfig } from 'cypress'
import defaultConfig from './cypress-webpack.config'
import { devServer } from '@cypress/webpack-dev-server'

export default defineConfig({
  component: {
    devServer: (devServerOptions) => devServer({
      ...devServerOptions,
      framework: 'react',
      webpackConfig: (env) => {
         // customize it here! your own config, import a rewired one, etc.
         const craRewiredConfig = require('./your-webpack-config.js')(env);
         return craRewiredConfig
      },
    }),
  }
})

@lmiller1990
Copy link
Contributor

Just to clarify @bencergazda, given:

import { defineConfig } from 'cypress'
import defaultConfig from './cypress-webpack.config'
import { devServer } from '@cypress/webpack-dev-server'

export default defineConfig({
  component: {
    devServer: (devServerOptions) => devServer({
      ...devServerOptions,
      framework: 'react',
      webpackConfig: (env) => {
         // customize it here! your own config, import a rewired one, etc.
         const craRewiredConfig = require('./your-webpack-config.js')(env);
         return craRewiredConfig
      },
    }),
  }
})

Does this work for you, as you'd expect?

The goal is ultimately to enable developers (such as yourself) to be able to define Framework Definition so other developers using Rewired don't need to write the same snippet you did for webpackConfig - this becomes much more complex for frameworks for Next.js.

So it might be also a good option to have the #22521 (comment)

Sure - we need it to work for things like Next.js etc too, so we will need to do a bit more internal exploration to find out what exactly the arguments to webpackConfig would be. Right now, it's config - you access a huge amount of Cypress internal config, not just env. I think env should be in there too, though (not sure if it is right now, but you could also just grab it via process.env, right?)

@bencergazda
Copy link
Author

@lmiller1990

Does this work for you, as you'd expect?

Not, unfortunately. :-(

webpackConfig() doesn't provide with an 'env' parameter, and even if I manually set 'development' like this,

import { defineConfig } from 'cypress';
import { devServer } from '@cypress/webpack-dev-server';

export default defineConfig({
  component: {
    devServer: devServerOptions =>
      devServer({
        ...devServerOptions,
        framework: 'react',
        webpackConfig: () => {
          return require('react-app-rewired/config/webpack.config')(
            'development',
          );
        },
      }),
  },
});

I get the following error:

image

And even if I set the NODE_ENV and BABEL_ENV explicitly like this,

export default defineConfig({
  component: {
    devServer: devServerOptions =>
      devServer({
        ...devServerOptions,
        framework: 'react',
        webpackConfig: () => {
          process.env.NODE_ENV = 'test';
          process.env.BABEL_ENV = 'test';

          return require('react-app-rewired/config/webpack.config')(
            'development',
          );
        },
      }),
  },
});

the config won't work due to some other webpack error:

image

There must be differences between how a) framework: 'react-app-rewired' and b) framework: 'react' + webpackConfig() handles the webpack configuration.


But it still works OK with the original solution.

import { defineConfig } from 'cypress';

/**
 * "Rewire" the webpack config before `loadWebpackConfig` accesses it through `react-scripts`
 * @see @cypress/webpack-dev-server/dist/helpers/createReactAppHandler.js:41
 */
process.env.NODE_ENV = 'test';
process.env.BABEL_ENV = 'test';
require('react-app-rewired/config/webpack.config')('development');

export default defineConfig({
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
});

@lmiller1990
Copy link
Contributor

When you get "another error"

image

Looks like CRA rewired cannot understand TypeScript? What if you remove the declare global from your cypress/support/component.ts?

@bencergazda
Copy link
Author

What if you remove the declare global from your cypress/support/component.ts?

Then I get Unexpected token error for the .tsx files. And most probably I would get a lot more. :-( Though the webpack config returned by require('react-app-rewired/config/webpack.config')('development'); seems to be a valid CRA config.

image

(Also, now I see that in this callback-function scenario the envs should be set to to development insetad of test. This seems to be better like this, but this doesn't solve the invalid webpack config issue.)

import { defineConfig } from 'cypress';
import { devServer } from '@cypress/webpack-dev-server';

export default defineConfig({
  component: {
    devServer: devServerOptions =>
      devServer({
        ...devServerOptions,
        framework: 'react',
        webpackConfig: () => {
          const env = 'development';
          process.env.NODE_ENV = env;
          process.env.BABEL_ENV = env;

          return require('react-app-rewired/config/webpack.config')(env);
        },
      }),
  },
});

@lmiller1990
Copy link
Contributor

lmiller1990 commented Mar 2, 2023

Let me clarify

But it still works OK with the #22521 (comment).

Which is

import { defineConfig } from 'cypress';

/**
 * "Rewire" the webpack config before `loadWebpackConfig` accesses it through `react-scripts`
 * @see @cypress/webpack-dev-server/dist/helpers/createReactAppHandler.js:41
 */
process.env.NODE_ENV = 'test';
process.env.BABEL_ENV = 'test';
require('react-app-rewired/config/webpack.config')('development');

export default defineConfig({
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
});

But

import { defineConfig } from 'cypress';
import { devServer } from '@cypress/webpack-dev-server';

export default defineConfig({
  component: {
    devServer: devServerOptions =>
    devServer({
        ...devServerOptions,
        framework: 'react',
        webpackConfig: () => {
          const env = 'development';
          process.env.NODE_ENV = env;
          process.env.BABEL_ENV = env;

          return require('react-app-rewired/config/webpack.config')(env);
        },
      }),
  },
});

Is not working?

The devServer function is executed in a child process so that if it crashes, it won't crash all of Cypress. If my summary ^ is correct, we can start to dig into the child process and find out what the difference is.

I assume the returned object from return require('react-app-rewired/config/webpack.config')(env); is different between the two examples? Do we have a diff? I wonder what's changing.

Also, you mentioned you get "unexpected token" if you remove the declare code. What is the unexpected token? Is the theory that TS no longer works when requiring the react app config in devServer correct?

@bencergazda
Copy link
Author

bencergazda commented Mar 6, 2023

@lmiller1990 I created a minimal reproducible example: https://github.com/bencergazda/cypress-25865-mre

This is a minimal CRA project with and without react-app-rewired. The last six commits shows different setups. Only the first two is working: one with react-scripts and one with react-app-rewired. All approaches with the webpackConfig method failed for me.

@lmiller1990
Copy link
Contributor

Thanks for sharing that! This will be useful once we further explore supporting this kind of API. I can't work on this right now, but I can ping you when we come back to this (probably not during March, unfortunately).

@nagash77 nagash77 assigned lmiller1990 and unassigned mike-plummer Mar 15, 2023
@lmiller1990
Copy link
Contributor

We will work on improving the Framework Definition public API to support this. At this point, I don't think we are going to add any additional options to cypress.config, so I'm going to close this PR. There's a lot of good work arounds here for now, and we intend of pursuing a more general solution in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cypress config with monorepo and rewired cra
6 participants