Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';import { tmpdir } from 'node:os';import { dirname, join } from 'node:path';import { afterEach, expect, test } from 'vite-plus/test';import { buildComponentGuideInventory } from './component-guide-inventory.js';import { findComponentPropsContractIssues } from './component-props-contract.js';
const testDirectories: Array<string> = [];
afterEach(() => { for (const directory of testDirectories) { rmSync(directory, { force: true, recursive: true }); } testDirectories.length = 0;});
test('requires component props from an exported component parameter', () => { const fixture = createFixture({ entry: "export { Button, type ButtonProps } from './button.js';\n", files: { 'button.ts': 'export interface ButtonProps {}\nexport function Button(props: ButtonProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('follows an export-all module into core', () => { const fixture = createFixture({ entry: "export * from './button.js';\n", files: { 'button.ts': 'export interface ButtonProps {}\nexport function Button(props: ButtonProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('requires provider props from an exported provider parameter', () => { const fixture = createFixture({ entry: "export { Provider, type ProviderProps } from './provider.js';\n", files: { 'provider.ts': 'export interface ProviderProps {}\nexport function Provider(props: ProviderProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ProviderProps" in the API section', ]);});
test('requires factory options and the factory return type', () => { const fixture = createFixture({ entry: "export { createThing, type CreateThingOptions, type CreatedThing } from './factory.js';\n", files: { 'factory.ts': "export interface CreateThingOptions {}\nexport type CreatedThing = { value: string };\nexport function createThing(options: CreateThingOptions): CreatedThing { return { value: 'thing' }; }\n", }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "CreateThingOptions" in the API section', 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "CreatedThing" in the API section', ]);});
test('requires a render callback value', () => { const fixture = createFixture({ entry: "export { createRender, type RenderProps } from './render.js';\n", files: { 'render.ts': 'export type RenderProps = { value: string };\nexport function createRender(): (props: RenderProps) => string { return ({ value }) => value; }\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "RenderProps" in the API section', ]);});
test('requires a named object generic constraint', () => { const fixture = createFixture({ entry: "export { createThing, type ThingProps } from './factory.js';\n", files: { 'factory.ts': 'export type ThingProps = { value: string };\nexport function createThing<T extends ThingProps>(): T { throw new Error(); }\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ThingProps" in the API section', ]);});
test('requires contracts from a multipart entry point', () => { const fixture = createFixture({ entry: "export { Root, Item, type RootProps, type ItemProps } from './parts.js';\n", files: { 'parts.ts': 'export interface RootProps {}\nexport interface ItemProps {}\nexport function Root(props: RootProps) {}\nexport function Item(props: ItemProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "RootProps" in the API section', 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ItemProps" in the API section', ]);});
test('requires contracts re-exported through an intermediate local module', () => { const fixture = createFixture({ entry: "export { FieldLabel, type FieldLabelProps } from './field.js';\n", files: { 'field.ts': "import type { FieldLabelProps } from './label.js';\nimport { FieldLabel } from './label.js';\nexport type { FieldLabelProps };\nexport { FieldLabel };\n", 'label.ts': 'export interface FieldLabelProps {}\nexport function FieldLabel(props: FieldLabelProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "FieldLabelProps" in the API section', ]);});
test('follows aliased values and types through an intermediate re-export', () => { const fixture = createFixture({ entry: "export { Button, type ButtonProps } from './public.js';\n", files: { 'public.ts': "export { InternalButton as Button, type InternalProps as ButtonProps } from './impl.js';\n", 'impl.ts': 'export interface InternalProps {}\nexport function InternalButton(props: InternalProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('requires a public imported type from another local module', () => { const fixture = createFixture({ entry: "export { Button } from './button.js';\nexport type { ButtonProps } from './types.js';\n", files: { 'button.ts': "import type { ButtonProps } from './types.js';\nexport function Button(props: ButtonProps) {}\n", 'types.ts': 'export interface ButtonProps {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('uses the public alias for an imported type from another local module', () => { const fixture = createFixture({ entry: "export { Button } from './button.js';\nexport type { InternalProps as ButtonProps } from './types.js';\nexport { Item } from './item.js';\n", files: { 'button.ts': "import type { InternalProps } from './types.js';\nexport function Button(props: InternalProps) {}\n", 'types.ts': 'export interface InternalProps {}\n', 'item.ts': "export type InternalProps = 'small' | 'large';\nexport function Item(size: InternalProps) {}\n", }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('excludes a public imported leaf type from another local module', () => { const fixture = createFixture({ entry: "export { Button } from './button.js';\nexport type { ButtonSize } from './types.js';\n", files: { 'button.ts': "import type { ButtonSize } from './types.js';\nexport function Button(size: ButtonSize) {}\n", 'types.ts': "export type ButtonSize = 'small' | 'large';\n", }, });
expect(issues(fixture)).toEqual([]);});
test('excludes an imported object type that is not publicly exported', () => { const fixture = createFixture({ entry: "export { Button } from './button.js';\n", files: { 'button.ts': "import type { PrivateProps } from './types.js';\nexport function Button(props: PrivateProps) {}\n", 'types.ts': 'export interface PrivateProps {}\n', }, });
expect(issues(fixture)).toEqual([]);});
test('excludes a recipe variant type', () => { const fixture = createFixture({ entry: "export { buttonRecipe, type ButtonRecipeVariants } from './recipe.css.js';\n", files: { 'recipe.css.ts': 'export type ButtonRecipeVariants = { size: string };\n' }, });
expect(issues(fixture)).toEqual([]);});
test('excludes a leaf type', () => { const fixture = createFixture({ entry: "export { Button, type ButtonSize } from './button.js';\n", files: { 'button.ts': "export type ButtonSize = 'small' | 'large';\nexport function Button(size: ButtonSize) {}\n", }, });
expect(issues(fixture)).toEqual([]);});
test('excludes a leaf alias of a union', () => { const fixture = createFixture({ entry: "export { Button, type ButtonSize } from './button.js';\n", files: { 'button.ts': "type Size = 'small' | 'large';\nexport type ButtonSize = Size;\nexport function Button(size: ButtonSize) {}\n", }, });
expect(issues(fixture)).toEqual([]);});
test('requires an indirect alias that resolves to an object type', () => { const fixture = createFixture({ entry: "export { Button, type ButtonProps } from './button.js';\n", files: { 'button.ts': 'type Inner = { value: string };\ntype Alias = Inner;\nexport type ButtonProps = Alias;\nexport function Button(props: ButtonProps) {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', ]);});
test('classifies private aliases using the module that declared them', () => { const fixture = createFixture({ entry: "export { Root, type RootProps } from './root.js';\nexport { Item, type ItemProps } from './item.js';\n", files: { 'root.ts': 'type Alias = { value: string };\nexport type RootProps = Alias;\nexport function Root(props: RootProps) {}\n', 'item.ts': "type Alias = 'small' | 'large';\nexport type ItemProps = Alias;\nexport function Item(size: ItemProps) {}\n", }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "RootProps" in the API section', ]);});
test('does not require an object type with the same name as a public leaf type', () => { const fixture = createFixture({ entry: "export { Root, type SharedProps } from './root.js';\nexport { Item } from './item.js';\n", files: { 'root.ts': "export type SharedProps = 'small' | 'large';\nexport function Root(props: SharedProps) {}\n", 'item.ts': 'export type SharedProps = { value: string };\nexport function Item(props: SharedProps) {}\n', }, });
expect(issues(fixture)).toEqual([]);});
test('requires a public object type when a local leaf type has the same name', () => { const fixture = createFixture({ entry: "export { Root, type SharedProps } from './root.js';\nexport { Item } from './item.js';\n", files: { 'root.ts': 'export type SharedProps = { value: string };\nexport function Root(props: SharedProps) {}\n', 'item.ts': "export type SharedProps = 'small' | 'large';\nexport function Item(props: SharedProps) {}\n", }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "SharedProps" in the API section', ]);});
test('inspects only the re-exported callable when local modules share a value name', () => { const fixture = createFixture({ entry: "export { Root, type RootProps } from './root.js';\nexport { Item } from './item.js';\n", files: { 'root.ts': 'export interface RootProps {}\nexport function Root(props: RootProps) {}\n', 'item.ts': 'export declare const Root: (props: RootProps) => unknown;\nexport function Item() {}\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "RootProps" in the API section', ]);});
test('reports an unsupported relevant exported signature', () => { const fixture = createFixture({ entry: "export { Button, type ButtonProps } from './button.js';\n", files: { 'button.ts': 'export interface ButtonProps {}\nexport declare const Button: (props: ButtonProps) => unknown;\n', }, });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" has an unsupported exported signature "Button"', ]);});
test('does not treat a typed data constant as an unsupported signature', () => { const fixture = createFixture({ entry: "export { iconNames, iconViewBoxes } from './icon.js';\n", files: { 'icon.ts': "export const iconNames = ['add'] as const;\nexport const iconViewBoxes: Record<string, string> = { add: '0 0 24 24' };\n", }, });
expect(issues(fixture)).toEqual([]);});
test('does not follow public re-exports outside the implementation directory', () => { const fixture = createFixture({ entry: "export { Icon, iconNames, type IconProps } from './icon.js';\n", files: { 'icon.ts': "import { iconNames } from '../../generated/icon-data.js';\nexport { iconNames };\nexport interface IconProps {}\nexport function Icon(props: IconProps) {}\n", '../../generated/icon-data.ts': 'export declare const iconNames: (props: IconProps) => unknown;\n', }, props: ['IconProps'], });
expect(issues(fixture)).toEqual([]);});
test('reports frontmatter props that the entry point does not export', () => { const fixture = createFixture({ entry: "export { Button, type ButtonProps } from './button.js';\n", files: { 'button.ts': 'export interface ButtonProps {}\nexport function Button(props: ButtonProps) {}\n', }, props: ['MissingProps'], });
expect(issues(fixture)).toEqual([ 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" requires public object contract "ButtonProps" in the API section', 'actions/button.mdx: entry point "packages/@luke-ui/react/src/exports/button.ts" does not export API section type "MissingProps"', ]);});
function createFixture(input: { entry: string; files: Record<string, string>; props?: ReadonlyArray<string>;}): { inventory: ReturnType<typeof buildComponentGuideInventory>; reactPackageDir: string } { const directory = mkdtempSync(join(tmpdir(), 'luke-ui-component-props-contract-')); testDirectories.push(directory);
const componentsDir = join(directory, 'content', 'components'); const reactPackageDir = join(directory, 'react-package'); const sourceDir = join(reactPackageDir, 'src', 'core', 'button'); const exportsDir = join(reactPackageDir, 'src', 'exports'); mkdirSync(componentsDir, { recursive: true }); mkdirSync(sourceDir, { recursive: true }); mkdirSync(exportsDir, { recursive: true }); writeFileSync(join(componentsDir, 'meta.json'), '{"pages":["---Actions---","actions/button"]}'); writeFileSync( join(reactPackageDir, 'package.json'), '{"exports":{"./button":"./dist/button/index.js"}}', ); writeFileSync(join(exportsDir, 'button.ts'), input.entry.replaceAll("'./", "'../core/button/"));
for (const [path, contents] of Object.entries(input.files)) { const filePath = join(sourceDir, path); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, contents); }
const props = input.props ?? []; const guide = `---\ntitle: Button\nsource: packages/@luke-ui/react/src/exports/button.ts\n---\n\n${renderApiSection(props)}`; return { inventory: buildComponentGuideInventory({ componentsDir, guides: [{ group: 'actions', relativePath: 'actions/button.mdx', source: guide }], reactPackageJsonPath: join(reactPackageDir, 'package.json'), }), reactPackageDir, };}
function renderApiSection(props: ReadonlyArray<string>): string { if (props.length === 0) return ''; const tables = props .map( (name) => `<component-props-table path="packages/@luke-ui/react/src/core/button/button.ts" name="${name}" />`, ) .join('\n\n'); return `## API\n\n${tables}\n`;}
function issues(fixture: { inventory: ReturnType<typeof buildComponentGuideInventory>; reactPackageDir: string;}): Array<string> { return findComponentPropsContractIssues(fixture.inventory, fixture.reactPackageDir);}