diff --git a/docs/config/server-options.md b/docs/config/server-options.md index ade62028b6754d..241cb0c1c08545 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -176,9 +176,9 @@ The error that appears in the Browser when the fallback happens can be ignored. ## server.watch -- **Type:** `object` +- **Type:** `object | false` -File system watcher options to pass on to [chokidar](https://github.com/paulmillr/chokidar#api). +File system watcher options to pass on to [chokidar](https://github.com/paulmillr/chokidar#api). Pass `false` to disable watcher completely. The Vite server watcher skips `.git/` and `node_modules/` directories by default. If you want to watch a package inside `node_modules/`, you can pass a negated glob pattern to `server.watch.ignored`. That is: diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index a9a5a2592bc8cd..7924c90b164c4b 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -77,7 +77,7 @@ interface ViteDevServer { * Chokidar watcher instance. * https://github.com/paulmillr/chokidar#api */ - watcher: FSWatcher + watcher?: FSWatcher /** * Web socket server with `send(payload)` method. */ diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 591e2883453fe3..a863165897e201 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -264,7 +264,7 @@ export function esbuildPlugin(config: ResolvedConfig): Plugin { configureServer(_server) { server = _server server.watcher - .on('add', reloadOnTsconfigChange) + ?.on('add', reloadOnTsconfigChange) .on('change', reloadOnTsconfigChange) .on('unlink', reloadOnTsconfigChange) }, @@ -515,14 +515,14 @@ async function loadTsconfigJsonForFile( try { const result = await parse(filename, await tsconfckParseOptions) // tsconfig could be out of root, make sure it is watched on dev - if (server && result.tsconfigFile !== 'no_tsconfig_file_found') { + if (server?.watcher && result.tsconfigFile !== 'no_tsconfig_file_found') { ensureWatchedFile(server.watcher, result.tsconfigFile, server.config.root) } return result.tsconfig } catch (e) { if (e instanceof TSConfckParseError) { // tsconfig could be out of root, make sure it is watched on dev - if (server && e.tsconfigFile) { + if (server?.watcher && e.tsconfigFile) { ensureWatchedFile(server.watcher, e.tsconfigFile, server.config.root) } } diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d1c83bf43a7b91..3740f2a8b8e1c7 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -88,7 +88,7 @@ export interface ServerOptions extends CommonServerOptions { * chokidar watch options * https://github.com/paulmillr/chokidar#api */ - watch?: WatchOptions + watch?: WatchOptions | false /** * Create Vite dev server to be used as a middleware in an existing server * @default false @@ -194,8 +194,9 @@ export interface ViteDevServer { /** * chokidar watcher instance * https://github.com/paulmillr/chokidar#api + * // TODO: Setting this as optional breaks typings without major bump. Maybe pass no-op instead? */ - watcher: FSWatcher + watcher?: FSWatcher /** * web socket server with `send(payload)` method */ @@ -344,11 +345,6 @@ export async function _createServer( const httpsOptions = await resolveHttpsConfig(config.server.https) const { middlewareMode } = serverConfig - const resolvedWatchOptions = resolveChokidarOptions(config, { - disableGlobbing: true, - ...serverConfig.watch, - }) - const middlewares = connect() as Connect.Server const httpServer = middlewareMode ? null @@ -359,11 +355,17 @@ export async function _createServer( setClientErrorHandler(httpServer, config.logger) } - const watcher = chokidar.watch( - // config file dependencies and env file might be outside of root - [root, ...config.configFileDependencies, config.envDir], - resolvedWatchOptions, - ) as FSWatcher + const watcher = + config.server.watch === false + ? undefined + : chokidar.watch( + // config file dependencies and env file might be outside of root + [root, ...config.configFileDependencies, config.envDir], + resolveChokidarOptions(config, { + disableGlobbing: true, + ...serverConfig.watch, + }), + ) const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }), @@ -456,7 +458,7 @@ export async function _createServer( } } await Promise.allSettled([ - watcher.close(), + watcher?.close(), ws.close(), container.close(), getDepsOptimizer(server.config)?.close(), @@ -548,7 +550,7 @@ export async function _createServer( await onHMRUpdate(file, true) } - watcher.on('change', async (file) => { + watcher?.on('change', async (file) => { file = normalizePath(file) // invalidate module graph cache on file change moduleGraph.onFileChange(file) @@ -556,8 +558,8 @@ export async function _createServer( await onHMRUpdate(file, false) }) - watcher.on('add', onFileAddUnlink) - watcher.on('unlink', onFileAddUnlink) + watcher?.on('add', onFileAddUnlink) + watcher?.on('unlink', onFileAddUnlink) ws.on('vite:invalidate', async ({ path, message }: InvalidatePayload) => { const mod = moduleGraph.urlToModuleMap.get(path) diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index 678c09fc7c029c..b6318d8669061a 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -277,7 +277,10 @@ const devHtmlHook: IndexHtmlTransformHook = async ( // ensure module in graph after successful load const mod = await moduleGraph.ensureEntryFromUrl(url, false) - ensureWatchedFile(watcher, mod.file, config.root) + + if (watcher) { + ensureWatchedFile(watcher, mod.file, config.root) + } const result = await server!.pluginContainer.transform(code, mod.id!) let content = '' diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index cbc0b768f70198..ea17452bd5b0d3 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -263,7 +263,10 @@ async function loadAndTransform( // ensure module in graph after successful load mod ??= await moduleGraph._ensureEntryFromUrl(url, ssr, undefined, resolved) - ensureWatchedFile(watcher, mod.file, root) + + if (watcher) { + ensureWatchedFile(watcher, mod.file, root) + } // transform const transformStart = debugTransform ? performance.now() : 0 diff --git a/packages/vite/src/node/watch.ts b/packages/vite/src/node/watch.ts index 64c42a7b97dddc..9fc1c8e18a3f35 100644 --- a/packages/vite/src/node/watch.ts +++ b/packages/vite/src/node/watch.ts @@ -6,6 +6,10 @@ export function resolveChokidarOptions( config: ResolvedConfig, options: WatchOptions | undefined, ): WatchOptions { + if (config.server.watch === false) { + return {} + } + const { ignored = [], ...otherOptions } = options ?? {} const resolvedWatchOptions: WatchOptions = { diff --git a/playground/test-utils.ts b/playground/test-utils.ts index 39e2f56c5e6a86..e6e5b19ea89976 100644 --- a/playground/test-utils.ts +++ b/playground/test-utils.ts @@ -40,6 +40,7 @@ export const ports = { 'css/postcss-plugins-different-dir': 5006, 'css/dynamic-import': 5007, 'css/lightningcss-proxy': 5008, + watch: 5009, } export const hmrPorts = { 'optimize-missing-deps': 24680, diff --git a/playground/watch/__tests__/watch.spec.ts b/playground/watch/__tests__/watch.spec.ts new file mode 100644 index 00000000000000..5325f515edc525 --- /dev/null +++ b/playground/watch/__tests__/watch.spec.ts @@ -0,0 +1,86 @@ +import path from 'node:path' +import fs from 'node:fs' +import { createServer } from 'vite' +import { afterEach, expect, test } from 'vitest' +import { isBuild, page, ports } from '~utils' + +const filename = path.resolve(__dirname, '../src/header-text.mjs') +const port = ports.watch +const cleanups: (() => void | Promise)[] = [] + +afterEach(async () => { + await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) +}) + +test.skipIf(isBuild)('watch mode is enabled by default', async () => { + const server = await createServer({ + root: path.join(__dirname, '..'), + logLevel: 'silent', + server: { + port, + strictPort: true, + }, + }) + + cleanups.push(() => server.close()) + await server.listen() + + const initialHeader = 'Initial header' + const newHeader = 'New header' + + await page.goto(`http://localhost:${port}`) + expect(await page.textContent('h1')).toBe(initialHeader) + + // Edit file and wait for content to appear on page + cleanups.push(() => + fs.writeFileSync( + filename, + `export const headerText = "${initialHeader}";`, + 'utf8', + ), + ) + fs.writeFileSync( + filename, + `export const headerText = "${newHeader}";`, + 'utf8', + ) + + await page.waitForSelector('text=New header') +}) + +test.skipIf(isBuild)('watch mode can be disabled', async () => { + const server = await createServer({ + root: path.join(__dirname, '..'), + logLevel: 'silent', + server: { + port, + strictPort: true, + watch: false, + }, + }) + cleanups.push(() => server.close()) + await server.listen() + + const initialHeader = 'Initial header' + const newHeader = 'New header' + + await page.goto(`http://localhost:${port}`) + expect(await page.textContent('h1')).toBe(initialHeader) + + cleanups.push(() => + fs.writeFileSync( + filename, + `export const headerText = "${initialHeader}";`, + 'utf8', + ), + ) + fs.writeFileSync( + filename, + `export const headerText = "${newHeader}";`, + 'utf8', + ) + + // Initial header should still be visible after some time + await new Promise((r) => setTimeout(r, 1000)) + expect(await page.textContent('h1')).toBe(initialHeader) +}) diff --git a/playground/watch/index.html b/playground/watch/index.html new file mode 100644 index 00000000000000..fbc3d276fc36a5 --- /dev/null +++ b/playground/watch/index.html @@ -0,0 +1,12 @@ + + + + + + Watch + + + +

Watch

+ + diff --git a/playground/watch/package.json b/playground/watch/package.json new file mode 100644 index 00000000000000..01efca2d5c193c --- /dev/null +++ b/playground/watch/package.json @@ -0,0 +1,9 @@ +{ + "name": "@vitejs/test-watch", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": {}, + "dependencies": {}, + "devDependencies": {} +} diff --git a/playground/watch/src/header-text.mjs b/playground/watch/src/header-text.mjs new file mode 100644 index 00000000000000..abe38cdaa3c64e --- /dev/null +++ b/playground/watch/src/header-text.mjs @@ -0,0 +1 @@ +export const headerText = 'Initial header' diff --git a/playground/watch/src/index.mjs b/playground/watch/src/index.mjs new file mode 100644 index 00000000000000..ffc1f214fbbaf2 --- /dev/null +++ b/playground/watch/src/index.mjs @@ -0,0 +1,3 @@ +import { headerText } from './header-text.mjs' + +document.querySelector('h1').textContent = headerText