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: allow vite/webpack config to be an async function #23605

Merged
merged 12 commits into from
Sep 22, 2022
9 changes: 6 additions & 3 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3073,18 +3073,21 @@ declare namespace Cypress {

type DevServerFn<ComponentDevServerOpts = any> = (cypressDevServerConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise<ResolvedDevServerConfig>

type ConfigHandler<T> = T
| (() => T | Promise<T>)

type DevServerConfigOptions = {
bundler: 'webpack'
framework: 'react' | 'vue' | 'vue-cli' | 'nuxt' | 'create-react-app' | 'next' | 'svelte'
webpackConfig?: PickConfigOpt<'webpackConfig'>
webpackConfig?: ConfigHandler<PickConfigOpt<'webpackConfig'>>
} | {
bundler: 'vite'
framework: 'react' | 'vue' | 'svelte'
viteConfig?: Omit<Exclude<PickConfigOpt<'viteConfig'>, undefined>, 'base' | 'root'>
viteConfig?: ConfigHandler<Omit<Exclude<PickConfigOpt<'viteConfig'>, undefined>, 'base' | 'root'>>
} | {
bundler: 'webpack',
framework: 'angular',
webpackConfig?: PickConfigOpt<'webpackConfig'>,
webpackConfig?: ConfigHandler<PickConfigOpt<'webpackConfig'>>,
options?: {
projectConfig: AngularDevServerProjectConfig
}
Expand Down
16 changes: 16 additions & 0 deletions npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,20 @@ describe('Config options', () => {
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)
})

it('supports viteConfig as an async function', () => {
cy.scaffoldProject('vite2.9.1-react')
cy.openProject('vite2.9.1-react', ['--config-file', 'cypress-vite-async-function-config.config.ts'])
cy.startAppServer('component')

cy.visitApp()
cy.contains('App.cy.jsx').click()
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)
cy.withCtx(async (ctx) => {
const verifyFile = await ctx.file.readFileInProject('wrote-to-file')

expect(verifyFile).to.eq('OK')
})
})
})
5 changes: 4 additions & 1 deletion npm/vite-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import debugFn from 'debug'
import type { InlineConfig, UserConfig } from 'vite'
import { getVite, Vite } from './getVite'
import { createViteDevServerConfig } from './resolveConfig'

const debug = debugFn('cypress:vite-dev-server:devServer')

const ALL_FRAMEWORKS = ['react', 'vue'] as const

type ConfigHandler = UserConfig | (() => UserConfig | Promise<UserConfig>)

export type ViteDevServerConfig = {
specs: Cypress.Spec[]
cypressConfig: Cypress.PluginConfigOptions
devServerEvents: NodeJS.EventEmitter
onConfigNotFound?: (devServer: 'vite', cwd: string, lookedIn: string[]) => void
} & {
framework?: typeof ALL_FRAMEWORKS[number] // Add frameworks here as we implement
viteConfig?: unknown // Derived from the user's webpack
viteConfig?: ConfigHandler // Derived from the user's vite config
}

export async function devServer (config: ViteDevServerConfig): Promise<Cypress.ResolvedDevServerConfig> {
Expand Down
12 changes: 10 additions & 2 deletions npm/vite-dev-server/src/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import debugFn from 'debug'
import { importModule } from 'local-pkg'
import { relative, resolve } from 'pathe'
import type { InlineConfig } from 'vite'
import type { InlineConfig, UserConfig } from 'vite'
import path from 'path'

import { configFiles } from './constants'
Expand Down Expand Up @@ -90,7 +90,15 @@ export const createViteDevServerConfig = async (config: ViteDevServerConfig, vit
].filter((p) => p != null),
}

const finalConfig = vite.mergeConfig(viteBaseConfig, viteOverrides as Record<string, any>)
let resolvedOverrides: UserConfig = {}

if (typeof viteOverrides === 'function') {
resolvedOverrides = await viteOverrides()
} else if (typeof viteOverrides === 'object') {
resolvedOverrides = viteOverrides
}

const finalConfig = vite.mergeConfig(viteBaseConfig, resolvedOverrides)

debug('The resolved server config is', JSON.stringify(finalConfig, null, 2))

Expand Down
29 changes: 28 additions & 1 deletion npm/vite-dev-server/test/resolveConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { expect } from 'chai'
import Chai, { expect } from 'chai'
import { EventEmitter } from 'events'
import * as vite from 'vite'
import { scaffoldSystemTestProject } from './test-helpers/scaffoldProject'
import { createViteDevServerConfig } from '../src/resolveConfig'
import sinon from 'sinon'
import SinonChai from 'sinon-chai'
import type { ViteDevServerConfig } from '../src/devServer'

Chai.use(SinonChai)

const getViteDevServerConfig = (projectRoot: string) => {
return {
specs: [],
Expand All @@ -21,6 +25,29 @@ const getViteDevServerConfig = (projectRoot: string) => {
describe('resolveConfig', function () {
this.timeout(1000 * 60)

it('calls viteConfig if it is a function, passing in the base config', async () => {
const viteConfigFn = sinon.spy(async () => {
return {
server: {
fs: {
allow: ['some/other/file'],
},
},
}
})

const projectRoot = await scaffoldSystemTestProject('vite-inspect')
const viteDevServerConfig = {
...getViteDevServerConfig(projectRoot),
viteConfig: viteConfigFn,
}

const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite)

expect(viteConfigFn).to.be.called
expect(viteConfig.server.fs.allow).to.include('some/other/file')
})

context('inspect plugin', () => {
it('should not include inspect plugin by default', async () => {
const projectRoot = await scaffoldSystemTestProject('vite-inspect')
Expand Down
17 changes: 17 additions & 0 deletions npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,21 @@ describe('Config options', () => {
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)
})

it('supports webpackConfig as an async function', () => {
cy.scaffoldProject('webpack5_wds4-react')
cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-async-config.config.ts'])
cy.startAppServer('component')

cy.visitApp()
cy.contains('App.cy.jsx').click()
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)

cy.withCtx(async (ctx) => {
const verifyFile = await ctx.file.readFileInProject('wrote-to-file')

expect(verifyFile).to.eq('OK')
})
})
})
6 changes: 5 additions & 1 deletion npm/webpack-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ type FrameworkConfig = {
}
}

export type ConfigHandler =
Partial<Configuration>
| (() => Partial<Configuration> | Promise<Partial<Configuration>>)

export type WebpackDevServerConfig = {
specs: Cypress.Spec[]
cypressConfig: Cypress.PluginConfigOptions
devServerEvents: NodeJS.EventEmitter
onConfigNotFound?: (devServer: 'webpack', cwd: string, lookedIn: string[]) => void
webpackConfig?: unknown // Derived from the user's webpack
webpackConfig?: ConfigHandler // Derived from the user's webpack config
} & FrameworkConfig

/**
Expand Down
6 changes: 5 additions & 1 deletion npm/webpack-dev-server/src/makeWebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export async function makeWebpackConfig (
config: CreateFinalWebpackConfig,
) {
const { module: webpack } = config.sourceWebpackModulesResult.webpack
let userWebpackConfig = config.devServerConfig.webpackConfig as Partial<Configuration>
let userWebpackConfig = config.devServerConfig.webpackConfig
const frameworkWebpackConfig = config.frameworkConfig as Partial<Configuration>
const {
cypressConfig: {
Expand Down Expand Up @@ -125,6 +125,10 @@ export async function makeWebpackConfig (
}
}

userWebpackConfig = typeof userWebpackConfig === 'function'
? await userWebpackConfig()
: userWebpackConfig

const userAndFrameworkWebpackConfig = modifyWebpackConfigForCypress(
merge(frameworkWebpackConfig ?? {}, userWebpackConfig ?? {}),
)
Expand Down
48 changes: 47 additions & 1 deletion npm/webpack-dev-server/test/makeWebpackConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { expect } from 'chai'
import Chai, { expect } from 'chai'
import EventEmitter from 'events'
import snapshot from 'snap-shot-it'
import { IgnorePlugin } from 'webpack'
import { WebpackDevServerConfig } from '../src/devServer'
import { CYPRESS_WEBPACK_ENTRYPOINT, makeWebpackConfig } from '../src/makeWebpackConfig'
import { createModuleMatrixResult } from './test-helpers/createModuleMatrixResult'
import sinon from 'sinon'
import SinonChai from 'sinon-chai'

Chai.use(SinonChai)

describe('makeWebpackConfig', () => {
it('ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v3', async () => {
Expand Down Expand Up @@ -144,4 +149,45 @@ describe('makeWebpackConfig', () => {
'cypress-entry': CYPRESS_WEBPACK_ENTRYPOINT,
})
})

it('calls webpackConfig if it is a function, passing in the base config', async () => {
const testPlugin = new IgnorePlugin({
contextRegExp: /aaa/,
resourceRegExp: /bbb/,
})

const modifyConfig = sinon.spy(async () => {
return {
plugins: [testPlugin],
}
})

const devServerConfig: WebpackDevServerConfig = {
specs: [],
cypressConfig: {
isTextTerminal: false,
projectRoot: '.',
supportFile: '/support.js',
devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
} as Cypress.PluginConfigOptions,
webpackConfig: modifyConfig,
devServerEvents: new EventEmitter(),
}

const actual = await makeWebpackConfig({
devServerConfig,
sourceWebpackModulesResult: createModuleMatrixResult({
webpack: 4,
webpackDevServer: 4,
}),
})

expect(actual.plugins.length).to.eq(3)
expect(modifyConfig).to.have.been.called
// merged plugins get added at the top of the chain by default
// should be merged, not overriding existing plugins
expect(actual.plugins[0].constructor.name).to.eq('IgnorePlugin')
expect(actual.plugins[1].constructor.name).to.eq('HtmlWebpackPlugin')
expect(actual.plugins[2].constructor.name).to.eq('CypressCTWebpackPlugin')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineConfig } from 'cypress'
import reactPlugin from '@vitejs/plugin-react'
import * as path from 'path'
import * as fs from 'fs'

module.exports = defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
viteConfig: async () => {
// to be really sure this was called, write a file to the disc with
// the initial config
fs.writeFileSync(path.join(__dirname, 'wrote-to-file'), 'OK')

// inline vite config via async function
return Promise.resolve({
plugins: [
reactPlugin({
jsxRuntime: 'classic',
}),
],
})
},
},
},
})

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'cypress'
import * as path from 'path'
import * as fs from 'fs'

export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'webpack',
webpackConfig: async (baseConfig: any) => {
console.log(baseConfig)
fs.writeFileSync(path.join(__dirname, 'wrote-to-file'), 'OK')
const cfg = await import('./webpack.config.js')
return cfg.default
},
},
},
// These tests should run quickly / fail quickly,
// since we intentionally causing error states for testing
defaultCommandTimeout: 1000,
})
6 changes: 1 addition & 5 deletions system-tests/projects/nuxtjs-vue2-configured/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@
"vue": "^2.6.14"
},
"devDependencies": {
"@cypress/webpack-dev-server": "file:../../../npm/webpack-dev-server-fresh",
"vue-server-renderer": "^2.6.14",
"vue-template-compiler": "^2.6.14",
"webpack": "^4.46.0",
"webpack-dev-server": "4.7.4",
"core-js": "^3.19.3",
"nuxt": "^2.15.8",
"cypress": "file:../../../cli"
"nuxt": "^2.15.8"
}
}
Loading