Skip to content

Commit

Permalink
feat: added move-component schematic
Browse files Browse the repository at this point in the history
  • Loading branch information
Danilo Hoffmann authored and dhhyi committed Dec 18, 2019
1 parent b04fadf commit 6b1523b
Show file tree
Hide file tree
Showing 6 changed files with 608 additions and 0 deletions.
6 changes: 6 additions & 0 deletions schematics/src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
"factory": "./azure-pipeline/factory#createAzurePipeline",
"description": "Create Azure Pipeline.",
"schema": "./azure-pipeline/schema.json"
},
"move-component": {
"factory": "./move-component/factory#move",
"description": "Move Component.",
"schema": "./move-component/schema.json",
"hidden": true
}
}
}
156 changes: 156 additions & 0 deletions schematics/src/move-component/factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@angular-devkit/core");
const schematics_1 = require("@angular-devkit/schematics");
const tsquery_1 = require("@phenomnomnominal/tsquery");
const project_1 = require("@schematics/angular/utility/project");
const filesystem_1 = require("../utils/filesystem");
function similarIdx(str1, str2) {
for (let index = 0; index < Math.min(str1.length, str2.length); index++) {
if (str1[index] !== str2[index]) {
return index;
}
}
return 0;
}
function getAbsolutePath(base, rel) {
if (rel.startsWith('..')) {
const myPath = base.split('/');
myPath.pop();
const otherPath = rel.split('/').reverse();
while (otherPath.length && otherPath[otherPath.length - 1] === '..') {
otherPath.pop();
myPath.pop();
}
for (const el of otherPath.reverse()) {
myPath.push(el);
}
return myPath.join('/');
}
}
function getRelativePath(base, abs) {
const basePath = base.split('/');
basePath.pop();
const absPath = abs.split('/');
while (basePath[0] === absPath[0]) {
basePath.shift();
absPath.shift();
}
while (basePath.length) {
basePath.pop();
absPath.splice(0, 0, '..');
}
return absPath.join('/');
}
function move(options) {
return host => {
if (!options.project) {
throw new schematics_1.SchematicsException('Option (project) is required.');
}
if (!options.from) {
throw new schematics_1.SchematicsException('Option (from) is required.');
}
if (!options.to) {
throw new schematics_1.SchematicsException('Option (to) is required.');
}
const from = options.from.replace(/\/$/, '');
host.getDir(from);
const to = options.to.replace(/\/$/, '');
const renames = [];
const fromName = from.replace(/.*\//, '');
const fromClassName = core_1.strings.classify(fromName) + 'Component';
const toName = to.replace(/.*\//, '');
if (toName.includes('.')) {
throw new schematics_1.SchematicsException(`target must be a directory`);
}
const toClassName = core_1.strings.classify(toName) + 'Component';
const similarIndex = similarIdx(from, to);
const replacePath = (path) => path
.replace(from.substr(similarIndex), to.substr(similarIndex))
.replace(fromName + '.component', toName + '.component');
const replaceImportPath = (file, path) => {
const newPath = replacePath(path);
if (path !== newPath) {
return newPath;
}
else if (path.includes('..')) {
const match = /(\.\.[\w\/\.\-]+)/.exec(path);
if (match) {
const fromRelative = match[0];
const fromAbsolute = getAbsolutePath(file, fromRelative);
const toAbsolute = replacePath(fromAbsolute);
const potentiallyMovedFile = replacePath(file);
const toRelative = getRelativePath(potentiallyMovedFile, toAbsolute);
return path.replace(fromRelative, toRelative);
}
}
return newPath;
};
// tslint:disable-next-line:no-console
console.log('moving', options.from, '\n to', options.to);
const sourceRoot = project_1.getProject(host, options.project).sourceRoot;
host.visit(file => {
if (file.startsWith(`/${sourceRoot}/app/`)) {
if (file.includes(from + '/')) {
renames.push([file, replacePath(file)]);
if (fromName !== toName && file.endsWith('.component.ts')) {
const updater = host.beginUpdate(file);
tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]')
.map(x => x.parent)
.forEach(componentDecorator => {
tsquery_1.tsquery(componentDecorator, 'PropertyAssignment')
.map((pa) => pa.initializer)
.forEach(x => {
updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName));
});
});
host.commitUpdate(updater);
}
}
if (file.endsWith('.ts')) {
if (fromClassName !== toClassName) {
const identifiers = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`);
if (identifiers.length) {
const updater = host.beginUpdate(file);
identifiers.forEach(x => updater
.remove(x.pos, x.end - x.pos)
.insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName)));
host.commitUpdate(updater);
}
}
const imports = tsquery_1.tsquery(filesystem_1.readIntoSourceFile(host, file), file.includes(fromName) ? `ImportDeclaration` : `ImportDeclaration[text=/.*${fromName}.*/]`).filter((x) => file.includes(fromName) || x.getText().includes(`/${fromName}/`));
if (imports.length) {
const updates = [];
imports.forEach(importDeclaration => {
tsquery_1.tsquery(importDeclaration, 'StringLiteral').forEach(node => {
const replacement = replaceImportPath(file, node.getFullText());
if (node.getFullText() !== replacement) {
updates.push({ node, replacement });
}
});
});
if (updates.length) {
const updater = host.beginUpdate(file);
updates.forEach(({ node, replacement }) => {
updater.remove(node.pos, node.end - node.pos).insertLeft(node.pos, replacement);
});
host.commitUpdate(updater);
}
}
}
else if (fromName !== toName && file.endsWith('.html')) {
const content = host.read(file).toString();
const replacement = content.replace(new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'), 'ish-' + toName);
if (content !== replacement) {
host.overwrite(file, replacement);
}
}
}
});
renames.forEach(([source, target]) => {
host.create(target, host.read(source));
host.delete(source);
});
};
}
exports.move = move;
185 changes: 185 additions & 0 deletions schematics/src/move-component/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { strings } from '@angular-devkit/core';
import { Rule, SchematicsException } from '@angular-devkit/schematics';
import { tsquery } from '@phenomnomnominal/tsquery';
import { getProject } from '@schematics/angular/utility/project';
import * as ts from 'typescript';

import { readIntoSourceFile } from '../utils/filesystem';

import { MoveComponentOptionsSchema as Options } from './schema';

function similarIdx(str1: string, str2: string) {
for (let index = 0; index < Math.min(str1.length, str2.length); index++) {
if (str1[index] !== str2[index]) {
return index;
}
}
return 0;
}

function getAbsolutePath(base: string, rel: string): string {
if (rel.startsWith('..')) {
const myPath = base.split('/');
myPath.pop();
const otherPath = rel.split('/').reverse();
while (otherPath.length && otherPath[otherPath.length - 1] === '..') {
otherPath.pop();
myPath.pop();
}
for (const el of otherPath.reverse()) {
myPath.push(el);
}
return myPath.join('/');
}
}

function getRelativePath(base: string, abs: string): string {
const basePath = base.split('/');
basePath.pop();
const absPath = abs.split('/');

while (basePath[0] === absPath[0]) {
basePath.shift();
absPath.shift();
}

while (basePath.length) {
basePath.pop();
absPath.splice(0, 0, '..');
}

return absPath.join('/');
}

export function move(options: Options): Rule {
return host => {
if (!options.project) {
throw new SchematicsException('Option (project) is required.');
}

if (!options.from) {
throw new SchematicsException('Option (from) is required.');
}

if (!options.to) {
throw new SchematicsException('Option (to) is required.');
}

const from = options.from.replace(/\/$/, '');
host.getDir(from);
const to = options.to.replace(/\/$/, '');

const renames = [];

const fromName = from.replace(/.*\//, '');
const fromClassName = strings.classify(fromName) + 'Component';
const toName = to.replace(/.*\//, '');
if (toName.includes('.')) {
throw new SchematicsException(`target must be a directory`);
}

const toClassName = strings.classify(toName) + 'Component';

const similarIndex = similarIdx(from, to);

const replacePath = (path: string) =>
path
.replace(from.substr(similarIndex), to.substr(similarIndex))
.replace(fromName + '.component', toName + '.component');

const replaceImportPath = (file: string, path: string) => {
const newPath = replacePath(path);
if (path !== newPath) {
return newPath;
} else if (path.includes('..')) {
const match = /(\.\.[\w\/\.\-]+)/.exec(path);
if (match) {
const fromRelative = match[0];
const fromAbsolute = getAbsolutePath(file, fromRelative);
const toAbsolute = replacePath(fromAbsolute);
const potentiallyMovedFile = replacePath(file);
const toRelative = getRelativePath(potentiallyMovedFile, toAbsolute);
return path.replace(fromRelative, toRelative);
}
}
return newPath;
};
// tslint:disable-next-line:no-console
console.log('moving', options.from, '\n to', options.to);

const sourceRoot = getProject(host, options.project).sourceRoot;

host.visit(file => {
if (file.startsWith(`/${sourceRoot}/app/`)) {
if (file.includes(from + '/')) {
renames.push([file, replacePath(file)]);

if (fromName !== toName && file.endsWith('.component.ts')) {
const updater = host.beginUpdate(file);
tsquery(readIntoSourceFile(host, file), 'Decorator Identifier[name=Component]')
.map(x => x.parent)
.forEach(componentDecorator => {
tsquery(componentDecorator, 'PropertyAssignment')
.map((pa: ts.PropertyAssignment) => pa.initializer)
.forEach(x => {
updater.remove(x.pos, x.end - x.pos).insertLeft(x.pos, x.getFullText().replace(fromName, toName));
});
});
host.commitUpdate(updater);
}
}
if (file.endsWith('.ts')) {
if (fromClassName !== toClassName) {
const identifiers = tsquery(readIntoSourceFile(host, file), `Identifier[name=${fromClassName}]`);
if (identifiers.length) {
const updater = host.beginUpdate(file);
identifiers.forEach(x =>
updater
.remove(x.pos, x.end - x.pos)
.insertLeft(x.pos, x.getFullText().replace(fromClassName, toClassName))
);
host.commitUpdate(updater);
}
}

const imports = tsquery(
readIntoSourceFile(host, file),
file.includes(fromName) ? `ImportDeclaration` : `ImportDeclaration[text=/.*${fromName}.*/]`
).filter((x: ts.ImportDeclaration) => file.includes(fromName) || x.getText().includes(`/${fromName}/`));
if (imports.length) {
const updates: { node: ts.Node; replacement: string }[] = [];
imports.forEach(importDeclaration => {
tsquery(importDeclaration, 'StringLiteral').forEach(node => {
const replacement = replaceImportPath(file, node.getFullText());
if (node.getFullText() !== replacement) {
updates.push({ node, replacement });
}
});
});
if (updates.length) {
const updater = host.beginUpdate(file);
updates.forEach(({ node, replacement }) => {
updater.remove(node.pos, node.end - node.pos).insertLeft(node.pos, replacement);
});
host.commitUpdate(updater);
}
}
} else if (fromName !== toName && file.endsWith('.html')) {
const content = host.read(file).toString();
const replacement = content.replace(
new RegExp(`(?!.*${fromName}[a-z-]+.*)ish-${fromName}`, 'g'),
'ish-' + toName
);
if (content !== replacement) {
host.overwrite(file, replacement);
}
}
}
});

renames.forEach(([source, target]) => {
host.create(target, host.read(source));
host.delete(source);
});
};
}
Loading

0 comments on commit 6b1523b

Please sign in to comment.