Skip to content

Commit

Permalink
feat: add ssrInvalidates to HMR payload
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jan 4, 2024
1 parent 27d5788 commit 2793ced
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 142 deletions.
3 changes: 3 additions & 0 deletions packages/vite/src/node/server/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ export function updateModules(
...boundaries.map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as const,
timestamp,
ssrInvalidates: [
...moduleGraph.getSSRInvalidatedImporters(boundary),
].map((m) => m.file!),
path: normalizeHmrUrl(boundary.url),
explicitImportRequired:
boundary.type === 'js'
Expand Down
17 changes: 17 additions & 0 deletions packages/vite/src/node/server/moduleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,23 @@ export class ModuleGraph {
})
}

getSSRInvalidatedImporters(
mod: ModuleNode,
date = mod.lastHMRTimestamp,
seen: Set<ModuleNode> = new Set(),
): Set<ModuleNode> {
if (seen.has(mod)) {
return seen
}
mod.ssrImportedModules.forEach((importer) => {
if (importer.lastHMRTimestamp === date) {
seen.add(importer)
this.getSSRInvalidatedImporters(importer, date, seen)
}
})
return seen
}

invalidateAll(): void {
const timestamp = Date.now()
const seen = new Set<ModuleNode>()
Expand Down
182 changes: 102 additions & 80 deletions packages/vite/src/node/ssr/runtime/__tests__/server-hmr.spec.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,108 @@
import { afterEach, describe, expect, it } from 'vitest'
import { describe, expect } from 'vitest'
import { createTestClient, editFile, resolvePath, waitUntil } from './utils'

describe('vite-server-runetime hmr works as expected', async () => {
const { runtime, stdout, waitForWatcher } = await createTestClient(
{
describe(
'vite-server-runetime hmr works as expected',
async () => {
const it = await createTestClient({
server: {
// override watch options because it's disabled by default
watch: {},
},
},
// { printLog: true },
)
await waitForWatcher()

afterEach(() => {
runtime.clearCache()
})

it('hmr options are defined', async () => {
expect(runtime.hmrClient).toBeDefined()

const mod = await runtime.executeId('./fixtures/hmr.js')
expect(mod).toHaveProperty('hmr')
expect(mod.hmr).toHaveProperty('accept')
})

it('correctly populates hmr client', async () => {
const mod = await runtime.executeId('./fixtures/d')
expect(mod.d).toBe('a')

const fixtureC = resolvePath(import.meta.url, './fixtures/c.ts')
const fixtureD = resolvePath(import.meta.url, './fixtures/d.ts')

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

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

it('correctly invalidates tree when one module is updated')

it('correctly does full-reload', async () => {
const mod = await runtime.executeId(
'./fixtures/circular/circular-index.js',
true,
)
expect(mod.a).toBe('a')
expect(mod.b).toBe('b')

expect(
runtime.hmrClient?.customListenersMap.has('vite:beforeFullReload'),
).toBe(true)

editFile(
resolvePath(import.meta.url, './fixtures/circular/circular-b.js'),
(content) => content.replace("b = 'b'", "b = 'bb'"),
)
await waitUntil(() => stdout().includes('full reload'))

const mod2 = await runtime.executeId(
'./fixtures/circular/circular-index.js',
)
expect(mod2.b).toBe('bb')
})

it('correctly reruns accepted ssr code', async () => {
const [mod] = await runtime.executeEntrypoints(['./fixtures/simple.js'])
expect(mod.test).toEqual('I am initialized')
expect(stdout()).toMatch('I am initialized')

editFile(resolvePath(import.meta.url, './fixtures/simple.js'), (content) =>
content.replace(/initialized/g, 'changed'),
)

await waitUntil(() => stdout().includes('I am accepted'))
const mod2 = await runtime.executeId('./fixtures/simple.js')
expect(mod2.test).toEqual('I am changed')
expect(stdout()).toMatch('I am changed')
})
}, 10_000)
})

it('hmr options are defined', async ({ runtime }) => {
expect(runtime.hmrClient).toBeDefined()

const mod = await runtime.executeId('./fixtures/hmr.js')
expect(mod).toHaveProperty('hmr')
expect(mod.hmr).toHaveProperty('accept')
})

it('correctly populates hmr client', async ({ runtime }) => {
const mod = await runtime.executeId('./fixtures/d')
expect(mod.d).toBe('a')

const fixtureC = resolvePath(import.meta.url, './fixtures/c.ts')
const fixtureD = resolvePath(import.meta.url, './fixtures/d.ts')

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

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

it('correctly invalidates tree when one module is updated', async ({
runtime,
stdout,
}) => {
const mod = await runtime.executeId(
// "c" only depends on "a"
'./fixtures/c.ts',
true,
)

expect(mod.c).toBe('a')

editFile(resolvePath(import.meta.url, './fixtures/a.ts'), (content) =>
content.replace("a = 'a'", "a = 'aa'"),
)

await waitUntil(() => stdout().includes('hot update'))

const mod2 = await runtime.executeId(
// "c" only depends on "a"
'./fixtures/c.ts',
true,
)

expect(mod2.c).toBe('aa')
})

it('correctly does full-reload', async ({ runtime, stdout }) => {
const mod = await runtime.executeId(
'./fixtures/circular/circular-index.js',
true,
)
expect(mod.a).toBe('a')
expect(mod.b).toBe('b')

expect(
runtime.hmrClient?.customListenersMap.has('vite:beforeFullReload'),
).toBe(true)

editFile(
resolvePath(import.meta.url, './fixtures/circular/circular-b.js'),
(content) => content.replace("b = 'b'", "b = 'bb'"),
)
await waitUntil(() => stdout().includes('full reload'))

const mod2 = await runtime.executeId(
'./fixtures/circular/circular-index.js',
)
expect(mod2.b).toBe('bb')
})

it('correctly reruns accepted ssr code', async ({ runtime, stdout }) => {
const [mod] = await runtime.executeEntrypoints(['./fixtures/simple.js'])
expect(mod.test).toEqual('I am initialized')
expect(stdout()).toMatch('I am initialized')

editFile(
resolvePath(import.meta.url, './fixtures/simple.js'),
(content) => content.replace(/initialized/g, 'changed'),
)

await waitUntil(() => stdout().includes('I am accepted'))
const mod2 = await runtime.executeId('./fixtures/simple.js')
expect(mod2.test).toEqual('I am changed')
expect(stdout()).toMatch('I am changed')
})
},
process.env.CI ? 30_00 : 5_000,
)
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { afterEach, describe, expect, it } from 'vitest'
import { describe, expect } from 'vitest'
import { createTestClient } from './utils'

describe('vite-server-runetime hmr works as expected', async () => {
const { runtime, restoreConsole } = await createTestClient({
const it = await createTestClient({
server: {
// override watch options because it's disabled by default
watch: {},
hmr: false,
},
})
afterEach(() => {
restoreConsole()
})

it("hmr client is not defined if it's disabled", async () => {
it("hmr client is not defined if it's disabled", async ({ runtime }) => {
expect(runtime.hmrClient).toBeUndefined()

const mod = await runtime.executeId('./fixtures/hmr.js')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { afterAll, describe, expect, it } from 'vitest'
import { createTestClient, resolvePath } from './utils'
import { describe, expect } from 'vitest'
import { createTestClient } from './utils'

describe('vite-server-runetime initialization', async () => {
const { runtime, server } = await createTestClient(
{},
{
printLog: true,
},
)
const it = await createTestClient()

afterAll(async () => {
await server.close()
})

it('correctly runs ssr code', async () => {
it('correctly runs ssr code', async ({ runtime }) => {
const mod = await runtime.executeId('./fixtures/simple.js')
expect(mod.test).toEqual('I am initialized')
})
Expand Down
71 changes: 32 additions & 39 deletions packages/vite/src/node/ssr/runtime/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Writable } from 'node:stream'
import { Console } from 'node:console'
import { afterAll, afterEach, beforeEach } from 'vitest'
import type { TestAPI } from 'vitest'
import { afterAll, afterEach, beforeEach, test } from 'vitest'
import stripAnsi from 'strip-ansi'
import type { InlineConfig, ViteDevServer } from '../../../index'
import { createServer } from '../../../index'
Expand All @@ -14,65 +15,57 @@ interface TestClient {
restoreConsole(): void
stdout(): string
stderr(): string
waitForWatcher(): Promise<void>
server: ViteDevServer
runtime: ViteRuntime
}

export async function createTestClient(
config: InlineConfig = {},
{ printLog }: { printLog?: boolean } = {},
): Promise<TestClient> {
const server = await createServer({
root: __dirname,
logLevel: 'error',
server: {
watch: null,
},
...config,
})

): Promise<TestAPI<TestClient>> {
const { restore, getLogs, printLogs } = captureLogs()

const runtime = await createViteRuntime(server as any)
function waitForWatcher(server: ViteDevServer) {
return new Promise<void>((resolve) => {
if (Object.keys(server.watcher.getWatched()).length) {
resolve()
} else {
server.watcher.once('ready', () => resolve())
}
})
}

beforeEach(({ onTestFailed }) => {
onTestFailed(() => {
beforeEach<TestClient>(async (t) => {
t.onTestFailed(() => {
printLogs()
})

t.server = await createServer({
root: __dirname,
logLevel: 'error',
server: {
watch: null,
},
...config,
})
t.runtime = await createViteRuntime(t.server)
await waitForWatcher(t.server)
t.stderr = () => getLogs().stderr
t.stdout = () => getLogs().stdout
})

afterEach(() => {
afterEach<TestClient>(async (t) => {
await t.server.close()
if (printLog) {
printLogs()
}
})

afterAll(async () => {
afterAll(() => {
restore()
await server.close()
})

return {
restoreConsole: restore,
stdout() {
return getLogs().stdout
},
stderr() {
return getLogs().stderr
},
waitForWatcher() {
return new Promise<void>((resolve) => {
if (Object.keys(server.watcher.getWatched()).length) {
resolve()
} else {
server.watcher.once('ready', () => resolve())
}
})
},
server,
runtime,
}
return test as TestAPI<TestClient>
}

function captureLogs() {
Expand Down Expand Up @@ -143,7 +136,7 @@ export async function waitUntil(cb: () => boolean): Promise<void> {

const originalFiles = new Map<string, string>()
const createdFiles = new Set<string>()
afterEach(() => {
afterEach((t) => {
originalFiles.forEach((content, file) => {
fs.writeFileSync(file, content, 'utf-8')
})
Expand Down
5 changes: 3 additions & 2 deletions packages/vite/src/node/ssr/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export class ViteRuntime {
this.hmrClient = new HMRClient(
console,
options.hmr,
({ acceptedPath }) => {
this.moduleCache.invalidateDepTree([acceptedPath])
({ acceptedPath, ssrInvalidates }) => {
this.moduleCache.delete(acceptedPath)
ssrInvalidates?.forEach((id) => this.moduleCache.delete(id))
return this.executeId(acceptedPath, false)
},
)
Expand Down
Loading

0 comments on commit 2793ced

Please sign in to comment.