UNPKG

igniteui-angular

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

434 lines (433 loc) • 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isMemberIgniteUI = exports.getTypeDefinitionAtPosition = exports.createProjectService = exports.getLanguageService = exports.findMatches = exports.getRenamePositions = exports.namedImportFilter = exports.getImportModulePositions = exports.getIdentifierPositions = exports.MemberInfo = exports.CUSTOM_TS_PLUGIN_NAME = exports.CUSTOM_TS_PLUGIN_PATH = exports.NG_CORE_PACKAGE_NAME = exports.NG_LANG_SERVICE_PACKAGE_NAME = exports.IG_PACKAGE_NAME = void 0; const ts = require("typescript"); const tss = require("typescript/lib/tsserverlibrary"); const util_1 = require("./util"); const tsLogger_1 = require("./tsLogger"); exports.IG_PACKAGE_NAME = 'igniteui-angular'; exports.NG_LANG_SERVICE_PACKAGE_NAME = '@angular/language-service'; exports.NG_CORE_PACKAGE_NAME = '@angular/core'; exports.CUSTOM_TS_PLUGIN_PATH = './tsPlugin'; exports.CUSTOM_TS_PLUGIN_NAME = 'igx-ts-plugin'; var SyntaxTokens; (function (SyntaxTokens) { SyntaxTokens["ClosingParenthesis"] = ")"; SyntaxTokens["MemberAccess"] = "."; SyntaxTokens["Question"] = "?"; })(SyntaxTokens || (SyntaxTokens = {})); class MemberInfo { } exports.MemberInfo = MemberInfo; /** Returns a source file */ // export function getFileSource(sourceText: string): ts.SourceFile { // return ts.createSourceFile('', sourceText, ts.ScriptTarget.Latest, true); // } const getIdentifierPositions = (source, name) => { if (typeof source === 'string') { source = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true); } const positions = []; const checkIdentifier = (node) => { if (!ts.isIdentifier(node) || !node.parent) { return false; } if (node.parent.kind === ts.SyntaxKind.PropertyDeclaration) { // `const identifier = ...` return false; } if (ts.isPropertyAssignment(node.parent) || ts.isPropertySignature(node.parent)) { // make sure it's not prop assign `= { IgxClass: "fake"}` // definition `prop: { IgxClass: string; }` // name: initializer if (node.parent.name.getText() === name) { return false; } } // for methods the node.text will not contain characters like () const cleanName = name.match(/\w+/g)[0] || name; return node.text === cleanName; }; const findIdentifiers = (node) => { if (checkIdentifier(node)) { // Use `.getStart()` as node.pos includes the space(s) before the identifier text positions.push({ start: node.getStart(), end: node.end }); } ts.forEachChild(node, findIdentifiers); }; source.forEachChild(findIdentifiers); return positions; }; exports.getIdentifierPositions = getIdentifierPositions; /** Returns the positions of import from module string literals */ const getImportModulePositions = (sourceText, startsWith) => { const source = ts.createSourceFile('', sourceText, ts.ScriptTarget.Latest, true); const positions = []; for (const statement of source.statements) { if (statement.kind === ts.SyntaxKind.ImportDeclaration) { const specifier = statement.moduleSpecifier; if (specifier.text.startsWith(startsWith)) { // string literal pos will include quotes, trim with 1 positions.push({ start: specifier.getStart() + 1, end: specifier.end - 1 }); } } } return positions; }; exports.getImportModulePositions = getImportModulePositions; /** Filters out statements to named imports (e.g. `import {x, y}`) from PACKAGE_IMPORT */ const namedImportFilter = (statement) => { if (statement.kind === ts.SyntaxKind.ImportDeclaration && statement.moduleSpecifier.text.endsWith(exports.IG_PACKAGE_NAME)) { const clause = statement.importClause; return clause && clause.namedBindings && clause.namedBindings.kind === ts.SyntaxKind.NamedImports; } return false; }; exports.namedImportFilter = namedImportFilter; const getRenamePositions = (sourcePath, name, service) => { const source = service.getProgram().getSourceFile(sourcePath); const positions = []; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const imports = source.statements.filter(exports.namedImportFilter); if (!imports.length) { return positions; } const elements = imports .map(x => x.importClause.namedBindings.elements) .reduce((prev, current) => prev.concat(current)); for (const elem of elements) { if (elem.propertyName && elem.propertyName.text === name) { // alias imports `igxClass as smth` -> <propertyName> as <name> // other references are only for the name portion positions.push({ start: elem.propertyName.getStart(), end: elem.propertyName.getEnd() }); break; } if (!elem.propertyName && elem.name.text === name) { const renames = service.findRenameLocations(sourcePath, elem.name.getStart(), false, false, false); if (renames) { const renamesPos = renames.map(x => ({ start: x.textSpan.start, end: x.textSpan.start + x.textSpan.length })); positions.push(...renamesPos); } } } return positions; }; exports.getRenamePositions = getRenamePositions; const findMatches = (content, toFind) => { let matches; const regex = new RegExp((0, util_1.escapeRegExp)(toFind), 'g'); const matchesPositions = []; do { matches = regex.exec(content); if (matches) { matchesPositions.push(matches.index); } } while (matches); return matchesPositions; }; exports.findMatches = findMatches; //#region Language Service /** * Create a TypeScript language service * * @param serviceHost A TypeScript language service host */ const getLanguageService = (filePaths, host, options = {}) => { const fileVersions = new Map(); patchHostOverwrite(host, fileVersions); const servicesHost = { getCompilationSettings: () => options, getScriptFileNames: () => filePaths, getScriptVersion: fileName => { // return host.actions.filter(x => x.path === fileName && x.kind !== 'c').length.toString(); const version = fileVersions.get(fileName) || 0; return version.toString(); }, getScriptSnapshot: fileName => { if (!host.exists(fileName)) { return undefined; } return ts.ScriptSnapshot.fromString(host.read(fileName).toString()); }, getCurrentDirectory: () => process.cwd(), getDefaultLibFileName: opts => ts.getDefaultLibFilePath(opts), fileExists: fileName => filePaths.indexOf(fileName) !== -1, readFile: (path, encoding) => host.read(path).toString(encoding) }; return ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); }; exports.getLanguageService = getLanguageService; const patchHostOverwrite = (host, fileVersions) => { const original = host.overwrite; host.overwrite = (path, content) => { const version = fileVersions.get(path) || 0; fileVersions.set(path, version + 1); original.call(host, path, content); }; }; /** * Create a project service singleton that holds all projects within a directory tree * * @param serverHost Used by the tss to navigate the directory tree */ const createProjectService = (serverHost) => { // set traceToConsole to true to enable logging const logger = new tsLogger_1.Logger(false, tss.server.LogLevel.verbose); const projectService = new tss.server.ProjectService({ host: serverHost, logger, /* not needed since we will run only migrations */ cancellationToken: tss.server.nullCancellationToken, /* do not allow more than one InferredProject per project root */ useSingleInferredProject: true, useInferredProjectPerProjectRoot: true, /* will load only global plug-ins */ globalPlugins: [exports.CUSTOM_TS_PLUGIN_NAME, exports.NG_LANG_SERVICE_PACKAGE_NAME], allowLocalPluginLoads: false, typingsInstaller: tss.server.nullTypingsInstaller, session: undefined }); projectService.setHostConfiguration({ formatOptions: projectService.getHostFormatCodeOptions(), extraFileExtensions: [ { extension: '.html', isMixedContent: false, scriptKind: tss.ScriptKind.External, } ] }); projectService.configurePlugin({ pluginName: exports.CUSTOM_TS_PLUGIN_NAME, configuration: {} }); projectService.configurePlugin({ pluginName: exports.NG_LANG_SERVICE_PACKAGE_NAME, configuration: { ivy: true, angularOnly: false, }, }); return projectService; }; exports.createProjectService = createProjectService; /** * Attempts to get type definitions using the TypeScript Language Service. * Can fall back to a cached version of the TSLS. */ const getTypeDefinitions = (langServ, entryPath, position) => /* getTypeScriptLanguageService is attached by us to the Typescript Language Service via a custom made plugin, it's sole purpose is to cache the language service and return it before any other plugins modify it */ langServ.getTypeDefinitionAtPosition(entryPath, position) || langServ.getTypeScriptLanguageService().getTypeDefinitionAtPosition(entryPath, position); /** * Get type information about a TypeScript identifier * * @param langServ TypeScript/Angular LanguageService * @param entryPath path to file (absolute) * @param position Index of identifier */ const getTypeDefinitionAtPosition = (langServ, entryPath, position) => { var _a; let definition; try { definition = (_a = langServ.getDefinitionAndBoundSpan(entryPath, position)) === null || _a === void 0 ? void 0 : _a.definitions[0]; } catch (err) { return null; } if (!definition) { return null; } if (definition.kind.toString() === 'reference') { // if the definition's kind is a reference, the identifier is a template variable referred in an internal/external template return langServ.getDefinitionAndBoundSpan(entryPath, definition.textSpan.start).definitions[0]; } if (definition.kind.toString() === 'method') { return getMethodTypeDefinition(langServ, definition); } if (entryPath.endsWith('.ts')) { // for ts files we can use the type checker to look up a specific node // and attempt to resolve its actual type const sourceFile = langServ.getProgram().getSourceFile(entryPath); // const node = (tss as any).getTouchingPropertyName(sourceFile, position); -> tss internal that looks up a node const node = findNodeAtPosition(sourceFile, position); if (node) { const memberInfo = resolveMemberInfo(langServ, node); if (memberInfo) { return memberInfo; } } } let typeDefs = getTypeDefinitions(langServ, definition.fileName || entryPath, definition.textSpan.start); // if there are no type definitions found, the identifier is a ts property, referred in an internal/external template // or is a reference in a decorator if (!typeDefs) { /* normally, the tsserver will consider non .ts files as external to the project however, we load .html files which we can handle with the Angular language service here we're only looking for definitions in a .ts source file we call the getSourceFile function which accesses a map of files, previously loaded by the tsserver at this point the map contains all .html files that we've included we have to ignore them, since the language service will attempt to parse them as .ts files */ if (!definition.fileName.endsWith('.ts')) { return null; } const sourceFile = langServ.getProgram().getSourceFile(definition.fileName); if (!sourceFile) { return null; } // find the class declaration in the source file where the member that we want to migrate is declared // atm we are explicitly ignoring unnamed class declarations like - export default class { ... } const classDeclaration = sourceFile.statements .filter((m) => m.kind === tss.SyntaxKind.ClassDeclaration) .find(m => { var _a; return ((_a = m.name) === null || _a === void 0 ? void 0 : _a.getText()) === definition.containerName; }); if (!classDeclaration) { // there must be at least one class declaration in the .ts file and the property must belong to it return null; } const member = classDeclaration.members .filter(x => x.kind !== ts.SyntaxKind.Constructor) .find(m => { var _a; return ((_a = m.name) === null || _a === void 0 ? void 0 : _a.getText()) === definition.name; }); if (!member) { return null; } typeDefs = getTypeDefinitions(langServ, definition.fileName, member.name.getStart() + 1); } if (typeDefs === null || typeDefs === void 0 ? void 0 : typeDefs.length) { return typeDefs[0]; } return null; }; exports.getTypeDefinitionAtPosition = getTypeDefinitionAtPosition; /** * Determines if a member belongs to a type in the `igniteui-angular` toolkit. * * @param change The change that will be applied. * @param langServ The Typescript/Angular Language Service * @param entryPath Relative file path. * @param matchPosition The position of the identifier. */ const isMemberIgniteUI = (change, langServ, entryPath, matchPosition) => { var _a, _b; const content = langServ.getProgram().getSourceFile(entryPath).getFullText(); matchPosition = shiftMatchPosition(matchPosition, content); const prevChar = content.substr(matchPosition - 1, 1); if (prevChar === SyntaxTokens.ClosingParenthesis) { // methodCall().identifier matchPosition = (_b = (_a = langServ.getBraceMatchingAtPosition(entryPath, matchPosition - 1)[0]) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : matchPosition; } let typeDef; try { typeDef = (0, exports.getTypeDefinitionAtPosition)(langServ, entryPath, matchPosition); } catch (err) { false; } if (!typeDef) { return false; } return typeDef.fileName.includes(exports.IG_PACKAGE_NAME) && change.definedIn.indexOf(typeDef.name) !== -1; }; exports.isMemberIgniteUI = isMemberIgniteUI; const resolveMemberInfo = (langServ, node) => { var _a, _b; const typeChecker = langServ.getProgram().getTypeChecker(); const nodeType = typeChecker.getTypeAtLocation(node); const typeArguments = typeChecker.getTypeArguments(nodeType); if (typeArguments && typeArguments.length < 1) { // it's not a generic type so try to look up its name and fileName // atm we do not support migrating union/intersection generic types // a type symbol (type) should have only one declaration // if the type is 'any' or 'some', there will be no type symbol const name = (_a = nodeType.getSymbol()) === null || _a === void 0 ? void 0 : _a.getName(); const declarations = (_b = nodeType.getSymbol()) === null || _b === void 0 ? void 0 : _b.getDeclarations(); if (declarations && declarations.length > 0) { const fileName = declarations[0].getSourceFile().fileName; if (name && fileName) { return { name, fileName }; } } } return null; }; /** * Looks up a node which end property matches the specified position. * Can go to the next node if the currently found one is invalid (comment for example) */ const findNodeAtPosition = (sourceFile, position) => { if (!sourceFile) { return null; } return findInnerNode(sourceFile, position); }; const findInnerNode = (node, position) => { if (position <= node.getEnd()) { // see tss.forEachChild for documentation // look for the innermost child that matches the position return node.forEachChild(cn => findInnerNode(cn, position)) || node; } return null; }; /** * Shifts the match position of the identifier to the left * until any character other than an empty string or a '.' is reached. #9347 */ const shiftMatchPosition = (matchPosition, content) => { do { matchPosition--; } while (matchPosition > 0 && !content[matchPosition - 1].trim() || content[matchPosition - 1] === SyntaxTokens.MemberAccess || content[matchPosition - 1] === SyntaxTokens.Question); return matchPosition; }; /** * Looks up a method's definition return type. * * @param langServ The TypeScript LanguageService. * @param definition The method definition. */ const getMethodTypeDefinition = (langServ, definition) => { var _a, _b, _c, _d; // TODO: use typechecker for all the things? const sourceFile = langServ.getProgram().getSourceFile(definition.fileName); // find the class declaration in the source file where the method that we want to migrate is declared const classDeclaration = sourceFile.statements .filter((m) => m.kind === tss.SyntaxKind.ClassDeclaration) .find(m => { var _a; return ((_a = m.name) === null || _a === void 0 ? void 0 : _a.getText()) === definition.containerName; }); // find the node in the class declaration's members which represents the method const methodDeclaration = classDeclaration === null || classDeclaration === void 0 ? void 0 : classDeclaration.members.filter((m) => m.kind === tss.SyntaxKind.MethodDeclaration).find(m => m.name.getText() === definition.name); if (!methodDeclaration) { return null; } // use the TypeChecker to resolve implicit/explicit method return types const typeChecker = langServ.getProgram().getTypeChecker(); const signature = typeChecker.getSignatureFromDeclaration(methodDeclaration); if (!signature) { return null; } const returnType = typeChecker.getReturnTypeOfSignature(signature); const name = (_b = (_a = returnType === null || returnType === void 0 ? void 0 : returnType.symbol) === null || _a === void 0 ? void 0 : _a.escapedName) === null || _b === void 0 ? void 0 : _b.toString(); if (name && ((_d = (_c = returnType.symbol) === null || _c === void 0 ? void 0 : _c.declarations) === null || _d === void 0 ? void 0 : _d.length) > 0) { // there should never be a case where a type is declared in more than one file /** * For union return types like T | null | undefined * and intersection return types like T & null & undefined * the TypeChecker ignores null and undefined and returns only T which is not * marked as a union or intersection type. * * For union and intersection types like T | R | C * the TypeChecker returns a TypeObject which is marked as union or intersection type. */ const fileName = returnType.symbol.declarations[0].getSourceFile().fileName; return { name, fileName }; } return null; }; //#endregion