chore(react): move react schematics to generators (#4745)

* chore(react): move react schematics to generators

* chore(react): update lib generators

* chore(react): update redux generators

* chore(react): move react story book generators

* chore(react): add old implementation for update babel in next

* chore(react): rename tsconfig json template files to include __tmpl__

* chore(react): update deps

* chore(react): fix component template file

* chore(react): remove angular-devkit deps

* chore(react): remove angular-devkit deps
This commit is contained in:
Jonathan Cammisuli 2021-02-10 21:30:55 -05:00 committed by GitHub
parent 651f3b60e9
commit d9aef75bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 4312 additions and 4316 deletions

View File

@ -3,6 +3,7 @@ export {
WorkspaceJsonConfiguration,
TargetConfiguration,
ProjectConfiguration,
ProjectType,
Generator,
GeneratorCallback,
Executor,
@ -30,6 +31,7 @@ export {
} from './src/generators/project-configuration';
export { toJS } from './src/generators/to-js';
export { visitNotIgnoredFiles } from './src/generators/visit-not-ignored-files';
export { setDefaultCollection } from './src/generators/set-default-collection';
export { parseTargetString } from './src/executors/parse-target-string';
export { readTargetOptions } from './src/executors/read-target-options';

View File

@ -0,0 +1,26 @@
import { Tree } from '@nrwl/tao/src/shared/tree';
import {
readWorkspaceConfiguration,
updateWorkspaceConfiguration,
} from './project-configuration';
/**
* Sets the default collection within the workspace.
*
* Will only set the defaultCollection if one does not exist or if it is not `@nrwl/workspace`
*
* @param host
* @param collectionName Name of the collection to be set as the default
*/
export function setDefaultCollection(host: Tree, collectionName: string) {
const workspace = readWorkspaceConfiguration(host);
workspace.cli = workspace.cli || {};
const defaultCollection = workspace.cli.defaultCollection;
if (!defaultCollection || defaultCollection === '@nrwl/workspace') {
workspace.cli.defaultCollection = collectionName;
}
updateWorkspaceConfiguration(host, workspace);
}

View File

@ -7,6 +7,7 @@ export function createTreeWithEmptyWorkspace() {
const tree = new FsTree('/virtual', false);
tree.write('/workspace.json', JSON.stringify({ version: 1, projects: {} }));
tree.write('./.prettierrc', '{"singleQuote": true}');
tree.write(
'/package.json',
JSON.stringify({

View File

@ -1,6 +1,20 @@
import { chain, noop, Rule } from '@angular-devkit/schematics';
import { chain, noop, Rule, Tree } from '@angular-devkit/schematics';
import { NormalizedSchema } from './normalize-options';
import { updateBabelJestConfig } from '@nrwl/react/src/rules/update-babel-jest-config';
import { updateJsonInTree } from '@nrwl/workspace';
type BabelJestConfigUpdater<T> = (json: T) => T;
function updateBabelJestConfigOriginal<T = any>(
projectRoot: string,
update: BabelJestConfigUpdater<T>
) {
return (host: Tree) => {
const configPath = `${projectRoot}/babel-jest.config.json`;
return host.exists(configPath)
? updateJsonInTree<T>(configPath, update)
: noop();
};
}
export function updateJestConfig(options: NormalizedSchema): Rule {
return options.unitTestRunner === 'none'
@ -15,7 +29,7 @@ export function updateJestConfig(options: NormalizedSchema): Rule {
);
host.overwrite(configPath, content);
},
updateBabelJestConfig(options.appProjectRoot, (json) => {
updateBabelJestConfigOriginal(options.appProjectRoot, (json) => {
if (options.style === 'styled-jsx') {
json.plugins = (json.plugins || []).concat('styled-jsx/babel');
}

View File

@ -4,72 +4,144 @@
"extends": ["@nrwl/workspace"],
"schematics": {
"init": {
"factory": "./src/schematics/init/init",
"schema": "./src/schematics/init/schema.json",
"factory": "./src/generators/init/init#reactInitSchematic",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/react plugin",
"aliases": ["ng-add"],
"hidden": true
},
"application": {
"factory": "./src/schematics/application/application",
"schema": "./src/schematics/application/schema.json",
"factory": "./src/generators/application/application#applicationSchematic",
"schema": "./src/generators/application/schema.json",
"aliases": ["app"],
"description": "Create an application"
},
"library": {
"factory": "./src/schematics/library/library",
"schema": "./src/schematics/library/schema.json",
"factory": "./src/generators/library/library#librarySchematic",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"description": "Create a library"
},
"component": {
"factory": "./src/schematics/component/component",
"schema": "./src/schematics/component/schema.json",
"factory": "./src/generators/component/component#componentSchematic",
"schema": "./src/generators/component/schema.json",
"description": "Create a component",
"aliases": "c"
},
"redux": {
"factory": "./src/schematics/redux/redux",
"schema": "./src/schematics/redux/schema.json",
"factory": "./src/generators/redux/redux#reduxSchematic",
"schema": "./src/generators/redux/schema.json",
"description": "Create a redux slice for a project",
"aliases": ["slice"]
},
"storybook-configuration": {
"factory": "./src/schematics/storybook-configuration/configuration",
"schema": "./src/schematics/storybook-configuration/schema.json",
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationSchematic",
"schema": "./src/generators/storybook-configuration/schema.json",
"description": "Set up storybook for a react library",
"hidden": false
},
"storybook-migrate-defaults-5-to-6": {
"factory": "./src/schematics/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6",
"schema": "./src/schematics/storybook-migrate-defaults-5-to-6/schema.json",
"factory": "./src/generators/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6#storybookMigration5to6Schematic",
"schema": "./src/generators/storybook-migrate-defaults-5-to-6/schema.json",
"description": "Generate default Storybook configuration files using Storybook version >=6.x specs, for projects that already have Storybook instances and configurations of versions <6.x.",
"hidden": false
},
"component-story": {
"factory": "./src/schematics/component-story/component-story",
"schema": "./src/schematics/component-story/schema.json",
"factory": "./src/generators/component-story/component-story#componentStorySchematic",
"schema": "./src/generators/component-story/schema.json",
"description": "Generate storybook story for a react component",
"hidden": false
},
"stories": {
"factory": "./src/schematics/stories/stories",
"schema": "./src/schematics/stories/schema.json",
"factory": "./src/generators/stories/stories#storiesSchematic",
"schema": "./src/generators/stories/schema.json",
"description": "Create stories/specs for all components declared in a library",
"hidden": false
},
"component-cypress-spec": {
"factory": "./src/schematics/component-cypress-spec/component-cypress-spec",
"schema": "./src/schematics/component-cypress-spec/schema.json",
"factory": "./src/generators/component-cypress-spec/component-cypress-spec#componentCypressSchematic",
"schema": "./src/generators/component-cypress-spec/schema.json",
"description": "Create a cypress spec for a ui component that has a story",
"hidden": false
}
},
"generators": {
"init": {
"factory": "./src/generators/init/init#reactInitGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the @nrwl/react plugin",
"aliases": ["ng-add"],
"hidden": true
},
"application": {
"factory": "./src/generators/application/application#applicationGenerator",
"schema": "./src/generators/application/schema.json",
"aliases": ["app"],
"description": "Create an application"
},
"library": {
"factory": "./src/generators/library/library#libraryGenerator",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"description": "Create a library"
},
"component": {
"factory": "./src/generators/component/component#componentGenerator",
"schema": "./src/generators/component/schema.json",
"description": "Create a component",
"aliases": "c"
},
"redux": {
"factory": "./src/generators/redux/redux#reduxGenerator",
"schema": "./src/generators/redux/schema.json",
"description": "Create a redux slice for a project",
"aliases": ["slice"]
},
"storybook-configuration": {
"factory": "./src/generators/storybook-configuration/configuration#storybookConfigurationGenerator",
"schema": "./src/generators/storybook-configuration/schema.json",
"description": "Set up storybook for a react library",
"hidden": false
},
"storybook-migrate-defaults-5-to-6": {
"factory": "./src/generators/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6#storybookMigration5to6Generator",
"schema": "./src/generators/storybook-migrate-defaults-5-to-6/schema.json",
"description": "Generate default Storybook configuration files using Storybook version >=6.x specs, for projects that already have Storybook instances and configurations of versions <6.x.",
"hidden": false
},
"component-story": {
"factory": "./src/generators/component-story/component-story#componentStoryGenerator",
"schema": "./src/generators/component-story/schema.json",
"description": "Generate storybook story for a react component",
"hidden": false
},
"stories": {
"factory": "./src/generators/stories/stories#storiesGenerator",
"schema": "./src/generators/stories/schema.json",
"description": "Create stories/specs for all components declared in a library",
"hidden": false
},
"component-cypress-spec": {
"factory": "./src/generators/component-cypress-spec/component-cypress-spec#componentCypressGenerator",
"schema": "./src/generators/component-cypress-spec/schema.json",
"description": "Create a cypress spec for a ui component that has a story",
"hidden": false
}

View File

@ -2,12 +2,12 @@ export { extraEslintDependencies, reactEslintJson } from './src/utils/lint';
export { CSS_IN_JS_DEPENDENCIES } from './src/utils/styled';
export { assertValidStyle } from './src/utils/assertion';
export { applicationGenerator } from './src/schematics/application/application';
export { componentGenerator } from './src/schematics/component/component';
export { componentCypressGenerator } from './src/schematics/component-cypress-spec/component-cypress-spec';
export { componentStoryGenerator } from './src/schematics/component-story/component-story';
export { libraryGenerator } from './src/schematics/library/library';
export { reduxGenerator } from './src/schematics/redux/redux';
export { storiesGenerator } from './src/schematics/stories/stories';
export { storybookConfigurationGenerator } from './src/schematics/storybook-configuration/configuration';
export { storybookMigration5to6Generator } from './src/schematics/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6';
export { applicationGenerator } from './src/generators/application/application';
export { componentGenerator } from './src/generators/component/component';
export { componentCypressGenerator } from './src/generators/component-cypress-spec/component-cypress-spec';
export { componentStoryGenerator } from './src/generators/component-story/component-story';
export { libraryGenerator } from './src/generators/library/library';
export { reduxGenerator } from './src/generators/redux/redux';
export { storiesGenerator } from './src/generators/stories/stories';
export { storybookConfigurationGenerator } from './src/generators/storybook-configuration/configuration';
export { storybookMigration5to6Generator } from './src/generators/storybook-migrate-defaults-5-to-6/migrate-defaults-5-to-6';

View File

@ -28,14 +28,14 @@
"migrations": "./migrations.json"
},
"dependencies": {
"@angular-devkit/core": "~11.0.1",
"@babel/core": "7.9.6",
"@babel/preset-react": "7.9.4",
"@nrwl/cypress": "*",
"@nrwl/devkit": "*",
"@nrwl/jest": "*",
"@nrwl/web": "*",
"@angular-devkit/schematics": "~11.0.1",
"@nrwl/linter": "*",
"@nrwl/storybook": "*",
"@svgr/webpack": "^5.4.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",

View File

@ -0,0 +1,619 @@
import * as stripJsonComments from 'strip-json-comments';
import {
getProjects,
readJson,
readWorkspaceConfiguration,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { applicationGenerator } from './application';
import { Schema } from './schema';
import { Linter } from '@nrwl/linter';
describe('app', () => {
let appTree: Tree;
let schema: Schema = {
babelJest: false,
e2eTestRunner: 'cypress',
skipFormat: false,
unitTestRunner: 'jest',
name: 'myApp',
linter: Linter.EsLint,
style: 'css',
};
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
});
describe('not nested', () => {
it('should update workspace.json', async () => {
await applicationGenerator(appTree, schema);
const workspaceJson = readWorkspaceConfiguration(appTree);
const projects = getProjects(appTree);
expect(projects.get('my-app').root).toEqual('apps/my-app');
expect(projects.get('my-app-e2e').root).toEqual('apps/my-app-e2e');
expect(workspaceJson.defaultProject).toEqual('my-app');
});
it('should update nx.json', async () => {
await applicationGenerator(appTree, { ...schema, tags: 'one,two' });
const nxJson = readJson(appTree, './nx.json');
expect(nxJson.projects).toEqual({
'my-app': {
tags: ['one', 'two'],
},
'my-app-e2e': {
tags: [],
implicitDependencies: ['my-app'],
},
});
});
it('should generate files', async () => {
await applicationGenerator(appTree, schema);
expect(appTree.exists('apps/my-app/.babelrc')).toBeTruthy();
expect(appTree.exists('apps/my-app/.browserslistrc')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/main.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.module.css')).toBeTruthy();
const jestConfig = appTree.read('apps/my-app/jest.config.js').toString();
expect(jestConfig).toContain('@nrwl/react/plugins/jest');
const tsconfig = readJson(appTree, 'apps/my-app/tsconfig.json');
expect(tsconfig.references).toEqual([
{
path: './tsconfig.app.json',
},
{
path: './tsconfig.spec.json',
},
]);
const tsconfigApp = JSON.parse(
stripJsonComments(
appTree.read('apps/my-app/tsconfig.app.json').toString()
)
);
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
const eslintJson = JSON.parse(
stripJsonComments(appTree.read('apps/my-app/.eslintrc.json').toString())
);
expect(eslintJson.extends).toEqual([
'plugin:@nrwl/nx/react',
'../../.eslintrc.json',
]);
expect(appTree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
const tsconfigE2E = JSON.parse(
stripJsonComments(
appTree.read('apps/my-app-e2e/tsconfig.e2e.json').toString()
)
);
expect(tsconfigE2E.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigE2E.extends).toEqual('./tsconfig.json');
});
});
describe('nested', () => {
it('should update workspace.json', async () => {
await applicationGenerator(appTree, { ...schema, directory: 'myDir' });
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-dir-my-app').root).toEqual(
'apps/my-dir/my-app'
);
expect(workspaceJson.get('my-dir-my-app-e2e').root).toEqual(
'apps/my-dir/my-app-e2e'
);
});
it('should update nx.json', async () => {
await applicationGenerator(appTree, {
...schema,
directory: 'myDir',
tags: 'one,two',
});
const nxJson = readJson(appTree, '/nx.json');
expect(nxJson.projects).toEqual({
'my-dir-my-app': {
tags: ['one', 'two'],
},
'my-dir-my-app-e2e': {
tags: [],
implicitDependencies: ['my-dir-my-app'],
},
});
});
it('should generate files', async () => {
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
const content = appTree.read(path).toString();
const config = JSON.parse(stripJsonComments(content));
expect(lookupFn(config)).toEqual(expectedValue);
};
await applicationGenerator(appTree, { ...schema, directory: 'myDir' });
// Make sure these exist
[
'apps/my-dir/my-app/src/main.tsx',
'apps/my-dir/my-app/src/app/app.tsx',
'apps/my-dir/my-app/src/app/app.spec.tsx',
'apps/my-dir/my-app/src/app/app.module.css',
].forEach((path) => {
expect(appTree.exists(path)).toBeTruthy();
});
// Make sure these have properties
[
{
path: 'apps/my-dir/my-app/tsconfig.app.json',
lookupFn: (json) => json.compilerOptions.outDir,
expectedValue: '../../../dist/out-tsc',
},
{
path: 'apps/my-dir/my-app-e2e/tsconfig.e2e.json',
lookupFn: (json) => json.compilerOptions.outDir,
expectedValue: '../../../dist/out-tsc',
},
{
path: 'apps/my-dir/my-app/.eslintrc.json',
lookupFn: (json) => json.extends,
expectedValue: ['plugin:@nrwl/nx/react', '../../../.eslintrc.json'],
},
].forEach(hasJsonValue);
});
});
it('should create Nx specific template', async () => {
await applicationGenerator(appTree, { ...schema, directory: 'myDir' });
expect(
appTree.read('apps/my-dir/my-app/src/app/app.tsx').toString()
).toContain('Welcome to my-app');
});
describe('--style scss', () => {
it('should generate scss styles', async () => {
await applicationGenerator(appTree, { ...schema, style: 'scss' });
expect(appTree.exists('apps/my-app/src/app/app.module.scss')).toEqual(
true
);
});
});
it('should setup jest with tsx support', async () => {
await applicationGenerator(appTree, { ...schema, name: 'my-app' });
expect(appTree.read('apps/my-app/jest.config.js').toString()).toContain(
`moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],`
);
});
it('should setup jest without serializers', async () => {
await applicationGenerator(appTree, { ...schema, name: 'my-app' });
expect(appTree.read('apps/my-app/jest.config.js').toString()).not.toContain(
`'jest-preset-angular/build/AngularSnapshotSerializer.js',`
);
});
it('should setup the nrwl web build builder', async () => {
await applicationGenerator(appTree, { ...schema, name: 'my-app' });
const workspaceJson = getProjects(appTree);
const targetConfig = workspaceJson.get('my-app').targets;
expect(targetConfig.build.executor).toEqual('@nrwl/web:build');
expect(targetConfig.build.outputs).toEqual(['{options.outputPath}']);
expect(targetConfig.build.options).toEqual({
assets: ['apps/my-app/src/favicon.ico', 'apps/my-app/src/assets'],
index: 'apps/my-app/src/index.html',
main: 'apps/my-app/src/main.tsx',
outputPath: 'dist/apps/my-app',
polyfills: 'apps/my-app/src/polyfills.ts',
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',
webpackConfig: '@nrwl/react/plugins/webpack',
});
expect(targetConfig.build.configurations.production).toEqual({
optimization: true,
budgets: [
{
maximumError: '5mb',
maximumWarning: '2mb',
type: 'initial',
},
],
extractCss: true,
extractLicenses: true,
fileReplacements: [
{
replace: 'apps/my-app/src/environments/environment.ts',
with: 'apps/my-app/src/environments/environment.prod.ts',
},
],
namedChunks: false,
outputHashing: 'all',
sourceMap: false,
vendorChunk: false,
});
});
it('should setup the nrwl web dev server builder', async () => {
await applicationGenerator(appTree, { ...schema, name: 'my-app' });
const workspaceJson = getProjects(appTree);
const targetConfig = workspaceJson.get('my-app').targets;
expect(targetConfig.serve.executor).toEqual('@nrwl/web:dev-server');
expect(targetConfig.serve.options).toEqual({
buildTarget: 'my-app:build',
});
expect(targetConfig.serve.configurations.production).toEqual({
buildTarget: 'my-app:build:production',
});
});
it('should setup the eslint builder', async () => {
await applicationGenerator(appTree, { ...schema, name: 'my-app' });
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app').targets.lint).toEqual({
executor: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['apps/my-app/**/*.{ts,tsx,js,jsx}'],
},
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
await applicationGenerator(appTree, {
...schema,
unitTestRunner: 'none',
});
expect(appTree.exists('jest.config.js')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeFalsy();
expect(appTree.exists('apps/my-app/tsconfig.spec.json')).toBeFalsy();
expect(appTree.exists('apps/my-app/jest.config.js')).toBeFalsy();
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app').targets.test).toBeUndefined();
expect(workspaceJson.get('my-app').targets.lint).toMatchInlineSnapshot(`
Object {
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app/**/*.{ts,tsx,js,jsx}",
],
},
}
`);
});
});
describe('--e2e-test-runner none', () => {
it('should not generate test configuration', async () => {
await applicationGenerator(appTree, { ...schema, e2eTestRunner: 'none' });
expect(appTree.exists('apps/my-app-e2e')).toBeFalsy();
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app-e2e')).toBeUndefined();
});
});
describe('--pascalCaseFiles', () => {
it('should use upper case app file', async () => {
await applicationGenerator(appTree, { ...schema, pascalCaseFiles: true });
expect(appTree.exists('apps/my-app/src/app/App.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/App.spec.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/App.module.css')).toBeTruthy();
});
});
it('should generate functional components by default', async () => {
await applicationGenerator(appTree, schema);
const appContent = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(appContent).not.toMatch(/extends Component/);
});
it('should add .eslintrc.json and dependencies', async () => {
await applicationGenerator(appTree, { ...schema, linter: Linter.EsLint });
const eslintJson = readJson(appTree, '/apps/my-app/.eslintrc.json');
const packageJson = readJson(appTree, '/package.json');
expect(eslintJson.extends).toEqual(
expect.arrayContaining(['plugin:@nrwl/nx/react'])
);
expect(packageJson.devDependencies.eslint).toBeDefined();
expect(packageJson.devDependencies['@nrwl/linter']).toBeDefined();
expect(packageJson.devDependencies['@nrwl/eslint-plugin-nx']).toBeDefined();
expect(packageJson.devDependencies['eslint-plugin-react']).toBeDefined();
expect(
packageJson.devDependencies['eslint-plugin-react-hooks']
).toBeDefined();
expect(
packageJson.devDependencies['@typescript-eslint/parser']
).toBeDefined();
expect(
packageJson.devDependencies['@typescript-eslint/eslint-plugin']
).toBeDefined();
expect(packageJson.devDependencies['eslint-config-prettier']).toBeDefined();
});
describe('--class-component', () => {
it('should generate class components', async () => {
await applicationGenerator(appTree, { ...schema, classComponent: true });
const appContent = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(appContent).toMatch(/extends Component/);
});
});
describe('--style none', () => {
it('should not generate any styles', async () => {
await applicationGenerator(appTree, { ...schema, style: 'none' });
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/app.css')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.scss')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.styl')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.module.css')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.module.scss')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.module.styl')).toBeFalsy();
const content = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).not.toContain('styled-components');
expect(content).not.toContain('<StyledApp>');
expect(content).not.toContain('@emotion/styled');
expect(content).not.toContain('<StyledApp>');
//for imports
expect(content).not.toContain('app.styl');
expect(content).not.toContain('app.css');
expect(content).not.toContain('app.scss');
expect(content).not.toContain('app.module.styl');
expect(content).not.toContain('app.module.css');
expect(content).not.toContain('app.module.scss');
});
it('should set defaults when style: none', async () => {
await applicationGenerator(appTree, { ...schema, style: 'none' });
const workspaceJson = readWorkspaceConfiguration(appTree);
expect(workspaceJson.generators['@nrwl/react']).toMatchObject({
application: {
style: 'none',
},
component: {
style: 'none',
},
library: {
style: 'none',
},
});
});
it('should exclude styles from workspace.json', async () => {
await applicationGenerator(appTree, { ...schema, style: 'none' });
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app').targets.build.options.styles).toEqual(
[]
);
});
});
describe('--style styled-components', () => {
it('should use styled-components as the styled API library', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-components',
});
expect(
appTree.exists('apps/my-app/src/app/app.styled-components')
).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/styles.styled-components')
).toBeFalsy();
const content = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).toContain('styled-component');
expect(content).toContain('<StyledApp>');
});
it('should add dependencies to package.json', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-components',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['styled-components']).toBeDefined();
});
});
describe('--style @emotion/styled', () => {
it('should use @emotion/styled as the styled API library', async () => {
await applicationGenerator(appTree, {
...schema,
style: '@emotion/styled',
});
expect(
appTree.exists('apps/my-app/src/app/app.@emotion/styled')
).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
const content = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).toContain('@emotion/styled');
expect(content).toContain('<StyledApp>');
});
it('should exclude styles from workspace.json', async () => {
await applicationGenerator(appTree, {
...schema,
style: '@emotion/styled',
});
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-app').targets.build.options.styles).toEqual(
[]
);
});
it('should add dependencies to package.json', async () => {
await applicationGenerator(appTree, {
...schema,
style: '@emotion/styled',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['@emotion/react']).toBeDefined();
expect(packageJSON.dependencies['@emotion/styled']).toBeDefined();
});
});
describe('--style styled-jsx', () => {
it('should use styled-jsx as the styled API library', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-jsx',
});
expect(appTree.exists('apps/my-app/src/app/app.styled-jsx')).toBeFalsy();
expect(appTree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
const content = appTree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).toContain('<style jsx>');
});
it('should add dependencies to package.json', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-jsx',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['styled-jsx']).toBeDefined();
});
it('should update babel config', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-jsx',
});
const babelrc = readJson(appTree, 'apps/my-app/.babelrc');
const babelJestConfig = readJson(
appTree,
'apps/my-app/babel-jest.config.json'
);
expect(babelrc.plugins).toContain('styled-jsx/babel');
expect(babelJestConfig.plugins).toContain('styled-jsx/babel');
});
});
describe('--routing', () => {
it('should add routes to the App component', async () => {
await applicationGenerator(appTree, {
...schema,
routing: true,
});
const mainSource = appTree.read('apps/my-app/src/main.tsx').toString();
const componentSource = appTree
.read('apps/my-app/src/app/app.tsx')
.toString();
expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(mainSource).toContain('</BrowserRouter>');
expect(componentSource).toMatch(/<Route\s*path="\/"/);
expect(componentSource).toMatch(/<Link\s*to="\/"/);
});
});
it('should adds custom webpack config', async () => {
await applicationGenerator(appTree, {
...schema,
});
const workspaceJson = getProjects(appTree);
expect(
workspaceJson.get('my-app').targets.build.options.webpackConfig
).toEqual('@nrwl/react/plugins/webpack');
});
it('should add required polyfills for core-js and regenerator', async () => {
await applicationGenerator(appTree, {
...schema,
});
const polyfillsSource = appTree
.read('apps/my-app/src/polyfills.ts')
.toString();
expect(polyfillsSource).toContain('regenerator');
expect(polyfillsSource).toContain('core-js');
});
describe('--skipWorkspaceJson', () => {
it('should update workspace with defaults when --skipWorkspaceJson=false', async () => {
await applicationGenerator(appTree, {
...schema,
style: 'styled-components',
skipWorkspaceJson: false,
});
const workspaceJson = readWorkspaceConfiguration(appTree);
expect(workspaceJson.generators['@nrwl/react']).toMatchObject({
application: {
babel: true,
style: 'styled-components',
},
component: {
style: 'styled-components',
},
library: {
style: 'styled-components',
},
});
});
});
describe('--js', () => {
it('generates JS files', async () => {
await applicationGenerator(appTree, {
...schema,
js: true,
});
expect(appTree.exists('/apps/my-app/src/app/app.js')).toBe(true);
expect(appTree.exists('/apps/my-app/src/main.js')).toBe(true);
});
});
});

View File

@ -0,0 +1,82 @@
import { extraEslintDependencies, reactEslintJson } from '../../utils/lint';
import { NormalizedSchema, Schema } from './schema';
import { createApplicationFiles } from './lib/create-application-files';
import { updateJestConfig } from './lib/update-jest-config';
import { normalizeOptions } from './lib/normalize-options';
import { addProject } from './lib/add-project';
import { addCypress } from './lib/add-cypress';
import { addJest } from './lib/add-jest';
import { addRouting } from './lib/add-routing';
import { setDefaults } from './lib/set-defaults';
import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies';
import {
addDependenciesToPackageJson,
convertNxGenerator,
formatFiles,
GeneratorCallback,
joinPathFragments,
Tree,
updateJson,
} from '@nrwl/devkit';
import reactInitGenerator from '../init/init';
import { lintProjectGenerator } from '@nrwl/linter';
async function addLinting(host: Tree, options: NormalizedSchema) {
let installTask: GeneratorCallback;
installTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.projectName,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
eslintFilePatterns: [`${options.appProjectRoot}/**/*.{ts,tsx,js,jsx}`],
skipFormat: true,
});
updateJson(
host,
joinPathFragments(options.appProjectRoot, '.eslintrc.json'),
(json) => {
json.extends = [...reactEslintJson.extends, ...json.extends];
return json;
}
);
installTask = await addDependenciesToPackageJson(
host,
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies
);
return installTask;
}
export async function applicationGenerator(host: Tree, schema: Schema) {
let installTask: GeneratorCallback;
const options = normalizeOptions(host, schema);
installTask = await reactInitGenerator(host, {
...options,
skipFormat: true,
});
createApplicationFiles(host, options);
addProject(host, options);
await addLinting(host, options);
await addCypress(host, options);
await addJest(host, options);
updateJestConfig(host, options);
addStyledModuleDependencies(host, options.styledModule);
addRouting(host, options);
setDefaults(host, options);
if (!options.skipFormat) {
await formatFiles(host);
}
return installTask;
}
export default applicationGenerator;
export const applicationSchematic = convertNxGenerator(applicationGenerator);

View File

@ -1,7 +1,7 @@
{
"presets": [
"@nrwl/react/babel",
<% if (style === '@emotion/styled') { %>"@emotion/babel-preset-css-prop"<% } %>
"@nrwl/react/babel"
<% if (style === '@emotion/styled') { %>,"@emotion/babel-preset-css-prop"<% } %>
],
"plugins": [
<% if (style === 'styled-components') { %>["styled-components", { "pure": true, "ssr": true }]<% } %>

View File

@ -0,0 +1,16 @@
import { cypressProjectGenerator } from '@nrwl/cypress';
import { Tree } from '@nrwl/devkit';
import { NormalizedSchema } from '../schema';
export async function addCypress(host: Tree, options: NormalizedSchema) {
if (options.e2eTestRunner !== 'cypress') {
return;
}
return await cypressProjectGenerator(host, {
...options,
name: options.name + '-e2e',
directory: options.directory,
project: options.projectName,
});
}

View File

@ -0,0 +1,17 @@
import { Tree } from '@nrwl/devkit';
import { jestProjectGenerator } from '@nrwl/jest';
import { NormalizedSchema } from '../schema';
export async function addJest(host: Tree, options: NormalizedSchema) {
if (options.unitTestRunner !== 'jest') {
return;
}
return await jestProjectGenerator(host, {
project: options.projectName,
supportTsx: true,
skipSerializers: true,
setupFile: 'none',
babelJest: true,
});
}

View File

@ -0,0 +1,114 @@
import { NormalizedSchema } from '../schema';
import {
joinPathFragments,
addProjectConfiguration,
NxJsonProjectConfiguration,
ProjectConfiguration,
TargetConfiguration,
} from '@nrwl/devkit';
export function addProject(host, options: NormalizedSchema) {
const nxConfig: NxJsonProjectConfiguration = {
tags: options.parsedTags,
};
const project: ProjectConfiguration = {
root: options.appProjectRoot,
sourceRoot: `${options.appProjectRoot}/src`,
projectType: 'application',
targets: {
build: createBuildTarget(options),
serve: createServeTarget(options),
},
};
addProjectConfiguration(host, options.projectName, {
...project,
...nxConfig,
});
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}
function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
return {
executor: '@nrwl/web:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.appProjectRoot),
index: joinPathFragments(options.appProjectRoot, 'src/index.html'),
main: joinPathFragments(
options.appProjectRoot,
maybeJs(options, `src/main.tsx`)
),
polyfills: joinPathFragments(
options.appProjectRoot,
maybeJs(options, 'src/polyfills.ts')
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [
joinPathFragments(options.appProjectRoot, 'src/favicon.ico'),
joinPathFragments(options.appProjectRoot, 'src/assets'),
],
styles:
options.styledModule || !options.hasStyles
? []
: [
joinPathFragments(
options.appProjectRoot,
`src/styles.${options.style}`
),
],
scripts: [],
webpackConfig: '@nrwl/react/plugins/webpack',
},
configurations: {
production: {
fileReplacements: [
{
replace: joinPathFragments(
options.appProjectRoot,
maybeJs(options, `src/environments/environment.ts`)
),
with: joinPathFragments(
options.appProjectRoot,
maybeJs(options, `src/environments/environment.prod.ts`)
),
},
],
optimization: true,
outputHashing: 'all',
sourceMap: false,
extractCss: true,
namedChunks: false,
extractLicenses: true,
vendorChunk: false,
budgets: [
{
type: 'initial',
maximumWarning: '2mb',
maximumError: '5mb',
},
],
},
},
};
}
function createServeTarget(options: NormalizedSchema): TargetConfiguration {
return {
executor: '@nrwl/web:dev-server',
options: {
buildTarget: `${options.projectName}:build`,
},
configurations: {
production: {
buildTarget: `${options.projectName}:build:production`,
},
},
};
}

View File

@ -0,0 +1,50 @@
import * as ts from 'typescript';
import { addInitialRoutes } from '../../../utils/ast-utils';
import { NormalizedSchema } from '../schema';
import {
reactRouterDomVersion,
typesReactRouterDomVersion,
} from '../../../utils/versions';
import {
joinPathFragments,
Tree,
StringInsertion,
applyChangesToString,
addDependenciesToPackageJson,
} from '@nrwl/devkit';
export function addRouting(host: Tree, options: NormalizedSchema) {
if (!options.routing) {
return;
}
const appPath = joinPathFragments(
options.appProjectRoot,
maybeJs(options, `src/app/${options.fileName}.tsx`)
);
const appFileContent = host.read(appPath).toString('utf-8');
const appSource = ts.createSourceFile(
appPath,
appFileContent,
ts.ScriptTarget.Latest,
true
);
const changes = applyChangesToString(
appFileContent,
addInitialRoutes(appPath, appSource)
);
host.write(appPath, changes);
addDependenciesToPackageJson(
host,
{ 'react-router-dom': reactRouterDomVersion },
{ '@types/react-router-dom': typesReactRouterDomVersion }
);
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}

View File

@ -0,0 +1,48 @@
import { NormalizedSchema } from '../schema';
import { names, offsetFromRoot, Tree, toJS, generateFiles } from '@nrwl/devkit';
import { join } from 'path';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
let styleSolutionSpecificAppFiles: string;
if (options.styledModule && options.style !== 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/styled-module';
} else if (options.style === 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/styled-jsx';
} else if (options.style === 'none') {
styleSolutionSpecificAppFiles = '../files/none';
} else if (options.globalCss) {
styleSolutionSpecificAppFiles = '../files/global-css';
} else {
styleSolutionSpecificAppFiles = '../files/css-module';
}
const templateVariables = {
...names(options.name),
...options,
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
};
generateFiles(
host,
join(__dirname, '../files/common'),
options.appProjectRoot,
templateVariables
);
if (options.unitTestRunner === 'none') {
host.delete(
`${options.appProjectRoot}/src/app/${options.fileName}.spec.tsx`
);
}
generateFiles(
host,
join(__dirname, styleSolutionSpecificAppFiles),
options.appProjectRoot,
templateVariables
);
if (options.js) {
toJS(host);
}
}

View File

@ -1,9 +1,6 @@
import { Tree } from '@angular-devkit/schematics';
import { normalize } from '@angular-devkit/core';
import { appsDir } from '@nrwl/workspace/src/utils/ast-utils';
import { NormalizedSchema, Schema } from '../schema';
import { assertValidStyle } from '../../../utils/assertion';
import { names } from '@nrwl/devkit';
import { names, Tree, normalizePath, getWorkspaceLayout } from '@nrwl/devkit';
export function normalizeOptions(
host: Tree,
@ -16,7 +13,8 @@ export function normalizeOptions(
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
const e2eProjectName = `${appProjectName}-e2e`;
const appProjectRoot = normalize(`${appsDir(host)}/${appDirectory}`);
const { appsDir } = getWorkspaceLayout(host);
const appProjectRoot = normalizePath(`${appsDir}/${appDirectory}`);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
@ -30,6 +28,9 @@ export function normalizeOptions(
assertValidStyle(options.style);
options.routing = options.routing ?? false;
options.classComponent = options.classComponent ?? false;
return {
...options,
name: names(options.name).fileName,

View File

@ -0,0 +1,47 @@
import {
readWorkspaceConfiguration,
Tree,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { NormalizedSchema } from '../schema';
export function setDefaults(host: Tree, options: NormalizedSchema) {
if (options.skipWorkspaceJson) {
return;
}
const workspace = readWorkspaceConfiguration(host);
if (!workspace.defaultProject) {
workspace.defaultProject = options.projectName;
}
workspace.generators = workspace.generators || {};
workspace.generators['@nrwl/react'] =
workspace.generators['@nrwl/react'] || {};
const prev = { ...workspace.generators['@nrwl/react'] };
workspace.generators = {
...workspace.generators,
'@nrwl/react': {
...prev,
application: {
style: options.style,
linter: options.linter,
...prev.application,
},
component: {
style: options.style,
...prev.component,
},
library: {
style: options.style,
linter: options.linter,
...prev.library,
},
},
};
updateWorkspaceConfiguration(host, workspace);
}

View File

@ -0,0 +1,36 @@
import { updateBabelJestConfig } from '../../../rules/update-babel-jest-config';
import { updateJestConfigContent } from '../../../utils/jest-utils';
import { NormalizedSchema } from '../schema';
import { offsetFromRoot, Tree, updateJson } from '@nrwl/devkit';
export function updateJestConfig(host: Tree, options: NormalizedSchema) {
if (options.unitTestRunner !== 'jest') {
return;
}
updateJson(host, `${options.appProjectRoot}/tsconfig.spec.json`, (json) => {
const offset = offsetFromRoot(options.appProjectRoot);
json.files = [
`${offset}node_modules/@nrwl/react/typings/cssmodule.d.ts`,
`${offset}node_modules/@nrwl/react/typings/image.d.ts`,
];
if (options.style === 'styled-jsx') {
json.files.unshift(
`${offset}node_modules/@nrwl/react/typings/styled-jsx.d.ts`
);
}
return json;
});
const configPath = `${options.appProjectRoot}/jest.config.js`;
const originalContent = host.read(configPath).toString();
const content = updateJestConfigContent(originalContent);
host.write(configPath, content);
updateBabelJestConfig(host, options.appProjectRoot, (json) => {
if (options.style === 'styled-jsx') {
json.plugins = (json.plugins || []).concat('styled-jsx/babel');
}
return json;
});
}

View File

@ -1,10 +1,9 @@
import { Linter } from '@nrwl/workspace';
import { SupportedStyles } from 'packages/react/typings/style';
import { Path } from '@angular-devkit/core';
import { Linter } from '@nrwl/linter';
import { SupportedStyles } from '../../../typings/style';
export interface Schema {
name: string;
style?: SupportedStyles;
style: SupportedStyles;
skipFormat: boolean;
directory?: string;
tags?: string;
@ -22,7 +21,7 @@ export interface Schema {
export interface NormalizedSchema extends Schema {
projectName: string;
appProjectRoot: Path;
appProjectRoot: string;
e2eProjectName: string;
parsedTags: string[];
fileName: string;

View File

@ -1,13 +1,13 @@
import { externalSchematic, Tree } from '@angular-devkit/schematics';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../utils/testing';
import { CreateComponentSpecFileSchema } from './component-cypress-spec';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { Tree } from '@nrwl/devkit';
import componentCypressSpecGenerator from './component-cypress-spec';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from '../library/library';
import { Linter } from '@nrwl/linter';
import applicationGenerator from '../application/application';
import { formatFile } from '../../utils/format-file';
describe('react:component-cypress-spec', () => {
let appTree: Tree;
let tree: UnitTestTree;
[
{
@ -91,22 +91,20 @@ describe('react:component-cypress-spec', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib', testConfig.plainJS);
appTree.overwrite(cmpPath, testConfig.testCmpSrcWithProps);
appTree.write(cmpPath, testConfig.testCmpSrcWithProps);
tree = await runSchematic(
'component-cypress-spec',
<CreateComponentSpecFileSchema>{
await componentCypressSpecGenerator(appTree, {
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
project: 'test-ui-lib',
js: testConfig.plainJS,
},
appTree
);
});
});
it('should properly set up the spec', () => {
expect(stripIndents`${tree.readContent(cypressStorySpecFilePath)}`)
.toContain(stripIndents`describe('test-ui-lib: Test component', () => {
expect(
formatFile`${appTree.read(cypressStorySpecFilePath).toString()}`
)
.toContain(formatFile`describe('test-ui-lib: Test component', () => {
beforeEach(() => cy.visit('/iframe.html?id=test--primary&knob-name=&knob-displayAge=false'));
it('should render the component', () => {
@ -122,22 +120,19 @@ describe('react:component-cypress-spec', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib', testConfig.plainJS);
appTree.overwrite(cmpPath, testConfig.testCmpSrcWithoutProps);
appTree.write(cmpPath, testConfig.testCmpSrcWithoutProps);
tree = await runSchematic(
'component-cypress-spec',
<CreateComponentSpecFileSchema>{
await componentCypressSpecGenerator(appTree, {
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
project: 'test-ui-lib',
js: testConfig.plainJS,
},
appTree
);
});
});
it('should properly set up the spec', () => {
expect(stripIndents`${tree.readContent(cypressStorySpecFilePath)}`)
.toContain(stripIndents`describe('test-ui-lib: Test component', () => {
expect(
formatFile`${appTree.read(cypressStorySpecFilePath).toString()}`
).toContain(formatFile`describe('test-ui-lib: Test component', () => {
beforeEach(() => cy.visit('/iframe.html?id=test--primary'));
it('should render the component', () => {
@ -155,24 +150,30 @@ export async function createTestUILib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = await callRule(
externalSchematic('@nrwl/react', 'library', {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
name: libName,
linter: Linter.EsLint,
js: plainJS,
}),
appTree
);
component: true,
skipFormat: true,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'jest',
});
// create some Nx app that we'll use to generate the cypress
// spec into it. We don't need a real Cypress setup
appTree = await callRule(
externalSchematic('@nrwl/react', 'application', {
name: `${libName}-e2e`,
await applicationGenerator(appTree, {
babelJest: false,
js: plainJS,
}),
appTree
);
e2eTestRunner: 'none',
linter: Linter.EsLint,
name: `${libName}-e2e`,
skipFormat: true,
style: 'css',
unitTestRunner: 'none',
});
return appTree;
}

View File

@ -0,0 +1,112 @@
import {
getComponentName,
getComponentPropsInterface,
} from '../../utils/ast-utils';
import * as ts from 'typescript';
import {
convertNxGenerator,
generateFiles,
getProjects,
joinPathFragments,
Tree,
} from '@nrwl/devkit';
export interface CreateComponentSpecFileSchema {
project: string;
componentPath: string;
js?: boolean;
}
export function componentCypressGenerator(
host: Tree,
schema: CreateComponentSpecFileSchema
) {
createComponentSpecFile(host, schema);
}
// TODO: candidate to refactor with the angular component story
export function getKnobDefaultValue(property: ts.SyntaxKind): string {
const typeNameToDefault: Record<number, any> = {
[ts.SyntaxKind.StringKeyword]: '',
[ts.SyntaxKind.NumberKeyword]: 0,
[ts.SyntaxKind.BooleanKeyword]: false,
};
const resolvedValue = typeNameToDefault[property];
if (typeof resolvedValue === undefined) {
return '';
} else {
return resolvedValue;
}
}
export function createComponentSpecFile(
tree: Tree,
{ project, componentPath, js }: CreateComponentSpecFileSchema
) {
const projects = getProjects(tree);
const e2eLibIntegrationFolderPath =
projects.get(project + '-e2e').sourceRoot + '/integration';
const proj = projects.get(project);
const componentFilePath = joinPathFragments(proj.sourceRoot, componentPath);
const componentName = componentFilePath
.slice(componentFilePath.lastIndexOf('/') + 1)
.replace('.tsx', '')
.replace('.jsx', '')
.replace('.js', '');
const contents = tree.read(componentFilePath);
if (!contents) {
throw new Error(`Failed to read ${componentFilePath}`);
}
const sourceFile = ts.createSourceFile(
componentFilePath,
contents.toString(),
ts.ScriptTarget.Latest,
true
);
const cmpDeclaration = getComponentName(sourceFile);
if (!cmpDeclaration) {
throw new Error(
`Could not find any React component in file ${componentFilePath}`
);
}
const propsInterface = getComponentPropsInterface(sourceFile);
let props: {
name: string;
defaultValue: any;
}[] = [];
if (propsInterface) {
props = propsInterface.members.map((member: ts.PropertySignature) => {
return {
name: (member.name as ts.Identifier).text,
defaultValue: getKnobDefaultValue(member.type.kind),
};
});
}
generateFiles(
tree,
joinPathFragments(__dirname, './files'),
e2eLibIntegrationFolderPath + '/' + componentName,
{
projectName: project,
componentName,
componentSelector: (cmpDeclaration as any).name.text,
props,
fileExt: js ? 'js' : 'ts',
}
);
}
export default componentCypressGenerator;
export const componentCypressSchematic = convertNxGenerator(
componentCypressGenerator
);

View File

@ -1,13 +1,12 @@
import { externalSchematic, Tree } from '@angular-devkit/schematics';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../utils/testing';
import { CreateComponentStoriesFileSchema } from './component-story';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { getProjects, Tree, updateProjectConfiguration } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from '../library/library';
import componentStoryGenerator from './component-story';
import { Linter } from '@nrwl/linter';
import { formatFile } from '../../utils/format-file';
describe('react:component-story', () => {
let appTree: Tree;
let tree: UnitTestTree;
let cmpPath = 'libs/test-ui-lib/src/lib/test-ui-lib.tsx';
let storyFilePath = 'libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx';
@ -18,7 +17,7 @@ describe('react:component-story', () => {
describe('when file does not contain a component', () => {
beforeEach(() => {
appTree.overwrite(
appTree.write(
cmpPath,
`export const add = (a: number, b: number) => a + b;`
);
@ -26,14 +25,10 @@ describe('react:component-story', () => {
it('should fail with a descriptive error message', async (done) => {
try {
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
} catch (e) {
expect(e.message).toContain(
'Could not find any React component in file libs/test-ui-lib/src/lib/test-ui-lib.tsx'
@ -45,23 +40,19 @@ describe('react:component-story', () => {
describe('default component setup', () => {
beforeEach(async () => {
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should create the story file', () => {
expect(tree.exists(storyFilePath)).toBeTruthy();
expect(appTree.exists(storyFilePath)).toBeTruthy();
});
it('should properly set up the story', () => {
expect(stripIndents`${tree.readContent(storyFilePath)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePath).toString()}`)
.toContain(formatFile`
import React from 'react';
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
@ -85,7 +76,7 @@ describe('react:component-story', () => {
'libs/test-ui-lib/src/lib/test-ui-libplain.stories.jsx';
beforeEach(async () => {
appTree.create(
appTree.write(
'libs/test-ui-lib/src/lib/test-ui-libplain.jsx',
`import React from 'react';
@ -103,23 +94,19 @@ describe('react:component-story', () => {
`
);
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-libplain.jsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should create the story file', () => {
expect(tree.exists(storyFilePathPlain)).toBeTruthy();
expect(appTree.exists(storyFilePathPlain)).toBeTruthy();
});
it('should properly set up the story', () => {
expect(stripIndents`${tree.readContent(storyFilePathPlain)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePathPlain).toString()}`)
.toContain(formatFile`
import React from 'react';
import { Test } from './test-ui-libplain';
@ -140,7 +127,7 @@ describe('react:component-story', () => {
describe('component without any props defined', () => {
beforeEach(async () => {
appTree.overwrite(
appTree.write(
cmpPath,
`import React from 'react';
@ -158,19 +145,15 @@ describe('react:component-story', () => {
`
);
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should create a story without knobs', () => {
expect(stripIndents`${tree.readContent(storyFilePath)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePath).toString()}`)
.toContain(formatFile`
import React from 'react';
import { Test } from './test-ui-lib';
@ -181,14 +164,14 @@ describe('react:component-story', () => {
export const primary = () => {
return <Test />;
};
}
`);
});
});
describe('component with props', () => {
beforeEach(async () => {
appTree.overwrite(
appTree.write(
cmpPath,
`import React from 'react';
@ -211,19 +194,15 @@ describe('react:component-story', () => {
`
);
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should setup knobs based on the component props', () => {
expect(stripIndents`${tree.readContent(storyFilePath)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePath).toString()}`)
.toContain(formatFile`
import { text, boolean } from '@storybook/addon-knobs';
import React from 'react';
import { Test, TestProps } from './test-ui-lib';
@ -325,7 +304,7 @@ describe('react:component-story', () => {
].forEach((config) => {
describe(`React component defined as:${config.name}`, () => {
beforeEach(async () => {
appTree.overwrite(
appTree.write(
cmpPath,
`import React from 'react';
@ -340,19 +319,15 @@ describe('react:component-story', () => {
`
);
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should properly setup the knobs based on the component props', () => {
expect(stripIndents`${tree.readContent(storyFilePath)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePath).toString()}`)
.toContain(formatFile`
import { text, boolean } from '@storybook/addon-knobs';
import React from 'react';
import { Test, TestProps } from './test-ui-lib';
@ -379,19 +354,15 @@ describe('react:component-story', () => {
describe('using eslint', () => {
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib', false);
tree = await runSchematic(
'component-story',
<CreateComponentStoriesFileSchema>{
await componentStoryGenerator(appTree, {
componentPath: 'lib/test-ui-lib.tsx',
project: 'test-ui-lib',
},
appTree
);
});
});
it('should properly set up the story', () => {
expect(stripIndents`${tree.readContent(storyFilePath)}`)
.toContain(stripIndents`
expect(formatFile`${appTree.read(storyFilePath).toString()}`)
.toContain(formatFile`
import React from 'react';
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
@ -415,24 +386,24 @@ export async function createTestUILib(
libName: string,
useEsLint = false
): Promise<Tree> {
let appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = await callRule(
externalSchematic('@nrwl/react', 'library', {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
name: libName,
}),
appTree
);
linter: useEsLint ? Linter.EsLint : Linter.TsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'jest',
});
if (useEsLint) {
const currentWorkspaceJson = JSON.parse(
appTree.read('workspace.json').toString('utf-8')
);
const currentWorkspaceJson = getProjects(appTree);
currentWorkspaceJson.projects[libName].architect.lint.options.linter =
'eslint';
const projectConfig = currentWorkspaceJson.get(libName);
projectConfig.targets.lint.options.linter = 'eslint';
appTree.overwrite('workspace.json', JSON.stringify(currentWorkspaceJson));
updateProjectConfiguration(appTree, libName, projectConfig);
}
return appTree;

View File

@ -0,0 +1,150 @@
import {
convertNxGenerator,
formatFiles,
generateFiles,
getProjects,
joinPathFragments,
normalizePath,
Tree,
} from '@nrwl/devkit';
import * as ts from 'typescript';
import {
getComponentName,
getComponentPropsInterface,
} from '../../utils/ast-utils';
export interface CreateComponentStoriesFileSchema {
project: string;
componentPath: string;
}
export type KnobType = 'text' | 'boolean' | 'number' | 'select';
// TODO: candidate to refactor with the angular component story
export function getKnobDefaultValue(property: ts.SyntaxKind): string {
const typeNameToDefault: Record<number, any> = {
[ts.SyntaxKind.StringKeyword]: "''",
[ts.SyntaxKind.NumberKeyword]: 0,
[ts.SyntaxKind.BooleanKeyword]: false,
};
const resolvedValue = typeNameToDefault[property];
if (typeof resolvedValue === undefined) {
return "''";
} else {
return resolvedValue;
}
}
export function createComponentStoriesFile(
host: Tree,
{
// name,
project,
componentPath,
}: CreateComponentStoriesFileSchema
) {
const proj = getProjects(host).get(project);
const sourceRoot = proj.sourceRoot;
// TODO: Remove this entirely, given we don't support TSLint with React?
const usesEsLint = true;
const componentFilePath = joinPathFragments(sourceRoot, componentPath);
const componentDirectory = componentFilePath.replace(
componentFilePath.slice(componentFilePath.lastIndexOf('/')),
''
);
const isPlainJs = componentFilePath.endsWith('.jsx');
let fileExt = 'tsx';
if (componentFilePath.endsWith('.jsx')) {
fileExt = 'jsx';
} else if (componentFilePath.endsWith('.js')) {
fileExt = 'js';
}
const componentFileName = componentFilePath
.slice(componentFilePath.lastIndexOf('/') + 1)
.replace('.tsx', '')
.replace('.jsx', '')
.replace('.js', '');
const name = componentFileName;
const contents = host.read(componentFilePath);
if (!contents) {
throw new Error(`Failed to read ${componentFilePath}`);
}
const sourceFile = ts.createSourceFile(
componentFilePath,
contents.toString(),
ts.ScriptTarget.Latest,
true
);
const cmpDeclaration = getComponentName(sourceFile);
if (!cmpDeclaration) {
throw new Error(
`Could not find any React component in file ${componentFilePath}`
);
}
const propsInterface = getComponentPropsInterface(sourceFile);
let propsTypeName: string = null;
let props: {
name: string;
type: KnobType;
defaultValue: any;
}[] = [];
if (propsInterface) {
propsTypeName = propsInterface.name.text;
props = propsInterface.members.map((member: ts.PropertySignature) => {
const initializerKindToKnobType: Record<number, KnobType> = {
[ts.SyntaxKind.StringKeyword]: 'text',
[ts.SyntaxKind.NumberKeyword]: 'number',
[ts.SyntaxKind.BooleanKeyword]: 'boolean',
};
return {
name: (member.name as ts.Identifier).text,
type: initializerKindToKnobType[member.type.kind],
defaultValue: getKnobDefaultValue(member.type.kind),
};
});
}
generateFiles(
host,
joinPathFragments(__dirname, './files'),
normalizePath(componentDirectory),
{
componentFileName: name,
propsTypeName,
props,
usedKnobs: props.map((x) => x.type).join(', '),
componentName: (cmpDeclaration as any).name.text,
isPlainJs,
fileExt,
usesEsLint,
}
);
}
export async function componentStoryGenerator(
host: Tree,
schema: CreateComponentStoriesFileSchema
) {
createComponentStoriesFile(host, schema);
await formatFiles(host);
}
export default componentStoryGenerator;
export const componentStorySchematic = convertNxGenerator(
componentStoryGenerator
);

View File

@ -11,7 +11,7 @@ export const primary = () => {
<% if (propsTypeName || isPlainJs ) { %>
<% if (props.length === 0) { %>/* <%= usesEsLint ? 'eslint' : 'tslint'%>-disable-next-line */<% } %>
const props<%= isPlainJs ? '': ':' + propsTypeName %> = {<% for (let prop of props) { %>
<%= prop.name %>: <%= prop.type %>('<%= prop.name %>', <%= prop.defaultValue %>),<% } %>
<%= prop.name %>: <%= prop.type %>('<%= prop.name %>', <%- prop.defaultValue %>),<% } %>
};
<% } %>

View File

@ -0,0 +1,356 @@
import { createApp, createLib } from '../../utils/testing-generators';
import { readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { componentGenerator } from './component';
import applicationGenerator from '../application/application';
import { Linter } from '@nrwl/linter';
import { Schema } from './schema';
describe('component', () => {
let appTree: Tree;
let projectName: string;
beforeEach(async () => {
projectName = 'my-lib';
appTree = createTreeWithEmptyWorkspace();
await createApp(appTree, 'my-app');
await createLib(appTree, projectName);
});
it('should generate files', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
});
expect(appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.spec.tsx')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.module.css')
).toBeTruthy();
});
it('should generate files with global CSS', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
globalCss: true,
});
expect(appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.spec.tsx')
).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/lib/hello/hello.css')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.module.css')
).toBeFalsy();
});
it('should generate files for an app', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: 'my-app',
});
expect(appTree.exists('apps/my-app/src/app/hello/hello.tsx')).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/app/hello/hello.spec.tsx')
).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/app/hello/hello.module.css')
).toBeTruthy();
});
it('should generate files for an app with global CSS', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: 'my-app',
globalCss: true,
});
expect(appTree.exists('apps/my-app/src/app/hello/hello.tsx')).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/app/hello/hello.spec.tsx')
).toBeTruthy();
expect(appTree.exists('apps/my-app/src/app/hello/hello.css')).toBeTruthy();
expect(
appTree.exists('apps/my-app/src/app/hello/hello.module.css')
).toBeFalsy();
});
describe('--export', () => {
it('should add to index.ts barrel', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
export: true,
});
const indexContent = appTree.read('libs/my-lib/src/index.ts').toString();
expect(indexContent).toMatch(/lib\/hello/);
});
it('should not export from an app', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: 'my-app',
export: true,
});
const indexContent = appTree.read('libs/my-lib/src/index.ts').toString();
expect(indexContent).not.toMatch(/lib\/hello/);
});
});
describe('--pascalCaseFiles', () => {
it('should generate component files with upper case names', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
pascalCaseFiles: true,
});
expect(
appTree.exists('libs/my-lib/src/lib/hello/Hello.tsx')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/Hello.spec.tsx')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/Hello.module.css')
).toBeTruthy();
});
});
describe('--style none', () => {
it('should generate component files without styles', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: 'none',
});
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.spec.tsx')
).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/lib/hello/hello.css')).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.scss')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.styl')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.module.css')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.module.scss')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.module.styl')
).toBeFalsy();
const content = appTree
.read('libs/my-lib/src/lib/hello/hello.tsx')
.toString();
expect(content).not.toContain('styled-components');
expect(content).not.toContain('<StyledHello>');
expect(content).not.toContain('@emotion/styled');
expect(content).not.toContain('<StyledHello>');
//for imports
expect(content).not.toContain('hello.styl');
expect(content).not.toContain('hello.css');
expect(content).not.toContain('hello.scss');
expect(content).not.toContain('hello.module.styl');
expect(content).not.toContain('hello.module.css');
expect(content).not.toContain('hello.module.scss');
});
});
describe('--style styled-components', () => {
it('should use styled-components as the styled API library', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: 'styled-components',
});
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.styled-components')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')
).toBeTruthy();
const content = appTree
.read('libs/my-lib/src/lib/hello/hello.tsx')
.toString();
expect(content).toContain('styled-components');
expect(content).toContain('<StyledHello>');
});
it('should add dependencies to package.json', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: 'styled-components',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['styled-components']).toBeDefined();
});
});
describe('--style @emotion/styled', () => {
it('should use @emotion/styled as the styled API library', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: '@emotion/styled',
});
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.@emotion/styled')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')
).toBeTruthy();
const content = appTree
.read('libs/my-lib/src/lib/hello/hello.tsx')
.toString();
expect(content).toContain('@emotion/styled');
expect(content).toContain('<StyledHello>');
});
it('should add dependencies to package.json', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: '@emotion/styled',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['@emotion/styled']).toBeDefined();
expect(packageJSON.dependencies['@emotion/react']).toBeDefined();
});
});
describe('--style styled-jsx', () => {
it('should use styled-jsx as the styled API library', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: 'styled-jsx',
});
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.styled-jsx')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/hello/hello.tsx')
).toBeTruthy();
const content = appTree
.read('libs/my-lib/src/lib/hello/hello.tsx')
.toString();
expect(content).toContain('<style jsx>');
});
it('should add dependencies to package.json', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
style: 'styled-jsx',
});
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['styled-jsx']).toBeDefined();
});
});
describe('--routing', () => {
it('should add routes to the component', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
routing: true,
});
const content = appTree
.read('libs/my-lib/src/lib/hello/hello.tsx')
.toString();
expect(content).toContain('react-router-dom');
expect(content).toMatch(/<Route\s*path="\/"/);
expect(content).toMatch(/<Link\s*to="\/"/);
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['react-router-dom']).toBeDefined();
});
});
describe('--directory', () => {
it('should create component under the directory', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
directory: 'components',
});
expect(appTree.exists('/libs/my-lib/src/components/hello/hello.tsx'));
});
it('should create with nested directories', async () => {
await componentGenerator(appTree, {
name: 'helloWorld',
style: 'css',
project: projectName,
directory: 'lib/foo',
});
expect(
appTree.exists('/libs/my-lib/src/lib/foo/hello-world/hello-world.tsx')
);
});
});
describe('--flat', () => {
it('should create in project directory rather than in its own folder', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
flat: true,
});
expect(appTree.exists('/libs/my-lib/src/lib/hello.tsx'));
});
it('should work with custom directory path', async () => {
await componentGenerator(appTree, {
name: 'hello',
style: 'css',
project: projectName,
flat: true,
directory: 'components',
});
expect(appTree.exists('/libs/my-lib/src/components/hello.tsx'));
});
});
});

View File

@ -0,0 +1,212 @@
import * as ts from 'typescript';
import { Schema } from './schema';
import {
reactRouterDomVersion,
typesReactRouterDomVersion,
} from '../../utils/versions';
import { assertValidStyle } from '../../utils/assertion';
import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies';
import {
addDependenciesToPackageJson,
applyChangesToString,
convertNxGenerator,
formatFiles,
generateFiles,
GeneratorCallback,
getProjects,
joinPathFragments,
logger,
names,
toJS,
Tree,
} from '@nrwl/devkit';
import { addImport } from '../../utils/ast-utils';
interface NormalizedSchema extends Schema {
projectSourceRoot: string;
fileName: string;
className: string;
styledModule: null | string;
hasStyles: boolean;
}
export async function componentGenerator(host: Tree, schema: Schema) {
const options = await normalizeOptions(host, schema);
createComponentFiles(host, options);
addStyledModuleDependencies(host, options.styledModule);
addExportsToBarrel(host, options);
let installTask: GeneratorCallback;
if (options.routing) {
installTask = addDependenciesToPackageJson(
host,
{ 'react-router-dom': reactRouterDomVersion },
{ '@types/react-router-dom': typesReactRouterDomVersion }
);
}
await formatFiles(host);
if (installTask) {
return installTask;
}
}
function createComponentFiles(host: Tree, options: NormalizedSchema) {
const componentDir = joinPathFragments(
options.projectSourceRoot,
options.directory
);
generateFiles(host, joinPathFragments(__dirname, './files'), componentDir, {
...options,
tmpl: '',
});
for (const c of host.listChanges()) {
let deleteFile = false;
if (options.skipTests && /.*spec.tsx/.test(c.path)) {
deleteFile = true;
}
if (
(options.styledModule || !options.hasStyles) &&
c.path.endsWith(`.${options.style}`)
) {
deleteFile = true;
}
if (options.globalCss && c.path.endsWith(`.module.${options.style}`)) {
deleteFile = true;
}
if (
!options.globalCss &&
c.path.endsWith(`${options.fileName}.${options.style}`)
) {
deleteFile = true;
}
if (deleteFile) {
host.delete(c.path);
}
}
if (options.js) {
toJS(host);
}
}
function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
const workspace = getProjects(host);
const isApp = workspace.get(options.project).projectType === 'application';
if (options.export && !isApp) {
const indexFilePath = joinPathFragments(
options.projectSourceRoot,
options.js ? 'index.js' : '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
);
const changes = applyChangesToString(
indexSource,
addImport(
indexSourceFile,
`export * from './${options.directory}/${options.fileName}';`
)
);
host.write(indexFilePath, changes);
}
}
}
async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
assertValidOptions(options);
const { className, fileName } = names(options.name);
const componentFileName = options.pascalCaseFiles ? className : fileName;
const project = getProjects(host).get(options.project);
if (!project) {
logger.error(
`Cannot find the ${options.project} project. Please double check the project name.`
);
throw new Error();
}
const { sourceRoot: projectSourceRoot, projectType } = project;
const directory = await getDirectory(host, options);
const styledModule = /^(css|scss|less|styl|none)$/.test(options.style)
? null
: options.style;
if (options.export && projectType === 'application') {
logger.warn(
`The "--export" option should not be used with applications and will do nothing.`
);
}
options.classComponent = options.classComponent ?? false;
options.routing = options.routing ?? false;
options.globalCss = options.globalCss ?? false;
return {
...options,
directory,
styledModule,
hasStyles: options.style !== 'none',
className,
fileName: componentFileName,
projectSourceRoot,
};
}
async function getDirectory(host: Tree, options: Schema) {
const fileName = names(options.name).fileName;
const workspace = getProjects(host);
let baseDir: string;
if (options.directory) {
baseDir = options.directory;
} else {
baseDir =
workspace.get(options.project).projectType === 'application'
? 'app'
: 'lib';
}
return options.flat ? baseDir : joinPathFragments(baseDir, fileName);
}
function assertValidOptions(options: Schema) {
assertValidStyle(options.style);
const slashes = ['/', '\\'];
slashes.forEach((s) => {
if (options.name.indexOf(s) !== -1) {
const [name, ...rest] = options.name.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}\`)?`
);
}
});
}
export default componentGenerator;
export const componentSchematic = convertNxGenerator(componentGenerator);

View File

@ -16,7 +16,7 @@ import { Route, Link } from 'react-router-dom';
<% } else {
var wrapper = 'div';
%>
<%= style !== 'styled-jsx' ? globalCss ? `import './${fileName}.${style}';` : `import './${fileName}.module.${style}';`: '' %>
<%- style !== 'styled-jsx' ? globalCss ? `import './${fileName}.${style}';` : `import './${fileName}.module.${style}';`: '' %>
<% }
} else { var wrapper = 'div'; } %>
@ -50,7 +50,7 @@ export class <%= className %> extends Component<<%= className %>Props> {
export function <%= className %>(props: <%= className %>Props) {
return (
<<%= wrapper %>>
<%= styledModule === 'styled-jsx' ? `<style jsx>{\`div { color: pink; }\`}</style>` : `` %>
<% if (styledModule === 'styled-jsx') { %><style jsx>{`div { color: pink; }`}</style><% } %>
<h1>Welcome to <%= name %>!</h1>
<% if (routing) { %>
<ul>

View File

@ -1,9 +1,9 @@
import { SupportedStyles } from 'packages/react/typings/style';
import { SupportedStyles } from '../../../typings/style';
export interface Schema {
name: string;
project: string;
style?: SupportedStyles;
style: SupportedStyles;
skipTests?: boolean;
directory?: string;
export?: boolean;

View File

@ -0,0 +1,42 @@
import { readJson, readWorkspaceConfiguration, Tree } from '@nrwl/devkit';
import { reactVersion } from '../../utils/versions';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import reactInitGenerator from './init';
import { InitSchema } from './schema';
describe('init', () => {
let tree: Tree;
let schema: InitSchema = {
unitTestRunner: 'jest',
e2eTestRunner: 'cypress',
skipFormat: false,
};
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add react dependencies', async () => {
await reactInitGenerator(tree, schema);
const packageJson = readJson(tree, 'package.json');
expect(packageJson.dependencies['react']).toBeDefined();
expect(packageJson.dependencies['react-dom']).toBeDefined();
expect(packageJson.devDependencies['@types/react']).toBeDefined();
expect(packageJson.devDependencies['@types/react-dom']).toBeDefined();
expect(packageJson.devDependencies['@testing-library/react']).toBeDefined();
});
describe('defaultCollection', () => {
it('should be set if none was set before', async () => {
await reactInitGenerator(tree, schema);
const workspace = readWorkspaceConfiguration(tree);
expect(workspace.cli.defaultCollection).toEqual('@nrwl/react');
expect(workspace.generators['@nrwl/react'].application.babel).toBe(true);
});
});
it('should not add jest config if unitTestRunner is none', async () => {
await reactInitGenerator(tree, { ...schema, unitTestRunner: 'none' });
expect(tree.exists('jest.config.js')).toEqual(false);
});
});

View File

@ -0,0 +1,77 @@
import { InitSchema } from './schema';
import {
addDependenciesToPackageJson,
convertNxGenerator,
GeneratorCallback,
readWorkspaceConfiguration,
setDefaultCollection,
Tree,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { jestInitGenerator } from '@nrwl/jest';
import { cypressInitGenerator } from '@nrwl/cypress';
import { webInitGenerator } from '@nrwl/web';
import {
nxVersion,
reactDomVersion,
reactVersion,
testingLibraryReactVersion,
typesReactDomVersion,
typesReactVersion,
} from '../../utils/versions';
function setDefault(host: Tree) {
const workspace = readWorkspaceConfiguration(host);
workspace.generators = workspace.generators || {};
const reactGenerators = workspace.generators['@nrwl/react'] || {};
const generators = {
...workspace.generators,
'@nrwl/react': {
...reactGenerators,
application: {
...reactGenerators.application,
babel: true,
},
},
};
updateWorkspaceConfiguration(host, { ...workspace, generators });
setDefaultCollection(host, '@nrwl/react');
}
export async function reactInitGenerator(host: Tree, schema: InitSchema) {
let installTask: GeneratorCallback;
setDefault(host);
if (!schema.unitTestRunner || schema.unitTestRunner === 'jest') {
installTask = jestInitGenerator(host, {});
}
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') {
installTask = cypressInitGenerator(host) || installTask;
}
await webInitGenerator(host, schema);
installTask = addDependenciesToPackageJson(
host,
{
'core-js': '^3.6.5',
react: reactVersion,
'react-dom': reactDomVersion,
tslib: '^2.0.0',
},
{
'@nrwl/react': nxVersion,
'@types/react': typesReactVersion,
'@types/react-dom': typesReactDomVersion,
'@testing-library/react': testingLibraryReactVersion,
}
);
return installTask;
}
export default reactInitGenerator;
export const reactInitSchematic = convertNxGenerator(reactInitGenerator);

View File

@ -1,4 +1,4 @@
export interface Schema {
export interface InitSchema {
unitTestRunner: 'jest' | 'none';
e2eTestRunner: 'cypress' | 'none';
skipFormat: boolean;

View File

@ -2,6 +2,7 @@
"$schema": "http://json-schema.org/schema",
"id": "NxReactNgInit",
"title": "Init React Plugin",
"cli": "nx",
"type": "object",
"properties": {
"unitTestRunner": {

View File

@ -1,7 +1,7 @@
{
"presets": [
"@nrwl/react/babel",
<% if (style === '@emotion/styled') { %>"@emotion/babel-preset-css-prop"<% } %>
"@nrwl/react/babel"
<% if (style === '@emotion/styled') { %>,"@emotion/babel-preset-css-prop"<% } %>
],
"plugins": [
<% if (style === 'styled-components') { %>["styled-components", { "pure": true, "ssr": true }]<% } %>

View File

@ -0,0 +1,543 @@
import { getProjects, readJson, Tree, updateJson } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from './library';
import { Linter } from '@nrwl/linter';
import { Schema } from './schema';
import applicationGenerator from '../application/application';
describe('lib', () => {
let appTree: Tree;
let defaultSchema: Schema = {
name: 'myLib',
linter: Linter.EsLint,
skipFormat: false,
skipTsConfig: false,
unitTestRunner: 'jest',
style: 'css',
component: true,
};
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
});
describe('not nested', () => {
it('should update workspace.json', async () => {
await libraryGenerator(appTree, defaultSchema);
const workspaceJson = readJson(appTree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib');
expect(workspaceJson.projects['my-lib'].architect.build).toBeUndefined();
expect(workspaceJson.projects['my-lib'].architect.lint).toEqual({
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['libs/my-lib/**/*.{ts,tsx,js,jsx}'],
},
});
});
it('should update nx.json', async () => {
await libraryGenerator(appTree, { ...defaultSchema, tags: 'one,two' });
const nxJson = readJson(appTree, '/nx.json');
expect(nxJson.projects).toEqual({
'my-lib': {
tags: ['one', 'two'],
},
});
});
it('should add react and react-dom packages to package.json if not already present', async () => {
await libraryGenerator(appTree, defaultSchema);
const packageJson = readJson(appTree, '/package.json');
expect(packageJson).toMatchObject({
dependencies: {
react: expect.anything(),
'react-dom': expect.anything(),
},
});
});
it('should update tsconfig.base.json', async () => {
await libraryGenerator(appTree, defaultSchema);
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.ts',
]);
});
it('should update root tsconfig.base.json (no existing path mappings)', async () => {
updateJson(appTree, 'tsconfig.base.json', (json) => {
json.compilerOptions.paths = undefined;
return json;
});
await libraryGenerator(appTree, defaultSchema);
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'libs/my-lib/src/index.ts',
]);
});
it('should create a local tsconfig.json', async () => {
await libraryGenerator(appTree, defaultSchema);
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.json');
expect(tsconfigJson.references).toEqual([
{
path: './tsconfig.lib.json',
},
{
path: './tsconfig.spec.json',
},
]);
});
it('should extend the local tsconfig.json with tsconfig.spec.json', async () => {
await libraryGenerator(appTree, defaultSchema);
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.spec.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
it('should extend the local tsconfig.json with tsconfig.lib.json', async () => {
await libraryGenerator(appTree, defaultSchema);
const tsconfigJson = readJson(appTree, 'libs/my-lib/tsconfig.lib.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
it('should generate files', async () => {
await libraryGenerator(appTree, defaultSchema);
expect(appTree.exists('libs/my-lib/package.json')).toBeFalsy();
expect(appTree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/lib/my-lib.tsx')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.css')
).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.spec.tsx')
).toBeTruthy();
});
});
describe('nested', () => {
it('should update nx.json', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
directory: 'myDir',
tags: 'one',
});
const nxJson = readJson(appTree, '/nx.json');
expect(nxJson.projects).toEqual({
'my-dir-my-lib': {
tags: ['one'],
},
});
await libraryGenerator(appTree, {
...defaultSchema,
name: 'myLib2',
directory: 'myDir',
tags: 'one,two',
});
const nxJson2 = readJson(appTree, '/nx.json');
expect(nxJson2.projects).toEqual({
'my-dir-my-lib': {
tags: ['one'],
},
'my-dir-my-lib2': {
tags: ['one', 'two'],
},
});
});
it('should generate files', async () => {
await libraryGenerator(appTree, { ...defaultSchema, directory: 'myDir' });
expect(appTree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy();
expect(appTree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.tsx')
).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.module.css')
).toBeTruthy();
expect(
appTree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.tsx')
).toBeTruthy();
});
it('should update workspace.json', async () => {
await libraryGenerator(appTree, { ...defaultSchema, directory: 'myDir' });
const workspaceJson = readJson(appTree, '/workspace.json');
expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual(
'libs/my-dir/my-lib'
);
expect(workspaceJson.projects['my-dir-my-lib'].architect.lint).toEqual({
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['libs/my-dir/my-lib/**/*.{ts,tsx,js,jsx}'],
},
});
});
it('should update tsconfig.base.json', async () => {
await libraryGenerator(appTree, { ...defaultSchema, directory: 'myDir' });
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
expect(
tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']
).toEqual(['libs/my-dir/my-lib/src/index.ts']);
expect(
tsconfigJson.compilerOptions.paths['my-dir-my-lib/*']
).toBeUndefined();
});
it('should create a local tsconfig.json', async () => {
await libraryGenerator(appTree, { ...defaultSchema, directory: 'myDir' });
const tsconfigJson = readJson(
appTree,
'libs/my-dir/my-lib/tsconfig.json'
);
expect(tsconfigJson.references).toEqual([
{
path: './tsconfig.lib.json',
},
{
path: './tsconfig.spec.json',
},
]);
});
});
describe('--style scss', () => {
it('should use scss for styles', async () => {
await libraryGenerator(appTree, { ...defaultSchema, style: 'scss' });
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.scss')
).toBeTruthy();
});
});
describe('--style none', () => {
it('should not use styles when style none', async () => {
await libraryGenerator(appTree, { ...defaultSchema, style: 'none' });
expect(appTree.exists('libs/my-lib/src/lib/my-lib.tsx')).toBeTruthy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.spec.tsx')
).toBeTruthy();
expect(appTree.exists('libs/my-lib/src/lib/my-lib.css')).toBeFalsy();
expect(appTree.exists('libs/my-lib/src/lib/my-lib.scss')).toBeFalsy();
expect(appTree.exists('libs/my-lib/src/lib/my-lib.styl')).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.css')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.scss')
).toBeFalsy();
expect(
appTree.exists('libs/my-lib/src/lib/my-lib.module.styl')
).toBeFalsy();
const content = appTree.read('libs/my-lib/src/lib/my-lib.tsx').toString();
expect(content).not.toContain('styled-components');
expect(content).not.toContain('<StyledApp>');
expect(content).not.toContain('@emotion/styled');
expect(content).not.toContain('<StyledApp>');
//for imports
expect(content).not.toContain('app.styl');
expect(content).not.toContain('app.css');
expect(content).not.toContain('app.scss');
expect(content).not.toContain('app.module.styl');
expect(content).not.toContain('app.module.css');
expect(content).not.toContain('app.module.scss');
});
});
describe('--no-component', () => {
it('should not generate components or styles', async () => {
await libraryGenerator(appTree, { ...defaultSchema, component: false });
expect(appTree.exists('libs/my-lib/src/lib')).toBeFalsy();
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
unitTestRunner: 'none',
});
expect(appTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy();
expect(appTree.exists('libs/my-lib/jest.config.js')).toBeFalsy();
const workspaceJson = readJson(appTree, 'workspace.json');
expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined();
expect(workspaceJson.projects['my-lib'].architect.lint)
.toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"libs/my-lib/**/*.{ts,tsx,js,jsx}",
],
},
}
`);
});
});
describe('--appProject', () => {
it('should add new route to existing routing code', async () => {
await applicationGenerator(appTree, {
babelJest: true,
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
unitTestRunner: 'jest',
name: 'myApp',
routing: true,
style: 'css',
});
await libraryGenerator(appTree, {
...defaultSchema,
appProject: 'my-app',
});
const appSource = appTree.read('apps/my-app/src/app/app.tsx').toString();
const mainSource = appTree.read('apps/my-app/src/main.tsx').toString();
expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(appSource).toContain('@proj/my-lib');
expect(appSource).toContain('react-router-dom');
expect(appSource).toMatch(/<Route\s*path="\/my-lib"/);
});
it('should initialize routes if none were set up then add new route', async () => {
await applicationGenerator(appTree, {
babelJest: true,
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
unitTestRunner: 'jest',
name: 'myApp',
style: 'css',
});
await libraryGenerator(appTree, {
...defaultSchema,
appProject: 'my-app',
});
const appSource = appTree.read('apps/my-app/src/app/app.tsx').toString();
const mainSource = appTree.read('apps/my-app/src/main.tsx').toString();
expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(appSource).toContain('@proj/my-lib');
expect(appSource).toContain('react-router-dom');
expect(appSource).toMatch(/<Route\s*path="\/my-lib"/);
});
});
describe('--buildable', () => {
it('should have a builder defined', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
buildable: true,
});
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-lib').targets.build).toBeDefined();
});
});
describe('--publishable', () => {
it('should add build architect', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const workspaceJson = getProjects(appTree);
expect(workspaceJson.get('my-lib').targets.build).toMatchObject({
executor: '@nrwl/web:package',
outputs: ['{options.outputPath}'],
options: {
external: ['react', 'react-dom'],
entryFile: 'libs/my-lib/src/index.ts',
outputPath: 'dist/libs/my-lib',
project: 'libs/my-lib/package.json',
tsConfig: 'libs/my-lib/tsconfig.lib.json',
babelConfig: '@nrwl/react/plugins/bundle-babel',
rollupConfig: '@nrwl/react/plugins/bundle-rollup',
},
});
});
it('should fail if no importPath is provided with publishable', async () => {
expect.assertions(1);
try {
await libraryGenerator(appTree, {
...defaultSchema,
directory: 'myDir',
publishable: true,
});
} catch (e) {
expect(e.message).toContain(
'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)'
);
}
});
it('should support styled-components', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
style: 'styled-components',
});
const workspaceJson = readJson(appTree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({
options: {
external: ['react', 'react-dom', 'react-is', 'styled-components'],
},
});
});
it('should support @emotion/styled', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
style: '@emotion/styled',
});
const workspaceJson = readJson(appTree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({
options: {
external: ['react', 'react-dom', '@emotion/styled', '@emotion/react'],
},
});
});
it('should support styled-jsx', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
style: 'styled-jsx',
});
const workspaceJson = readJson(appTree, '/workspace.json');
const babelrc = readJson(appTree, 'libs/my-lib/.babelrc');
const babelJestConfig = readJson(
appTree,
'libs/my-lib/babel-jest.config.json'
);
expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({
options: {
external: ['react', 'react-dom', 'styled-jsx'],
},
});
expect(babelrc.plugins).toContain('styled-jsx/babel');
expect(babelJestConfig.plugins).toContain('styled-jsx/babel');
});
it('should support style none', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
style: 'none',
});
const workspaceJson = readJson(appTree, '/workspace.json');
expect(workspaceJson.projects['my-lib'].architect.build).toMatchObject({
options: {
external: ['react', 'react-dom'],
},
});
});
it('should add package.json and .babelrc', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const packageJson = readJson(appTree, '/libs/my-lib/package.json');
expect(packageJson.name).toEqual('@proj/my-lib');
expect(appTree.exists('/libs/my-lib/.babelrc'));
});
});
describe('--js', () => {
it('should generate JS files', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
js: true,
});
expect(appTree.exists('/libs/my-lib/src/index.js')).toBe(true);
});
});
describe('--importPath', () => {
it('should update the package.json & tsconfig with the given import path', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
publishable: true,
directory: 'myDir',
importPath: '@myorg/lib',
});
const packageJson = readJson(appTree, 'libs/my-dir/my-lib/package.json');
const tsconfigJson = readJson(appTree, '/tsconfig.base.json');
expect(packageJson.name).toBe('@myorg/lib');
expect(
tsconfigJson.compilerOptions.paths[packageJson.name]
).toBeDefined();
});
it('should fail if the same importPath has already been used', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
name: 'myLib1',
publishable: true,
importPath: '@myorg/lib',
});
try {
await libraryGenerator(appTree, {
...defaultSchema,
name: 'myLib2',
publishable: true,
importPath: '@myorg/lib',
});
} catch (e) {
expect(e.message).toContain(
'You already have a library using the import path'
);
}
expect.assertions(1);
});
});
});

View File

@ -0,0 +1,418 @@
import { CSS_IN_JS_DEPENDENCIES } from '../../utils/styled';
import * as ts from 'typescript';
import { assertValidStyle } from '../../utils/assertion';
import {
addBrowserRouter,
addInitialRoutes,
addRoute,
findComponentImportPath,
} from '../../utils/ast-utils';
import { extraEslintDependencies, reactEslintJson } from '../../utils/lint';
import {
reactDomVersion,
reactRouterDomVersion,
reactVersion,
typesReactRouterDomVersion,
} from '../../utils/versions';
import { Schema } from './schema';
import { updateBabelJestConfig } from '../../rules/update-babel-jest-config';
import {
addDependenciesToPackageJson,
addProjectConfiguration,
applyChangesToString,
convertNxGenerator,
formatFiles,
generateFiles,
GeneratorCallback,
getProjects,
getWorkspaceLayout,
joinPathFragments,
names,
normalizePath,
offsetFromRoot,
toJS,
Tree,
updateJson,
} from '@nrwl/devkit';
import init from '../init/init';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { jestProjectGenerator } from '@nrwl/jest';
import componentGenerator from '../component/component';
export interface NormalizedSchema extends Schema {
name: string;
fileName: string;
projectRoot: string;
routePath: string;
projectDirectory: string;
parsedTags: string[];
appMain?: string;
appSourceRoot?: string;
}
export async function libraryGenerator(host: Tree, schema: Schema) {
let installTask: GeneratorCallback;
const options = 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)`
);
}
if (!options.component) {
options.style = 'none';
}
installTask = await init(host, {
...options,
e2eTestRunner: 'none',
skipFormat: true,
});
addProject(host, options);
await addLinting(host, options);
createFiles(host, options);
if (!options.skipTsConfig) {
updateTsConfig(host, options);
}
if (options.unitTestRunner === 'jest') {
await jestProjectGenerator(host, {
project: options.name,
setupFile: 'none',
supportTsx: true,
skipSerializers: true,
babelJest: true,
});
updateBabelJestConfig(host, options.projectRoot, (json) => {
if (options.style === 'styled-jsx') {
json.plugins = (json.plugins || []).concat('styled-jsx/babel');
}
return json;
});
}
if (options.component) {
await componentGenerator(host, {
name: options.name,
project: options.name,
flat: true,
style: options.style,
skipTests: options.unitTestRunner === 'none',
export: true,
routing: options.routing,
js: options.js,
pascalCaseFiles: options.pascalCaseFiles,
});
}
if (options.publishable || options.buildable) {
updateLibPackageNpmScope(host, options);
}
await addDependenciesToPackageJson(
host,
{
react: reactVersion,
'react-dom': reactDomVersion,
},
{}
);
updateAppRoutes(host, options);
if (!options.skipFormat) {
await formatFiles(host);
}
return installTask;
}
async function addLinting(host: Tree, options: NormalizedSchema) {
let installTask: GeneratorCallback;
installTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.name,
tsConfigPaths: [
joinPathFragments(options.projectRoot, 'tsconfig.lib.json'),
],
eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,js,jsx}`],
skipFormat: true,
});
if (options.linter === Linter.TsLint) {
return;
}
updateJson(
host,
joinPathFragments(options.projectRoot, '.eslintrc.json'),
(json) => {
json.extends = [...reactEslintJson.extends, ...json.extends];
return json;
}
);
installTask = await addDependenciesToPackageJson(
host,
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies
);
return installTask;
}
function addProject(host: Tree, options: NormalizedSchema) {
const targets: { [key: string]: any } = {};
if (options.publishable || options.buildable) {
const { libsDir } = getWorkspaceLayout(host);
const external = ['react', 'react-dom'];
// Also exclude CSS-in-JS packages from build
if (
options.style !== 'css' &&
options.style !== 'scss' &&
options.style !== 'styl' &&
options.style !== 'less' &&
options.style !== 'none'
) {
external.push(
...Object.keys(CSS_IN_JS_DEPENDENCIES[options.style].dependencies)
);
}
targets.build = {
builder: '@nrwl/web:package',
outputs: ['{options.outputPath}'],
options: {
outputPath: `dist/${libsDir}/${options.projectDirectory}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
external,
babelConfig: `@nrwl/react/plugins/bundle-babel`,
rollupConfig: `@nrwl/react/plugins/bundle-rollup`,
assets: [
{
glob: 'README.md',
input: '.',
output: '.',
},
],
},
};
}
addProjectConfiguration(host, options.name, {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags,
targets,
});
}
function updateTsConfig(host: Tree, options: NormalizedSchema) {
updateJson(host, 'tsconfig.base.json', (json) => {
const c = json.compilerOptions;
c.paths = c.paths || {};
delete c.paths[options.name];
if (c.paths[options.importPath]) {
throw new Error(
`You already have a library using the import path "${options.importPath}". Make sure to specify a unique one.`
);
}
const { libsDir } = getWorkspaceLayout(host);
c.paths[options.importPath] = [
maybeJs(options, `${libsDir}/${options.projectDirectory}/src/index.ts`),
];
return json;
});
}
function createFiles(host: Tree, options: NormalizedSchema) {
generateFiles(
host,
joinPathFragments(__dirname, './files/lib'),
options.projectRoot,
{
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
}
);
if (!options.publishable && !options.buildable) {
host.delete(`${options.projectRoot}/package.json`);
}
if (options.js) {
toJS(host);
}
}
function updateAppRoutes(host: Tree, options: NormalizedSchema) {
if (!options.appMain || !options.appSourceRoot) {
return;
}
const { content, source } = readComponent(host, options.appMain);
const componentImportPath = findComponentImportPath('App', source);
if (!componentImportPath) {
throw new Error(
`Could not find App component in ${options.appMain} (Hint: you can omit --appProject, or make sure App exists)`
);
}
const appComponentPath = joinPathFragments(
options.appSourceRoot,
maybeJs(options, `${componentImportPath}.tsx`)
);
addDependenciesToPackageJson(
host,
{ 'react-router-dom': reactRouterDomVersion },
{ '@types/react-router-dom': typesReactRouterDomVersion }
);
// addBrowserRouterToMain
const isRouterPresent = content.match(/react-router-dom/);
if (!isRouterPresent) {
const changes = applyChangesToString(
content,
addBrowserRouter(options.appMain, source)
);
host.write(options.appMain, changes);
}
// addInitialAppRoutes
{
const {
content: componentContent,
source: componentSource,
} = readComponent(host, appComponentPath);
const isComponentRouterPresent = componentContent.match(/react-router-dom/);
if (!isComponentRouterPresent) {
const changes = applyChangesToString(
componentContent,
addInitialRoutes(appComponentPath, componentSource)
);
host.write(appComponentPath, changes);
}
}
// addNewAppRoute
{
const {
content: componentContent,
source: componentSource,
} = readComponent(host, appComponentPath);
const { npmScope } = getWorkspaceLayout(host);
const changes = applyChangesToString(
componentContent,
addRoute(appComponentPath, componentSource, {
routePath: options.routePath,
componentName: names(options.name).className,
moduleName: `@${npmScope}/${options.projectDirectory}`,
})
);
host.write(appComponentPath, changes);
}
}
function readComponent(
host: Tree,
path: string
): { content: string; source: ts.SourceFile } {
if (!host.exists(path)) {
throw new Error(`Cannot find ${path}`);
}
const content = host.read(path).toString('utf-8');
const source = ts.createSourceFile(
path,
content,
ts.ScriptTarget.Latest,
true
);
return { content, source };
}
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, npmScope } = getWorkspaceLayout(host);
const projectRoot = joinPathFragments(`${libsDir}/${projectDirectory}`);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const importPath = options.importPath || `@${npmScope}/${projectDirectory}`;
const normalized: NormalizedSchema = {
...options,
fileName,
routePath: `/${name}`,
name: projectName,
projectRoot,
projectDirectory,
parsedTags,
importPath,
};
if (options.appProject) {
const appProjectConfig = getProjects(host).get(options.appProject);
if (appProjectConfig.projectType !== 'application') {
throw new Error(
`appProject expected type of "application" but got "${appProjectConfig.projectType}"`
);
}
try {
normalized.appMain = appProjectConfig.targets.build.options.main;
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
} catch (e) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
}
}
assertValidStyle(normalized.style);
return normalized;
}
function updateLibPackageNpmScope(host: Tree, options: NormalizedSchema) {
return updateJson(host, `${options.projectRoot}/package.json`, (json) => {
json.name = options.importPath;
return json;
});
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}
export default libraryGenerator;
export const librarySchematic = convertNxGenerator(libraryGenerator);

View File

@ -1,10 +1,10 @@
import { Linter } from '@nrwl/workspace';
import { SupportedStyles } from 'packages/react/typings/style';
import { SupportedStyles } from '../../../typings/style';
import { Linter } from '@nrwl/linter';
export interface Schema {
name: string;
directory?: string;
style?: SupportedStyles;
style: SupportedStyles;
skipTsConfig: boolean;
skipFormat: boolean;
tags?: string;

View File

@ -0,0 +1,95 @@
import { readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { applicationGenerator, libraryGenerator } from '@nrwl/react';
import { Linter } from '@nrwl/linter';
import { reduxGenerator } from './redux';
describe('redux', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
name: 'my-lib',
linter: Linter.EsLint,
skipFormat: false,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'jest',
});
});
it('should add dependencies', async () => {
await reduxGenerator(appTree, {
name: 'my-slice',
project: 'my-lib',
});
const packageJson = readJson(appTree, '/package.json');
expect(packageJson.dependencies['@reduxjs/toolkit']).toBeDefined();
expect(packageJson.dependencies['react-redux']).toBeDefined();
});
it('should add slice and spec files', async () => {
await reduxGenerator(appTree, {
name: 'my-slice',
project: 'my-lib',
});
expect(
appTree.exists('/libs/my-lib/src/lib/my-slice.slice.ts')
).toBeTruthy();
expect(
appTree.exists('/libs/my-lib/src/lib/my-slice.slice.spec.ts')
).toBeTruthy();
});
describe('--appProject', () => {
it('should configure app main', async () => {
await applicationGenerator(appTree, {
babelJest: false,
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
style: 'css',
unitTestRunner: 'none',
name: 'my-app',
});
await reduxGenerator(appTree, {
name: 'my-slice',
project: 'my-lib',
appProject: 'my-app',
});
await reduxGenerator(appTree, {
name: 'another-slice',
project: 'my-lib',
appProject: 'my-app',
});
await reduxGenerator(appTree, {
name: 'third-slice',
project: 'my-lib',
appProject: 'my-app',
});
const main = appTree.read('/apps/my-app/src/main.tsx').toString();
expect(main).toContain('@reduxjs/toolkit');
expect(main).toContain('configureStore');
expect(main).toContain('[THIRD_SLICE_FEATURE_KEY]: thirdSliceReducer,');
expect(main).toContain(
'[ANOTHER_SLICE_FEATURE_KEY]: anotherSliceReducer,'
);
expect(main).toContain('[MY_SLICE_FEATURE_KEY]: mySliceReducer');
expect(main).toMatch(/<Provider store={store}>/);
});
it('should throw error for lib project', async () => {
await expect(
reduxGenerator(appTree, {
name: 'my-slice',
project: 'my-lib',
appProject: 'my-lib',
})
).rejects.toThrow(/Expected m/);
});
});
});

View File

@ -0,0 +1,201 @@
import * as path from 'path';
import * as ts from 'typescript';
import {
addImport,
addReduxStoreToMain,
updateReduxStore,
} from '../../utils/ast-utils';
import {
reactReduxVersion,
reduxjsToolkitVersion,
typesReactReduxVersion,
} from '../../utils/versions';
import { NormalizedSchema, Schema } from './schema';
import {
addDependenciesToPackageJson,
applyChangesToString,
convertNxGenerator,
formatFiles,
generateFiles,
getProjects,
joinPathFragments,
names,
readJson,
toJS,
Tree,
} from '@nrwl/devkit';
export async function reduxGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema);
generateReduxFiles(host, options);
addExportsToBarrel(host, options);
addReduxPackageDependencies(host);
addStoreConfiguration(host, options);
updateReducerConfiguration(host, options);
await formatFiles(host);
}
function generateReduxFiles(host: Tree, options: NormalizedSchema) {
generateFiles(
host,
joinPathFragments(__dirname, './files'),
options.filesPath,
{
...options,
tmpl: '',
}
);
if (options.js) {
toJS(host);
}
}
function addReduxPackageDependencies(host: Tree) {
addDependenciesToPackageJson(
host,
{
'@reduxjs/toolkit': reduxjsToolkitVersion,
'react-redux': reactReduxVersion,
},
{
'@types/react-redux': typesReactReduxVersion,
}
);
}
function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
const indexFilePath = path.join(
options.projectSourcePath,
options.js ? 'index.js' : '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
);
const statePath = options.directory
? `./lib/${options.directory}/${options.fileName}`
: `./lib/${options.fileName}`;
const changes = applyChangesToString(
indexSource,
addImport(indexSourceFile, `export * from '${statePath}.slice';`)
);
host.write(indexFilePath, changes);
}
}
function addStoreConfiguration(host: Tree, options: NormalizedSchema) {
if (!options.appProjectSourcePath) {
return;
}
const mainSource = host.read(options.appMainFilePath).toString();
if (!mainSource.includes('redux')) {
const mainSourceFile = ts.createSourceFile(
options.appMainFilePath,
mainSource,
ts.ScriptTarget.Latest,
true
);
const changes = applyChangesToString(
mainSource,
addReduxStoreToMain(options.appMainFilePath, mainSourceFile)
);
host.write(options.appMainFilePath, changes);
}
}
function updateReducerConfiguration(host: Tree, options: NormalizedSchema) {
if (!options.appProjectSourcePath) {
return;
}
const mainSource = host.read(options.appMainFilePath).toString();
const mainSourceFile = ts.createSourceFile(
options.appMainFilePath,
mainSource,
ts.ScriptTarget.Latest,
true
);
const changes = applyChangesToString(
mainSource,
updateReduxStore(options.appMainFilePath, mainSourceFile, {
keyName: `${options.constantName}_FEATURE_KEY`,
reducerName: `${options.propertyName}Reducer`,
modulePath: `${options.projectModulePath}`,
})
);
host.write(options.appMainFilePath, changes);
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
let appProjectSourcePath: string;
let appMainFilePath: string;
const extraNames = names(options.name);
const projects = getProjects(host);
const project = projects.get(options.project);
const { sourceRoot, projectType } = project;
const tsConfigJson = readJson(host, 'tsconfig.base.json');
const tsPaths: { [module: string]: string[] } = tsConfigJson.compilerOptions
? tsConfigJson.compilerOptions.paths || {}
: {};
const modulePath =
projectType === 'application'
? options.directory
? `./app/${options.directory}/${extraNames.fileName}.slice`
: `./app/${extraNames.fileName}.slice`
: Object.keys(tsPaths).find((k) =>
tsPaths[k].some((s) => s.includes(sourceRoot))
);
// If --project is set to an app, automatically configure store
// for it without needing to specify --appProject.
options.appProject =
options.appProject ||
(projectType === 'application' ? options.project : undefined);
if (options.appProject) {
const appConfig = projects.get(options.appProject);
if (appConfig.projectType !== 'application') {
throw new Error(
`Expected ${options.appProject} to be an application but got ${appConfig.projectType}`
);
}
appProjectSourcePath = appConfig.sourceRoot;
appMainFilePath = path.join(
appProjectSourcePath,
options.js ? 'main.js' : 'main.tsx'
);
if (!host.exists(appMainFilePath)) {
throw new Error(
`Could not find ${appMainFilePath} during store configuration`
);
}
}
return {
...options,
...extraNames,
constantName: names(options.name).constantName.toUpperCase(),
directory: names(options.directory ?? '').fileName,
projectType,
projectSourcePath: sourceRoot,
projectModulePath: modulePath,
appProjectSourcePath,
appMainFilePath,
filesPath: joinPathFragments(
sourceRoot,
projectType === 'application' ? 'app' : 'lib'
),
};
}
export default reduxGenerator;
export const reduxSchematic = convertNxGenerator(reduxGenerator);

View File

@ -1,20 +1,18 @@
import { Path } from '@angular-devkit/core';
export interface Schema {
name: string;
project: string;
directory: string;
appProject: string;
directory?: string;
appProject?: string;
js?: string;
}
interface NormalizedSchema extends Schema {
projectType: string;
projectSourcePath: Path;
projectSourcePath: string;
projectModulePath: string;
appProjectSourcePath: Path;
appProjectSourcePath: string;
appMainFilePath: string;
filesPath: Path;
filesPath: string;
className: string;
constantName: string;
propertyName: string;

View File

@ -0,0 +1,107 @@
import { Tree } from '@nrwl/devkit';
import storiesGenerator from './stories';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import applicationGenerator from '../application/application';
import { Linter } from '@nrwl/linter';
describe('react:stories for applications', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = await createTestUIApp('test-ui-app');
// create another component
appTree.write(
'apps/test-ui-app/src/app/anothercmp/another-cmp.tsx',
`import React from 'react';
import './test.scss';
export interface TestProps {
name: string;
displayAge: boolean;
}
export const Test = (props: TestProps) => {
return (
<div>
<h1>Welcome to test component, {props.name}</h1>
</div>
);
};
export default Test;
`
);
});
it('should create the stories', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
generateCypressSpecs: false,
});
expect(
appTree.exists('apps/test-ui-app/src/app/app.stories.tsx')
).toBeTruthy();
expect(
appTree.exists(
'apps/test-ui-app/src/app/anothercmp/another-cmp.stories.tsx'
)
).toBeTruthy();
});
it('should generate Cypress specs', async () => {
await storiesGenerator(appTree, {
project: 'test-ui-app',
generateCypressSpecs: true,
});
expect(
appTree.exists('apps/test-ui-app-e2e/src/integration/app.spec.ts')
).toBeTruthy();
expect(
appTree.exists(
'apps/test-ui-app-e2e/src/integration/another-cmp/another-cmp.spec.ts'
)
).toBeTruthy();
});
it('should ignore files that do not contain components', async () => {
// create another component
appTree.write(
'apps/test-ui-app/src/app/some-utils.js',
`export const add = (a: number, b: number) => a + b;`
);
await storiesGenerator(appTree, {
project: 'test-ui-app',
generateCypressSpecs: false,
});
// should just create the story and not error, even though there's a js file
// not containing any react component
expect(
appTree.exists('apps/test-ui-app/src/app/app.stories.tsx')
).toBeTruthy();
});
});
export async function createTestUIApp(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await applicationGenerator(appTree, {
babelJest: false,
e2eTestRunner: 'cypress',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'none',
name: libName,
js: plainJS,
});
return appTree;
}

View File

@ -1,19 +1,18 @@
import { Tree, externalSchematic } from '@angular-devkit/schematics';
import { runSchematic, callRule } from '../../utils/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { StorybookStoriesSchema } from './stories';
import { Schema } from 'packages/react/src/schematics/application/schema';
import { Tree } from '@nrwl/devkit';
import storiesGenerator from './stories';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import applicationGenerator from '../application/application';
import { Linter } from '@nrwl/linter';
import libraryGenerator from '../library/library';
describe('react:stories for libraries', () => {
let appTree: Tree;
let tree: UnitTestTree;
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
// create another component
appTree.create(
appTree.write(
'libs/test-ui-lib/src/lib/anothercmp/another-cmp.tsx',
`import React from 'react';
@ -38,39 +37,34 @@ describe('react:stories for libraries', () => {
});
it('should create the stories', async () => {
tree = await runSchematic(
'stories',
<StorybookStoriesSchema>{
await storiesGenerator(appTree, {
project: 'test-ui-lib',
},
appTree
);
generateCypressSpecs: false,
});
expect(
tree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx')
appTree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx')
).toBeTruthy();
expect(
tree.exists('libs/test-ui-lib/src/lib/anothercmp/another-cmp.stories.tsx')
appTree.exists(
'libs/test-ui-lib/src/lib/anothercmp/another-cmp.stories.tsx'
)
).toBeTruthy();
});
it('should generate Cypress specs', async () => {
tree = await runSchematic(
'stories',
<StorybookStoriesSchema>{
await storiesGenerator(appTree, {
project: 'test-ui-lib',
generateCypressSpecs: true,
},
appTree
);
});
expect(
tree.exists(
appTree.exists(
'apps/test-ui-lib-e2e/src/integration/test-ui-lib/test-ui-lib.spec.ts'
)
).toBeTruthy();
expect(
tree.exists(
appTree.exists(
'apps/test-ui-lib-e2e/src/integration/another-cmp/another-cmp.spec.ts'
)
).toBeTruthy();
@ -80,23 +74,20 @@ describe('react:stories for libraries', () => {
it('should ignore files that do not contain components', async () => {
// create another component
appTree.create(
appTree.write(
'libs/test-ui-lib/src/lib/some-utils.js',
`export const add = (a: number, b: number) => a + b;`
);
tree = await runSchematic(
'stories',
<StorybookStoriesSchema>{
await storiesGenerator(appTree, {
project: 'test-ui-lib',
},
appTree
);
generateCypressSpecs: false,
});
// should just create the story and not error, even though there's a js file
// not containing any react component
expect(
tree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx')
appTree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx')
).toBeTruthy();
});
});
@ -105,23 +96,30 @@ export async function createTestUILib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = await callRule(
externalSchematic('@nrwl/react', 'library', {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'none',
name: libName,
}),
appTree
);
});
// create some Nx app that we'll use to generate the cypress
// spec into it. We don't need a real Cypress setup
appTree = await callRule(
externalSchematic('@nrwl/react', 'application', {
name: `${libName}-e2e`,
await applicationGenerator(appTree, {
babelJest: false,
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'none',
name: libName + '-e2e',
js: plainJS,
} as Partial<Schema>),
appTree
);
});
return appTree;
}

View File

@ -0,0 +1,122 @@
import componentStoryGenerator from '../component-story/component-story';
import componentCypressSpecGenerator from '../component-cypress-spec/component-cypress-spec';
import { getComponentName } from '../../utils/ast-utils';
import * as ts from 'typescript';
import {
convertNxGenerator,
getProjects,
joinPathFragments,
ProjectType,
Tree,
} from '@nrwl/devkit';
import { join } from 'path';
export interface StorybookStoriesSchema {
project: string;
generateCypressSpecs: boolean;
js?: boolean;
}
export function projectRootPath(
tree: Tree,
sourceRoot: string,
projectType: ProjectType
): string {
let projectDir = '';
if (projectType === 'application') {
// apps/test-app/src/app
projectDir = 'app';
} else if (projectType == 'library') {
// libs/test-lib/src/lib
projectDir = 'lib';
}
return joinPathFragments(sourceRoot, projectDir);
}
function containsComponentDeclaration(
tree: Tree,
componentPath: string
): boolean {
const contents = tree.read(componentPath);
if (!contents) {
throw new Error(`Failed to read ${componentPath}`);
}
const sourceFile = ts.createSourceFile(
componentPath,
contents.toString(),
ts.ScriptTarget.Latest,
true
);
return !!getComponentName(sourceFile);
}
export async function createAllStories(
tree: Tree,
projectName: string,
generateCypressSpecs: boolean,
js: boolean
) {
const projects = getProjects(tree);
const project = projects.get(projectName);
const { sourceRoot, projectType } = project;
const libPath = projectRootPath(tree, sourceRoot, projectType);
let componentPaths: string[] = [];
tree.listChanges().forEach((fileChange) => {
const filePath = fileChange.path;
if (!filePath.startsWith(libPath) || fileChange.type === 'DELETE') {
return;
}
if (
(filePath.endsWith('.tsx') && !filePath.endsWith('.spec.tsx')) ||
(filePath.endsWith('.js') && !filePath.endsWith('.spec.js')) ||
(filePath.endsWith('.jsx') && !filePath.endsWith('.spec.jsx'))
) {
componentPaths.push(filePath);
}
});
await Promise.all(
componentPaths.map(async (componentPath) => {
const relativeCmpDir = componentPath.replace(join(sourceRoot, '/'), '');
if (!containsComponentDeclaration(tree, componentPath)) {
return;
}
await componentStoryGenerator(tree, {
componentPath: relativeCmpDir,
project: projectName,
});
if (generateCypressSpecs) {
await componentCypressSpecGenerator(tree, {
project: projectName,
componentPath: relativeCmpDir,
js,
});
}
})
);
}
export async function storiesGenerator(
host: Tree,
schema: StorybookStoriesSchema
) {
await createAllStories(
host,
schema.project,
schema.generateCypressSpecs,
schema.js
);
}
export default storiesGenerator;
export const storiesSchematic = convertNxGenerator(storiesGenerator);

View File

@ -0,0 +1,166 @@
import * as fileUtils from '@nrwl/workspace/src/core/file-utils';
import { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import libraryGenerator from '../library/library';
import { Linter } from '@nrwl/linter';
import applicationGenerator from '../application/application';
import componentGenerator from '../component/component';
import storybookConfigurationGenerator from './configuration';
describe('react:storybook-configuration', () => {
let appTree;
beforeEach(async () => {
jest.spyOn(fileUtils, 'readPackageJson').mockReturnValue({
devDependencies: {
'@storybook/addon-essentials': '^6.0.21',
'@storybook/react': '^6.0.21',
},
});
});
it('should configure everything at once', async () => {
appTree = await createTestUILib('test-ui-lib');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
configureCypress: true,
});
expect(appTree.exists('libs/test-ui-lib/.storybook/main.js')).toBeTruthy();
expect(
appTree.exists('libs/test-ui-lib/.storybook/tsconfig.json')
).toBeTruthy();
expect(appTree.exists('apps/test-ui-lib-e2e/cypress.json')).toBeTruthy();
});
it('should generate stories for components', async () => {
appTree = await createTestUILib('test-ui-lib');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
generateStories: true,
configureCypress: false,
});
expect(
appTree.exists('libs/test-ui-lib/src/lib/test-ui-lib.stories.tsx')
).toBeTruthy();
});
it('should generate stories for components written in plain JS', async () => {
appTree = await createTestUILib('test-ui-lib', true);
appTree.write(
'libs/test-ui-lib/src/lib/test-ui-libplain.js',
`import React from 'react';
import './test.scss';
export const Test = (props) => {
return (
<div>
<h1>Welcome to test component</h1>
</div>
);
};
export default Test;
`
);
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
generateCypressSpecs: true,
generateStories: true,
configureCypress: false,
js: true,
});
expect(
appTree.exists('libs/test-ui-lib/src/lib/test-ui-libplain.stories.js')
).toBeTruthy();
});
it('should configure everything at once', async () => {
appTree = await createTestAppLib('test-ui-app');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-app',
configureCypress: true,
});
expect(appTree.exists('apps/test-ui-app/.storybook/main.js')).toBeTruthy();
expect(
appTree.exists('apps/test-ui-app/.storybook/tsconfig.json')
).toBeTruthy();
/**
* Note on the removal of
* expect(tree.exists('apps/test-ui-app-e2e/cypress.json')).toBeTruthy();
*
* When calling createTestAppLib() we do not generate an e2e suite.
* The storybook schematic for apps does not generate e2e test.
* So, there exists no test-ui-app-e2e!
*/
});
it('should generate stories for components', async () => {
appTree = await createTestAppLib('test-ui-app');
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-app',
generateStories: true,
configureCypress: false,
});
// Currently the auto-generate stories feature only picks up components under the 'lib' directory.
// In our 'createTestAppLib' function, we call @nrwl/react:component to generate a component
// under the specified 'lib' directory
expect(
appTree.exists(
'apps/test-ui-app/src/app/my-component/my-component.stories.tsx'
)
).toBeTruthy();
});
});
export async function createTestUILib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await libraryGenerator(appTree, {
linter: Linter.EsLint,
component: true,
skipFormat: true,
skipTsConfig: false,
style: 'css',
unitTestRunner: 'none',
name: libName,
});
return appTree;
}
export async function createTestAppLib(
libName: string,
plainJS = false
): Promise<Tree> {
let appTree = createTreeWithEmptyWorkspace();
await applicationGenerator(appTree, {
babelJest: false,
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: false,
style: 'css',
unitTestRunner: 'none',
name: libName,
js: plainJS,
});
await componentGenerator(appTree, {
name: 'my-component',
project: libName,
directory: 'app',
style: 'css',
});
return appTree;
}

View File

@ -0,0 +1,35 @@
import { StorybookConfigureSchema } from './schema';
import storiesGenerator from '../stories/stories';
import { convertNxGenerator, Tree } from '@nrwl/devkit';
import { configurationGenerator } from '@nrwl/storybook';
async function generateStories(host: Tree, schema: StorybookConfigureSchema) {
await storiesGenerator(host, {
project: schema.name,
generateCypressSpecs:
schema.configureCypress && schema.generateCypressSpecs,
js: schema.js,
});
}
export async function storybookConfigurationGenerator(
host: Tree,
schema: StorybookConfigureSchema
) {
await configurationGenerator(host, {
name: schema.name,
uiFramework: '@storybook/react',
configureCypress: schema.configureCypress,
js: schema.js,
linter: schema.linter,
});
if (schema.generateStories) {
await generateStories(host, schema);
}
}
export default storybookConfigurationGenerator;
export const storybookConfigurationSchematic = convertNxGenerator(
storybookConfigurationGenerator
);

View File

@ -1,4 +1,4 @@
import { Linter } from '@nrwl/workspace';
import { Linter } from '@nrwl/linter';
export interface StorybookConfigureSchema {
name: string;

View File

@ -0,0 +1,45 @@
import { createTestUILib } from '../stories/stories-lib.spec';
import { storybookVersion } from '@nrwl/storybook';
import { readJson, Tree, updateJson } from '@nrwl/devkit';
import storybookConfigurationGenerator from '../storybook-configuration/configuration';
import { storybookMigration5to6Generator } from '@nrwl/react';
describe('migrate-defaults-5-to-6 schematic', () => {
let appTree: Tree;
beforeEach(async () => {
appTree = await createTestUILib('test-ui-lib');
updateJson(appTree, 'package.json', (json) => {
return {
...json,
devDependencies: {
...json.devDependencies,
'@nrwl/storybook': '10.4.0',
'@nrwl/workspace': '10.4.0',
'@storybook/addon-knobs': '^5.3.8',
'@storybook/react': '^5.3.8',
},
};
});
await storybookConfigurationGenerator(appTree, {
name: 'test-ui-lib',
configureCypress: false,
generateCypressSpecs: false,
generateStories: false,
});
});
it('should update the correct dependencies', async () => {
storybookMigration5to6Generator(appTree, { all: true });
const packageJson = readJson(appTree, 'package.json');
// general deps
expect(packageJson.devDependencies['@storybook/react']).toEqual(
storybookVersion
);
expect(packageJson.devDependencies['@storybook/addon-knobs']).toEqual(
storybookVersion
);
});
});

View File

@ -0,0 +1,20 @@
import { StorybookMigrateDefault5to6Schema } from './schema';
import { convertNxGenerator, Tree } from '@nrwl/devkit';
import { migrateDefaultsGenerator } from '@nrwl/storybook';
export function storybookMigration5to6Generator(
host: Tree,
schema: StorybookMigrateDefault5to6Schema
) {
return migrateDefaultsGenerator(host, {
name: schema.name,
all: schema.all,
keepOld: schema.keepOld,
});
}
export default storybookMigration5to6Generator;
export const storybookMigration5to6Schematic = convertNxGenerator(
storybookMigration5to6Generator
);

View File

@ -1,13 +1,14 @@
import { noop, Rule } from '@angular-devkit/schematics';
import { addDepsToPackageJson } from '@nrwl/workspace';
import { CSS_IN_JS_DEPENDENCIES } from '../utils/styled';
import { addDependenciesToPackageJson, Tree } from '@nrwl/devkit';
export function addStyledModuleDependencies(styledModule: string): Rule {
export function addStyledModuleDependencies(host: Tree, styledModule: string) {
const extraDependencies = CSS_IN_JS_DEPENDENCIES[styledModule];
return extraDependencies
? addDepsToPackageJson(
if (extraDependencies) {
return addDependenciesToPackageJson(
host,
extraDependencies.dependencies,
extraDependencies.devDependencies
)
: noop();
);
}
}

View File

@ -1,41 +1,31 @@
import { Tree } from '@angular-devkit/schematics';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { readJsonInTree } from '@nrwl/workspace';
import { readJson, Tree } from '@nrwl/devkit';
import { updateBabelJestConfig } from './update-babel-jest-config';
import { callRule } from '@nrwl/workspace/src/utils/testing';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
describe('updateBabelJestConfig', () => {
let tree: Tree;
beforeEach(() => {
tree = Tree.empty();
tree = createEmptyWorkspace(tree);
tree = createTreeWithEmptyWorkspace();
});
it('should update babel-jest.config.json', async () => {
tree.create('/apps/demo/babel-jest.config.json', JSON.stringify({}));
tree.write('/apps/demo/babel-jest.config.json', JSON.stringify({}));
tree = await callRule(
updateBabelJestConfig('/apps/demo', (json) => {
updateBabelJestConfig(tree, '/apps/demo', (json) => {
json.plugins = ['test'];
return json;
}),
tree
);
});
const config = readJsonInTree(tree, '/apps/demo/babel-jest.config.json');
const config = readJson(tree, '/apps/demo/babel-jest.config.json');
expect(config.plugins).toEqual(['test']);
});
it('should do nothing if project does not use babel jest', async () => {
tree = await callRule(
updateBabelJestConfig('/apps/demo', (json) => {
updateBabelJestConfig(tree, '/apps/demo', (json) => {
json.plugins = ['test'];
return json;
}),
tree
);
});
expect(tree.exists('/apps/demo/babel-jest.config.json')).toBe(false);
});
});

View File

@ -1,16 +1,14 @@
import { noop, Tree } from '@angular-devkit/schematics';
import { updateJsonInTree } from '@nrwl/workspace';
import { Tree, updateJson } from '@nrwl/devkit';
type BabelJestConfigUpdater<T> = (json: T) => T;
export function updateBabelJestConfig<T = any>(
host: Tree,
projectRoot: string,
update: BabelJestConfigUpdater<T>
) {
return (host: Tree) => {
const configPath = `${projectRoot}/babel-jest.config.json`;
return host.exists(configPath)
? updateJsonInTree<T>(configPath, update)
: noop();
};
if (host.exists(configPath)) {
updateJson(host, configPath, update);
}
}

View File

@ -1,693 +0,0 @@
import { Tree } from '@angular-devkit/schematics';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import * as stripJsonComments from 'strip-json-comments';
import { NxJson, readJsonInTree } from '@nrwl/workspace';
import { runSchematic } from '../../utils/testing';
describe('app', () => {
let appTree: Tree;
beforeEach(() => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
});
describe('not nested', () => {
it('should update workspace.json', async () => {
const tree = await runSchematic('app', { name: 'myApp' }, appTree);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-app'].root).toEqual('apps/my-app');
expect(workspaceJson.projects['my-app-e2e'].root).toEqual(
'apps/my-app-e2e'
);
expect(workspaceJson.defaultProject).toEqual('my-app');
});
it('should update nx.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', tags: 'one,two' },
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
expect(nxJson.projects).toEqual({
'my-app': {
tags: ['one', 'two'],
},
'my-app-e2e': {
tags: [],
implicitDependencies: ['my-app'],
},
});
});
it('should generate files', async () => {
const tree = await runSchematic('app', { name: 'myApp' }, appTree);
expect(tree.exists('apps/my-app/.babelrc')).toBeTruthy();
expect(tree.exists('apps/my-app/.browserslistrc')).toBeTruthy();
expect(tree.exists('apps/my-app/src/main.tsx')).toBeTruthy();
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.module.css')).toBeTruthy();
const jestConfig = tree.readContent('apps/my-app/jest.config.js');
expect(jestConfig).toContain('@nrwl/react/plugins/jest');
const tsconfig = readJsonInTree(tree, 'apps/my-app/tsconfig.json');
expect(tsconfig.references).toEqual([
{
path: './tsconfig.app.json',
},
{
path: './tsconfig.spec.json',
},
]);
const tsconfigApp = JSON.parse(
stripJsonComments(tree.readContent('apps/my-app/tsconfig.app.json'))
);
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
const eslintJson = JSON.parse(
stripJsonComments(tree.readContent('apps/my-app/.eslintrc.json'))
);
expect(eslintJson.extends).toEqual([
'plugin:@nrwl/nx/react',
'../../.eslintrc.json',
]);
expect(tree.exists('apps/my-app-e2e/cypress.json')).toBeTruthy();
const tsconfigE2E = JSON.parse(
stripJsonComments(tree.readContent('apps/my-app-e2e/tsconfig.e2e.json'))
);
expect(tsconfigE2E.compilerOptions.outDir).toEqual('../../dist/out-tsc');
expect(tsconfigE2E.extends).toEqual('./tsconfig.json');
});
});
describe('nested', () => {
it('should update workspace.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', directory: 'myDir' },
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.projects['my-dir-my-app'].root).toEqual(
'apps/my-dir/my-app'
);
expect(workspaceJson.projects['my-dir-my-app-e2e'].root).toEqual(
'apps/my-dir/my-app-e2e'
);
});
it('should update nx.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', directory: 'myDir', tags: 'one,two' },
appTree
);
const nxJson = readJsonInTree<NxJson>(tree, '/nx.json');
expect(nxJson.projects).toEqual({
'my-dir-my-app': {
tags: ['one', 'two'],
},
'my-dir-my-app-e2e': {
tags: [],
implicitDependencies: ['my-dir-my-app'],
},
});
});
it('should generate files', async () => {
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
const content = tree.readContent(path);
const config = JSON.parse(stripJsonComments(content));
expect(lookupFn(config)).toEqual(expectedValue);
};
const tree = await runSchematic(
'app',
{ name: 'myApp', directory: 'myDir' },
appTree
);
// Make sure these exist
[
'apps/my-dir/my-app/src/main.tsx',
'apps/my-dir/my-app/src/app/app.tsx',
'apps/my-dir/my-app/src/app/app.spec.tsx',
'apps/my-dir/my-app/src/app/app.module.css',
].forEach((path) => {
expect(tree.exists(path)).toBeTruthy();
});
// Make sure these have properties
[
{
path: 'apps/my-dir/my-app/tsconfig.app.json',
lookupFn: (json) => json.compilerOptions.outDir,
expectedValue: '../../../dist/out-tsc',
},
{
path: 'apps/my-dir/my-app-e2e/tsconfig.e2e.json',
lookupFn: (json) => json.compilerOptions.outDir,
expectedValue: '../../../dist/out-tsc',
},
{
path: 'apps/my-dir/my-app/.eslintrc.json',
lookupFn: (json) => json.extends,
expectedValue: ['plugin:@nrwl/nx/react', '../../../.eslintrc.json'],
},
].forEach(hasJsonValue);
});
});
it('should create Nx specific template', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', directory: 'myDir' },
appTree
);
expect(tree.readContent('apps/my-dir/my-app/src/app/app.tsx')).toBeTruthy();
expect(tree.readContent('apps/my-dir/my-app/src/app/app.tsx')).toContain(
'Welcome to my-app'
);
});
describe('--style scss', () => {
it('should generate scss styles', async () => {
const result = await runSchematic(
'app',
{ name: 'myApp', style: 'scss' },
appTree
);
expect(result.exists('apps/my-app/src/app/app.module.scss')).toEqual(
true
);
});
});
it('should setup jest with tsx support', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-app',
},
appTree
);
expect(tree.readContent('apps/my-app/jest.config.js')).toContain(
`moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],`
);
});
it('should setup jest without serializers', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-app',
},
appTree
);
expect(tree.readContent('apps/my-app/jest.config.js')).not.toContain(
`'jest-preset-angular/build/AngularSnapshotSerializer.js',`
);
});
it('should setup the nrwl web build builder', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-app',
},
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
const architectConfig = workspaceJson.projects['my-app'].architect;
expect(architectConfig.build.builder).toEqual('@nrwl/web:build');
expect(architectConfig.build.outputs).toEqual(['{options.outputPath}']);
expect(architectConfig.build.options).toEqual({
assets: ['apps/my-app/src/favicon.ico', 'apps/my-app/src/assets'],
index: 'apps/my-app/src/index.html',
main: 'apps/my-app/src/main.tsx',
outputPath: 'dist/apps/my-app',
polyfills: 'apps/my-app/src/polyfills.ts',
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',
webpackConfig: '@nrwl/react/plugins/webpack',
});
expect(architectConfig.build.configurations.production).toEqual({
optimization: true,
budgets: [
{
maximumError: '5mb',
maximumWarning: '2mb',
type: 'initial',
},
],
extractCss: true,
extractLicenses: true,
fileReplacements: [
{
replace: 'apps/my-app/src/environments/environment.ts',
with: 'apps/my-app/src/environments/environment.prod.ts',
},
],
namedChunks: false,
outputHashing: 'all',
sourceMap: false,
vendorChunk: false,
});
});
it('should setup the nrwl web dev server builder', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-app',
},
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
const architectConfig = workspaceJson.projects['my-app'].architect;
expect(architectConfig.serve.builder).toEqual('@nrwl/web:dev-server');
expect(architectConfig.serve.options).toEqual({
buildTarget: 'my-app:build',
});
expect(architectConfig.serve.configurations.production).toEqual({
buildTarget: 'my-app:build:production',
});
});
it('should setup the eslint builder', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-app',
},
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(workspaceJson.projects['my-app'].architect.lint).toEqual({
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['apps/my-app/**/*.{ts,tsx,js,jsx}'],
},
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', unitTestRunner: 'none' },
appTree
);
expect(tree.exists('jest.config.js')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.spec.tsx')).toBeFalsy();
expect(tree.exists('apps/my-app/tsconfig.spec.json')).toBeFalsy();
expect(tree.exists('apps/my-app/jest.config.js')).toBeFalsy();
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(workspaceJson.projects['my-app'].architect.test).toBeUndefined();
expect(workspaceJson.projects['my-app'].architect.lint)
.toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app/**/*.{ts,tsx,js,jsx}",
],
},
}
`);
});
});
describe('--e2e-test-runner none', () => {
it('should not generate test configuration', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', e2eTestRunner: 'none' },
appTree
);
expect(tree.exists('apps/my-app-e2e')).toBeFalsy();
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(workspaceJson.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.module.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/);
});
it('should add .eslintrc.json and dependencies', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', linter: 'eslint' },
appTree
);
const eslintJson = readJsonInTree(tree, '/apps/my-app/.eslintrc.json');
const packageJson = readJsonInTree(tree, '/package.json');
expect(eslintJson.extends).toEqual(
expect.arrayContaining(['plugin:@nrwl/nx/react'])
);
expect(packageJson.devDependencies.eslint).toBeDefined();
expect(packageJson.devDependencies['@nrwl/linter']).toBeDefined();
expect(packageJson.devDependencies['@nrwl/eslint-plugin-nx']).toBeDefined();
expect(packageJson.devDependencies['eslint-plugin-react']).toBeDefined();
expect(
packageJson.devDependencies['eslint-plugin-react-hooks']
).toBeDefined();
expect(
packageJson.devDependencies['@typescript-eslint/parser']
).toBeDefined();
expect(
packageJson.devDependencies['@typescript-eslint/eslint-plugin']
).toBeDefined();
expect(packageJson.devDependencies['eslint-config-prettier']).toBeDefined();
});
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 none', () => {
it('should not generate any styles', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: 'none' },
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')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.scss')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.styl')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.module.css')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.module.scss')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.module.styl')).toBeFalsy();
const content = tree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).not.toContain('styled-components');
expect(content).not.toContain('<StyledApp>');
expect(content).not.toContain('@emotion/styled');
expect(content).not.toContain('<StyledApp>');
//for imports
expect(content).not.toContain('app.styl');
expect(content).not.toContain('app.css');
expect(content).not.toContain('app.scss');
expect(content).not.toContain('app.module.styl');
expect(content).not.toContain('app.module.css');
expect(content).not.toContain('app.module.scss');
});
it('should set defaults when style: none', async () => {
const tree = await runSchematic(
'app',
{
name: 'myApp',
style: 'none',
},
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.schematics['@nrwl/react']).toMatchObject({
application: {
style: 'none',
},
component: {
style: 'none',
},
library: {
style: 'none',
},
});
});
it('should exclude styles from workspace.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: 'none' },
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(
workspaceJson.projects['my-app'].architect.build.options.styles
).toEqual([]);
});
});
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();
expect(
tree.exists('apps/my-app/src/styles.styled-components')
).toBeFalsy();
const content = tree.read('apps/my-app/src/app/app.tsx').toString();
expect(content).toContain('styled-component');
expect(content).toContain('<StyledApp>');
});
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('<StyledApp>');
});
it('should exclude styles from workspace.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: '@emotion/styled' },
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(
workspaceJson.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/react']).toBeDefined();
expect(packageJSON.dependencies['@emotion/styled']).toBeDefined();
});
});
describe('--style styled-jsx', () => {
it('should use styled-jsx as the styled API library', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: 'styled-jsx' },
appTree
);
expect(tree.exists('apps/my-app/src/app/app.styled-jsx')).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('<style jsx>');
});
it('should add dependencies to package.json', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: 'styled-jsx' },
appTree
);
const packageJSON = readJsonInTree(tree, 'package.json');
expect(packageJSON.dependencies['styled-jsx']).toBeDefined();
});
it('should update babel config', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', style: 'styled-jsx' },
appTree
);
const babelrc = readJsonInTree(tree, 'apps/my-app/.babelrc');
const babelJestConfig = readJsonInTree(
tree,
'apps/my-app/babel-jest.config.json'
);
expect(babelrc.plugins).toContain('styled-jsx/babel');
expect(babelJestConfig.plugins).toContain('styled-jsx/babel');
});
});
describe('--routing', () => {
it('should add routes to the App component', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', routing: true },
appTree
);
const mainSource = tree.read('apps/my-app/src/main.tsx').toString();
const componentSource = tree
.read('apps/my-app/src/app/app.tsx')
.toString();
expect(mainSource).toContain('react-router-dom');
expect(mainSource).toContain('<BrowserRouter>');
expect(mainSource).toContain('</BrowserRouter>');
expect(componentSource).toMatch(/<Route\s*path="\/"/);
expect(componentSource).toMatch(/<Link\s*to="\/"/);
});
});
it('should adds custom webpack config', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', babel: true },
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(
workspaceJson.projects['my-app'].architect.build.options.webpackConfig
).toEqual('@nrwl/react/plugins/webpack');
});
it('should add required polyfills for core-js and regenerator', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', babel: true },
appTree
);
const polyfillsSource = tree
.read('apps/my-app/src/polyfills.ts')
.toString();
expect(polyfillsSource).toContain('regenerator');
expect(polyfillsSource).toContain('core-js');
});
describe('--skipWorkspaceJson', () => {
it('should update workspace with defaults when --skipWorkspaceJson=false', async () => {
const tree = await runSchematic(
'app',
{
name: 'myApp',
babel: true,
style: 'styled-components',
skipWorkspaceJson: false,
},
appTree
);
const workspaceJson = readJsonInTree(tree, '/workspace.json');
expect(workspaceJson.schematics['@nrwl/react']).toMatchObject({
application: {
babel: true,
style: 'styled-components',
},
component: {
style: 'styled-components',
},
library: {
style: 'styled-components',
},
});
});
});
describe('--js', () => {
it('generates JS files', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', js: true },
appTree
);
expect(tree.exists('/apps/my-app/src/app/app.js')).toBe(true);
expect(tree.exists('/apps/my-app/src/main.js')).toBe(true);
});
});
});

View File

@ -1,52 +0,0 @@
import {
chain,
Rule,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import { addLintFiles, formatFiles } from '@nrwl/workspace';
import { extraEslintDependencies, reactEslintJson } from '../../utils/lint';
import init from '../init/init';
import { Schema } from './schema';
import { createApplicationFiles } from './lib/create-application-files';
import { updateJestConfig } from './lib/update-jest-config';
import { normalizeOptions } from './lib/normalize-options';
import { addProject } from './lib/add-project';
import { addCypress } from './lib/add-cypress';
import { addJest } from './lib/add-jest';
import { addRouting } from './lib/add-routing';
import { setDefaults } from './lib/set-defaults';
import { updateNxJson } from './lib/update-nx-json';
import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
export default function (schema: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(host, schema);
return chain([
init({
...options,
skipFormat: true,
}),
addLintFiles(options.appProjectRoot, options.linter, {
localConfig: reactEslintJson,
extraPackageDeps: extraEslintDependencies,
}),
createApplicationFiles(options),
updateNxJson(options),
addProject(options),
addCypress(options),
addJest(options),
updateJestConfig(options),
addStyledModuleDependencies(options.styledModule),
addRouting(options, context),
setDefaults(options),
formatFiles(options),
]);
};
}
export const applicationGenerator = wrapAngularDevkitSchematic(
'@nrwl/react',
'application'
);

View File

@ -1,13 +0,0 @@
import { externalSchematic, noop, Rule } from '@angular-devkit/schematics';
import { NormalizedSchema } from '../schema';
export 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();
}

View File

@ -1,14 +0,0 @@
import { externalSchematic, noop, Rule } from '@angular-devkit/schematics';
import { NormalizedSchema } from '../schema';
export function addJest(options: NormalizedSchema): Rule {
return options.unitTestRunner === 'jest'
? externalSchematic('@nrwl/jest', 'jest-project', {
project: options.projectName,
supportTsx: true,
skipSerializers: true,
setupFile: 'none',
babelJest: true,
})
: noop();
}

View File

@ -1,101 +0,0 @@
import { Rule } from '@angular-devkit/schematics';
import { generateProjectLint, updateWorkspaceInTree } from '@nrwl/workspace';
import { join, normalize } from '@angular-devkit/core';
import { NormalizedSchema } from '../schema';
export function addProject(options: NormalizedSchema): Rule {
return updateWorkspaceInTree((json) => {
const architect: { [key: string]: any } = {};
architect.build = {
builder: '@nrwl/web:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: join(normalize('dist'), options.appProjectRoot),
index: join(options.appProjectRoot, 'src/index.html'),
main: join(options.appProjectRoot, maybeJs(options, `src/main.tsx`)),
polyfills: join(
options.appProjectRoot,
maybeJs(options, 'src/polyfills.ts')
),
tsConfig: join(options.appProjectRoot, 'tsconfig.app.json'),
assets: [
join(options.appProjectRoot, 'src/favicon.ico'),
join(options.appProjectRoot, 'src/assets'),
],
styles:
options.styledModule || !options.hasStyles
? []
: [join(options.appProjectRoot, `src/styles.${options.style}`)],
scripts: [],
webpackConfig: '@nrwl/react/plugins/webpack',
},
configurations: {
production: {
fileReplacements: [
{
replace: join(
options.appProjectRoot,
maybeJs(options, `src/environments/environment.ts`)
),
with: join(
options.appProjectRoot,
maybeJs(options, `src/environments/environment.prod.ts`)
),
},
],
optimization: true,
outputHashing: 'all',
sourceMap: false,
extractCss: true,
namedChunks: false,
extractLicenses: true,
vendorChunk: false,
budgets: [
{
type: 'initial',
maximumWarning: '2mb',
maximumError: '5mb',
},
],
},
},
};
architect.serve = {
builder: '@nrwl/web:dev-server',
options: {
buildTarget: `${options.projectName}:build`,
},
configurations: {
production: {
buildTarget: `${options.projectName}:build:production`,
},
},
};
architect.lint = generateProjectLint(
normalize(options.appProjectRoot),
join(normalize(options.appProjectRoot), 'tsconfig.app.json'),
options.linter,
[`${options.appProjectRoot}/**/*.{ts,tsx,js,jsx}`]
);
json.projects[options.projectName] = {
root: options.appProjectRoot,
sourceRoot: join(options.appProjectRoot, 'src'),
projectType: 'application',
architect,
};
json.defaultProject = json.defaultProject || options.projectName;
return json;
});
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}

View File

@ -1,51 +0,0 @@
import * as ts from 'typescript';
import {
chain,
noop,
Rule,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import { join } from '@angular-devkit/core';
import { addDepsToPackageJson, insert } from '@nrwl/workspace';
import { addInitialRoutes } from '../../../utils/ast-utils';
import { NormalizedSchema } from '../schema';
import {
reactRouterDomVersion,
typesReactRouterDomVersion,
} from '../../../../src/utils/versions';
export function addRouting(
options: NormalizedSchema,
context: SchematicContext
): Rule {
return options.routing
? chain([
function addRouterToComponent(host: Tree) {
const appPath = join(
options.appProjectRoot,
maybeJs(options, `src/app/${options.fileName}.tsx`)
);
const appFileContent = host.read(appPath).toString('utf-8');
const appSource = ts.createSourceFile(
appPath,
appFileContent,
ts.ScriptTarget.Latest,
true
);
insert(host, appPath, addInitialRoutes(appPath, appSource, context));
},
addDepsToPackageJson(
{ 'react-router-dom': reactRouterDomVersion },
{ '@types/react-router-dom': typesReactRouterDomVersion }
),
])
: noop();
}
function maybeJs(options: NormalizedSchema, path: string): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
? path.replace(/\.tsx?$/, '.js')
: path;
}

View File

@ -1,56 +0,0 @@
import {
apply,
chain,
filter,
mergeWith,
move,
noop,
Rule,
template,
url,
} from '@angular-devkit/schematics';
import { toJS } from '@nrwl/workspace/src/utils/rules/to-js';
import { NormalizedSchema } from '../schema';
import { names, offsetFromRoot } from '@nrwl/devkit';
export function createApplicationFiles(options: NormalizedSchema): Rule {
let styleSolutionSpecificAppFiles: string;
if (options.styledModule && options.style !== 'styled-jsx') {
styleSolutionSpecificAppFiles = './files/styled-module';
} else if (options.style === 'styled-jsx') {
styleSolutionSpecificAppFiles = './files/styled-jsx';
} else if (options.style === 'none') {
styleSolutionSpecificAppFiles = './files/none';
} else if (options.globalCss) {
styleSolutionSpecificAppFiles = './files/global-css';
} else {
styleSolutionSpecificAppFiles = './files/css-module';
}
const templateVariables = {
...names(options.name),
...options,
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
};
return chain([
mergeWith(
apply(url(`./files/common`), [
template(templateVariables),
options.unitTestRunner === 'none'
? filter((file) => file !== `/src/app/${options.fileName}.spec.tsx`)
: noop(),
move(options.appProjectRoot),
options.js ? toJS() : noop(),
])
),
mergeWith(
apply(url(styleSolutionSpecificAppFiles), [
template(templateVariables),
move(options.appProjectRoot),
options.js ? toJS() : noop(),
])
),
]);
}

Some files were not shown because too many files have changed in this diff Show More