diff --git a/npm/vite-dev-server/cypress/components/circular-dependencies/a.spec.ts b/npm/vite-dev-server/cypress/components/circular-dependencies/a.spec.ts new file mode 100644 index 000000000000..7986eadf5a6a --- /dev/null +++ b/npm/vite-dev-server/cypress/components/circular-dependencies/a.spec.ts @@ -0,0 +1,7 @@ +/// + +import { a } from './a' + +it('handles circular dependencies', () => { + expect(a()).to.eq('This is the message') +}) diff --git a/npm/vite-dev-server/cypress/components/circular-dependencies/a.ts b/npm/vite-dev-server/cypress/components/circular-dependencies/a.ts new file mode 100644 index 000000000000..75b069630fb2 --- /dev/null +++ b/npm/vite-dev-server/cypress/components/circular-dependencies/a.ts @@ -0,0 +1,9 @@ +import { b } from './b' + +export function a () { + const msg = b() + + return msg +} + +export const message = 'This is the message' diff --git a/npm/vite-dev-server/cypress/components/circular-dependencies/b.ts b/npm/vite-dev-server/cypress/components/circular-dependencies/b.ts new file mode 100644 index 000000000000..31d8ed470dfa --- /dev/null +++ b/npm/vite-dev-server/cypress/components/circular-dependencies/b.ts @@ -0,0 +1,5 @@ +import { c } from './c' + +export function b () { + return c() +} diff --git a/npm/vite-dev-server/cypress/components/circular-dependencies/c.ts b/npm/vite-dev-server/cypress/components/circular-dependencies/c.ts new file mode 100644 index 000000000000..e0639bd3d969 --- /dev/null +++ b/npm/vite-dev-server/cypress/components/circular-dependencies/c.ts @@ -0,0 +1,5 @@ +import { message } from './a' + +export function c () { + return message +} diff --git a/npm/vite-dev-server/src/makeCypressPlugin.ts b/npm/vite-dev-server/src/makeCypressPlugin.ts index 277b87275214..93344f28a645 100644 --- a/npm/vite-dev-server/src/makeCypressPlugin.ts +++ b/npm/vite-dev-server/src/makeCypressPlugin.ts @@ -1,7 +1,10 @@ import { resolve, posix, sep } from 'path' import { readFile } from 'fs' import { promisify } from 'util' -import { Plugin, ViteDevServer } from 'vite' +import Debug from 'debug' +import { ModuleNode, Plugin, ViteDevServer } from 'vite' + +const debug = Debug('cypress:vite-dev-server:plugin') const read = promisify(readFile) @@ -16,13 +19,18 @@ function convertPathToPosix (path: string): string { const INIT_FILEPATH = resolve(__dirname, '../client/initCypressTests.js') +const HMR_DEPENDENCY_LOOKUP_MAX_ITERATION = 50 + export const makeCypressPlugin = ( projectRoot: string, supportFilePath: string, devServerEvents: EventEmitter, + specs: {absolute: string, relative: string}[], ): Plugin => { let base = '/' + const specsPathsSet = new Set(specs.map((spec) => spec.absolute)) + const posixSupportFilePath = supportFilePath ? convertPathToPosix(resolve(projectRoot, supportFilePath)) : undefined const normalizedSupportFilePath = posixSupportFilePath ? `${base}@fs/${posixSupportFilePath}` : undefined @@ -44,6 +52,8 @@ export const makeCypressPlugin = ( base = config.base }, transformIndexHtml () { + debug('transformIndexHtml with base', base) + return [ // load the script at the end of the body // script has to be loaded when the vite client is connected @@ -62,11 +72,46 @@ export const makeCypressPlugin = ( server.middlewares.use(`${base}index.html`, (req, res) => res.end(transformedIndexHtml)) }, - handleHotUpdate: () => { - // restart tests when code is updated - devServerEvents.emit('dev-server:compile:success') + handleHotUpdate: ({ server, file }) => { + debug('handleHotUpdate - file', file) + // get the graph node for the file that just got updated + let moduleImporters = server.moduleGraph.fileToModulesMap.get(file) + let iterationNumber = 0 + + // until we reached a point where the current module is imported by no other + while (moduleImporters && 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()) { + if (specsPathsSet.has(mod.file)) { + debug('handleHotUpdate - compile success') + devServerEvents.emit('dev-server:compile:success') + + return [] + } + } + + // get all the modules that import the current one + moduleImporters = getImporters(moduleImporters) + iterationNumber += 1 + } return [] }, } } + +function getImporters (modules: Set): Set { + const allImporters = new Set() + + modules.forEach((m) => { + m.importers.forEach((imp) => allImporters.add(imp)) + }) + + return allImporters +} diff --git a/npm/vite-dev-server/src/startServer.ts b/npm/vite-dev-server/src/startServer.ts index 20f0d5efdbfc..36e5c7a23f25 100644 --- a/npm/vite-dev-server/src/startServer.ts +++ b/npm/vite-dev-server/src/startServer.ts @@ -37,7 +37,7 @@ const resolveServerConfig = async ({ viteConfig, options }: StartDevServer): Pro const finalConfig: InlineConfig = { ...viteConfig, ...requiredOptions } - finalConfig.plugins = [...(viteConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents)] + finalConfig.plugins = [...(viteConfig.plugins || []), makeCypressPlugin(projectRoot, supportFile, options.devServerEvents, options.specs)] // This alias is necessary to avoid a "prefixIdentifiers" issue from slots mounting // only cjs compiler-core accepts using prefixIdentifiers in slots which vue test utils use.