Skip to content

Commit 988c512

Browse files
feat: Vite prebuild before run (#114)
1 parent cee8331 commit 988c512

File tree

7 files changed

+273
-105
lines changed

7 files changed

+273
-105
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pnpm add --save-dev cypress-vite
4141

4242
## Usage
4343

44-
For Cypress 10, add the following to your `cypress.config.ts` file:
44+
For Cypress 10+, add the following to your `cypress.config.ts` file:
4545

4646
```typescript
4747
import { defineConfig } from 'cypress'
@@ -94,6 +94,26 @@ export default defineConfig({
9494
})
9595
```
9696

97+
### Pre-building before run
98+
99+
Can speed up preprocessing by performing one overall Vite preprocessing build
100+
before all specs are ran. (Does not work in watch mode)
101+
102+
```typescript
103+
import { getVitePrebuilder } from 'cypress-vite'
104+
105+
const { vitePrebuild, vitePreprocessor } = getVitePrebuilder()
106+
107+
export default defineConfig({
108+
e2e: {
109+
setupNodeEvents(on, config) {
110+
on('before:run', (details) => vitePrebuild(details, config))
111+
on('file:preprocessor', vitePreprocessor)
112+
},
113+
},
114+
})
115+
```
116+
97117
## Debugging
98118

99119
Run your tests with the following environment variable to log the debugging

example/cypress-prebuild.config.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { fileURLToPath } from 'url'
2+
import { resolve, dirname } from 'path'
3+
import { defineConfig } from 'cypress'
4+
import { getVitePrebuilder } from 'cypress-vite'
5+
6+
const __filename = fileURLToPath(import.meta.url)
7+
const __dirname = dirname(__filename)
8+
9+
const { vitePrebuild, vitePreprocessor } = getVitePrebuilder(
10+
resolve(__dirname, './vite.config.ts'),
11+
)
12+
13+
export default defineConfig({
14+
e2e: {
15+
baseUrl: 'http://localhost:5173/',
16+
viewportWidth: 1280,
17+
viewportHeight: 768,
18+
specPattern: '**/*.e2e.ts',
19+
video: false,
20+
screenshotOnRunFailure: false,
21+
22+
setupNodeEvents(on, config) {
23+
on('before:run', (details) => vitePrebuild(details, config))
24+
on('file:preprocessor', vitePreprocessor)
25+
},
26+
},
27+
})

example/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"scripts": {
77
"dev": "vite",
88
"cypress": "start-server-and-test dev http://localhost:5173 'cypress open'",
9-
"test": "start-server-and-test dev http://localhost:5173 'cypress run'",
10-
"test:ci": "cypress run",
9+
"cypress-run-all": "cypress run && cypress run --config-file cypress-prebuild.config.ts",
10+
"test": "start-server-and-test dev http://localhost:5173 cypress-run-all",
11+
"test:ci": "pnpm cypress-run-all",
1112
"build": "tsc && vite build",
1213
"preview": "vite preview"
1314
},

src/common.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Debug from 'debug'
2+
import { type InlineConfig } from 'vite'
3+
4+
export type CypressPreprocessor = (
5+
file: Cypress.FileObject,
6+
) => string | Promise<string>
7+
8+
export function getConfig(
9+
userConfig: string | InlineConfig | undefined,
10+
): InlineConfig {
11+
const config: InlineConfig =
12+
typeof userConfig === 'string'
13+
? { configFile: userConfig }
14+
: (userConfig ?? {})
15+
return config
16+
}
17+
18+
export const debug = Debug('cypress-vite')

src/index.ts

Lines changed: 4 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,6 @@
1-
import path from 'path'
2-
import Debug from 'debug'
3-
import { build, InlineConfig } from 'vite'
4-
import chokidar from 'chokidar'
5-
6-
type FileObject = Cypress.FileObject
7-
type CypressPreprocessor = (file: FileObject) => Promise<string>
8-
9-
const debug = Debug('cypress-vite')
10-
const watchers: Record<string, chokidar.FSWatcher> = {}
11-
12-
/**
13-
* Cypress preprocessor for running e2e tests using vite.
14-
*
15-
* @param {InlineConfig | string} config - Vite config object, or path to user
16-
* Vite config file for backwards compatibility
17-
* @example
18-
* setupNodeEvents(on) {
19-
* on(
20-
* 'file:preprocessor',
21-
* vitePreprocessor(path.resolve(__dirname, './vite.config.ts')),
22-
* )
23-
* },
24-
*/
25-
function vitePreprocessor(
26-
userConfig?: InlineConfig | string,
27-
): CypressPreprocessor {
28-
const config: InlineConfig =
29-
typeof userConfig === 'string'
30-
? { configFile: userConfig }
31-
: userConfig ?? {}
32-
debug('User config path: %s', config.configFile)
33-
34-
return async (file) => {
35-
const { outputPath, filePath, shouldWatch } = file
36-
debug('Preprocessing file %s', filePath)
37-
38-
const fileName = path.basename(outputPath)
39-
const filenameWithoutExtension = path.basename(
40-
outputPath,
41-
path.extname(outputPath),
42-
)
43-
44-
if (shouldWatch && !watchers[filePath]) {
45-
// Watch this spec file if we are not already doing so (and Cypress is
46-
// not in headless mode)
47-
let initial = true
48-
watchers[filePath] = chokidar.watch(filePath)
49-
debug('Watcher for file %s cached', filePath)
50-
51-
file.on('close', async () => {
52-
await watchers[filePath].close()
53-
delete watchers[filePath]
54-
55-
debug('File %s closed.', filePath)
56-
})
57-
58-
watchers[filePath].on('all', () => {
59-
// Re-run the preprocessor if the file changes
60-
if (!initial) {
61-
file.emit('rerun')
62-
}
63-
initial = false
64-
})
65-
}
66-
67-
const defaultConfig: InlineConfig = {
68-
logLevel: 'warn',
69-
define: {
70-
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
71-
},
72-
build: {
73-
emptyOutDir: false,
74-
minify: false,
75-
outDir: path.dirname(outputPath),
76-
sourcemap: true,
77-
write: true,
78-
watch: null,
79-
lib: {
80-
entry: filePath,
81-
fileName: () => fileName,
82-
formats: ['umd'],
83-
name: filenameWithoutExtension,
84-
},
85-
rollupOptions: {
86-
output: {
87-
// override any manualChunks from the user config because they don't work with UMD
88-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89-
manualChunks: false as any,
90-
},
91-
},
92-
},
93-
}
94-
95-
await build({
96-
...config,
97-
...defaultConfig,
98-
})
99-
100-
return outputPath
101-
}
102-
}
1+
import vitePreprocessor from './vitePreprocessor'
2+
import getVitePrebuilder from './vitePrebuild'
1033

1044
export default vitePreprocessor
5+
6+
export { vitePreprocessor, getVitePrebuilder }

src/vitePrebuild.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import path from 'path'
2+
import { build, mergeConfig, type InlineConfig } from 'vite'
3+
import baseVitePreprocessor from './vitePreprocessor'
4+
import { debug, getConfig } from './common'
5+
6+
let wasPrebuilt = true
7+
8+
/**
9+
* Pre-process all files at the beginning of the test run, before they are ran through the
10+
* preprocessor for each spec.
11+
*
12+
* Can greatly improve overall test time, since this pre-build takes into account the shared
13+
* assets/imports of all spec files being ran (as opposed to the regular preprocessor alone, which
14+
* performs on each spec file individually).
15+
*
16+
* Does not work in watch mode, since watch mode preprocesses the current watched spec rather than the
17+
* "whole" of them. (TODO: look into a way to do this)
18+
*
19+
* @param {InlineConfig | string} config - Vite config object, or path to user
20+
* Vite config file for backwards compatibility
21+
*
22+
* @example
23+
* import { getVitePrebuilder } from 'cypress-vite'
24+
* ...
25+
* const { vitePrebuild, vitePreprocessor } = getVitePrebuilder(path.resolve(__dirname, './vite.config.ts'))
26+
* ...
27+
* setupNodeEvents(on, config) {
28+
* on('before:run', (details) => vitePrebuild(details, config))
29+
* on('file:preprocessor', vitePreprocessor)
30+
* },
31+
*/
32+
export function getVitePrebuilder(userConfig?: InlineConfig | string) {
33+
const viteConfig: InlineConfig = getConfig(userConfig)
34+
35+
const OUT_DIR = 'node_modules/.cypress-vite-prebuild'
36+
37+
/**
38+
* Prebuild with all spec files as entries.
39+
* Does not pre-build if only 1 spec ran, or if `watchForFileChanges=true`.
40+
*
41+
* @param details
42+
* Cypress details
43+
* @param config
44+
* Cypress config
45+
*/
46+
async function maybeVitePrebuild(
47+
details: Cypress.BeforeRunDetails,
48+
config: Cypress.PluginConfigOptions,
49+
) {
50+
// TODO: there may be a way to get it to work for watch mode... Could we watch the file while keeping imported assets?
51+
if (
52+
config.watchForFileChanges ||
53+
!details.specs ||
54+
details.specs.length < 2
55+
) {
56+
// We don't gain anything from pre-preprocessing if there isn't more than 1 spec being ran.
57+
wasPrebuilt = false
58+
debug('Not pre-building with Vite.')
59+
return
60+
}
61+
const files: string[] = details.specs.map((spec) => spec.absolute)
62+
if (config.supportFile) {
63+
files.push(config.supportFile)
64+
}
65+
66+
debug(`Pre-building ${files.length} files with Vite.`)
67+
68+
await build(
69+
mergeConfig(viteConfig, {
70+
build: {
71+
outDir: OUT_DIR,
72+
emptyOutDir: true,
73+
minify: false,
74+
rollupOptions: {
75+
input: files,
76+
output: { entryFileNames: '[name].ts', format: 'es' },
77+
treeshake: true,
78+
},
79+
},
80+
esbuild: { treeShaking: true },
81+
} satisfies InlineConfig),
82+
)
83+
}
84+
85+
function customVitePreprocessor(file: Cypress.FileObject) {
86+
if (wasPrebuilt && !file.shouldWatch) {
87+
file.filePath = path.join(OUT_DIR, path.basename(file.filePath))
88+
}
89+
90+
// TODO: make so initial preprocess works, don't have to re-preprocess here
91+
return baseVitePreprocessor(viteConfig)(file)
92+
}
93+
94+
return {
95+
vitePrebuild: maybeVitePrebuild,
96+
vitePreprocessor: customVitePreprocessor,
97+
}
98+
}
99+
100+
export default getVitePrebuilder

0 commit comments

Comments
 (0)