fix(react): handle more scenarios when collecting component props for generating stories (#27528)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> Stories generation for React components only handles components receiving a `props` argument typed with an `Interface`. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Stories generation for React components should handle receiving the props with any name or as a destructured object and typed with an `Interface`, a type literal definition, or an inline type literal. In case it's a destructured object, it also supports having no type, in which case the story args will have those properties with type `unknown`. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #22053
This commit is contained in:
parent
6d6c3515fd
commit
fd74969367
@ -10,7 +10,7 @@ import type * as ts from 'typescript';
|
|||||||
import {
|
import {
|
||||||
findExportDeclarationsForJsx,
|
findExportDeclarationsForJsx,
|
||||||
getComponentNode,
|
getComponentNode,
|
||||||
getComponentPropsInterface,
|
parseComponentPropsInfo,
|
||||||
} from '../../utils/ast-utils';
|
} from '../../utils/ast-utils';
|
||||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export function getArgsDefaultValue(property: ts.SyntaxKind): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolvedValue = typeNameToDefault[property];
|
const resolvedValue = typeNameToDefault[property];
|
||||||
if (typeof resolvedValue === undefined) {
|
if (resolvedValue === undefined) {
|
||||||
return '';
|
return '';
|
||||||
} else if (typeof resolvedValue === 'string') {
|
} else if (typeof resolvedValue === 'string') {
|
||||||
return resolvedValue.replace(/\s/g, '+');
|
return resolvedValue.replace(/\s/g, '+');
|
||||||
@ -138,18 +138,24 @@ function findPropsAndGenerateFileForCypress(
|
|||||||
js: boolean,
|
js: boolean,
|
||||||
fromNodeArray?: boolean
|
fromNodeArray?: boolean
|
||||||
) {
|
) {
|
||||||
const propsInterface = getComponentPropsInterface(sourceFile, cmpDeclaration);
|
const info = parseComponentPropsInfo(sourceFile, cmpDeclaration);
|
||||||
|
|
||||||
let props: {
|
let props: {
|
||||||
name: string;
|
name: string;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (propsInterface) {
|
if (info) {
|
||||||
props = propsInterface.members.map((member: ts.PropertySignature) => {
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
|
||||||
|
props = info.props.map((member) => {
|
||||||
return {
|
return {
|
||||||
name: (member.name as ts.Identifier).text,
|
name: (member.name as ts.Identifier).text,
|
||||||
defaultValue: getArgsDefaultValue(member.type.kind),
|
defaultValue: tsModule.isBindingElement(member)
|
||||||
|
? getArgsDefaultValue(member.kind)
|
||||||
|
: getArgsDefaultValue(member.type.kind),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -717,6 +717,42 @@ const meta: Meta<typeof Test> = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof Test>;
|
type Story = StoryObj<typeof Test>;
|
||||||
|
|
||||||
|
export const Primary = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
style: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Heading: Story = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
style: '',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`react:component-story default setup component with props should setup controls based on the component destructured props defined in an inline literal type 1`] = `
|
||||||
|
"import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Test } from './test-ui-lib';
|
||||||
|
|
||||||
|
import { within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Test> = {
|
||||||
|
component: Test,
|
||||||
|
title: 'Test',
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Test>;
|
||||||
|
|
||||||
export const Primary = {
|
export const Primary = {
|
||||||
args: {
|
args: {
|
||||||
name: '',
|
name: '',
|
||||||
@ -737,7 +773,109 @@ export const Heading: Story = {
|
|||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`react:component-story default setup component with props should setup controls based on the component props 1`] = `
|
exports[`react:component-story default setup component with props should setup controls based on the component destructured props without type 1`] = `
|
||||||
|
"import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Test } from './test-ui-lib';
|
||||||
|
|
||||||
|
import { within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Test> = {
|
||||||
|
component: Test,
|
||||||
|
title: 'Test',
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Test>;
|
||||||
|
|
||||||
|
export const Primary = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Heading: Story = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: '',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`react:component-story default setup component with props should setup controls based on the component props defined in a literal type 1`] = `
|
||||||
|
"import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Test } from './test-ui-lib';
|
||||||
|
|
||||||
|
import { within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Test> = {
|
||||||
|
component: Test,
|
||||||
|
title: 'Test',
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Test>;
|
||||||
|
|
||||||
|
export const Primary = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Heading: Story = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`react:component-story default setup component with props should setup controls based on the component props defined in an inline literal type 1`] = `
|
||||||
|
"import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Test } from './test-ui-lib';
|
||||||
|
|
||||||
|
import { within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Test> = {
|
||||||
|
component: Test,
|
||||||
|
title: 'Test',
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Test>;
|
||||||
|
|
||||||
|
export const Primary = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Heading: Story = {
|
||||||
|
args: {
|
||||||
|
name: '',
|
||||||
|
displayAge: false,
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
expect(canvas.getByText(/Welcome to Test!/gi)).toBeTruthy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`react:component-story default setup component with props should setup controls based on the component props defined in an interface 1`] = `
|
||||||
"import type { Meta, StoryObj } from '@storybook/react';
|
"import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { Test } from './test-ui-lib';
|
import { Test } from './test-ui-lib';
|
||||||
|
|
||||||
|
|||||||
@ -126,24 +126,16 @@ describe('react:component-story', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('component with props', () => {
|
describe('component with props', () => {
|
||||||
beforeEach(async () => {
|
it('should setup controls based on the component props defined in an interface', async () => {
|
||||||
appTree.write(
|
appTree.write(
|
||||||
cmpPath,
|
cmpPath,
|
||||||
`import React from 'react';
|
`export interface TestProps {
|
||||||
|
|
||||||
import './test.scss';
|
|
||||||
|
|
||||||
export interface TestProps {
|
|
||||||
name: string;
|
name: string;
|
||||||
displayAge: boolean;
|
displayAge: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Test = (props: TestProps) => {
|
export const Test = (props: TestProps) => {
|
||||||
return (
|
return <h1>Welcome to test component, {props.name}</h1>;
|
||||||
<div>
|
|
||||||
<h1>Welcome to test component, {props.name}</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Test;
|
export default Test;
|
||||||
@ -154,9 +146,88 @@ describe('react:component-story', () => {
|
|||||||
componentPath: 'lib/test-ui-lib.tsx',
|
componentPath: 'lib/test-ui-lib.tsx',
|
||||||
project: 'test-ui-lib',
|
project: 'test-ui-lib',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should setup controls based on the component props', () => {
|
it('should setup controls based on the component props defined in a literal type', async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`export type TestProps = {
|
||||||
|
name: string;
|
||||||
|
displayAge: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Test = (props: TestProps) => {
|
||||||
|
return <h1>Welcome to test component, {props.name}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup controls based on the component props defined in an inline literal type', async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`export const Test = (props: { name: string; displayAge: boolean }) => {
|
||||||
|
return <h1>Welcome to test component, {props.name}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup controls based on the component destructured props defined in an inline literal type', async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`export const Test = ({ name, displayAge }: { name: string; displayAge: boolean }) => {
|
||||||
|
return <h1>Welcome to test component, {props.name}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup controls based on the component destructured props without type', async () => {
|
||||||
|
appTree.write(
|
||||||
|
cmpPath,
|
||||||
|
`export const Test = ({ name, displayAge }) => {
|
||||||
|
return <h1>Welcome to test component, {props.name}</h1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Test;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await componentStoryGenerator(appTree, {
|
||||||
|
componentPath: 'lib/test-ui-lib.tsx',
|
||||||
|
project: 'test-ui-lib',
|
||||||
|
});
|
||||||
|
|
||||||
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
expect(appTree.read(storyFilePath, 'utf-8')).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
findExportDeclarationsForJsx,
|
findExportDeclarationsForJsx,
|
||||||
getComponentNode,
|
getComponentNode,
|
||||||
} from '../../utils/ast-utils';
|
} from '../../utils/ast-utils';
|
||||||
import { getDefaultsForComponent } from '../../utils/component-props';
|
import { getComponentPropDefaults } from '../../utils/component-props';
|
||||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
|
|
||||||
let tsModule: typeof import('typescript');
|
let tsModule: typeof import('typescript');
|
||||||
@ -108,7 +108,7 @@ export function findPropsAndGenerateFile(
|
|||||||
isPlainJs: boolean,
|
isPlainJs: boolean,
|
||||||
fromNodeArray?: boolean
|
fromNodeArray?: boolean
|
||||||
) {
|
) {
|
||||||
const { propsTypeName, props, argTypes } = getDefaultsForComponent(
|
const { props, argTypes } = getComponentPropDefaults(
|
||||||
sourceFile,
|
sourceFile,
|
||||||
cmpDeclaration
|
cmpDeclaration
|
||||||
);
|
);
|
||||||
@ -123,7 +123,6 @@ export function findPropsAndGenerateFile(
|
|||||||
? `${name}--${(cmpDeclaration as any).name.text}`
|
? `${name}--${(cmpDeclaration as any).name.text}`
|
||||||
: name,
|
: name,
|
||||||
componentImportFileName: name,
|
componentImportFileName: name,
|
||||||
propsTypeName,
|
|
||||||
props,
|
props,
|
||||||
argTypes,
|
argTypes,
|
||||||
componentName: (cmpDeclaration as any).name.text,
|
componentName: (cmpDeclaration as any).name.text,
|
||||||
|
|||||||
@ -138,6 +138,36 @@ describe(AnotherCmp.name, () => {
|
|||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`componentTestGenerator single component per file should handle destructured props with no type 1`] = `
|
||||||
|
"import * as React from 'react'
|
||||||
|
import { AnotherCmp } from './some-lib'
|
||||||
|
|
||||||
|
|
||||||
|
describe(AnotherCmp.name, () => {
|
||||||
|
let props: {
|
||||||
|
handleClick: unknown;
|
||||||
|
text: unknown;
|
||||||
|
count: unknown;
|
||||||
|
isOkay: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
handleClick: '',
|
||||||
|
text: '',
|
||||||
|
count: '',
|
||||||
|
isOkay: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
cy.mount(<AnotherCmp {...props}/>)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`componentTestGenerator single component per file should handle named exports 1`] = `
|
exports[`componentTestGenerator single component per file should handle named exports 1`] = `
|
||||||
"import * as React from 'react'
|
"import * as React from 'react'
|
||||||
import { AnotherCmpProps, AnotherCmp } from './some-lib'
|
import { AnotherCmpProps, AnotherCmp } from './some-lib'
|
||||||
@ -201,3 +231,33 @@ describe(AnotherCmp.name, () => {
|
|||||||
|
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`componentTestGenerator single component per file should handle props with inline type 1`] = `
|
||||||
|
"import * as React from 'react'
|
||||||
|
import { AnotherCmp } from './some-lib'
|
||||||
|
|
||||||
|
|
||||||
|
describe(AnotherCmp.name, () => {
|
||||||
|
let props: {
|
||||||
|
handleClick: () => void;
|
||||||
|
text: string;
|
||||||
|
count: number;
|
||||||
|
isOkay: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
text: '',
|
||||||
|
count: 0,
|
||||||
|
isOkay: false,
|
||||||
|
handleClick: undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
cy.mount(<AnotherCmp {...props}/>)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|||||||
@ -321,6 +321,79 @@ export function AnotherCmp(props: AnotherCmpProps) {
|
|||||||
tree.read('some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
|
tree.read('some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle props with inline type', async () => {
|
||||||
|
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||||
|
await libraryGenerator(tree, {
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
name: 'some-lib',
|
||||||
|
skipFormat: true,
|
||||||
|
skipTsConfig: false,
|
||||||
|
style: 'scss',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
component: true,
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
tree.write(
|
||||||
|
'some-lib/src/lib/some-lib.tsx',
|
||||||
|
`export function AnotherCmp(props: {
|
||||||
|
handleClick: () => void;
|
||||||
|
text: string;
|
||||||
|
count: number;
|
||||||
|
isOkay: boolean;
|
||||||
|
}) {
|
||||||
|
return <button onClick='{handleClick}'>{props.text}</button>;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
await componentTestGenerator(tree, {
|
||||||
|
project: 'some-lib',
|
||||||
|
componentPath: 'some-lib/src/lib/some-lib.tsx',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists('some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
tree.read('some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle destructured props with no type', async () => {
|
||||||
|
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||||
|
await libraryGenerator(tree, {
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
name: 'some-lib',
|
||||||
|
skipFormat: true,
|
||||||
|
skipTsConfig: false,
|
||||||
|
style: 'scss',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
component: true,
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
});
|
||||||
|
|
||||||
|
tree.write(
|
||||||
|
'some-lib/src/lib/some-lib.tsx',
|
||||||
|
`export function AnotherCmp({
|
||||||
|
handleClick,
|
||||||
|
text,
|
||||||
|
count,
|
||||||
|
isOkay,
|
||||||
|
}) {
|
||||||
|
return <button onClick='{handleClick}'>{props.text}</button>;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
await componentTestGenerator(tree, {
|
||||||
|
project: 'some-lib',
|
||||||
|
componentPath: 'some-lib/src/lib/some-lib.tsx',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.exists('some-lib/src/lib/some-lib.cy.tsx')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
tree.read('some-lib/src/lib/some-lib.cy.tsx', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle no props', async () => {
|
it('should handle no props', async () => {
|
||||||
// this is the default behavior of the library component generator
|
// this is the default behavior of the library component generator
|
||||||
mockedAssertMinimumCypressVersion.mockReturnValue();
|
mockedAssertMinimumCypressVersion.mockReturnValue();
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
findExportDeclarationsForJsx,
|
findExportDeclarationsForJsx,
|
||||||
getComponentNode,
|
getComponentNode,
|
||||||
} from '../../utils/ast-utils';
|
} from '../../utils/ast-utils';
|
||||||
import { getDefaultsForComponent } from '../../utils/component-props';
|
import { getComponentPropDefaults } from '../../utils/component-props';
|
||||||
import { nxVersion } from '../../utils/versions';
|
import { nxVersion } from '../../utils/versions';
|
||||||
import { ComponentTestSchema } from './schema';
|
import { ComponentTestSchema } from './schema';
|
||||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
@ -72,7 +72,7 @@ function generateSpecsForComponents(tree: Tree, filePath: string) {
|
|||||||
|
|
||||||
if (cmpNodes?.length) {
|
if (cmpNodes?.length) {
|
||||||
const components = cmpNodes.map((cmp) => {
|
const components = cmpNodes.map((cmp) => {
|
||||||
const defaults = getDefaultsForComponent(sourceFile, cmp);
|
const defaults = getComponentPropDefaults(sourceFile, cmp);
|
||||||
const isDefaultExport = defaultExport
|
const isDefaultExport = defaultExport
|
||||||
? (defaultExport as any).name.text === (cmp as any).name.text
|
? (defaultExport as any).name.text === (cmp as any).name.text
|
||||||
: false;
|
: false;
|
||||||
@ -81,6 +81,7 @@ function generateSpecsForComponents(tree: Tree, filePath: string) {
|
|||||||
props: [...defaults.props, ...defaults.argTypes],
|
props: [...defaults.props, ...defaults.argTypes],
|
||||||
name: (cmp as any).name.text as string,
|
name: (cmp as any).name.text as string,
|
||||||
typeName: defaults.propsTypeName,
|
typeName: defaults.propsTypeName,
|
||||||
|
inlineTypeString: defaults.inlineTypeString,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const namedImports = components
|
const namedImports = components
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import * as React from 'react'
|
|||||||
<%- importStatement %>
|
<%- importStatement %>
|
||||||
|
|
||||||
<% for (let cmp of components) { %>
|
<% for (let cmp of components) { %>
|
||||||
describe(<%= cmp.name %>.name, () => {<% if (cmp.typeName) { %>
|
describe(<%= cmp.name %>.name, () => {<% if (cmp.props.length > 0) { %>
|
||||||
let props: <%= cmp.typeName%>;
|
let props: <% if (cmp.typeName) { %><%= cmp.typeName %><% } else if (cmp.inlineTypeString) { %><%- cmp.inlineTypeString %><% } else { %>unknown<% } %>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {<% for (let prop of cmp.props) { %>
|
props = {<% for (let prop of cmp.props) { %>
|
||||||
|
|||||||
@ -15,11 +15,15 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof NxWelcome>;
|
type Story = StoryObj<typeof NxWelcome>;
|
||||||
|
|
||||||
export const Primary = {
|
export const Primary = {
|
||||||
args: {},
|
args: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Heading: Story = {
|
export const Heading: Story = {
|
||||||
args: {},
|
args: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy();
|
expect(canvas.getByText(/Welcome to NxWelcome!/gi)).toBeTruthy();
|
||||||
@ -74,7 +78,9 @@ export default meta;
|
|||||||
type Story = StoryObj<typeof NxWelcome>;
|
type Story = StoryObj<typeof NxWelcome>;
|
||||||
|
|
||||||
export const Primary = {
|
export const Primary = {
|
||||||
args: {},
|
args: {
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -592,3 +592,221 @@ describe('getComponentNode', () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseComponentPropsInfo', () => {
|
||||||
|
it('should parse props from a function with typed props using an interface', () => {
|
||||||
|
const sourceCode = `export interface TestProps {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
export function Test(props: TestProps) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[1]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBe('TestProps');
|
||||||
|
expect(result.inlineTypeString).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with destructured typed props using an interface', () => {
|
||||||
|
const sourceCode = `export interface TestProps {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
export function Test({ name, age }: TestProps) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[1]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBe('TestProps');
|
||||||
|
expect(result.inlineTypeString).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with typed props using a literal type', () => {
|
||||||
|
const sourceCode = `export type TestProps = {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
export function Test(props: TestProps) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[1]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBe('TestProps');
|
||||||
|
expect(result.inlineTypeString).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with destructured typed props using a literal type', () => {
|
||||||
|
const sourceCode = `export type TestProps = {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
export function Test({ name, age }: TestProps) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[1]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBe('TestProps');
|
||||||
|
expect(result.inlineTypeString).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with typed props using an inline type', () => {
|
||||||
|
const sourceCode = `export function Test(props: { name: string; age: number }) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[0]);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBeNull();
|
||||||
|
expect(result.inlineTypeString).toMatchInlineSnapshot(
|
||||||
|
`"{ name: string; age: number }"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with destructured typed props using an inline type', () => {
|
||||||
|
const sourceCode = `export function Test({ name, age }: { name: string; age: number }) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[0]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect((result.props[0] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect((result.props[1] as ts.PropertySignature).type.getText()).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
expect(result.propsTypeName).toBeNull();
|
||||||
|
expect(result.inlineTypeString).toMatchInlineSnapshot(
|
||||||
|
`"{ name: string; age: number }"`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse props from a function with no type', () => {
|
||||||
|
const sourceCode = `export function Test({ name, age }) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[0]);
|
||||||
|
|
||||||
|
expect(result.props.length).toBe(2);
|
||||||
|
expect(result.props[0].name.getText()).toBe('name');
|
||||||
|
expect(result.props[1].name.getText()).toBe('age');
|
||||||
|
expect(result.propsTypeName).toBeNull();
|
||||||
|
expect(result.inlineTypeString).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
name: unknown;
|
||||||
|
age: unknown;
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when the props are not destructured and have not type', () => {
|
||||||
|
const sourceCode = `export function Test(props) {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[0]);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when there are no props', () => {
|
||||||
|
const sourceCode = `export function Test() {}`;
|
||||||
|
const source = ts.createSourceFile(
|
||||||
|
'some-component.tsx',
|
||||||
|
sourceCode,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = utils.parseComponentPropsInfo(source, source.statements[0]);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -665,44 +665,70 @@ export function getComponentNode(sourceFile: ts.SourceFile): ts.Node | null {
|
|||||||
return defaultExport;
|
return defaultExport;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComponentPropsInterface(
|
export function parseComponentPropsInfo(
|
||||||
sourceFile: ts.SourceFile,
|
sourceFile: ts.SourceFile,
|
||||||
cmpDeclaration: ts.Node
|
cmpDeclaration: ts.Node
|
||||||
): ts.InterfaceDeclaration | null {
|
): {
|
||||||
|
props: Array<ts.PropertySignature | ts.BindingElement>;
|
||||||
|
propsTypeName: string | null;
|
||||||
|
inlineTypeString: string | null;
|
||||||
|
} | null {
|
||||||
if (!tsModule) {
|
if (!tsModule) {
|
||||||
tsModule = ensureTypescript();
|
tsModule = ensureTypescript();
|
||||||
}
|
}
|
||||||
let propsTypeName: string = null;
|
let propsTypeName: string = null;
|
||||||
|
let inlineTypeString: string = null;
|
||||||
|
const props: Array<ts.PropertySignature | ts.BindingElement> = [];
|
||||||
|
|
||||||
|
const processParameters = (
|
||||||
|
parameters: ts.NodeArray<ts.ParameterDeclaration>
|
||||||
|
): boolean => {
|
||||||
|
if (!parameters.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsParam = parameters[0];
|
||||||
|
if (propsParam.type) {
|
||||||
|
if (tsModule.isTypeReferenceNode(propsParam.type)) {
|
||||||
|
// function Cmp(props: Props) {}
|
||||||
|
propsTypeName = propsParam.type.typeName.getText();
|
||||||
|
} else if (tsModule.isTypeLiteralNode(propsParam.type)) {
|
||||||
|
// function Cmp(props: {a: string, b: number}) {}
|
||||||
|
props.push(
|
||||||
|
...(propsParam.type.members as ts.NodeArray<ts.PropertySignature>)
|
||||||
|
);
|
||||||
|
inlineTypeString = propsParam.type.getText();
|
||||||
|
} else {
|
||||||
|
// we don't support other types (e.g. union types)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (tsModule.isObjectBindingPattern(propsParam.name)) {
|
||||||
|
// function Cmp({a, b}) {}
|
||||||
|
props.push(...propsParam.name.elements);
|
||||||
|
inlineTypeString = `{\n${propsParam.name.elements
|
||||||
|
.map((x) => `${x.name.getText()}: unknown;\n`)
|
||||||
|
.join('')}}`;
|
||||||
|
} else {
|
||||||
|
// function Cmp(props) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
if (tsModule.isFunctionDeclaration(cmpDeclaration)) {
|
if (tsModule.isFunctionDeclaration(cmpDeclaration)) {
|
||||||
const propsParam: ts.ParameterDeclaration = cmpDeclaration.parameters.find(
|
const result = processParameters(cmpDeclaration.parameters);
|
||||||
(x) =>
|
if (!result) {
|
||||||
tsModule.isParameter(x) && (x.name as ts.Identifier).text === 'props'
|
return null;
|
||||||
);
|
|
||||||
|
|
||||||
if (propsParam?.type?.['typeName']) {
|
|
||||||
propsTypeName = (
|
|
||||||
(propsParam.type as ts.TypeReferenceNode).typeName as ts.Identifier
|
|
||||||
).text;
|
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(cmpDeclaration as ts.VariableDeclaration).initializer &&
|
tsModule.isVariableDeclaration(cmpDeclaration) &&
|
||||||
tsModule.isArrowFunction(
|
cmpDeclaration.initializer &&
|
||||||
(cmpDeclaration as ts.VariableDeclaration).initializer
|
tsModule.isArrowFunction(cmpDeclaration.initializer)
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
const arrowFn = (cmpDeclaration as ts.VariableDeclaration)
|
const result = processParameters(cmpDeclaration.initializer.parameters);
|
||||||
.initializer as ts.ArrowFunction;
|
if (!result) {
|
||||||
|
return null;
|
||||||
const propsParam: ts.ParameterDeclaration = arrowFn.parameters.find(
|
|
||||||
(x) =>
|
|
||||||
tsModule.isParameter(x) && (x.name as ts.Identifier).text === 'props'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (propsParam?.type?.['typeName']) {
|
|
||||||
propsTypeName = (
|
|
||||||
(propsParam.type as ts.TypeReferenceNode).typeName as ts.Identifier
|
|
||||||
).text;
|
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
// do we have a class component extending from React.Component
|
// do we have a class component extending from React.Component
|
||||||
@ -731,12 +757,68 @@ export function getComponentPropsInterface(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (propsTypeName) {
|
if (propsTypeName) {
|
||||||
return findNodes(sourceFile, tsModule.SyntaxKind.InterfaceDeclaration).find(
|
const foundProps = getPropsFromTypeName(sourceFile, propsTypeName);
|
||||||
(x: ts.InterfaceDeclaration) => {
|
if (!foundProps) {
|
||||||
return (x.name as ts.Identifier).getText() === propsTypeName;
|
return null;
|
||||||
}
|
}
|
||||||
) as ts.InterfaceDeclaration;
|
|
||||||
} else {
|
for (const prop of foundProps) {
|
||||||
|
props.push(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
propsTypeName,
|
||||||
|
props,
|
||||||
|
inlineTypeString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPropsFromTypeName(
|
||||||
|
sourceFile: ts.SourceFile,
|
||||||
|
propsTypeName: string
|
||||||
|
): Array<ts.PropertySignature | ts.BindingElement> {
|
||||||
|
const matchingNode = findNodes(sourceFile, [
|
||||||
|
tsModule.SyntaxKind.InterfaceDeclaration,
|
||||||
|
tsModule.SyntaxKind.TypeAliasDeclaration,
|
||||||
|
]).find((x): x is ts.InterfaceDeclaration | ts.TypeAliasDeclaration => {
|
||||||
|
if (
|
||||||
|
tsModule.isTypeAliasDeclaration(x) ||
|
||||||
|
tsModule.isInterfaceDeclaration(x)
|
||||||
|
) {
|
||||||
|
return x.name.getText() === propsTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!matchingNode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props: Array<ts.PropertySignature | ts.BindingElement> = [];
|
||||||
|
if (tsModule.isTypeAliasDeclaration(matchingNode)) {
|
||||||
|
if (tsModule.isTypeLiteralNode(matchingNode.type)) {
|
||||||
|
for (const prop of matchingNode.type.members) {
|
||||||
|
props.push(prop as ts.PropertySignature);
|
||||||
|
}
|
||||||
|
} else if (tsModule.isTypeReferenceNode(matchingNode.type)) {
|
||||||
|
const result = getPropsFromTypeName(
|
||||||
|
sourceFile,
|
||||||
|
matchingNode.type.typeName.getText()
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
props.push(...result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we don't support other types of type aliases (e.g. union types)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const prop of matchingNode.members) {
|
||||||
|
props.push(prop as ts.PropertySignature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
import type * as ts from 'typescript';
|
import type * as ts from 'typescript';
|
||||||
import { getComponentPropsInterface } from './ast-utils';
|
import { parseComponentPropsInfo } from './ast-utils';
|
||||||
|
|
||||||
let tsModule: typeof import('typescript');
|
let tsModule: typeof import('typescript');
|
||||||
|
|
||||||
@ -16,18 +16,19 @@ export function getArgsDefaultValue(property: ts.SyntaxKind): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resolvedValue = typeNameToDefault[property];
|
const resolvedValue = typeNameToDefault[property];
|
||||||
if (typeof resolvedValue === undefined) {
|
if (resolvedValue === undefined) {
|
||||||
return "''";
|
return "''";
|
||||||
} else {
|
} else {
|
||||||
return resolvedValue;
|
return resolvedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultsForComponent(
|
export function getComponentPropDefaults(
|
||||||
sourceFile: ts.SourceFile,
|
sourceFile: ts.SourceFile,
|
||||||
cmpDeclaration: ts.Node
|
cmpDeclaration: ts.Node
|
||||||
): {
|
): {
|
||||||
propsTypeName: string;
|
propsTypeName: string | null;
|
||||||
|
inlineTypeString: string | null;
|
||||||
props: {
|
props: {
|
||||||
name: string;
|
name: string;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
@ -41,9 +42,11 @@ export function getDefaultsForComponent(
|
|||||||
if (!tsModule) {
|
if (!tsModule) {
|
||||||
tsModule = ensureTypescript();
|
tsModule = ensureTypescript();
|
||||||
}
|
}
|
||||||
const propsInterface = getComponentPropsInterface(sourceFile, cmpDeclaration);
|
|
||||||
|
const info = parseComponentPropsInfo(sourceFile, cmpDeclaration);
|
||||||
|
|
||||||
let propsTypeName: string = null;
|
let propsTypeName: string = null;
|
||||||
|
let inlineTypeString: string = null;
|
||||||
let props: {
|
let props: {
|
||||||
name: string;
|
name: string;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
@ -54,25 +57,35 @@ export function getDefaultsForComponent(
|
|||||||
actionText: string;
|
actionText: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (propsInterface) {
|
if (info) {
|
||||||
propsTypeName = propsInterface.name.text;
|
propsTypeName = info.propsTypeName;
|
||||||
props = propsInterface.members.map((member: ts.PropertySignature) => {
|
inlineTypeString = info.inlineTypeString;
|
||||||
if (member.type.kind === tsModule.SyntaxKind.FunctionType) {
|
props = info.props.map((member) => {
|
||||||
argTypes.push({
|
if (tsModule.isPropertySignature(member)) {
|
||||||
name: (member.name as ts.Identifier).text,
|
if (member.type.kind === tsModule.SyntaxKind.FunctionType) {
|
||||||
type: 'action',
|
argTypes.push({
|
||||||
actionText: `${(member.name as ts.Identifier).text} executed!`,
|
name: member.name.getText(),
|
||||||
});
|
type: 'action',
|
||||||
|
actionText: `${member.name.getText()} executed!`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: member.name.getText(),
|
||||||
|
defaultValue: getArgsDefaultValue(member.type.kind),
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// it's a binding element, which doesn't have a type, e.g.:
|
||||||
|
// const Cmp = ({ a, b }) => {}
|
||||||
return {
|
return {
|
||||||
name: (member.name as ts.Identifier).text,
|
name: member.name.getText(),
|
||||||
defaultValue: getArgsDefaultValue(member.type.kind),
|
defaultValue: getArgsDefaultValue(member.kind),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
props = props.filter((p) => p && p.defaultValue !== undefined);
|
props = props.filter((p) => p && p.defaultValue !== undefined);
|
||||||
}
|
}
|
||||||
return { propsTypeName, props, argTypes };
|
return { propsTypeName, inlineTypeString, props, argTypes };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImportForType(sourceFile: ts.SourceFile, typeName: string) {
|
export function getImportForType(sourceFile: ts.SourceFile, typeName: string) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user