feat(detox): use helper to determine project name and root in application generator (#18674)

This commit is contained in:
Leosvel Pérez Espinosa 2023-08-21 22:07:36 +01:00 committed by GitHub
parent d56605522b
commit 9002662b07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 85 additions and 69 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "application", "name": "application",
"factory": "./src/generators/application/application#detoxApplicationGenerator", "factory": "./src/generators/application/application#detoxApplicationGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"title": "Create Detox Configuration for the workspace", "title": "Create Detox Configuration for the workspace",
@ -17,7 +17,8 @@
"type": "string", "type": "string",
"description": "Name of the E2E Project.", "description": "Name of the E2E Project.",
"$default": { "$source": "argv", "index": 0 }, "$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the E2E project?" "x-prompt": "What name would you like to use for the E2E project?",
"pattern": "^[a-zA-Z][^:]*$"
}, },
"appName": { "appName": {
"type": "string", "type": "string",
@ -37,6 +38,11 @@
"type": "string", "type": "string",
"description": "A directory where the project is placed relative to apps directory." "description": "A directory where the project is placed relative to apps directory."
}, },
"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": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",
@ -66,7 +72,7 @@
"aliases": ["app"], "aliases": ["app"],
"x-type": "application", "x-type": "application",
"description": "Create a Detox application.", "description": "Create a Detox application.",
"implementation": "/packages/detox/src/generators/application/application#detoxApplicationGenerator.ts", "implementation": "/packages/detox/src/generators/application/application#detoxApplicationGeneratorInternal.ts",
"hidden": false, "hidden": false,
"path": "/packages/detox/src/generators/application/schema.json", "path": "/packages/detox/src/generators/application/schema.json",
"type": "generator" "type": "generator"

View File

@ -47,6 +47,27 @@ describe('Detox', () => {
expect(lintResults.combinedOutput).toContain('All files pass linting'); expect(lintResults.combinedOutput).toContain('All files pass linting');
}); });
it('should support generating projects with the new name and root format', async () => {
const appName = uniq('app1');
runCLI(
`generate @nx/react-native:app ${appName} --e2eTestRunner=detox --linter=eslint --install=false --project-name-and-root-format=as-provided`
);
// check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided
checkFilesExist(
`${appName}-e2e/.detoxrc.json`,
`${appName}-e2e/tsconfig.json`,
`${appName}-e2e/tsconfig.e2e.json`,
`${appName}-e2e/test-setup.ts`,
`${appName}-e2e/src/app.spec.ts`
);
const lintResults = await runCLIAsync(`lint ${appName}-e2e`);
expect(lintResults.combinedOutput).toContain('All files pass linting');
});
// TODO: @xiongemi please fix or remove this test // TODO: @xiongemi please fix or remove this test
xdescribe('React Native Detox MACOS-Tests', () => { xdescribe('React Native Detox MACOS-Tests', () => {
if (isOSX()) { if (isOSX()) {

View File

@ -25,7 +25,7 @@
"hidden": true "hidden": true
}, },
"application": { "application": {
"factory": "./src/generators/application/application#detoxApplicationGenerator", "factory": "./src/generators/application/application#detoxApplicationGeneratorInternal",
"schema": "./src/generators/application/schema.json", "schema": "./src/generators/application/schema.json",
"aliases": ["app"], "aliases": ["app"],
"x-type": "application", "x-type": "application",

View File

@ -14,7 +14,17 @@ import { normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema'; import { Schema } from './schema';
export async function detoxApplicationGenerator(host: Tree, schema: Schema) { export async function detoxApplicationGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema); return await detoxApplicationGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function detoxApplicationGeneratorInternal(
host: Tree,
schema: Schema
) {
const options = await normalizeOptions(host, schema);
const initTask = await detoxInitGenerator(host, { const initTask = await detoxInitGenerator(host, {
...options, ...options,

View File

@ -12,7 +12,6 @@ describe('Add Linting', () => {
addProject(tree, { addProject(tree, {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
@ -29,7 +28,6 @@ describe('Add Linting', () => {
addLinting(tree, { addLinting(tree, {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
@ -50,7 +48,6 @@ describe('Add Linting', () => {
addLinting(tree, { addLinting(tree, {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',

View File

@ -31,7 +31,6 @@ describe('Add Project', () => {
addProject(tree, { addProject(tree, {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
@ -81,7 +80,6 @@ describe('Add Project', () => {
addProject(tree, { addProject(tree, {
e2eName: 'my-dir-my-app-e2e', e2eName: 'my-dir-my-app-e2e',
e2eProjectName: 'my-dir-my-app-e2e', e2eProjectName: 'my-dir-my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-dir/my-app-e2e', e2eProjectRoot: 'apps/my-dir/my-app-e2e',
appProject: 'my-dir-my-app', appProject: 'my-dir-my-app',
appFileName: 'my-app', appFileName: 'my-app',

View File

@ -31,8 +31,8 @@ function getTargets(options: NormalizedSchema) {
targets['test-ios'] = { targets['test-ios'] = {
executor: '@nx/detox:test', executor: '@nx/detox:test',
...(options.framework === 'react-native' ...(options.framework === 'react-native'
? reactNativeTestTarget('ios.sim', options.e2eName) ? reactNativeTestTarget('ios.sim', options.e2eProjectName)
: expoTestTarget('ios.sim', options.e2eName)), : expoTestTarget('ios.sim', options.e2eProjectName)),
}; };
targets['build-android'] = { targets['build-android'] = {
@ -45,8 +45,8 @@ function getTargets(options: NormalizedSchema) {
targets['test-android'] = { targets['test-android'] = {
executor: '@nx/detox:test', executor: '@nx/detox:test',
...(options.framework === 'react-native' ...(options.framework === 'react-native'
? reactNativeTestTarget('android.emu', options.e2eName) ? reactNativeTestTarget('android.emu', options.e2eProjectName)
: expoTestTarget('android.emu', options.e2eName)), : expoTestTarget('android.emu', options.e2eProjectName)),
}; };
return targets; return targets;

View File

@ -14,7 +14,6 @@ describe('Create Files', () => {
createFiles(tree, { createFiles(tree, {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',

View File

@ -12,7 +12,7 @@ describe('Normalize Options', () => {
appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
}); });
it('should normalize options with name in kebab case', () => { it('should normalize options with name in kebab case', async () => {
addProjectConfiguration(appTree, 'my-app', { addProjectConfiguration(appTree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',
targets: {}, targets: {},
@ -23,12 +23,11 @@ describe('Normalize Options', () => {
appProject: 'my-app', appProject: 'my-app',
linter: Linter.EsLint, linter: Linter.EsLint,
}; };
const options = normalizeOptions(appTree, schema); const options = await normalizeOptions(appTree, schema);
expect(options).toEqual({ expect(options).toEqual({
framework: 'react-native', framework: 'react-native',
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
appFileName: 'my-app', appFileName: 'my-app',
@ -40,7 +39,7 @@ describe('Normalize Options', () => {
}); });
}); });
it('should normalize options with name in camel case', () => { it('should normalize options with name in camel case', async () => {
addProjectConfiguration(appTree, 'my-app', { addProjectConfiguration(appTree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',
targets: {}, targets: {},
@ -50,7 +49,7 @@ describe('Normalize Options', () => {
e2eName: 'myAppE2e', e2eName: 'myAppE2e',
appProject: 'myApp', appProject: 'myApp',
}; };
const options = normalizeOptions(appTree, schema); const options = await normalizeOptions(appTree, schema);
expect(options).toEqual({ expect(options).toEqual({
appClassName: 'MyApp', appClassName: 'MyApp',
appDisplayName: 'MyApp', appDisplayName: 'MyApp',
@ -60,13 +59,12 @@ describe('Normalize Options', () => {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
appProject: 'myApp', appProject: 'myApp',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
framework: 'react-native', framework: 'react-native',
}); });
}); });
it('should normalize options with display name', () => { it('should normalize options with display name', async () => {
addProjectConfiguration(appTree, 'my-app', { addProjectConfiguration(appTree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',
targets: {}, targets: {},
@ -77,7 +75,7 @@ describe('Normalize Options', () => {
appProject: 'myApp', appProject: 'myApp',
appDisplayName: 'app display name', appDisplayName: 'app display name',
}; };
const options = normalizeOptions(appTree, schema); const options = await normalizeOptions(appTree, schema);
expect(options).toEqual({ expect(options).toEqual({
appDisplayName: 'app display name', appDisplayName: 'app display name',
appExpoName: 'appdisplayname', appExpoName: 'appdisplayname',
@ -87,13 +85,12 @@ describe('Normalize Options', () => {
e2eName: 'my-app-e2e', e2eName: 'my-app-e2e',
appProject: 'myApp', appProject: 'myApp',
e2eProjectName: 'my-app-e2e', e2eProjectName: 'my-app-e2e',
e2eProjectDirectory: 'apps',
e2eProjectRoot: 'apps/my-app-e2e', e2eProjectRoot: 'apps/my-app-e2e',
framework: 'react-native', framework: 'react-native',
}); });
}); });
it('should normalize options with directory', () => { it('should normalize options with directory', async () => {
addProjectConfiguration(appTree, 'my-app', { addProjectConfiguration(appTree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',
targets: {}, targets: {},
@ -104,7 +101,7 @@ describe('Normalize Options', () => {
appProject: 'my-app', appProject: 'my-app',
e2eDirectory: 'directory', e2eDirectory: 'directory',
}; };
const options = normalizeOptions(appTree, schema); const options = await normalizeOptions(appTree, schema);
expect(options).toEqual({ expect(options).toEqual({
appProject: 'my-app', appProject: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -112,16 +109,15 @@ describe('Normalize Options', () => {
appExpoName: 'MyApp', appExpoName: 'MyApp',
appFileName: 'my-app', appFileName: 'my-app',
appRoot: 'apps/my-app', appRoot: 'apps/my-app',
e2eProjectDirectory: 'apps/directory',
e2eProjectRoot: 'apps/directory/my-app-e2e', e2eProjectRoot: 'apps/directory/my-app-e2e',
e2eName: 'my-app-e2e', e2eName: 'directory-my-app-e2e',
e2eDirectory: 'directory', e2eDirectory: 'directory',
e2eProjectName: 'directory-my-app-e2e', e2eProjectName: 'directory-my-app-e2e',
framework: 'react-native', framework: 'react-native',
}); });
}); });
it('should normalize options with directory in its name', () => { it('should normalize options with directory in its name', async () => {
addProjectConfiguration(appTree, 'my-app', { addProjectConfiguration(appTree, 'my-app', {
root: 'apps/my-app', root: 'apps/my-app',
targets: {}, targets: {},
@ -131,7 +127,7 @@ describe('Normalize Options', () => {
e2eName: 'directory/my-app-e2e', e2eName: 'directory/my-app-e2e',
appProject: 'my-app', appProject: 'my-app',
}; };
const options = normalizeOptions(appTree, schema); const options = await normalizeOptions(appTree, schema);
expect(options).toEqual({ expect(options).toEqual({
appProject: 'my-app', appProject: 'my-app',
appClassName: 'MyApp', appClassName: 'MyApp',
@ -140,8 +136,7 @@ describe('Normalize Options', () => {
appFileName: 'my-app', appFileName: 'my-app',
appRoot: 'apps/my-app', appRoot: 'apps/my-app',
e2eProjectRoot: 'apps/directory/my-app-e2e', e2eProjectRoot: 'apps/directory/my-app-e2e',
e2eProjectDirectory: 'apps', e2eName: 'directory-my-app-e2e',
e2eName: 'directory/my-app-e2e',
e2eProjectName: 'directory-my-app-e2e', e2eProjectName: 'directory-my-app-e2e',
framework: 'react-native', framework: 'react-native',
}); });

View File

@ -1,10 +1,5 @@
import { import { names, readProjectConfiguration, Tree } from '@nx/devkit';
getProjects, import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { Schema } from '../schema'; import { Schema } from '../schema';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
@ -13,41 +8,29 @@ export interface NormalizedSchema extends Schema {
appExpoName: string; // the expo name of app to be tested in class case appExpoName: string; // the expo name of app to be tested in class case
appRoot: string; // the root path of e2e project. e.g. apps/app-directory/app appRoot: string; // the root path of e2e project. e.g. apps/app-directory/app
e2eProjectName: string; // the name of e2e project e2eProjectName: string; // the name of e2e project
e2eProjectDirectory: string; // root path the directory of e2e project directory. e,g. apps/e2e-directory
e2eProjectRoot: string; // the root path of e2e project. e.g. apps/e2e-directory/e2e-app e2eProjectRoot: string; // the root path of e2e project. e.g. apps/e2e-directory/e2e-app
} }
/** export async function normalizeOptions(
* if options.e2eName = 'my-app-e2e' with no options.directory
* e2eProjectName = 'my-app', e2eProjectRoot = 'apps/my-app'
* if options.e2eName = 'my-app' with options.e2eDirectory = 'my-dir'
* e2eProjectName = 'my-dir-my-app', e2eProjectRoot = 'apps/my-dir/my-apps'
*/
export function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): NormalizedSchema { ): Promise<NormalizedSchema> {
const { appsDir } = getWorkspaceLayout(host); const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } =
const e2eFileName = names(options.e2eName).fileName; await determineProjectNameAndRootOptions(host, {
const e2eDirectoryFileName = options.e2eDirectory name: options.e2eName,
? names(options.e2eDirectory).fileName projectType: 'application',
: ''; directory: options.e2eDirectory,
const e2eProjectName = ( projectNameAndRootFormat: options.projectNameAndRootFormat,
e2eDirectoryFileName callingGenerator: '@nx/detox:application',
? `${e2eDirectoryFileName}-${e2eFileName}` });
: e2eFileName
).replace(/\//g, '-');
const e2eProjectDirectory = e2eDirectoryFileName
? joinPathFragments(appsDir, e2eDirectoryFileName)
: appsDir;
const e2eProjectRoot = joinPathFragments(e2eProjectDirectory, e2eFileName);
const { fileName: appFileName, className: appClassName } = names( const { fileName: appFileName, className: appClassName } = names(
options.appName || options.appProject options.appName || options.appProject
); );
const project = getProjects(host).get(options.appProject); const { root: appRoot } = readProjectConfiguration(
const appRoot = host,
project?.root || joinPathFragments(e2eProjectDirectory, appFileName); names(options.appProject).fileName
);
return { return {
...options, ...options,
@ -56,9 +39,8 @@ export function normalizeOptions(
appDisplayName: options.appDisplayName || appClassName, appDisplayName: options.appDisplayName || appClassName,
appExpoName: options.appDisplayName?.replace(/\s/g, '') || appClassName, appExpoName: options.appDisplayName?.replace(/\s/g, '') || appClassName,
appRoot, appRoot,
e2eName: e2eFileName, e2eName: e2eProjectName,
e2eProjectName, e2eProjectName,
e2eProjectDirectory,
e2eProjectRoot, e2eProjectRoot,
}; };
} }

View File

@ -1,10 +1,12 @@
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 { export interface Schema {
appProject: string; // name of the project app to be tested (directory + app name in kebab class) appProject: string; // name of the project app to be tested (directory + app name in kebab class)
appDisplayName?: string; // display name of the app to be tested appDisplayName?: string; // display name of the app to be tested
appName?: string; // name of app to be tested if different form appProject, case insenstive appName?: string; // name of app to be tested if different form appProject, case insenstive
e2eDirectory?: string; // the directory where e2e app going to be located e2eDirectory?: string; // the directory where e2e app going to be located
projectNameAndRootFormat?: ProjectNameAndRootFormat;
e2eName: string; // name of the e2e app e2eName: string; // name of the e2e app
linter?: Linter; linter?: Linter;
js?: boolean; js?: boolean;

View File

@ -19,7 +19,8 @@
"$source": "argv", "$source": "argv",
"index": 0 "index": 0
}, },
"x-prompt": "What name would you like to use for the E2E project?" "x-prompt": "What name would you like to use for the E2E project?",
"pattern": "^[a-zA-Z][^:]*$"
}, },
"appName": { "appName": {
"type": "string", "type": "string",
@ -39,6 +40,11 @@
"type": "string", "type": "string",
"description": "A directory where the project is placed relative to apps directory." "description": "A directory where the project is placed relative to apps directory."
}, },
"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": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",