-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added move-component schematic
- Loading branch information
Showing
6 changed files
with
608 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}; | ||
} |
Oops, something went wrong.