From a668e0b7d6310af12e3ceca38bbc18df401c2b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 22 Aug 2023 15:25:54 +0100 Subject: [PATCH] feat(expo): use helper to determine project name and root in project generators (#18678) --- .../packages/expo/generators/application.json | 12 ++++-- .../packages/expo/generators/library.json | 11 +++-- e2e/expo/src/expo.test.ts | 38 +++++++++++++++++ packages/expo/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 | 42 +++++++++---------- .../src/generators/application/schema.d.ts | 4 +- .../src/generators/application/schema.json | 8 +++- .../library/lib/normalize-options.ts | 40 +++++++++--------- .../expo/src/generators/library/library.ts | 16 +++++-- .../expo/src/generators/library/schema.d.ts | 4 +- .../expo/src/generators/library/schema.json | 7 +++- 15 files changed, 156 insertions(+), 74 deletions(-) diff --git a/docs/generated/packages/expo/generators/application.json b/docs/generated/packages/expo/generators/application.json index 6ea6bddd89b9b..4e457fbc5d3ca 100644 --- a/docs/generated/packages/expo/generators/application.json +++ b/docs/generated/packages/expo/generators/application.json @@ -1,6 +1,6 @@ { "name": "application", - "factory": "./src/generators/application/application#expoApplicationGenerator", + "factory": "./src/generators/application/application#expoApplicationGeneratorInternal", "schema": { "cli": "nx", "$id": "NxExpoApplication", @@ -22,7 +22,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 @@ "type": "string", "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"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", @@ -90,7 +96,7 @@ "aliases": ["app"], "x-type": "application", "description": "Create an application", - "implementation": "/packages/expo/src/generators/application/application#expoApplicationGenerator.ts", + "implementation": "/packages/expo/src/generators/application/application#expoApplicationGeneratorInternal.ts", "hidden": false, "path": "/packages/expo/src/generators/application/schema.json", "type": "generator" diff --git a/docs/generated/packages/expo/generators/library.json b/docs/generated/packages/expo/generators/library.json index 855c37afd4682..10f1a98e01761 100644 --- a/docs/generated/packages/expo/generators/library.json +++ b/docs/generated/packages/expo/generators/library.json @@ -1,6 +1,6 @@ { "name": "library", - "factory": "./src/generators/library/library#expoLibraryGenerator", + "factory": "./src/generators/library/library#expoLibraryGeneratorInternal", "schema": { "cli": "nx", "$id": "NxExpoLibrary", @@ -19,13 +19,18 @@ "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", "description": "A directory where the lib is placed.", "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", @@ -101,7 +106,7 @@ "aliases": ["lib"], "x-type": "library", "description": "Create a library", - "implementation": "/packages/expo/src/generators/library/library#expoLibraryGenerator.ts", + "implementation": "/packages/expo/src/generators/library/library#expoLibraryGeneratorInternal.ts", "hidden": false, "path": "/packages/expo/src/generators/library/schema.json", "type": "generator" diff --git a/e2e/expo/src/expo.test.ts b/e2e/expo/src/expo.test.ts index 1fce2d1d3c359..0824db1123df0 100644 --- a/e2e/expo/src/expo.test.ts +++ b/e2e/expo/src/expo.test.ts @@ -155,4 +155,42 @@ describe('expo', () => { ); }).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/expo: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/expo:library ${libName} --buildable --project-name-and-root-format=derived` + ) + ).toThrow(); + + runCLI( + `generate @nx/expo: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/expo/generators.json b/packages/expo/generators.json index 91dfee18d10b9..e683057b95dba 100644 --- a/packages/expo/generators.json +++ b/packages/expo/generators.json @@ -38,14 +38,14 @@ "hidden": true }, "application": { - "factory": "./src/generators/application/application#expoApplicationGenerator", + "factory": "./src/generators/application/application#expoApplicationGeneratorInternal", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", "description": "Create an application" }, "library": { - "factory": "./src/generators/library/library#expoLibraryGenerator", + "factory": "./src/generators/library/library#expoLibraryGeneratorInternal", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts index 627feb7869763..72c1f49ac66c3 100644 --- a/packages/expo/src/generators/application/application.ts +++ b/packages/expo/src/generators/application/application.ts @@ -23,7 +23,17 @@ export async function expoApplicationGenerator( host: Tree, schema: Schema ): Promise { - const options = normalizeOptions(host, schema); + return await expoApplicationGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function expoApplicationGeneratorInternal( + host: Tree, + schema: Schema +): Promise { + const options = await normalizeOptions(host, schema); createApplicationFiles(host, options); addProject(host, options); diff --git a/packages/expo/src/generators/application/lib/add-detox.ts b/packages/expo/src/generators/application/lib/add-detox.ts index 1b7f77797ed54..93366950af185 100644 --- a/packages/expo/src/generators/application/lib/add-detox.ts +++ b/packages/expo/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/expo/src/generators/application/lib/add-project.ts b/packages/expo/src/generators/application/lib/add-project.ts index d93911b7b9a4f..34a80311eb186 100644 --- a/packages/expo/src/generators/application/lib/add-project.ts +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -38,7 +38,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/expo/src/generators/application/lib/nomalize-options.spec.ts b/packages/expo/src/generators/application/lib/nomalize-options.spec.ts index b1955d1a040f6..71784da943e08 100644 --- a/packages/expo/src/generators/application/lib/nomalize-options.spec.ts +++ b/packages/expo/src/generators/application/lib/nomalize-options.spec.ts @@ -11,7 +11,7 @@ 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, @@ -20,7 +20,7 @@ describe('Normalize Options', () => { js: true, unitTestRunner: 'jest', }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ appProjectRoot: 'apps/my-app', className: 'MyApp', @@ -29,6 +29,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', linter: Linter.EsLint, e2eTestRunner: 'none', unitTestRunner: 'jest', @@ -37,7 +38,7 @@ 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', linter: Linter.EsLint, @@ -46,7 +47,7 @@ describe('Normalize Options', () => { js: true, unitTestRunner: 'jest', }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ appProjectRoot: 'apps/my-app', className: 'MyApp', @@ -55,6 +56,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', linter: Linter.EsLint, e2eTestRunner: 'none', skipFormat: false, @@ -63,7 +65,7 @@ describe('Normalize Options', () => { }); }); - it('should normalize options with directory', () => { + it('should normalize options with directory', async () => { const schema: Schema = { name: 'my-app', directory: 'directory', @@ -73,7 +75,7 @@ describe('Normalize Options', () => { js: true, unitTestRunner: 'jest', }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ appProjectRoot: 'apps/directory/my-app', className: 'MyApp', @@ -83,6 +85,7 @@ describe('Normalize Options', () => { directory: 'directory', parsedTags: [], projectName: 'directory-my-app', + projectNameAndRootFormat: 'derived', e2eTestRunner: 'none', unitTestRunner: 'jest', linter: Linter.EsLint, @@ -91,7 +94,7 @@ 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', linter: Linter.EsLint, @@ -100,7 +103,7 @@ describe('Normalize Options', () => { js: true, unitTestRunner: 'jest', }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ appProjectRoot: 'apps/directory/my-app', className: 'DirectoryMyApp', @@ -109,6 +112,7 @@ describe('Normalize Options', () => { name: 'directory/my-app', parsedTags: [], projectName: 'directory-my-app', + projectNameAndRootFormat: 'derived', e2eTestRunner: 'none', unitTestRunner: 'jest', linter: Linter.EsLint, @@ -117,7 +121,7 @@ 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', @@ -127,7 +131,7 @@ describe('Normalize Options', () => { js: true, unitTestRunner: 'jest', }; - const options = normalizeOptions(appTree, schema); + const options = await normalizeOptions(appTree, schema); expect(options).toEqual({ appProjectRoot: 'apps/my-app', className: 'MyApp', @@ -136,6 +140,7 @@ describe('Normalize Options', () => { name: 'my-app', parsedTags: [], projectName: 'my-app', + projectNameAndRootFormat: 'derived', e2eTestRunner: 'none', unitTestRunner: 'jest', linter: Linter.EsLint, diff --git a/packages/expo/src/generators/application/lib/normalize-options.ts b/packages/expo/src/generators/application/lib/normalize-options.ts index 53fd7dfb8f8c4..6802d217fdcfe 100644 --- a/packages/expo/src/generators/application/lib/normalize-options.ts +++ b/packages/expo/src/generators/application/lib/normalize-options.ts @@ -1,4 +1,5 @@ -import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit'; +import { 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 { @@ -9,39 +10,34 @@ export interface NormalizedSchema extends Schema { parsedTags: 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(new RegExp('/', '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/expo:application', + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; + const { className } = names(options.name); const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; - /** - * 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/expo/src/generators/application/schema.d.ts b/packages/expo/src/generators/application/schema.d.ts index ee0720edc6f90..f1529d67f17eb 100644 --- a/packages/expo/src/generators/application/schema.d.ts +++ b/packages/expo/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; // default is false directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; unitTestRunner: 'jest' | 'none'; // default is jest pascalCaseFiles?: boolean; diff --git a/packages/expo/src/generators/application/schema.json b/packages/expo/src/generators/application/schema.json index 27ac2c95d8a3d..904106a0d75e4 100644 --- a/packages/expo/src/generators/application/schema.json +++ b/packages/expo/src/generators/application/schema.json @@ -22,7 +22,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 @@ "type": "string", "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"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", diff --git a/packages/expo/src/generators/library/lib/normalize-options.ts b/packages/expo/src/generators/library/lib/normalize-options.ts index d882f087ab887..f0162ccfdf3ad 100644 --- a/packages/expo/src/generators/library/lib/normalize-options.ts +++ b/packages/expo/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,41 +7,39 @@ export interface NormalizedSchema extends Schema { fileName: string; projectRoot: string; routePath: string; - projectDirectory: string; parsedTags: string[]; appMain: 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/expo:library', + }); const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; - - const importPath = - options.importPath || getImportPath(host, projectDirectory); - const appMain = options.js ? 'src/index.js' : 'src/index.ts'; const normalized: NormalizedSchema = { ...options, - fileName, - routePath: `/${name}`, + fileName: projectName, + routePath: `/${projectNames.projectSimpleName}`, name: projectName, projectRoot, - projectDirectory, parsedTags, importPath, appMain, diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 8b431b39f5589..96cafbacae22e 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -5,7 +5,6 @@ import { formatFiles, generateFiles, GeneratorCallback, - getWorkspaceLayout, joinPathFragments, names, offsetFromRoot, @@ -32,7 +31,17 @@ export async function expoLibraryGenerator( host: Tree, schema: Schema ): Promise { - const options = normalizeOptions(host, schema); + return await expoLibraryGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function expoLibraryGeneratorInternal( + 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/expo/src/generators/library/schema.d.ts b/packages/expo/src/generators/library/schema.d.ts index 706cf0222ae8f..6407f8b80fd92 100644 --- a/packages/expo/src/generators/library/schema.d.ts +++ b/packages/expo/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; // default is false skipFormat: boolean; // default is false tags?: string; diff --git a/packages/expo/src/generators/library/schema.json b/packages/expo/src/generators/library/schema.json index 8ef6e4d777602..1115a76e43036 100644 --- a/packages/expo/src/generators/library/schema.json +++ b/packages/expo/src/generators/library/schema.json @@ -19,13 +19,18 @@ "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", "description": "A directory where the lib is placed.", "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",