From 0923dab3180da59190c902d73a7edd320f1d269d Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 15 May 2019 13:02:43 -0400 Subject: [PATCH] feat(react): adds and updates React schematics with more options - Added component schematic that adds to existing project. * Supports CSS-in-JS styles, functional components, etc. - Lib and app schematics now support new style, funtional components options (same as component) --- docs/api-react/schematics/application.md | 16 ++ docs/api-react/schematics/component.md | 64 ++++++++ docs/api-react/schematics/library.md | 8 + packages/react/collection.json | 6 + .../application/application.spec.ts | 135 +++++++++++++--- .../src/schematics/application/application.ts | 129 +++++++++------ .../files/app/src/app}/__fileName__.__style__ | 0 ...__tmpl__ => __fileName__.spec.tsx__tmpl__} | 4 +- .../app/src/app/__fileName__.tsx__tmpl__ | 79 ++++++++++ .../files/app/src/app/app.tsx__tmpl__ | 32 ---- .../application/files/app/src/index.html | 2 +- .../files/app/src/main.tsx__tmpl__ | 8 +- .../application/files/app/tsconfig.json | 2 + .../src/schematics/application/schema.d.ts | 4 +- .../src/schematics/application/schema.json | 24 ++- .../schematics/component/component.spec.ts | 149 ++++++++++++++++++ .../src/schematics/component/component.ts | 127 +++++++++++++++ .../files/__name__/__fileName__.__style__} | 0 .../__name__/__fileName__.spec.tsx__tmpl__ | 13 ++ .../files/__name__/__fileName__.tsx__tmpl__ | 45 ++++++ .../src/schematics/component/schema.d.ts | 9 ++ .../src/schematics/component/schema.json | 79 ++++++++++ .../library/files/lib/src/index.ts__tmpl__ | 1 - .../lib/src/lib/__fileName__.spec.tsx__tmpl__ | 13 -- .../lib/src/lib/__fileName__.tsx__tmpl__ | 12 -- .../library/files/lib/tsconfig.json | 2 + .../src/schematics/library/library.spec.ts | 48 +++--- .../react/src/schematics/library/library.ts | 51 +++--- .../react/src/schematics/library/schema.d.ts | 3 +- .../react/src/schematics/library/schema.json | 19 ++- packages/react/src/utils/dependencies.ts | 8 + packages/react/src/utils/styled.ts | 26 +++ packages/react/src/utils/versions.ts | 3 + 33 files changed, 926 insertions(+), 195 deletions(-) create mode 100644 docs/api-react/schematics/component.md rename packages/react/src/schematics/{library/files/lib/src/lib => application/files/app/src/app}/__fileName__.__style__ (100%) rename packages/react/src/schematics/application/files/app/src/app/{app.spec.tsx__tmpl__ => __fileName__.spec.tsx__tmpl__} (87%) create mode 100644 packages/react/src/schematics/application/files/app/src/app/__fileName__.tsx__tmpl__ delete mode 100644 packages/react/src/schematics/application/files/app/src/app/app.tsx__tmpl__ create mode 100644 packages/react/src/schematics/component/component.spec.ts create mode 100644 packages/react/src/schematics/component/component.ts rename packages/react/src/schematics/{application/files/app/src/app/app.__style__ => component/files/__name__/__fileName__.__style__} (100%) create mode 100644 packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ create mode 100644 packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ create mode 100644 packages/react/src/schematics/component/schema.d.ts create mode 100644 packages/react/src/schematics/component/schema.json delete mode 100644 packages/react/src/schematics/library/files/lib/src/lib/__fileName__.spec.tsx__tmpl__ delete mode 100644 packages/react/src/schematics/library/files/lib/src/lib/__fileName__.tsx__tmpl__ create mode 100644 packages/react/src/utils/dependencies.ts create mode 100644 packages/react/src/utils/styled.ts diff --git a/docs/api-react/schematics/application.md b/docs/api-react/schematics/application.md index 61837736199d8..1f9f6dbaeab06 100644 --- a/docs/api-react/schematics/application.md +++ b/docs/api-react/schematics/application.md @@ -11,6 +11,14 @@ ng generate application ... ## Options +### classComponent + +Default: `false` + +Type: `boolean` + +Use class components instead of functional component + ### directory Type: `string` @@ -31,6 +39,14 @@ Type: `string` The name of the application. +### pascalCaseFiles + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx)® + ### skipFormat Default: `false` diff --git a/docs/api-react/schematics/component.md b/docs/api-react/schematics/component.md new file mode 100644 index 0000000000000..4b380c0d1c7db --- /dev/null +++ b/docs/api-react/schematics/component.md @@ -0,0 +1,64 @@ +# component + +Create a component + +## Usage + +```bash +ng generate component ... + +``` + +## Options + +### classComponent + +Default: `false` + +Type: `boolean` + +Use class components instead of functional component + +### export + +Default: `false` + +Type: `boolean` + +When true, the component is exported from the project index.ts (if it exists). + +### name + +Type: `string` + +The name of the component. + +### pascalCaseFiles + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx) + +### project + +Type: `string` + +The name of the project (as specified in angular.json). + +### skipTests + +Default: `false` + +Type: `boolean` + +When true, does not create "spec.ts" test files for the new component. + +### style + +Default: `css` + +Type: `string` + +The file extension to be used for style files. diff --git a/docs/api-react/schematics/library.md b/docs/api-react/schematics/library.md index 97a38804846a3..5acc0c0787f96 100644 --- a/docs/api-react/schematics/library.md +++ b/docs/api-react/schematics/library.md @@ -23,6 +23,14 @@ Type: `string` Library name +### pascalCaseFiles + +Default: `false` + +Type: `boolean` + +Use pascal case component file name (e.g. App.tsx)® + ### skipFormat Default: `false` diff --git a/packages/react/collection.json b/packages/react/collection.json index 6eff550e2a27b..665363abeb992 100644 --- a/packages/react/collection.json +++ b/packages/react/collection.json @@ -22,6 +22,12 @@ "schema": "./src/schematics/library/schema.json", "aliases": ["lib"], "description": "Create a library" + }, + + "component": { + "factory": "./src/schematics/component/component", + "schema": "./src/schematics/component/schema.json", + "description": "Create a component" } } } diff --git a/packages/react/src/schematics/application/application.spec.ts b/packages/react/src/schematics/application/application.spec.ts index 7d1ad41d22f78..c13157627a472 100644 --- a/packages/react/src/schematics/application/application.spec.ts +++ b/packages/react/src/schematics/application/application.spec.ts @@ -192,7 +192,7 @@ describe('app', () => { const tree = await runSchematic( 'app', { - name: 'my-App' + name: 'my-app' }, appTree ); @@ -206,7 +206,7 @@ describe('app', () => { const tree = await runSchematic( 'app', { - name: 'my-App' + name: 'my-app' }, appTree ); @@ -220,7 +220,7 @@ describe('app', () => { const tree = await runSchematic( 'app', { - name: 'my-App' + name: 'my-app' }, appTree ); @@ -265,7 +265,7 @@ describe('app', () => { const tree = await runSchematic( 'app', { - name: 'my-App' + name: 'my-app' }, appTree ); @@ -284,7 +284,7 @@ describe('app', () => { const tree = await runSchematic( 'app', { - name: 'my-App' + name: 'my-app' }, appTree ); @@ -301,20 +301,6 @@ describe('app', () => { }); }); - describe('--prefix', () => { - it('should use the prefix in the index.html', async () => { - const tree = await runSchematic( - 'app', - { name: 'myApp', prefix: 'prefix' }, - appTree - ); - - expect(tree.readContent('apps/my-app/src/index.html')).toContain( - '' - ); - }); - }); - describe('--unit-test-runner none', () => { it('should not generate test configuration', async () => { const tree = await runSchematic( @@ -345,4 +331,115 @@ describe('app', () => { expect(angularJson.projects['my-app-e2e']).toBeUndefined(); }); }); + + describe('--pascalCaseFiles', () => { + it('should use upper case app file', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', pascalCaseFiles: true }, + appTree + ); + + expect(tree.exists('apps/my-app/src/app/App.tsx')).toBeTruthy(); + expect(tree.exists('apps/my-app/src/app/App.spec.tsx')).toBeTruthy(); + expect(tree.exists('apps/my-app/src/app/App.css')).toBeTruthy(); + }); + }); + + it('should generate functional components by default', async () => { + const tree = await runSchematic('app', { name: 'myApp' }, appTree); + + const appContent = tree.read('apps/my-app/src/app/app.tsx').toString(); + + expect(appContent).not.toMatch(/extends Component/); + }); + + describe('--class-component', () => { + it('should generate class components', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', classComponent: true }, + appTree + ); + + const appContent = tree.read('apps/my-app/src/app/app.tsx').toString(); + + expect(appContent).toMatch(/extends Component/); + }); + }); + + describe('--style styled-components', () => { + it('should use styled-components as the styled API library', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', style: 'styled-components' }, + appTree + ); + + expect( + tree.exists('apps/my-app/src/app/app.styled-components') + ).toBeFalsy(); + expect(tree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); + + const content = tree.read('apps/my-app/src/app/app.tsx').toString(); + expect(content).toContain('styled-component'); + expect(content).toContain(''); + }); + + it('should add dependencies to package.json', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', style: 'styled-components' }, + appTree + ); + + const packageJSON = readJsonInTree(tree, 'package.json'); + expect(packageJSON.dependencies['styled-components']).toBeDefined(); + }); + }); + + describe('--style @emotion/styled', () => { + it('should use @emotion/styled as the styled API library', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', style: '@emotion/styled' }, + appTree + ); + + expect( + tree.exists('apps/my-app/src/app/app.@emotion/styled') + ).toBeFalsy(); + expect(tree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy(); + + const content = tree.read('apps/my-app/src/app/app.tsx').toString(); + expect(content).toContain('@emotion/styled'); + expect(content).toContain(''); + }); + + it('should exclude styles from angular.json', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', style: '@emotion/styled' }, + appTree + ); + + const angularJSON = readJsonInTree(tree, 'angular.json'); + + expect( + angularJSON.projects['my-app'].architect.build.options.styles + ).toEqual([]); + }); + + it('should add dependencies to package.json', async () => { + const tree = await runSchematic( + 'app', + { name: 'myApp', style: '@emotion/styled' }, + appTree + ); + + const packageJSON = readJsonInTree(tree, 'package.json'); + expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); + expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); + }); + }); }); diff --git a/packages/react/src/schematics/application/application.ts b/packages/react/src/schematics/application/application.ts index b31fb5fb3a785..02d6ea28a0323 100644 --- a/packages/react/src/schematics/application/application.ts +++ b/packages/react/src/schematics/application/application.ts @@ -1,28 +1,29 @@ import { join, normalize } from '@angular-devkit/core'; import { + apply, chain, - Rule, - Tree, + externalSchematic, + filter, mergeWith, - apply, - template, move, - url, - externalSchematic, noop, - filter + Rule, + template, + Tree, + url } from '@angular-devkit/schematics'; import { Schema } from './schema'; import { - updateJsonInTree, - NxJson, - toFileName, + formatFiles, names, + NxJson, offsetFromRoot, - getNpmScope, - formatFiles + toFileName, + updateJsonInTree } from '@nrwl/workspace'; import ngAdd from '../ng-add/ng-add'; +import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled'; +import { addDepsToPackageJson } from '@nrwl/workspace/src/utils/ast-utils'; interface NormalizedSchema extends Schema { projectName: string; @@ -30,19 +31,57 @@ interface NormalizedSchema extends Schema { e2eProjectName: string; e2eProjectRoot: string; parsedTags: string[]; + fileName: string; + styledModule: null | string; +} + +export default function(schema: Schema): Rule { + return (host: Tree) => { + const options = normalizeOptions(host, schema); + + return chain([ + ngAdd({ + skipFormat: true + }), + 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(), + addStyledModuleDependencies(options), + formatFiles(options) + ]); + }; } function createApplicationFiles(options: NormalizedSchema): Rule { return mergeWith( apply(url(`./files/app`), [ template({ - ...options, ...names(options.name), + ...options, tmpl: '', offsetFromRoot: offsetFromRoot(options.appProjectRoot) }), + options.styledModule + ? filter(file => !file.endsWith(`.${options.style}`)) + : noop(), options.unitTestRunner === 'none' - ? filter(file => file !== '/src/app/app.spec.tsx') + ? filter(file => file !== `/src/app/${options.fileName}.spec.tsx`) : noop(), move(options.appProjectRoot) ]) @@ -65,16 +104,21 @@ function addProject(options: NormalizedSchema): Rule { options: { outputPath: join(normalize('dist'), options.appProjectRoot), index: join(normalize(options.appProjectRoot), 'src/index.html'), - main: join(normalize(options.appProjectRoot), 'src/main.tsx'), + main: join(normalize(options.appProjectRoot), `src/main.tsx`), polyfills: join(normalize(options.appProjectRoot), 'src/polyfills.ts'), tsConfig: join(normalize(options.appProjectRoot), 'tsconfig.app.json'), assets: [ join(normalize(options.appProjectRoot), 'src/favicon.ico'), join(normalize(options.appProjectRoot), 'src/assets') ], - styles: [ - join(normalize(options.appProjectRoot), `src/styles.${options.style}`) - ], + styles: options.styledModule + ? [] + : [ + join( + normalize(options.appProjectRoot), + `src/styles.${options.style}` + ) + ], scripts: [] }, configurations: { @@ -142,36 +186,15 @@ function addProject(options: NormalizedSchema): Rule { }); } -export default function(schema: Schema): Rule { - return (host: Tree) => { - const options = normalizeOptions(host, schema); +function addStyledModuleDependencies(options: NormalizedSchema): Rule { + const extraDependencies = CSS_IN_JS_DEPENDENCIES[options.styledModule]; - return chain([ - ngAdd({ - skipFormat: true - }), - 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(), - formatFiles(options) - ]); - }; + return extraDependencies + ? addDepsToPackageJson( + extraDependencies.dependencies, + extraDependencies.devDependencies + ) + : noop(); } function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { @@ -189,15 +212,21 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { ? options.tags.split(',').map(s => s.trim()) : []; - const defaultPrefix = getNpmScope(host); + const fileName = options.pascalCaseFiles ? 'App' : 'app'; + + const styledModule = /^(css|scss|less|styl)$/.test(options.style) + ? null + : options.style; + return { ...options, - prefix: options.prefix ? options.prefix : defaultPrefix, name: toFileName(options.name), projectName: appProjectName, appProjectRoot, e2eProjectRoot, e2eProjectName, - parsedTags + parsedTags, + fileName, + styledModule }; } diff --git a/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.__style__ b/packages/react/src/schematics/application/files/app/src/app/__fileName__.__style__ similarity index 100% rename from packages/react/src/schematics/library/files/lib/src/lib/__fileName__.__style__ rename to packages/react/src/schematics/application/files/app/src/app/__fileName__.__style__ diff --git a/packages/react/src/schematics/application/files/app/src/app/app.spec.tsx__tmpl__ b/packages/react/src/schematics/application/files/app/src/app/__fileName__.spec.tsx__tmpl__ similarity index 87% rename from packages/react/src/schematics/application/files/app/src/app/app.spec.tsx__tmpl__ rename to packages/react/src/schematics/application/files/app/src/app/__fileName__.spec.tsx__tmpl__ index 99b664d19ebc1..ca2ec879994fa 100644 --- a/packages/react/src/schematics/application/files/app/src/app/app.spec.tsx__tmpl__ +++ b/packages/react/src/schematics/application/files/app/src/app/__fileName__.spec.tsx__tmpl__ @@ -1,7 +1,7 @@ -import * as React from 'react'; +import React from 'react'; import { render, cleanup } from 'react-testing-library'; -import { App } from './app'; +import App from './app'; describe('App', () => { afterEach(cleanup); diff --git a/packages/react/src/schematics/application/files/app/src/app/__fileName__.tsx__tmpl__ b/packages/react/src/schematics/application/files/app/src/app/__fileName__.tsx__tmpl__ new file mode 100644 index 0000000000000..0a8fdda197ab2 --- /dev/null +++ b/packages/react/src/schematics/application/files/app/src/app/__fileName__.tsx__tmpl__ @@ -0,0 +1,79 @@ +<% if (classComponent) { %> +import React, { Component } from 'react'; +<% } else { %> +import React from 'react'; +<% } %> +<% if (styledModule) { + var wrapper = 'StyledApp'; + var header = 'Header'; + var headerAttributes = ''; +%>import styled from '<%= styledModule %>';<% } else { + var wrapper = 'div'; + var header = 'header'; + var headerAttributes = " styles={{ textAlign: 'center'}}"; +%>import './app.<%= style %>';<% } %> + +<% if (styledModule) { %> +const StyledApp = styled.div` +`; + +const Header = styled.header` + text-align: center; +`; +<% }%> + +<% if (classComponent) { %> +export class App extends Component { + render() { + return ( + <<%= wrapper %>> + <<%= header %><%= headerAttributes %>> +

Welcome to <%= projectName %>!

+ + > +

This is a React app built with Nx.

+

🔎 **Nx is a set of Angular CLI power-ups for modern development.**

+

Quick Start & Documentation

+ + > + ); + } +} +<% } else { %> +export const App = () => { + return ( + <<%= wrapper %>> + <<%= header %><%= headerAttributes %>> +

Welcome to <%= projectName %>!

+ + > +

This is a React app built with Nx.

+

🔎 **Nx is a set of Angular CLI power-ups for modern development.**

+

Quick Start & Documentation

+ + > + ); +}; +<% } %> + +export default App; diff --git a/packages/react/src/schematics/application/files/app/src/app/app.tsx__tmpl__ b/packages/react/src/schematics/application/files/app/src/app/app.tsx__tmpl__ deleted file mode 100644 index 0561d586529db..0000000000000 --- a/packages/react/src/schematics/application/files/app/src/app/app.tsx__tmpl__ +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; - -import './app.<%= style %>'; - -export class App extends Component { - render() { - const title = '<%= projectName %>'; - return ( -
-
-

Welcome to {title}!

- -
-

This is a React app built with Nx.

-

🔎 **Nx is a set of Angular CLI power-ups for modern development.**

-

Quick Start & Documentation

- -
- ); - } -} diff --git a/packages/react/src/schematics/application/files/app/src/index.html b/packages/react/src/schematics/application/files/app/src/index.html index 42ece406e8b8a..85edca9f5dbfe 100644 --- a/packages/react/src/schematics/application/files/app/src/index.html +++ b/packages/react/src/schematics/application/files/app/src/index.html @@ -9,6 +9,6 @@ - <<%= prefix %>-root>-root> +
diff --git a/packages/react/src/schematics/application/files/app/src/main.tsx__tmpl__ b/packages/react/src/schematics/application/files/app/src/main.tsx__tmpl__ index 1503fa1be9c4e..76f479848c893 100644 --- a/packages/react/src/schematics/application/files/app/src/main.tsx__tmpl__ +++ b/packages/react/src/schematics/application/files/app/src/main.tsx__tmpl__ @@ -1,6 +1,6 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import React from 'react'; +import ReactDOM from 'react-dom'; -import { App } from './app/app'; +import App from './app/app'; -ReactDOM.render(, document.querySelector('<%= prefix %>-root')); +ReactDOM.render(, document.getElementById('root')); diff --git a/packages/react/src/schematics/application/files/app/tsconfig.json b/packages/react/src/schematics/application/files/app/tsconfig.json index d6f394e13d7a4..7901092a7351a 100644 --- a/packages/react/src/schematics/application/files/app/tsconfig.json +++ b/packages/react/src/schematics/application/files/app/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "jsx": "react", "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "target": "es2015", "types": [] }, diff --git a/packages/react/src/schematics/application/schema.d.ts b/packages/react/src/schematics/application/schema.d.ts index 86a55a3ac5bdc..bc71f921fc51e 100644 --- a/packages/react/src/schematics/application/schema.d.ts +++ b/packages/react/src/schematics/application/schema.d.ts @@ -1,13 +1,13 @@ -import { Framework } from './.../utils/frameworks'; import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; export interface Schema { name: string; - prefix?: string; style?: string; skipFormat: boolean; directory?: string; tags?: string; unitTestRunner: UnitTestRunner; e2eTestRunner: E2eTestRunner; + pascalCaseFiles?: boolean; + classComponent?: boolean; } diff --git a/packages/react/src/schematics/application/schema.json b/packages/react/src/schematics/application/schema.json index 78470aabc3ca1..4e0c46b708930 100644 --- a/packages/react/src/schematics/application/schema.json +++ b/packages/react/src/schematics/application/schema.json @@ -29,15 +29,23 @@ { "value": "css", "label": "CSS" }, { "value": "scss", - "label": "SASS(.scss) [ http://sass-lang.com ]" + "label": "SASS(.scss) [ http://sass-lang.com ]" }, { "value": "styl", - "label": "Stylus(.styl)[ http://stylus-lang.com ]" + "label": "Stylus(.styl) [ http://stylus-lang.com ]" }, { "value": "less", - "label": "LESS [ http://lesscss.org ]" + "label": "LESS [ http://lesscss.org ]" + }, + { + "value": "styled-components", + "label": "styled-components [ https://styled-components.com ]" + }, + { + "value": "@emotion/styled", + "label": "emotion [ https://emotion.sh ]" } ] } @@ -63,6 +71,16 @@ "type": "string", "description": "Add tags to the application (used for linting)", "x-prompt": "Which tags would you like to add to the application? (used for linting)" + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx)®", + "default": false + }, + "classComponent": { + "type": "boolean", + "description": "Use class components instead of functional component", + "default": false } }, "required": [] diff --git a/packages/react/src/schematics/component/component.spec.ts b/packages/react/src/schematics/component/component.spec.ts new file mode 100644 index 0000000000000..0e11e11739ec7 --- /dev/null +++ b/packages/react/src/schematics/component/component.spec.ts @@ -0,0 +1,149 @@ +import { Tree } from '@angular-devkit/schematics'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { runSchematic } from '../../utils/testing'; +import { names } from '@nrwl/workspace/src/utils/name-utils'; +import { readJsonInTree } from '@nrwl/workspace/src/utils/ast-utils'; + +describe('component', () => { + let appTree: Tree; + let projectName: string; + + beforeEach(() => { + projectName = 'my-lib'; + appTree = Tree.empty(); + appTree = createEmptyWorkspace(appTree); + appTree = createLib(appTree, projectName); + }); + + it('should generate files', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName }, + 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(); + }); + + describe('--export', () => { + it('should add to index.ts barrel', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, export: true }, + appTree + ); + + const indexContent = tree.read('libs/my-lib/src/index.ts').toString(); + + expect(indexContent).toMatch(/lib\/hello\/hello/); + }); + }); + + describe('--pascalCaseFiles', () => { + it('should generate component files with upper case names', async () => { + const tree = await runSchematic( + '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(); + }); + }); + + describe('--style styled-components', () => { + it('should use styled-components as the styled API library', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, style: 'styled-components' }, + appTree + ); + + expect( + tree.exists('libs/my-lib/src/lib/hello/hello.styled-components') + ).toBeFalsy(); + expect(tree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + + const content = tree + .read('libs/my-lib/src/lib/hello/hello.tsx') + .toString(); + expect(content).toContain('styled-components'); + expect(content).toContain(''); + }); + + it('should add dependencies to package.json', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, style: 'styled-components' }, + appTree + ); + + const packageJSON = readJsonInTree(tree, 'package.json'); + expect(packageJSON.dependencies['styled-components']).toBeDefined(); + }); + }); + + describe('--style @emotion/styled', () => { + it('should use @emotion/styled as the styled API library', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, style: '@emotion/styled' }, + appTree + ); + + expect( + tree.exists('libs/my-lib/src/lib/hello/hello.@emotion/styled') + ).toBeFalsy(); + expect(tree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy(); + + const content = tree + .read('libs/my-lib/src/lib/hello/hello.tsx') + .toString(); + expect(content).toContain('@emotion/styled'); + expect(content).toContain(''); + }); + + it('should add dependencies to package.json', async () => { + const tree = await runSchematic( + 'component', + { name: 'hello', project: projectName, style: '@emotion/styled' }, + appTree + ); + + const packageJSON = readJsonInTree(tree, 'package.json'); + expect(packageJSON.dependencies['@emotion/styled']).toBeDefined(); + expect(packageJSON.dependencies['@emotion/core']).toBeDefined(); + }); + }); +}); + +export function createLib(tree: Tree, libName: string): Tree { + const { fileName } = names(libName); + + tree.create(`/libs/${fileName}/src/index.ts`, `\n`); + + tree.overwrite( + '/angular.json', + ` +{ + "projects": { + "${libName}": { + "root": "libs/${fileName}", + "sourceRoot": "libs/${fileName}/src", + "projectType": "library", + "schematics": {} + } + } +} +` + ); + + return tree; +} diff --git a/packages/react/src/schematics/component/component.ts b/packages/react/src/schematics/component/component.ts new file mode 100644 index 0000000000000..7a85fdff03002 --- /dev/null +++ b/packages/react/src/schematics/component/component.ts @@ -0,0 +1,127 @@ +import { join, Path } from '@angular-devkit/core'; +import * as ts from 'typescript'; +import { + apply, + chain, + filter, + mergeWith, + move, + noop, + Rule, + template, + Tree, + url +} from '@angular-devkit/schematics'; +import { Schema } from './schema'; +import { names } from '@nrwl/workspace'; +import { + addDepsToPackageJson, + addGlobal, + getProjectConfig, + insert +} from '@nrwl/workspace/src/utils/ast-utils'; +import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled'; +import { formatFiles } from '@nrwl/workspace'; + +interface NormalizedSchema extends Schema { + projectSourceRoot: Path; + fileName: string; + className: string; + styledModule: null | string; +} + +export default function(schema: Schema): Rule { + return (host: Tree) => { + const options = normalizeOptions(host, schema); + return chain([ + createComponentFiles(options), + addStyledModuleDependencies(options), + addExportsToBarrel(options), + formatFiles({ skipFormat: false }) + ]); + }; +} + +function createComponentFiles(options: NormalizedSchema): Rule { + return mergeWith( + apply(url(`./files`), [ + template({ + ...options, + tmpl: '' + }), + options.skipTests ? filter(file => !/.*spec.tsx/.test(file)) : noop(), + options.styledModule + ? filter(file => !file.endsWith(`.${options.style}`)) + : noop(), + move(join(options.projectSourceRoot, 'lib')) + ]) + ); +} + +function addStyledModuleDependencies(options: NormalizedSchema): Rule { + const extraDependencies = CSS_IN_JS_DEPENDENCIES[options.styledModule]; + + return extraDependencies + ? addDepsToPackageJson( + extraDependencies.dependencies, + extraDependencies.devDependencies + ) + : noop(); +} + +function addExportsToBarrel(options: NormalizedSchema): Rule { + return options.export + ? (host: Tree) => { + const indexFilePath = join(options.projectSourceRoot, 'index.ts'); + const buffer = host.read(indexFilePath); + + if (!!buffer) { + const indexSource = buffer!.toString('utf-8'); + const indexSourceFile = ts.createSourceFile( + indexFilePath, + indexSource, + ts.ScriptTarget.Latest, + true + ); + + insert( + host, + indexFilePath, + addGlobal( + indexSourceFile, + indexFilePath, + `export { default as ${options.className}, ${ + options.className + }Props } from './lib/${options.name}/${options.fileName}';` + ) + ); + } + + return host; + } + : noop(); +} + +function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { + const { className, fileName } = names(options.name); + + const componentFileName = options.pascalCaseFiles ? className : fileName; + + const { sourceRoot: projectSourceRoot } = getProjectConfig( + host, + options.project + ); + + const styledModule = /^(css|scss|less|styl)$/.test(options.style) + ? null + : options.style; + + return { + ...options, + styledModule, + className, + name: fileName, + fileName: componentFileName, + projectSourceRoot + }; +} diff --git a/packages/react/src/schematics/application/files/app/src/app/app.__style__ b/packages/react/src/schematics/component/files/__name__/__fileName__.__style__ similarity index 100% rename from packages/react/src/schematics/application/files/app/src/app/app.__style__ rename to packages/react/src/schematics/component/files/__name__/__fileName__.__style__ diff --git a/packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ b/packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ new file mode 100644 index 0000000000000..8afedc72dd37c --- /dev/null +++ b/packages/react/src/schematics/component/files/__name__/__fileName__.spec.tsx__tmpl__ @@ -0,0 +1,13 @@ +import React from 'react'; +import { render, cleanup } from 'react-testing-library'; + +import <%= className %> from './<%= fileName %>'; + +describe(' <%= className %>', () => { + afterEach(cleanup); + + it('should render successfully', () => { + const { baseElement } = render(< <%= className %> />); + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ b/packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ new file mode 100644 index 0000000000000..6f66be474175d --- /dev/null +++ b/packages/react/src/schematics/component/files/__name__/__fileName__.tsx__tmpl__ @@ -0,0 +1,45 @@ +<% if (classComponent) { %> +import React, { Component } from 'react'; +<% } else { %> +import React from 'react'; +<% } %> +<% if (styledModule) { + var wrapper = 'Styled' + className; +%> +import styled from '<%= styledModule %>'; +<% } else { + var wrapper = 'div'; +%> +import './<%= fileName %>.<%= style %>'; +<% } %> + +/* tslint:disable:no-empty-interface */ +export interface <%= className %>Props { +} + +<% if (styledModule) { %> +const Styled<%= className %> = styled.div` + color: pink; +`; +<% }%> +<% if (classComponent) { %> +export class <%= className %> extends Component<<%= className %>Props> { + render() { + return ( + <<%= wrapper %>> + Welcome to <%= name %> component! + > + ); + } +} +<% } else { %> +export const <%= className %> = (props: <%= className %>Props) => { + return ( + <<%= wrapper %>> + Welcome to <%= name %> component! + > + ); +}; +<% } %> + +export default <%= className %>; diff --git a/packages/react/src/schematics/component/schema.d.ts b/packages/react/src/schematics/component/schema.d.ts new file mode 100644 index 0000000000000..8bfeb6f9e77ef --- /dev/null +++ b/packages/react/src/schematics/component/schema.d.ts @@ -0,0 +1,9 @@ +export interface Schema { + name: string; + project: string; + style?: string; + skipTests?: boolean; + export?: boolean; + pascalCaseFiles?: boolean; + classComponent?: boolean; +} diff --git a/packages/react/src/schematics/component/schema.json b/packages/react/src/schematics/component/schema.json new file mode 100644 index 0000000000000..3cd49b449c374 --- /dev/null +++ b/packages/react/src/schematics/component/schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "NxReactApp", + "title": "Create a React Application for Nx", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project (as specified in angular.json).", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the project for ths component?" + }, + "name": { + "type": "string", + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the component?" + }, + "style": { + "description": "The file extension to be used for style files.", + "type": "string", + "default": "css", + "x-prompt": { + "message": "Which stylesheet format would you like to use?", + "type": "list", + "items": [ + { "value": "css", "label": "CSS" }, + { + "value": "scss", + "label": "SASS(.scss) [ http://sass-lang.com ]" + }, + { + "value": "styl", + "label": "Stylus(.styl) [ http://stylus-lang.com ]" + }, + { + "value": "less", + "label": "LESS [ http://lesscss.org ]" + }, + { + "value": "styled-components", + "label": "styled-components [ https://styled-components.com ]" + }, + { + "value": "@emotion/styled", + "label": "emotion [ https://emotion.sh ]" + } + ] + } + }, + "skipTests": { + "type": "boolean", + "description": "When true, does not create \"spec.ts\" test files for the new component.", + "default": false + }, + "export": { + "type": "boolean", + "default": false, + "description": "When true, the component is exported from the project index.ts (if it exists).", + "x-prompt": "Should this component be exported in the project?" + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx)", + "default": false + }, + "classComponent": { + "type": "boolean", + "description": "Use class components instead of functional component", + "default": false + } + }, + "required": ["name", "project"] +} diff --git a/packages/react/src/schematics/library/files/lib/src/index.ts__tmpl__ b/packages/react/src/schematics/library/files/lib/src/index.ts__tmpl__ index 4c90909a8d5f2..e69de29bb2d1d 100644 --- a/packages/react/src/schematics/library/files/lib/src/index.ts__tmpl__ +++ b/packages/react/src/schematics/library/files/lib/src/index.ts__tmpl__ @@ -1 +0,0 @@ -export { <%= className %> } from './lib/<%= fileName %>'; diff --git a/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.spec.tsx__tmpl__ b/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.spec.tsx__tmpl__ deleted file mode 100644 index e607b38d180e6..0000000000000 --- a/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.spec.tsx__tmpl__ +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; -import { render, cleanup } from 'react-testing-library'; - -import { <%= className %> } from './<%= fileName %>'; - -describe('<%= className %>', () => { - afterEach(cleanup); - - it('should render successfully', () => { - const { getByText } = render(<<%= className %> />); - expect(getByText('<%= name %> works!')).toBeTruthy(); - }); -}); diff --git a/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.tsx__tmpl__ b/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.tsx__tmpl__ deleted file mode 100644 index 1f39a481b677b..0000000000000 --- a/packages/react/src/schematics/library/files/lib/src/lib/__fileName__.tsx__tmpl__ +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; - -import './<%= fileName %>.<%= style %>'; - -export class <%= className %> extends Component { - render() { - const title = '<%= name %>'; - return (
<%= name %> works!
- ); - } -} diff --git a/packages/react/src/schematics/library/files/lib/tsconfig.json b/packages/react/src/schematics/library/files/lib/tsconfig.json index 07dbb2c7cf2d7..d4f8d7cb3d328 100644 --- a/packages/react/src/schematics/library/files/lib/tsconfig.json +++ b/packages/react/src/schematics/library/files/lib/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "jsx": "react", "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "types": [] }, "include": ["**/*.ts", "**/*.tsx"] diff --git a/packages/react/src/schematics/library/library.spec.ts b/packages/react/src/schematics/library/library.spec.ts index 2efb1cae91da3..adbf32c192e29 100644 --- a/packages/react/src/schematics/library/library.spec.ts +++ b/packages/react/src/schematics/library/library.spec.ts @@ -64,6 +64,8 @@ describe('lib', () => { compilerOptions: { allowJs: true, jsx: 'react', + allowSyntheticDefaultImports: true, + esModuleInterop: true, types: ['node', 'jest'] }, include: ['**/*.ts', '**/*.tsx'] @@ -92,20 +94,11 @@ 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.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(); - - expect(tree.readContent('libs/my-lib/src/lib/my-lib.tsx')).toContain( - '
my-lib works!
' - ); - expect(tree.readContent('libs/my-lib/src/lib/my-lib.tsx')).toContain( - 'export class MyLib extends Component {' - ); - - expect(tree.readContent('libs/my-lib/src/lib/my-lib.spec.tsx')).toContain( - `describe('MyLib', () => {` - ); + 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(); }); }); @@ -163,25 +156,20 @@ 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.tsx') + tree.exists( + 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.tsx' + ) ).toBeTruthy(); expect( - tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.css') + tree.exists( + 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.css' + ) ).toBeTruthy(); expect( - tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.tsx') + tree.exists( + 'libs/my-dir/my-lib/src/lib/my-dir-my-lib/my-dir-my-lib.spec.tsx' + ) ).toBeTruthy(); - - expect( - tree.readContent('libs/my-dir/my-lib/src/lib/my-dir-my-lib.tsx') - ).toContain('
my-dir-my-lib works!
'); - expect( - tree.readContent('libs/my-dir/my-lib/src/lib/my-dir-my-lib.tsx') - ).toContain('export class MyDirMyLib extends Component {'); - - expect( - tree.readContent('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.tsx') - ).toContain(`describe('MyDirMyLib', () => {`); }); it('should update angular.json', async () => { @@ -238,6 +226,8 @@ describe('lib', () => { compilerOptions: { allowJs: true, jsx: 'react', + allowSyntheticDefaultImports: true, + esModuleInterop: true, types: ['node', 'jest'] }, include: ['**/*.ts', '**/*.tsx'] @@ -253,7 +243,7 @@ describe('lib', () => { appTree ); - expect(result.exists('libs/my-lib/src/lib/my-lib.scss')); + expect(result.exists('libs/my-lib/src/lib/my-lib/my-lib.scss')); }); }); diff --git a/packages/react/src/schematics/library/library.ts b/packages/react/src/schematics/library/library.ts index d45661bcec7c8..af3480625e937 100644 --- a/packages/react/src/schematics/library/library.ts +++ b/packages/react/src/schematics/library/library.ts @@ -28,6 +28,35 @@ export interface NormalizedSchema extends Schema { parsedTags: string[]; } +export default function(schema: Schema): Rule { + return (host: Tree, context: SchematicContext) => { + const options = normalizeOptions(schema); + + return chain([ + createFiles(options), + !options.skipTsConfig ? updateTsConfig(options) : noop(), + addProject(options), + updateNxJson(options), + options.unitTestRunner !== 'none' + ? externalSchematic('@nrwl/jest', 'jest-project', { + project: options.name, + setupFile: 'none', + supportTsx: true, + skipSerializers: true + }) + : noop(), + externalSchematic('@nrwl/react', 'component', { + name: options.name, + project: options.name, + style: options.style, + skipTests: options.unitTestRunner === 'none', + export: true + }), + formatFiles(options) + ])(host, context); + }; +} + function addProject(options: NormalizedSchema): Rule { return updateJsonInTree('angular.json', json => { const architect: { [key: string]: any } = {}; @@ -88,28 +117,6 @@ function updateNxJson(options: NormalizedSchema): Rule { }); } -export default function(schema: Schema): Rule { - return (host: Tree, context: SchematicContext) => { - const options = normalizeOptions(schema); - - return chain([ - createFiles(options), - !options.skipTsConfig ? updateTsConfig(options) : noop(), - addProject(options), - updateNxJson(options), - options.unitTestRunner !== 'none' - ? externalSchematic('@nrwl/jest', 'jest-project', { - project: options.name, - setupFile: 'none', - supportTsx: true, - skipSerializers: true - }) - : noop(), - formatFiles(options) - ])(host, context); - }; -} - function normalizeOptions(options: Schema): NormalizedSchema { const name = toFileName(options.name); const projectDirectory = options.directory diff --git a/packages/react/src/schematics/library/schema.d.ts b/packages/react/src/schematics/library/schema.d.ts index 99773d089bcfd..ab5604d993bf0 100644 --- a/packages/react/src/schematics/library/schema.d.ts +++ b/packages/react/src/schematics/library/schema.d.ts @@ -1,5 +1,4 @@ import { UnitTestRunner } from '../../utils/test-runners'; -import { Framework } from '../../utils/framework'; export interface Schema { name: string; @@ -9,6 +8,6 @@ export interface Schema { skipFormat: boolean; tags?: string; simpleModuleName: boolean; - + pascalCaseFiles?: boolean; unitTestRunner: UnitTestRunner; } diff --git a/packages/react/src/schematics/library/schema.json b/packages/react/src/schematics/library/schema.json index ccc6fce6ac494..6f5af96b73f95 100644 --- a/packages/react/src/schematics/library/schema.json +++ b/packages/react/src/schematics/library/schema.json @@ -29,15 +29,23 @@ { "value": "css", "label": "CSS" }, { "value": "scss", - "label": "SASS(.scss) [ http://sass-lang.com ]" + "label": "SASS(.scss) [ http://sass-lang.com ]" }, { "value": "styl", - "label": "Stylus(.styl)[ http://stylus-lang.com ]" + "label": "Stylus(.styl) [ http://stylus-lang.com ]" }, { "value": "less", - "label": "LESS [ http://lesscss.org ]" + "label": "LESS [ http://lesscss.org ]" + }, + { + "value": "styled-components", + "label": "styled-components [ https://styled-components.com ]" + }, + { + "value": "@emotion/styled", + "label": "emotion [ https://emotion.sh ]" } ] } @@ -62,6 +70,11 @@ "type": "boolean", "default": false, "description": "Do not update tsconfig.json for development experience." + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case component file name (e.g. App.tsx)®", + "default": false } }, "required": ["name"] diff --git a/packages/react/src/utils/dependencies.ts b/packages/react/src/utils/dependencies.ts new file mode 100644 index 0000000000000..8dcd07e1c1e5d --- /dev/null +++ b/packages/react/src/utils/dependencies.ts @@ -0,0 +1,8 @@ +export interface PackageDependencies { + dependencies: DependencyEntries; + devDependencies: DependencyEntries; +} + +export interface DependencyEntries { + [module: string]: string; +} diff --git a/packages/react/src/utils/styled.ts b/packages/react/src/utils/styled.ts new file mode 100644 index 0000000000000..17b28c9c357c3 --- /dev/null +++ b/packages/react/src/utils/styled.ts @@ -0,0 +1,26 @@ +import { + emotionVersion, + styledComponentVersion, + styledComponentTypesVersion +} from './versions'; +import { PackageDependencies } from './dependencies'; + +export const CSS_IN_JS_DEPENDENCIES: { + [style: string]: PackageDependencies; +} = { + 'styled-components': { + dependencies: { + 'styled-components': styledComponentVersion + }, + devDependencies: { + '@types/styled-components': styledComponentTypesVersion + } + }, + '@emotion/styled': { + dependencies: { + '@emotion/styled': emotionVersion, + '@emotion/core': emotionVersion + }, + devDependencies: {} + } +}; diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index 0b920e24bed3b..a9e3bcbdc1bee 100644 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -1,5 +1,8 @@ export const nxVersion = '*'; export const frameworkVersion = '16.8.3'; export const typesVersion = '16.8.4'; +export const styledComponentVersion = '4.2.0'; +export const styledComponentTypesVersion = '4.1.15'; +export const emotionVersion = '10.0.10'; export const domTypesVersion = '16.8.2'; export const testingLibraryVersion = '6.0.0';