From 856949b9f8c517935019a4b96f1ece0abbd00db0 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 18 Jul 2023 13:49:55 +0200 Subject: [PATCH 1/4] feat(tools): bootstrap react-component generator --- tools/generators/react-component/README.md | 38 ++++++++++++++ .../files/constants.ts__tmpl__ | 1 + .../generators/react-component/index.spec.ts | 20 ++++++++ tools/generators/react-component/index.ts | 50 +++++++++++++++++++ .../react-component/lib/utils.spec.ts | 7 +++ tools/generators/react-component/lib/utils.ts | 5 ++ tools/generators/react-component/schema.json | 17 +++++++ tools/generators/react-component/schema.ts | 6 +++ 8 files changed, 144 insertions(+) create mode 100644 tools/generators/react-component/README.md create mode 100644 tools/generators/react-component/files/constants.ts__tmpl__ create mode 100644 tools/generators/react-component/index.spec.ts create mode 100644 tools/generators/react-component/index.ts create mode 100644 tools/generators/react-component/lib/utils.spec.ts create mode 100644 tools/generators/react-component/lib/utils.ts create mode 100644 tools/generators/react-component/schema.json create mode 100644 tools/generators/react-component/schema.ts diff --git a/tools/generators/react-component/README.md b/tools/generators/react-component/README.md new file mode 100644 index 00000000000000..c77872b081eac3 --- /dev/null +++ b/tools/generators/react-component/README.md @@ -0,0 +1,38 @@ +# react-component + +Workspace Generator ...TODO... + + + +- [Usage](#usage) + - [Examples](#examples) +- [Options](#options) + - [`name`](#name) + + + +## Usage + +```sh +yarn nx workspace-generator react-component ... +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator react-component --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator react-component +``` + +## Options + +#### `name` + +Type: `string` + +TODO... diff --git a/tools/generators/react-component/files/constants.ts__tmpl__ b/tools/generators/react-component/files/constants.ts__tmpl__ new file mode 100644 index 00000000000000..d9913111a9dc76 --- /dev/null +++ b/tools/generators/react-component/files/constants.ts__tmpl__ @@ -0,0 +1 @@ +export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/react-component/index.spec.ts b/tools/generators/react-component/index.spec.ts new file mode 100644 index 00000000000000..2da95c0dc7c3f1 --- /dev/null +++ b/tools/generators/react-component/index.spec.ts @@ -0,0 +1,20 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Tree, readProjectConfiguration } from '@nrwl/devkit'; + +import generator from './index'; +import { ReactComponentGeneratorSchema } from './schema'; + +describe('react-component generator', () => { + let appTree: Tree; + const options: ReactComponentGeneratorSchema = { name: 'test' }; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + }); + + it('should run successfully', async () => { + await generator(appTree, options); + const config = readProjectConfiguration(appTree, 'test'); + expect(config).toBeDefined(); + }); +}); diff --git a/tools/generators/react-component/index.ts b/tools/generators/react-component/index.ts new file mode 100644 index 00000000000000..80375c30653f86 --- /dev/null +++ b/tools/generators/react-component/index.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; +import { Tree, formatFiles, installPackagesTask, names, generateFiles } from '@nrwl/devkit'; +import { libraryGenerator } from '@nrwl/workspace/generators'; + +import { getProjectConfig } from '../../utils'; + +import { ReactComponentGeneratorSchema } from './schema'; + +interface NormalizedSchema extends ReturnType {} + +export default async function (tree: Tree, schema: ReactComponentGeneratorSchema) { + await libraryGenerator(tree, { name: schema.name }); + + const normalizedOptions = normalizeOptions(tree, schema); + + addFiles(tree, normalizedOptions); + + await formatFiles(tree); + + return () => { + installPackagesTask(tree); + }; +} + +function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { + const project = getProjectConfig(tree, { packageName: options.name }); + + return { + ...options, + ...project, + ...names(options.name), + }; +} + +/** + * NOTE: remove this if your generator doesn't process any static/dynamic templates + */ +function addFiles(tree: Tree, options: NormalizedSchema) { + const templateOptions = { + ...options, + tmpl: '', + }; + + generateFiles( + tree, + path.join(__dirname, 'files'), + path.join(options.projectConfig.root, options.name), + templateOptions, + ); +} diff --git a/tools/generators/react-component/lib/utils.spec.ts b/tools/generators/react-component/lib/utils.spec.ts new file mode 100644 index 00000000000000..91e05a54a86f53 --- /dev/null +++ b/tools/generators/react-component/lib/utils.spec.ts @@ -0,0 +1,7 @@ +import { dummyHelper } from './utils'; + +describe(`utils`, () => { + it(`should behave...`, () => { + expect(dummyHelper()).toBe(undefined); + }); +}); diff --git a/tools/generators/react-component/lib/utils.ts b/tools/generators/react-component/lib/utils.ts new file mode 100644 index 00000000000000..c340a27c29c8b0 --- /dev/null +++ b/tools/generators/react-component/lib/utils.ts @@ -0,0 +1,5 @@ +// use this module to define any kind of generic utilities that are used in more than 1 place within the generator implementation + +export function dummyHelper() { + return; +} diff --git a/tools/generators/react-component/schema.json b/tools/generators/react-component/schema.json new file mode 100644 index 00000000000000..51ab1b434a0f71 --- /dev/null +++ b/tools/generators/react-component/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "react-component", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": ["name"] +} diff --git a/tools/generators/react-component/schema.ts b/tools/generators/react-component/schema.ts new file mode 100644 index 00000000000000..ce03ad1a4e7d78 --- /dev/null +++ b/tools/generators/react-component/schema.ts @@ -0,0 +1,6 @@ +export interface ReactComponentGeneratorSchema { + /** + * Library name + */ + name: string; +} From d33b90ad3b112fb5ad3bf1df91f4e8a8bdbc2f35 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 18 Jul 2023 15:37:01 +0200 Subject: [PATCH 2/4] feat(tools): implement react-component generator --- .../__componentName__.test.tsx__tmpl__ | 18 ++ .../component/__componentName__.tsx__tmpl__ | 18 ++ .../__componentName__.types.ts__tmpl__ | 18 ++ .../files/component/index.ts__tmpl__ | 5 + .../render__componentName__.tsx__tmpl__ | 16 ++ .../component/use__componentName__.ts__tmpl__ | 28 +++ ...e__componentName__Styles.styles.ts__tmpl__ | 34 +++ .../files/constants.ts__tmpl__ | 1 - .../__componentName__BestPractices.md__tmpl__ | 5 + ...componentName__Default.stories.tsx__tmpl__ | 6 + .../__componentName__Description.md__tmpl__ | 0 .../files/story/index.stories.tsx__tmpl__ | 18 ++ .../generators/react-component/index.spec.ts | 195 +++++++++++++++++- tools/generators/react-component/index.ts | 83 ++++++-- tools/generators/react-component/lib/.gitkeep | 0 .../react-component/lib/utils.spec.ts | 7 - tools/generators/react-component/lib/utils.ts | 5 - tools/generators/react-component/schema.json | 18 +- tools/generators/react-component/schema.ts | 12 +- 19 files changed, 447 insertions(+), 40 deletions(-) create mode 100644 tools/generators/react-component/files/component/__componentName__.test.tsx__tmpl__ create mode 100644 tools/generators/react-component/files/component/__componentName__.tsx__tmpl__ create mode 100644 tools/generators/react-component/files/component/__componentName__.types.ts__tmpl__ create mode 100644 tools/generators/react-component/files/component/index.ts__tmpl__ create mode 100644 tools/generators/react-component/files/component/render__componentName__.tsx__tmpl__ create mode 100644 tools/generators/react-component/files/component/use__componentName__.ts__tmpl__ create mode 100644 tools/generators/react-component/files/component/use__componentName__Styles.styles.ts__tmpl__ delete mode 100644 tools/generators/react-component/files/constants.ts__tmpl__ create mode 100644 tools/generators/react-component/files/story/__componentName__BestPractices.md__tmpl__ create mode 100644 tools/generators/react-component/files/story/__componentName__Default.stories.tsx__tmpl__ create mode 100644 tools/generators/react-component/files/story/__componentName__Description.md__tmpl__ create mode 100644 tools/generators/react-component/files/story/index.stories.tsx__tmpl__ create mode 100644 tools/generators/react-component/lib/.gitkeep delete mode 100644 tools/generators/react-component/lib/utils.spec.ts delete mode 100644 tools/generators/react-component/lib/utils.ts diff --git a/tools/generators/react-component/files/component/__componentName__.test.tsx__tmpl__ b/tools/generators/react-component/files/component/__componentName__.test.tsx__tmpl__ new file mode 100644 index 00000000000000..e0e546f4908c2e --- /dev/null +++ b/tools/generators/react-component/files/component/__componentName__.test.tsx__tmpl__ @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { <%= componentName %> } from './<%= componentName %>'; + +describe('<%= componentName %>', () => { + isConformant({ + Component: <%= componentName %>, + displayName: '<%= componentName %>', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(<<%= componentName %>>Default <%= componentName %>>); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/tools/generators/react-component/files/component/__componentName__.tsx__tmpl__ b/tools/generators/react-component/files/component/__componentName__.tsx__tmpl__ new file mode 100644 index 00000000000000..970c0c293b20e4 --- /dev/null +++ b/tools/generators/react-component/files/component/__componentName__.tsx__tmpl__ @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { use<%= componentName %>_unstable } from './use<%= componentName %>'; +import { render<%= componentName %>_unstable } from './render<%= componentName %>'; +import { use<%= componentName %>Styles_unstable } from './use<%= componentName %>Styles.styles'; +import type { <%= componentName %>Props } from './<%= componentName %>.types'; + +/** + * <%= componentName %> component - TODO: add more docs + */ +export const <%= componentName %>: ForwardRefComponent<<%= componentName %>Props> = React.forwardRef((props, ref) => { + const state = use<%= componentName %>_unstable(props, ref); + + use<%= componentName %>Styles_unstable(state); + return render<%= componentName %>_unstable(state); +}); + +<%= componentName %>.displayName = '<%= componentName %>'; diff --git a/tools/generators/react-component/files/component/__componentName__.types.ts__tmpl__ b/tools/generators/react-component/files/component/__componentName__.types.ts__tmpl__ new file mode 100644 index 00000000000000..26aaaef3cb5464 --- /dev/null +++ b/tools/generators/react-component/files/component/__componentName__.types.ts__tmpl__ @@ -0,0 +1,18 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type <%= componentName %>Slots = { + root: Slot<'div'>; +}; + +/** + * <%= componentName %> Props + */ +export type <%= componentName %>Props = ComponentProps<<%= componentName %>Slots> & {}; + +/** + * State used in rendering <%= componentName %> + */ +export type <%= componentName %>State = ComponentState<<%= componentName %>Slots>; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from <%= componentName %>Props. +// & RequiredProps, 'propName'>> +; diff --git a/tools/generators/react-component/files/component/index.ts__tmpl__ b/tools/generators/react-component/files/component/index.ts__tmpl__ new file mode 100644 index 00000000000000..b71d9258498439 --- /dev/null +++ b/tools/generators/react-component/files/component/index.ts__tmpl__ @@ -0,0 +1,5 @@ +export * from './<%= componentName %>'; +export * from './<%= componentName %>.types'; +export * from './render<%= componentName %>'; +export * from './use<%= componentName %>'; +export * from './use<%= componentName %>Styles.styles'; diff --git a/tools/generators/react-component/files/component/render__componentName__.tsx__tmpl__ b/tools/generators/react-component/files/component/render__componentName__.tsx__tmpl__ new file mode 100644 index 00000000000000..77735347d2f0f3 --- /dev/null +++ b/tools/generators/react-component/files/component/render__componentName__.tsx__tmpl__ @@ -0,0 +1,16 @@ +/** @jsxRuntime classic */ +/** @jsx createElement */ + +import { createElement } from '@fluentui/react-jsx-runtime'; +import { getSlotsNext } from '@fluentui/react-utilities'; +import type { <%= componentName %>State, <%= componentName %>Slots } from './<%= componentName %>.types'; + +/** + * Render the final JSX of <%= componentName %> + */ +export const render<%= componentName %>_unstable = (state: <%= componentName %>State) => { + const { slots, slotProps } = getSlotsNext<<%= componentName %>Slots>(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/tools/generators/react-component/files/component/use__componentName__.ts__tmpl__ b/tools/generators/react-component/files/component/use__componentName__.ts__tmpl__ new file mode 100644 index 00000000000000..a579cb15496f29 --- /dev/null +++ b/tools/generators/react-component/files/component/use__componentName__.ts__tmpl__ @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import type { <%= componentName %>Props, <%= componentName %>State } from './<%= componentName %>.types'; + +/** + * Create the state required to render <%= componentName %>. + * + * The returned state can be modified with hooks such as use<%= componentName %>Styles_unstable, + * before being passed to render<%= componentName %>_unstable. + * + * @param props - props from this instance of <%= componentName %> + * @param ref - reference to root HTMLElement of <%= componentName %> + */ +export const use<%= componentName %>_unstable = (props: <%= componentName %>Props, ref: React.Ref): <%= componentName %>State => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: getNativeElementProps('div', { + ref, + ...props, + }), + }; +}; diff --git a/tools/generators/react-component/files/component/use__componentName__Styles.styles.ts__tmpl__ b/tools/generators/react-component/files/component/use__componentName__Styles.styles.ts__tmpl__ new file mode 100644 index 00000000000000..2b504f46913556 --- /dev/null +++ b/tools/generators/react-component/files/component/use__componentName__Styles.styles.ts__tmpl__ @@ -0,0 +1,34 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { <%= componentName %>Slots, <%= componentName %>State } from './<%= componentName %>.types'; + + +export const <%= propertyName %>ClassNames:SlotClassNames<<%= componentName %>Slots> = { + root: 'fui-<%= componentName %>' + // TODO: add class names for all slots on <%= componentName %>Slots. + // Should be of the form `: 'fui-<%= componentName %>__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the <%= componentName %> slots based on the state + */ +export const use<%= componentName %>Styles_unstable = (state: <%= componentName %>State): <%= componentName %>State => { + const styles = useStyles(); + state.root.className = mergeClasses(<%= propertyName %>ClassNames.root, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/tools/generators/react-component/files/constants.ts__tmpl__ b/tools/generators/react-component/files/constants.ts__tmpl__ deleted file mode 100644 index d9913111a9dc76..00000000000000 --- a/tools/generators/react-component/files/constants.ts__tmpl__ +++ /dev/null @@ -1 +0,0 @@ -export const variable = "<%= name %>"; \ No newline at end of file diff --git a/tools/generators/react-component/files/story/__componentName__BestPractices.md__tmpl__ b/tools/generators/react-component/files/story/__componentName__BestPractices.md__tmpl__ new file mode 100644 index 00000000000000..08ff8ddeeb5f86 --- /dev/null +++ b/tools/generators/react-component/files/story/__componentName__BestPractices.md__tmpl__ @@ -0,0 +1,5 @@ +## Best practices + +### Do + +### Don't diff --git a/tools/generators/react-component/files/story/__componentName__Default.stories.tsx__tmpl__ b/tools/generators/react-component/files/story/__componentName__Default.stories.tsx__tmpl__ new file mode 100644 index 00000000000000..51df0d79f6b656 --- /dev/null +++ b/tools/generators/react-component/files/story/__componentName__Default.stories.tsx__tmpl__ @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { <%= componentName %>, <%= componentName %>Props } from '<%= npmPackageName %>'; + +export const Default = (props: Partial<<%= componentName %>Props>) => ( + <<%= componentName %> {...props} /> +); diff --git a/tools/generators/react-component/files/story/__componentName__Description.md__tmpl__ b/tools/generators/react-component/files/story/__componentName__Description.md__tmpl__ new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tools/generators/react-component/files/story/index.stories.tsx__tmpl__ b/tools/generators/react-component/files/story/index.stories.tsx__tmpl__ new file mode 100644 index 00000000000000..37cffb0041e53b --- /dev/null +++ b/tools/generators/react-component/files/story/index.stories.tsx__tmpl__ @@ -0,0 +1,18 @@ +import { <%= componentName %> } from '<%= npmPackageName %>'; + +import descriptionMd from './<%= componentName %>Description.md'; +import bestPracticesMd from './<%= componentName %>BestPractices.md'; + +export { Default } from './<%= componentName %>Default.stories'; + +export default { + title: 'Preview Components/<%= componentName %>', + component: <%= componentName %>, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + } + } + } +}; diff --git a/tools/generators/react-component/index.spec.ts b/tools/generators/react-component/index.spec.ts index 2da95c0dc7c3f1..1233ba9d8725d6 100644 --- a/tools/generators/react-component/index.spec.ts +++ b/tools/generators/react-component/index.spec.ts @@ -1,20 +1,197 @@ import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -import { Tree, readProjectConfiguration } from '@nrwl/devkit'; +import { Tree, addProjectConfiguration, writeJson, joinPathFragments } from '@nrwl/devkit'; import generator from './index'; -import { ReactComponentGeneratorSchema } from './schema'; describe('react-component generator', () => { - let appTree: Tree; - const options: ReactComponentGeneratorSchema = { name: 'test' }; + let tree: Tree; beforeEach(() => { - appTree = createTreeWithEmptyWorkspace(); + tree = createTreeWithEmptyWorkspace(); + createLibrary(tree, 'react-one'); }); - it('should run successfully', async () => { - await generator(appTree, options); - const config = readProjectConfiguration(appTree, 'test'); - expect(config).toBeDefined(); + describe(`assertions`, () => { + it(`should throw error if one wants to add component to non v9 package`, async () => { + createLibrary(tree, 'react-old', { tags: ['v8'], version: '8.123.4' }); + try { + await generator(tree, { project: '@proj/react-old', name: 'MyOne' }); + } catch (err) { + expect(err).toMatchInlineSnapshot( + `[Error: this generator works only with v9 packages. "@proj/react-old" is not!]`, + ); + } + }); + + it(`should throw error if component already exists`, async () => { + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + try { + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[Error: The component "MyOne" already exists]`); + } + }); + }); + + it('should create component', async () => { + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + const projectSourceRootPath = 'packages/react-components/react-one/src'; + const componentRootPath = `${projectSourceRootPath}/components/MyOne`; + + expect(tree.read(joinPathFragments(projectSourceRootPath, 'MyOne.ts'), 'utf-8')).toMatchInlineSnapshot(` + "export * from './components/MyOne/index'; + " + `); + + expect(tree.children(componentRootPath)).toMatchInlineSnapshot(` + Array [ + "MyOne.test.tsx", + "MyOne.tsx", + "MyOne.types.ts", + "index.ts", + "renderMyOne.tsx", + "useMyOne.ts", + "useMyOneStyles.styles.ts", + ] + `); + + expect(tree.read(joinPathFragments(componentRootPath, 'MyOne.tsx'), 'utf-8')).toMatchInlineSnapshot(` + "import * as React from 'react'; + import type { ForwardRefComponent } from '@fluentui/react-utilities'; + import { useMyOne_unstable } from './useMyOne'; + import { renderMyOne_unstable } from './renderMyOne'; + import { useMyOneStyles_unstable } from './useMyOneStyles.styles'; + import type { MyOneProps } from './MyOne.types'; + + /** + * MyOne component - TODO: add more docs + */ + export const MyOne: ForwardRefComponent = React.forwardRef( + (props, ref) => { + const state = useMyOne_unstable(props, ref); + + useMyOneStyles_unstable(state); + return renderMyOne_unstable(state); + } + ); + + MyOne.displayName = 'MyOne'; + " + `); + + expect(tree.read(joinPathFragments(componentRootPath, 'useMyOneStyles.styles.ts'), 'utf-8')).toMatchInlineSnapshot(` + "import { makeStyles, mergeClasses } from '@griffel/react'; + import type { SlotClassNames } from '@fluentui/react-utilities'; + import type { MyOneSlots, MyOneState } from './MyOne.types'; + + export const myOneClassNames: SlotClassNames = { + root: 'fui-MyOne', + // TODO: add class names for all slots on MyOneSlots. + // Should be of the form \`: 'fui-MyOne__\` + }; + + /** + * Styles for the root slot + */ + const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots + }); + + /** + * Apply styling to the MyOne slots based on the state + */ + export const useMyOneStyles_unstable = (state: MyOneState): MyOneState => { + const styles = useStyles(); + state.root.className = mergeClasses( + myOneClassNames.root, + styles.root, + state.root.className + ); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; + }; + " + `); + }); + + it(`should update barrel file`, async () => { + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + const projectSourceRootPath = 'packages/react-components/react-one/src'; + const barrelPath = joinPathFragments(projectSourceRootPath, 'index.ts'); + + expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` + "export * from './MyOne'; + " + `); + + await generator(tree, { project: '@proj/react-one', name: 'MyTwo' }); + + expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` + "export * from './MyOne'; + export * from './MyTwo'; + " + `); + }); + + it('should create component story', async () => { + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + const componentStoryRootPath = 'packages/react-components/react-one/stories/MyOne'; + expect(tree.children(componentStoryRootPath)).toMatchInlineSnapshot(` + Array [ + "MyOneBestPractices.md", + "MyOneDefault.stories.tsx", + "MyOneDescription.md", + "index.stories.tsx", + ] + `); + + expect(tree.read(joinPathFragments(componentStoryRootPath, 'index.stories.tsx'), 'utf-8')).toMatchInlineSnapshot(` + "import { MyOne } from '@proj/react-one'; + + import descriptionMd from './MyOneDescription.md'; + import bestPracticesMd from './MyOneBestPractices.md'; + + export { Default } from './MyOneDefault.stories'; + + export default { + title: 'Preview Components/MyOne', + component: MyOne, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\\\\n'), + }, + }, + }, + }; + " + `); }); }); + +function createLibrary(tree: Tree, name: string, options: Partial<{ version: string; tags: string[] }> = {}) { + const _options = { version: '9.0.0', tags: ['vNext'], ...options }; + const projectName = '@proj/' + name; + const root = `packages/react-components/${name}`; + const sourceRoot = `${root}/src`; + addProjectConfiguration(tree, projectName, { root, tags: _options.tags, sourceRoot }); + writeJson(tree, joinPathFragments(root, 'package.json'), { + name: projectName, + version: _options.version, + }); + tree.write(joinPathFragments(root, 'stories/.gitkeep'), ''); + tree.write(joinPathFragments(sourceRoot, 'index.ts'), 'export {}'); + + return tree; +} diff --git a/tools/generators/react-component/index.ts b/tools/generators/react-component/index.ts index 80375c30653f86..ac539da0ba43d1 100644 --- a/tools/generators/react-component/index.ts +++ b/tools/generators/react-component/index.ts @@ -1,50 +1,105 @@ import * as path from 'path'; -import { Tree, formatFiles, installPackagesTask, names, generateFiles } from '@nrwl/devkit'; -import { libraryGenerator } from '@nrwl/workspace/generators'; +import { Tree, formatFiles, names, generateFiles, joinPathFragments } from '@nrwl/devkit'; -import { getProjectConfig } from '../../utils'; +import { getProjectConfig, isPackageConverged } from '../../utils'; import { ReactComponentGeneratorSchema } from './schema'; +import { execSync } from 'child_process'; interface NormalizedSchema extends ReturnType {} export default async function (tree: Tree, schema: ReactComponentGeneratorSchema) { - await libraryGenerator(tree, { name: schema.name }); + const options = normalizeOptions(tree, schema); - const normalizedOptions = normalizeOptions(tree, schema); + assertComponent(tree, options); - addFiles(tree, normalizedOptions); + addFiles(tree, options); + + updateBarrel(tree, options); await formatFiles(tree); return () => { - installPackagesTask(tree); + const root = options.projectConfig.root; + const { npmPackageName, componentName } = options; + + execSync(`yarn workspace ${npmPackageName} generate-api`, { + cwd: root, + stdio: 'inherit', + }); + + execSync(`yarn workspace ${npmPackageName} test -t ${componentName}`, { + cwd: root, + stdio: 'inherit', + }); + + execSync(`yarn workspace ${npmPackageName} lint --fix`, { + cwd: root, + stdio: 'inherit', + }); }; } function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { - const project = getProjectConfig(tree, { packageName: options.name }); + const project = getProjectConfig(tree, { packageName: options.project }); + const nameCasings = names(options.name); return { ...options, ...project, - ...names(options.name), + ...nameCasings, + directory: 'components', + componentName: nameCasings.className, + npmPackageName: project.projectConfig.name as string, }; } -/** - * NOTE: remove this if your generator doesn't process any static/dynamic templates - */ function addFiles(tree: Tree, options: NormalizedSchema) { + const sourceRoot = options.projectConfig.sourceRoot as string; const templateOptions = { ...options, tmpl: '', }; + // component generateFiles( tree, - path.join(__dirname, 'files'), - path.join(options.projectConfig.root, options.name), + path.join(__dirname, 'files', 'component'), + path.join(sourceRoot, options.directory, options.componentName), templateOptions, ); + + tree.write( + joinPathFragments(sourceRoot, options.componentName + '.ts'), + `export * from './${options.directory}/${options.componentName}/index';`, + ); + + // story + generateFiles( + tree, + path.join(__dirname, 'files', 'story'), + path.join(options.paths.stories, options.componentName), + templateOptions, + ); +} + +function updateBarrel(tree: Tree, options: NormalizedSchema) { + const indexPath = joinPathFragments(options.paths.sourceRoot, 'index.ts'); + const content = tree.read(indexPath, 'utf-8') as string; + let newContent = content.replace('export {}', ''); + newContent = newContent + `export * from './${options.componentName}';` + '\n'; + + tree.write(indexPath, newContent); +} + +function assertComponent(tree: Tree, options: NormalizedSchema) { + const componentDirPath = joinPathFragments(options.projectConfig.sourceRoot as string, options.componentName + '.ts'); + + if (!isPackageConverged(tree, options.projectConfig)) { + throw new Error(`this generator works only with v9 packages. "${options.projectConfig.name}" is not!`); + } + if (tree.exists(componentDirPath)) { + throw new Error(`The component "${options.componentName}" already exists`); + } + return; } diff --git a/tools/generators/react-component/lib/.gitkeep b/tools/generators/react-component/lib/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/tools/generators/react-component/lib/utils.spec.ts b/tools/generators/react-component/lib/utils.spec.ts deleted file mode 100644 index 91e05a54a86f53..00000000000000 --- a/tools/generators/react-component/lib/utils.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { dummyHelper } from './utils'; - -describe(`utils`, () => { - it(`should behave...`, () => { - expect(dummyHelper()).toBe(undefined); - }); -}); diff --git a/tools/generators/react-component/lib/utils.ts b/tools/generators/react-component/lib/utils.ts deleted file mode 100644 index c340a27c29c8b0..00000000000000 --- a/tools/generators/react-component/lib/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -// use this module to define any kind of generic utilities that are used in more than 1 place within the generator implementation - -export function dummyHelper() { - return; -} diff --git a/tools/generators/react-component/schema.json b/tools/generators/react-component/schema.json index 51ab1b434a0f71..0844e973345cfc 100644 --- a/tools/generators/react-component/schema.json +++ b/tools/generators/react-component/schema.json @@ -6,12 +6,24 @@ "properties": { "name": { "type": "string", - "description": "Library name", + "description": "The name of the component.", "$default": { "$source": "argv", "index": 0 - } + }, + "x-prompt": "What name would you like to use for the component?", + "x-priority": "important" + }, + "project": { + "type": "string", + "description": "The name of the project.", + "alias": "p", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the project for this component?", + "x-priority": "important" } }, - "required": ["name"] + "required": ["name", "project"] } diff --git a/tools/generators/react-component/schema.ts b/tools/generators/react-component/schema.ts index ce03ad1a4e7d78..82fdd5cc0f8649 100644 --- a/tools/generators/react-component/schema.ts +++ b/tools/generators/react-component/schema.ts @@ -1,6 +1,16 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + export interface ReactComponentGeneratorSchema { /** - * Library name + * The name of the component. */ name: string; + /** + * The name of the project. + */ + project: string; } From 4591f03b6a9cdefb36bed6b65fd26ebe7fc2b6e5 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 18 Jul 2023 16:49:39 +0200 Subject: [PATCH 3/4] fixup! feat(tools): implement react-component generator --- tools/generators/react-component/README.md | 13 ++++++++++--- tools/generators/react-component/schema.json | 1 + tools/generators/react-component/schema.ts | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tools/generators/react-component/README.md b/tools/generators/react-component/README.md index c77872b081eac3..162cc609c3834a 100644 --- a/tools/generators/react-component/README.md +++ b/tools/generators/react-component/README.md @@ -1,6 +1,6 @@ # react-component -Workspace Generator ...TODO... +Workspace Generator for creating React Component within v9 package. @@ -8,13 +8,14 @@ Workspace Generator ...TODO... - [Examples](#examples) - [Options](#options) - [`name`](#name) + - [`project`](#project) ## Usage ```sh -yarn nx workspace-generator react-component ... +yarn nx workspace-generator react-component --help ``` Show what will be generated without writing to disk: @@ -35,4 +36,10 @@ yarn nx workspace-generator react-component Type: `string` -TODO... +The name of the component. + +#### `project` + +Type: `string` + +The name of the project where the component will be generated. diff --git a/tools/generators/react-component/schema.json b/tools/generators/react-component/schema.json index 0844e973345cfc..49c6bd3887161a 100644 --- a/tools/generators/react-component/schema.json +++ b/tools/generators/react-component/schema.json @@ -2,6 +2,7 @@ "$schema": "http://json-schema.org/schema", "cli": "nx", "id": "react-component", + "description": "Create React Component within v9 package", "type": "object", "properties": { "name": { diff --git a/tools/generators/react-component/schema.ts b/tools/generators/react-component/schema.ts index 82fdd5cc0f8649..b427270528df04 100644 --- a/tools/generators/react-component/schema.ts +++ b/tools/generators/react-component/schema.ts @@ -1,9 +1,13 @@ +/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, * and run json-schema-to-typescript to regenerate this file. */ +/** + * Create React Component within v9 package + */ export interface ReactComponentGeneratorSchema { /** * The name of the component. From a2752681ecdfcb08cb1705db1699f3d6c5694a1b Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 18 Jul 2023 17:07:27 +0200 Subject: [PATCH 4/4] fixup! fixup! feat(tools): implement react-component generator --- tools/generators/react-component/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/generators/react-component/index.ts b/tools/generators/react-component/index.ts index ac539da0ba43d1..4d5e00ed371d78 100644 --- a/tools/generators/react-component/index.ts +++ b/tools/generators/react-component/index.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { Tree, formatFiles, names, generateFiles, joinPathFragments } from '@nrwl/devkit'; +import { Tree, formatFiles, names, generateFiles, joinPathFragments, workspaceRoot } from '@nrwl/devkit'; import { getProjectConfig, isPackageConverged } from '../../utils'; @@ -20,10 +20,10 @@ export default async function (tree: Tree, schema: ReactComponentGeneratorSchema await formatFiles(tree); return () => { - const root = options.projectConfig.root; + const root = workspaceRoot; const { npmPackageName, componentName } = options; - execSync(`yarn workspace ${npmPackageName} generate-api`, { + execSync(`yarn lage generate-api --to ${npmPackageName}`, { cwd: root, stdio: 'inherit', });