igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
297 lines (251 loc) • 9.73 kB
text/typescript
import { normalize } from '@angular-devkit/core';
import * as path from 'path';
import { SchematicContext, Tree } from '@angular-devkit/schematics';
import { WorkspaceSchema, WorkspaceProject } from '@schematics/angular/utility/workspace-models';
import { execSync } from 'child_process';
import {
Attribute,
Comment,
Element,
Expansion,
ExpansionCase,
HtmlParser,
HtmlTagDefinition,
Node,
Text,
Visitor
} from '@angular/compiler';
import { replaceMatch } from './tsUtils';
const configPaths = ['/.angular.json', '/angular.json'];
export const getProjectPaths = (config: WorkspaceSchema): string[] => {
const sourceDirs = [];
const projects = getProjects(config);
for (const proj of projects) {
const sourcePath = path.join('/', proj.sourceRoot);
sourceDirs.push(normalize(sourcePath));
}
return sourceDirs;
};
export const getWorkspacePath = (host: Tree): string => configPaths.find(x => host.exists(x));
export const getWorkspace = (host: Tree): WorkspaceSchema => {
const configPath = getWorkspacePath(host);
if (configPath) {
return JSON.parse(host.read(configPath).toString());
}
return null;
};
export const getProjects = (config: WorkspaceSchema): WorkspaceProject[] => {
const projects: WorkspaceProject[] = [];
for (const projName of Object.keys(config.projects)) {
const proj = config.projects[projName];
if ((proj.architect && proj.architect.e2e && !proj.architect.build)) {
// skip old style e2e-only projects
continue;
}
projects.push(proj);
}
return projects;
};
export const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const supports = (name: string): boolean => {
try {
execSync(`${name} --version`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
export const getPackageManager = (host: Tree) => {
const hasYarn = supports('yarn');
const hasYarnLock = host.exists('yarn.lock');
if (hasYarn && hasYarnLock) {
return 'yarn';
}
return 'npm';
};
export const canResolvePackage = (pkg: string): boolean => {
try {
// attempt resolve in child process to keep result out of package.json cache
// otherwise resolve will not read the json again (after install) and won't load the main correctly
// https://stackoverflow.com/questions/59865584/how-to-invalidate-cached-require-resolve-results
execSync(`node -e "require.resolve('${pkg}');"`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
};
export const getPackageVersion = (pkg: string): string => {
let version = null;
try {
version = require(path.posix.join(pkg, 'package.json'))?.version;
} catch {
return version;
}
return version;
};
export const tryInstallPackage = (context: SchematicContext, packageManager: string, pkg: string) => {
try {
context.logger.debug(`Installing ${pkg} via ${packageManager}.`);
switch (packageManager) {
case 'yarn':
execSync(`${packageManager} add ${pkg}`, { stdio: 'ignore' });
break;
case 'npm':
execSync(`${packageManager} i ${pkg} --no-save --no-audit`, { stdio: 'ignore' });
break;
}
context.logger.debug(`${pkg} installed successfully.`);
} catch (e) {
context.logger.warn(`Could not install ${pkg}.`, e);
}
};
export const tryUninstallPackage = (context: SchematicContext, packageManager: string, pkg: string) => {
try {
context.logger.debug(`Uninstalling ${pkg} via ${packageManager}`);
switch (packageManager) {
case 'yarn':
execSync(`${packageManager} remove ${pkg}`, { stdio: 'ignore' });
break;
case 'npm':
execSync(`${packageManager} uninstall ${pkg} --no-save`, { stdio: 'ignore' });
break;
}
context.logger.debug(`${pkg} uninstalled successfully.`);
} catch (e) {
context.logger
.warn(`Could not uninstall ${pkg}, you may want to uninstall it manually.`, e);
}
};
interface TagOffset {
start: number;
end: number;
}
export interface SourceOffset {
startTag: TagOffset;
endTag: TagOffset;
file: {
content: string;
url: string;
};
node?: Element;
}
export class FileChange {
constructor(
public position = 0,
public text = '',
public replaceText = '',
public type: 'insert' | 'replace' = 'insert'
) { }
public apply(content: string) {
if (this.type === 'insert') {
return `${content.substring(0, this.position)}${this.text}${content.substring(this.position)}`;
}
return replaceMatch(content, this.replaceText, this.text, this.position);
}
}
/**
* Parses an Angular template file/content and returns an array of the root nodes of the file.
* TODO: Maybe make async and dynamically import the HtmlParser
* @param host
* @param filePath
* @param encoding
*/
export const parseFile = (parser: HtmlParser, host: Tree, filePath: string, encoding: BufferEncoding = 'utf8') =>
parser.parse(host.read(filePath).toString(encoding), filePath).rootNodes;
// export const parseFile = async (host: Tree, filePath: string, encoding = 'utf8') => {
// const { HtmlParser } = await import('@angular/compiler')
// return new HtmlParser().parse(host.read(filePath).toString(encoding), filePath).rootNodes;
// }
export const findElementNodes = (root: Node[], tag: string | string[]): Node[] => {
const tags = new Set(Array.isArray(tag) ? tag : [tag]);
return flatten(Array.isArray(root) ? root : [root])
.filter((node: Element) => tags.has(node.name));
};
export const hasAttribute = (root: Element, attribute: string | string[]) => {
const attrs = Array.isArray(attribute) ? attribute : [attribute];
return !!root.attrs.find(a => attrs.includes(a.name));
};
export const getAttribute = (root: Element, attribute: string | string[]) => {
const attrs = Array.isArray(attribute) ? attribute : [attribute];
return root.attrs.filter(a => attrs.includes(a.name));
};
export const getSourceOffset = (element: Element): SourceOffset => {
const { startSourceSpan, endSourceSpan } = element;
return {
startTag: { start: startSourceSpan.start.offset, end: startSourceSpan.end.offset },
// V.S. May 11th, 2021: Tag could be self-closing
endTag: { start: endSourceSpan?.start.offset, end: endSourceSpan?.end.offset },
file: {
content: startSourceSpan.start.file.content,
url: startSourceSpan.start.file.url
},
node: element
};
};
const isElement = (node: Node | Element): boolean => (node as Element).children !== undefined;
/**
* Given an array of `Node` objects, flattens the ast tree to a single array.
* De facto only `Element` type objects have children.
*
* @param list
*/
export const flatten = (list: Node[]) => {
let r: Node[] = [];
for (const node of list) {
r.push(node);
if (isElement(node)) {
r = r.concat(flatten((node as Element).children));
}
}
return r;
};
/**
* https://github.com/angular/angular/blob/master/packages/compiler/test/ml_parser/util/util.ts
*
* May be useful for validating the output of our own migrations,
*/
class SerializerVisitor implements Visitor {
/**
*
*/
constructor(private getHtmlTagDefinition: (tagName: string) => HtmlTagDefinition) { }
public visitElement(element: Element, _context: any): any {
if (this.getHtmlTagDefinition(element.name).isVoid) {
return `<${element.name}${this._visitAll(element.attrs, ' ')}/>`;
}
return `<${element.name}${this._visitAll(element.attrs, ' ')}>${this._visitAll(element.children)}</${element.name}>`;
}
public visitAttribute(attribute: Attribute, _context: any): any {
return attribute.value === '' ? `${attribute.name}` : `${attribute.name}="${attribute.value}"`;
}
public visitText(text: Text, _context: any): any {
return text.value;
}
public visitComment(comment: Comment, _context: any): any {
return `<!--${comment.value}-->`;
}
public visitExpansion(expansion: Expansion, _context: any): any {
return `{${expansion.switchValue}, ${expansion.type},${this._visitAll(expansion.cases)}}`;
}
public visitExpansionCase(expansionCase: ExpansionCase, _context: any): any {
return ` ${expansionCase.value} {${this._visitAll(expansionCase.expression)}}`;
}
private _visitAll(nodes: Node[], join = ''): string {
if (nodes.length === 0) {
return '';
}
return join + nodes.map(a => a.visit(this, null)).join(join);
}
}
export const serializeNodes = (nodes: Node[], getHtmlTagDefinition: (tagName: string) => HtmlTagDefinition): string[] => {
return nodes.map(node => node.visit(new SerializerVisitor(getHtmlTagDefinition), null))
};
export const makeNgIf = (name: string, value: string) => name.startsWith('[') && value !== 'true';
export const stringifyAttriutes = (attributes: Attribute[]) => {
let stringAttributes = '';
attributes.forEach(element => {
// eslint-disable-next-line max-len
stringAttributes = stringAttributes.concat(element.name.includes('#') ? ` ${element.name} ` : `${element.name}="${element.value}" `);
});
return stringAttributes;
};