diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index fd5982b73c05f9..d8d2bc3a2aaff6 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -1,6 +1,6 @@ import fs from 'fs' import path from 'path' -import { getBg, getColor, browserLogs, isBuild, testDir } from '../../testUtils' +import { getBg, getColor, isBuild, testDir } from '../../testUtils' const assetMatch = isBuild ? /\/foo\/assets\/asset\.\w{8}\.png/ @@ -9,7 +9,7 @@ const assetMatch = isBuild const iconMatch = isBuild ? `/foo/icon.png` : `icon.png` test('should have no 404s', () => { - browserLogs.forEach((msg) => { + pageLogs.forEach((msg) => { expect(msg).not.toMatch('404') }) }) diff --git a/packages/playground/hmr/__tests__/hmr.spec.ts b/packages/playground/hmr/__tests__/hmr.spec.ts new file mode 100644 index 00000000000000..3bd388eb16d2f4 --- /dev/null +++ b/packages/playground/hmr/__tests__/hmr.spec.ts @@ -0,0 +1,113 @@ +import { isBuild, editFile, untilUpdated } from '../../testUtils' + +test('should render', async () => { + expect(await page.textContent('.app')).toBe('1') + expect(await page.textContent('.dep')).toBe('1') + expect(await page.textContent('.nested')).toBe('1') +}) + +if (!isBuild) { + test('should connect', async () => { + expect(pageLogs.length).toBe(2) + expect(pageLogs.some((msg) => msg.match('connected'))).toBe(true) + pageLogs.length = 0 + }) + + test('self accept', async () => { + const el = await page.$('.app') + + editFile('hmr.js', (code) => code.replace('const foo = 1', 'const foo = 2')) + await untilUpdated(() => el.textContent(), '2') + + expect(pageLogs).toMatchObject([ + 'foo was: 1', + '(self-accepting 1) foo is now: 2', + '(self-accepting 2) foo is now: 2', + '[vite] hot updated: /hmr.js' + ]) + pageLogs.length = 0 + + editFile('hmr.js', (code) => code.replace('const foo = 2', 'const foo = 3')) + await untilUpdated(() => el.textContent(), '3') + + expect(pageLogs).toMatchObject([ + 'foo was: 2', + '(self-accepting 1) foo is now: 3', + '(self-accepting 2) foo is now: 3', + '[vite] hot updated: /hmr.js' + ]) + pageLogs.length = 0 + }) + + test('accept dep', async () => { + const el = await page.$('.dep') + + editFile('hmrDep.js', (code) => + code.replace('const foo = 1', 'const foo = 2') + ) + await untilUpdated(() => el.textContent(), '2') + + expect(pageLogs).toMatchObject([ + '(dep) foo was: 1', + '(dep) foo from dispose: 1', + '(single dep) foo is now: 2', + '(single dep) nested foo is now: 1', + '(multi deps) foo is now: 2', + '(multi deps) nested foo is now: 1', + '[vite] hot updated: /hmrDep.js via /hmr.js' + ]) + pageLogs.length = 0 + + editFile('hmrDep.js', (code) => + code.replace('const foo = 2', 'const foo = 3') + ) + await untilUpdated(() => el.textContent(), '3') + + expect(pageLogs).toMatchObject([ + '(dep) foo was: 2', + '(dep) foo from dispose: 2', + '(single dep) foo is now: 3', + '(single dep) nested foo is now: 1', + '(multi deps) foo is now: 3', + '(multi deps) nested foo is now: 1', + '[vite] hot updated: /hmrDep.js via /hmr.js' + ]) + pageLogs.length = 0 + }) + + test('nested dep propagation', async () => { + const el = await page.$('.nested') + + editFile('hmrNestedDep.js', (code) => + code.replace('const foo = 1', 'const foo = 2') + ) + await untilUpdated(() => el.textContent(), '2') + + expect(pageLogs).toMatchObject([ + '(dep) foo was: 3', + '(dep) foo from dispose: 3', + '(single dep) foo is now: 3', + '(single dep) nested foo is now: 2', + '(multi deps) foo is now: 3', + '(multi deps) nested foo is now: 2', + '[vite] hot updated: /hmrDep.js via /hmr.js' + ]) + pageLogs.length = 0 + + editFile('hmrNestedDep.js', (code) => + code.replace('const foo = 2', 'const foo = 3') + ) + await untilUpdated(() => el.textContent(), '3') + + expect(pageLogs).toMatchObject([ + '(dep) foo was: 3', + '(dep) foo from dispose: 3', + '(single dep) foo is now: 3', + '(single dep) nested foo is now: 3', + '(multi deps) foo is now: 3', + '(multi deps) nested foo is now: 3', + '[vite] hot updated: /hmrDep.js via /hmr.js' + ]) + pageLogs.length = 0 + }) +} diff --git a/packages/playground/hmr/hmr.js b/packages/playground/hmr/hmr.js index 2ff3b070d37f1f..0e470c74e27c27 100644 --- a/packages/playground/hmr/hmr.js +++ b/packages/playground/hmr/hmr.js @@ -1,26 +1,39 @@ -import './hmrDep.js' -import './style.css' +import { foo as depFoo, nestedFoo } from './hmrDep' -export const foo = 222233 +export const foo = 1 +text('.app', foo) +text('.dep', depFoo) +text('.nested', nestedFoo) if (import.meta.hot) { import.meta.hot.accept(({ foo }) => { - console.log('(self-accepting)1.foo is now:', foo) + console.log('(self-accepting 1) foo is now:', foo) }) import.meta.hot.accept(({ foo }) => { - console.log('(self-accepting)2.foo is now:', foo) + console.log('(self-accepting 2) foo is now:', foo) }) - import.meta.hot.accept('./hmrDep', ({ foo }) => { - console.log('(single dep) foo is now:', foo) + const handleDep = (type, newFoo, newNestedFoo) => { + console.log(`(${type}) foo is now: ${newFoo}`) + console.log(`(${type}) nested foo is now: ${newNestedFoo}`) + text('.dep', newFoo) + text('.nested', newNestedFoo) + } + + import.meta.hot.accept('./hmrDep', ({ foo, nestedFoo }) => { + handleDep('single dep', foo, nestedFoo) }) - import.meta.hot.accept(['./hmrDep'], (modules) => { - console.log('(multiple deps) foo is now:', modules[0].foo) + import.meta.hot.accept(['./hmrDep'], ([{ foo, nestedFoo }]) => { + handleDep('multi deps', foo, nestedFoo) }) import.meta.hot.dispose(() => { console.log(`foo was:`, foo) }) } + +function text(el, text) { + document.querySelector(el).textContent = text +} diff --git a/packages/playground/hmr/hmrDep.js b/packages/playground/hmr/hmrDep.js index c249a0cbf94ecc..9142a6c87cabf3 100644 --- a/packages/playground/hmr/hmrDep.js +++ b/packages/playground/hmr/hmrDep.js @@ -1,6 +1,5 @@ -import './hmrNestedDep' - -export const foo = 'from depppp' +export const foo = 1 +export { foo as nestedFoo } from './hmrNestedDep' if (import.meta.hot) { const data = import.meta.hot.data @@ -10,6 +9,6 @@ if (import.meta.hot) { import.meta.hot.dispose((data) => { console.log(`(dep) foo was: ${foo}`) - data.fromDispose = foo * 1033 + data.fromDispose = foo }) } diff --git a/packages/playground/hmr/hmrNestedDep.js b/packages/playground/hmr/hmrNestedDep.js index 7972d09effb427..766766a6260612 100644 --- a/packages/playground/hmr/hmrNestedDep.js +++ b/packages/playground/hmr/hmrNestedDep.js @@ -1 +1 @@ -console.log('I should log?') +export const foo = 1 diff --git a/packages/playground/hmr/index.html b/packages/playground/hmr/index.html new file mode 100644 index 00000000000000..3a421c5bba15e5 --- /dev/null +++ b/packages/playground/hmr/index.html @@ -0,0 +1,5 @@ + + +
+
+
\ No newline at end of file diff --git a/packages/playground/hmr/package.json b/packages/playground/hmr/package.json new file mode 100644 index 00000000000000..97fb6577fb4d28 --- /dev/null +++ b/packages/playground/hmr/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-hmr", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../vite/bin/vite" + } +} diff --git a/packages/playground/testEnv.d.ts b/packages/playground/testEnv.d.ts index ebc9910e6fc953..d0f1cc057508fa 100644 --- a/packages/playground/testEnv.d.ts +++ b/packages/playground/testEnv.d.ts @@ -3,5 +3,6 @@ import { Page } from 'playwright-chromium' declare global { // injected by the custom jest env in scripts/jestEnv.js const page: Page + const pageLogs: string[] const __viteTestDir__: string } diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts index f079546325a773..9210c126b39958 100644 --- a/packages/playground/testUtils.ts +++ b/packages/playground/testUtils.ts @@ -8,14 +8,6 @@ import slash from 'slash' import colors from 'css-color-names' import { ElementHandle } from 'playwright-chromium' -export const browserLogs = [] - -beforeAll(() => { - page.on('console', (msg) => { - browserLogs.push(msg.text()) - }) -}) - export const isBuild = !!process.env.VITE_TEST_BUILD const testPath = expect.getState().testPath @@ -79,7 +71,7 @@ export function editFile(filename: string, replacer: (str: string) => string) { * Poll a getter until the value it returns includes the expected value. */ export async function untilUpdated( - poll: () => Promise, + poll: () => string | Promise, expected: string ) { if (isBuild) return diff --git a/packages/playground/vue/__tests__/vue.spec.ts b/packages/playground/vue/__tests__/vue.spec.ts index 915c656c8f010f..606c9ed2a3c533 100644 --- a/packages/playground/vue/__tests__/vue.spec.ts +++ b/packages/playground/vue/__tests__/vue.spec.ts @@ -1,10 +1,4 @@ -import { - browserLogs, - editFile, - getColor, - isBuild, - untilUpdated -} from 'testUtils' +import { editFile, getColor, isBuild, untilUpdated } from 'testUtils' test('should render', async () => { expect(await page.textContent('h1')).toMatch('Vue SFCs') @@ -74,7 +68,7 @@ describe('template asset reference', () => { : '/assets/asset.png' test('should not 404', () => { - browserLogs.forEach((msg) => { + pageLogs.forEach((msg) => { expect(msg).not.toMatch('404') }) }) diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index e7fd4e6f701dfc..27c3f77f829571 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -278,7 +278,7 @@ async function fetchUpdate({ path, accpetedPath, timestamp }: Update) { // dep update for (const { deps } of mod.callbacks) { deps.forEach((dep) => { - if (accpetedPath.startsWith(dep)) { + if (accpetedPath === dep) { modulesToUpdate.add(dep) } }) diff --git a/packages/vite/src/node/plugins/importsAnalysis.ts b/packages/vite/src/node/plugins/importsAnalysis.ts index 9c7b045f5127a1..3fb06b7c685287 100644 --- a/packages/vite/src/node/plugins/importsAnalysis.ts +++ b/packages/vite/src/node/plugins/importsAnalysis.ts @@ -254,11 +254,13 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set() - acceptedUrls.forEach(({ url, start, end }) => { - const normalized = toAbsoluteUrl(markExplicitImport(url)) + for (const { url, start, end } of acceptedUrls) { + const [normalized] = await moduleGraph.resolveUrl( + toAbsoluteUrl(markExplicitImport(url)) + ) normalizedAcceptedUrls.add(normalized) str().overwrite(start, end, JSON.stringify(normalized)) - }) + } // update the module graph for HMR analysis. // node CSS imports does its own graph update in the css plugin so we diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 14abf67ea70a37..3cae68ea83220a 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -3,6 +3,7 @@ import { isCSSRequest } from '../plugins/css' import { cleanUrl, normalizePath, removeTimestampQuery } from '../utils' import { TransformResult } from './transformRequest' import { PluginContainer } from './pluginContainer' +import { parse as parseUrl } from 'url' export class ModuleNode { /** @@ -157,9 +158,9 @@ export class ModuleGraph { url = removeTimestampQuery(url) const resolvedId = (await this.container.resolveId(url)).id const ext = extname(cleanUrl(resolvedId)) - const [pathname, query] = url.split('?') - if (ext && !pathname.endsWith(ext)) { - url = pathname + ext + (query ? `?${query}` : ``) + const { pathname, search, hash } = parseUrl(url) + if (ext && !pathname!.endsWith(ext)) { + url = pathname + ext + (search || '') + (hash || '') } return [url, resolvedId] } diff --git a/scripts/jestPerTestSetup.ts b/scripts/jestPerTestSetup.ts index 63033164ef468b..fc29a9f5d079d8 100644 --- a/scripts/jestPerTestSetup.ts +++ b/scripts/jestPerTestSetup.ts @@ -15,6 +15,8 @@ let server: ViteDevServer | http.Server let tempDir: string let err: Error +const logs = ((global as any).pageLogs = []) + beforeAll(async () => { try { const testPath = expect.getState().testPath @@ -46,6 +48,10 @@ beforeAll(async () => { } } + page.on('console', (msg) => { + logs.push(msg.text()) + }) + if (!isBuildTest) { server = await (await createServer(options)).listen() // use resolved port from server