Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { b } from './b.js'

await Promise.resolve()

export const a = 'a'
export const getB = () => b
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { a } from './a.js'

await Promise.resolve()

export const b = 'b'
export const getA = () => a
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { a, getB } from './a.js'
import { b, getA } from './b.js'

export { a, b, getA, getB }
142 changes: 59 additions & 83 deletions packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, onTestFinished } from 'vitest'
import { assert, describe, expect, onTestFinished, vi } from 'vitest'
import { promiseWithResolvers } from '../../../../shared/utils'
import { createModuleRunnerTester } from './utils'

describe(
Expand Down Expand Up @@ -26,21 +27,26 @@ describe(
const fixtureC = '/fixtures/c.ts'
const fixtureD = '/fixtures/d.ts'

expect(runner.hmrClient!.hotModulesMap.size).toBe(2)
expect(runner.hmrClient!.dataMap.size).toBe(2)
expect(runner.hmrClient!.ctxToListenersMap.size).toBe(2)
assert(runner.hmrClient)
expect(runner.hmrClient.hotModulesMap.size).toBe(2)
expect(runner.hmrClient.dataMap.size).toBe(2)
expect(runner.hmrClient.ctxToListenersMap.size).toBe(2)

for (const fixture of [fixtureC, fixtureD]) {
expect(runner.hmrClient!.hotModulesMap.has(fixture)).toBe(true)
expect(runner.hmrClient!.dataMap.has(fixture)).toBe(true)
expect(runner.hmrClient!.ctxToListenersMap.has(fixture)).toBe(true)
expect(runner.hmrClient.hotModulesMap.has(fixture)).toBe(true)
expect(runner.hmrClient.dataMap.has(fixture)).toBe(true)
expect(runner.hmrClient.ctxToListenersMap.has(fixture)).toBe(true)
}
})

it('does not expose partial exports during concurrent updates', async ({
runner,
}) => {
const testGlobal = globalThis as any
const sharedUrl = '/fixtures/hmr-reexport-race/shared.js'
const coreUrl = '/fixtures/hmr-reexport-race/core.js'
const entryAUrl = '/fixtures/hmr-reexport-race/entry-a.js'
const entryBUrl = '/fixtures/hmr-reexport-race/entry-b.js'

testGlobal.__vite_ssr_hmr_reexport_race__ = {
wait: () => Promise.resolve(),
Expand All @@ -49,34 +55,22 @@ describe(
delete testGlobal.__vite_ssr_hmr_reexport_race__
})

await runner.import('/fixtures/hmr-reexport-race/entry-a.js')
await runner.import('/fixtures/hmr-reexport-race/entry-b.js')

const sharedModule = runner.evaluatedModules.getModuleByUrl(
'/fixtures/hmr-reexport-race/shared.js',
)
const coreModule = runner.evaluatedModules.getModuleByUrl(
'/fixtures/hmr-reexport-race/core.js',
)
const entryAModule = runner.evaluatedModules.getModuleByUrl(
'/fixtures/hmr-reexport-race/entry-a.js',
)
const entryBModule = runner.evaluatedModules.getModuleByUrl(
'/fixtures/hmr-reexport-race/entry-b.js',
)
expect(sharedModule).toBeDefined()
expect(coreModule).toBeDefined()
expect(entryAModule).toBeDefined()
expect(entryBModule).toBeDefined()

let waitStarted!: () => void
const waitStartedPromise = new Promise<void>((resolve) => {
waitStarted = resolve
})
let releaseWait!: () => void
const waitPromise = new Promise<void>((resolve) => {
releaseWait = resolve
})
await runner.import(entryAUrl)
await runner.import(entryBUrl)

const sharedModule = runner.evaluatedModules.getModuleByUrl(sharedUrl)
const coreModule = runner.evaluatedModules.getModuleByUrl(coreUrl)
const entryAModule = runner.evaluatedModules.getModuleByUrl(entryAUrl)
const entryBModule = runner.evaluatedModules.getModuleByUrl(entryBUrl)
assert(sharedModule)
assert(coreModule)
assert(entryAModule)
assert(entryBModule)

const { promise: waitStartedPromise, resolve: waitStarted } =
promiseWithResolvers<void>()
const { promise: waitPromise, resolve: releaseWait } =
promiseWithResolvers<void>()

testGlobal.__vite_ssr_hmr_reexport_race__ = {
wait: () => {
Expand All @@ -86,31 +80,26 @@ describe(
}

for (const module of [
entryAModule!,
entryBModule!,
sharedModule!,
coreModule!,
entryAModule,
entryBModule,
sharedModule,
coreModule,
]) {
runner.evaluatedModules.invalidateModule(module)
}

const importA = runner.import('/fixtures/hmr-reexport-race/entry-a.js')
const importA = runner.import(entryAUrl)
await waitStartedPromise

const importB = runner.import('/fixtures/hmr-reexport-race/entry-b.js')
const importB = runner.import(entryBUrl)
// Wait deterministically until entry-b has reached the point where it
// observes shared as in-flight. The `mod.imports.add(depMod.id)` line
// in `request()` runs synchronously immediately before `cachedRequest`
// executes its cycle-detection prefix, so observing this edge means
// the buggy/fixed branch has either just run or is about to run on
// the same microtask. `imports` is cleared by invalidateModule, so
// this is a fresh signal (unlike `importers`, which is preserved).
const entryBNode = runner.evaluatedModules.getModuleByUrl(
'/fixtures/hmr-reexport-race/entry-b.js',
)!
while (!entryBNode.imports.has(sharedModule!.id)) {
await new Promise((resolve) => setImmediate(resolve))
}
await vi.waitUntil(() => entryBModule.imports.has(sharedModule.id))
releaseWait()
const results = await Promise.allSettled([importA, importB] as const)

Expand All @@ -125,13 +114,10 @@ describe(
},
])

const hmrListeners = runner.hmrClient!.hotModulesMap
expect(hmrListeners.has('/fixtures/hmr-reexport-race/entry-a.js')).toBe(
true,
)
expect(hmrListeners.has('/fixtures/hmr-reexport-race/entry-b.js')).toBe(
true,
)
assert(runner.hmrClient)
const hmrListeners = runner.hmrClient.hotModulesMap
expect(hmrListeners.has(entryAUrl)).toBe(true)
expect(hmrListeners.has(entryBUrl)).toBe(true)
})

it('does not treat evaluated imports as live circular requests', async ({
Expand All @@ -154,27 +140,21 @@ describe(
const sharedModule = runner.evaluatedModules.getModuleByUrl(sharedUrl)
const evaluatedModule =
runner.evaluatedModules.getModuleByUrl(evaluatedUrl)
expect(sharedModule).toBeDefined()
expect(evaluatedModule).toBeDefined()
assert(sharedModule)
assert(evaluatedModule)

const { promise: waitStartedPromise, resolve: waitStarted } =
promiseWithResolvers<void>()
const { promise: waitPromise, resolve: releaseWait } =
promiseWithResolvers<void>()
const {
promise: evaluatedWaitStartedPromise,
resolve: evaluatedWaitStarted,
} = promiseWithResolvers<void>()
const { promise: evaluatedWaitPromise, resolve: releaseEvaluatedWait } =
promiseWithResolvers<void>()

let waitStarted!: () => void
const waitStartedPromise = new Promise<void>((resolve) => {
waitStarted = resolve
})
let releaseWait!: () => void
const waitPromise = new Promise<void>((resolve) => {
releaseWait = resolve
})
let evaluatedWaitStarted!: () => void
const evaluatedWaitStartedPromise = new Promise<void>((resolve) => {
evaluatedWaitStarted = resolve
})
let releaseEvaluatedWait!: () => void
const evaluatedWaitPromise = new Promise<void>((resolve) => {
releaseEvaluatedWait = resolve
})
let evaluatedRequestCount = 0

testGlobal.__vite_ssr_hmr_evaluated_import_race__ = {
wait: () => {
waitStarted()
Expand All @@ -190,14 +170,14 @@ describe(
},
}

runner.evaluatedModules.invalidateModule(sharedModule!)
runner.evaluatedModules.invalidateModule(sharedModule)

const sharedRequest = runner.import(sharedUrl)
await waitStartedPromise
expect(sharedModule!.imports.has(evaluatedModule!.id)).toBe(true)
expect(sharedModule.imports.has(evaluatedModule.id)).toBe(true)

// Start an evaluation that pauses before dynamically importing shared.
runner.evaluatedModules.invalidateModule(evaluatedModule!)
runner.evaluatedModules.invalidateModule(evaluatedModule)

let staleEvaluatedRequestSettled = false
const staleEvaluatedRequest = runner
Expand All @@ -210,16 +190,12 @@ describe(

// Simulate HMR restarting evaluated while the older evaluation is still
// paused. The newer evaluation completes without importing shared.
runner.evaluatedModules.invalidateModule(evaluatedModule!)
runner.evaluatedModules.invalidateModule(evaluatedModule)
await runner.import(evaluatedUrl)
expect(evaluatedModule!.evaluated).toBe(true)
expect(evaluatedModule.evaluated).toBe(true)

releaseEvaluatedWait()
while (!evaluatedModule!.imports.has(sharedModule!.id)) {
await new Promise((resolve) => setImmediate(resolve))
}

await new Promise((resolve) => setImmediate(resolve))
await vi.waitUntil(() => evaluatedModule.imports.has(sharedModule.id))
expect(staleEvaluatedRequestSettled).toBe(false)

releaseWait()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,17 @@ describe('module runner initialization', async () => {
expect(action).toBeDefined()
})

it('resolves circular imports between modules with top-level await', async ({
runner,
}) => {
const mod = await runner.import('/fixtures/tla-circular/index.js')

expect(mod.a).toBe('a')
expect(mod.b).toBe('b')
expect(mod.getA()).toBe('a')
expect(mod.getB()).toBe('b')
})

it('this of the exported function should be undefined', async ({
runner,
}) => {
Expand Down
Loading