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
Expand Up @@ -1085,10 +1085,10 @@ describe('ChangeDetectionService', () => {
await expect(service.dispose()).resolves.toBeUndefined();
});

it('rescans the working tree when the story index is invalidated', async () => {
it('rescans the working tree when the module graph revision advances', async () => {
// Graph-side reconciliation (replaying add/unlink, the refreshInFlight guard) is covered by
// module-graph-engine.test.ts; here we assert the status side of the seam: an index
// invalidation re-runs the git-diff scan.
// module-graph-engine.test.ts; here we assert the status side of the seam: a graph revision
// bump (from a file change or story-index reconciliation) re-runs the git-diff scan.
const reverseIndex = buildReverseIndex([]);
installDependencyGraphMocks(reverseIndex);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ export function createWiredChangeDetection(
workingDir: options.workingDir,
onSnapshot: () => moduleGraphMockRef.current?.applySnapshot(),
onUpdate: ({ bumpedStoryFiles }) => moduleGraphMockRef.current?.applyUpdate(bumpedStoryFiles),
onStoryIndexInvalidated: () => moduleGraphMockRef.current?.bumpGraphRevision(),
onError: (error) => moduleGraphMockRef.current?.applyError(error),
onUnavailable: (reason, error) => moduleGraphMockRef.current?.applyUnavailable(reason, error),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,19 +257,6 @@ export const moduleGraphServiceDef = defineService({
});
},
},
_bumpGraphRevision: {
internal: true,
description:
'Bumps the graph revision when the story index invalidates without an immediate graph snapshot/update.',
input: noInputSchema,
output: v.void(),
handler: async (_input, ctx) => {
ctx.self.setState((state) => {
state.graphRevision += 1;
state.latestChangedStoryFiles = [];
});
},
},
_setStatus: {
internal: true,
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ function setup(options?: {
const callbacks = {
onSnapshot: vi.fn(),
onUpdate: vi.fn(),
onStoryIndexInvalidated: vi.fn(),
onError: vi.fn(),
onUnavailable: vi.fn(),
};
Expand Down Expand Up @@ -230,8 +229,32 @@ describe('ModuleGraphEngine', () => {
await vi.runAllTimersAsync();

expect(patchSpy).toHaveBeenCalledWith({ kind: 'add', path: '/repo/src/B.stories.tsx' });
// The index changed, so the service wrapper is notified to bump graph revision.
expect(callbacks.onStoryIndexInvalidated).toHaveBeenCalled();
// The replayed add flows through the normal patch path, so the new story is reported as a
// targeted update — no separate untargeted index-invalidation bump is needed.
expect(callbacks.onUpdate).toHaveBeenCalledWith(
expect.objectContaining({ bumpedStoryFiles: ['./src/B.stories.tsx'] })
);
});

it('does not emit an update when an invalidation leaves the story set unchanged', async () => {
// A content-only story edit (or a config invalidation) re-fires the index without changing the
// set of story files. The builder's own file-change event carries any content change; the
// invalidation itself must not produce an untargeted update.
installDependencyGraphMocks(buildReverseIndex([]));
const { service, adapter, callbacks } = setup({
storyIndex: createStoryIndex([
{ storyId: 'a--default', importPath: './src/A.stories.tsx', title: 'A' },
]),
});

service.start(adapter);
await vi.runAllTimersAsync();
expect(callbacks.onUpdate).not.toHaveBeenCalled();

service.onStoryIndexInvalidated();
await vi.runAllTimersAsync();

expect(callbacks.onUpdate).not.toHaveBeenCalled();
});

it('guards duplicate onStoryIndexInvalidated so a newly-added story is replayed only once', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ export interface ModuleGraphEngineOptions {
onError?: (error: Error) => void;
/** Fired when the builder adapter reports a startup failure. */
onUnavailable?: (reason: string, error?: Error) => void;
/** Fired when the story index invalidates, even if no graph edges changed. */
onStoryIndexInvalidated?: () => void;
/** Mirrors the built reverse index into the `core/module-graph` open service. */
onSnapshot?: (storiesByFile: ReturnType<typeof reverseIndexToStoriesByFile>) => void;
/** Mirrors state after each settled patch; includes story files whose graph may have changed. */
Expand Down Expand Up @@ -271,9 +269,6 @@ export class ModuleGraphEngine {
this.refreshInFlight = false;
});
}
// The story index changed even when no story files were added/removed (e.g. a story renamed
// within a file); signal consumers so derived state is recomputed.
this.options.onStoryIndexInvalidated?.();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { clearRegistry } from '../../server.ts';
import {
buildReverseIndex,
createMockAdapter,
createStoryIndex,
installDependencyGraphMocks,
registerTestModuleGraphService,
} from './module-graph.test-helpers.ts';
Expand Down Expand Up @@ -356,21 +357,6 @@ describe('module-graph open service', () => {
});
});

it('clears story files but keeps the current revision after _bumpGraphRevision', async () => {
const runtime = registerBareModuleGraph();

await runtime.commands._applyGraphUpdate({
storiesByFile: {},
bumpedStoryFiles: ['./src/Button.stories.tsx'],
});
await runtime.commands._bumpGraphRevision(undefined);

expect(runtime.queries.getLatestStoryChanges(undefined)).toEqual({
revision: 2,
storyFiles: [],
});
});

it('notifies subscribers when the latest change set updates', async () => {
const runtime = registerBareModuleGraph();
const seen: Array<{ revision: number; storyFiles: string[] }> = [];
Expand Down Expand Up @@ -433,22 +419,6 @@ describe('module-graph open service', () => {
).toBe(1);
expect(runtime.queries.getGraphRevision({ storyFiles: ['src/Button.stories.tsx'] })).toBe(1);
});

it('advances watch-all but not scoped reads on a bare revision bump', async () => {
const runtime = registerBareModuleGraph();
await runtime.commands._applyGraphSnapshot({
storiesByFile: { './src/Button.tsx': { './src/Button.stories.tsx': 1 } },
});

await runtime.commands._bumpGraphRevision(undefined);

// Index reconciliation advances the global (watch-all) revision...
expect(runtime.queries.getGraphRevision(undefined)).toBe(1);
// ...but is not attributed to any specific story.
expect(runtime.queries.getGraphRevision({ storyFiles: ['./src/Button.stories.tsx'] })).toBe(
0
);
});
});

describe('getGraphRevision subscription', () => {
Expand Down Expand Up @@ -522,18 +492,33 @@ describe('module-graph open service', () => {
});

// Must run last: it resolves the process-global adapter promise, which cannot be un-resolved.
it('builds the graph once the adapter is provided and mirrors it into service state', async () => {
it('builds the graph from the adapter and turns index invalidations into targeted updates', async () => {
const reverseIndex = buildReverseIndex([
['/repo/src/Button.tsx', '/repo/src/Button.stories.tsx', 1],
]);
installDependencyGraphMocks(reverseIndex);

const channel = { on: vi.fn(() => () => undefined), emit: vi.fn() };
const baselineIndex = createStoryIndex([
{ storyId: 'button--primary', importPath: './src/Button.stories.tsx', title: 'Button' },
]);
const indexWithCard = createStoryIndex([
{ storyId: 'button--primary', importPath: './src/Button.stories.tsx', title: 'Button' },
{ storyId: 'card--primary', importPath: './src/Card.stories.tsx', title: 'Card' },
]);
// Build reads the baseline; the first invalidation re-reads it unchanged, the second adds Card.
const getIndex = vi
.fn()
.mockResolvedValueOnce(baselineIndex)
.mockResolvedValueOnce(baselineIndex)
.mockResolvedValue(indexWithCard);

const on = vi.fn<(event: string, listener: () => void) => () => void>(() => () => undefined);
const channel = { on, emit: vi.fn() };
const { adapter } = createMockAdapter({ resolveConfig: { projectRoot: '/repo' } });

const runtime = registerModuleGraphService({
channel: channel as never,
getIndex: vi.fn().mockResolvedValue({ v: 5, entries: {} }),
getIndex,
workingDir: '/repo',
});

Expand All @@ -548,6 +533,30 @@ describe('module-graph open service', () => {
expect(runtime.queries.getStoriesForFiles({ files: ['/repo/src/Button.tsx'] })).toEqual([
[{ storyFile: './src/Button.stories.tsx', depth: 1 }],
]);

const invalidate = channel.on.mock.calls.find(
([event]) => event === STORY_INDEX_INVALIDATED
)?.[1];
expect(invalidate).toBeTypeOf('function');

// An invalidation that does not change the story set must not advance the revision on its own
// (no untargeted bump, no clobbered change set).
invalidate!();
await runtime.commands._waitForSettledEngine(undefined);
expect(runtime.queries.getGraphRevision(undefined)).toBe(0);
expect(runtime.queries.getLatestStoryChanges(undefined)).toEqual({
revision: 0,
storyFiles: [],
});

// A newly-indexed story is reconciled and reported as a targeted change.
invalidate!();
await vi.waitFor(() => {
expect(runtime.queries.getLatestStoryChanges(undefined)).toEqual({
revision: 1,
storyFiles: ['./src/Card.stories.tsx'],
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ export function registerModuleGraphService(options: RegisterModuleGraphServiceOp
onUpdate: ({ storiesByFile, bumpedStoryFiles }) => {
void runtime.commands._applyGraphUpdate({ storiesByFile, bumpedStoryFiles });
},
onStoryIndexInvalidated: () => {
void runtime.commands._bumpGraphRevision(undefined);
},
onError: (error) => {
void runtime.commands._setStatus({ value: 'error', error: errorToErrorLike(error) });
},
Expand Down