mirror of
https://github.com/zoriya/v10.git
synced 2026-08-06 14:18:10 +00:00
feat(site): add util reference pipeline (#537)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c11395ece1
commit
78112fbefd
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractDataAttrs } from '../data-attrs-handler.js';
|
||||
import { createTestProgram } from './test-utils.js';
|
||||
import { createTestProgram, createTypedTestProgram } from './test-utils.js';
|
||||
|
||||
describe('extractDataAttrs', () => {
|
||||
it('extracts from {Name}DataAttrs constant', () => {
|
||||
@@ -141,6 +141,55 @@ describe('extractDataAttrs', () => {
|
||||
expect(result!.attrs[0]!.description).toBe('Present when the component is focused.');
|
||||
});
|
||||
|
||||
it('extracts @type JSDoc tag as type field', () => {
|
||||
const code = `
|
||||
export const MockComponentDataAttrs = {
|
||||
/**
|
||||
* The fill level.
|
||||
* @type {'empty' | 'partial' | 'full'}
|
||||
*/
|
||||
fillState: 'data-fill-state',
|
||||
} as const;
|
||||
`;
|
||||
const program = createTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBe("'empty' | 'partial' | 'full'");
|
||||
});
|
||||
|
||||
it('separates description from @type line', () => {
|
||||
const code = `
|
||||
export const MockComponentDataAttrs = {
|
||||
/**
|
||||
* The fill level.
|
||||
* @type {'empty' | 'partial' | 'full'}
|
||||
*/
|
||||
fillState: 'data-fill-state',
|
||||
} as const;
|
||||
`;
|
||||
const program = createTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.description).toBe('The fill level.');
|
||||
expect(result!.attrs[0]!.description).not.toContain('@type');
|
||||
});
|
||||
|
||||
it('omits type when no @type tag present', () => {
|
||||
const code = `
|
||||
export const MockComponentDataAttrs = {
|
||||
/** Present when the component is active. */
|
||||
active: 'data-active',
|
||||
} as const;
|
||||
`;
|
||||
const program = createTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to data-{key} when value is not a string literal', () => {
|
||||
const code = `
|
||||
const PREFIX = 'data-';
|
||||
@@ -154,4 +203,112 @@ describe('extractDataAttrs', () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.name).toBe('data-active');
|
||||
});
|
||||
|
||||
it('infers boolean as omitted type', () => {
|
||||
const code = `
|
||||
type StateAttrMap<State> = { [Key in keyof State]?: string };
|
||||
interface MockComponentState {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export const MockComponentDataAttrs = {
|
||||
active: 'data-active',
|
||||
} as const satisfies StateAttrMap<MockComponentState>;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBeUndefined();
|
||||
});
|
||||
|
||||
it('infers string literal union from state type', () => {
|
||||
const code = `
|
||||
type StateAttrMap<State> = { [Key in keyof State]?: string };
|
||||
interface MockComponentState {
|
||||
level: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export const MockComponentDataAttrs = {
|
||||
level: 'data-level',
|
||||
} as const satisfies StateAttrMap<MockComponentState>;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBe("'low' | 'medium' | 'high'");
|
||||
});
|
||||
|
||||
it('infers number type from state', () => {
|
||||
const code = `
|
||||
type StateAttrMap<State> = { [Key in keyof State]?: string };
|
||||
interface MockComponentState {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const MockComponentDataAttrs = {
|
||||
count: 'data-count',
|
||||
} as const satisfies StateAttrMap<MockComponentState>;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBe('number');
|
||||
});
|
||||
|
||||
it('infers through type alias to expanded literals', () => {
|
||||
const code = `
|
||||
type StateAttrMap<State> = { [Key in keyof State]?: string };
|
||||
type VolumeLevel = 'off' | 'low';
|
||||
interface MockComponentState {
|
||||
level: VolumeLevel;
|
||||
}
|
||||
|
||||
export const MockComponentDataAttrs = {
|
||||
level: 'data-level',
|
||||
} as const satisfies StateAttrMap<MockComponentState>;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBe("'off' | 'low'");
|
||||
});
|
||||
|
||||
it('JSDoc @type overrides inferred type', () => {
|
||||
const code = `
|
||||
type StateAttrMap<State> = { [Key in keyof State]?: string };
|
||||
interface MockComponentState {
|
||||
level: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export const MockComponentDataAttrs = {
|
||||
/**
|
||||
* The volume level.
|
||||
* @type {'quiet' | 'loud'}
|
||||
*/
|
||||
level: 'data-level',
|
||||
} as const satisfies StateAttrMap<MockComponentState>;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBe("'quiet' | 'loud'");
|
||||
});
|
||||
|
||||
it('no satisfies expression produces no inferred type', () => {
|
||||
const code = `
|
||||
export const MockComponentDataAttrs = {
|
||||
active: 'data-active',
|
||||
} as const;
|
||||
`;
|
||||
const program = createTypedTestProgram(code);
|
||||
const result = extractDataAttrs('test.ts', program, 'MockComponent');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.attrs[0]!.type).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
interface MediaState {
|
||||
playing: boolean;
|
||||
volume: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
buffered: number;
|
||||
fullscreen: boolean;
|
||||
}
|
||||
|
||||
interface PlaybackState {
|
||||
playing: boolean;
|
||||
}
|
||||
|
||||
interface VolumeState {
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface TimeState {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/** Select playback state from media state. */
|
||||
export function selectPlayback(state: MediaState): PlaybackState {
|
||||
return { playing: state.playing };
|
||||
}
|
||||
|
||||
/** Select volume state from media state. */
|
||||
export function selectVolume(state: MediaState): VolumeState {
|
||||
return { volume: state.volume };
|
||||
}
|
||||
|
||||
/** Select time state from media state. */
|
||||
export function selectTime(state: MediaState): TimeState {
|
||||
return { currentTime: state.currentTime, duration: state.duration };
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export { playerContext } from './player/context';
|
||||
export { createPlayer } from './player/create-player';
|
||||
export { PlayerController } from './player/player-controller';
|
||||
export { createContainerMixin } from './store/container-mixin';
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
interface PlayerContext {
|
||||
readonly player: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The player context for dependency injection.
|
||||
* @public
|
||||
*/
|
||||
export const playerContext: PlayerContext = { player: null };
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
interface HtmlPlayerInstance {
|
||||
play(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
interface HtmlPlayerOptions {
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
/** Create an HTML player instance. */
|
||||
export function createPlayer(options: HtmlPlayerOptions): HtmlPlayerInstance {
|
||||
return {} as HtmlPlayerInstance;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
interface ReactiveControllerHost {
|
||||
addController(controller: ReactiveController): void;
|
||||
requestUpdate(): void;
|
||||
}
|
||||
|
||||
interface ReactiveController {
|
||||
hostConnected?(): void;
|
||||
hostDisconnected?(): void;
|
||||
}
|
||||
|
||||
/** Manages the video player lifecycle. */
|
||||
export class PlayerController implements ReactiveController {
|
||||
#host: ReactiveControllerHost;
|
||||
|
||||
constructor(host: ReactiveControllerHost) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/** Whether the player is ready. */
|
||||
get ready(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
hostConnected(): void {}
|
||||
hostDisconnected(): void {}
|
||||
}
|
||||
site/scripts/api-docs-builder/src/tests/fixtures/monorepo/packages/html/src/store/container-mixin.ts
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
type Constructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
interface ContainerHost {
|
||||
connectedCallback(): void;
|
||||
}
|
||||
|
||||
/** Create a mixin that provides store container behavior. */
|
||||
export function createContainerMixin<T extends Constructor<ContainerHost>>(Base: T): T {
|
||||
return Base;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export { usePlayer } from './player/context';
|
||||
export { createPlayer } from './player/create-player';
|
||||
export { mergeProps } from './utils/merge-props';
|
||||
export { useFormat } from './utils/use-format';
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
interface PlayerStore {
|
||||
playing: boolean;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
playing: boolean;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
/** Access the player store or select state from it. */
|
||||
export function usePlayer(): PlayerStore;
|
||||
export function usePlayer<R>(selector: (state: StoreState) => R): R;
|
||||
export function usePlayer<R>(selector?: (state: StoreState) => R): PlayerStore | R {
|
||||
return {} as PlayerStore | R;
|
||||
}
|
||||
site/scripts/api-docs-builder/src/tests/fixtures/monorepo/packages/react/src/player/create-player.ts
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
interface PlayerInstance {
|
||||
play(): void;
|
||||
pause(): void;
|
||||
}
|
||||
|
||||
interface PlayerOptions {
|
||||
autoplay?: boolean;
|
||||
}
|
||||
|
||||
/** Create a React player instance. */
|
||||
export function createPlayer(options?: PlayerOptions): PlayerInstance {
|
||||
return {} as PlayerInstance;
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/** @public Merge multiple prop objects into one. */
|
||||
export function mergeProps<T extends Record<string, unknown>>(...args: T[]): T {
|
||||
return Object.assign({}, ...args) as T;
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/** Format a value for display. */
|
||||
|
||||
/** @label Number */
|
||||
export function useFormat(value: number): string;
|
||||
/** @label String */
|
||||
export function useFormat(value: string): string;
|
||||
export function useFormat(value: number | string): string {
|
||||
return String(value);
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
interface Store<S> {
|
||||
getState(): S;
|
||||
}
|
||||
|
||||
type SelectorFn<S, R> = (state: S) => R;
|
||||
|
||||
/** Create a memoized selector function. */
|
||||
export function createSelector<S, R>(fn: SelectorFn<S, R>): SelectorFn<S, R> {
|
||||
return fn;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { SnapshotController } from './snapshot-controller';
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
interface ReactiveControllerHost {
|
||||
addController(controller: ReactiveController): void;
|
||||
requestUpdate(): void;
|
||||
}
|
||||
|
||||
interface ReactiveController {
|
||||
hostConnected?(): void;
|
||||
hostDisconnected?(): void;
|
||||
}
|
||||
|
||||
interface Store<S> {
|
||||
getState(): S;
|
||||
}
|
||||
|
||||
/** Takes a snapshot of store state. */
|
||||
export class SnapshotController<S, R = S> implements ReactiveController {
|
||||
#host: ReactiveControllerHost;
|
||||
|
||||
/**
|
||||
* @param host - The host element.
|
||||
* @param state - The store to snapshot.
|
||||
* @param selector - Derives a value from state.
|
||||
*/
|
||||
constructor(host: ReactiveControllerHost, state: Store<S>, selector: (state: S) => R);
|
||||
/**
|
||||
* @param host - The host element.
|
||||
* @param state - The store to snapshot.
|
||||
*/
|
||||
constructor(host: ReactiveControllerHost, state: Store<S>);
|
||||
constructor(host: ReactiveControllerHost, state: Store<S>, selector?: (state: S) => R) {
|
||||
this.#host = host;
|
||||
}
|
||||
|
||||
/** The current snapshot value. */
|
||||
get value(): R {
|
||||
return {} as R;
|
||||
}
|
||||
|
||||
/** Track state changes. */
|
||||
track(): void {}
|
||||
|
||||
hostConnected(): void {}
|
||||
hostDisconnected(): void {}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export { useStore } from './use-store';
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
interface Store<S> {
|
||||
getState(): S;
|
||||
}
|
||||
|
||||
/** Subscribe to a store. */
|
||||
export function useStore<S>(store: Store<S>): S;
|
||||
export function useStore<S, R>(store: Store<S>, selector: (state: S) => R): R;
|
||||
export function useStore<S, R>(store: Store<S>, selector?: (state: S) => R): S | R {
|
||||
return {} as S | R;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,96 @@
|
||||
import * as tae from 'typescript-api-extractor';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatDetailedType, formatProperties, formatType, getShortPropType } from '../formatter';
|
||||
import { abbreviateType, formatDetailedType, formatProperties, formatType } from '../formatter';
|
||||
|
||||
describe('abbreviateType', () => {
|
||||
it("returns 'function' for pure function types (no union)", () => {
|
||||
expect(abbreviateType('selector', '((state: UnknownState) => R)')).toBe('function');
|
||||
expect(abbreviateType('subscribe', '((state: State) => void)')).toBe('function');
|
||||
expect(abbreviateType('isEqual', '((a: R, b: R) => boolean)')).toBe('function');
|
||||
expect(abbreviateType('config', '((options: Options) => Config)')).toBe('function');
|
||||
});
|
||||
|
||||
it("returns 'function' for function types with union return", () => {
|
||||
expect(abbreviateType('selector', '(state: object) => InferSliceState<S> | undefined')).toBe('function');
|
||||
expect(abbreviateType('selector', '(state: object) => string | undefined')).toBe('function');
|
||||
});
|
||||
|
||||
it("returns 'undefined | function' for true top-level union of undefined and function", () => {
|
||||
expect(abbreviateType('selector', '((state: object) => string) | undefined')).toBe('undefined | function');
|
||||
});
|
||||
|
||||
describe('getShortPropType', () => {
|
||||
it("returns 'function' for callback props (onX with =>)", () => {
|
||||
expect(getShortPropType('onClick', '(event: Event) => void')).toBe('function');
|
||||
expect(getShortPropType('onChange', '(value: string) => void')).toBe('function');
|
||||
expect(abbreviateType('onClick', '(event: Event) => void')).toBe('function');
|
||||
expect(abbreviateType('onChange', '(value: string) => void')).toBe('function');
|
||||
});
|
||||
|
||||
it("returns 'function' for getter props (getX with =>)", () => {
|
||||
expect(getShortPropType('getValue', '() => string')).toBe('function');
|
||||
expect(getShortPropType('getState', '() => State')).toBe('function');
|
||||
expect(abbreviateType('getValue', '() => string')).toBe('function');
|
||||
expect(abbreviateType('getState', '() => State')).toBe('function');
|
||||
});
|
||||
|
||||
it("returns 'string | function' for className with =>", () => {
|
||||
expect(getShortPropType('className', 'string | ((state: State) => string)')).toBe('string | function');
|
||||
expect(abbreviateType('className', 'string | ((state: State) => string)')).toBe('string | function');
|
||||
});
|
||||
|
||||
it("returns 'CSSProperties | function' for style with =>", () => {
|
||||
expect(getShortPropType('style', 'CSSProperties | ((state: State) => CSSProperties)')).toBe(
|
||||
expect(abbreviateType('style', 'CSSProperties | ((state: State) => CSSProperties)')).toBe(
|
||||
'CSSProperties | function'
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 'ReactElement | function' for render with =>", () => {
|
||||
expect(getShortPropType('render', 'ReactElement | ((state: State) => ReactElement)')).toBe(
|
||||
'ReactElement | function'
|
||||
);
|
||||
expect(abbreviateType('render', 'ReactElement | ((state: State) => ReactElement)')).toBe('ReactElement | function');
|
||||
});
|
||||
|
||||
it('returns undefined for simple types (boolean, string, number)', () => {
|
||||
expect(getShortPropType('disabled', 'boolean')).toBeUndefined();
|
||||
expect(getShortPropType('label', 'string')).toBeUndefined();
|
||||
expect(getShortPropType('count', 'number')).toBeUndefined();
|
||||
expect(abbreviateType('disabled', 'boolean')).toBeUndefined();
|
||||
expect(abbreviateType('label', 'string')).toBeUndefined();
|
||||
expect(abbreviateType('count', 'number')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for short unions (< 3 members and < 40 chars)', () => {
|
||||
expect(getShortPropType('size', "'small' | 'large'")).toBeUndefined();
|
||||
expect(getShortPropType('value', 'string | number')).toBeUndefined();
|
||||
expect(abbreviateType('size', "'small' | 'large'")).toBeUndefined();
|
||||
expect(abbreviateType('value', 'string | number')).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns 'type | function' for short callback unions (< 40 chars, 2 members)", () => {
|
||||
const type = 'string | ((state: TimeState) => string)';
|
||||
expect(getShortPropType('label', type)).toBe('string | function');
|
||||
expect(abbreviateType('label', type)).toBe('string | function');
|
||||
});
|
||||
|
||||
it("returns 'type | function' for unions containing functions", () => {
|
||||
const type = "string | ((state: State) => string) | 'auto'";
|
||||
expect(getShortPropType('label', type)).toBe("string | 'auto' | function");
|
||||
expect(abbreviateType('label', type)).toBe("string | 'auto' | function");
|
||||
});
|
||||
|
||||
it('returns undefined for complex unions (NOT "Union")', () => {
|
||||
// Complex union with 3+ members, no function
|
||||
const complexUnion = "'small' | 'medium' | 'large' | 'xlarge'";
|
||||
expect(getShortPropType('size', complexUnion)).toBeUndefined();
|
||||
expect(abbreviateType('size', complexUnion)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns "object" for object literals > 40 chars', () => {
|
||||
const longObject = '{ volume: number; muted: boolean; level: string }';
|
||||
expect(longObject.length).toBeGreaterThan(40);
|
||||
expect(abbreviateType('result', longObject)).toBe('object');
|
||||
});
|
||||
|
||||
it('returns undefined for object literals <= 40 chars', () => {
|
||||
const shortObject = '{ x: number; y: number }';
|
||||
expect(abbreviateType('point', shortObject)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('truncates other types > 40 chars', () => {
|
||||
const longType = "'option-a' | 'option-b' | 'option-c' | 'option-d' | 'option-e'";
|
||||
expect(longType.length).toBeGreaterThan(40);
|
||||
expect(abbreviateType('choice', longType)).toBe(`${longType.slice(0, 37)}...`);
|
||||
});
|
||||
|
||||
it('returns undefined for other types <= 40 chars', () => {
|
||||
const shortType = "'small' | 'medium' | 'large' | 'xlarge'";
|
||||
expect(shortType.length).toBeLessThanOrEqual(40);
|
||||
expect(abbreviateType('size', shortType)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +137,7 @@ describe('formatProperties', () => {
|
||||
const result = formatProperties(props);
|
||||
|
||||
expect(result.simple).toEqual({ type: 'boolean' });
|
||||
expect(Object.keys(result.simple!)).not.toContain('shortType');
|
||||
expect(Object.keys(result.simple!)).not.toContain('detailedType');
|
||||
expect(Object.keys(result.simple!)).not.toContain('default');
|
||||
expect(Object.keys(result.simple!)).not.toContain('required');
|
||||
});
|
||||
@@ -151,7 +188,7 @@ describe('formatProperties', () => {
|
||||
expect(result.type?.type).toBe("'current' | 'duration' | 'remaining'");
|
||||
});
|
||||
|
||||
it('sets shortType for callback props', () => {
|
||||
it('sets abbreviated type and detailedType for callback props', () => {
|
||||
const fnType = createFunctionNode([
|
||||
{
|
||||
parameters: [
|
||||
@@ -176,7 +213,8 @@ describe('formatProperties', () => {
|
||||
|
||||
const result = formatProperties([prop]);
|
||||
|
||||
expect(result.onClick?.shortType).toBe('function');
|
||||
expect(result.onClick?.type).toBe('function');
|
||||
expect(result.onClick?.detailedType).toBe('((event: Event) => void)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -352,6 +390,27 @@ describe('formatType', () => {
|
||||
expect(formatType(node, false)).toBe('T');
|
||||
});
|
||||
|
||||
it('returns type name for TypeParameterNode with large union constraint (>5 members)', () => {
|
||||
const largeUnion = createUnionNode([
|
||||
createLiteralNode("'a'"),
|
||||
createLiteralNode("'b'"),
|
||||
createLiteralNode("'c'"),
|
||||
createLiteralNode("'d'"),
|
||||
createLiteralNode("'e'"),
|
||||
createLiteralNode("'f'"),
|
||||
]);
|
||||
const node = createTypeParameterNode('TagName', largeUnion);
|
||||
|
||||
expect(formatType(node, false)).toBe('TagName');
|
||||
});
|
||||
|
||||
it('expands TypeParameterNode with small union constraint (<=5 members)', () => {
|
||||
const smallUnion = createUnionNode([createLiteralNode("'a'"), createLiteralNode("'b'"), createLiteralNode("'c'")]);
|
||||
const node = createTypeParameterNode('T', smallUnion);
|
||||
|
||||
expect(formatType(node, false)).toBe("'a' | 'b' | 'c'");
|
||||
});
|
||||
|
||||
// --- UnionNode with typeName ---
|
||||
|
||||
it('formats UnionNode with typeName as fully qualified name', () => {
|
||||
@@ -363,10 +422,10 @@ describe('formatType', () => {
|
||||
|
||||
// --- ObjectNode edge cases ---
|
||||
|
||||
it('formats empty ObjectNode as {}', () => {
|
||||
it('formats empty ObjectNode as object', () => {
|
||||
const node = createObjectNode([]);
|
||||
|
||||
expect(formatType(node, false)).toBe('{}');
|
||||
expect(formatType(node, false)).toBe('object');
|
||||
});
|
||||
|
||||
// --- Unknown node ---
|
||||
@@ -391,13 +450,28 @@ describe('formatType', () => {
|
||||
|
||||
// --- TypeParameterNode constraint flattening in union ---
|
||||
|
||||
it('flattens TypeParameterNode constraint in union', () => {
|
||||
it('flattens TypeParameterNode constraint in union when small (<=5 members)', () => {
|
||||
const constraintUnion = createUnionNode([createIntrinsicNode('string'), createIntrinsicNode('number')]);
|
||||
const typeParam = createTypeParameterNode('T', constraintUnion);
|
||||
const union = createUnionNode([typeParam, createIntrinsicNode('boolean')]);
|
||||
|
||||
expect(formatType(union, false)).toBe('string | number | boolean');
|
||||
});
|
||||
|
||||
it('does not flatten TypeParameterNode constraint in union when large (>5 members)', () => {
|
||||
const largeConstraint = createUnionNode([
|
||||
createLiteralNode("'a'"),
|
||||
createLiteralNode("'b'"),
|
||||
createLiteralNode("'c'"),
|
||||
createLiteralNode("'d'"),
|
||||
createLiteralNode("'e'"),
|
||||
createLiteralNode("'f'"),
|
||||
]);
|
||||
const typeParam = createTypeParameterNode('TagName', largeConstraint);
|
||||
const union = createUnionNode([typeParam, createIntrinsicNode('boolean')]);
|
||||
|
||||
expect(formatType(union, false)).toBe('TagName | boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDetailedType', () => {
|
||||
|
||||
@@ -11,3 +11,17 @@ export function createTestProgram(code: string, fileName = 'test.ts'): ts.Progra
|
||||
compilerHost.fileExists = (name) => name === fileName;
|
||||
return ts.createProgram([fileName], {}, compilerHost);
|
||||
}
|
||||
|
||||
/** Suitable for tests that need type resolution via `getTypeChecker()`. */
|
||||
export function createTypedTestProgram(code: string, fileName = 'test.ts'): ts.Program {
|
||||
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
|
||||
const options: ts.CompilerOptions = { strict: true, target: ts.ScriptTarget.ESNext };
|
||||
const compilerHost = ts.createCompilerHost(options);
|
||||
const originalGetSourceFile = compilerHost.getSourceFile;
|
||||
const originalFileExists = compilerHost.fileExists;
|
||||
compilerHost.getSourceFile = (name, ...args) => {
|
||||
return name === fileName ? sourceFile : originalGetSourceFile.call(compilerHost, name, ...args);
|
||||
};
|
||||
compilerHost.fileExists = (name) => name === fileName || originalFileExists.call(compilerHost, name);
|
||||
return ts.createProgram([fileName], options, compilerHost);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getUtilEntries, type UtilEntry } from '../util-handler';
|
||||
|
||||
const FIXTURE_ROOT = path.resolve(import.meta.dirname, 'fixtures/monorepo');
|
||||
|
||||
describe('getUtilEntries', () => {
|
||||
const entries = getUtilEntries(FIXTURE_ROOT);
|
||||
|
||||
function findByName(name: string, framework?: 'react' | 'html' | null): UtilEntry | undefined {
|
||||
return entries.find((e) => e.data.name === name && (framework === undefined || e.framework === framework));
|
||||
}
|
||||
|
||||
it('discovers hooks', () => {
|
||||
expect(findByName('usePlayer', 'react')).toBeDefined();
|
||||
expect(findByName('useStore', 'react')).toBeDefined();
|
||||
});
|
||||
|
||||
it('discovers controllers', () => {
|
||||
expect(findByName('PlayerController', 'html')).toBeDefined();
|
||||
expect(findByName('SnapshotController', 'html')).toBeDefined();
|
||||
});
|
||||
|
||||
it('discovers mixin with stripped display name', () => {
|
||||
const mixin = findByName('ContainerMixin', 'html');
|
||||
expect(mixin).toBeDefined();
|
||||
expect(mixin!.slug).toBe('container-mixin');
|
||||
});
|
||||
|
||||
it('discovers factories including createSelector', () => {
|
||||
const reactCreate = findByName('createPlayer', 'react');
|
||||
const htmlCreate = findByName('createPlayer', 'html');
|
||||
const createSelector = findByName('createSelector', null);
|
||||
|
||||
expect(reactCreate).toBeDefined();
|
||||
expect(htmlCreate).toBeDefined();
|
||||
expect(createSelector).toBeDefined();
|
||||
});
|
||||
|
||||
it('discovers @public utility and context', () => {
|
||||
expect(findByName('mergeProps', 'react')).toBeDefined();
|
||||
expect(findByName('playerContext', 'html')).toBeDefined();
|
||||
});
|
||||
|
||||
it('discovers selectors as framework-agnostic', () => {
|
||||
const selectorNames = ['selectPlayback', 'selectVolume', 'selectTime'];
|
||||
for (const name of selectorNames) {
|
||||
const entry = findByName(name, null);
|
||||
expect(entry, `expected to find ${name}`).toBeDefined();
|
||||
expect(entry!.framework).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('assigns correct frameworks', () => {
|
||||
// React
|
||||
expect(findByName('usePlayer')!.framework).toBe('react');
|
||||
expect(findByName('useStore')!.framework).toBe('react');
|
||||
expect(findByName('mergeProps')!.framework).toBe('react');
|
||||
|
||||
// HTML
|
||||
expect(findByName('PlayerController')!.framework).toBe('html');
|
||||
expect(findByName('SnapshotController')!.framework).toBe('html');
|
||||
expect(findByName('playerContext')!.framework).toBe('html');
|
||||
|
||||
// Framework-agnostic
|
||||
expect(findByName('selectPlayback')!.framework).toBeNull();
|
||||
expect(findByName('createSelector')!.framework).toBeNull();
|
||||
});
|
||||
|
||||
it('handles slug collision', () => {
|
||||
const reactCreate = entries.find((e) => e.slug === 'create-player');
|
||||
const htmlCreate = entries.find((e) => e.slug === 'html-create-player');
|
||||
|
||||
expect(reactCreate).toBeDefined();
|
||||
expect(reactCreate!.framework).toBe('react');
|
||||
expect(htmlCreate).toBeDefined();
|
||||
expect(htmlCreate!.framework).toBe('html');
|
||||
});
|
||||
|
||||
it('extracts multi-overload signatures', () => {
|
||||
const usePlayer = findByName('usePlayer', 'react');
|
||||
expect(usePlayer!.data.overloads).toHaveLength(2);
|
||||
|
||||
const useStore = findByName('useStore', 'react');
|
||||
expect(useStore!.data.overloads).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('preserves overloads with identical return types', () => {
|
||||
const useFormat = findByName('useFormat', 'react');
|
||||
expect(useFormat).toBeDefined();
|
||||
expect(useFormat!.data.overloads).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('extracts @label from overload JSDoc', () => {
|
||||
const useFormat = findByName('useFormat', 'react');
|
||||
expect(useFormat!.data.overloads[0]!.label).toBe('Number');
|
||||
expect(useFormat!.data.overloads[1]!.label).toBe('String');
|
||||
});
|
||||
|
||||
it('omits label when @label is not present', () => {
|
||||
const useStore = findByName('useStore', 'react');
|
||||
expect(useStore!.data.overloads[0]!.label).toBeUndefined();
|
||||
expect(useStore!.data.overloads[1]!.label).toBeUndefined();
|
||||
});
|
||||
|
||||
it('strips "- " prefix from controller param descriptions', () => {
|
||||
const snapshot = findByName('SnapshotController', 'html');
|
||||
expect(snapshot).toBeDefined();
|
||||
|
||||
const firstOverload = snapshot!.data.overloads[0]!;
|
||||
const hostParam = firstOverload.parameters.host;
|
||||
expect(hostParam).toBeDefined();
|
||||
expect(hostParam!.description).toBe('The host element.');
|
||||
expect(hostParam!.description).not.toMatch(/^-\s/);
|
||||
});
|
||||
|
||||
it('extracts JSDoc descriptions', () => {
|
||||
const usePlayer = findByName('usePlayer', 'react');
|
||||
expect(usePlayer!.data.description).toBeDefined();
|
||||
|
||||
const playerController = findByName('PlayerController', 'html');
|
||||
expect(playerController!.data.description).toBeDefined();
|
||||
|
||||
const playerContext = findByName('playerContext', 'html');
|
||||
expect(playerContext!.data.description).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user