-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
Builder-Vite: Add onModuleGraphChange method #34323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
11d7074
Implement module graph functionality and related tests in Vite builder
ghengeveld cb10bac
Clean up logging
ghengeveld c72d208
Update event listener management in Vite builder to prevent memory leaks
ghengeveld f1626c8
Clear waitForModuleGraph interval on bail
ghengeveld 4deb104
Improve types and add a clarification
ghengeveld af5da75
Move buildModuleGraph to separate file
ghengeveld e7cb59c
Merge branch 'next' into module-graph-change-listener
ghengeveld 89f3d9f
Move tests to separate file
ghengeveld 76cbb12
Preserve edges for related Vite module nodes discovered before their …
ghengeveld File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,259 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import type { ModuleNode as StorybookModuleNode, Options } from 'storybook/internal/types'; | ||
| import type { ViteDevServer } from 'vite'; | ||
|
|
||
| import { bail, onModuleGraphChange, start } from './index'; | ||
| import { createViteServer } from './vite-server'; | ||
|
|
||
| vi.mock('./vite-server', () => ({ | ||
| createViteServer: vi.fn(), | ||
| })); | ||
|
|
||
| type ViteModuleNodeLike = { | ||
| file: string | null; | ||
| type: StorybookModuleNode['type']; | ||
| importers: Set<ViteModuleNodeLike>; | ||
| importedModules: Set<ViteModuleNodeLike>; | ||
| }; | ||
|
|
||
| function createViteModuleNode( | ||
| file: string | null, | ||
| type: StorybookModuleNode['type'] = 'js' | ||
| ): ViteModuleNodeLike { | ||
| return { | ||
| file, | ||
| type, | ||
| importers: new Set(), | ||
| importedModules: new Set(), | ||
| }; | ||
| } | ||
|
|
||
| function createFileToModulesMap(...entries: Array<[string, Set<ViteModuleNodeLike>]>) { | ||
| return new Map(entries) as ViteDevServer['moduleGraph']['fileToModulesMap']; | ||
| } | ||
|
|
||
| type WatcherHandler = (...args: unknown[]) => void; | ||
| type FakeWatcher = { | ||
| on: ReturnType<typeof vi.fn>; | ||
| off: ReturnType<typeof vi.fn>; | ||
| emit: (event: string, ...args: unknown[]) => void; | ||
| listenerCount: (event: string) => number; | ||
| }; | ||
|
|
||
| function createFakeViteServer() { | ||
| const watcherListeners = new Map<string, Set<WatcherHandler>>(); | ||
| const watcher = {} as FakeWatcher; | ||
|
|
||
| watcher.on = vi.fn((event: string, handler: WatcherHandler) => { | ||
| const handlers = watcherListeners.get(event) ?? new Set<WatcherHandler>(); | ||
| handlers.add(handler); | ||
| watcherListeners.set(event, handlers); | ||
| return watcher; | ||
| }); | ||
| watcher.off = vi.fn((event: string, handler: WatcherHandler) => { | ||
| watcherListeners.get(event)?.delete(handler); | ||
| return watcher; | ||
| }); | ||
| watcher.emit = (event: string, ...args: unknown[]) => { | ||
| watcherListeners.get(event)?.forEach((handler) => { | ||
| handler(...args); | ||
| }); | ||
| watcherListeners.get('all')?.forEach((handler) => { | ||
| handler(event, ...args); | ||
| }); | ||
| }; | ||
| watcher.listenerCount = (event: string) => watcherListeners.get(event)?.size ?? 0; | ||
|
|
||
| return { | ||
| watcher, | ||
| moduleGraph: { | ||
| fileToModulesMap: createFileToModulesMap(), | ||
| }, | ||
| middlewares: { | ||
| handle: vi.fn(), | ||
| }, | ||
| transformIndexHtml: vi.fn().mockResolvedValue(''), | ||
| waitForRequestsIdle: vi.fn().mockResolvedValue(undefined), | ||
| close: vi.fn().mockResolvedValue(undefined), | ||
| } as unknown as ViteDevServer & { | ||
| watcher: typeof watcher; | ||
| }; | ||
| } | ||
|
|
||
| function createStartArgs(): Parameters<typeof start>[0] { | ||
| return { | ||
| startTime: process.hrtime(), | ||
| options: {} as Options, | ||
| router: { | ||
| get: vi.fn(), | ||
| use: vi.fn(), | ||
| } as unknown as Parameters<typeof start>[0]['router'], | ||
| server: {} as Parameters<typeof start>[0]['server'], | ||
| channel: {} as Parameters<typeof start>[0]['channel'], | ||
| }; | ||
| } | ||
|
|
||
| describe('onModuleGraphChange', () => { | ||
| let fakeViteServer: ReturnType<typeof createFakeViteServer>; | ||
|
|
||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| fakeViteServer = createFakeViteServer(); | ||
| vi.mocked(createViteServer).mockResolvedValue(fakeViteServer); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await bail(); | ||
| vi.useRealTimers(); | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('registers callbacks and unsubscribes them', async () => { | ||
| const cb = vi.fn(); | ||
| const unsubscribe = onModuleGraphChange(cb); | ||
|
|
||
| expect(unsubscribe).toEqual(expect.any(Function)); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).toHaveBeenCalledTimes(1); | ||
|
|
||
| unsubscribe(); | ||
|
|
||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('passes the module graph payload to listeners', async () => { | ||
| const entry = createViteModuleNode('/src/Button.tsx'); | ||
| fakeViteServer.moduleGraph.fileToModulesMap = createFileToModulesMap([ | ||
| '/src/Button.tsx', | ||
| new Set([entry]), | ||
| ]); | ||
|
|
||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| const moduleGraph = cb.mock.calls[0]?.[0]; | ||
|
|
||
| expect(cb).toHaveBeenCalledWith(expect.any(Map)); | ||
| expect(moduleGraph?.has('/src/Button.tsx')).toBe(true); | ||
| }); | ||
|
|
||
| it('triggers change events after the debounce delay', async () => { | ||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(50); | ||
| expect(cb).not.toHaveBeenCalled(); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(50); | ||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('triggers add events after the debounce delay', async () => { | ||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('add', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('triggers unlink events after the debounce delay', async () => { | ||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('unlink', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('debounces multiple rapid events into a single callback', async () => { | ||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
| fakeViteServer.watcher.emit('add', '/src/Button.tsx'); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('notifies multiple listeners', async () => { | ||
| const cb1 = vi.fn(); | ||
| const cb2 = vi.fn(); | ||
|
|
||
| onModuleGraphChange(cb1); | ||
| onModuleGraphChange(cb2); | ||
|
|
||
| await start(createStartArgs()); | ||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb1).toHaveBeenCalledTimes(1); | ||
| expect(cb2).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('removes the all-event watcher during bail to avoid leaks across restarts', async () => { | ||
| const cb = vi.fn(); | ||
| onModuleGraphChange(cb); | ||
|
|
||
| await start(createStartArgs()); | ||
| expect(fakeViteServer.watcher.listenerCount('all')).toBe(1); | ||
|
|
||
| await bail(); | ||
| expect(fakeViteServer.watcher.listenerCount('all')).toBe(0); | ||
|
|
||
| await start(createStartArgs()); | ||
| expect(fakeViteServer.watcher.listenerCount('all')).toBe(1); | ||
|
|
||
| fakeViteServer.watcher.emit('change', '/src/Button.tsx'); | ||
| await vi.advanceTimersByTimeAsync(100); | ||
|
|
||
| expect(cb).not.toHaveBeenCalled(); | ||
| expect(fakeViteServer.watcher.off).toHaveBeenCalledWith('all', expect.any(Function)); | ||
| }); | ||
|
|
||
| it('clears the module-graph polling interval during bail', async () => { | ||
| await start(createStartArgs()); | ||
|
|
||
| expect(vi.getTimerCount()).toBe(1); | ||
|
|
||
| await bail(); | ||
|
|
||
| fakeViteServer.moduleGraph.fileToModulesMap = createFileToModulesMap([ | ||
| '/src/Button.tsx', | ||
| new Set([createViteModuleNode('/src/Button.tsx')]), | ||
| ]); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(1000); | ||
|
|
||
| expect(vi.getTimerCount()).toBe(0); | ||
| expect(fakeViteServer.waitForRequestsIdle).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.