/
cypress.ts
158 lines (128 loc) · 5.34 KB
/
cypress.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import debugFn from 'debug'
import { resolve } from 'pathe'
import type { ModuleNode, Plugin, ViteDevServer } from 'vite'
import type { Vite } from '../getVite'
import fs from 'fs'
import type { ViteDevServerConfig } from '../devServer'
import path from 'path'
const debug = debugFn('cypress:vite-dev-server:plugins:cypress')
const INIT_FILEPATH = resolve(__dirname, '../../client/initCypressTests.js')
const HMR_DEPENDENCY_LOOKUP_MAX_ITERATION = 50
interface Spec {
absolute: string
relative: string
}
function getSpecsPathsSet (specs: Spec[]) {
return new Set<string>(
specs.map((spec) => spec.absolute),
)
}
export const Cypress = (
options: ViteDevServerConfig,
vite: Vite,
): Plugin => {
let base = '/'
const projectRoot = options.cypressConfig.projectRoot
const supportFilePath = options.cypressConfig.supportFile ? path.resolve(projectRoot, options.cypressConfig.supportFile) : false
const devServerEvents = options.devServerEvents
const specs = options.specs
const indexHtmlFile = options.cypressConfig.indexHtmlFile
let specsPathsSet = getSpecsPathsSet(specs)
let loader = fs.readFileSync(INIT_FILEPATH, 'utf8')
devServerEvents.on('dev-server:specs:changed', (specs: Spec[]) => {
specsPathsSet = getSpecsPathsSet(specs)
})
return {
name: 'cypress:main',
enforce: 'pre',
configResolved (config) {
base = config.base
},
async transformIndexHtml () {
const indexHtmlPath = resolve(projectRoot, indexHtmlFile)
debug('resolved the indexHtmlPath as', indexHtmlPath, 'from', indexHtmlFile)
const indexHtmlContent = await fs.promises.readFile(indexHtmlPath, { encoding: 'utf8' })
// find </body> last index
const endOfBody = indexHtmlContent.lastIndexOf('</body>')
// insert the script in the end of the body
return `${indexHtmlContent.substring(0, endOfBody)
}<script>
${loader}
</script>${
indexHtmlContent.substring(endOfBody)
}`
},
configureServer: async (server: ViteDevServer) => {
server.middlewares.use(`${base}index.html`, async (req, res) => {
let transformedIndexHtml = await server.transformIndexHtml(base, '')
const viteImport = `<script type="module" src="${options.cypressConfig.devServerPublicPathRoute}/@vite/client"></script>`
// If we're doing cy-in-cy, we need to be able to access the Cypress instance from the parent frame.
if (process.env.CYPRESS_INTERNAL_VITE_OPEN_MODE_TESTING) {
transformedIndexHtml = transformedIndexHtml.replace(viteImport, `<script>document.domain = 'localhost';</script>${viteImport}`)
}
return res.end(transformedIndexHtml)
})
},
handleHotUpdate: ({ server, file }) => {
debug('handleHotUpdate - file', file)
// If the user provided IndexHtml is changed, do a full-reload
if (vite.normalizePath(file) === resolve(projectRoot, indexHtmlFile)) {
server.ws.send({
type: 'full-reload',
})
return
}
// get the graph node for the file that just got updated
let moduleImporters = server.moduleGraph.fileToModulesMap.get(file)
let iterationNumber = 0
const exploredFiles = new Set<string>()
// until we reached a point where the current module is imported by no other
while (moduleImporters?.size) {
if (iterationNumber > HMR_DEPENDENCY_LOOKUP_MAX_ITERATION) {
debug(`max hmr iteration reached: ${HMR_DEPENDENCY_LOOKUP_MAX_ITERATION}; Rerun will not happen on this file change.`)
return []
}
// as soon as we find one of the specs, we trigger the re-run of tests
for (const mod of moduleImporters.values()) {
debug('handleHotUpdate - mod.file', mod.file)
if (mod.file === supportFilePath) {
debug('handleHotUpdate - support compile success')
devServerEvents.emit('dev-server:compile:success')
// if we update support we know we have to re-run it all
// no need to check further
return []
}
if (mod.file && specsPathsSet.has(mod.file)) {
debug('handleHotUpdate - spec compile success', mod.file)
devServerEvents.emit('dev-server:compile:success', { specFile: mod.file })
// if we find one spec, does not mean we are done yet,
// there could be other spec files to re-run
// see https://github.com/cypress-io/cypress/issues/17691
}
}
// get all the modules that import the current one
moduleImporters = getImporters(moduleImporters, exploredFiles)
iterationNumber += 1
}
return []
},
}
}
/**
* Gets all the modules that import the set of modules passed in parameters
* @param modules the set of module whose dependents to return
* @param alreadyExploredFiles set of files that have already been looked at and should be avoided in case of circular dependency
* @returns a set of ModuleMode that import directly the current modules
*/
function getImporters (modules: Set<ModuleNode>, alreadyExploredFiles: Set<string>): Set<ModuleNode> {
const allImporters = new Set<ModuleNode>()
modules.forEach((m) => {
if (m.file && !alreadyExploredFiles.has(m.file)) {
alreadyExploredFiles.add(m.file)
m.importers.forEach((imp) => {
allImporters.add(imp)
})
}
})
return allImporters
}