UNPKG

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
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; };