+
+
+
-
direct
+inline
+from-js
+dynamic
+js: error
+dynamic-js: error
+inline-js: error
+double-nonce-js: error
+ + diff --git a/playground/csp/index.js b/playground/csp/index.js new file mode 100644 index 00000000000000..465359baca8297 --- /dev/null +++ b/playground/csp/index.js @@ -0,0 +1,5 @@ +import './from-js.css' + +document.querySelector('.js').textContent = 'js: ok' + +import('./dynamic.js') diff --git a/playground/csp/linked.css b/playground/csp/linked.css new file mode 100644 index 00000000000000..51636e6cfad81f --- /dev/null +++ b/playground/csp/linked.css @@ -0,0 +1,3 @@ +.linked { + color: blue; +} diff --git a/playground/csp/package.json b/playground/csp/package.json new file mode 100644 index 00000000000000..e8a834d93abd25 --- /dev/null +++ b/playground/csp/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-csp", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} diff --git a/playground/csp/vite.config.js b/playground/csp/vite.config.js new file mode 100644 index 00000000000000..84d6d92ba0d0bb --- /dev/null +++ b/playground/csp/vite.config.js @@ -0,0 +1,67 @@ +import fs from 'node:fs/promises' +import url from 'node:url' +import path from 'node:path' +import crypto from 'node:crypto' +import { defineConfig } from 'vite' + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) + +const noncePlaceholder = '#$NONCE$#' +const createNonce = () => crypto.randomBytes(16).toString('base64') + +/** + * @param {import('node:http').ServerResponse} res + * @param {string} nonce + */ +const setNonceHeader = (res, nonce) => { + res.setHeader( + 'Content-Security-Policy', + `default-src 'nonce-${nonce}'; connect-src 'self'`, + ) +} + +/** + * @param {string} file + * @param {(input: string, originalUrl: string) => PromiseThis should be green
+This should be blue
This should not be yellow
This should be yellow
diff --git a/playground/css-codesplit/main.js b/playground/css-codesplit/main.js index e548142add8786..ec266fa003156d 100644 --- a/playground/css-codesplit/main.js +++ b/playground/css-codesplit/main.js @@ -9,6 +9,7 @@ import chunkCssUrl from './chunk.css?url' globalThis.__test_chunkCssUrl = chunkCssUrl import('./async.css') +import('./async-js') import('./inline.css?inline').then((css) => { document.querySelector('.dynamic-inline').textContent = css.default diff --git a/playground/css-lightningcss-proxy/package.json b/playground/css-lightningcss-proxy/package.json index eda58ad563d946..ad6b503221a9f6 100644 --- a/playground/css-lightningcss-proxy/package.json +++ b/playground/css-lightningcss-proxy/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "devDependencies": { - "lightningcss": "^1.24.0", - "express": "^4.18.2" + "lightningcss": "^1.25.1", + "express": "^4.19.2" } } diff --git a/playground/css-lightningcss/package.json b/playground/css-lightningcss/package.json index 1ebca75d1ab941..6844ef9ffb0959 100644 --- a/playground/css-lightningcss/package.json +++ b/playground/css-lightningcss/package.json @@ -9,6 +9,6 @@ "preview": "vite preview" }, "devDependencies": { - "lightningcss": "^1.24.0" + "lightningcss": "^1.25.1" } } diff --git a/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts new file mode 100644 index 00000000000000..5110ef3a77ff7b --- /dev/null +++ b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' +import { expectWithRetry, getColor, isBuild, listAssets } from '~utils' + +test('should load all stylesheets', async () => { + expect(await getColor('.shared-linked')).toBe('blue') + await expectWithRetry(() => getColor('.async-js')).toBe('blue') +}) + +describe.runIf(isBuild)('build', () => { + test('should remove empty chunk', async () => { + const assets = listAssets() + expect(assets).not.toContainEqual( + expect.stringMatching(/shared-linked-.*\.js$/), + ) + expect(assets).not.toContainEqual(expect.stringMatching(/async-js-.*\.js$/)) + }) +}) diff --git a/playground/css-no-codesplit/async-js.css b/playground/css-no-codesplit/async-js.css new file mode 100644 index 00000000000000..ed61a7f513c277 --- /dev/null +++ b/playground/css-no-codesplit/async-js.css @@ -0,0 +1,3 @@ +.async-js { + color: blue; +} diff --git a/playground/css-no-codesplit/async-js.js b/playground/css-no-codesplit/async-js.js new file mode 100644 index 00000000000000..2ce31a1e741d2d --- /dev/null +++ b/playground/css-no-codesplit/async-js.js @@ -0,0 +1,2 @@ +// a JS file that becomes an empty file but imports CSS files +import './async-js.css' diff --git a/playground/css-no-codesplit/index.html b/playground/css-no-codesplit/index.html new file mode 100644 index 00000000000000..e7673c84e45933 --- /dev/null +++ b/playground/css-no-codesplit/index.html @@ -0,0 +1,5 @@ + + + + +async JS importing CSS: this should be blue
diff --git a/playground/css-no-codesplit/index.js b/playground/css-no-codesplit/index.js new file mode 100644 index 00000000000000..44b33fda36a9cd --- /dev/null +++ b/playground/css-no-codesplit/index.js @@ -0,0 +1 @@ +import('./async-js') diff --git a/playground/css-no-codesplit/package.json b/playground/css-no-codesplit/package.json new file mode 100644 index 00000000000000..61d806d3d264fa --- /dev/null +++ b/playground/css-no-codesplit/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-css-no-codesplit", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/css-no-codesplit/shared-linked.css b/playground/css-no-codesplit/shared-linked.css new file mode 100644 index 00000000000000..51857a50efca1f --- /dev/null +++ b/playground/css-no-codesplit/shared-linked.css @@ -0,0 +1,3 @@ +.shared-linked { + color: blue; +} diff --git a/playground/css-no-codesplit/sub.html b/playground/css-no-codesplit/sub.html new file mode 100644 index 00000000000000..f535a771d06482 --- /dev/null +++ b/playground/css-no-codesplit/sub.html @@ -0,0 +1 @@ + diff --git a/playground/css-no-codesplit/vite.config.js b/playground/css-no-codesplit/vite.config.js new file mode 100644 index 00000000000000..f48d875832b928 --- /dev/null +++ b/playground/css-no-codesplit/vite.config.js @@ -0,0 +1,14 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + cssCodeSplit: false, + rollupOptions: { + input: { + index: resolve(__dirname, './index.html'), + sub: resolve(__dirname, './sub.html'), + }, + }, + }, +}) diff --git a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts index 6706b5dcaae510..6c6472c848823d 100644 --- a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts +++ b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts @@ -138,6 +138,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.sass", @@ -158,6 +159,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.module.sass", @@ -178,6 +180,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.less", @@ -200,6 +203,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE,cAAM", "sources": [ "/root/imported.styl", diff --git a/playground/css-sourcemap/package.json b/playground/css-sourcemap/package.json index 143a60950f7436..11c26f182ae79d 100644 --- a/playground/css-sourcemap/package.json +++ b/playground/css-sourcemap/package.json @@ -11,9 +11,9 @@ }, "devDependencies": { "less": "^4.2.0", - "magic-string": "^0.30.7", - "sass": "^1.71.1", - "stylus": "^0.62.0", + "magic-string": "^0.30.10", + "sass": "^1.77.2", + "stylus": "^0.63.0", "sugarss": "^4.0.1" } } diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index 89226a8fbd5ba1..cb7af939bbd152 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -448,6 +448,16 @@ test('?raw', async () => { expect(await rawImportCss.textContent()).toBe( readFileSync(require.resolve('../raw-imported.css'), 'utf-8'), ) + + if (!isBuild) { + editFile('raw-imported.css', (code) => + code.replace('color: yellow', 'color: blue'), + ) + await untilUpdated( + () => page.textContent('.raw-imported-css'), + 'color: blue', + ) + } }) test('import css in less', async () => { @@ -533,3 +543,8 @@ test.runIf(isBuild)('manual chunk path', async () => { findAssetFile(/dir\/dir2\/manual-chunk-[-\w]{8}\.css$/), ).not.toBeUndefined() }) + +test.runIf(isBuild)('CSS modules should be treeshaken if not used', () => { + const css = findAssetFile(/\.css$/, undefined, undefined, true) + expect(css).not.toContain('treeshake-module-b') +}) diff --git a/playground/css/index.html b/playground/css/index.html index 508744160526de..a0e92b205e79f6 100644 --- a/playground/css/index.html +++ b/playground/css/index.html @@ -105,6 +105,8 @@Imported SASS module:
+CSS modules should treeshake in build
+Imported compose/from CSS/SASS module:
CSS modules composes path resolving: this should be turquoise diff --git a/playground/css/main.js b/playground/css/main.js index 8b3eb488fe813b..05a9c426f3419c 100644 --- a/playground/css/main.js +++ b/playground/css/main.js @@ -20,6 +20,11 @@ import sassMod from './mod.module.scss' document.querySelector('.modules-sass').classList.add(sassMod['apply-color']) text('.modules-sass-code', JSON.stringify(sassMod, null, 2)) +import { a as treeshakeMod } from './treeshake-module/index.js' +document + .querySelector('.modules-treeshake') + .classList.add(treeshakeMod()['treeshake-module-a']) + import composesPathResolvingMod from './composes-path-resolving.module.css' document .querySelector('.path-resolved-modules-css') diff --git a/playground/css/package.json b/playground/css/package.json index b2aec5504ae22c..ef7a2eb0688474 100644 --- a/playground/css/package.json +++ b/playground/css/package.json @@ -24,8 +24,8 @@ "fast-glob": "^3.3.2", "less": "^4.2.0", "postcss-nested": "^6.0.1", - "sass": "^1.71.1", - "stylus": "^0.62.0", + "sass": "^1.77.2", + "stylus": "^0.63.0", "sugarss": "^4.0.1" }, "imports": { diff --git a/playground/css/treeshake-module/a.js b/playground/css/treeshake-module/a.js new file mode 100644 index 00000000000000..7272fa1dc1d9c1 --- /dev/null +++ b/playground/css/treeshake-module/a.js @@ -0,0 +1,5 @@ +import style from './a.module.css' + +export function a() { + return style +} diff --git a/playground/css/treeshake-module/a.module.css b/playground/css/treeshake-module/a.module.css new file mode 100644 index 00000000000000..72ab1a9fdb001a --- /dev/null +++ b/playground/css/treeshake-module/a.module.css @@ -0,0 +1,3 @@ +.treeshake-module-a { + color: red; +} diff --git a/playground/css/treeshake-module/b.js b/playground/css/treeshake-module/b.js new file mode 100644 index 00000000000000..b3db996f7f64cd --- /dev/null +++ b/playground/css/treeshake-module/b.js @@ -0,0 +1,5 @@ +import style from './b.module.css' + +export function b() { + return style +} diff --git a/playground/css/treeshake-module/b.module.css b/playground/css/treeshake-module/b.module.css new file mode 100644 index 00000000000000..5ad402ef7353e8 --- /dev/null +++ b/playground/css/treeshake-module/b.module.css @@ -0,0 +1,3 @@ +.treeshake-module-b { + color: red; +} diff --git a/playground/css/treeshake-module/index.js b/playground/css/treeshake-module/index.js new file mode 100644 index 00000000000000..67332c5a21eb3d --- /dev/null +++ b/playground/css/treeshake-module/index.js @@ -0,0 +1,2 @@ +export { a } from './a.js' +export { b } from './b.js' diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts index 3892251bfd2e41..56f0fbc294661f 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -1,5 +1,12 @@ import { expect, test } from 'vitest' -import { getColor, isBuild, page, serverLogs, untilUpdated } from '~utils' +import { + findAssetFile, + getColor, + isBuild, + page, + serverLogs, + untilUpdated, +} from '~utils' test('should load literal dynamic import', async () => { await page.click('.baz') @@ -170,3 +177,9 @@ test.runIf(isBuild)( ) }, ) + +test.runIf(isBuild)('should not preload for non-analyzable urls', () => { + const js = findAssetFile(/index-[-\w]{8}\.js$/) + // should match e.g. await import(e.jss);o(".view",p===i) + expect(js).to.match(/\.jss\);/) +}) diff --git a/playground/extensions/package.json b/playground/extensions/package.json index 1c07fb97ff3c49..68cb75360d3840 100644 --- a/playground/extensions/package.json +++ b/playground/extensions/package.json @@ -10,6 +10,6 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/dep-that-imports/package.json b/playground/external/dep-that-imports/package.json index bbf185932fcfd1..bae6b993a48c61 100644 --- a/playground/external/dep-that-imports/package.json +++ b/playground/external/dep-that-imports/package.json @@ -5,6 +5,6 @@ "dependencies": { "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/dep-that-requires/package.json b/playground/external/dep-that-requires/package.json index 9655cb314df0d5..7c56ad60573d2e 100644 --- a/playground/external/dep-that-requires/package.json +++ b/playground/external/dep-that-requires/package.json @@ -5,6 +5,6 @@ "dependencies": { "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/package.json b/playground/external/package.json index dcc8140c248d7e..934e0dc6ca0202 100644 --- a/playground/external/package.json +++ b/playground/external/package.json @@ -17,7 +17,7 @@ "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", "vite": "workspace:*", - "vue": "^3.4.20", + "vue": "^3.4.27", "vue32": "npm:vue@~3.2.0" } } diff --git a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts new file mode 100644 index 00000000000000..fb60922e86e1ae --- /dev/null +++ b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' +import { isServe, page, viteTestUrl } from '~utils' + +describe.runIf(isServe)('main', () => { + test('**/deny/** should deny src/deny/deny.txt', async () => { + const res = await page.request.fetch( + new URL('/src/deny/deny.txt', viteTestUrl).href, + ) + expect(res.status()).toBe(403) + }) + test('**/deny/** should deny src/deny/.deny', async () => { + const res = await page.request.fetch( + new URL('/src/deny/.deny', viteTestUrl).href, + ) + expect(res.status()).toBe(403) + }) +}) diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json index b66b79268d8601..f71a082b890c6a 100644 --- a/playground/fs-serve/package.json +++ b/playground/fs-serve/package.json @@ -10,6 +10,9 @@ "preview": "vite preview root", "dev:base": "vite root --config ./root/vite.config-base.js", "build:base": "vite build root --config ./root/vite.config-base.js", - "preview:base": "vite preview root --config ./root/vite.config-base.js" + "preview:base": "vite preview root --config ./root/vite.config-base.js", + "dev:deny": "vite root --config ./root/vite.config-deny.js", + "build:deny": "vite build root --config ./root/vite.config-deny.js", + "preview:deny": "vite preview root --config ./root/vite.config-deny.js" } } diff --git a/playground/fs-serve/root/src/deny/.deny b/playground/fs-serve/root/src/deny/.deny new file mode 100644 index 00000000000000..73bd3960853c61 --- /dev/null +++ b/playground/fs-serve/root/src/deny/.deny @@ -0,0 +1 @@ +.deny diff --git a/playground/fs-serve/root/src/deny/deny.txt b/playground/fs-serve/root/src/deny/deny.txt new file mode 100644 index 00000000000000..f9df83416f8a72 --- /dev/null +++ b/playground/fs-serve/root/src/deny/deny.txt @@ -0,0 +1 @@ +deny diff --git a/playground/fs-serve/root/vite.config-deny.js b/playground/fs-serve/root/vite.config-deny.js new file mode 100644 index 00000000000000..27501c55f38180 --- /dev/null +++ b/playground/fs-serve/root/vite.config-deny.js @@ -0,0 +1,22 @@ +import path from 'node:path' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: path.resolve(__dirname, 'src/index.html'), + }, + }, + }, + server: { + fs: { + strict: true, + allow: [path.resolve(__dirname, 'src')], + deny: ['**/deny/**'], + }, + }, + define: { + ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')), + }, +}) diff --git a/playground/hasWindowsUnicodeFsBug.js b/playground/hasWindowsUnicodeFsBug.js deleted file mode 100644 index c46dd2a5545392..00000000000000 --- a/playground/hasWindowsUnicodeFsBug.js +++ /dev/null @@ -1,10 +0,0 @@ -import os from 'node:os' - -const isWindows = os.platform() === 'win32' -const nodeVersionArray = process.versions.node.split('.') -// ignore some files due to https://github.com/nodejs/node/issues/48673 -// node <=21.0.0 and ^20.4.0 has the bug -export const hasWindowsUnicodeFsBug = - isWindows && - (+nodeVersionArray[0] > 20 || - (+nodeVersionArray[0] === 20 && +nodeVersionArray[1] >= 4)) diff --git a/playground/hmr-ssr/__tests__/hmr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts similarity index 95% rename from playground/hmr-ssr/__tests__/hmr.spec.ts rename to playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index 0998ee8ddb406f..6a2b3763b3ffec 100644 --- a/playground/hmr-ssr/__tests__/hmr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -7,7 +7,14 @@ import type { InlineConfig, Logger, ViteDevServer } from 'vite' import { createServer, createViteRuntime } from 'vite' import type { ViteRuntime } from 'vite/runtime' import type { RollupError } from 'rollup' -import { page, promiseWithResolvers, slash, untilUpdated } from '~utils' +import { + addFile, + page, + promiseWithResolvers, + readFile, + slash, + untilUpdated, +} from '~utils' let server: ViteDevServer const clientLogs: string[] = [] @@ -246,7 +253,7 @@ describe('hmr works correctly', () => { }) // TODO - // test.skipIf(hasWindowsUnicodeFsBug)('full-reload encodeURI path', async () => { + // test('full-reload encodeURI path', async () => { // await page.goto( // viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', // ) @@ -737,31 +744,19 @@ test.todo('should hmr when file is deleted and restored', async () => { ) await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child1') + // delete the file editFile(parentFile, (code) => code.replace( "export { value as childValue } from './child'", "export const childValue = 'not-child'", ), ) + const originalChildFileCode = readFile(childFile) removeFile(childFile) await untilUpdated(() => hmr('.file-delete-restore'), 'parent:not-child') - createFile( - childFile, - ` -import { rerender } from './runtime' - -export const value = 'child' - -if (import.meta.hot) { - import.meta.hot.accept((newMod) => { - if (!newMod) return - - rerender({ child: newMod.value }) - }) -} -`, - ) + // restore the file + createFile(childFile, originalChildFileCode) editFile(parentFile, (code) => code.replace( "export const childValue = 'not-child'", @@ -822,6 +817,45 @@ test.todo('delete file should not break hmr', async () => { ) }) +test.todo( + 'deleted file should trigger dispose and prune callbacks', + async () => { + await setupViteRuntime('/hmr.ts') + + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' + + // delete the file + editFile(parentFile, (code) => + code.replace( + "export { value as childValue } from './child'", + "export const childValue = 'not-child'", + ), + ) + const originalChildFileCode = readFile(childFile) + removeFile(childFile) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ) + expect(clientLogs).to.include('file-delete-restore/child.js is disposed') + expect(clientLogs).to.include('file-delete-restore/child.js is pruned') + + // restore the file + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }, +) + test('import.meta.hot?.accept', async () => { await setupViteRuntime('/hmr.ts') await untilConsoleLogAfter( diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 0d95557aa65fb3..d4281ec1bbe5ae 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1,13 +1,15 @@ import { beforeAll, describe, expect, it, test } from 'vitest' -import { hasWindowsUnicodeFsBug } from '../../hasWindowsUnicodeFsBug' +import type { Page } from 'playwright-chromium' import { addFile, + browser, browserLogs, editFile, getBg, getColor, isBuild, page, + readFile, removeFile, serverLogs, untilBrowserLogAfter, @@ -152,7 +154,7 @@ if (!isBuild) { }) test('invalidate', async () => { - const el = await page.$('.invalidation') + const el = await page.$('.invalidation-parent') await untilBrowserLogAfter( () => editFile('invalidation/child.js', (code) => @@ -174,6 +176,47 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), 'child updated') }) + test('invalidate works with multiple tabs', async () => { + let page2: Page + try { + page2 = await browser.newPage() + await page2.goto(viteTestUrl) + + const el = await page.$('.invalidation-parent') + await untilBrowserLogAfter( + () => + editFile('invalidation/child.js', (code) => + code.replace('child', 'child updated'), + ), + [ + '>>> vite:beforeUpdate -- update', + '>>> vite:invalidate -- /invalidation/child.js', + '[vite] invalidate /invalidation/child.js', + '[vite] hot updated: /invalidation/child.js', + '>>> vite:afterUpdate -- update', + // if invalidate dedupe doesn't work correctly, this beforeUpdate will be called twice + '>>> vite:beforeUpdate -- update', + '(invalidation) parent is executing', + '[vite] hot updated: /invalidation/parent.js', + '>>> vite:afterUpdate -- update', + ], + true, + ) + await untilUpdated(() => el.textContent(), 'child updated') + } finally { + await page2.close() + } + }) + + test('invalidate on root triggers page reload', async () => { + editFile('invalidation/root.js', (code) => code.replace('Init', 'Updated')) + await page.waitForEvent('load') + await untilUpdated( + async () => (await page.$('.invalidation-root')).textContent(), + 'Updated', + ) + }) + test('soft invalidate', async () => { const el = await page.$('.soft-invalidation') expect(await el.textContent()).toBe( @@ -218,24 +261,21 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), '3') }) - test.skipIf(hasWindowsUnicodeFsBug)( - 'full-reload encodeURI path', - async () => { - await page.goto( - viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', - ) - const el = await page.$('#app') - expect(await el.textContent()).toBe('title') - editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => - code.replace('title', 'title2'), - ) - await page.waitForEvent('load') - await untilUpdated( - async () => (await page.$('#app')).textContent(), - 'title2', - ) - }, - ) + test('full-reload encodeURI path', async () => { + await page.goto( + viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', + ) + const el = await page.$('#app') + expect(await el.textContent()).toBe('title') + editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => + code.replace('title', 'title2'), + ) + await page.waitForEvent('load') + await untilUpdated( + async () => (await page.$('#app')).textContent(), + 'title2', + ) + }) test('CSS update preserves query params', async () => { await page.goto(viteTestUrl) @@ -784,40 +824,31 @@ if (!isBuild) { 'parent:child1', ) + // delete the file editFile(parentFile, (code) => code.replace( "export { value as childValue } from './child'", "export const childValue = 'not-child'", ), ) + const originalChildFileCode = readFile(childFile) removeFile(childFile) await untilUpdated( () => page.textContent('.file-delete-restore'), 'parent:not-child', ) - addFile( - childFile, - ` -import { rerender } from './runtime' - -export const value = 'child' - -if (import.meta.hot) { - import.meta.hot.accept((newMod) => { - if (!newMod) return - - rerender({ child: newMod.value }) - }) -} -`, - ) - editFile(parentFile, (code) => - code.replace( - "export const childValue = 'not-child'", - "export { value as childValue } from './child'", - ), - ) + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await loadPromise + }, [/connected/]) await untilUpdated( () => page.textContent('.file-delete-restore'), 'parent:child', @@ -875,6 +906,42 @@ if (import.meta.hot) { ) }) + test('deleted file should trigger dispose and prune callbacks', async () => { + await page.goto(viteTestUrl) + + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' + + // delete the file + editFile(parentFile, (code) => + code.replace( + "export { value as childValue } from './child'", + "export const childValue = 'not-child'", + ), + ) + const originalChildFileCode = readFile(childFile) + removeFile(childFile) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ) + expect(browserLogs).to.include('file-delete-restore/child.js is disposed') + expect(browserLogs).to.include('file-delete-restore/child.js is pruned') + + // restore the file + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }) + test('import.meta.hot?.accept', async () => { const el = await page.$('.optional-chaining') await untilBrowserLogAfter( @@ -938,4 +1005,23 @@ if (import.meta.hot) { editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`)) await untilUpdated(() => getColor('.css-deps'), 'green') }) + + test('hmr should happen after missing file is created', async () => { + const file = 'missing-file/a.js' + const code = 'console.log("a.js")' + + await untilBrowserLogAfter( + () => + page.goto(viteTestUrl + '/missing-file/index.html', { + waitUntil: 'load', + }), + /connected/, // wait for HMR connection + ) + + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(file, code) + await loadPromise + }, [/connected/, 'a.js']) + }) } diff --git a/playground/hmr/file-delete-restore/child.js b/playground/hmr/file-delete-restore/child.js index 704c7d8c7e98cc..7031ef7db067c3 100644 --- a/playground/hmr/file-delete-restore/child.js +++ b/playground/hmr/file-delete-restore/child.js @@ -8,4 +8,12 @@ if (import.meta.hot) { rerender({ child: newMod.value }) }) + + import.meta.hot.dispose(() => { + console.log('file-delete-restore/child.js is disposed') + }) + + import.meta.hot.prune(() => { + console.log('file-delete-restore/child.js is pruned') + }) } diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 5e572f83b703aa..1f764da0861d6f 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -1,7 +1,6 @@ import { virtual } from 'virtual:file' import { foo as depFoo, nestedFoo } from './hmrDep' import './importing-updated' -import './invalidation/parent' import './file-delete-restore' import './optional-chaining/parent' import './intermediate-file-delete' diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 221a3bf39e1705..d5adaab6bd5629 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -7,6 +7,7 @@