From 289555868806ac81f515e4f1d40b679f2a9fc16e Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 15 Aug 2019 21:26:43 -0400 Subject: [PATCH] feat(react): add --directory option for React components Also adds alias support to tao cli. Closes #1702 --- docs/api-react/schematics/component.md | 24 ++++++- .../src/schematics/application/application.ts | 40 ++++++----- .../schematics/component/component.spec.ts | 70 +++++++++++-------- .../src/schematics/component/component.ts | 24 ++++++- .../__fileName__.__style__ | 0 .../__fileName__.spec.tsx__tmpl__ | 0 .../__fileName__.tsx__tmpl__ | 2 +- .../src/schematics/component/schema.d.ts | 1 + .../src/schematics/component/schema.json | 18 +++-- .../src/schematics/library/library.spec.ts | 22 ++---- packages/tao/src/commands/generate.ts | 15 ++-- packages/tao/src/shared/params.spec.ts | 30 +++++++- packages/tao/src/shared/params.ts | 16 +++++ 13 files changed, 182 insertions(+), 80 deletions(-) rename packages/react/src/schematics/component/files/{__name__ => __directory__}/__fileName__.__style__ (100%) rename packages/react/src/schematics/component/files/{__name__ => __directory__}/__fileName__.spec.tsx__tmpl__ (100%) rename packages/react/src/schematics/component/files/{__name__ => __directory__}/__fileName__.tsx__tmpl__ (96%) diff --git a/docs/api-react/schematics/component.md b/docs/api-react/schematics/component.md index 6ebc22f7dddb4..cdc95afd924dc 100644 --- a/docs/api-react/schematics/component.md +++ b/docs/api-react/schematics/component.md @@ -13,14 +13,26 @@ ng generate component ... ### classComponent +Alias(es): C + Default: `false` Type: `boolean` -Use class components instead of functional component +Use class components instead of functional component. + +### directory + +Alias(es): d + +Type: `string` + +Create the component under this directory (can be nested). ### export +Alias(es): e + Default: `false` Type: `boolean` @@ -35,14 +47,18 @@ The name of the component. ### pascalCaseFiles +Alias(es): P + Default: `false` Type: `boolean` -Use pascal case component file name (e.g. App.tsx) +Use pascal case component file name (e.g. App.tsx). ### project +Alias(es): p + Type: `string` The name of the project. @@ -51,7 +67,7 @@ The name of the project. Type: `boolean` -Generate library with routes +Generate a library with routes. ### skipTests @@ -63,6 +79,8 @@ When true, does not create "spec.ts" test files for the new component. ### style +Alias(es): s + Default: `css` Type: `string` diff --git a/packages/react/src/schematics/application/application.ts b/packages/react/src/schematics/application/application.ts index ef89b78290e66..f395de7b11331 100644 --- a/packages/react/src/schematics/application/application.ts +++ b/packages/react/src/schematics/application/application.ts @@ -76,22 +76,8 @@ export default function(schema: Schema): Rule { createApplicationFiles(options), updateNxJson(options), addProject(options), - options.e2eTestRunner === 'cypress' - ? externalSchematic('@nrwl/cypress', 'cypress-project', { - ...options, - name: options.name + '-e2e', - directory: options.directory, - project: options.projectName - }) - : noop(), - options.unitTestRunner === 'jest' - ? externalSchematic('@nrwl/jest', 'jest-project', { - project: options.projectName, - supportTsx: true, - skipSerializers: true, - setupFile: 'none' - }) - : noop(), + addCypress(options), + addJest(options), addStyledModuleDependencies(options), addRouting(options, context), addBabel(options), @@ -215,6 +201,28 @@ function addProject(options: NormalizedSchema): Rule { }); } +function addCypress(options: NormalizedSchema): Rule { + return options.e2eTestRunner === 'cypress' + ? externalSchematic('@nrwl/cypress', 'cypress-project', { + ...options, + name: options.name + '-e2e', + directory: options.directory, + project: options.projectName + }) + : noop(); +} + +function addJest(options: NormalizedSchema): Rule { + return options.unitTestRunner === 'jest' + ? externalSchematic('@nrwl/jest', 'jest-project', { + project: options.projectName, + supportTsx: true, + skipSerializers: true, + setupFile: 'none' + }) + : noop(); +} + function addStyledModuleDependencies(options: NormalizedSchema): Rule { const extraDependencies = CSS_IN_JS_DEPENDENCIES[options.styledModule]; diff --git a/packages/react/src/schematics/component/component.spec.ts b/packages/react/src/schematics/component/component.spec.ts index 2813e13245ce2..c087504ae99a2 100644 --- a/packages/react/src/schematics/component/component.spec.ts +++ b/packages/react/src/schematics/component/component.spec.ts @@ -22,11 +22,9 @@ describe('component', () => { appTree ); - expect(tree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); - expect( - tree.exists('libs/my-lib/src/lib/hello/hello.spec.tsx') - ).toBeTruthy(); - expect(tree.exists('libs/my-lib/src/lib/hello/hello.css')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/hello.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/hello.spec.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/hello.css')).toBeTruthy(); }); it('should generate files for an app', async () => { @@ -36,11 +34,9 @@ describe('component', () => { appTree ); - expect(tree.exists('apps/my-app/src/app/hello/hello.tsx')).toBeTruthy(); - expect( - tree.exists('apps/my-app/src/app/hello/hello.spec.tsx') - ).toBeTruthy(); - expect(tree.exists('apps/my-app/src/app/hello/hello.css')).toBeTruthy(); + expect(tree.exists('apps/my-app/src/app/hello.tsx')).toBeTruthy(); + expect(tree.exists('apps/my-app/src/app/hello.spec.tsx')).toBeTruthy(); + expect(tree.exists('apps/my-app/src/app/hello.css')).toBeTruthy(); }); describe('--export', () => { @@ -53,7 +49,7 @@ describe('component', () => { const indexContent = tree.read('libs/my-lib/src/index.ts').toString(); - expect(indexContent).toMatch(/lib\/hello\/hello/); + expect(indexContent).toMatch(/lib\/hello/); }); it('should not export from an app', async () => { @@ -65,7 +61,7 @@ describe('component', () => { const indexContent = tree.read('libs/my-lib/src/index.ts').toString(); - expect(indexContent).not.toMatch(/lib\/hello\/hello/); + expect(indexContent).not.toMatch(/lib\/hello/); }); }); @@ -76,11 +72,9 @@ describe('component', () => { { name: 'hello', project: projectName, pascalCaseFiles: true }, appTree ); - expect(tree.exists('libs/my-lib/src/lib/hello/Hello.tsx')).toBeTruthy(); - expect( - tree.exists('libs/my-lib/src/lib/hello/Hello.spec.tsx') - ).toBeTruthy(); - expect(tree.exists('libs/my-lib/src/lib/hello/Hello.css')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/Hello.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/Hello.spec.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/Hello.css')).toBeTruthy(); }); }); @@ -93,13 +87,11 @@ describe('component', () => { ); expect( - tree.exists('libs/my-lib/src/lib/hello/hello.styled-components') + tree.exists('libs/my-lib/src/lib/hello.styled-components') ).toBeFalsy(); - expect(tree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/hello.tsx')).toBeTruthy(); - const content = tree - .read('libs/my-lib/src/lib/hello/hello.tsx') - .toString(); + const content = tree.read('libs/my-lib/src/lib/hello.tsx').toString(); expect(content).toContain('styled-components'); expect(content).toContain(''); }); @@ -125,13 +117,11 @@ describe('component', () => { ); expect( - tree.exists('libs/my-lib/src/lib/hello/hello.@emotion/styled') + tree.exists('libs/my-lib/src/lib/hello.@emotion/styled') ).toBeFalsy(); - expect(tree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/hello.tsx')).toBeTruthy(); - const content = tree - .read('libs/my-lib/src/lib/hello/hello.tsx') - .toString(); + const content = tree.read('libs/my-lib/src/lib/hello.tsx').toString(); expect(content).toContain('@emotion/styled'); expect(content).toContain(''); }); @@ -157,9 +147,7 @@ describe('component', () => { appTree ); - const content = tree - .read('libs/my-lib/src/lib/hello/hello.tsx') - .toString(); + const content = tree.read('libs/my-lib/src/lib/hello.tsx').toString(); expect(content).toContain('react-router-dom'); expect(content).toMatch(/ { expect(packageJSON.dependencies['react-router-dom']).toBeDefined(); }); }); + + describe('--directory', () => { + it('should create component under the directory', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, directory: 'components' }, + appTree + ); + + expect(tree.exists('/libs/my-lib/src/lib/components/hello.tsx')); + }); + + it('should create with nested directories', async () => { + const tree = await runSchematic( + 'component', + { name: 'helloWorld', project: projectName, directory: 'foo' }, + appTree + ); + + expect(tree.exists('/libs/my-lib/src/lib/foo/bar/faz/hello-world.tsx')); + }); + }); }); diff --git a/packages/react/src/schematics/component/component.ts b/packages/react/src/schematics/component/component.ts index 8bdfbd0997d98..ac342e9c4f5ba 100644 --- a/packages/react/src/schematics/component/component.ts +++ b/packages/react/src/schematics/component/component.ts @@ -14,7 +14,7 @@ import { url } from '@angular-devkit/schematics'; import { Schema } from './schema'; -import { getWorkspace, names, formatFiles } from '@nrwl/workspace'; +import { formatFiles, getWorkspace, names } from '@nrwl/workspace'; import { addDepsToPackageJson, addGlobal, @@ -108,7 +108,11 @@ function addExportsToBarrel(options: NormalizedSchema): Rule { addGlobal( indexSourceFile, indexFilePath, - `export * from './lib/${options.name}/${options.fileName}';` + options.directory + ? `export * from './lib/${options.directory}/${ + options.fileName + }';` + : `export * from './lib/${options.fileName}';` ) ); } @@ -143,11 +147,25 @@ function normalizeOptions( ); } + const slashes = ['/', '\\']; + slashes.forEach(s => { + if (componentFileName.indexOf(s) !== -1) { + const [name, ...rest] = componentFileName.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}\`)?` + ); + } + }); + return { ...options, + directory: options.directory || '', styledModule, className, - name: fileName, fileName: componentFileName, projectSourceRoot }; diff --git a/packages/react/src/schematics/component/files/__name__/__fileName__.__style__ b/packages/react/src/schematics/component/files/__directory__/__fileName__.__style__ similarity index 100% rename from packages/react/src/schematics/component/files/__name__/__fileName__.__style__ rename to packages/react/src/schematics/component/files/__directory__/__fileName__.__style__ diff --git a/packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ b/packages/react/src/schematics/component/files/__directory__/__fileName__.spec.tsx__tmpl__ similarity index 100% rename from packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ rename to packages/react/src/schematics/component/files/__directory__/__fileName__.spec.tsx__tmpl__ diff --git a/packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ b/packages/react/src/schematics/component/files/__directory__/__fileName__.tsx__tmpl__ similarity index 96% rename from packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ rename to packages/react/src/schematics/component/files/__directory__/__fileName__.tsx__tmpl__ index 33d6845cb2b5b..adb36513c6050 100644 --- a/packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ +++ b/packages/react/src/schematics/component/files/__directory__/__fileName__.tsx__tmpl__ @@ -31,7 +31,7 @@ export class <%= className %> extends Component<<%= className %>Props> { render() { return ( <<%= wrapper %>> -

Welcome to <%= name %> component!

+

Welcome to <%= name %> component!

<% if (routing) { %>
  • <%= name %> root
  • diff --git a/packages/react/src/schematics/component/schema.d.ts b/packages/react/src/schematics/component/schema.d.ts index 6aa1bcc735626..2da269a0ef7aa 100644 --- a/packages/react/src/schematics/component/schema.d.ts +++ b/packages/react/src/schematics/component/schema.d.ts @@ -3,6 +3,7 @@ export interface Schema { project: string; style?: string; skipTests?: boolean; + directory?: string; export?: boolean; pascalCaseFiles?: boolean; classComponent?: boolean; diff --git a/packages/react/src/schematics/component/schema.json b/packages/react/src/schematics/component/schema.json index d43b884e6e0eb..f10126343fd16 100644 --- a/packages/react/src/schematics/component/schema.json +++ b/packages/react/src/schematics/component/schema.json @@ -7,6 +7,7 @@ "project": { "type": "string", "description": "The name of the project.", + "alias": "p", "$default": { "$source": "projectName" }, @@ -24,6 +25,7 @@ "style": { "description": "The file extension to be used for style files.", "type": "string", + "alias": "s", "default": "css", "x-prompt": { "message": "Which stylesheet format would you like to use?", @@ -58,25 +60,33 @@ "description": "When true, does not create \"spec.ts\" test files for the new component.", "default": false }, + "directory": { + "type": "string", + "description": "Create the component under this directory (can be nested).", + "alias": "d" + }, "export": { "type": "boolean", - "default": false, "description": "When true, the component is exported from the project index.ts (if it exists).", + "alias": "e", + "default": false, "x-prompt": "Should this component be exported in the project?" }, "pascalCaseFiles": { "type": "boolean", - "description": "Use pascal case component file name (e.g. App.tsx)", + "description": "Use pascal case component file name (e.g. App.tsx).", + "alias": "P", "default": false }, "classComponent": { "type": "boolean", - "description": "Use class components instead of functional component", + "alias": "C", + "description": "Use class components instead of functional component.", "default": false }, "routing": { "type": "boolean", - "description": "Generate library with routes" + "description": "Generate a library with routes." } }, "required": ["name", "project"] diff --git a/packages/react/src/schematics/library/library.spec.ts b/packages/react/src/schematics/library/library.spec.ts index 27cf04c961321..364f631ee4c33 100644 --- a/packages/react/src/schematics/library/library.spec.ts +++ b/packages/react/src/schematics/library/library.spec.ts @@ -93,11 +93,9 @@ describe('lib', () => { const tree = await runSchematic('lib', { name: 'myLib' }, appTree); expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy(); - expect(tree.exists('libs/my-lib/src/lib/my-lib/my-lib.tsx')).toBeTruthy(); - expect(tree.exists('libs/my-lib/src/lib/my-lib/my-lib.css')).toBeTruthy(); - expect( - tree.exists('libs/my-lib/src/lib/my-lib/my-lib.spec.tsx') - ).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.tsx')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.css')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.tsx')).toBeTruthy(); }); }); @@ -154,19 +152,13 @@ describe('lib', () => { expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy(); expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy(); expect( - tree.exists( - 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.tsx' - ) + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.tsx') ).toBeTruthy(); expect( - tree.exists( - 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.css' - ) + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.css') ).toBeTruthy(); expect( - tree.exists( - 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.spec.tsx' - ) + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.tsx') ).toBeTruthy(); }); @@ -241,7 +233,7 @@ describe('lib', () => { appTree ); - expect(result.exists('libs/my-lib/src/lib/my-lib/my-lib.scss')); + expect(result.exists('libs/my-lib/src/lib/my-lib.scss')).toBeTruthy(); }); }); diff --git a/packages/tao/src/commands/generate.ts b/packages/tao/src/commands/generate.ts index 250bdda068cf9..31ec947e43ab0 100644 --- a/packages/tao/src/commands/generate.ts +++ b/packages/tao/src/commands/generate.ts @@ -1,25 +1,26 @@ import { + coerceTypes, + convertAliases, convertToCamelCase, handleErrors, - Schema, - coerceTypes + Schema } from '../shared/params'; import { + experimental, JsonObject, logging, normalize, schema, tags, terminal, - virtualFs, - experimental + virtualFs } from '@angular-devkit/core'; import { DryRunEvent, HostTree, Schematic } from '@angular-devkit/schematics'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { NodeWorkflow } from '@angular-devkit/schematics/tools'; import * as inquirer from 'inquirer'; import { logger } from '../shared/logger'; -import { printHelp, commandName } from '../shared/print-help'; +import { commandName, printHelp } from '../shared/print-help'; import * as fs from 'fs'; import minimist = require('minimist'); @@ -281,8 +282,8 @@ async function runSchematic( ); const record = { loggingQueue: [] as string[], error: false }; workflow.reporter.subscribe(createRecorder(record, logger)); - const schematicOptions = coerceTypes( - opts.schematicOptions, + const schematicOptions = convertAliases( + coerceTypes(opts.schematicOptions, flattenedSchema as any), flattenedSchema as any ); await workflow diff --git a/packages/tao/src/shared/params.spec.ts b/packages/tao/src/shared/params.spec.ts index 63cb76a6f7c58..3382eeab9a906 100644 --- a/packages/tao/src/shared/params.spec.ts +++ b/packages/tao/src/shared/params.spec.ts @@ -1,4 +1,4 @@ -import { convertToCamelCase } from './params'; +import { convertToCamelCase, convertAliases } from './params'; describe('params', () => { describe('convertToCamelCase', () => { @@ -32,4 +32,32 @@ describe('params', () => { }); }); }); + + describe('convertAliases', () => { + it('should replace aliases with actual keys', () => { + expect( + convertAliases( + { d: 'test' }, + { + properties: { directory: { type: 'string', alias: 'd' } }, + required: [], + description: '' + } + ) + ).toEqual({ directory: 'test' }); + }); + + it('should filter out unknown keys without alias', () => { + expect( + convertAliases( + { d: 'test' }, + { + properties: { directory: { type: 'string' } }, + required: [], + description: '' + } + ) + ).toEqual({}); + }); + }); }); diff --git a/packages/tao/src/shared/params.ts b/packages/tao/src/shared/params.ts index 8a30a61b1fd7c..14bacb5c4ac05 100644 --- a/packages/tao/src/shared/params.ts +++ b/packages/tao/src/shared/params.ts @@ -48,3 +48,19 @@ export function coerceTypes(opts: { [k: string]: any }, schema: Schema) { }); return opts; } + +export function convertAliases(opts: { [k: string]: any }, schema: Schema) { + return Object.keys(opts).reduce((acc, k) => { + if (schema.properties[k]) { + acc[k] = opts[k]; + } else { + const found = Object.entries(schema.properties).find( + ([_, d]) => d.alias === k + ); + if (found) { + acc[found[0]] = opts[k]; + } + } + return acc; + }, {}); +}