From a2f761352b11e72cf2aaa024abf30625b7b81aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 22 Aug 2023 15:33:55 +0100 Subject: [PATCH] feat(react-native): use helper to determine project name and root in project generators (#18734) --- .../react-native/generators/application.json | 12 ++++-- .../react-native/generators/library.json | 11 +++-- e2e/react-native/src/react-native.test.ts | 38 ++++++++++++++++ packages/react-native/generators.json | 4 +- .../src/generators/application/application.ts | 12 +++++- .../generators/application/lib/add-detox.ts | 5 ++- .../generators/application/lib/add-project.ts | 2 +- .../application/lib/nomalize-options.spec.ts | 25 ++++++----- .../application/lib/normalize-options.ts | 43 +++++++++---------- .../src/generators/application/schema.d.ts | 4 +- .../src/generators/application/schema.json | 8 +++- .../library/lib/normalize-options.ts | 39 ++++++++--------- .../src/generators/library/library.ts | 16 +++++-- .../src/generators/library/schema.d.ts | 4 +- .../src/generators/library/schema.json | 7 ++- 15 files changed, 157 insertions(+), 73 deletions(-) diff --git a/docs/generated/packages/react-native/generators/application.json b/docs/generated/packages/react-native/generators/application.json index 3294493dfa2dd..e98472482f473 100644 --- a/docs/generated/packages/react-native/generators/application.json +++ b/docs/generated/packages/react-native/generators/application.json @@ -1,6 +1,6 @@ { "name": "application", - "factory": "./src/generators/application/application#reactNativeApplicationGenerator", + "factory": "./src/generators/application/application#reactNativeApplicationGeneratorInternal", "schema": { "cli": "nx", "$id": "NxReactNativeApplication", @@ -23,7 +23,8 @@ "description": "The name of the application.", "type": "string", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What name would you like to use for the application?" + "x-prompt": "What name would you like to use for the application?", + "pattern": "^[a-zA-Z][^:]*$" }, "displayName": { "description": "The display name to show in the application. Defaults to name.", @@ -33,6 +34,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", @@ -91,7 +97,7 @@ "aliases": ["app"], "x-type": "application", "description": "Create a React Native application.", - "implementation": "/packages/react-native/src/generators/application/application#reactNativeApplicationGenerator.ts", + "implementation": "/packages/react-native/src/generators/application/application#reactNativeApplicationGeneratorInternal.ts", "hidden": false, "path": "/packages/react-native/src/generators/application/schema.json", "type": "generator" diff --git a/docs/generated/packages/react-native/generators/library.json b/docs/generated/packages/react-native/generators/library.json index 7e6bb92c4b78b..7865034c3bd37 100644 --- a/docs/generated/packages/react-native/generators/library.json +++ b/docs/generated/packages/react-native/generators/library.json @@ -1,6 +1,6 @@ { "name": "library", - "factory": "./src/generators/library/library#reactNativeLibraryGenerator", + "factory": "./src/generators/library/library#reactNativeLibraryGeneratorInternal", "schema": { "cli": "nx", "$id": "NxReactNativeLibrary", @@ -20,7 +20,7 @@ "description": "Library name.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the library?", - "pattern": "^[a-zA-Z].*$" + "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$" }, "directory": { "type": "string", @@ -28,6 +28,11 @@ "alias": "dir", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "linter": { "description": "The tool to use for running lint checks.", "type": "string", @@ -104,7 +109,7 @@ "aliases": ["lib"], "x-type": "library", "description": "Create a React Native library.", - "implementation": "/packages/react-native/src/generators/library/library#reactNativeLibraryGenerator.ts", + "implementation": "/packages/react-native/src/generators/library/library#reactNativeLibraryGeneratorInternal.ts", "hidden": false, "path": "/packages/react-native/src/generators/library/schema.json", "type": "generator" diff --git a/e2e/react-native/src/react-native.test.ts b/e2e/react-native/src/react-native.test.ts index 67286404b7c42..a6166ab06c13b 100644 --- a/e2e/react-native/src/react-native.test.ts +++ b/e2e/react-native/src/react-native.test.ts @@ -222,4 +222,42 @@ describe('react native', () => { ); }).not.toThrow(); }); + + it('should support generating projects with the new name and root format', () => { + const appName = uniq('app1'); + const libName = uniq('@my-org/lib1'); + + runCLI( + `generate @nx/react-native:application ${appName} --project-name-and-root-format=as-provided --no-interactive` + ); + + // check files are generated without the layout directory ("apps/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${appName}/src/app/App.tsx`); + // check tests pass + const appTestResult = runCLI(`test ${appName}`); + expect(appTestResult).toContain( + `Successfully ran target test for project ${appName}` + ); + + // assert scoped project names are not supported when --project-name-and-root-format=derived + expect(() => + runCLI( + `generate @nx/react-native:library ${libName} --buildable --project-name-and-root-format=derived` + ) + ).toThrow(); + + runCLI( + `generate @nx/react-native:library ${libName} --buildable --project-name-and-root-format=as-provided` + ); + + // check files are generated without the layout directory ("libs/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${libName}/src/index.ts`); + // check tests pass + const libTestResult = runCLI(`test ${libName}`); + expect(libTestResult).toContain( + `Successfully ran target test for project ${libName}` + ); + }); }); diff --git a/packages/react-native/generators.json b/packages/react-native/generators.json index f50bafa8446a0..aec2a322ff8e4 100644 --- a/packages/react-native/generators.json +++ b/packages/react-native/generators.json @@ -62,14 +62,14 @@ "hidden": true }, "application": { - "factory": "./src/generators/application/application#reactNativeApplicationGenerator", + "factory": "./src/generators/application/application#reactNativeApplicationGeneratorInternal", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", "description": "Create a React Native application." }, "library": { - "factory": "./src/generators/library/library#reactNativeLibraryGenerator", + "factory": "./src/generators/library/library#reactNativeLibraryGeneratorInternal", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index fa5f0224cc71c..089a1ea597f60 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -23,7 +23,17 @@ export async function reactNativeApplicationGenerator( host: Tree, schema: Schema ): Promise { - const options = normalizeOptions(host, schema); + return await reactNativeApplicationGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function reactNativeApplicationGeneratorInternal( + host: Tree, + schema: Schema +): Promise { + const options = await normalizeOptions(host, schema); createApplicationFiles(host, options); addProject(host, options); diff --git a/packages/react-native/src/generators/application/lib/add-detox.ts b/packages/react-native/src/generators/application/lib/add-detox.ts index 61cdc400cada1..981a118169afc 100644 --- a/packages/react-native/src/generators/application/lib/add-detox.ts +++ b/packages/react-native/src/generators/application/lib/add-detox.ts @@ -11,8 +11,9 @@ export async function addDetox(host: Tree, options: NormalizedSchema) { return detoxApplicationGenerator(host, { ...options, linter: Linter.EsLint, - e2eName: `${options.name}-e2e`, - e2eDirectory: options.directory, + e2eName: `${options.projectName}-e2e`, + e2eDirectory: `${options.appProjectRoot}-e2e`, + projectNameAndRootFormat: 'as-provided', appProject: options.projectName, appDisplayName: options.displayName, appName: options.name, diff --git a/packages/react-native/src/generators/application/lib/add-project.ts b/packages/react-native/src/generators/application/lib/add-project.ts index 8a3220d16ef46..ad57cae332508 100644 --- a/packages/react-native/src/generators/application/lib/add-project.ts +++ b/packages/react-native/src/generators/application/lib/add-project.ts @@ -34,7 +34,7 @@ function getTargets(options: NormalizedSchema) { architect.serve = { executor: 'nx:run-commands', options: { - command: `nx start ${options.name}`, + command: `nx start ${options.projectName}`, }, }; diff --git a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts index ffa7b03c8386e..480639a1a1a11 100644 --- a/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts +++ b/packages/react-native/src/generators/application/lib/nomalize-options.spec.ts @@ -11,14 +11,14 @@ describe('Normalize Options', () => { appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); - it('should normalize options with name in kebab case', () => { + it('should normalize options with name in kebab case', async () => { const schema: Schema = { name: 'my-app', linter: Linter.EsLint, e2eTestRunner: 'none', install: false, }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ androidProjectRoot: 'apps/my-app/android', appProjectRoot: 'apps/my-app', @@ -29,6 +29,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', linter: Linter.EsLint, entryFile: 'src/main.tsx', e2eTestRunner: 'none', @@ -37,13 +38,13 @@ describe('Normalize Options', () => { }); }); - it('should normalize options with name in camel case', () => { + it('should normalize options with name in camel case', async () => { const schema: Schema = { name: 'myApp', e2eTestRunner: 'none', install: false, }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ androidProjectRoot: 'apps/my-app/android', appProjectRoot: 'apps/my-app', @@ -54,6 +55,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', entryFile: 'src/main.tsx', e2eTestRunner: 'none', unitTestRunner: 'jest', @@ -61,14 +63,14 @@ describe('Normalize Options', () => { }); }); - it('should normalize options with directory', () => { + it('should normalize options with directory', async () => { const schema: Schema = { name: 'my-app', directory: 'directory', e2eTestRunner: 'none', install: false, }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ androidProjectRoot: 'apps/directory/my-app/android', appProjectRoot: 'apps/directory/my-app', @@ -80,6 +82,7 @@ describe('Normalize Options', () => { directory: 'directory', parsedTags: [], projectName: 'directory-my-app', + projectNameAndRootFormat: 'derived', entryFile: 'src/main.tsx', e2eTestRunner: 'none', unitTestRunner: 'jest', @@ -87,13 +90,13 @@ describe('Normalize Options', () => { }); }); - it('should normalize options that has directory in its name', () => { + it('should normalize options that has directory in its name', async () => { const schema: Schema = { name: 'directory/my-app', e2eTestRunner: 'none', install: false, }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ androidProjectRoot: 'apps/directory/my-app/android', appProjectRoot: 'apps/directory/my-app', @@ -104,6 +107,7 @@ describe('Normalize Options', () => { name: 'directory/my-app', parsedTags: [], projectName: 'directory-my-app', + projectNameAndRootFormat: 'derived', entryFile: 'src/main.tsx', e2eTestRunner: 'none', unitTestRunner: 'jest', @@ -111,14 +115,14 @@ describe('Normalize Options', () => { }); }); - it('should normalize options with display name', () => { + it('should normalize options with display name', async () => { const schema: Schema = { name: 'my-app', displayName: 'My App', e2eTestRunner: 'none', install: false, }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ androidProjectRoot: 'apps/my-app/android', appProjectRoot: 'apps/my-app', @@ -129,6 +133,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', entryFile: 'src/main.tsx', e2eTestRunner: 'none', unitTestRunner: 'jest', diff --git a/packages/react-native/src/generators/application/lib/normalize-options.ts b/packages/react-native/src/generators/application/lib/normalize-options.ts index b370b36b86de7..7fb1542ee07ba 100644 --- a/packages/react-native/src/generators/application/lib/normalize-options.ts +++ b/packages/react-native/src/generators/application/lib/normalize-options.ts @@ -1,4 +1,5 @@ -import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit'; +import { joinPathFragments, names, Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Schema } from '../schema'; export interface NormalizedSchema extends Schema { @@ -12,23 +13,25 @@ export interface NormalizedSchema extends Schema { entryFile: string; } -export function normalizeOptions( +export async function normalizeOptions( host: Tree, options: Schema -): NormalizedSchema { - const { fileName, className } = names(options.name); - const { appsDir } = getWorkspaceLayout(host); - - const directoryName = options.directory - ? names(options.directory).fileName - : ''; - const projectDirectory = directoryName - ? `${directoryName}/${fileName}` - : fileName; - - const appProjectName = projectDirectory.replace(/\//g, '-'); - - const appProjectRoot = joinPathFragments(appsDir, projectDirectory); +): Promise { + const { + projectName: appProjectName, + names: projectNames, + projectRoot: appProjectRoot, + projectNameAndRootFormat, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/react-native:application', + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; + + const { className } = names(options.name); const iosProjectRoot = joinPathFragments(appProjectRoot, 'ios'); const androidProjectRoot = joinPathFragments(appProjectRoot, 'android'); @@ -38,17 +41,11 @@ export function normalizeOptions( const entryFile = options.js ? 'src/main.js' : 'src/main.tsx'; - /** - * if options.name is "my-app" - * name: "my-app", className: 'MyApp', lowerCaseName: 'myapp', displayName: 'MyApp', projectName: 'my-app', appProjectRoot: 'apps/my-app', androidProjectRoot: 'apps/my-app/android', iosProjectRoot: 'apps/my-app/ios' - * if options.name is "myApp" - * name: "my-app", className: 'MyApp', lowerCaseName: 'myapp', displayName: 'MyApp', projectName: 'my-app', appProjectRoot: 'apps/my-app', androidProjectRoot: 'apps/my-app/android', iosProjectRoot: 'apps/my-app/ios' - */ return { ...options, unitTestRunner: options.unitTestRunner || 'jest', e2eTestRunner: options.e2eTestRunner || 'detox', - name: fileName, + name: projectNames.projectSimpleName, className, lowerCaseName: className.toLowerCase(), displayName: options.displayName || className, diff --git a/packages/react-native/src/generators/application/schema.d.ts b/packages/react-native/src/generators/application/schema.d.ts index 5d098dee36249..fa553bc5c4dcd 100644 --- a/packages/react-native/src/generators/application/schema.d.ts +++ b/packages/react-native/src/generators/application/schema.d.ts @@ -1,4 +1,5 @@ -import { Linter } from '@nx/linter'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; export interface Schema { name: string; @@ -6,6 +7,7 @@ export interface Schema { style?: string; skipFormat?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; unitTestRunner?: 'jest' | 'none'; pascalCaseFiles?: boolean; diff --git a/packages/react-native/src/generators/application/schema.json b/packages/react-native/src/generators/application/schema.json index fb33f20f40869..c528bbcf6439c 100644 --- a/packages/react-native/src/generators/application/schema.json +++ b/packages/react-native/src/generators/application/schema.json @@ -23,7 +23,8 @@ "$source": "argv", "index": 0 }, - "x-prompt": "What name would you like to use for the application?" + "x-prompt": "What name would you like to use for the application?", + "pattern": "^[a-zA-Z][^:]*$" }, "displayName": { "description": "The display name to show in the application. Defaults to name.", @@ -33,6 +34,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", diff --git a/packages/react-native/src/generators/library/lib/normalize-options.ts b/packages/react-native/src/generators/library/lib/normalize-options.ts index 7f594c8b43631..04a29afc3de81 100644 --- a/packages/react-native/src/generators/library/lib/normalize-options.ts +++ b/packages/react-native/src/generators/library/lib/normalize-options.ts @@ -1,5 +1,5 @@ -import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit'; -import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Schema } from '../schema'; export interface NormalizedSchema extends Schema { @@ -7,40 +7,39 @@ export interface NormalizedSchema extends Schema { fileName: string; projectRoot: string; routePath: string; - projectDirectory: string; parsedTags: string[]; appMain?: string; appSourceRoot?: string; } -export function normalizeOptions( +export async function normalizeOptions( host: Tree, options: Schema -): NormalizedSchema { - const name = names(options.name).fileName; - const projectDirectory = options.directory - ? `${names(options.directory).fileName}/${name}` - : name; - - const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); - const fileName = projectName; - const { libsDir } = getWorkspaceLayout(host); - const projectRoot = joinPathFragments(libsDir, projectDirectory); +): Promise { + const { + projectName, + names: projectNames, + projectRoot, + importPath, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'library', + directory: options.directory, + importPath: options.importPath, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/react-native:library', + }); const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; - const importPath = - options.importPath || getImportPath(host, projectDirectory); - const normalized: NormalizedSchema = { ...options, - fileName, - routePath: `/${name}`, + fileName: projectName, + routePath: `/${projectNames.projectSimpleName}`, name: projectName, projectRoot, - projectDirectory, parsedTags, importPath, }; diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index 8f3710e84d54f..f0b7a09f94141 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -5,7 +5,6 @@ import { formatFiles, generateFiles, GeneratorCallback, - getWorkspaceLayout, joinPathFragments, names, offsetFromRoot, @@ -32,7 +31,17 @@ export async function reactNativeLibraryGenerator( host: Tree, schema: Schema ): Promise { - const options = normalizeOptions(host, schema); + return await reactNativeLibraryGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function reactNativeLibraryGeneratorInternal( + host: Tree, + schema: Schema +): Promise { + const options = await normalizeOptions(host, schema); if (options.publishable === true && !schema.importPath) { throw new Error( `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)` @@ -105,7 +114,6 @@ async function addProject(host: Tree, options: NormalizedSchema) { nxVersion ); - const { libsDir } = getWorkspaceLayout(host); const external = [ 'react/jsx-runtime', 'react-native', @@ -117,7 +125,7 @@ async function addProject(host: Tree, options: NormalizedSchema) { executor: '@nx/rollup:rollup', outputs: ['{options.outputPath}'], options: { - outputPath: `dist/${libsDir}/${options.projectDirectory}`, + outputPath: `dist/${options.projectRoot}`, tsConfig: `${options.projectRoot}/tsconfig.lib.json`, project: `${options.projectRoot}/package.json`, entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`), diff --git a/packages/react-native/src/generators/library/schema.d.ts b/packages/react-native/src/generators/library/schema.d.ts index 885038f3ee233..b4ae7b807f59d 100644 --- a/packages/react-native/src/generators/library/schema.d.ts +++ b/packages/react-native/src/generators/library/schema.d.ts @@ -1,4 +1,5 @@ -import { Linter } from '@nx/linter'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; /** * Same as the @nx/react library schema, except it removes keys: style, component, routing, appProject @@ -6,6 +7,7 @@ import { Linter } from '@nx/linter'; export interface Schema { name: string; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; skipTsConfig: boolean; skipFormat: boolean; tags?: string; diff --git a/packages/react-native/src/generators/library/schema.json b/packages/react-native/src/generators/library/schema.json index 4eb1cef4ab637..21f3961f87480 100644 --- a/packages/react-native/src/generators/library/schema.json +++ b/packages/react-native/src/generators/library/schema.json @@ -20,7 +20,7 @@ "index": 0 }, "x-prompt": "What name would you like to use for the library?", - "pattern": "^[a-zA-Z].*$" + "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$" }, "directory": { "type": "string", @@ -28,6 +28,11 @@ "alias": "dir", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "linter": { "description": "The tool to use for running lint checks.", "type": "string",