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

New example request: @cypress/webpack-preprocessor using exact same webpack config Cypress uses #25998

Open
karlhorky opened this issue Mar 2, 2023 · 7 comments
Labels
type: enhancement Requested enhancement of existing feature

Comments

@karlhorky
Copy link
Contributor

karlhorky commented Mar 2, 2023

Current behavior

It's hard to find the correct way to extend the webpack configuration which Cypress already uses (to keep the features that Cypress already has, such as TypeScript compilation).

It's even hard to find the Cypress webpack configuration, with multiple locations including fragments of webpack configuration:

  • const finalConfig = {
    mode: 'development',
    optimization,
    output: {
    filename: '[name].js',
    path: OUTPUT_PATH,
    publicPath,
    },
    plugins: [
    new (HtmlWebpackPlugin as typeof import('html-webpack-plugin-5'))({
    template: indexHtmlFile,
    // Angular generates all of it's scripts with <script type="module">. Live-reloading breaks without this option.
    // We need to manually set the base here to `/__cypress/src/` so that static assets load with our proxy
    ...(framework === 'angular' ? { scriptLoading: 'module', base: '/__cypress/src/' } : {}),
    }),
    new CypressCTWebpackPlugin({
    files,
    projectRoot,
    devServerEvents,
    supportFile,
    webpack,
    indexHtmlFile,
    }),
    ],
    devtool: 'inline-source-map',
    } as any
    if (isRunMode) {
    // Disable file watching when executing tests in `run` mode
    finalConfig.watchOptions = {
    ignored: '**/*',
    }
    }
    if (webpackDevServerMajorVersion === 4) {
    return {
    ...finalConfig,
    devServer: {
    client: {
    overlay: false,
    },
    },
    }
    }
    // @ts-ignore
    return {
    ...finalConfig,
    devServer: {
    overlay: false,
    },
    }
  • const commonConfig = getCommonConfig()
    const CopyWebpackPlugin = getCopyWebpackPlugin()
    // @ts-ignore
    const babelLoader = _.find(commonConfig.module.rules, (rule) => {
    // @ts-ignore
    return _.includes(rule.use.loader, 'babel-loader')
    })
    // @ts-ignore
    babelLoader.use.options.plugins.push([require.resolve('babel-plugin-prismjs'), {
    'languages': ['javascript', 'coffeescript', 'typescript', 'jsx', 'tsx'],
    'plugins': ['line-numbers', 'line-highlight'],
    'theme': 'default',
    'css': false,
    }])
    let pngRule
    // @ts-ignore
    const nonPngRules = _.filter(commonConfig.module.rules, (rule) => {
    // @ts-ignore
    if (rule.test.toString().includes('png')) {
    pngRule = rule
    return false
    }
    return true
    })
    pngRule.use[0].options = {
    name: '[name].[ext]',
    outputPath: 'img',
    publicPath: '/__cypress/runner/img/',
    }
    // @ts-ignore
    const mainConfig: webpack.Configuration = {
    ...commonConfig,
    module: {
    rules: [
    ...nonPngRules,
    pngRule,
    ],
    },
    entry: {
    cypress_runner: [path.resolve(__dirname, 'src/index.js')],
    },
    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    },
    }
    // @ts-ignore
    mainConfig.plugins = [
    // @ts-ignore
    ...mainConfig.plugins,
    new CopyWebpackPlugin([{
    // @ts-ignore // There's a race condition in how these types are generated.
    from: cyIcons.getPathToFavicon('favicon.ico'),
    }]),
    ]
    mainConfig.resolve = {
    ...mainConfig.resolve,
    alias: {
    'bluebird': require.resolve('bluebird'),
    'lodash': require.resolve('lodash'),
    'mobx': require.resolve('mobx'),
    'mobx-react': require.resolve('mobx-react'),
    'react': require.resolve('react'),
    'react-dom': require.resolve('react-dom'),
    },
    }
    // @ts-ignore
    const crossOriginConfig: webpack.Configuration = {
    ...commonConfig,
    entry: {
    cypress_cross_origin_runner: [path.resolve(__dirname, 'src/cross-origin.js')],
    },
    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    },
    }
    // @ts-ignore
    const mainInjectionConfig: webpack.Configuration = {
    ...getSimpleConfig(),
    mode: 'production',
    entry: {
    injection: [path.resolve(__dirname, 'injection/main.js')],
    },
    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    },
    }
    // @ts-ignore
    const crossOriginInjectionConfig: webpack.Configuration = {
    ...getSimpleConfig(),
    mode: 'production',
    entry: {
    injection_cross_origin: [path.resolve(__dirname, 'injection/cross-origin.js')],
    },
    output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    },
    }
  • export const getCommonConfig = () => {
    const commonConfig: Configuration = {
    mode: 'none',
    node: {
    fs: 'empty',
    child_process: 'empty',
    net: 'empty',
    tls: 'empty',
    module: 'empty',
    },
    resolve: {
    extensions: ['.ts', '.js', '.jsx', '.tsx', '.scss', '.json'],
    },
    stats,
    optimization,
    module: {
    rules: [
    {
    test: /\.(ts|js|jsx|tsx)$/,
    exclude: /node_modules/,
    use: {
    loader: require.resolve('babel-loader'),
    options: {
    plugins: [
    // "istanbul",
    [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
    [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
    ],
    presets: [
    babelPresetEnvConfig,
    require.resolve('@babel/preset-react'),
    babelPresetTypeScriptConfig,
    ],
    babelrc: false,
    },
    },
    },
    {
    test: /\.s?css$/,
    exclude: /node_modules/,
    use: [
    { loader: MiniCSSExtractWebpackPlugin.loader },
    ],
    },
    makeSassLoaders({ modules: false }),
    makeSassLoaders({ modules: true }),
    {
    test: /\.(eot|svg|ttf|woff|woff2)$/,
    use: [
    {
    loader: require.resolve('file-loader'),
    options: {
    name: './fonts/[name].[ext]',
    },
    },
    ],
    },
    {
    test: /\.(png|gif)$/,
    use: [
    {
    loader: require.resolve('file-loader'),
    options: {
    name: './img/[name].[ext]',
    },
    },
    ],
    },
    {
    test: /\.wasm$/,
    type: 'javascript/auto',
    use: [
    {
    loader: require.resolve('arraybuffer-loader'),
    },
    ],
    },
    ],
    },
    plugins: [
    new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
    new MiniCSSExtractWebpackPlugin(),
    // Enable source maps / eval maps
    // 'EvalDevtoolModulePlugin' is used in development
    // because it is fast and maps to filenames while showing compiled source
    // 'SourceMapDevToolPlugin' is used in production for the same reasons as 'eval', but it
    // shows full source and does not cause crossorigin errors like 'eval' (in Chromium < 63)
    // files will be mapped like: `cypress://../driver/cy/commands/click.coffee`
    // other sourcemap options:
    // [new webpack.SourceMapDevToolPlugin({
    // moduleFilenameTemplate: 'cypress://[namespace]/[resource-path]',
    // fallbackModuleFilenameTemplate: 'cypress://[namespace]/[resourcePath]?[hash]'
    // })] :
    ...[
    (env === 'production'
    ? new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') })
    : evalDevToolPlugin
    ),
    ],
    ...(liveReloadEnabled ? [new LiveReloadPlugin({ appendScriptTag: 'true', port: 0, hostname: 'localhost', protocol: 'http' })] : []),
    ],
    cache: true,
    }
    return commonConfig
    }
    // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
    export const getSimpleConfig = () => ({
    node: {
    fs: 'empty',
    child_process: 'empty',
    net: 'empty',
    tls: 'empty',
    module: 'empty',
    },
    resolve: {
    extensions: ['.js', '.ts', '.json'],
    },
    stats,
    optimization,
    cache: true,
    module: {
    rules: [
    {
    test: /\.(js|ts)$/,
    exclude: /node_modules/,
    use: {
    loader: require.resolve('babel-loader'),
    options: {
    plugins: [
    [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
    ],
    presets: [
    babelPresetEnvConfig,
    babelPresetTypeScriptConfig,
    ],
    babelrc: false,
    },
    },
    },
    // FIXME: we don't actually want or need wasm support in the
    // cross origin bundle that uses this config, but we need to refactor
    // the driver so that it doesn't load the wasm code in
    // packages/driver/src/cypress/source_map_utils.js when creating
    // the cross origin bundle. for now, this is necessary so the build
    // doesn't fail
    // https://github.com/cypress-io/cypress/issues/19888
    {
    test: /\.wasm$/,
    type: 'javascript/auto',
    use: [
    {
    loader: require.resolve('arraybuffer-loader'),
    },
    ],
    },
    ],
    },
    plugins: [
    new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
    ],
    })

Desired behavior

It would be great to have a new example project with the exact same webpack configuration as is used inside Cypress for bundling user test code, similar to the 4 existing examples in the system-tests/projects folder:

  • webpack-preprocessor-awesome-typescript-loader
  • webpack-preprocessor-ts-loader-compiler-options
  • webpack-preprocessor-ts-loader
  • webpack-preprocessor-webpack-5

This example project should have the cypress.config.js file with the setupNodeEvents method configured, similar to the other examples above.

Additionally, this example + documentation should be:

  • easy to use
    • copy + paste into Cypress config, not multiple steps
    • documented (what are some common use cases of extending the config?)
  • tested by the Cypress maintainers
  • maintained by the Cypress maintainers

This way, it's easy to extend the existing webpack config used by Cypress by copying and pasting this example config and then adding your own configuration.

Test code to reproduce

The point of this issue is that it is difficult to find working code. There are many examples of outdated or non-working code throughout the issues in this repository and other locations.

Cypress Version

12.6.0

Node version

18.14.1

Operating System

macOS Ventura 13.2.1 (22D68)

Debug Logs

No response

Other

No response

@nagash77 nagash77 assigned mjhenkes and mike-plummer and unassigned mjhenkes Mar 2, 2023
@mike-plummer
Copy link
Contributor

@karlhorky I'm not sure I understand the use case for exposing internal webpack configs used for bundling up Cypress itself. Only portions of Cypress are bundled with Webpack (some areas use Vite), and the internal bundler configs are very specific and optimized for our codebase. The Webpack configs you referenced above are either specific to bundling up internal segments of Cypress (the runner package) or are used in Cypress Component Testing and are already extensible in that context using the devServer.webpackConfig configuration field. The internal configs rely on devDependencies that a third-party project likely doesn't have installed and aren't introduced transitively (for instance, use of SCSS & WASM).

The "default" webpack config used for loading spec files from a Cypress project is already exposed by @cypress/webpackpreprocessor. It is very basic (just JS/JSX via Babel) but can be extended/adjusted/replaced as needed.

const webpackPreprocessor = require('@cypress/webpack-preprocessor')

const options = webpackPreprocessor.defaultOptions

//...adjust default config

on('file:preprocessor', webpackPreprocessor(options))

@karlhorky
Copy link
Contributor Author

karlhorky commented Mar 3, 2023

exposing internal webpack configs used for bundling up Cypress itself

This is not what I'm talking about - I'm talking about exposing the webpack configuration that Cypress uses to bundle user test code.

The "default" webpack config used for loading spec files from a Cypress project is already exposed by @cypress/webpack-preprocessor.

This is not true - the default webpack configuration of Cypress allows for TypeScript out of the box (and other features, I believe), whereas @cypress/webpack-preprocessor does not.

There is @cypress/webpack-batteries-included-preprocessor which should include more webpack configuration that Cypress uses for user test code, but this is still stuck on webpack 4, so it cannot be used.

@mike-plummer
Copy link
Contributor

@karlhorky Have you looked at the getFullWebpackOptions function from @cypress/webpack-batteries-included-preprocessor? It's not currently documented as part of the public API of the module but I think it would give you what you're looking for - the TS args are dependent on your project using TS, of course.

const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor')

const options = webpackPreprocessor.getFullWebpackOptions(
  "...path to project to find tsconfig file",
  require.resolve("typescript")
)

Might be worth adding a blurb to the README assuming that meets your needs

@karlhorky
Copy link
Contributor Author

karlhorky commented Mar 3, 2023

That seems almost ok - but as I mentioned in my comment above, the @cypress/webpack-batteries-included-preprocessor package is still stuck on webpack 4, so it cannot be used.

@mike-plummer mike-plummer added type: feature New feature that does not currently exist type: enhancement Requested enhancement of existing feature and removed type: feature New feature that does not currently exist labels Mar 7, 2023
@lmiller1990
Copy link
Contributor

I agree we should update to webpack 5 by default, but it's not trivial - it will likely be a breaking change. It's not possible to just slot in webpack 5 with the existing defaults, since those defaults assume webpack 4.

What you can do is what I did here: https://github.com/lmiller1990/cypress-test-tiny/pull/1/files

  1. install your own webpack locally and the latest @cypress/webpack-preprocessor, so npm install @cypress/webpack-preprocessor webpack
  2. in cypress.config.js add:
const preprocessor = require('@cypress/webpack-preprocessor')

module.exports = {
  e2e: {
    setupNodeEvents(on, config) {
      on('file:preprocessor', preprocessor())
    },
  },
}

Now it will use the local webpack (so, v5, if you installed that). You will need to handle your own webpack config now, though - eg, adding whatever loaders you want, etc. The defaults are here. You could just copy-paste the ones you want from the batteries included one.

@karlhorky karlhorky changed the title Difficult to find how to correctly extend @cypress/webpack-preprocessor using exact same webpack config Cypress uses New example request: @cypress/webpack-preprocessor using exact same webpack config Cypress uses Mar 16, 2023
@karlhorky
Copy link
Contributor Author

karlhorky commented Mar 16, 2023

What you can do is what I did here: lmiller1990/cypress-test-tiny#1 (files)

  1. install your own webpack locally and the latest @cypress/webpack-preprocessor, so npm install @cypress/webpack-preprocessor webpack
  2. in cypress.config.js add:

You could just copy-paste the ones you want from the batteries included one.

Yes, I'm aware of that.

The point of this issue is to make an example + docs of an easy way to replicate the exact same configuration that Cypress uses for user test code internally - an example + docs which work out of the box.

Additionally this example + documentation should be:

  • easy to use
    • copy + paste into Cypress config, not multiple steps
    • documented (what are some common use cases of extending the config?)
  • tested by the Cypress maintainers
  • maintained by the Cypress maintainers

I would propose that this new example should be in the system-tests/projects folder.

Not sure whether Cypress internally uses @cypress/webpack-batteries-included-preprocessor for user test code in the latest Cypress version, but if that's the case, then that would be a good starting point for this example.

@lmiller1990
Copy link
Contributor

Sure thing. Are you interested in making a PR by any chance? We could

  1. document in the package README: https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor
  2. add example in system-tests
  3. reference example from README

If you want to make a PR, that'd be great. If not, it will need to wait for someone else to work on.

To answer your question about the batteries included preprocessor, it's the default, we require it here if you don't provide one. On CI, we run the latest commit against some real world apps, like https://github.com/cypress-io/cypress-realworld-app, which uses TS, React, etc - so if that passes, we know the batteries included preprocessor is doing it's job.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement Requested enhancement of existing feature
Projects
None yet
Development

No branches or pull requests

4 participants