feat(react): improve storybook schematics (#2831)
This commit is contained in:
parent
b1b6b84af9
commit
a47c24c05f
45
docs/angular/api-react/schematics/component-cypress-spec.md
Normal file
45
docs/angular/api-react/schematics/component-cypress-spec.md
Normal file
@ -0,0 +1,45 @@
|
||||
# component-cypress-spec
|
||||
|
||||
Create a cypress spec for a ui component that has a story
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate component-cypress-spec ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `component-cypress-spec` in the default collection provisioned in `angular.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:component-cypress-spec ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g component-cypress-spec ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### componentPath
|
||||
|
||||
Type: `string`
|
||||
|
||||
Relative path to the component file from the library root?
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Generate JavaScript files rather than TypeScript files
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The project name for which to generate tests
|
||||
37
docs/angular/api-react/schematics/component-story.md
Normal file
37
docs/angular/api-react/schematics/component-story.md
Normal file
@ -0,0 +1,37 @@
|
||||
# component-story
|
||||
|
||||
Generate storybook story for a react component
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate component-story ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `component-story` in the default collection provisioned in `angular.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:component-story ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g component-story ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### componentPath
|
||||
|
||||
Type: `string`
|
||||
|
||||
Relative path to the component file from the library root?
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The project name where to add the components
|
||||
45
docs/angular/api-react/schematics/stories.md
Normal file
45
docs/angular/api-react/schematics/stories.md
Normal file
@ -0,0 +1,45 @@
|
||||
# stories
|
||||
|
||||
Create stories/specs for all components declared in a library
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate stories ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `stories` in the default collection provisioned in `angular.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:stories ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g stories ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### generateCypressSpecs
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Automatically generate \*.spec.ts files in the cypress e2e app generated by the cypress-configure schematic
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Generate JavaScript files rather than TypeScript files
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The library project name
|
||||
@ -30,6 +30,12 @@ Type: `boolean`
|
||||
|
||||
Run the cypress-configure schematic
|
||||
|
||||
### generateStories
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Automatically generate \*.stories.ts files for components declared in this library
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
45
docs/react/api-react/schematics/component-cypress-spec.md
Normal file
45
docs/react/api-react/schematics/component-cypress-spec.md
Normal file
@ -0,0 +1,45 @@
|
||||
# component-cypress-spec
|
||||
|
||||
Create a cypress spec for a ui component that has a story
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate component-cypress-spec ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `component-cypress-spec` in the default collection provisioned in `workspace.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:component-cypress-spec ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g component-cypress-spec ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### componentPath
|
||||
|
||||
Type: `string`
|
||||
|
||||
Relative path to the component file from the library root?
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Generate JavaScript files rather than TypeScript files
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The project name for which to generate tests
|
||||
37
docs/react/api-react/schematics/component-story.md
Normal file
37
docs/react/api-react/schematics/component-story.md
Normal file
@ -0,0 +1,37 @@
|
||||
# component-story
|
||||
|
||||
Generate storybook story for a react component
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate component-story ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `component-story` in the default collection provisioned in `workspace.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:component-story ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g component-story ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### componentPath
|
||||
|
||||
Type: `string`
|
||||
|
||||
Relative path to the component file from the library root?
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The project name where to add the components
|
||||
45
docs/react/api-react/schematics/stories.md
Normal file
45
docs/react/api-react/schematics/stories.md
Normal file
@ -0,0 +1,45 @@
|
||||
# stories
|
||||
|
||||
Create stories/specs for all components declared in a library
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nx generate stories ...
|
||||
```
|
||||
|
||||
By default, Nx will search for `stories` in the default collection provisioned in `workspace.json`.
|
||||
|
||||
You can specify the collection explicitly as follows:
|
||||
|
||||
```bash
|
||||
nx g @nrwl/react:stories ...
|
||||
```
|
||||
|
||||
Show what will be generated without writing to disk:
|
||||
|
||||
```bash
|
||||
nx g stories ... --dry-run
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### generateCypressSpecs
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Automatically generate \*.spec.ts files in the cypress e2e app generated by the cypress-configure schematic
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Generate JavaScript files rather than TypeScript files
|
||||
|
||||
### project
|
||||
|
||||
Type: `string`
|
||||
|
||||
The library project name
|
||||
@ -30,6 +30,12 @@ Type: `boolean`
|
||||
|
||||
Run the cypress-configure schematic
|
||||
|
||||
### generateStories
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Automatically generate \*.stories.ts files for components declared in this library
|
||||
|
||||
### js
|
||||
|
||||
Default: `false`
|
||||
|
||||
@ -16,15 +16,12 @@ import {
|
||||
PropertyDeclaration,
|
||||
SyntaxKind
|
||||
} from 'typescript';
|
||||
import {
|
||||
getTsSourceFile,
|
||||
getDecoratorMetadata,
|
||||
applyWithSkipExisting
|
||||
} from '../../utils/ast-utils';
|
||||
import { getTsSourceFile, getDecoratorMetadata } from '../../utils/ast-utils';
|
||||
import {
|
||||
getInputPropertyDeclarations,
|
||||
getKnobType
|
||||
} from '../component-story/component-story';
|
||||
import { applyWithSkipExisting } from '@nrwl/workspace/src/utils/ast-utils';
|
||||
|
||||
export default function(schema: CreateComponentSpecFileSchema): Rule {
|
||||
return chain([createComponentSpecFile(schema)]);
|
||||
|
||||
@ -9,8 +9,11 @@ import {
|
||||
} from '@angular-devkit/schematics';
|
||||
import { findNodes } from '@nrwl/workspace';
|
||||
import { PropertyDeclaration, SyntaxKind } from 'typescript';
|
||||
import { getTsSourceFile, applyWithSkipExisting } from '../../utils/ast-utils';
|
||||
import { getSourceNodes } from '@nrwl/workspace/src/utils/ast-utils';
|
||||
import { getTsSourceFile } from '../../utils/ast-utils';
|
||||
import {
|
||||
getSourceNodes,
|
||||
applyWithSkipExisting
|
||||
} from '@nrwl/workspace/src/utils/ast-utils';
|
||||
|
||||
export interface CreateComponentStoriesFileSchema {
|
||||
libPath: string;
|
||||
|
||||
@ -641,21 +641,3 @@ export function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
export function applyWithSkipExisting(source: Source, rules: Rule[]): Rule {
|
||||
return (tree: Tree, _context: SchematicContext) => {
|
||||
const rule = mergeWith(
|
||||
apply(source, [
|
||||
...rules,
|
||||
forEach(fileEntry => {
|
||||
if (tree.exists(fileEntry.path)) {
|
||||
return null;
|
||||
}
|
||||
return fileEntry;
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
return rule(tree, _context);
|
||||
};
|
||||
}
|
||||
|
||||
@ -92,7 +92,23 @@ function updateWorkspaceJson(options: CypressProjectSchema): Rule {
|
||||
export default function(options: CypressProjectSchema): Rule {
|
||||
options = normalizeOptions(options);
|
||||
return chain([
|
||||
addLintFiles(options.projectRoot, options.linter),
|
||||
addLintFiles(options.projectRoot, options.linter, {
|
||||
localConfig: {
|
||||
// we need this overrides because we enabled
|
||||
// allowJS in the tsconfig to allow for JS based
|
||||
// Cypress tests. That however leads to issues
|
||||
// with the CommonJS Cypress plugin file
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/plugins/index.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-undef': 'off'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
generateFiles(options),
|
||||
updateWorkspaceJson(options),
|
||||
updateNxJson(options)
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc"
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js"]
|
||||
}
|
||||
|
||||
@ -44,6 +44,27 @@
|
||||
"schema": "./src/schematics/storybook-configuration/schema.json",
|
||||
"description": "Set up storybook for a react library",
|
||||
"hidden": false
|
||||
},
|
||||
|
||||
"component-story": {
|
||||
"factory": "./src/schematics/component-story/component-story",
|
||||
"schema": "./src/schematics/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",
|
||||
"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",
|
||||
"description": "Create a cypress spec for a ui component that has a story",
|
||||
"hidden": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
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';
|
||||
|
||||
describe('react:component-cypress-spec', () => {
|
||||
let appTree: Tree;
|
||||
let tree: UnitTestTree;
|
||||
|
||||
[
|
||||
{
|
||||
plainJS: false,
|
||||
testCmpSrcWithProps: `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;
|
||||
`,
|
||||
testCmpSrcWithoutProps: `import React from 'react';
|
||||
|
||||
import './test.scss';
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`
|
||||
},
|
||||
{
|
||||
plainJS: true,
|
||||
testCmpSrcWithProps: `import React from 'react';
|
||||
|
||||
import './test.scss';
|
||||
export const Test = (props: TestProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`,
|
||||
testCmpSrcWithoutProps: `import React from 'react';
|
||||
import './test.scss';
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`
|
||||
}
|
||||
].forEach(testConfig => {
|
||||
let fileCmpExt = testConfig.plainJS ? 'js' : 'tsx';
|
||||
let fileExt = testConfig.plainJS ? 'js' : 'ts';
|
||||
|
||||
describe(`using ${
|
||||
testConfig.plainJS ? 'plain JS' : 'TypeScript'
|
||||
} setup`, () => {
|
||||
let cmpPath = `libs/test-ui-lib/src/lib/test-ui-lib.${fileCmpExt}`;
|
||||
let cypressStorySpecFilePath = `apps/test-ui-lib-e2e/src/integration/test-ui-lib/test-ui-lib.spec.${fileExt}`;
|
||||
|
||||
if (!testConfig.plainJS) {
|
||||
// hacky, but we should do this check only if we run with TypeScript,
|
||||
// detecting component props in plain JS is "not possible"
|
||||
describe('component with properties', () => {
|
||||
beforeEach(async () => {
|
||||
appTree = await createTestUILib('test-ui-lib', testConfig.plainJS);
|
||||
|
||||
appTree.overwrite(cmpPath, testConfig.testCmpSrcWithProps);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-cypress-spec',
|
||||
<CreateComponentSpecFileSchema>{
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly set up the spec', () => {
|
||||
expect(stripIndents`${tree.readContent(cypressStorySpecFilePath)}`)
|
||||
.toContain(stripIndents`describe('test-ui-lib: Test component', () => {
|
||||
beforeEach(() => cy.visit('/iframe.html?id=test--primary&knob-name=&knob-displayAge=false'));
|
||||
|
||||
it('should render the component', () => {
|
||||
cy.get('h1').should('contain', 'Welcome to test-ui-lib!');
|
||||
});
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('component without properties', () => {
|
||||
beforeEach(async () => {
|
||||
appTree = await createTestUILib('test-ui-lib', testConfig.plainJS);
|
||||
|
||||
appTree.overwrite(cmpPath, testConfig.testCmpSrcWithoutProps);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-cypress-spec',
|
||||
<CreateComponentSpecFileSchema>{
|
||||
componentPath: `lib/test-ui-lib.${fileCmpExt}`,
|
||||
project: 'test-ui-lib',
|
||||
js: testConfig.plainJS
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly set up the spec', () => {
|
||||
expect(stripIndents`${tree.readContent(cypressStorySpecFilePath)}`)
|
||||
.toContain(stripIndents`describe('test-ui-lib: Test component', () => {
|
||||
beforeEach(() => cy.visit('/iframe.html?id=test--primary'));
|
||||
|
||||
it('should render the component', () => {
|
||||
cy.get('h1').should('contain', 'Welcome to test-ui-lib!');
|
||||
});
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
return appTree;
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import { normalize, schema } from '@angular-devkit/core';
|
||||
import {
|
||||
chain,
|
||||
move,
|
||||
Rule,
|
||||
SchematicContext,
|
||||
SchematicsException,
|
||||
applyTemplates,
|
||||
Tree,
|
||||
url
|
||||
} from '@angular-devkit/schematics';
|
||||
import { findNodes, getProjectConfig } from '@nrwl/workspace';
|
||||
import { applyWithSkipExisting } from '@nrwl/workspace/src/utils/ast-utils';
|
||||
import { join } from 'path';
|
||||
import ts = require('typescript');
|
||||
import {
|
||||
getComponentName,
|
||||
getComponentPropsInterface
|
||||
} from '../../utils/ast-utils';
|
||||
|
||||
export interface CreateComponentSpecFileSchema {
|
||||
project: string;
|
||||
componentPath: string;
|
||||
js?: boolean;
|
||||
}
|
||||
|
||||
export default function(schema: CreateComponentSpecFileSchema): Rule {
|
||||
return chain([createComponentSpecFile(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({
|
||||
project,
|
||||
componentPath,
|
||||
js
|
||||
}: CreateComponentSpecFileSchema): Rule {
|
||||
return (tree: Tree, context: SchematicContext): Rule => {
|
||||
const e2eLibIntegrationFolderPath =
|
||||
getProjectConfig(tree, project + '-e2e').sourceRoot + '/integration';
|
||||
|
||||
const proj = getProjectConfig(tree, project);
|
||||
const componentFilePath = normalize(join(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 SchematicsException(`Failed to read ${componentFilePath}`);
|
||||
}
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
componentFilePath,
|
||||
contents.toString(),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const cmpDeclaration = getComponentName(sourceFile);
|
||||
if (!cmpDeclaration) {
|
||||
throw new SchematicsException(
|
||||
`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)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return applyWithSkipExisting(url('./files'), [
|
||||
applyTemplates({
|
||||
projectName: project,
|
||||
componentName,
|
||||
componentSelector: (cmpDeclaration as any).name.text,
|
||||
props,
|
||||
fileExt: js ? 'js' : 'ts'
|
||||
}),
|
||||
move(e2eLibIntegrationFolderPath + '/' + componentName)
|
||||
]);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
describe('<%=projectName%>: <%= componentSelector %> component', () => {
|
||||
beforeEach(() => cy.visit('/iframe.html?id=<%= componentSelector.toLowerCase() %>--primary<%
|
||||
for(let prop of props) {
|
||||
%>&knob-<%=prop.name%><%
|
||||
if(prop.defaultValue !== undefined) {
|
||||
%>=<%=prop.defaultValue%><%
|
||||
} %><%}%>'));
|
||||
|
||||
it('should render the component', () => {
|
||||
cy.get('h1').should('contain', 'Welcome to <%=projectName%>!');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"id": "component-cypress-spec",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project name for which to generate tests",
|
||||
"examples": ["shared-ui-component"],
|
||||
"$default": {
|
||||
"$source": "projectName",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What's name of the project for which to generate tests?"
|
||||
},
|
||||
"componentPath": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the component file from the library root?",
|
||||
"examples": ["lib/components"],
|
||||
"x-prompt": "What's path of the component relative to the project's lib root for which to generate a test?"
|
||||
},
|
||||
"js": {
|
||||
"type": "boolean",
|
||||
"description": "Generate JavaScript files rather than TypeScript files",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["project", "componentPath"]
|
||||
}
|
||||
@ -0,0 +1,430 @@
|
||||
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 { updateWorkspaceInTree } from '@nrwl/workspace';
|
||||
|
||||
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';
|
||||
|
||||
describe('default setup', () => {
|
||||
beforeEach(async () => {
|
||||
appTree = await createTestUILib('test-ui-lib', true);
|
||||
});
|
||||
|
||||
describe('when file does not contain a component', () => {
|
||||
beforeEach(() => {
|
||||
appTree.overwrite(
|
||||
cmpPath,
|
||||
`export const add = (a: number, b: number) => a + b;`
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'Could not find any React component in file libs/test-ui-lib/src/lib/test-ui-lib.tsx'
|
||||
);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('default component setup', () => {
|
||||
beforeEach(async () => {
|
||||
tree = await runSchematic(
|
||||
'component-story',
|
||||
<CreateComponentStoriesFileSchema>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should create the story file', () => {
|
||||
expect(tree.exists(storyFilePath)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should properly set up the story', () => {
|
||||
expect(tree.readContent(storyFilePath))
|
||||
.toContain(`import React from 'react';
|
||||
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
|
||||
|
||||
export default {
|
||||
component: TestUiLib,
|
||||
title: 'TestUiLib'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
/* eslint-disable-next-line */
|
||||
const props: TestUiLibProps = {};
|
||||
|
||||
return <TestUiLib />;
|
||||
};`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using plain JS components', () => {
|
||||
let storyFilePathPlain =
|
||||
'libs/test-ui-lib/src/lib/test-ui-libplain.stories.jsx';
|
||||
|
||||
beforeEach(async () => {
|
||||
appTree.create(
|
||||
'libs/test-ui-lib/src/lib/test-ui-libplain.jsx',
|
||||
`import React from 'react';
|
||||
|
||||
import './test.scss';
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`
|
||||
);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-story',
|
||||
<CreateComponentStoriesFileSchema>{
|
||||
componentPath: 'lib/test-ui-libplain.jsx',
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should create the story file', () => {
|
||||
expect(tree.exists(storyFilePathPlain)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should properly set up the story', () => {
|
||||
expect(tree.readContent(storyFilePathPlain))
|
||||
.toContain(`import React from 'react';
|
||||
import { Test } from './test-ui-libplain';
|
||||
|
||||
export default {
|
||||
component: Test,
|
||||
title: 'Test'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
/* eslint-disable-next-line */
|
||||
const props = {};
|
||||
|
||||
return <Test />;
|
||||
};`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('component without any props defined', () => {
|
||||
beforeEach(async () => {
|
||||
appTree.overwrite(
|
||||
cmpPath,
|
||||
`import React from 'react';
|
||||
|
||||
import './test.scss';
|
||||
|
||||
export const Test = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
`
|
||||
);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-story',
|
||||
<CreateComponentStoriesFileSchema>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a story without knobs', () => {
|
||||
expect(tree.readContent(storyFilePath))
|
||||
.toContain(`import React from 'react';
|
||||
import { Test } from './test-ui-lib';
|
||||
|
||||
export default {
|
||||
component: Test,
|
||||
title: 'Test'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
return <Test />;
|
||||
};
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('component with props', () => {
|
||||
beforeEach(async () => {
|
||||
appTree.overwrite(
|
||||
cmpPath,
|
||||
`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;
|
||||
`
|
||||
);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-story',
|
||||
<CreateComponentStoriesFileSchema>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should setup knobs based on the component props', () => {
|
||||
expect(tree.readContent(storyFilePath))
|
||||
.toContain(`import { text, boolean } from '@storybook/addon-knobs';
|
||||
import React from 'react';
|
||||
import { Test, TestProps } from './test-ui-lib';
|
||||
|
||||
export default {
|
||||
component: Test,
|
||||
title: 'Test'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
const props: TestProps = {
|
||||
name: text('name', ''),
|
||||
displayAge: boolean('displayAge', false)
|
||||
};
|
||||
|
||||
return <Test name={props.name} displayAge={props.displayAge} />;
|
||||
};
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
name: 'default export function',
|
||||
src: `export default function Test(props: TestProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'function and then export',
|
||||
src: `
|
||||
function Test(props: TestProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Test;
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'arrow function',
|
||||
src: `
|
||||
const Test = (props: TestProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Test;
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'arrow function without {..}',
|
||||
src: `
|
||||
const Test = (props: TestProps) => <div><h1>Welcome to test component, {props.name}</h1></div>;
|
||||
export default Test
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'direct export of component class',
|
||||
src: `
|
||||
export default class Test extends React.Component<TestProps> {
|
||||
render() {
|
||||
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'component class & then default export',
|
||||
src: `
|
||||
class Test extends React.Component<TestProps> {
|
||||
render() {
|
||||
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||
}
|
||||
}
|
||||
export default Test
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'PureComponent class & then default export',
|
||||
src: `
|
||||
class Test extends React.PureComponent<TestProps> {
|
||||
render() {
|
||||
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||
}
|
||||
}
|
||||
export default Test
|
||||
`
|
||||
}
|
||||
].forEach(config => {
|
||||
describe(`React component defined as:${config.name}`, () => {
|
||||
beforeEach(async () => {
|
||||
appTree.overwrite(
|
||||
cmpPath,
|
||||
`import React from 'react';
|
||||
|
||||
import './test.scss';
|
||||
|
||||
export interface TestProps {
|
||||
name: string;
|
||||
displayAge: boolean;
|
||||
}
|
||||
|
||||
${config.src}
|
||||
`
|
||||
);
|
||||
|
||||
tree = await runSchematic(
|
||||
'component-story',
|
||||
<CreateComponentStoriesFileSchema>{
|
||||
componentPath: 'lib/test-ui-lib.tsx',
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly setup the knobs based on the component props', () => {
|
||||
expect(tree.readContent(storyFilePath))
|
||||
.toContain(`import { text, boolean } from '@storybook/addon-knobs';
|
||||
import React from 'react';
|
||||
import { Test, TestProps } from './test-ui-lib';
|
||||
|
||||
export default {
|
||||
component: Test,
|
||||
title: 'Test'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
const props: TestProps = {
|
||||
name: text('name', ''),
|
||||
displayAge: boolean('displayAge', false)
|
||||
};
|
||||
|
||||
return <Test name={props.name} displayAge={props.displayAge} />;
|
||||
};`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('using tslint', () => {
|
||||
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
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly set up the story', () => {
|
||||
expect(tree.readContent(storyFilePath))
|
||||
.toContain(`import React from 'react';
|
||||
import { TestUiLib, TestUiLibProps } from './test-ui-lib';
|
||||
|
||||
export default {
|
||||
component: TestUiLib,
|
||||
title: 'TestUiLib'
|
||||
};
|
||||
|
||||
export const primary = () => {
|
||||
/* tslint-disable-next-line */
|
||||
const props: TestUiLibProps = {};
|
||||
|
||||
return <TestUiLib />;
|
||||
};`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
if (useEsLint) {
|
||||
const currentWorkspaceJson = JSON.parse(
|
||||
appTree.read('workspace.json').toString('utf-8')
|
||||
);
|
||||
|
||||
currentWorkspaceJson.projects[libName].architect.lint.options.linter =
|
||||
'eslint';
|
||||
|
||||
appTree.overwrite('workspace.json', JSON.stringify(currentWorkspaceJson));
|
||||
}
|
||||
|
||||
return appTree;
|
||||
}
|
||||
148
packages/react/src/schematics/component-story/component-story.ts
Normal file
148
packages/react/src/schematics/component-story/component-story.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import {
|
||||
Rule,
|
||||
chain,
|
||||
SchematicContext,
|
||||
Tree,
|
||||
template,
|
||||
move,
|
||||
url,
|
||||
SchematicsException,
|
||||
applyTemplates
|
||||
} from '@angular-devkit/schematics';
|
||||
import { normalize } from '@angular-devkit/core';
|
||||
import { getProjectConfig, formatFiles } from '@nrwl/workspace';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
applyWithSkipExisting,
|
||||
findNodes
|
||||
} from '@nrwl/workspace/src/utils/ast-utils';
|
||||
import * as ts from 'typescript';
|
||||
import {
|
||||
findDefaultExport,
|
||||
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({
|
||||
// name,
|
||||
project,
|
||||
componentPath
|
||||
}: CreateComponentStoriesFileSchema): Rule {
|
||||
return (tree: Tree, context: SchematicContext): Rule => {
|
||||
const proj = getProjectConfig(tree, project);
|
||||
const sourceRoot = proj.sourceRoot;
|
||||
const usesEsLint = proj.architect.lint.options.linter === 'eslint';
|
||||
|
||||
const componentFilePath = normalize(join(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 = tree.read(componentFilePath);
|
||||
if (!contents) {
|
||||
throw new SchematicsException(`Failed to read ${componentFilePath}`);
|
||||
}
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
componentFilePath,
|
||||
contents.toString(),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const cmpDeclaration = getComponentName(sourceFile);
|
||||
|
||||
if (!cmpDeclaration) {
|
||||
throw new SchematicsException(
|
||||
`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)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return chain([
|
||||
applyWithSkipExisting(url('./files'), [
|
||||
applyTemplates({
|
||||
componentFileName: name,
|
||||
propsTypeName,
|
||||
props,
|
||||
usedKnobs: props.map(x => x.type).join(', '),
|
||||
componentName: (cmpDeclaration as any).name.text,
|
||||
isPlainJs,
|
||||
fileExt,
|
||||
usesEsLint
|
||||
}),
|
||||
move(normalize(componentDirectory))
|
||||
])
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export default function(schema: CreateComponentStoriesFileSchema): Rule {
|
||||
return chain([createComponentStoriesFile(schema), formatFiles()]);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<% if(usedKnobs && usedKnobs.length > 0) { %>import { <%= usedKnobs %> } from '@storybook/addon-knobs';<% } %>
|
||||
import React from 'react';
|
||||
import { <%= componentName %><% if ( propsTypeName && !isPlainJs ) { %>, <%= propsTypeName %> <% } %> } from './<%= componentFileName %>';
|
||||
|
||||
export default {
|
||||
component: <%= componentName %>,
|
||||
title: '<%= componentName %>'
|
||||
};
|
||||
|
||||
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 %>),<% } %>
|
||||
};
|
||||
<% } %>
|
||||
|
||||
return <<%= componentName %> <% for (let prop of props) { %><%= prop.name %> = {props.<%= prop.name %>} <% } %> />;
|
||||
};
|
||||
24
packages/react/src/schematics/component-story/schema.json
Normal file
24
packages/react/src/schematics/component-story/schema.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"id": "component-story",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project name where to add the components",
|
||||
"examples": ["shared-ui-component"],
|
||||
"$default": {
|
||||
"$source": "projectName",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What's name of the project where the component lives?"
|
||||
},
|
||||
"componentPath": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the component file from the library root?",
|
||||
"examples": ["lib/components"],
|
||||
"x-prompt": "What's path of the component relative to the project's lib root"
|
||||
}
|
||||
},
|
||||
"required": ["project", "componentPath"]
|
||||
}
|
||||
27
packages/react/src/schematics/stories/schema.json
Normal file
27
packages/react/src/schematics/stories/schema.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema",
|
||||
"id": "storybook-stories",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The library project name",
|
||||
"$default": {
|
||||
"$source": "projectName",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What's name of the project for which you want to generate stories?"
|
||||
},
|
||||
"generateCypressSpecs": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically generate *.spec.ts files in the cypress e2e app generated by the cypress-configure schematic",
|
||||
"x-prompt": "Do you want to generate Cypress specs as well?"
|
||||
},
|
||||
"js": {
|
||||
"type": "boolean",
|
||||
"description": "Generate JavaScript files rather than TypeScript files",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["project"]
|
||||
}
|
||||
127
packages/react/src/schematics/stories/stories.spec.ts
Normal file
127
packages/react/src/schematics/stories/stories.spec.ts
Normal file
@ -0,0 +1,127 @@
|
||||
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';
|
||||
|
||||
describe('react:stories', () => {
|
||||
let appTree: Tree;
|
||||
let tree: UnitTestTree;
|
||||
|
||||
beforeEach(async () => {
|
||||
appTree = await createTestUILib('test-ui-lib');
|
||||
|
||||
// create another component
|
||||
appTree.create(
|
||||
'libs/test-ui-lib/src/lib/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 () => {
|
||||
tree = await runSchematic(
|
||||
'stories',
|
||||
<StorybookStoriesSchema>{
|
||||
project: 'test-ui-lib'
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
expect(
|
||||
tree.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')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should generate Cypress specs', async () => {
|
||||
tree = await runSchematic(
|
||||
'stories',
|
||||
<StorybookStoriesSchema>{
|
||||
project: 'test-ui-lib',
|
||||
generateCypressSpecs: true
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
expect(
|
||||
tree.exists(
|
||||
'apps/test-ui-lib-e2e/src/integration/test-ui-lib/test-ui-lib.spec.ts'
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
tree.exists(
|
||||
'apps/test-ui-lib-e2e/src/integration/another-cmp/another-cmp.spec.ts'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
xit('should not overwrite existing stories', () => {});
|
||||
|
||||
it('should ignore files that do not contain components', async () => {
|
||||
// create another component
|
||||
appTree.create(
|
||||
'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
|
||||
);
|
||||
|
||||
// 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')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
return appTree;
|
||||
}
|
||||
98
packages/react/src/schematics/stories/stories.ts
Normal file
98
packages/react/src/schematics/stories/stories.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import {
|
||||
chain,
|
||||
Rule,
|
||||
schematic,
|
||||
SchematicContext,
|
||||
Tree,
|
||||
SchematicsException,
|
||||
noop
|
||||
} from '@angular-devkit/schematics';
|
||||
import { getProjectConfig } from '@nrwl/workspace';
|
||||
import { join } from 'path';
|
||||
import { CreateComponentStoriesFileSchema } from '../component-story/component-story';
|
||||
import { CreateComponentSpecFileSchema } from '../component-cypress-spec/component-cypress-spec';
|
||||
import { getComponentName } from '../../utils/ast-utils';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export interface StorybookStoriesSchema {
|
||||
project: string;
|
||||
generateCypressSpecs: boolean;
|
||||
js?: boolean;
|
||||
}
|
||||
|
||||
function containsComponentDeclaration(
|
||||
tree: Tree,
|
||||
componentPath: string
|
||||
): boolean {
|
||||
const contents = tree.read(componentPath);
|
||||
if (!contents) {
|
||||
throw new SchematicsException(`Failed to read ${componentPath}`);
|
||||
}
|
||||
|
||||
const sourceFile = ts.createSourceFile(
|
||||
componentPath,
|
||||
contents.toString(),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
return !!getComponentName(sourceFile);
|
||||
}
|
||||
|
||||
export function createAllStories(
|
||||
projectName: string,
|
||||
generateCypressSpecs: boolean,
|
||||
js
|
||||
): Rule {
|
||||
return (tree: Tree, context: SchematicContext) => {
|
||||
const projectSrcRoot = getProjectConfig(tree, projectName).sourceRoot;
|
||||
const libPath = join(projectSrcRoot, '/lib');
|
||||
|
||||
let componentPaths: string[] = [];
|
||||
tree.getDir(libPath).visit(filePath => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return chain(
|
||||
componentPaths.map(componentPath => {
|
||||
const relativeCmpDir = componentPath.replace(
|
||||
join('/', projectSrcRoot, '/'),
|
||||
''
|
||||
);
|
||||
|
||||
if (!containsComponentDeclaration(tree, componentPath)) {
|
||||
return chain([noop()]);
|
||||
}
|
||||
|
||||
return chain([
|
||||
schematic<CreateComponentStoriesFileSchema>('component-story', {
|
||||
componentPath: relativeCmpDir,
|
||||
project: projectName
|
||||
}),
|
||||
generateCypressSpecs
|
||||
? schematic<CreateComponentSpecFileSchema>(
|
||||
'component-cypress-spec',
|
||||
{
|
||||
project: projectName,
|
||||
componentPath: relativeCmpDir,
|
||||
js
|
||||
}
|
||||
)
|
||||
: () => {}
|
||||
]);
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default function(schema: StorybookStoriesSchema): Rule {
|
||||
return chain([
|
||||
createAllStories(schema.project, schema.generateCypressSpecs, schema.js)
|
||||
]);
|
||||
}
|
||||
@ -1,11 +1,18 @@
|
||||
import { Tree, schematic, externalSchematic } from '@angular-devkit/schematics';
|
||||
import { runSchematic, callRule } from '../../utils/testing';
|
||||
import { StorybookConfigureSchema } from './schema';
|
||||
import { externalSchematic, Tree } from '@angular-devkit/schematics';
|
||||
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
|
||||
import { callRule, runSchematic } from '../../utils/testing';
|
||||
import { StorybookConfigureSchema } from './schema';
|
||||
|
||||
describe('react:storybook-configuration', () => {
|
||||
let appTree;
|
||||
|
||||
// beforeEach(async () => {
|
||||
// appTree = await createTestUILib('test-ui-lib');
|
||||
// });
|
||||
|
||||
it('should configure everything at once', async () => {
|
||||
const appTree = await createTestUILib('test-ui-lib');
|
||||
appTree = await createTestUILib('test-ui-lib');
|
||||
|
||||
const tree = await runSchematic(
|
||||
'storybook-configuration',
|
||||
<StorybookConfigureSchema>{
|
||||
@ -21,14 +28,72 @@ describe('react:storybook-configuration', () => {
|
||||
).toBeTruthy();
|
||||
expect(tree.exists('apps/test-ui-lib-e2e/cypress.json')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should generate stories for components', async () => {
|
||||
appTree = await createTestUILib('test-ui-lib');
|
||||
|
||||
const tree = await runSchematic(
|
||||
'storybook-configuration',
|
||||
<StorybookConfigureSchema>{
|
||||
name: 'test-ui-lib',
|
||||
generateStories: true
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
expect(
|
||||
tree.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.create(
|
||||
'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;
|
||||
`
|
||||
);
|
||||
|
||||
const tree = await runSchematic(
|
||||
'storybook-configuration',
|
||||
<StorybookConfigureSchema>{
|
||||
name: 'test-ui-lib',
|
||||
generateCypressSpecs: true,
|
||||
generateStories: true,
|
||||
js: true
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
expect(
|
||||
tree.exists('libs/test-ui-lib/src/lib/test-ui-libplain.stories.js')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
export async function createTestUILib(libName: string): Promise<Tree> {
|
||||
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
|
||||
name: libName,
|
||||
js: plainJS
|
||||
}),
|
||||
appTree
|
||||
);
|
||||
|
||||
@ -1,5 +1,23 @@
|
||||
import { chain, externalSchematic, Rule } from '@angular-devkit/schematics';
|
||||
import {
|
||||
chain,
|
||||
externalSchematic,
|
||||
Rule,
|
||||
schematic,
|
||||
noop
|
||||
} from '@angular-devkit/schematics';
|
||||
import { StorybookConfigureSchema } from './schema';
|
||||
import { StorybookStoriesSchema } from '../stories/stories';
|
||||
|
||||
function generateStories(schema: StorybookConfigureSchema): Rule {
|
||||
return (tree, context) => {
|
||||
return schematic<StorybookStoriesSchema>('stories', {
|
||||
project: schema.name,
|
||||
generateCypressSpecs:
|
||||
schema.configureCypress && schema.generateCypressSpecs,
|
||||
js: schema.js
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default function(schema: StorybookConfigureSchema): Rule {
|
||||
return chain([
|
||||
@ -8,6 +26,7 @@ export default function(schema: StorybookConfigureSchema): Rule {
|
||||
uiFramework: '@storybook/react',
|
||||
configureCypress: schema.configureCypress,
|
||||
js: schema.js
|
||||
})
|
||||
}),
|
||||
schema.generateStories ? generateStories(schema) : noop()
|
||||
]);
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
export interface StorybookConfigureSchema {
|
||||
name: string;
|
||||
configureCypress: boolean;
|
||||
generateStories?: boolean;
|
||||
generateCypressSpecs?: boolean;
|
||||
js?: boolean;
|
||||
}
|
||||
|
||||
@ -16,6 +16,11 @@
|
||||
"description": "Run the cypress-configure schematic",
|
||||
"x-prompt": "Configure a cypress e2e app to run against the storybook instance?"
|
||||
},
|
||||
"generateStories": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically generate *.stories.ts files for components declared in this library",
|
||||
"x-prompt": "Automatically generate story files for components declared in this library?"
|
||||
},
|
||||
"js": {
|
||||
"type": "boolean",
|
||||
"description": "Generate JavaScript files rather than TypeScript files",
|
||||
|
||||
@ -56,6 +56,41 @@ describe('findDefaultExport', () => {
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name.text).toEqual('main');
|
||||
});
|
||||
|
||||
it('should find default class export', () => {
|
||||
const sourceCode = `
|
||||
export default class Main {};
|
||||
`;
|
||||
const source = ts.createSourceFile(
|
||||
'test.ts',
|
||||
sourceCode,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const result = utils.findDefaultExport(source) as any;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name.text).toEqual('Main');
|
||||
});
|
||||
|
||||
it('should find exported class', () => {
|
||||
const sourceCode = `
|
||||
class Main {};
|
||||
export default Main;
|
||||
`;
|
||||
const source = ts.createSourceFile(
|
||||
'test.ts',
|
||||
sourceCode,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const result = utils.findDefaultExport(source) as any;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.name.text).toEqual('Main');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRoute', () => {
|
||||
@ -361,3 +396,103 @@ const store = createStore(combineReducer({}));
|
||||
expect(result).toContain('[SLICE_KEY]: sliceReducer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getComponentName', () => {
|
||||
[
|
||||
{
|
||||
testName: 'exporting a function',
|
||||
src: `export default function Test(props: TestProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);`,
|
||||
expectedName: 'Test'
|
||||
},
|
||||
{
|
||||
testName: 'defining a function and then default exporting it',
|
||||
src: `
|
||||
function Test(props: TestProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Test;
|
||||
`,
|
||||
expectedName: 'Test'
|
||||
},
|
||||
{
|
||||
testName: 'defining an arrow function and then exporting it',
|
||||
src: `
|
||||
const Test = (props: TestProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to test component, {props.name}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Test;
|
||||
`,
|
||||
expectedName: 'Test'
|
||||
},
|
||||
{
|
||||
testName: 'defining an arrow function that directly returns JSX',
|
||||
src: `
|
||||
const Test = (props: TestProps) => <div><h1>Welcome to test component, {props.name}</h1></div>;
|
||||
export default Test
|
||||
`,
|
||||
expectedName: 'Test'
|
||||
},
|
||||
{
|
||||
testName: 'exporting a react class component',
|
||||
src: `
|
||||
export default class Test extends React.Component<TestProps> {
|
||||
render() {
|
||||
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedName: 'Test'
|
||||
},
|
||||
{
|
||||
testName: 'defining a react class component & then default exporting it',
|
||||
src: `
|
||||
export default class Test extends React.Component<TestProps> {
|
||||
render() {
|
||||
return <div><h1>Welcome to test component, {this.props.name}</h1></div>;
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedName: 'Test'
|
||||
}
|
||||
].forEach(testConfig => {
|
||||
it(`should find the component when ${testConfig.testName}`, () => {
|
||||
const source = ts.createSourceFile(
|
||||
'some-component.tsx',
|
||||
testConfig.src,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const result = utils.getComponentName(source) as any;
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect((result as any).name.text).toEqual(testConfig.expectedName);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is no component', () => {
|
||||
const source = ts.createSourceFile(
|
||||
'some-component.tsx',
|
||||
`console.log('hi there');`,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
const result = utils.getComponentName(source) as any;
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,7 +6,10 @@ import {
|
||||
ReplaceChange
|
||||
} from '@nrwl/workspace/src/utils/ast-utils';
|
||||
import * as ts from 'typescript';
|
||||
import { SchematicContext } from '@angular-devkit/schematics';
|
||||
import {
|
||||
SchematicContext,
|
||||
SchematicsException
|
||||
} from '@angular-devkit/schematics';
|
||||
|
||||
export function findMainRenderStatement(
|
||||
source: ts.SourceFile
|
||||
@ -67,8 +70,9 @@ export function findDefaultExportDeclaration(
|
||||
if (identifier) {
|
||||
const variables = findNodes(source, ts.SyntaxKind.VariableDeclaration);
|
||||
const fns = findNodes(source, ts.SyntaxKind.FunctionDeclaration);
|
||||
const all = variables.concat(fns) as Array<
|
||||
ts.VariableDeclaration | ts.FunctionDeclaration
|
||||
const cls = findNodes(source, ts.SyntaxKind.ClassDeclaration);
|
||||
const all = [...variables, ...fns, ...cls] as Array<
|
||||
ts.VariableDeclaration | ts.FunctionDeclaration | ts.ClassDeclaration
|
||||
>;
|
||||
|
||||
const exported = all
|
||||
@ -119,6 +123,7 @@ function hasDefaultExportModifier(
|
||||
x: ts.ClassDeclaration | ts.FunctionDeclaration
|
||||
) {
|
||||
return (
|
||||
x.modifiers &&
|
||||
x.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
|
||||
x.modifiers.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)
|
||||
);
|
||||
@ -445,3 +450,85 @@ export function updateReduxStore(
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
export function getComponentName(sourceFile: ts.SourceFile): ts.Node | null {
|
||||
const defaultExport = findDefaultExport(sourceFile);
|
||||
|
||||
if (
|
||||
!(
|
||||
defaultExport &&
|
||||
findNodes(defaultExport, ts.SyntaxKind.JsxElement).length > 0
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return defaultExport;
|
||||
}
|
||||
|
||||
export function getComponentPropsInterface(
|
||||
sourceFile: ts.SourceFile
|
||||
): ts.InterfaceDeclaration | null {
|
||||
const cmpDeclaration = getComponentName(sourceFile);
|
||||
let propsTypeName: string = null;
|
||||
|
||||
if (ts.isFunctionDeclaration(cmpDeclaration)) {
|
||||
const propsParam: ts.ParameterDeclaration = cmpDeclaration.parameters.find(
|
||||
x => ts.isParameter(x) && (x.name as ts.Identifier).text === 'props'
|
||||
);
|
||||
|
||||
if (propsParam && propsParam.type) {
|
||||
propsTypeName = ((propsParam.type as ts.TypeReferenceNode)
|
||||
.typeName as ts.Identifier).text;
|
||||
}
|
||||
} else if (
|
||||
(cmpDeclaration as ts.VariableDeclaration).initializer &&
|
||||
ts.isArrowFunction((cmpDeclaration as ts.VariableDeclaration).initializer)
|
||||
) {
|
||||
const arrowFn = (cmpDeclaration as ts.VariableDeclaration)
|
||||
.initializer as ts.ArrowFunction;
|
||||
|
||||
const propsParam: ts.ParameterDeclaration = arrowFn.parameters.find(
|
||||
x => ts.isParameter(x) && (x.name as ts.Identifier).text === 'props'
|
||||
);
|
||||
|
||||
if (propsParam && propsParam.type) {
|
||||
propsTypeName = ((propsParam.type as ts.TypeReferenceNode)
|
||||
.typeName as ts.Identifier).text;
|
||||
}
|
||||
} else if (
|
||||
// do we have a class component extending from React.Component
|
||||
ts.isClassDeclaration(cmpDeclaration) &&
|
||||
cmpDeclaration.heritageClauses &&
|
||||
cmpDeclaration.heritageClauses.length > 0
|
||||
) {
|
||||
const heritageClause = cmpDeclaration.heritageClauses[0];
|
||||
|
||||
if (heritageClause) {
|
||||
const propsTypeExpression = heritageClause.types.find(
|
||||
x =>
|
||||
(x.expression as ts.PropertyAccessExpression).name.text ===
|
||||
'Component' ||
|
||||
(x.expression as ts.PropertyAccessExpression).name.text ===
|
||||
'PureComponent'
|
||||
);
|
||||
|
||||
if (propsTypeExpression && propsTypeExpression.typeArguments) {
|
||||
propsTypeName = (propsTypeExpression
|
||||
.typeArguments[0] as ts.TypeReferenceNode).typeName.getText();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (propsTypeName) {
|
||||
return findNodes(sourceFile, ts.SyntaxKind.InterfaceDeclaration).find(
|
||||
(x: ts.InterfaceDeclaration) => {
|
||||
return (x.name as ts.Identifier).getText() === propsTypeName;
|
||||
}
|
||||
) as ts.InterfaceDeclaration;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,4 +2,4 @@ import { configure, addDecorator } from '<%= uiFramework %>';
|
||||
import { withKnobs } from '@storybook/addon-knobs';
|
||||
|
||||
addDecorator(withKnobs);
|
||||
configure(require.context('../src/lib', true, /\.stories\.tsx?$/), module);
|
||||
configure(require.context('../src/lib', true, /\.stories\.(j|t)sx?$/), module);
|
||||
|
||||
@ -13,7 +13,8 @@ import {
|
||||
babelLoaderVersion,
|
||||
babelCoreVersion,
|
||||
storybookVersion,
|
||||
nxVersion
|
||||
nxVersion,
|
||||
babelPresetTypescriptVersion
|
||||
} from '../../utils/versions';
|
||||
import { Schema } from './schema';
|
||||
|
||||
@ -29,6 +30,9 @@ function checkDependenciesInstalled(): Rule {
|
||||
devDependencies['@storybook/addon-knobs'] = storybookVersion;
|
||||
devDependencies['babel-loader'] = babelLoaderVersion;
|
||||
devDependencies['@babel/core'] = babelCoreVersion;
|
||||
devDependencies[
|
||||
'@babel/preset-typescript'
|
||||
] = babelPresetTypescriptVersion;
|
||||
}
|
||||
if (
|
||||
!packageJson.dependencies['@angular/forms'] &&
|
||||
|
||||
@ -2,3 +2,4 @@ export const nxVersion = '*';
|
||||
export const storybookVersion = '5.3.9';
|
||||
export const babelCoreVersion = '7.8.3';
|
||||
export const babelLoaderVersion = '8.0.6';
|
||||
export const babelPresetTypescriptVersion = '7.8.3';
|
||||
|
||||
@ -11,7 +11,11 @@ import {
|
||||
SchematicContext,
|
||||
DirEntry,
|
||||
noop,
|
||||
chain
|
||||
chain,
|
||||
Source,
|
||||
mergeWith,
|
||||
apply,
|
||||
forEach
|
||||
} from '@angular-devkit/schematics';
|
||||
import * as ts from 'typescript';
|
||||
import * as stripJsonComments from 'strip-json-comments';
|
||||
@ -795,3 +799,24 @@ function renameFile(tree: Tree, from: string, to: string) {
|
||||
tree.create(to, buffer.toString());
|
||||
tree.delete(from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a template merge but skips for already existing entries
|
||||
*/
|
||||
export function applyWithSkipExisting(source: Source, rules: Rule[]): Rule {
|
||||
return (tree: Tree, _context: SchematicContext) => {
|
||||
const rule = mergeWith(
|
||||
apply(source, [
|
||||
...rules,
|
||||
forEach(fileEntry => {
|
||||
if (tree.exists(fileEntry.path)) {
|
||||
return null;
|
||||
}
|
||||
return fileEntry;
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
return rule(tree, _context);
|
||||
};
|
||||
}
|
||||
|
||||
@ -129,6 +129,7 @@ export function addLintFiles(
|
||||
: [options.localConfig.extends]
|
||||
: [];
|
||||
configJson = {
|
||||
rules: {},
|
||||
...options.localConfig,
|
||||
extends: [...extendsOption, rootConfig]
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user