Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
67b411b
Vue: Skip docgen for module ids carrying a query
seanogdev Jul 26, 2026
ac23048
Address review: assert the bare .vue id rather than a .ts id
seanogdev Jul 26, 2026
f48b141
Bench: Add the docgen perf methodology note
valentinpalkovic Jul 28, 2026
ff3c2df
Refactor: Update docgen performance methodology for clarity and struc…
valentinpalkovic Jul 29, 2026
74e7654
Refactor: Update budgets table formatting for improved readability
valentinpalkovic Jul 29, 2026
721c65f
Bench: Add the shared docgen bench plumbing
valentinpalkovic Jul 28, 2026
1732854
Bench: Address review feedback on the shared plumbing
Jul 30, 2026
6703f6e
Bench: Reject --heavy-factor 0 instead of quietly raising it to 1
Jul 30, 2026
9e2f2b2
Bench: Address review on the shared harness plumbing
Jul 31, 2026
08359d9
Bench: Add the docgen perf engine framework and the compodoc engine
valentinpalkovic Jul 28, 2026
bb96cbc
Bench: Address review feedback on the engine framework
Jul 30, 2026
0660705
Bench: Kill a compodoc run that overruns its timeout
Jul 30, 2026
ac43139
Bench: Make the Angular generator's tree readable
Jul 31, 2026
2c6a563
Bench: Make the docgen perf suite runnable
valentinpalkovic Jul 29, 2026
f0ccab4
Bench: Address review feedback on the orchestration
Jul 30, 2026
511b1bb
Bench: Assert the report against its rendered output
Jul 31, 2026
1d53c2e
Bench: Let the suite drain its failure output before exiting
Jul 31, 2026
c3ab1db
Bench: Add the React and Vue docgen perf engines
valentinpalkovic Jul 29, 2026
b8687f6
Bench: Address review feedback on the React and Vue engines
Jul 30, 2026
8a0aaa7
Bench: Make the Vue generator's type chain readable
Jul 31, 2026
588767e
Bench: Record why both checker calls stay in the timed path
Jul 31, 2026
3d8892b
Bench: Compare an engine against another release of itself
valentinpalkovic Jul 28, 2026
b528027
Bench: Address review feedback on the version pair
Jul 30, 2026
3078cca
Bench: Document how to run a version comparison
Jul 31, 2026
c166b99
Bench: Say same resolved version, not same install
Jul 31, 2026
5fbccda
Bench: Declare the React renderer dependency the harnesses rely on
Jul 31, 2026
32b5d16
Bench: Move the docgen perf suite into the docgen harness
Jul 31, 2026
75617ee
CI: Require Core or DX approval before merging PRs
cursoragent Jul 31, 2026
d07fd24
CI: Inline Core/DX approval check in Dangerfile
cursoragent Jul 31, 2026
7d35519
Core: Extract ComponentMetaManager to Core
huang-julien Jul 29, 2026
be04a61
refactor: move ak parseTsconfigCommandLine to core
huang-julien Jul 31, 2026
8d1cfba
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
2c01834
Vue: Move vue-component-meta as renderer internal aand implement docg…
huang-julien Jul 30, 2026
2cca233
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
3d2fc9c
refactor: remove oxc-resolver and
huang-julien Jul 31, 2026
7acf784
refactor: extrat fallbackTitle into util
huang-julien Jul 31, 2026
19b9821
refactor: move map to record
huang-julien Jul 31, 2026
dcecfa2
refactor: remove resolvePath from BuildDoGEnbContext - test only
huang-julien Jul 31, 2026
734e67d
fix: merge issue + apply preset instead of path exports
huang-julien Jul 31, 2026
6ffbfd4
chore: dedupe
huang-julien Aug 3, 2026
6241937
test: drop build-docgen unit tests in favor of docgen-harness vue3 su…
huang-julien Aug 3, 2026
6192b9c
Core: Decouple component-meta from the consumer's typescript instance
huang-julien Jul 29, 2026
5be0aac
refactor: move ak parseTsconfigCommandLine to core
huang-julien Jul 31, 2026
72aa77c
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
2d9a2dd
Vue: Move vue-component-meta as renderer internal aand implement docg…
huang-julien Jul 30, 2026
b5bd3bc
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
41eae7b
refactor: remove oxc-resolver and
huang-julien Jul 31, 2026
a33c018
refactor: extrat fallbackTitle into util
huang-julien Jul 31, 2026
d5af6cf
refactor: move map to record
huang-julien Jul 31, 2026
3eab8e3
fix: merge issue + apply preset instead of path exports
huang-julien Jul 31, 2026
b6ab437
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
121a546
Vue: Move vue-component-meta as renderer internal aand implement docg…
huang-julien Jul 30, 2026
7d11364
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
75c4905
Vue: Run docgen through component-meta project manager
huang-julien Jul 30, 2026
1405125
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
b6bc470
Vue: Move vue-component-meta as renderer internal aand implement docg…
huang-julien Jul 30, 2026
8d72c7c
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
bacad33
refactor: remove oxc-resolver and
huang-julien Jul 31, 2026
ed3f9c4
refactor: extrat fallbackTitle into util
huang-julien Jul 31, 2026
1dca913
refactor: move map to record
huang-julien Jul 31, 2026
14fc347
chore: remove optionnal arg from vuecomponentmetamanager
huang-julien Jul 31, 2026
4cbeced
chore: dedupe checker_options
huang-julien Jul 31, 2026
31bd8cb
refactor: preffer getter
huang-julien Aug 3, 2026
35897d8
chore: dedupe
huang-julien Aug 3, 2026
5c32fde
fiox: expose only docgen-worker
huang-julien Aug 3, 2026
6e24ced
chore: dedupe
huang-julien Aug 3, 2026
c6722df
Core: Decouple component-meta from the consumer's typescript instance
huang-julien Jul 29, 2026
baee3a7
refactor: move ak parseTsconfigCommandLine to core
huang-julien Jul 31, 2026
f56f649
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
e88e0d2
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
1878a02
refactor: extrat fallbackTitle into util
huang-julien Jul 31, 2026
cf4f1ed
refactor: move map to record
huang-julien Jul 31, 2026
70bbf2e
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
bb8974f
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
0c2cd5f
Vue: Run docgen through component-meta project manager
huang-julien Jul 30, 2026
e47d3bf
Core: Add pass docgen options to docgen worker
huang-julien Jul 29, 2026
92f3df6
Vue: Move vue-component-meta as renderer internal aand implement docg…
huang-julien Jul 30, 2026
a1f3d04
Revert "Core: Add pass docgen options to docgen worker"
huang-julien Jul 30, 2026
f4e8211
refactor: extrat fallbackTitle into util
huang-julien Jul 31, 2026
8e090ac
refactor: move map to record
huang-julien Jul 31, 2026
afbd00f
fiox: expose only docgen-worker
huang-julien Aug 3, 2026
a82ee2f
chore: regen lockfile
huang-julien Aug 3, 2026
b40ea08
Vue: contribute experimental_docgenProvider preset
huang-julien Aug 3, 2026
88a044b
fix: use resolvePath
huang-julien Aug 3, 2026
8561d9f
Core: lazy docgen middleware
huang-julien Aug 3, 2026
a752a56
refactor: apply sugggestions
huang-julien Aug 4, 2026
df12640
chore: remove dead dep TS
huang-julien Aug 3, 2026
a4fdb53
Merge pull request #35687 from storybookjs/julien/lazy-docgen-middleware
valentinpalkovic Aug 4, 2026
f72e4de
Merge pull request #35670 from storybookjs/julien/vue_expeirmental_do…
valentinpalkovic Aug 4, 2026
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
1 change: 1 addition & 0 deletions code/core/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export * from './utils/formatter.ts';
export * from './utils/get-story-id.ts';
export * from './utils/component-id.ts';
export * from './utils/select-component-entry.ts';
export * from './utils/lazy-docgen-middleware.ts';
export * from './utils/posix.ts';
export * from './utils/sync-main-preview-addons.ts';
export * from './utils/setup-addon-in-config.ts';
Expand Down
50 changes: 50 additions & 0 deletions code/core/src/common/utils/lazy-docgen-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type {
DocgenMiddleware,
DocgenPayload,
DocgenProvider,
DocgenProviderInput,
} from '../../shared/open-service/services/docgen/types.ts';
import { STORY_FILE_TEST_REGEXP, getStoryImportPathFromEntry } from './select-component-entry.ts';

export interface LazyDocgenMiddlewareOptions<TManager> {
/**
* Builds the renderer's extraction manager.
* Called once, lazily, on the first eligible request and memoized for the worker's lifetime.
* Return `undefined` to permanently pass through to the rest of the chain.
*/
createManager: () => Promise<TManager | undefined>;
/**
* Extracts one payload
* Returns `undefined` to delegate the request downstream
*/
extract: (manager: TManager, input: DocgenProviderInput) => Promise<DocgenPayload | undefined>;
}

export function createLazyDocgenMiddleware<TManager>({
createManager,
extract,
}: LazyDocgenMiddlewareOptions<TManager>): DocgenMiddleware {
let managerPromise: Promise<TManager | undefined> | undefined;
const getManager = () => (managerPromise ??= createManager());

return (nextDocgen: DocgenProvider): DocgenProvider =>
async (input) => {
const storyImportPath = getStoryImportPathFromEntry(input.entry);
if (!storyImportPath || !STORY_FILE_TEST_REGEXP.test(storyImportPath)) {
return nextDocgen(input);
}

const manager = await getManager();
if (!manager) {
return nextDocgen(input);
}

const ours = await extract(manager, input);
if (!ours) {
return nextDocgen(input);
}

const downstream = await nextDocgen(input);
return { ...downstream, ...ours };
};
}
6 changes: 5 additions & 1 deletion code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,11 @@ export interface ComponentsManifest {
v: number;
components: Record<string, ComponentManifest>;
meta?: {
docgen: 'react-docgen' | 'react-docgen-typescript' | 'react-component-meta';
docgen:
| 'react-docgen'
| 'react-docgen-typescript'
| 'react-component-meta'
| 'vue-component-meta';
durationMs: number;
};
}
Expand Down
1 change: 0 additions & 1 deletion code/frameworks/vue3-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"@storybook/builder-vite": "workspace:*",
"@storybook/vue3": "workspace:*",
"magic-string": "^1.1.0",
"typescript": "^5.9.3",
"vue-component-meta": "^3.3.9",
"vue-docgen-api": "^4.75.1"
},
Expand Down
14 changes: 14 additions & 0 deletions code/frameworks/vue3-vite/src/docgen/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Category, StorybookError } from 'storybook/internal/server-errors';

export class Vue3ViteDocgenManifestError extends StorybookError {
constructor() {
super({
name: 'Vue3ViteDocgenManifestError',
category: Category.FRAMEWORK_VUE3_VITE,
code: 1,
message:
"The Vue docgen manifest currently requires `docgen: 'vue-component-meta'` in `framework.options`.\n" +
'Update the Vue framework configuration or disable `features.componentsManifest`.',
});
}
}
30 changes: 30 additions & 0 deletions code/frameworks/vue3-vite/src/docgen/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
import type { Options, StorybookConfigRaw } from 'storybook/internal/types';

import type { FrameworkOptions, VueDocgenPlugin } from '../types.ts';

export const VUE_COMPONENT_META = 'vue-component-meta' satisfies VueDocgenPlugin;

export type ResolvedDocgenOptions = false | { plugin: VueDocgenPlugin; tsconfig?: string };

export interface DocgenContext {
docgen: ResolvedDocgenOptions;
features: StorybookConfigRaw['features'];
/**
* Only true if the docgen server is active and the `vue-component-meta` plugin is selected.
*/
docgenServerActive: boolean;
}

export async function resolveDocgenContext(options: Options): Promise<DocgenContext> {
const [frameworkOptions, features] = await Promise.all([
options.presets.apply<FrameworkOptions | null>('frameworkOptions'),
options.presets.apply('features', {}),
]);
const docgen = resolveDocgenOptions(frameworkOptions?.docgen);

return {
docgen,
features,
docgenServerActive:
features?.experimentalDocgenServer === true &&
docgen !== false &&
docgen.plugin === VUE_COMPONENT_META,
};
}

export function resolveDocgenOptions(docgen?: FrameworkOptions['docgen']): ResolvedDocgenOptions {
if (docgen === false) {
return false;
Expand Down
84 changes: 84 additions & 0 deletions code/frameworks/vue3-vite/src/docgen/preset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest';

import type { DocgenProviderDescriptor, Options } from 'storybook/internal/types';

import type { FrameworkOptions } from '../types.ts';
import { experimental_docgenProvider, experimental_manifests } from './preset.ts';

const optionsWith = (docgen?: FrameworkOptions['docgen'], features: Record<string, boolean> = {}) =>
({
presets: {
apply: async (key: string) => {
if (key === 'frameworkOptions') {
return { docgen };
}
return key === 'features' ? features : {};
},
},
}) as unknown as Options;

const existing: DocgenProviderDescriptor[] = [{ moduleSpecifier: '/addon/docgen-worker.js' }];
const docgenServerOn = { experimentalDocgenServer: true };
const componentsManifestOn = { ...docgenServerOn, componentsManifest: true };

describe('experimental_docgenProvider', () => {
it('appends a descriptor pointing at the renderer worker module', async () => {
const descriptors = await experimental_docgenProvider(
existing,
optionsWith('vue-component-meta', docgenServerOn)
);

expect(descriptors).toHaveLength(2);
// Appended, so addon providers stack on top of ours rather than replacing it.
expect(descriptors[0]).toBe(existing[0]);
expect(descriptors[1].moduleSpecifier).toMatch(/docgen-worker\.js$/);
});

// The worker extracts with vue-component-meta only. Registering for vue-docgen-api would
// silently swap the engine the project asked for; `docgen: false` opted out of extraction
// altogether and must stay opted out.
it.each(['vue-docgen-api' as const, undefined, false as const])(
'registers nothing for docgen: %s',
async (docgen) => {
await expect(
experimental_docgenProvider(existing, optionsWith(docgen, docgenServerOn))
).resolves.toEqual(existing);
}
);
});

describe('experimental_manifests', () => {
const manifests = (docgen?: FrameworkOptions['docgen'], features?: Record<string, boolean>) =>
experimental_manifests({}, optionsWith(docgen, features) as never);

// Core asserts `components.meta.docgen` is present whenever the feature is on, so omitting it
// fails a Vue `storybook build` outright rather than degrading the debugger.
it('declares the engine so core can label the components debugger', async () => {
await expect(manifests('vue-component-meta', componentsManifestOn)).resolves.toEqual({
components: { v: 0, components: {}, meta: { docgen: 'vue-component-meta', durationMs: 0 } },
});
});

it.each(['vue-docgen-api' as const, undefined, false as const])(
'rejects the components manifest for docgen: %s',
async (docgen) => {
await expect(manifests(docgen, componentsManifestOn)).rejects.toThrow(
"The Vue docgen manifest currently requires `docgen: 'vue-component-meta'` in `framework.options`."
);
}
);

// The manifest is populated from docgen-service payloads. When another engine (or none) runs,
// there are no payloads, so claiming an engine here would report "0 components" against it.
it.each(['vue-docgen-api' as const, undefined, false as const])(
'contributes nothing for docgen: %s',
async (docgen) => {
await expect(manifests(docgen, docgenServerOn)).resolves.toEqual({});
}
);

// Vue has no legacy component manifest, so with the feature off there is nothing to contribute.
it('contributes nothing when the docgen service is off', async () => {
await expect(manifests('vue-component-meta')).resolves.toEqual({});
});
});
64 changes: 64 additions & 0 deletions code/frameworks/vue3-vite/src/docgen/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { fileURLToPath } from 'node:url';

import type {
DocgenProviderDescriptor,
IndexEntry,
Options,
PresetPropertyFn,
StorybookConfigRaw,
} from 'storybook/internal/types';

import { Vue3ViteDocgenManifestError } from './errors.ts';
import { VUE_COMPONENT_META, resolveDocgenContext } from './options.ts';

/**
* Vue docgen provider.
*
* Contributes a {@link DocgenProviderDescriptor} pointing at `@storybook/vue3/internal/docgen-worker`
*/
export const experimental_docgenProvider = async (
existing: DocgenProviderDescriptor[] = [],
options: Options
): Promise<DocgenProviderDescriptor[]> => {
const { docgenServerActive } = await resolveDocgenContext(options);

if (!docgenServerActive) {
return existing;
}

return [
...existing,
{
moduleSpecifier: fileURLToPath(import.meta.resolve('@storybook/vue3/internal/docgen-worker')),
},
];
};

export const experimental_manifests: PresetPropertyFn<
'experimental_manifests',
StorybookConfigRaw,
{ manifestEntries: IndexEntry[]; watch: boolean }
> = async (existingManifests = {}, options) => {
const { features, docgenServerActive } = await resolveDocgenContext(options);

if (
features?.experimentalDocgenServer === true &&
features.componentsManifest === true &&
!docgenServerActive
) {
throw new Vue3ViteDocgenManifestError();
}

if (!docgenServerActive) {
return existingManifests;
}

return {
...existingManifests,
components: {
v: 0,
components: {},
meta: { docgen: VUE_COMPONENT_META, durationMs: 0 },
},
};
};
74 changes: 74 additions & 0 deletions code/frameworks/vue3-vite/src/preset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { Options } from 'storybook/internal/types';

import { vueComponentMeta } from './plugins/vue-component-meta.ts';
import { vueDocgen } from './plugins/vue-docgen.ts';
import { templateCompilation } from './plugins/vue-template.ts';
import type { FrameworkOptions } from './types.ts';

// The real plugin factories build a vue-component-meta checker / vue-docgen-api parser, which is
// far too heavy for a preset test. Identify them by name instead.
vi.mock('./plugins/vue-template.ts', { spy: true });
vi.mock('./plugins/vue-component-meta.ts', { spy: true });
vi.mock('./plugins/vue-docgen.ts', { spy: true });

beforeEach(() => {
vi.mocked(templateCompilation).mockResolvedValue({ name: 'template' });
vi.mocked(vueComponentMeta).mockResolvedValue({ name: 'vue-component-meta' });
vi.mocked(vueDocgen).mockResolvedValue({ name: 'vue-docgen-api' });
});

const optionsWith = (docgen: FrameworkOptions['docgen'], features: Record<string, boolean> = {}) =>
({
presets: {
apply: async (key: string) => {
if (key === 'frameworkOptions') {
return { docgen };
}
return key === 'features' ? features : {};
},
},
}) as unknown as Options;

const pluginNames = async (
docgen: FrameworkOptions['docgen'],
features?: Record<string, boolean>
) => {
const { viteFinal } = await import('./preset.ts');
const config = await viteFinal!({}, optionsWith(docgen, features));
return (config.plugins ?? []).map((plugin) => (plugin as { name: string }).name);
};

describe('viteFinal', () => {
it.each([
['vue-component-meta' as const, 'vue-component-meta'],
[undefined, 'vue-docgen-api'],
])('adds the %s docgen plugin when the docgen service is off', async (docgen, expected) => {
expect(await pluginNames(docgen)).toEqual(['template', expected]);
});

// The service extracts the same metadata, so leaving the plugin on would compile every component
// twice and put a `__docgenInfo` in the preview bundle that nothing reads.
it('omits the vue-component-meta plugin when the docgen service is on', async () => {
expect(await pluginNames('vue-component-meta', { experimentalDocgenServer: true })).toEqual([
'template',
]);
});

// vue-docgen-api has no worker-side extractor, so the feature flag must not strip its plugin —
// that would leave these projects with no docgen at all.
it.each(['vue-docgen-api' as const, undefined])(
'keeps the docgen plugin for docgen: %s even when the docgen service is on',
async (docgen) => {
expect(await pluginNames(docgen, { experimentalDocgenServer: true })).toEqual([
'template',
'vue-docgen-api',
]);
}
);

it('keeps template compilation when docgen is disabled', async () => {
expect(await pluginNames(false)).toEqual(['template']);
});
});
16 changes: 7 additions & 9 deletions code/frameworks/vue3-vite/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { PresetProperty } from 'storybook/internal/types';

import type { Plugin } from 'vite';

import { resolveDocgenOptions } from './docgen/options.ts';
import { VUE_COMPONENT_META, resolveDocgenContext } from './docgen/options.ts';
import { type VueDocgenEngine, vueComponentMeta } from './plugins/vue-component-meta.ts';
import { vueDocgen } from './plugins/vue-docgen.ts';
import { templateCompilation } from './plugins/vue-template.ts';
import type { FrameworkOptions, StorybookConfig } from './types.ts';
import type { StorybookConfig } from './types.ts';

export { experimental_docgenProvider, experimental_manifests } from './docgen/preset.ts';

export const core: PresetProperty<'core'> = {
builder: import.meta.resolve('@storybook/builder-vite'),
Expand All @@ -16,15 +18,11 @@ export const core: PresetProperty<'core'> = {
export const viteFinal: StorybookConfig['viteFinal'] = async (config, options) => {
const plugins: Plugin[] = [await templateCompilation()];

const framework = await options.presets.apply('framework');
const frameworkOptions: FrameworkOptions =
typeof framework === 'string' ? {} : (framework.options ?? {});

const docgen = resolveDocgenOptions(frameworkOptions.docgen);
const { docgen, docgenServerActive } = await resolveDocgenContext(options);

// add docgen plugin depending on framework option
if (docgen !== false) {
if (docgen.plugin === 'vue-component-meta') {
if (docgen !== false && !docgenServerActive) {
if (docgen.plugin === VUE_COMPONENT_META) {
const engine: VueDocgenEngine = await options.presets.apply('experimental_vueDocgenEngine');
plugins.push(await vueComponentMeta(engine, docgen.tsconfig));
} else {
Expand Down
Loading
Loading