Skip to content

Commit

Permalink
feat: allow vite/webpack config to be an async function (#23605)
Browse files Browse the repository at this point in the history
* vite dev server async fn

* test projects

* update types

* simplify setup

* fix tests

* types

* remove junk file

* update comments

* add more tests

* fix system tests
  • Loading branch information
lmiller1990 committed Sep 22, 2022
1 parent eb8ae02 commit 4c647f6
Show file tree
Hide file tree
Showing 16 changed files with 808 additions and 1,491 deletions.
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"
}
}

4 comments on commit 4c647f6

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 4c647f6 Sep 22, 2022

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/linux-x64/develop-4c647f6d5b0f58a797b50436e63c645418bc07ac/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 4c647f6 Sep 22, 2022

Choose a reason for hiding this comment

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

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/linux-arm64/develop-4c647f6d5b0f58a797b50436e63c645418bc07ac/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 4c647f6 Sep 22, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-arm64/develop-4c647f6d5b0f58a797b50436e63c645418bc07ac/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 4c647f6 Sep 22, 2022

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.9.0/darwin-x64/develop-4c647f6d5b0f58a797b50436e63c645418bc07ac/cypress.tgz

Please sign in to comment.