From 513e2346b39b4610958a1847a8350008389d54a6 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Mon, 16 Oct 2023 16:04:01 -0400 Subject: [PATCH] feat(react): support generating components using the path as provided (#19608) --- .../packages/expo/generators/component.json | 21 ++- .../packages/next/generators/component.json | 25 ++-- .../packages/next/generators/page.json | 19 ++- .../react-native/generators/component.json | 21 ++- .../packages/react/generators/component.json | 36 +++-- .../packages/react/generators/hook.json | 25 ++-- .../packages/react/generators/redux.json | 14 +- packages/expo/generators.json | 2 +- .../src/generators/component/component.ts | 34 +++-- .../component/lib/normalize-options.ts | 60 ++++----- .../expo/src/generators/component/schema.d.ts | 10 ++ .../expo/src/generators/component/schema.json | 17 ++- packages/next/generators.json | 4 +- .../src/generators/component/component.ts | 25 +++- .../next/src/generators/component/schema.json | 26 ++-- packages/next/src/generators/page/page.ts | 73 +++++++++-- packages/next/src/generators/page/schema.d.ts | 7 + packages/next/src/generators/page/schema.json | 20 ++- packages/react-native/generators.json | 2 +- .../src/generators/component/component.ts | 35 +++-- .../component/lib/normalize-options.ts | 60 ++++----- .../src/generators/component/schema.d.ts | 10 ++ .../src/generators/component/schema.json | 17 ++- packages/react/docs/component-examples.md | 22 ++++ packages/react/generators.json | 6 +- .../src/generators/component/component.ts | 123 ++++-------------- .../{ => lib}/get-component-tests.ts | 2 +- .../component/lib/normalize-options.ts | 67 ++++++++++ .../generators/component/noramlized-schema.ts | 9 -- .../src/generators/component/schema.d.ts | 25 ++++ .../src/generators/component/schema.json | 37 +++--- packages/react/src/generators/hook/hook.ts | 91 +++++++------ .../react/src/generators/hook/schema.d.ts | 13 ++ .../react/src/generators/hook/schema.json | 21 ++- .../react/src/generators/library/library.ts | 3 +- .../__fileName__.slice.spec.ts__tmpl__ | 0 .../__fileName__.slice.ts__tmpl__ | 0 packages/react/src/generators/redux/redux.ts | 45 +++++-- .../react/src/generators/redux/schema.d.ts | 2 +- .../react/src/generators/redux/schema.json | 10 +- 40 files changed, 654 insertions(+), 385 deletions(-) create mode 100644 packages/react/docs/component-examples.md rename packages/react/src/generators/component/{ => lib}/get-component-tests.ts (80%) create mode 100644 packages/react/src/generators/component/lib/normalize-options.ts delete mode 100644 packages/react/src/generators/component/noramlized-schema.ts rename packages/react/src/generators/redux/files/{__directory__ => }/__fileName__.slice.spec.ts__tmpl__ (100%) rename packages/react/src/generators/redux/files/{__directory__ => }/__fileName__.slice.ts__tmpl__ (100%) diff --git a/docs/generated/packages/expo/generators/component.json b/docs/generated/packages/expo/generators/component.json index 46abac021bdec..7ba2a2c0279be 100644 --- a/docs/generated/packages/expo/generators/component.json +++ b/docs/generated/packages/expo/generators/component.json @@ -1,6 +1,6 @@ { "name": "component", - "factory": "./src/generators/component/component#expoComponentGenerator", + "factory": "./src/generators/component/component#expoComponentGeneratorInternal", "schema": { "cli": "nx", "$id": "NxExpoComponent", @@ -23,7 +23,7 @@ "description": "The name of the project.", "alias": "p", "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -49,13 +49,19 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -68,7 +74,8 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. App.tsx).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -77,12 +84,12 @@ "default": false } }, - "required": ["name", "project"], + "required": ["name"], "presets": [] }, "description": "Create a component", "aliases": ["c"], - "implementation": "/packages/expo/src/generators/component/component#expoComponentGenerator.ts", + "implementation": "/packages/expo/src/generators/component/component#expoComponentGeneratorInternal.ts", "hidden": false, "path": "/packages/expo/src/generators/component/schema.json", "type": "generator" diff --git a/docs/generated/packages/next/generators/component.json b/docs/generated/packages/next/generators/component.json index 8d21652a55ea4..95d9c1773ae0f 100644 --- a/docs/generated/packages/next/generators/component.json +++ b/docs/generated/packages/next/generators/component.json @@ -1,6 +1,6 @@ { "name": "component", - "factory": "./src/generators/component/component#componentGenerator", + "factory": "./src/generators/component/component#componentGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -14,8 +14,7 @@ "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" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -66,10 +65,15 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "export": { "type": "boolean", "description": "When true, the component is exported from the project index.ts (if it exists).", @@ -84,19 +88,22 @@ "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseFiles": { "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `App/App.tsx`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "skipFormat": { "description": "Skip formatting files.", @@ -105,12 +112,12 @@ "x-priority": "internal" } }, - "required": ["name", "project"], + "required": ["name"], "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Create an app component\" %}\n\n```shell\nnx g component my-cmp --project=my-app\n```\n\n{% /tab %}\n{% tab label=\"Create a component without its own folder\" %}\n\nRunning the following will create a component under `apps/my-app/components/my-cmp.tsx` rather than `apps/my-app/components/my-cmp/my-cmp.tsx`.\n\n```shell\nnx g component my-cmp --flat --project=my-app\n```\n\n{% /tab %}\n{% tab label=\"Create component in a custom directory\" %}\n\nRunning the following will create a component under `apps/my-app/foo/my-cmp.tsx` rather than `apps/my-app/my-cmp/my-cmp.tsx`.\n\n```shell\nnx g component my-cmp --directory=foo --flat --project=my-app\n```\n\n{% /tab %}\n{% /tabs %}\n", "presets": [] }, "description": "Create a component.", - "implementation": "/packages/next/src/generators/component/component#componentGenerator.ts", + "implementation": "/packages/next/src/generators/component/component#componentGeneratorInternal.ts", "aliases": [], "hidden": false, "path": "/packages/next/src/generators/component/schema.json", diff --git a/docs/generated/packages/next/generators/page.json b/docs/generated/packages/next/generators/page.json index 3dcbcfa323f90..9b3221776f5ba 100644 --- a/docs/generated/packages/next/generators/page.json +++ b/docs/generated/packages/next/generators/page.json @@ -1,6 +1,6 @@ { "name": "page", - "factory": "./src/generators/page/page#pageGenerator", + "factory": "./src/generators/page/page#pageGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -14,8 +14,7 @@ "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" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -26,10 +25,15 @@ }, "directory": { "type": "string", - "description": "Create the page under this directory (can be nested). Will be created under `pages/`.", + "description": "The directory at which to create the page file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", @@ -84,7 +88,8 @@ "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. It will be removed in Nx v18." }, "skipFormat": { "description": "Skip formatting files.", @@ -93,12 +98,12 @@ "x-priority": "internal" } }, - "required": ["name", "project"], + "required": ["name"], "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Create static page in an app\" %}\n\n```shell\nnx g page my-page --project=my-app\n```\n\n{% /tab %}\n{% tab label=\"Create dynamic page in an app\" %}\n\nThe following creates a page under `apps/my-app/pages/products/[id].tsx`.\n\n```shell\nnx g page \"[id]\" --project=my-app --directory=products\n```\n\n{% /tab %}\n\n{% /tabs %}\n", "presets": [] }, "description": "Create a page.", - "implementation": "/packages/next/src/generators/page/page#pageGenerator.ts", + "implementation": "/packages/next/src/generators/page/page#pageGeneratorInternal.ts", "aliases": [], "hidden": false, "path": "/packages/next/src/generators/page/schema.json", diff --git a/docs/generated/packages/react-native/generators/component.json b/docs/generated/packages/react-native/generators/component.json index 357a1b5c5b536..4f88072b33383 100644 --- a/docs/generated/packages/react-native/generators/component.json +++ b/docs/generated/packages/react-native/generators/component.json @@ -1,6 +1,6 @@ { "name": "component", - "factory": "./src/generators/component/component#reactNativeComponentGenerator", + "factory": "./src/generators/component/component#reactNativeComponentGeneratorInternal", "schema": { "cli": "nx", "$id": "NxReactNativeApplication", @@ -24,7 +24,7 @@ "description": "The name of the project.", "alias": "p", "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -44,13 +44,19 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -63,7 +69,8 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -72,12 +79,12 @@ "default": false } }, - "required": ["name", "project"], + "required": ["name"], "presets": [] }, "description": "Create a React Native component.", "aliases": ["c"], - "implementation": "/packages/react-native/src/generators/component/component#reactNativeComponentGenerator.ts", + "implementation": "/packages/react-native/src/generators/component/component#reactNativeComponentGeneratorInternal.ts", "hidden": false, "path": "/packages/react-native/src/generators/component/schema.json", "type": "generator" diff --git a/docs/generated/packages/react/generators/component.json b/docs/generated/packages/react/generators/component.json index 80d578a7ee9c6..5bf5ea4a2f18a 100644 --- a/docs/generated/packages/react/generators/component.json +++ b/docs/generated/packages/react/generators/component.json @@ -1,6 +1,6 @@ { "name": "component", - "factory": "./src/generators/component/component#componentGenerator", + "factory": "./src/generators/component/component#componentGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -8,24 +8,13 @@ "title": "Create a React Component", "description": "Create a React Component for Nx.", "type": "object", - "examples": [ - { - "command": "nx g component my-component --project=mylib", - "description": "Generate a component in the `mylib` library" - }, - { - "command": "nx g component my-component --project=mylib --classComponent", - "description": "Generate a class component in the `mylib` library" - } - ], "properties": { "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" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -81,14 +70,20 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -101,13 +96,15 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `App/App.tsx`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -140,12 +137,13 @@ "x-priority": "internal" } }, - "required": ["name", "project"], + "required": ["name"], + "examplesFile": "## Examples\n\n{% tabs %}\n{% tab label=\"Simple Component\" %}\n\nCreate a component named `my-component` under the `libs/ui` project:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component\n```\n\n{% /tab %}\n\n{% tab label=\"Standalone Component\" %}\n\nCreate a class component named `my-component` under the `libs/ui` project:\n\n```shell\nnx g @nx/react:component libs/ui/src/my-component --classComponent\n```\n\n{% /tab %}\n", "presets": [] }, "description": "Create a React component.", "aliases": ["c"], - "implementation": "/packages/react/src/generators/component/component#componentGenerator.ts", + "implementation": "/packages/react/src/generators/component/component#componentGeneratorInternal.ts", "hidden": false, "path": "/packages/react/src/generators/component/schema.json", "type": "generator" diff --git a/docs/generated/packages/react/generators/hook.json b/docs/generated/packages/react/generators/hook.json index 466b12241bb78..7852066ed8ec2 100644 --- a/docs/generated/packages/react/generators/hook.json +++ b/docs/generated/packages/react/generators/hook.json @@ -1,6 +1,6 @@ { "name": "hook", - "factory": "./src/generators/hook/hook#hookGenerator", + "factory": "./src/generators/hook/hook#hookGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -20,8 +20,7 @@ "description": "The name of the project.", "alias": "p", "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this hook?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -43,13 +42,19 @@ }, "directory": { "type": "string", - "description": "Create the hook under this directory (can be nested).", + "description": "The directory at which to create the hook file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create hook at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. It will be removed in Nx v18." }, "export": { "type": "boolean", @@ -62,21 +67,23 @@ "type": "boolean", "description": "Use pascal case hook file name (e.g. `useHook.ts`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `useHook/useHook.ts`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." } }, - "required": ["name", "project"], + "required": ["name"], "presets": [] }, "description": "Create a hook.", "aliases": ["c"], - "implementation": "/packages/react/src/generators/hook/hook#hookGenerator.ts", + "implementation": "/packages/react/src/generators/hook/hook#hookGeneratorInternal.ts", "hidden": false, "path": "/packages/react/src/generators/hook/schema.json", "type": "generator" diff --git a/docs/generated/packages/react/generators/redux.json b/docs/generated/packages/react/generators/redux.json index a870aa34e854e..bcd947e526468 100644 --- a/docs/generated/packages/react/generators/redux.json +++ b/docs/generated/packages/react/generators/redux.json @@ -1,6 +1,6 @@ { "name": "redux", - "factory": "./src/generators/redux/redux#reduxGenerator", + "factory": "./src/generators/redux/redux#reduxGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -20,16 +20,20 @@ "description": "The name of the project to add the slice to. If it is an application, then the store configuration will be updated too.", "alias": "p", "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this slice?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "directory": { "type": "string", "alias": "dir", "default": "", - "description": "The name of the folder used to contain/group the generated Redux files.", + "description": "The directory at which to create the Redux files. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "appProject": { "type": "string", "description": "The application project to add the slice to.", @@ -46,7 +50,7 @@ }, "description": "Create a Redux slice for a project.", "aliases": ["slice"], - "implementation": "/packages/react/src/generators/redux/redux#reduxGenerator.ts", + "implementation": "/packages/react/src/generators/redux/redux#reduxGeneratorInternal.ts", "hidden": false, "path": "/packages/react/src/generators/redux/schema.json", "type": "generator" diff --git a/packages/expo/generators.json b/packages/expo/generators.json index 63ea53981685c..3b595b5684f4d 100644 --- a/packages/expo/generators.json +++ b/packages/expo/generators.json @@ -24,7 +24,7 @@ "description": "Create a library" }, "component": { - "factory": "./src/generators/component/component#expoComponentGenerator", + "factory": "./src/generators/component/component#expoComponentGeneratorInternal", "schema": "./src/generators/component/schema.json", "description": "Create a component", "aliases": ["c"] diff --git a/packages/expo/src/generators/component/component.ts b/packages/expo/src/generators/component/component.ts index b4ee7fa7cb0a1..a9808a3fdd349 100644 --- a/packages/expo/src/generators/component/component.ts +++ b/packages/expo/src/generators/component/component.ts @@ -11,9 +11,19 @@ import { } from '@nx/devkit'; import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; import { addImport } from './lib/add-import'; -import { join } from 'path'; +import { dirname, join, parse, relative } from 'path'; export async function expoComponentGenerator(host: Tree, schema: Schema) { + return expoComponentGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function expoComponentGeneratorInternal( + host: Tree, + schema: Schema +) { const options = await normalizeOptions(host, schema); createComponentFiles(host, options); @@ -25,12 +35,7 @@ export async function expoComponentGenerator(host: Tree, schema: Schema) { } function createComponentFiles(host: Tree, options: NormalizedSchema) { - const componentDir = joinPathFragments( - options.projectSourceRoot, - options.directory - ); - - generateFiles(host, join(__dirname, './files'), componentDir, { + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, tmpl: '', }); @@ -69,16 +74,23 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { ts.ScriptTarget.Latest, true ); + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); const changes = applyChangesToString( indexSource, - addImport( - indexSourceFile, - `export * from './${options.directory}/${options.fileName}';` - ) + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) ); host.write(indexFilePath, changes); } } } +function getRelativeImportToFile(indexPath: string, filePath: string) { + const { name, dir } = parse(filePath); + const relativeDirToTarget = relative(dirname(indexPath), dir); + return `./${joinPathFragments(relativeDirToTarget, name)}`; +} + export default expoComponentGenerator; diff --git a/packages/expo/src/generators/component/lib/normalize-options.ts b/packages/expo/src/generators/component/lib/normalize-options.ts index 186af0ed55f64..bd89f17f3fc1b 100644 --- a/packages/expo/src/generators/component/lib/normalize-options.ts +++ b/packages/expo/src/generators/component/lib/normalize-options.ts @@ -1,16 +1,12 @@ -import { - getProjects, - joinPathFragments, - logger, - names, - Tree, -} from '@nx/devkit'; +import { getProjects, logger, names, Tree } from '@nx/devkit'; import { Schema } from '../schema'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; export interface NormalizedSchema extends Schema { projectSourceRoot: string; fileName: string; className: string; + filePath: string; } export async function normalizeOptions( @@ -19,20 +15,30 @@ export async function normalizeOptions( ): Promise { assertValidOptions(options); - const { className, fileName } = names(options.name); - const componentFileName = options.pascalCaseFiles ? className : fileName; - const project = getProjects(host).get(options.project); + const { + artifactName: name, + directory, + fileName, + filePath, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'component', + callingGenerator: '@nx/expo:component', + name: options.name, + directory: options.directory, + derivedDirectory: options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + pascalCaseFile: options.pascalCaseFiles, + }); - if (!project) { - logger.error( - `Cannot find the ${options.project} project. Please double check the project name.` - ); - throw new Error(); - } + const project = getProjects(host).get(projectName); - const { sourceRoot: projectSourceRoot, projectType } = project; + const { className } = names(name); - const directory = await getDirectory(host, options); + const { sourceRoot: projectSourceRoot, projectType } = project; if (options.export && projectType === 'application') { logger.warn( @@ -46,26 +52,12 @@ export async function normalizeOptions( ...options, directory, className, - fileName: componentFileName, + fileName, + filePath, projectSourceRoot, }; } -async function getDirectory(host: Tree, options: Schema) { - const fileName = names(options.name).fileName; - const workspace = getProjects(host); - let baseDir: string; - if (options.directory) { - baseDir = options.directory; - } else { - baseDir = - workspace.get(options.project).projectType === 'application' - ? 'app' - : 'lib'; - } - return options.flat ? baseDir : joinPathFragments(baseDir, fileName); -} - function assertValidOptions(options: Schema) { const slashes = ['/', '\\']; slashes.forEach((s) => { diff --git a/packages/expo/src/generators/component/schema.d.ts b/packages/expo/src/generators/component/schema.d.ts index 35a13eebfb1e6..9b7bd3cb0d74b 100644 --- a/packages/expo/src/generators/component/schema.d.ts +++ b/packages/expo/src/generators/component/schema.d.ts @@ -3,13 +3,23 @@ */ export interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; directory?: string; skipFormat: boolean; // default is false skipTests: boolean; // default is false + /** + * @deprecated Provide the name in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ export: boolean; // default is false pascalCaseFiles: boolean; // default is false classComponent: boolean; // default is false js: boolean; // default is false + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ flat: boolean; // default is false + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } diff --git a/packages/expo/src/generators/component/schema.json b/packages/expo/src/generators/component/schema.json index 6c61759cd7c1d..d1a220c028cac 100644 --- a/packages/expo/src/generators/component/schema.json +++ b/packages/expo/src/generators/component/schema.json @@ -22,7 +22,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -51,13 +51,19 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -70,7 +76,8 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. App.tsx).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -79,5 +86,5 @@ "default": false } }, - "required": ["name", "project"] + "required": ["name"] } diff --git a/packages/next/generators.json b/packages/next/generators.json index da4a3d1bf51d6..5f8123b5a376a 100644 --- a/packages/next/generators.json +++ b/packages/next/generators.json @@ -17,12 +17,12 @@ "description": "Create an application." }, "page": { - "factory": "./src/generators/page/page#pageGenerator", + "factory": "./src/generators/page/page#pageGeneratorInternal", "schema": "./src/generators/page/schema.json", "description": "Create a page." }, "component": { - "factory": "./src/generators/component/component#componentGenerator", + "factory": "./src/generators/component/component#componentGeneratorInternal", "schema": "./src/generators/component/schema.json", "description": "Create a component." }, diff --git a/packages/next/src/generators/component/component.ts b/packages/next/src/generators/component/component.ts index 1e357d943fe95..609a4a41ac21b 100644 --- a/packages/next/src/generators/component/component.ts +++ b/packages/next/src/generators/component/component.ts @@ -13,12 +13,25 @@ import { addStyleDependencies } from '../../utils/styles'; interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; style: SupportedStyles; directory?: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ flat?: boolean; + /** + * @deprecated Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseFiles?: boolean; + /** + * @deprecated Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseDirectory?: boolean; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; skipFormat?: boolean; } @@ -33,15 +46,23 @@ function getDirectory(host: Tree, options: Schema) { : undefined; } +export async function componentGenerator(host: Tree, schema: Schema) { + return componentGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + /* * This schematic is basically the React one, but for Next we need * extra dependencies for css, sass, less style options. */ -export async function componentGenerator(host: Tree, options: Schema) { +export async function componentGeneratorInternal(host: Tree, options: Schema) { const project = readProjectConfiguration(host, options.project); const componentInstall = await reactComponentGenerator(host, { ...options, - directory: getDirectory(host, options), + directory: options.directory, + derivedDirectory: getDirectory(host, options), classComponent: false, routing: false, skipFormat: true, diff --git a/packages/next/src/generators/component/schema.json b/packages/next/src/generators/component/schema.json index 33f6e17fda41d..635b1f7dfad12 100644 --- a/packages/next/src/generators/component/schema.json +++ b/packages/next/src/generators/component/schema.json @@ -13,8 +13,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -35,7 +34,10 @@ "message": "Which stylesheet format would you like to use?", "type": "list", "items": [ - { "value": "css", "label": "CSS" }, + { + "value": "css", + "label": "CSS" + }, { "value": "scss", "label": "SASS(.scss) [ http://sass-lang.com ]" @@ -71,10 +73,15 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "export": { "type": "boolean", "description": "When true, the component is exported from the project index.ts (if it exists).", @@ -89,19 +96,22 @@ "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseFiles": { "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `App/App.tsx`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "skipFormat": { "description": "Skip formatting files.", @@ -110,6 +120,6 @@ "x-priority": "internal" } }, - "required": ["name", "project"], + "required": ["name"], "examplesFile": "../../../docs/component-examples.md" } diff --git a/packages/next/src/generators/page/page.ts b/packages/next/src/generators/page/page.ts index 2683dad0d2211..5dc74b915b169 100644 --- a/packages/next/src/generators/page/page.ts +++ b/packages/next/src/generators/page/page.ts @@ -9,14 +9,22 @@ import { import { addStyleDependencies } from '../../utils/styles'; import { Schema } from './schema'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +export async function pageGenerator(host: Tree, schema: Schema) { + return pageGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} /* * This schematic is basically the React component one, but for Next we need * extra dependencies for css, sass, less style options, and make sure * it is under `pages` folder. */ -export async function pageGenerator(host: Tree, schema: Schema) { - const options = normalizeOptions(host, schema); +export async function pageGeneratorInternal(host: Tree, schema: Schema) { + const options = await normalizeOptions(host, schema); const componentTask = await reactComponentGenerator(host, { ...options, project: schema.project, @@ -29,9 +37,10 @@ export async function pageGenerator(host: Tree, schema: Schema) { skipFormat: true, }); + const project = readProjectConfiguration(host, options.projectName); const styledTask = addStyleDependencies(host, { style: options.style, - swc: !host.exists(joinPathFragments(options.project.root, '.babelrc')), + swc: !host.exists(joinPathFragments(project.root, '.babelrc')), }); if (!options.skipFormat) { @@ -41,17 +50,61 @@ export async function pageGenerator(host: Tree, schema: Schema) { return runTasksInSerial(componentTask, styledTask); } -function normalizeOptions(host: Tree, options: Schema) { - const project = readProjectConfiguration(host, options.project); +async function normalizeOptions(host: Tree, options: Schema) { + let isAppRouter: boolean; + + if (options.project) { + // Legacy behavior, detect app vs page router from specified project. + const project = readProjectConfiguration(host, options.project); + // app/ is a reserved folder in nextjs so it is safe to check it's existence + isAppRouter = host.exists(`${project.root}/app`); + } else { + // New behavior, detect app vs page router from the positional arg or directory path + const parts = + options.name.includes('/') || // mac, linux + options.name.includes('\\') // windows + ? options.name.split(/[\/\\]/) + : options.directory.split(/[\/\\]/); + if (parts.includes('pages')) { + isAppRouter = false; + } else if (parts.includes('app')) { + isAppRouter = true; + } else { + } + } + + const { + artifactName: name, + project: projectName, + filePath, + fileName, + nameAndDirectoryFormat, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'component', + callingGenerator: '@nx/react:component', + name: options.name, + fileName: isAppRouter ? 'page' : 'index', + directory: options.directory, + derivedDirectory: options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + }); - // app/ is a reserved folder in nextjs so it is safe to check it's existence - const isAppRouter = host.exists(`${project.root}/app`); const routerDirectory = isAppRouter ? 'app' : 'pages'; - const directory = options.directory + const derivedDirectory = options.directory ? `${routerDirectory}/${options.directory}` : `${routerDirectory}`; - const fileName = isAppRouter ? 'page' : !options.flat ? 'index' : undefined; - return { ...options, project, directory, fileName }; + + return { + ...options, + fileName, + projectName, + derivedDirectory, + filePath, + nameAndDirectoryFormat, + }; } export default pageGenerator; diff --git a/packages/next/src/generators/page/schema.d.ts b/packages/next/src/generators/page/schema.d.ts index 48306c10b9a0c..d2402956119ef 100644 --- a/packages/next/src/generators/page/schema.d.ts +++ b/packages/next/src/generators/page/schema.d.ts @@ -2,12 +2,19 @@ import { SupportedStyles } from '@nx/react'; export interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; style: SupportedStyles; directory?: string; fileName?: string; withTests?: boolean; js?: boolean; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ flat?: boolean; skipFormat?: boolean; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } diff --git a/packages/next/src/generators/page/schema.json b/packages/next/src/generators/page/schema.json index 04c4cf5e7b84a..a09b716c5f0c9 100644 --- a/packages/next/src/generators/page/schema.json +++ b/packages/next/src/generators/page/schema.json @@ -13,8 +13,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -28,10 +27,15 @@ }, "directory": { "type": "string", - "description": "Create the page under this directory (can be nested). Will be created under `pages/`.", + "description": "The directory at which to create the page file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", @@ -41,7 +45,10 @@ "message": "Which stylesheet format would you like to use?", "type": "list", "items": [ - { "value": "css", "label": "CSS" }, + { + "value": "css", + "label": "CSS" + }, { "value": "scss", "label": "SASS(.scss) [ http://sass-lang.com ]" @@ -89,7 +96,8 @@ "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. It will be removed in Nx v18." }, "skipFormat": { "description": "Skip formatting files.", @@ -98,6 +106,6 @@ "x-priority": "internal" } }, - "required": ["name", "project"], + "required": ["name"], "examplesFile": "../../../docs/page-examples.md" } diff --git a/packages/react-native/generators.json b/packages/react-native/generators.json index 975087282b859..6a905b6c0980b 100644 --- a/packages/react-native/generators.json +++ b/packages/react-native/generators.json @@ -24,7 +24,7 @@ "description": "Create a React Native library." }, "component": { - "factory": "./src/generators/component/component#reactNativeComponentGenerator", + "factory": "./src/generators/component/component#reactNativeComponentGeneratorInternal", "schema": "./src/generators/component/schema.json", "description": "Create a React Native component.", "aliases": ["c"] diff --git a/packages/react-native/src/generators/component/component.ts b/packages/react-native/src/generators/component/component.ts index 8bdedbe89f510..1df5ccd49abfb 100644 --- a/packages/react-native/src/generators/component/component.ts +++ b/packages/react-native/src/generators/component/component.ts @@ -11,11 +11,21 @@ import { import { NormalizedSchema, normalizeOptions } from './lib/normalize-options'; import { addImport } from './lib/add-import'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; -import { join } from 'path'; +import { dirname, join, parse, relative } from 'path'; export async function reactNativeComponentGenerator( host: Tree, schema: Schema +) { + return reactNativeComponentGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function reactNativeComponentGeneratorInternal( + host: Tree, + schema: Schema ) { const options = await normalizeOptions(host, schema); createComponentFiles(host, options); @@ -26,12 +36,7 @@ export async function reactNativeComponentGenerator( } function createComponentFiles(host: Tree, options: NormalizedSchema) { - const componentDir = joinPathFragments( - options.projectSourceRoot, - options.directory - ); - - generateFiles(host, join(__dirname, './files'), componentDir, { + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, tmpl: '', }); @@ -75,16 +80,24 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { tsModule.ScriptTarget.Latest, true ); + + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); const changes = applyChangesToString( indexSource, - addImport( - indexSourceFile, - `export * from './${options.directory}/${options.fileName}';` - ) + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) ); host.write(indexFilePath, changes); } } } +function getRelativeImportToFile(indexPath: string, filePath: string) { + const { name, dir } = parse(filePath); + const relativeDirToTarget = relative(dirname(indexPath), dir); + return `./${joinPathFragments(relativeDirToTarget, name)}`; +} + export default reactNativeComponentGenerator; diff --git a/packages/react-native/src/generators/component/lib/normalize-options.ts b/packages/react-native/src/generators/component/lib/normalize-options.ts index 186af0ed55f64..e4d16bf9214ae 100644 --- a/packages/react-native/src/generators/component/lib/normalize-options.ts +++ b/packages/react-native/src/generators/component/lib/normalize-options.ts @@ -1,16 +1,12 @@ -import { - getProjects, - joinPathFragments, - logger, - names, - Tree, -} from '@nx/devkit'; +import { getProjects, logger, names, Tree } from '@nx/devkit'; import { Schema } from '../schema'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; export interface NormalizedSchema extends Schema { projectSourceRoot: string; fileName: string; className: string; + filePath: string; } export async function normalizeOptions( @@ -19,20 +15,30 @@ export async function normalizeOptions( ): Promise { assertValidOptions(options); - const { className, fileName } = names(options.name); - const componentFileName = options.pascalCaseFiles ? className : fileName; - const project = getProjects(host).get(options.project); + const { + artifactName: name, + directory, + fileName, + filePath, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'component', + callingGenerator: '@nx/react-native:component', + name: options.name, + directory: options.directory, + derivedDirectory: options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + pascalCaseFile: options.pascalCaseFiles, + }); - if (!project) { - logger.error( - `Cannot find the ${options.project} project. Please double check the project name.` - ); - throw new Error(); - } + const project = getProjects(host).get(projectName); - const { sourceRoot: projectSourceRoot, projectType } = project; + const { className } = names(name); - const directory = await getDirectory(host, options); + const { sourceRoot: projectSourceRoot, projectType } = project; if (options.export && projectType === 'application') { logger.warn( @@ -46,26 +52,12 @@ export async function normalizeOptions( ...options, directory, className, - fileName: componentFileName, + fileName, + filePath, projectSourceRoot, }; } -async function getDirectory(host: Tree, options: Schema) { - const fileName = names(options.name).fileName; - const workspace = getProjects(host); - let baseDir: string; - if (options.directory) { - baseDir = options.directory; - } else { - baseDir = - workspace.get(options.project).projectType === 'application' - ? 'app' - : 'lib'; - } - return options.flat ? baseDir : joinPathFragments(baseDir, fileName); -} - function assertValidOptions(options: Schema) { const slashes = ['/', '\\']; slashes.forEach((s) => { diff --git a/packages/react-native/src/generators/component/schema.d.ts b/packages/react-native/src/generators/component/schema.d.ts index 9f795353418ea..857e5505c4ae0 100644 --- a/packages/react-native/src/generators/component/schema.d.ts +++ b/packages/react-native/src/generators/component/schema.d.ts @@ -3,12 +3,22 @@ */ export interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; skipTests?: boolean; directory?: string; export?: boolean; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseFiles?: boolean; classComponent?: boolean; js?: boolean; + /** + * @deprecated Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ flat?: boolean; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } diff --git a/packages/react-native/src/generators/component/schema.json b/packages/react-native/src/generators/component/schema.json index 11e5292ac6a3a..ac8a09eda4963 100644 --- a/packages/react-native/src/generators/component/schema.json +++ b/packages/react-native/src/generators/component/schema.json @@ -23,7 +23,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -46,13 +46,19 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -65,7 +71,8 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -74,5 +81,5 @@ "default": false } }, - "required": ["name", "project"] + "required": ["name"] } diff --git a/packages/react/docs/component-examples.md b/packages/react/docs/component-examples.md new file mode 100644 index 0000000000000..a8839c24eba44 --- /dev/null +++ b/packages/react/docs/component-examples.md @@ -0,0 +1,22 @@ +## Examples + +{% tabs %} +{% tab label="Simple Component" %} + +Create a component named `my-component` under the `libs/ui` project: + +```shell +nx g @nx/react:component libs/ui/src/my-component +``` + +{% /tab %} + +{% tab label="Standalone Component" %} + +Create a class component named `my-component` under the `libs/ui` project: + +```shell +nx g @nx/react:component libs/ui/src/my-component --classComponent +``` + +{% /tab %} diff --git a/packages/react/generators.json b/packages/react/generators.json index a1db78e9c8ac5..7ae0134ecf4d4 100644 --- a/packages/react/generators.json +++ b/packages/react/generators.json @@ -25,13 +25,13 @@ "description": "Create a React library." }, "component": { - "factory": "./src/generators/component/component#componentGenerator", + "factory": "./src/generators/component/component#componentGeneratorInternal", "schema": "./src/generators/component/schema.json", "description": "Create a React component.", "aliases": ["c"] }, "redux": { - "factory": "./src/generators/redux/redux#reduxGenerator", + "factory": "./src/generators/redux/redux#reduxGeneratorInternal", "schema": "./src/generators/redux/schema.json", "description": "Create a Redux slice for a project.", "aliases": ["slice"] @@ -61,7 +61,7 @@ "hidden": false }, "hook": { - "factory": "./src/generators/hook/hook#hookGenerator", + "factory": "./src/generators/hook/hook#hookGeneratorInternal", "schema": "./src/generators/hook/schema.json", "description": "Create a hook.", "aliases": ["c"] diff --git a/packages/react/src/generators/component/component.ts b/packages/react/src/generators/component/component.ts index 8c4e55ef99eb9..2c8f3797b06ec 100644 --- a/packages/react/src/generators/component/component.ts +++ b/packages/react/src/generators/component/component.ts @@ -6,25 +6,29 @@ import { GeneratorCallback, getProjects, joinPathFragments, - logger, - names, runTasksInSerial, toJS, Tree, } from '@nx/devkit'; +import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; +import { dirname, join, parse, relative } from 'path'; import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies'; -import { assertValidStyle } from '../../utils/assertion'; import { addImport } from '../../utils/ast-utils'; import { getInSourceVitestTestsTemplate } from '../../utils/get-in-source-vitest-tests-template'; import { reactRouterDomVersion } from '../../utils/versions'; -import { getComponentTests } from './get-component-tests'; -import { NormalizedSchema } from './noramlized-schema'; -import { Schema } from './schema'; -import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; -import { join } from 'path'; +import { getComponentTests } from './lib/get-component-tests'; +import { NormalizedSchema, Schema } from './schema'; +import { normalizeOptions } from './lib/normalize-options'; export async function componentGenerator(host: Tree, schema: Schema) { + return componentGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function componentGeneratorInternal(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); createComponentFiles(host, options); @@ -52,13 +56,8 @@ export async function componentGenerator(host: Tree, schema: Schema) { } function createComponentFiles(host: Tree, options: NormalizedSchema) { - const componentDir = joinPathFragments( - options.projectSourceRoot, - options.directory - ); - const componentTests = getComponentTests(options); - generateFiles(host, join(__dirname, './files'), componentDir, { + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, componentTests, inSourceVitestTests: getInSourceVitestTestsTemplate(componentTests), @@ -125,101 +124,23 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { tsModule.ScriptTarget.Latest, true ); + const relativePathFromIndex = getRelativeImportToFile( + indexFilePath, + options.filePath + ); const changes = applyChangesToString( indexSource, - addImport( - indexSourceFile, - `export * from './${options.directory}/${options.fileName}';` - ) + addImport(indexSourceFile, `export * from '${relativePathFromIndex}';`) ); host.write(indexFilePath, changes); } } } -async function normalizeOptions( - host: Tree, - options: Schema -): Promise { - assertValidOptions(options); - - const { className, fileName } = names(options.name); - const componentFileName = - options.fileName ?? (options.pascalCaseFiles ? className : fileName); - const project = getProjects(host).get(options.project); - - if (!project) { - logger.error( - `Cannot find the ${options.project} project. Please double check the project name.` - ); - throw new Error(); - } - - const { sourceRoot: projectSourceRoot, projectType } = project; - - const directory = await getDirectory(host, options); - - const styledModule = /^(css|scss|less|none)$/.test(options.style) - ? null - : options.style; - - if (options.export && projectType === 'application') { - logger.warn( - `The "--export" option should not be used with applications and will do nothing.` - ); - } - - options.classComponent = options.classComponent ?? false; - options.routing = options.routing ?? false; - options.globalCss = options.globalCss ?? false; - options.inSourceTests = options.inSourceTests ?? false; - - return { - ...options, - directory, - styledModule, - hasStyles: options.style !== 'none', - className, - fileName: componentFileName, - projectSourceRoot, - }; -} - -async function getDirectory(host: Tree, options: Schema) { - const genNames = names(options.name); - const fileName = - options.pascalCaseDirectory === true - ? genNames.className - : genNames.fileName; - const workspace = getProjects(host); - let baseDir: string; - if (options.directory) { - baseDir = options.directory; - } else { - baseDir = - workspace.get(options.project).projectType === 'application' - ? 'app' - : 'lib'; - } - return options.flat ? baseDir : joinPathFragments(baseDir, fileName); -} - -function assertValidOptions(options: Schema) { - assertValidStyle(options.style); - - const slashes = ['/', '\\']; - slashes.forEach((s) => { - if (options.name.indexOf(s) !== -1) { - const [name, ...rest] = options.name.split(s).reverse(); - let suggestion = rest.map((x) => x.toLowerCase()).join(s); - if (options.directory) { - suggestion = `${options.directory}${s}${suggestion}`; - } - throw new Error( - `Found "${s}" in the component name. Did you mean to use the --directory option (e.g. \`nx g c ${name} --directory ${suggestion}\`)?` - ); - } - }); +function getRelativeImportToFile(indexPath: string, filePath: string) { + const { name, dir } = parse(filePath); + const relativeDirToTarget = relative(dirname(indexPath), dir); + return `./${joinPathFragments(relativeDirToTarget, name)}`; } export default componentGenerator; diff --git a/packages/react/src/generators/component/get-component-tests.ts b/packages/react/src/generators/component/lib/get-component-tests.ts similarity index 80% rename from packages/react/src/generators/component/get-component-tests.ts rename to packages/react/src/generators/component/lib/get-component-tests.ts index ae6516605eeb1..b664852985993 100644 --- a/packages/react/src/generators/component/get-component-tests.ts +++ b/packages/react/src/generators/component/lib/get-component-tests.ts @@ -1,4 +1,4 @@ -import { NormalizedSchema } from './noramlized-schema'; +import { NormalizedSchema } from '../schema'; export function getComponentTests(schema: NormalizedSchema) { return ` diff --git a/packages/react/src/generators/component/lib/normalize-options.ts b/packages/react/src/generators/component/lib/normalize-options.ts new file mode 100644 index 0000000000000..67ee7e279f416 --- /dev/null +++ b/packages/react/src/generators/component/lib/normalize-options.ts @@ -0,0 +1,67 @@ +import { logger, names, readProjectConfiguration, Tree } from '@nx/devkit'; + +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; + +import { assertValidStyle } from '../../../utils/assertion'; +import { NormalizedSchema, Schema } from '../schema'; + +export async function normalizeOptions( + tree: Tree, + options: Schema +): Promise { + assertValidStyle(options.style); + + const { + artifactName: name, + directory, + fileName, + filePath, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(tree, { + artifactType: 'component', + callingGenerator: '@nx/react:component', + name: options.name, + directory: options.directory, + derivedDirectory: options.derivedDirectory ?? options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + fileName: options.fileName, + pascalCaseFile: options.pascalCaseFiles, + pascalCaseDirectory: options.pascalCaseDirectory, + }); + + const project = readProjectConfiguration(tree, projectName); + + const { className } = names(name); + + const { sourceRoot: projectSourceRoot, projectType } = project; + + const styledModule = /^(css|scss|less|none)$/.test(options.style) + ? null + : options.style; + + if (options.export && projectType === 'application') { + logger.warn( + `The "--export" option should not be used with applications and will do nothing.` + ); + } + + options.classComponent = options.classComponent ?? false; + options.routing = options.routing ?? false; + options.globalCss = options.globalCss ?? false; + options.inSourceTests = options.inSourceTests ?? false; + + return { + ...options, + project: projectName, + directory, + styledModule, + hasStyles: options.style !== 'none', + className, + fileName, + filePath, + projectSourceRoot, + }; +} diff --git a/packages/react/src/generators/component/noramlized-schema.ts b/packages/react/src/generators/component/noramlized-schema.ts deleted file mode 100644 index 138a54b2fc756..0000000000000 --- a/packages/react/src/generators/component/noramlized-schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Schema } from './schema'; - -export interface NormalizedSchema extends Schema { - projectSourceRoot: string; - fileName: string; - className: string; - styledModule: null | string; - hasStyles: boolean; -} diff --git a/packages/react/src/generators/component/schema.d.ts b/packages/react/src/generators/component/schema.d.ts index eebf19612e7d5..6ed9fb9c33d3e 100644 --- a/packages/react/src/generators/component/schema.d.ts +++ b/packages/react/src/generators/component/schema.d.ts @@ -2,19 +2,44 @@ import { SupportedStyles } from '../../../typings/style'; export interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; style: SupportedStyles; skipTests?: boolean; directory?: string; export?: boolean; + /** + * @deprecated Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseFiles?: boolean; + /** + * @deprecated Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseDirectory?: boolean; classComponent?: boolean; routing?: boolean; js?: boolean; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ flat?: boolean; globalCss?: boolean; fileName?: string; inSourceTests?: boolean; skipFormat?: boolean; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; + // Used by other wrapping generators to preserve previous behavior + // e.g. @nx/next:component + derivedDirectory?: string; +} + +export interface NormalizedSchema extends Schema { + projectSourceRoot: string; + fileName: string; + filePath: string; + className: string; + styledModule: null | string; + hasStyles: boolean; } diff --git a/packages/react/src/generators/component/schema.json b/packages/react/src/generators/component/schema.json index a8c25d316c198..985f215756ce9 100644 --- a/packages/react/src/generators/component/schema.json +++ b/packages/react/src/generators/component/schema.json @@ -5,16 +5,6 @@ "title": "Create a React Component", "description": "Create a React Component for Nx.", "type": "object", - "examples": [ - { - "command": "nx g component my-component --project=mylib", - "description": "Generate a component in the `mylib` library" - }, - { - "command": "nx g component my-component --project=mylib --classComponent", - "description": "Generate a class component in the `mylib` library" - } - ], "properties": { "project": { "type": "string", @@ -23,8 +13,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this component?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -45,7 +34,10 @@ "message": "Which stylesheet format would you like to use?", "type": "list", "items": [ - { "value": "css", "label": "CSS" }, + { + "value": "css", + "label": "CSS" + }, { "value": "scss", "label": "SASS(.scss) [ http://sass-lang.com ]" @@ -86,14 +78,20 @@ }, "directory": { "type": "string", - "description": "Create the component under this directory (can be nested).", + "description": "The directory at which to create the component file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "alias": "dir", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create component at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18." }, "export": { "type": "boolean", @@ -106,13 +104,15 @@ "type": "boolean", "description": "Use pascal case component file name (e.g. `App.tsx`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `App/App.tsx`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "classComponent": { "type": "boolean", @@ -145,5 +145,6 @@ "x-priority": "internal" } }, - "required": ["name", "project"] + "required": ["name"], + "examplesFile": "../../../docs/component-examples.md" } diff --git a/packages/react/src/generators/hook/hook.ts b/packages/react/src/generators/hook/hook.ts index 5ffad03848cf6..cf4ed3ff0c174 100644 --- a/packages/react/src/generators/hook/hook.ts +++ b/packages/react/src/generators/hook/hook.ts @@ -15,6 +15,7 @@ import { Schema } from './schema'; import { addImport } from '../../utils/ast-utils'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { join } from 'path'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; interface NormalizedSchema extends Schema { projectSourceRoot: string; @@ -24,6 +25,13 @@ interface NormalizedSchema extends Schema { } export async function hookGenerator(host: Tree, schema: Schema) { + return hookGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function hookGeneratorInternal(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); createFiles(host, options); @@ -33,12 +41,7 @@ export async function hookGenerator(host: Tree, schema: Schema) { } function createFiles(host: Tree, options: NormalizedSchema) { - const hookDir = joinPathFragments( - options.projectSourceRoot, - options.directory - ); - - generateFiles(host, join(__dirname, './files'), hookDir, { + generateFiles(host, join(__dirname, './files'), options.directory, { ...options, tmpl: '', }); @@ -100,7 +103,27 @@ async function normalizeOptions( ): Promise { assertValidOptions(options); - let base = options.name; + const { + artifactName: name, + directory: _directory, + fileName: _fileName, + nameAndDirectoryFormat, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'hook', + callingGenerator: '@nx/react:hook', + name: options.name, + directory: options.directory, + derivedDirectory: options.directory, + flat: options.flat, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + pascalCaseFile: options.pascalCaseFiles, + pascalCaseDirectory: options.pascalCaseDirectory, + }); + + let base = _fileName; if (base.startsWith('use-')) { base = base.substring(4); } else if (base.startsWith('use')) { @@ -108,30 +131,41 @@ async function normalizeOptions( } const { className, fileName } = names(base); - const hookFilename = options.pascalCaseFiles - ? 'use'.concat(className) - : 'use-'.concat(fileName); + // If using `as-provided` file and directory, then don't normalize. + // Otherwise, support legacy behavior of prefixing filename with `use-`. + const hookFilename = + nameAndDirectoryFormat === 'as-provided' + ? fileName + : options.pascalCaseFiles + ? 'use'.concat(className) + : 'use-'.concat(fileName); const hookName = 'use'.concat(className); const hookTypeName = 'Use'.concat(className); const project = getProjects(host).get(options.project); - if (!project) { - logger.error( - `Cannot find the ${options.project} project. Please double check the project name.` - ); - throw new Error(); - } - const { sourceRoot: projectSourceRoot, projectType } = project; - const directory = await getDirectory(host, options, base); - if (options.export && projectType === 'application') { logger.warn( `The "--export" option should not be used with applications and will do nothing.` ); } + // Support legacy behavior of derived directory to prefix with `use-`. + let directory = _directory; + if (nameAndDirectoryFormat === 'derived') { + const parts = directory.split('/'); + parts.pop(); + if (!options.flat) { + parts.push( + options.pascalCaseDirectory + ? 'use'.concat(className) + : 'use-'.concat(fileName) + ); + } + directory = parts.join('/'); + } + return { ...options, directory, @@ -142,25 +176,6 @@ async function normalizeOptions( }; } -async function getDirectory(host: Tree, options: Schema, baseHookName) { - const { className, fileName } = names(baseHookName); - const hookFileName = - options.pascalCaseDirectory === true - ? 'use'.concat(className) - : 'use-'.concat(fileName); - const workspace = getProjects(host); - let baseDir: string; - if (options.directory) { - baseDir = options.directory; - } else { - baseDir = - workspace.get(options.project).projectType === 'application' - ? 'app' - : 'lib'; - } - return options.flat ? baseDir : joinPathFragments(baseDir, hookFileName); -} - function assertValidOptions(options: Schema) { const slashes = ['/', '\\']; slashes.forEach((s) => { diff --git a/packages/react/src/generators/hook/schema.d.ts b/packages/react/src/generators/hook/schema.d.ts index 06c215d68a794..07bda20c4ee36 100644 --- a/packages/react/src/generators/hook/schema.d.ts +++ b/packages/react/src/generators/hook/schema.d.ts @@ -1,11 +1,24 @@ export interface Schema { name: string; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18. + */ project: string; skipTests?: boolean; directory?: string; export?: boolean; + /** + * @deprecated Provide the `name` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseFiles?: boolean; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ pascalCaseDirectory?: boolean; + /** + * @deprecated Provide the `directory` option instead and use the `as-provided` format. This option will be removed in Nx v18. + */ flat?: boolean; js?: boolean; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } diff --git a/packages/react/src/generators/hook/schema.json b/packages/react/src/generators/hook/schema.json index 62b3921e9e652..672f39dff3a48 100644 --- a/packages/react/src/generators/hook/schema.json +++ b/packages/react/src/generators/hook/schema.json @@ -19,8 +19,7 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this hook?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "name": { "type": "string", @@ -45,13 +44,19 @@ }, "directory": { "type": "string", - "description": "Create the hook under this directory (can be nested).", + "description": "The directory at which to create the hook file. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "flat": { "type": "boolean", "description": "Create hook at the source root rather than its own directory.", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. It will be removed in Nx v18." }, "export": { "type": "boolean", @@ -64,14 +69,16 @@ "type": "boolean", "description": "Use pascal case hook file name (e.g. `useHook.ts`).", "alias": "P", - "default": false + "default": false, + "x-deprecated": "Provide the `name` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." }, "pascalCaseDirectory": { "type": "boolean", "description": "Use pascal case directory name (e.g. `useHook/useHook.ts`).", "alias": "R", - "default": false + "default": false, + "x-deprecated": "Provide the `directory` in pascal-case and use the `as-provided` format. This option will be removed in Nx v18." } }, - "required": ["name", "project"] + "required": ["name"] } diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 71b90166f0cbc..5e7697e4080b8 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -172,7 +172,8 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { if (options.component) { const componentTask = await componentGenerator(host, { - name: options.fileName, + nameAndDirectoryFormat: 'as-provided', + name: joinPathFragments(options.projectRoot, 'src/lib', options.fileName), project: options.name, flat: true, style: options.style, diff --git a/packages/react/src/generators/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ b/packages/react/src/generators/redux/files/__fileName__.slice.spec.ts__tmpl__ similarity index 100% rename from packages/react/src/generators/redux/files/__directory__/__fileName__.slice.spec.ts__tmpl__ rename to packages/react/src/generators/redux/files/__fileName__.slice.spec.ts__tmpl__ diff --git a/packages/react/src/generators/redux/files/__directory__/__fileName__.slice.ts__tmpl__ b/packages/react/src/generators/redux/files/__fileName__.slice.ts__tmpl__ similarity index 100% rename from packages/react/src/generators/redux/files/__directory__/__fileName__.slice.ts__tmpl__ rename to packages/react/src/generators/redux/files/__fileName__.slice.ts__tmpl__ diff --git a/packages/react/src/generators/redux/redux.ts b/packages/react/src/generators/redux/redux.ts index 9328bf384d053..318ae3f7ed47d 100644 --- a/packages/react/src/generators/redux/redux.ts +++ b/packages/react/src/generators/redux/redux.ts @@ -20,11 +20,19 @@ import { } from '@nx/devkit'; import { getRootTsConfigPathInTree } from '@nx/js'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; let tsModule: typeof import('typescript'); export async function reduxGenerator(host: Tree, schema: Schema) { - const options = normalizeOptions(host, schema); + return reduxGeneratorInternal(host, { + nameAndDirectoryFormat: 'derived', + ...schema, + }); +} + +export async function reduxGeneratorInternal(host: Tree, schema: Schema) { + const options = await normalizeOptions(host, schema); generateReduxFiles(host, options); addExportsToBarrel(host, options); const installTask = addReduxPackageDependencies(host); @@ -40,7 +48,7 @@ function generateReduxFiles(host: Tree, options: NormalizedSchema) { generateFiles( host, joinPathFragments(__dirname, './files'), - options.filesPath, + options.directory, { ...options, tmpl: '', @@ -141,12 +149,32 @@ function updateReducerConfiguration(host: Tree, options: NormalizedSchema) { host.write(options.appMainFilePath, changes); } -function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { +async function normalizeOptions( + host: Tree, + options: Schema +): Promise { + const { + artifactName: name, + directory, + fileName, + project: projectName, + } = await determineArtifactNameAndDirectoryOptions(host, { + artifactType: 'slice', + callingGenerator: '@nx/react:redux', + name: options.name, + directory: options.directory, + derivedDirectory: options.directory, + flat: true, + nameAndDirectoryFormat: options.nameAndDirectoryFormat, + project: options.project, + fileExtension: 'tsx', + }); + let appProjectSourcePath: string; let appMainFilePath: string; - const extraNames = names(options.name); + const extraNames = names(name); const projects = getProjects(host); - const project = projects.get(options.project); + const project = projects.get(projectName); const { sourceRoot, projectType } = project; const tsConfigJson = readJson(host, getRootTsConfigPathInTree(host)); @@ -188,17 +216,14 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { return { ...options, ...extraNames, + fileName, constantName: names(options.name).constantName.toUpperCase(), - directory: names(options.directory ?? '').fileName, + directory, projectType, projectSourcePath: sourceRoot, projectModulePath: modulePath, appProjectSourcePath, appMainFilePath, - filesPath: joinPathFragments( - sourceRoot, - projectType === 'application' ? 'app' : 'lib' - ), }; } diff --git a/packages/react/src/generators/redux/schema.d.ts b/packages/react/src/generators/redux/schema.d.ts index 4d20fcddece6a..db1e5049080fb 100644 --- a/packages/react/src/generators/redux/schema.d.ts +++ b/packages/react/src/generators/redux/schema.d.ts @@ -4,6 +4,7 @@ export interface Schema { directory?: string; appProject?: string; js?: string; + nameAndDirectoryFormat?: 'as-provided' | 'derived'; } interface NormalizedSchema extends Schema { @@ -12,7 +13,6 @@ interface NormalizedSchema extends Schema { projectModulePath: string; appProjectSourcePath: string; appMainFilePath: string; - filesPath: string; className: string; constantName: string; propertyName: string; diff --git a/packages/react/src/generators/redux/schema.json b/packages/react/src/generators/redux/schema.json index fdb0af8afa8ec..4ca676293773d 100644 --- a/packages/react/src/generators/redux/schema.json +++ b/packages/react/src/generators/redux/schema.json @@ -22,16 +22,20 @@ "$default": { "$source": "projectName" }, - "x-prompt": "What is the name of the project for this slice?", - "x-priority": "important" + "x-deprecated": "Provide the `directory` option instead and use the `as-provided` format. The project will be determined from the directory provided. It will be removed in Nx v18." }, "directory": { "type": "string", "alias": "dir", "default": "", - "description": "The name of the folder used to contain/group the generated Redux files.", + "description": "The directory at which to create the Redux files. When `--nameAndDirectoryFormat=as-provided`, it will be relative to the current working directory. Otherwise, it will be relative to the project root.", "x-priority": "important" }, + "nameAndDirectoryFormat": { + "description": "Whether to generate the component in the directory as provided, relative to the current working directory and ignoring the project (`as-provided`) or generate it using the project and directory relative to the workspace root (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "appProject": { "type": "string", "description": "The application project to add the slice to.",