Skip to content

Commit

Permalink
feat(migration): add Sass theme props migration [WIP] #2994
Browse files Browse the repository at this point in the history
  • Loading branch information
damyanpetev committed Nov 13, 2018
1 parent ae5233a commit 9b7e158
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EmptyTree } from '@angular-devkit/schematics';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import * as fs from 'fs';
import * as path from 'path';
import { ClassChanges, BindingChanges, SelectorChanges } from './schema';
import { ClassChanges, BindingChanges, SelectorChanges, ThemePropertyChanges } from './schema';
import { UpdateChanges } from './UpdateChanges';

describe('UpdateChanges', () => {
Expand All @@ -15,6 +15,7 @@ describe('UpdateChanges', () => {
public getClassChanges() { return this.classChanges; }
public getOutputChanges() { return this.outputChanges; }
public getInputChanges() { return this.inputChanges; }
public getThemePropChanges() { return this.themePropsChanges; }
}

beforeEach(() => {
Expand Down Expand Up @@ -374,4 +375,63 @@ describe('UpdateChanges', () => {

done();
});

it('should replace/remove inputs', done => {
const themePropsJson: ThemePropertyChanges = {
changes: [
{
name: '$replace-me', replaceWith: '$replaced',
owner: 'igx-theme-func'
},
{
name: '$remove-me', remove: true,
owner: 'igx-theme-func'
},
{
name: '$old-prop', remove: true,
owner: 'igx-comp-theme'
}
]
};
const jsonPath = path.join(__dirname, 'changes', 'theme-props.json');
spyOn(fs, 'existsSync').and.callFake((filePath: string) => {
if (filePath === jsonPath) {
return true;
}
return false;
});
spyOn(fs, 'readFileSync').and.callFake(() => JSON.stringify(themePropsJson));

const fileContent =
`$var: igx-theme-func(
$prop1: red,
$replace-me: 3,
$remove-me: 0px,
$prop2: 2
);
$var2: igx-comp-theme(
$replace-me: not,
$old-prop: func(val)
);`;
appTree.create('styles.scss', fileContent);
appTree.create('src/app/app.component.sass', `igx-comp-theme($replace-me: not, $old-prop: 3, $prop3: 2);`);

const update = new UnitUpdateChanges(__dirname, appTree);
expect(fs.existsSync).toHaveBeenCalledWith(jsonPath);
expect(fs.readFileSync).toHaveBeenCalledWith(jsonPath, 'utf-8');
expect(update.getThemePropChanges()).toEqual(themePropsJson);

update.applyChanges();
expect(appTree.readContent('styles.scss')).toEqual(
`$var: igx-theme-func(
$prop1: red,
$replaced: 3,
$prop2: 2
);
$var2: igx-comp-theme(
$replace-me: not
);`);
expect(appTree.readContent('src/app/app.component.sass')).toEqual(`igx-comp-theme($replace-me: not, $prop3: 2);`);
done();
});
});
101 changes: 78 additions & 23 deletions projects/igniteui-angular/migrations/common/UpdateChanges.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
// tslint:disable-next-line:no-implicit-dependencies
import { FileEntry, SchematicContext, Tree, FileVisitor } from '@angular-devkit/schematics';
import { WorkspaceSchema } from '@angular-devkit/core/src/workspace';

import * as fs from 'fs';
import * as path from 'path';
import { ClassChanges, BindingChanges, SelectorChange, SelectorChanges } from './schema';
import { ClassChanges, BindingChanges, SelectorChange, SelectorChanges, ThemePropertyChanges } from './schema';
import { getIdentifierPositions } from './tsUtils';
import { getProjectPaths, getWorkspace } from './util';
import { getProjectPaths, getWorkspace, getProjects } from './util';

// tslint:disable:arrow-parens
export class UpdateChanges {
protected workspace: WorkspaceSchema;
protected sourcePaths: string[];
protected classChanges: ClassChanges;
protected outputChanges: BindingChanges;
protected inputChanges: BindingChanges;
protected selectorChanges: SelectorChanges;
protected themePropsChanges: ThemePropertyChanges;
protected conditionFunctions: Map<string, Function> = new Map<string, Function>();

private _templateFiles: string[] = [];
Expand Down Expand Up @@ -41,29 +43,35 @@ export class UpdateChanges {
return this._tsFiles;
}

private _sassFiles: string[] = [];
/** Sass (both .scss and .sass) files in the project being updagraded. */
public get sassFiles(): string[] {
if (!this._sassFiles.length) {
// files can be outside the app prefix, so start from sourceRoot
// also ignore schematics `styleext` as Sass can be used regardless
const sourceDirs = getProjects(this.workspace).map(x => x.sourceRoot).filter(x => x);
this.sourceDirsVisitor((fulPath, entry) => {
if (fulPath.endsWith('.scss') || fulPath.endsWith('.sass')) {
this._sassFiles.push(entry.path);
}
}, sourceDirs);
}
return this._sassFiles;
}

/**
* Create a new base schematic to apply changes
* @param rootPath Root folder for the schematic to read configs, pass __dirname
*/
constructor(private rootPath: string, private host: Tree, private context?: SchematicContext) {
this.sourcePaths = getProjectPaths(getWorkspace(host));
this.workspace = getWorkspace(host);
this.sourcePaths = getProjectPaths(this.workspace);

const selectorJson = path.join(this.rootPath, 'changes', 'selectors.json');
if (fs.existsSync(selectorJson)) {
this.selectorChanges = JSON.parse(fs.readFileSync(selectorJson, 'utf-8'));
}
const classJson = path.join(this.rootPath, 'changes', 'classes.json');
if (fs.existsSync(classJson)) {
this.classChanges = JSON.parse(fs.readFileSync(classJson, 'utf-8'));
}
const outputsJson = path.join(this.rootPath, 'changes', 'outputs.json');
if (fs.existsSync(outputsJson)) {
this.outputChanges = JSON.parse(fs.readFileSync(outputsJson, 'utf-8'));
}
const inputsJson = path.join(this.rootPath, 'changes', 'inputs.json');
if (fs.existsSync(inputsJson)) {
this.inputChanges = JSON.parse(fs.readFileSync(inputsJson, 'utf-8'));
}
this.selectorChanges = this.loadConfig('selectors.json');
this.classChanges = this.loadConfig('classes.json');
this.outputChanges = this.loadConfig('outputs.json');
this.inputChanges = this.loadConfig('inputs.json');
this.themePropsChanges = this.loadConfig('theme-props.json');
}

/** Apply configured changes to the Host Tree */
Expand Down Expand Up @@ -91,6 +99,13 @@ export class UpdateChanges {
this.updateClasses(entryPath);
}
}

/** Sass files */
if (this.themePropsChanges && this.themePropsChanges.changes.length) {
for (const entryPath of this.sassFiles) {
this.updateThemeProps(entryPath);
}
}
}

/** Add condition funtion. */
Expand Down Expand Up @@ -225,6 +240,46 @@ export class UpdateChanges {
}
}

protected updateThemeProps(entryPath: string) {
let fileContent = this.host.read(entryPath).toString();
let overwrite = false;
for (const change of this.themePropsChanges.changes) {
if (fileContent.indexOf(change.owner) !== -1) {
const searchPattern = String.raw`${change.owner}\([\s\S]+?\);`;
const matches = fileContent.match(new RegExp(searchPattern, 'g'));
if (!matches) {
continue;
}
for (const match of matches) {
if (match.indexOf(change.name)) {
const name = change.name.replace('$', '\\$');
let reg = new RegExp(String.raw`${name}:`, 'g');
let replace = change.replaceWith;
if (change.remove) {
reg = new RegExp(String.raw`((\()|,)[\s]*?${name}:[^,]+?(?!;)`, 'g');
replace = '';
}
fileContent = fileContent.replace(
match,
match.replace(reg, replace)
);
overwrite = true;
}
}
}
}
if (overwrite) {
this.host.overwrite(entryPath, fileContent);
}
}

private loadConfig(configJson: string) {
const filePath = path.join(this.rootPath, 'changes', configJson);
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
}

private areConditionsFulfiled(match: string, conditions: string[]): boolean {
if (conditions) {
for (const condition of conditions) {
Expand All @@ -245,7 +300,7 @@ export class UpdateChanges {
const propMatch = propertyMatchArray[0].trim();
const propValueMatch = propMatch.match(new RegExp(`=(["'])(.+?)${'\\1'}`));
if (propValueMatch && propValueMatch.length > 0) {
const propValue = propValueMatch[propValueMatch.length-1];
const propValue = propValueMatch[propValueMatch.length - 1];

if (propMatch.startsWith('[')) {
return fileContent.replace(ownerMatch, ownerMatch + `{{${propValue}}}`);
Expand All @@ -258,8 +313,8 @@ export class UpdateChanges {
return fileContent;
}

private sourceDirsVisitor(visitor: FileVisitor) {
for (const sourcePath of this.sourcePaths) {
private sourceDirsVisitor(visitor: FileVisitor, dirs = this.sourcePaths) {
for (const sourcePath of dirs) {
const srcDir = this.host.getDir(sourcePath);
srcDir.visit(visitor);
}
Expand Down
17 changes: 14 additions & 3 deletions projects/igniteui-angular/migrations/common/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
// generate schema:
// npx typescript-json-schema migrations/common/schema/index.ts SelectorChanges -o migrations/common/schema/selector.schema.json --required

// tslint:disable:interface-name
export interface ThemePropertyChanges {
/** An array of changes to theme function properties */
changes: ThemePropertyChange[];
}
export interface ThemePropertyChange extends ChangeAction {
/** Name of the theme property */
name: string;
/** Theming function this parameter belongs to */
owner: string;
}


export interface SelectorChanges {
/** An array of changes to component/directive selectors */
changes: SelectorChange[];
Expand Down Expand Up @@ -35,9 +46,9 @@ export interface ClassChange {
}

export interface ChangeAction {
/** Replace original selector with new one */
/** Replace original selector/property with new one */
replaceWith?: string;
/** Remove directive/component */
/** Remove directive/component/property */
remove?: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThemePropertyChange": {
"properties": {
"name": {
"description": "Name of the theme property",
"type": "string"
},
"owner": {
"description": "Theming function this parameter belongs to",
"type": "string"
},
"remove": {
"description": "Remove directive/component/property",
"type": "boolean"
},
"replaceWith": {
"description": "Replace original selector/property with new one",
"type": "string"
}
},
"required": [
"name",
"owner"
],
"type": "object"
}
},
"properties": {
"changes": {
"description": "An array of changes to theme function properties",
"items": {
"$ref": "#/definitions/ThemePropertyChange"
},
"type": "array"
}
},
"required": [
"changes"
],
"type": "object"
}

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"version": "6.2.0",
"description": "Updates Ignite UI for Angular from v6.1.x to v6.2.0",
"factory": "./update-6_2"
},
"migration-06": {
"version": "6.2.1",
"description": "Updates Ignite UI for Angular from v6.2.0 to v6.2.1",
"factory": "./update-6_2_1"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "../../common/schema/theme-props.schema.json",
"changes": [
{
"name": "$chip-background",
"replaceWith": "$background",
"owner": "igx-chip-theme"
},
{
"name": "$chip-hover-background",
"replaceWith": "$hover-background",
"owner": "igx-chip-theme"
}
]
}
51 changes: 51 additions & 0 deletions projects/igniteui-angular/migrations/update-6_2_1/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as path from 'path';

// tslint:disable:no-implicit-dependencies
import { virtualFs } from '@angular-devkit/core';
import { EmptyTree } from '@angular-devkit/schematics';
// tslint:disable-next-line:no-submodule-imports
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';

describe('Update 6.2.1', () => {
let appTree: UnitTestTree;
const schematicRunner = new SchematicTestRunner('ig-migrate', path.join(__dirname, '../migration-collection.json'));
const configJson = {
defaultProject: 'testProj',
projects: {
testProj: {
sourceRoot: '/testSrc'
}
},
schematics: {
'@schematics/angular:component': {
prefix: 'appPrefix'
}
}
};

beforeEach(() => {
appTree = new UnitTestTree(new EmptyTree());
appTree.create('/angular.json', JSON.stringify(configJson));
});

it('should update Sass files', done => {
appTree.create(
'/testSrc/appPrefix/style.scss',
`$dark-chip-theme: igx-chip-theme(
$roundness: 4px,
$chip-background: #180505,
$chip-hover-background: white
);`
);
const tree = schematicRunner.runSchematic('migration-06', {}, appTree);
expect(tree.readContent('/testSrc/appPrefix/style.scss'))
.toEqual(
`$dark-chip-theme: igx-chip-theme(
$roundness: 4px,
$background: #180505,
$hover-background: white
);`
);
done();
});
});
Loading

0 comments on commit 9b7e158

Please sign in to comment.