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:
parent
651f3b60e9
commit
d9aef75bd5
@ -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';
|
||||
|
||||
26
packages/devkit/src/generators/set-default-collection.ts
Normal file
26
packages/devkit/src/generators/set-default-collection.ts
Normal 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);
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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",
|
||||
|
||||
619
packages/react/src/generators/application/application.spec.ts
Normal file
619
packages/react/src/generators/application/application.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
82
packages/react/src/generators/application/application.ts
Normal file
82
packages/react/src/generators/application/application.ts
Normal 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);
|
||||
@ -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 }]<% } %>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 347 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
16
packages/react/src/generators/application/lib/add-cypress.ts
Normal file
16
packages/react/src/generators/application/lib/add-cypress.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
17
packages/react/src/generators/application/lib/add-jest.ts
Normal file
17
packages/react/src/generators/application/lib/add-jest.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
114
packages/react/src/generators/application/lib/add-project.ts
Normal file
114
packages/react/src/generators/application/lib/add-project.ts
Normal 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`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
50
packages/react/src/generators/application/lib/add-routing.ts
Normal file
50
packages/react/src/generators/application/lib/add-routing.ts
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
@ -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>{
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS,
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentCypressSpecGenerator(appTree, {
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS,
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS,
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentCypressSpecGenerator(appTree, {
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS,
|
||||
});
|
||||
});
|
||||
|
||||
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', {
|
||||
name: libName,
|
||||
js: plainJS,
|
||||
}),
|
||||
appTree
|
||||
);
|
||||
let appTree = createTreeWithEmptyWorkspace();
|
||||
await libraryGenerator(appTree, {
|
||||
name: libName,
|
||||
linter: Linter.EsLint,
|
||||
js: plainJS,
|
||||
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`,
|
||||
js: plainJS,
|
||||
}),
|
||||
appTree
|
||||
);
|
||||
await applicationGenerator(appTree, {
|
||||
babelJest: false,
|
||||
js: plainJS,
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
name: `${libName}-e2e`,
|
||||
skipFormat: true,
|
||||
style: 'css',
|
||||
unitTestRunner: 'none',
|
||||
});
|
||||
|
||||
return appTree;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
@ -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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
} 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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: 'lib/test-ui-libplain.jsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-libplain.jsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await componentStoryGenerator(appTree, {
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib',
|
||||
});
|
||||
});
|
||||
|
||||
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', {
|
||||
name: libName,
|
||||
}),
|
||||
appTree
|
||||
);
|
||||
let appTree = createTreeWithEmptyWorkspace();
|
||||
await libraryGenerator(appTree, {
|
||||
name: libName,
|
||||
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;
|
||||
150
packages/react/src/generators/component-story/component-story.ts
Normal file
150
packages/react/src/generators/component-story/component-story.ts
Normal 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
|
||||
);
|
||||
@ -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 %>),<% } %>
|
||||
};
|
||||
<% } %>
|
||||
|
||||
356
packages/react/src/generators/component/component.spec.ts
Normal file
356
packages/react/src/generators/component/component.spec.ts
Normal 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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
212
packages/react/src/generators/component/component.ts
Normal file
212
packages/react/src/generators/component/component.ts
Normal 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);
|
||||
@ -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>
|
||||
@ -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;
|
||||
42
packages/react/src/generators/init/init.spec.ts
Normal file
42
packages/react/src/generators/init/init.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
77
packages/react/src/generators/init/init.ts
Normal file
77
packages/react/src/generators/init/init.ts
Normal 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);
|
||||
@ -1,4 +1,4 @@
|
||||
export interface Schema {
|
||||
export interface InitSchema {
|
||||
unitTestRunner: 'jest' | 'none';
|
||||
e2eTestRunner: 'cypress' | 'none';
|
||||
skipFormat: boolean;
|
||||
@ -2,6 +2,7 @@
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"id": "NxReactNgInit",
|
||||
"title": "Init React Plugin",
|
||||
"cli": "nx",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"unitTestRunner": {
|
||||
@ -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 }]<% } %>
|
||||
543
packages/react/src/generators/library/library.spec.ts
Normal file
543
packages/react/src/generators/library/library.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
418
packages/react/src/generators/library/library.ts
Normal file
418
packages/react/src/generators/library/library.ts
Normal 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);
|
||||
@ -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;
|
||||
95
packages/react/src/generators/redux/redux.spec.ts
Normal file
95
packages/react/src/generators/redux/redux.spec.ts
Normal 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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
201
packages/react/src/generators/redux/redux.ts
Normal file
201
packages/react/src/generators/redux/redux.ts
Normal 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);
|
||||
@ -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;
|
||||
107
packages/react/src/generators/stories/stories-app.spec.ts
Normal file
107
packages/react/src/generators/stories/stories-app.spec.ts
Normal 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;
|
||||
}
|
||||
@ -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>{
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await storiesGenerator(appTree, {
|
||||
project: 'test-ui-lib',
|
||||
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>{
|
||||
project: 'test-ui-lib',
|
||||
generateCypressSpecs: true,
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await storiesGenerator(appTree, {
|
||||
project: 'test-ui-lib',
|
||||
generateCypressSpecs: true,
|
||||
});
|
||||
|
||||
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>{
|
||||
project: 'test-ui-lib',
|
||||
},
|
||||
appTree
|
||||
);
|
||||
await storiesGenerator(appTree, {
|
||||
project: 'test-ui-lib',
|
||||
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', {
|
||||
name: libName,
|
||||
}),
|
||||
appTree
|
||||
);
|
||||
let appTree = createTreeWithEmptyWorkspace();
|
||||
|
||||
await libraryGenerator(appTree, {
|
||||
linter: Linter.EsLint,
|
||||
component: true,
|
||||
skipFormat: true,
|
||||
skipTsConfig: false,
|
||||
style: 'css',
|
||||
unitTestRunner: 'none',
|
||||
name: libName,
|
||||
});
|
||||
|
||||
// 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`,
|
||||
js: plainJS,
|
||||
} as Partial<Schema>),
|
||||
appTree
|
||||
);
|
||||
|
||||
await applicationGenerator(appTree, {
|
||||
babelJest: false,
|
||||
e2eTestRunner: 'none',
|
||||
linter: Linter.EsLint,
|
||||
skipFormat: false,
|
||||
style: 'css',
|
||||
unitTestRunner: 'none',
|
||||
name: libName + '-e2e',
|
||||
js: plainJS,
|
||||
});
|
||||
return appTree;
|
||||
}
|
||||
122
packages/react/src/generators/stories/stories.ts
Normal file
122
packages/react/src/generators/stories/stories.ts
Normal 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);
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
@ -1,4 +1,4 @@
|
||||
import { Linter } from '@nrwl/workspace';
|
||||
import { Linter } from '@nrwl/linter';
|
||||
|
||||
export interface StorybookConfigureSchema {
|
||||
name: string;
|
||||
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
);
|
||||
@ -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(
|
||||
extraDependencies.dependencies,
|
||||
extraDependencies.devDependencies
|
||||
)
|
||||
: noop();
|
||||
|
||||
if (extraDependencies) {
|
||||
return addDependenciesToPackageJson(
|
||||
host,
|
||||
extraDependencies.dependencies,
|
||||
extraDependencies.devDependencies
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
json.plugins = ['test'];
|
||||
return json;
|
||||
}),
|
||||
tree
|
||||
);
|
||||
updateBabelJestConfig(tree, '/apps/demo', (json) => {
|
||||
json.plugins = ['test'];
|
||||
return json;
|
||||
});
|
||||
|
||||
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) => {
|
||||
json.plugins = ['test'];
|
||||
return json;
|
||||
}),
|
||||
tree
|
||||
);
|
||||
|
||||
updateBabelJestConfig(tree, '/apps/demo', (json) => {
|
||||
json.plugins = ['test'];
|
||||
return json;
|
||||
});
|
||||
expect(tree.exists('/apps/demo/babel-jest.config.json')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
};
|
||||
const configPath = `${projectRoot}/babel-jest.config.json`;
|
||||
if (host.exists(configPath)) {
|
||||
updateJson(host, configPath, update);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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'
|
||||
);
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user