Skip to content

Commit 32a552a

Browse files
committed
feat: add migrations to fix breaking changes
1 parent a6a6fda commit 32a552a

12 files changed

+327
-18
lines changed

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve",
7-
"build": "ng build --prod testing-library",
8-
"postbuild": "cp ./README.md ./dist/@angular-extensions/testing-library",
7+
"prebuild": "rimraf dist",
8+
"build": "npm run build:library && npm run build:migrations && npm run build:readme",
9+
"build:library": "ng build --prod testing-library",
10+
"build:migrations": "tsc -p ./projects/testing-library/migrations/tsconfig.migrations.json",
11+
"build:readme": "cp ./README.md ./dist/@angular-extensions/testing-library",
912
"test": "jest --config ./projects/jest.lib.config.js",
1013
"test:app": "jest --config ./src/jest.app.config.js",
1114
"precommit": "lint-staged",
@@ -31,9 +34,11 @@
3134
"@angular/platform-browser": "^7.0.0",
3235
"@angular/platform-browser-dynamic": "^7.0.0",
3336
"@angular/router": "^7.0.0",
37+
"@phenomnomnominal/tsquery": "^3.0.0",
3438
"core-js": "^2.5.4",
3539
"dom-testing-library": "^4.0.1",
3640
"rxjs": "^6.3.3",
41+
"tslint": "^5.16.0",
3742
"zone.js": "^0.8.26"
3843
},
3944
"devDependencies": {
@@ -50,11 +55,11 @@
5055
"lint-staged": "^7.2.0",
5156
"ng-packagr": "^3.0.6",
5257
"prettier": "^1.13.7",
58+
"rimraf": "^2.6.3",
5359
"semantic-release": "^15.8.1",
5460
"ts-node": "~7.0.0",
5561
"tsickle": "0.32.1",
5662
"tslib": "^1.9.3",
57-
"tslint": "~5.11.0",
5863
"typescript": "~3.1.3"
5964
}
6065
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Rule } from '@angular-devkit/schematics';
2+
import { TslintFixTask } from '@angular-devkit/schematics/tasks';
3+
import * as path from 'path';
4+
5+
function createRule(ruleName: string): TslintFixTask {
6+
return new TslintFixTask(
7+
{
8+
rulesDirectory: path.join(__dirname, 'rules'),
9+
rules: {
10+
[ruleName]: [true],
11+
},
12+
},
13+
{
14+
includes: ['**/*.spec.ts', '**/*.test.ts'],
15+
silent: false,
16+
},
17+
);
18+
}
19+
20+
export default function(): Rule {
21+
return (_, context) => {
22+
const noCreateComponentRule = createRule('no-create-component');
23+
const noComponentParametersRule = createRule('no-component-parameters');
24+
const noComponentPropertyRule = createRule('no-component-property');
25+
26+
const noCreateComponentRuleId = context.addTask(noCreateComponentRule);
27+
const noComponentParametersRuleId = context.addTask(noComponentParametersRule, [noCreateComponentRuleId]);
28+
context.addTask(noComponentPropertyRule, [noComponentParametersRuleId]);
29+
};
30+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as ts from 'typescript';
2+
import { Replacement, RuleFailure, Rules } from 'tslint';
3+
import { tsquery } from '@phenomnomnominal/tsquery';
4+
5+
const IS_COMPONENT_PROPERTY_QUERY =
6+
'CallExpression:has(Identifier[name="render"]) > ObjectLiteralExpression:first-child';
7+
const RENDER_OPTIONS_QUERY = 'CallExpression:has(Identifier[name="render"]) > ObjectLiteralExpression:last-child';
8+
const COMPONENT_PARAMETERS_PROPERTY_VALUE_QUERY = 'PropertyAssignment:has(Identifier[name="parameters"]) :last-child';
9+
10+
const FAILURE_MESSAGE = 'Found `parameters` parameter, use `componentProperties` instead.';
11+
12+
export class Rule extends Rules.AbstractRule {
13+
public apply(ast: ts.SourceFile): Array<RuleFailure> {
14+
return tsquery(ast, IS_COMPONENT_PROPERTY_QUERY)
15+
.map(result => {
16+
const [parameterNode] = tsquery(result, COMPONENT_PARAMETERS_PROPERTY_VALUE_QUERY);
17+
if (!parameterNode) {
18+
return [];
19+
}
20+
const [renderOptionsNode] = tsquery(ast, RENDER_OPTIONS_QUERY);
21+
22+
const renderOptionsText = renderOptionsNode.getFullText();
23+
const bracketIndex = renderOptionsText.indexOf('{');
24+
const renderOptions =
25+
renderOptionsText.substring(0, bracketIndex + 1) +
26+
`componentProperties:${parameterNode.getFullText()},` +
27+
renderOptionsText.substr(bracketIndex + 1);
28+
29+
const replacement = new Replacement(renderOptionsNode.getStart(), renderOptionsNode.getWidth(), renderOptions);
30+
const start = renderOptionsNode.getStart();
31+
const end = renderOptionsNode.getEnd();
32+
33+
const replacementOriginal = new Replacement(parameterNode.getStart(), parameterNode.getWidth(), '');
34+
const startOriginal = renderOptionsNode.getStart();
35+
const endOriginal = renderOptionsNode.getEnd();
36+
37+
return [
38+
new RuleFailure(ast, startOriginal, endOriginal, FAILURE_MESSAGE, this.ruleName, replacementOriginal),
39+
new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement),
40+
];
41+
})
42+
.reduce((rules, rule) => rules.concat(rule), []);
43+
}
44+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as ts from 'typescript';
2+
import { Replacement, RuleFailure, Rules } from 'tslint';
3+
import { tsquery } from '@phenomnomnominal/tsquery';
4+
5+
const IS_COMPONENT_PROPERTY_QUERY =
6+
'CallExpression:has(Identifier[name="render"]) > ObjectLiteralExpression:first-child';
7+
const COMPONENT_PROPERTY_VALUE_QUERY = 'PropertyAssignment:has(Identifier[name="component"]) :last-child';
8+
9+
const FAILURE_MESSAGE = 'Found component propety syntax, signature looks different.';
10+
11+
export class Rule extends Rules.AbstractRule {
12+
public apply(ast: ts.SourceFile): Array<RuleFailure> {
13+
return tsquery(ast, IS_COMPONENT_PROPERTY_QUERY).map(result => {
14+
const [valueNode] = tsquery(result, COMPONENT_PROPERTY_VALUE_QUERY);
15+
const replacement = new Replacement(result.getStart(), result.getWidth(), (valueNode || result).text);
16+
const start = result.getStart();
17+
const end = result.getEnd();
18+
19+
return new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement);
20+
});
21+
}
22+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as ts from 'typescript';
2+
import { Replacement, RuleFailure, Rules } from 'tslint';
3+
import { tsquery } from '@phenomnomnominal/tsquery';
4+
5+
const CREATE_COMPONENT_IDENTIFIER = 'Identifier[name="createComponent"]';
6+
const CREATE_COMPONENT_IMPORT_QUERY = `ImportSpecifier > ${CREATE_COMPONENT_IDENTIFIER}`;
7+
const CREATE_COMPONENT_CALL_EXPRESSION_QUERY = `CallExpression > ${CREATE_COMPONENT_IDENTIFIER}`;
8+
9+
const RENDER = 'render';
10+
11+
const FAILURE_MESSAGE = 'Found `createComponent`, use `render` instead.';
12+
13+
export class Rule extends Rules.AbstractRule {
14+
public apply(ast: ts.SourceFile): Array<RuleFailure> {
15+
const imports = this.getImports(ast);
16+
const usages = this.getUsages(ast);
17+
18+
return [...imports, ...usages];
19+
}
20+
21+
private getImports(ast: ts.SourceFile): Array<RuleFailure> {
22+
return tsquery(ast, CREATE_COMPONENT_IMPORT_QUERY).map(result => {
23+
const replacement = new Replacement(result.getStart(), result.getWidth(), RENDER);
24+
const start = result.getStart();
25+
const end = result.getEnd();
26+
27+
return new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement);
28+
});
29+
}
30+
31+
private getUsages(ast: ts.SourceFile): Array<RuleFailure> {
32+
return tsquery(ast, CREATE_COMPONENT_CALL_EXPRESSION_QUERY).map(result => {
33+
const replacement = new Replacement(result.getStart(), result.getWidth(), RENDER);
34+
const start = result.getStart();
35+
const end = result.getEnd();
36+
37+
return new RuleFailure(ast, start, end, FAILURE_MESSAGE, this.ruleName, replacement);
38+
});
39+
}
40+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3+
"schematics": {
4+
"migration-4.0.0": {
5+
"version": "4.0.0",
6+
"description": "Align API to *-testing-libraries",
7+
"factory": "./4_0_0"
8+
}
9+
}
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"module": "commonjs",
4+
"target": "es5",
5+
"sourceMap": true,
6+
"outDir": "../../../dist/@angular-extensions/testing-library/migrations",
7+
"noLib": false,
8+
"baseUrl": "./",
9+
"experimentalDecorators": true,
10+
"skipLibCheck": true,
11+
"declaration": true,
12+
"removeComments": true,
13+
"lib": ["es6"]
14+
},
15+
"include": ["."]
16+
}

projects/testing-library/ng-package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"lib": {
66
"entryFile": "src/public_api.ts"
77
},
8-
"whitelistedNonPeerDependencies": ["dom-testing-library"]
8+
"whitelistedNonPeerDependencies": ["dom-testing-library", "@phenomnomnominal/tsquery", "tslint"]
99
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
33
"dest": "../../dist/@angular-extensions/testing-library",
4+
"deleteDestPath": false,
45
"lib": {
56
"entryFile": "src/public_api.ts"
67
},
7-
"whitelistedNonPeerDependencies": ["dom-testing-library"]
8+
"whitelistedNonPeerDependencies": ["dom-testing-library", "@phenomnomnominal/tsquery", "tslint"]
89
}

projects/testing-library/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@
2727
"@angular/core": "^7.0.0"
2828
},
2929
"dependencies": {
30-
"dom-testing-library": "^4.0.1"
30+
"dom-testing-library": "^4.0.1",
31+
"@phenomnomnominal/tsquery": "^3.0.0",
32+
"tslint": "^5.16.0"
3133
},
3234
"publishConfig": {
3335
"access": "public"
36+
},
37+
"ng-update": {
38+
"migrations": "./migrations/migration.json"
3439
}
3540
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { getSystemPath, normalize, virtualFs } from '@angular-devkit/core';
2+
import { TempScopedNodeJsSyncHost } from '@angular-devkit/core/node/testing';
3+
import { HostTree } from '@angular-devkit/schematics';
4+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
5+
6+
describe('Migration to version 4.0.0', () => {
7+
/* tslint:disable */
8+
const fixtures = [
9+
{
10+
description: 'template syntax',
11+
input: virtualFs.stringToFileBuffer(`
12+
import { createComponent } from '@angular-extensions/testing-library';
13+
import { HomeComponent } from './home.component';
14+
15+
async function setup() {
16+
await createComponent<HomeComponent>('<home></home>', {
17+
declarations: [HomeComponent],
18+
});
19+
}`),
20+
expected: `
21+
import { render } from '@angular-extensions/testing-library';
22+
import { HomeComponent } from './home.component';
23+
24+
async function setup() {
25+
await render<HomeComponent>('<home></home>', {
26+
declarations: [HomeComponent],
27+
});
28+
}`,
29+
},
30+
{
31+
description: 'component syntax',
32+
input: virtualFs.stringToFileBuffer(`
33+
import { createComponent } from '@angular-extensions/testing-library';
34+
import { HomeComponent } from './home.component';
35+
36+
async function setup() {
37+
await createComponent(
38+
{
39+
component: HomeComponent
40+
},
41+
{
42+
declarations: [HomeComponent],
43+
}
44+
);
45+
}`),
46+
expected: `
47+
import { render } from '@angular-extensions/testing-library';
48+
import { HomeComponent } from './home.component';
49+
50+
async function setup() {
51+
await render(
52+
HomeComponent,
53+
{
54+
declarations: [HomeComponent],
55+
}
56+
);
57+
}`,
58+
},
59+
{
60+
description: 'component syntax with properties',
61+
input: virtualFs.stringToFileBuffer(`
62+
import { createComponent } from '@angular-extensions/testing-library';
63+
import { HomeComponent } from './home.component';
64+
65+
async function setup() {
66+
await createComponent(
67+
{
68+
component: HomeComponent,
69+
parameters: {
70+
value: 'foo',
71+
count: 2
72+
},
73+
},
74+
{
75+
declarations: [HomeComponent],
76+
}
77+
);
78+
}`),
79+
expected: `
80+
import { render } from '@angular-extensions/testing-library';
81+
import { HomeComponent } from './home.component';
82+
83+
async function setup() {
84+
await render(
85+
HomeComponent,
86+
87+
{componentProperties: {
88+
value: 'foo',
89+
count: 2
90+
},
91+
declarations: [HomeComponent],
92+
}
93+
);
94+
}`,
95+
},
96+
];
97+
/* tslint:enable */
98+
99+
const schematicRunner = new SchematicTestRunner('migrations', require.resolve('../../migrations/migration.json'));
100+
const specPath = normalize('tests/home.spec.ts');
101+
102+
fixtures.forEach(async ({ description, input, expected }) => {
103+
it(description, async () => {
104+
const host = new TempScopedNodeJsSyncHost();
105+
const tree = new UnitTestTree(new HostTree(host));
106+
tree.create('/package.json', JSON.stringify({}));
107+
process.chdir(getSystemPath(host.root));
108+
await host.write(specPath, input).toPromise();
109+
110+
schematicRunner.runSchematic('migration-4.0.0', {}, tree);
111+
await schematicRunner.engine.executePostTasks().toPromise();
112+
113+
const actual = await host
114+
.read(specPath)
115+
.toPromise()
116+
.then(virtualFs.fileBufferToString);
117+
118+
expect(actual).toBe(expected);
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)