UNPKG

@angular/material

Version:
813 lines 132 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ Object.defineProperty(exports, "__esModule", { value: true }); exports.HammerGesturesMigration = void 0; const core_1 = require("@angular-devkit/core"); const schematics_1 = require("@angular/cdk/schematics"); const change_1 = require("@schematics/angular/utility/change"); const fs_1 = require("fs"); const ts = require("typescript"); const find_hammer_script_tags_1 = require("./find-hammer-script-tags"); const find_main_module_1 = require("./find-main-module"); const hammer_template_check_1 = require("./hammer-template-check"); const import_manager_1 = require("./import-manager"); const remove_array_element_1 = require("./remove-array-element"); const remove_element_from_html_1 = require("./remove-element-from-html"); const GESTURE_CONFIG_CLASS_NAME = 'GestureConfig'; const GESTURE_CONFIG_FILE_NAME = 'gesture-config'; const GESTURE_CONFIG_TEMPLATE_PATH = './gesture-config.template'; const HAMMER_CONFIG_TOKEN_NAME = 'HAMMER_GESTURE_CONFIG'; const HAMMER_CONFIG_TOKEN_MODULE = '@angular/platform-browser'; const HAMMER_MODULE_NAME = 'HammerModule'; const HAMMER_MODULE_IMPORT = '@angular/platform-browser'; const HAMMER_MODULE_SPECIFIER = 'hammerjs'; const CANNOT_REMOVE_REFERENCE_ERROR = `Cannot remove reference to "GestureConfig". Please remove manually.`; class HammerGesturesMigration extends schematics_1.DevkitMigration { constructor() { super(...arguments); // The migration is enabled when v9 or v10 are targeted, but actual targets are only // migrated if they are not test targets. We cannot migrate test targets since they have // a limited scope, in regards to their source files, and therefore the HammerJS usage // detection could be incorrect. this.enabled = HammerGesturesMigration._isAllowedVersion(this.targetVersion) && !this.context.isTestTarget; this._printer = ts.createPrinter(); this._importManager = new import_manager_1.ImportManager(this.fileSystem, this._printer); this._nodeFailures = []; /** * Whether custom HammerJS events provided by the Material gesture * config are used in a template. */ this._customEventsUsedInTemplate = false; /** Whether standard HammerJS events are used in a template. */ this._standardEventsUsedInTemplate = false; /** Whether HammerJS is accessed at runtime. */ this._usedInRuntime = false; /** * List of imports that make "hammerjs" available globally. We keep track of these * since we might need to remove them if Hammer is not used. */ this._installImports = []; /** * List of identifiers which resolve to the gesture config from Angular Material. */ this._gestureConfigReferences = []; /** * List of identifiers which resolve to the "HAMMER_GESTURE_CONFIG" token from * "@angular/platform-browser". */ this._hammerConfigTokenReferences = []; /** * List of identifiers which resolve to the "HammerModule" from * "@angular/platform-browser". */ this._hammerModuleReferences = []; /** * List of identifiers that have been deleted from source files. This can be * used to determine if certain imports are still used or not. */ this._deletedIdentifiers = []; } visitTemplate(template) { if (!this._customEventsUsedInTemplate || !this._standardEventsUsedInTemplate) { const { standardEvents, customEvents } = (0, hammer_template_check_1.isHammerJsUsedInTemplate)(template.content); this._customEventsUsedInTemplate = this._customEventsUsedInTemplate || customEvents; this._standardEventsUsedInTemplate = this._standardEventsUsedInTemplate || standardEvents; } } visitNode(node) { this._checkHammerImports(node); this._checkForRuntimeHammerUsage(node); this._checkForMaterialGestureConfig(node); this._checkForHammerGestureConfigToken(node); this._checkForHammerModuleReference(node); } postAnalysis() { // Walk through all hammer config token references and check if there // is a potential custom gesture config setup. const hasCustomGestureConfigSetup = this._hammerConfigTokenReferences.some(r => this._checkForCustomGestureConfigSetup(r)); const usedInTemplate = this._standardEventsUsedInTemplate || this._customEventsUsedInTemplate; /* Possible scenarios and how the migration should change the project: 1. We detect that a custom HammerJS gesture config is set up: - Remove references to the Material gesture config if no HammerJS event is used. - Print a warning about ambiguous configuration that cannot be handled completely if there are references to the Material gesture config. 2. We detect that HammerJS is only used programmatically: - Remove references to GestureConfig of Material. - Remove references to the "HammerModule" if present. 3. We detect that standard HammerJS events are used in a template: - Set up the "HammerModule" from platform-browser. - Remove all gesture config references. 4. We detect that custom HammerJS events provided by the Material gesture config are used. - Copy the Material gesture config into the app. - Rewrite all gesture config references to the newly copied one. - Set up the new gesture config in the root app module. - Set up the "HammerModule" from platform-browser. 4. We detect no HammerJS usage at all: - Remove Hammer imports - Remove Material gesture config references - Remove HammerModule setup if present. - Remove Hammer script imports in "index.html" files. */ if (hasCustomGestureConfigSetup) { // If a custom gesture config is provided, we always assume that HammerJS is used. HammerGesturesMigration.globalUsesHammer = true; if (!usedInTemplate && this._gestureConfigReferences.length) { // If the Angular Material gesture events are not used and we found a custom // gesture config, we can safely remove references to the Material gesture config // since events provided by the Material gesture config are guaranteed to be unused. this._removeMaterialGestureConfigSetup(); this.printInfo('The HammerJS v9 migration for Angular Components detected that HammerJS is ' + 'manually set up in combination with references to the Angular Material gesture ' + 'config. This target cannot be migrated completely, but all references to the ' + 'deprecated Angular Material gesture have been removed. Read more here: ' + 'https://github.com/angular/components/blob/3a204da37fd1366cae411b5c234517ecad199737/guides/v9-hammerjs-migration.md#the-migration-reported-ambiguous-usage-what-should-i-do'); } else if (usedInTemplate && this._gestureConfigReferences.length) { // Since there is a reference to the Angular Material gesture config, and we detected // usage of a gesture event that could be provided by Angular Material, we *cannot* // automatically remove references. This is because we do *not* know whether the // event is actually provided by the custom config or by the Material config. this.printInfo('The HammerJS v9 migration for Angular Components detected that HammerJS is ' + 'manually set up in combination with references to the Angular Material gesture ' + 'config. This target cannot be migrated completely. Please manually remove ' + 'references to the deprecated Angular Material gesture config. Read more here: ' + 'https://github.com/angular/components/blob/3a204da37fd1366cae411b5c234517ecad199737/guides/v9-hammerjs-migration.md#the-migration-reported-ambiguous-usage-what-should-i-do'); } } else if (this._usedInRuntime || usedInTemplate) { // We keep track of whether Hammer is used globally. This is necessary because we // want to only remove Hammer from the "package.json" if it is not used in any project // target. Just because it isn't used in one target doesn't mean that we can safely // remove the dependency. HammerGesturesMigration.globalUsesHammer = true; // If hammer is only used at runtime, we don't need the gesture config or "HammerModule" // and can remove it (along with the hammer config token import if no longer needed). if (!usedInTemplate) { this._removeMaterialGestureConfigSetup(); this._removeHammerModuleReferences(); } else if (this._standardEventsUsedInTemplate && !this._customEventsUsedInTemplate) { this._setupHammerWithStandardEvents(); } else { this._setupHammerWithCustomEvents(); } } else { this._removeHammerSetup(); } // Record the changes collected in the import manager. Changes need to be applied // once the import manager registered all import modifications. This avoids collisions. this._importManager.recordChanges(); // Create migration failures that will be printed by the update-tool on migration // completion. We need special logic for updating failure positions to reflect // the new source file after modifications from the import manager. this.failures.push(...this._createMigrationFailures()); // The template check for HammerJS events is not completely reliable as the event // output could also be from a component having an output named similarly to a known // hammerjs event (e.g. "@Output() slide"). The usage is therefore somewhat ambiguous // and we want to print a message that developers might be able to remove Hammer manually. if (!hasCustomGestureConfigSetup && !this._usedInRuntime && usedInTemplate) { this.printInfo('The HammerJS v9 migration for Angular Components migrated the ' + 'project to keep HammerJS installed, but detected ambiguous usage of HammerJS. Please ' + 'manually check if you can remove HammerJS from your application. More details: ' + 'https://github.com/angular/components/blob/3a204da37fd1366cae411b5c234517ecad199737/guides/v9-hammerjs-migration.md#the-migration-reported-ambiguous-usage-what-should-i-do'); } } /** * Sets up the hammer gesture config in the current project. To achieve this, the * following steps are performed: * 1) Create copy of Angular Material gesture config. * 2) Rewrite all references to the Angular Material gesture config to the * new gesture config. * 3) Setup the HAMMER_GESTURE_CONFIG in the root app module (if not done already). * 4) Setup the "HammerModule" in the root app module (if not done already). */ _setupHammerWithCustomEvents() { const project = this.context.project; const sourceRoot = this.fileSystem.resolve(project.sourceRoot || project.root); const newConfigPath = (0, core_1.join)(sourceRoot, this._getAvailableGestureConfigFileName(sourceRoot)); // Copy gesture config template into the CLI project. this.fileSystem.create(newConfigPath, (0, fs_1.readFileSync)(require.resolve(GESTURE_CONFIG_TEMPLATE_PATH), 'utf8')); // Replace all Material gesture config references to resolve to the // newly copied gesture config. this._gestureConfigReferences.forEach(i => { const filePath = this.fileSystem.resolve(i.node.getSourceFile().fileName); return this._replaceGestureConfigReference(i, GESTURE_CONFIG_CLASS_NAME, getModuleSpecifier(newConfigPath, filePath)); }); // Setup the gesture config provider and the "HammerModule" in the root module // if not done already. The "HammerModule" is needed in v9 since it enables the // Hammer event plugin that was previously enabled by default in v8. this._setupNewGestureConfigInRootModule(newConfigPath); this._setupHammerModuleInRootModule(); } /** * Sets up the standard hammer module in the project and removes all * references to the deprecated Angular Material gesture config. */ _setupHammerWithStandardEvents() { // Setup the HammerModule. The HammerModule enables support for // the standard HammerJS events. this._setupHammerModuleInRootModule(); this._removeMaterialGestureConfigSetup(); } /** * Removes Hammer from the current project. The following steps are performed: * 1) Delete all TypeScript imports to "hammerjs". * 2) Remove references to the Angular Material gesture config. * 3) Remove "hammerjs" from all index HTML files of the current project. */ _removeHammerSetup() { this._installImports.forEach(i => this._importManager.deleteImportByDeclaration(i)); this._removeMaterialGestureConfigSetup(); this._removeHammerModuleReferences(); this._removeHammerFromIndexFile(); } /** * Removes the gesture config setup by deleting all found references to the Angular * Material gesture config. Additionally, unused imports to the hammer gesture config * token from "@angular/platform-browser" will be removed as well. */ _removeMaterialGestureConfigSetup() { this._gestureConfigReferences.forEach(r => this._removeGestureConfigReference(r)); this._hammerConfigTokenReferences.forEach(r => { if (r.isImport) { this._removeHammerConfigTokenImportIfUnused(r); } }); } /** Removes all references to the "HammerModule" from "@angular/platform-browser". */ _removeHammerModuleReferences() { this._hammerModuleReferences.forEach(({ node, isImport, importData }) => { const sourceFile = node.getSourceFile(); const recorder = this.fileSystem.edit(this.fileSystem.resolve(sourceFile.fileName)); // Only remove the import for the HammerModule if the module has been accessed // through a non-namespaced identifier access. if (!isNamespacedIdentifierAccess(node)) { this._importManager.deleteNamedBindingImport(sourceFile, HAMMER_MODULE_NAME, importData.moduleName); } // For references from within an import, we do not need to do anything other than // removing the import. For other references, we remove the import and the actual // identifier in the module imports. if (isImport) { return; } // If the "HammerModule" is referenced within an array literal, we can // remove the element easily. Otherwise if it's outside of an array literal, // we need to replace the reference with an empty object literal w/ todo to // not break the application. if (ts.isArrayLiteralExpression(node.parent)) { // Removes the "HammerModule" from the parent array expression. Removes // the trailing comma token if present. (0, remove_array_element_1.removeElementFromArrayExpression)(node, recorder); } else { recorder.remove(node.getStart(), node.getWidth()); recorder.insertRight(node.getStart(), `/* TODO: remove */ {}`); this._nodeFailures.push({ node: node, message: 'Unable to delete reference to "HammerModule".', }); } }); } /** * Checks if the given node is a reference to the hammer gesture config * token from platform-browser. If so, keeps track of the reference. */ _checkForHammerGestureConfigToken(node) { if (ts.isIdentifier(node)) { const importData = (0, schematics_1.getImportOfIdentifier)(node, this.typeChecker); if (importData && importData.symbolName === HAMMER_CONFIG_TOKEN_NAME && importData.moduleName === HAMMER_CONFIG_TOKEN_MODULE) { this._hammerConfigTokenReferences.push({ node, importData, isImport: ts.isImportSpecifier(node.parent), }); } } } /** * Checks if the given node is a reference to the HammerModule from * "@angular/platform-browser". If so, keeps track of the reference. */ _checkForHammerModuleReference(node) { if (ts.isIdentifier(node)) { const importData = (0, schematics_1.getImportOfIdentifier)(node, this.typeChecker); if (importData && importData.symbolName === HAMMER_MODULE_NAME && importData.moduleName === HAMMER_MODULE_IMPORT) { this._hammerModuleReferences.push({ node, importData, isImport: ts.isImportSpecifier(node.parent), }); } } } /** * Checks if the given node is an import to the HammerJS package. Imports to * HammerJS which load specific symbols from the package are considered as * runtime usage of Hammer. e.g. `import {Symbol} from "hammerjs";`. */ _checkHammerImports(node) { if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === HAMMER_MODULE_SPECIFIER) { // If there is an import to HammerJS that imports symbols, or is namespaced // (e.g. "import {A, B} from ..." or "import * as hammer from ..."), then we // assume that some exports are used at runtime. if (node.importClause && !(node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.length === 0)) { this._usedInRuntime = true; } else { this._installImports.push(node); } } } /** * Checks if the given node accesses the global "Hammer" symbol at runtime. If so, * the migration rule state will be updated to reflect that Hammer is used at runtime. */ _checkForRuntimeHammerUsage(node) { if (this._usedInRuntime) { return; } // Detects usages of "window.Hammer". if (ts.isPropertyAccessExpression(node) && node.name.text === 'Hammer') { const originExpr = unwrapExpression(node.expression); if (ts.isIdentifier(originExpr) && originExpr.text === 'window') { this._usedInRuntime = true; } return; } // Detects usages of "window['Hammer']". if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === 'Hammer') { const originExpr = unwrapExpression(node.expression); if (ts.isIdentifier(originExpr) && originExpr.text === 'window') { this._usedInRuntime = true; } return; } // Handles usages of plain identifier with the name "Hammer". These usage // are valid if they resolve to "@types/hammerjs". e.g. "new Hammer(myElement)". if (ts.isIdentifier(node) && node.text === 'Hammer' && !ts.isPropertyAccessExpression(node.parent) && !ts.isElementAccessExpression(node.parent)) { const symbol = this._getDeclarationSymbolOfNode(node); if (symbol && symbol.valueDeclaration && symbol.valueDeclaration.getSourceFile().fileName.includes('@types/hammerjs')) { this._usedInRuntime = true; } } } /** * Checks if the given node references the gesture config from Angular Material. * If so, we keep track of the found symbol reference. */ _checkForMaterialGestureConfig(node) { if (ts.isIdentifier(node)) { const importData = (0, schematics_1.getImportOfIdentifier)(node, this.typeChecker); if (importData && importData.symbolName === GESTURE_CONFIG_CLASS_NAME && importData.moduleName.startsWith('@angular/material/')) { this._gestureConfigReferences.push({ node, importData, isImport: ts.isImportSpecifier(node.parent), }); } } } /** * Checks if the given Hammer gesture config token reference is part of an * Angular provider definition that sets up a custom gesture config. */ _checkForCustomGestureConfigSetup(tokenRef) { // Walk up the tree to look for a parent property assignment of the // reference to the hammer gesture config token. let propertyAssignment = tokenRef.node; while (propertyAssignment && !ts.isPropertyAssignment(propertyAssignment)) { propertyAssignment = propertyAssignment.parent; } if (!propertyAssignment || !ts.isPropertyAssignment(propertyAssignment) || getPropertyNameText(propertyAssignment.name) !== 'provide') { return false; } const objectLiteralExpr = propertyAssignment.parent; const matchingIdentifiers = findMatchingChildNodes(objectLiteralExpr, ts.isIdentifier); // We naively assume that if there is a reference to the "GestureConfig" export // from Angular Material in the provider literal, that the provider sets up the // Angular Material gesture config. return !this._gestureConfigReferences.some(r => matchingIdentifiers.includes(r.node)); } /** * Determines an available file name for the gesture config which should * be stored in the specified file path. */ _getAvailableGestureConfigFileName(sourceRoot) { if (!this.fileSystem.fileExists((0, core_1.join)(sourceRoot, `${GESTURE_CONFIG_FILE_NAME}.ts`))) { return `${GESTURE_CONFIG_FILE_NAME}.ts`; } let possibleName = `${GESTURE_CONFIG_FILE_NAME}-`; let index = 1; while (this.fileSystem.fileExists((0, core_1.join)(sourceRoot, `${possibleName}-${index}.ts`))) { index++; } return `${possibleName + index}.ts`; } /** Replaces a given gesture config reference with a new import. */ _replaceGestureConfigReference({ node, importData, isImport }, symbolName, moduleSpecifier) { const sourceFile = node.getSourceFile(); const recorder = this.fileSystem.edit(this.fileSystem.resolve(sourceFile.fileName)); // List of all identifiers referring to the gesture config in the current file. This // allows us to add an import for the copied gesture configuration without generating a // new identifier for the import to avoid collisions. i.e. "GestureConfig_1". The import // manager checks for possible name collisions, but is able to ignore specific identifiers. // We use this to ignore all references to the original Angular Material gesture config, // because these will be replaced and therefore will not interfere. const gestureIdentifiersInFile = this._getGestureConfigIdentifiersOfFile(sourceFile); // If the parent of the identifier is accessed through a namespace, we can just // import the new gesture config without rewriting the import declaration because // the config has been imported through a namespaced import. if (isNamespacedIdentifierAccess(node)) { const newExpression = this._importManager.addImportToSourceFile(sourceFile, symbolName, moduleSpecifier, false, gestureIdentifiersInFile); recorder.remove(node.parent.getStart(), node.parent.getWidth()); recorder.insertRight(node.parent.getStart(), this._printNode(newExpression, sourceFile)); return; } // Delete the old import to the "GestureConfig". this._importManager.deleteNamedBindingImport(sourceFile, GESTURE_CONFIG_CLASS_NAME, importData.moduleName); // If the current reference is not from inside of a import, we need to add a new // import to the copied gesture config and replace the identifier. For references // within an import, we do nothing but removing the actual import. This allows us // to remove unused imports to the Material gesture config. if (!isImport) { const newExpression = this._importManager.addImportToSourceFile(sourceFile, symbolName, moduleSpecifier, false, gestureIdentifiersInFile); recorder.remove(node.getStart(), node.getWidth()); recorder.insertRight(node.getStart(), this._printNode(newExpression, sourceFile)); } } /** * Removes a given gesture config reference and its corresponding import from * its containing source file. Imports will be always removed, but in some cases, * where it's not guaranteed that a removal can be performed safely, we just * create a migration failure (and add a TODO if possible). */ _removeGestureConfigReference({ node, importData, isImport }) { const sourceFile = node.getSourceFile(); const recorder = this.fileSystem.edit(this.fileSystem.resolve(sourceFile.fileName)); // Only remove the import for the gesture config if the gesture config has // been accessed through a non-namespaced identifier access. if (!isNamespacedIdentifierAccess(node)) { this._importManager.deleteNamedBindingImport(sourceFile, GESTURE_CONFIG_CLASS_NAME, importData.moduleName); } // For references from within an import, we do not need to do anything other than // removing the import. For other references, we remove the import and the reference // identifier if used inside of a provider definition. if (isImport) { return; } const providerAssignment = node.parent; // Only remove references to the gesture config which are part of a statically // analyzable provider definition. We only support the common case of a gesture // config provider definition where the config is set up through "useClass". // Otherwise, it's not guaranteed that we can safely remove the provider definition. if (!ts.isPropertyAssignment(providerAssignment) || getPropertyNameText(providerAssignment.name) !== 'useClass') { this._nodeFailures.push({ node, message: CANNOT_REMOVE_REFERENCE_ERROR }); return; } const objectLiteralExpr = providerAssignment.parent; const provideToken = objectLiteralExpr.properties.find((p) => ts.isPropertyAssignment(p) && getPropertyNameText(p.name) === 'provide'); // Do not remove the reference if the gesture config is not part of a provider definition, // or if the provided toke is not referring to the known HAMMER_GESTURE_CONFIG token // from platform-browser. if (!provideToken || !this._isReferenceToHammerConfigToken(provideToken.initializer)) { this._nodeFailures.push({ node, message: CANNOT_REMOVE_REFERENCE_ERROR }); return; } // Collect all nested identifiers which will be deleted. This helps us // determining if we can remove imports for the "HAMMER_GESTURE_CONFIG" token. this._deletedIdentifiers.push(...findMatchingChildNodes(objectLiteralExpr, ts.isIdentifier)); // In case the found provider definition is not part of an array literal, // we cannot safely remove the provider. This is because it could be declared // as a variable. e.g. "const gestureProvider = {provide: .., useClass: GestureConfig}". // In that case, we just add an empty object literal with TODO and print a failure. if (!ts.isArrayLiteralExpression(objectLiteralExpr.parent)) { recorder.remove(objectLiteralExpr.getStart(), objectLiteralExpr.getWidth()); recorder.insertRight(objectLiteralExpr.getStart(), `/* TODO: remove */ {}`); this._nodeFailures.push({ node: objectLiteralExpr, message: `Unable to delete provider definition for "GestureConfig" completely. ` + `Please clean up the provider.`, }); return; } // Removes the object literal from the parent array expression. Removes // the trailing comma token if present. (0, remove_array_element_1.removeElementFromArrayExpression)(objectLiteralExpr, recorder); } /** Removes the given hammer config token import if it is not used. */ _removeHammerConfigTokenImportIfUnused({ node, importData }) { const sourceFile = node.getSourceFile(); const isTokenUsed = this._hammerConfigTokenReferences.some(r => !r.isImport && !isNamespacedIdentifierAccess(r.node) && r.node.getSourceFile() === sourceFile && !this._deletedIdentifiers.includes(r.node)); // We don't want to remove the import for the token if the token is // still used somewhere. if (!isTokenUsed) { this._importManager.deleteNamedBindingImport(sourceFile, HAMMER_CONFIG_TOKEN_NAME, importData.moduleName); } } /** Removes Hammer from all index HTML files of the current project. */ _removeHammerFromIndexFile() { const indexFilePaths = (0, schematics_1.getProjectIndexFiles)(this.context.project); indexFilePaths.forEach(filePath => { if (!this.fileSystem.fileExists(filePath)) { return; } const htmlContent = this.fileSystem.read(filePath); const recorder = this.fileSystem.edit(filePath); (0, find_hammer_script_tags_1.findHammerScriptImportElements)(htmlContent).forEach(el => (0, remove_element_from_html_1.removeElementFromHtml)(el, recorder)); }); } /** Sets up the Hammer gesture config in the root module if needed. */ _setupNewGestureConfigInRootModule(gestureConfigPath) { const { project } = this.context; const mainFilePath = (0, schematics_1.getProjectMainFile)(project); const rootModuleSymbol = this._getRootModuleSymbol(mainFilePath); if (rootModuleSymbol === null || rootModuleSymbol.valueDeclaration === undefined) { this.failures.push({ filePath: mainFilePath, message: `Could not setup Hammer gestures in module. Please ` + `manually ensure that the Hammer gesture config is set up.`, }); return; } const sourceFile = rootModuleSymbol.valueDeclaration.getSourceFile(); const metadata = (0, schematics_1.getDecoratorMetadata)(sourceFile, 'NgModule', '@angular/core'); // If no "NgModule" definition is found inside the source file, we just do nothing. if (!metadata.length) { return; } const filePath = this.fileSystem.resolve(sourceFile.fileName); const recorder = this.fileSystem.edit(filePath); const providersField = (0, schematics_1.getMetadataField)(metadata[0], 'providers')[0]; const providerIdentifiers = providersField ? findMatchingChildNodes(providersField, ts.isIdentifier) : null; const gestureConfigExpr = this._importManager.addImportToSourceFile(sourceFile, GESTURE_CONFIG_CLASS_NAME, getModuleSpecifier(gestureConfigPath, filePath), false, this._getGestureConfigIdentifiersOfFile(sourceFile)); const hammerConfigTokenExpr = this._importManager.addImportToSourceFile(sourceFile, HAMMER_CONFIG_TOKEN_NAME, HAMMER_CONFIG_TOKEN_MODULE); const newProviderNode = ts.factory.createObjectLiteralExpression([ ts.factory.createPropertyAssignment('provide', hammerConfigTokenExpr), ts.factory.createPropertyAssignment('useClass', gestureConfigExpr), ]); // If the providers field exists and already contains references to the hammer gesture // config token and the gesture config, we naively assume that the gesture config is // already set up. We only want to add the gesture config provider if it is not set up. if (!providerIdentifiers || !(this._hammerConfigTokenReferences.some(r => providerIdentifiers.includes(r.node)) && this._gestureConfigReferences.some(r => providerIdentifiers.includes(r.node)))) { const symbolName = this._printNode(newProviderNode, sourceFile); (0, schematics_1.addSymbolToNgModuleMetadata)(sourceFile, sourceFile.fileName, 'providers', symbolName, null).forEach(change => { if (change instanceof change_1.InsertChange) { recorder.insertRight(change.pos, change.toAdd); } }); } } /** * Gets the TypeScript symbol of the root module by looking for the module * bootstrap expression in the specified source file. */ _getRootModuleSymbol(mainFilePath) { const mainFile = this.program.getSourceFile(mainFilePath); if (!mainFile) { return null; } const appModuleExpr = (0, find_main_module_1.findMainModuleExpression)(mainFile); if (!appModuleExpr) { return null; } const appModuleSymbol = this._getDeclarationSymbolOfNode(unwrapExpression(appModuleExpr)); if (!appModuleSymbol || !appModuleSymbol.valueDeclaration) { return null; } return appModuleSymbol; } /** Sets up the "HammerModule" in the root module of the current project. */ _setupHammerModuleInRootModule() { const { project } = this.context; const mainFilePath = (0, schematics_1.getProjectMainFile)(project); const rootModuleSymbol = this._getRootModuleSymbol(mainFilePath); if (rootModuleSymbol === null || rootModuleSymbol.valueDeclaration === undefined) { this.failures.push({ filePath: mainFilePath, message: `Could not setup HammerModule. Please manually set up the "HammerModule" ` + `from "@angular/platform-browser".`, }); return; } const sourceFile = rootModuleSymbol.valueDeclaration.getSourceFile(); const metadata = (0, schematics_1.getDecoratorMetadata)(sourceFile, 'NgModule', '@angular/core'); if (!metadata.length) { return; } const importsField = (0, schematics_1.getMetadataField)(metadata[0], 'imports')[0]; const importIdentifiers = importsField ? findMatchingChildNodes(importsField, ts.isIdentifier) : null; const recorder = this.fileSystem.edit(this.fileSystem.resolve(sourceFile.fileName)); const hammerModuleExpr = this._importManager.addImportToSourceFile(sourceFile, HAMMER_MODULE_NAME, HAMMER_MODULE_IMPORT); // If the "HammerModule" is not already imported in the app module, we set it up // by adding it to the "imports" field of the app module. if (!importIdentifiers || !this._hammerModuleReferences.some(r => importIdentifiers.includes(r.node))) { const symbolName = this._printNode(hammerModuleExpr, sourceFile); (0, schematics_1.addSymbolToNgModuleMetadata)(sourceFile, sourceFile.fileName, 'imports', symbolName, null).forEach(change => { if (change instanceof change_1.InsertChange) { recorder.insertRight(change.pos, change.toAdd); } }); } } /** Prints a given node within the specified source file. */ _printNode(node, sourceFile) { return this._printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); } /** Gets all referenced gesture config identifiers of a given source file */ _getGestureConfigIdentifiersOfFile(sourceFile) { return this._gestureConfigReferences .filter(d => d.node.getSourceFile() === sourceFile) .map(d => d.node); } /** Gets the symbol that contains the value declaration of the specified node. */ _getDeclarationSymbolOfNode(node) { const symbol = this.typeChecker.getSymbolAtLocation(node); // Symbols can be aliases of the declaration symbol. e.g. in named import specifiers. // We need to resolve the aliased symbol back to the declaration symbol. // tslint:disable-next-line:no-bitwise if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) { return this.typeChecker.getAliasedSymbol(symbol); } return symbol; } /** * Checks whether the given expression resolves to a hammer gesture config * token reference from "@angular/platform-browser". */ _isReferenceToHammerConfigToken(expr) { const unwrapped = unwrapExpression(expr); if (ts.isIdentifier(unwrapped)) { return this._hammerConfigTokenReferences.some(r => r.node === unwrapped); } else if (ts.isPropertyAccessExpression(unwrapped)) { return this._hammerConfigTokenReferences.some(r => r.node === unwrapped.name); } return false; } /** * Creates migration failures of the collected node failures. The returned migration * failures are updated to reflect the post-migration state of source files. Meaning * that failure positions are corrected if source file modifications shifted lines. */ _createMigrationFailures() { return this._nodeFailures.map(({ node, message }) => { const sourceFile = node.getSourceFile(); const offset = node.getStart(); const position = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()); return { position: this._importManager.correctNodePosition(node, offset, position), message: message, filePath: this.fileSystem.resolve(sourceFile.fileName), }; }); } /** * Static migration rule method that will be called once all project targets * have been migrated individually. This method can be used to make changes based * on the analysis of the individual targets. For example: we only remove Hammer * from the "package.json" if it is not used in *any* project target. */ static globalPostMigration(tree, target, context) { // Skip printing any global messages when the target version is not allowed. if (!this._isAllowedVersion(target)) { return; } // Always notify the developer that the Hammer v9 migration does not migrate tests. context.logger.info('\n⚠ General notice: The HammerJS v9 migration for Angular Components is not able to ' + 'migrate tests. Please manually clean up tests in your project if they rely on ' + (this.globalUsesHammer ? 'the deprecated Angular Material gesture config.' : 'HammerJS.')); context.logger.info('Read more about migrating tests: https://github.com/angular/components/blob/3a204da37fd1366cae411b5c234517ecad199737/guides/v9-hammerjs-migration.md#how-to-migrate-my-tests'); if (!this.globalUsesHammer && this._removeHammerFromPackageJson(tree)) { // Since Hammer has been removed from the workspace "package.json" file, // we schedule a node package install task to refresh the lock file. return { runPackageManager: true }; } // Clean global state once the workspace has been migrated. This is technically // not necessary in "ng update", but in tests we re-use the same rule class. this.globalUsesHammer = false; } /** * Removes the hammer package from the workspace "package.json". * @returns Whether Hammer was set up and has been removed from the "package.json" */ static _removeHammerFromPackageJson(tree) { if (!tree.exists('/package.json')) { return false; } const packageJson = JSON.parse(tree.read('/package.json').toString('utf8')); // We do not handle the case where someone manually added "hammerjs" to the dev dependencies. if (packageJson.dependencies && packageJson.dependencies[HAMMER_MODULE_SPECIFIER]) { delete packageJson.dependencies[HAMMER_MODULE_SPECIFIER]; tree.overwrite('/package.json', JSON.stringify(packageJson, null, 2)); return true; } return false; } /** Gets whether the migration is allowed to run for specified target version. */ static _isAllowedVersion(target) { // This migration is only allowed to run for v9 or v10 target versions. return target === schematics_1.TargetVersion.V9 || target === schematics_1.TargetVersion.V10; } } exports.HammerGesturesMigration = HammerGesturesMigration; /** Global state of whether Hammer is used in any analyzed project target. */ HammerGesturesMigration.globalUsesHammer = false; /** * Recursively unwraps a given expression if it is wrapped * by parenthesis, type casts or type assertions. */ function unwrapExpression(node) { if (ts.isParenthesizedExpression(node)) { return unwrapExpression(node.expression); } else if (ts.isAsExpression(node)) { return unwrapExpression(node.expression); } else if (ts.isTypeAssertion(node)) { return unwrapExpression(node.expression); } return node; } /** * Converts the specified path to a valid TypeScript module specifier which is * relative to the given containing file. */ function getModuleSpecifier(newPath, containingFile) { let result = (0, core_1.relative)((0, core_1.dirname)(containingFile), newPath).replace(/\\/g, '/').replace(/\.ts$/, ''); if (!result.startsWith('.')) { result = `./${result}`; } return result; } /** * Gets the text of the given property name. * @returns Text of the given property name. Null if not statically analyzable. */ function getPropertyNameText(node) { if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) { return node.text; } return null; } /** Checks whether the given identifier is part of a namespaced access. */ function isNamespacedIdentifierAccess(node) { return ts.isQualifiedName(node.parent) || ts.isPropertyAccessExpression(node.parent); } /** * Walks through the specified node and returns all child nodes which match the * given predicate. */ function findMatchingChildNodes(parent, predicate) { const result = []; const visitNode = (node) => { if (predicate(node)) { result.push(node); } ts.forEachChild(node, visitNode); }; ts.forEachChild(parent, visitNode); return result; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGFtbWVyLWdlc3R1cmVzLW1pZ3JhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3NyYy9tYXRlcmlhbC9zY2hlbWF0aWNzL25nLXVwZGF0ZS9taWdyYXRpb25zL2hhbW1lci1nZXN0dXJlcy12OS9oYW1tZXItZ2VzdHVyZXMtbWlncmF0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7Ozs7O0dBTUc7OztBQUVILCtDQUFtRTtBQUVuRSx3REFhaUM7QUFDakMsK0RBQWdFO0FBQ2hFLDJCQUFnQztBQUNoQyxpQ0FBaUM7QUFFakMsdUVBQXlFO0FBQ3pFLHlEQUE0RDtBQUM1RCxtRUFBaUU7QUFDakUscURBQStDO0FBQy9DLGlFQUF3RTtBQUN4RSx5RUFBaUU7QUFFakUsTUFBTSx5QkFBeUIsR0FBRyxlQUFlLENBQUM7QUFDbEQsTUFBTSx3QkFBd0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUNsRCxNQUFNLDRCQUE0QixHQUFHLDJCQUEyQixDQUFDO0FBRWpFLE1BQU0sd0JBQXdCLEdBQUcsdUJBQXVCLENBQUM7QUFDekQsTUFBTSwwQkFBMEIsR0FBRywyQkFBMkIsQ0FBQztBQUUvRCxNQUFNLGtCQUFrQixHQUFHLGNBQWMsQ0FBQztBQUMxQyxNQUFNLG9CQUFvQixHQUFHLDJCQUEyQixDQUFDO0FBRXpELE1BQU0sdUJBQXVCLEdBQUcsVUFBVSxDQUFDO0FBRTNDLE1BQU0sNkJBQTZCLEdBQUcscUVBQXFFLENBQUM7QUFZNUcsTUFBYSx1QkFBd0IsU0FBUSw0QkFBcUI7SUFBbEU7O1FBQ0Usb0ZBQW9GO1FBQ3BGLHdGQUF3RjtRQUN4RixzRkFBc0Y7UUFDdEYsZ0NBQWdDO1FBQ2hDLFlBQU8sR0FDTCx1QkFBdUIsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUV0RixhQUFRLEdBQUcsRUFBRSxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBQzlCLG1CQUFjLEdBQUcsSUFBSSw4QkFBYSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ25FLGtCQUFhLEdBQXVDLEVBQUUsQ0FBQztRQUUvRDs7O1dBR0c7UUFDSyxnQ0FBMkIsR0FBRyxLQUFLLENBQUM7UUFFNUMsK0RBQStEO1FBQ3ZELGtDQUE2QixHQUFHLEtBQUssQ0FBQztRQUU5QywrQ0FBK0M7UUFDdkMsbUJBQWMsR0FBRyxLQUFLLENBQUM7UUFFL0I7OztXQUdHO1FBQ0ssb0JBQWUsR0FBMkIsRUFBRSxDQUFDO1FBRXJEOztXQUVHO1FBQ0ssNkJBQXdCLEdBQTBCLEVBQUUsQ0FBQztRQUU3RDs7O1dBR0c7UUFDSyxpQ0FBNEIsR0FBMEIsRUFBRSxDQUFDO1FBRWpFOzs7V0FHRztRQUNLLDRCQUF1QixHQUEwQixFQUFFLENBQUM7UUFFNUQ7OztXQUdHO1FBQ0ssd0JBQW1CLEdBQW9CLEVBQUUsQ0FBQztJQXMzQnBELENBQUM7SUFwM0JVLGFBQWEsQ0FBQyxRQUEwQjtRQUMvQyxJQUFJLENBQUMsSUFBSSxDQUFDLDJCQUEyQixJQUFJLENBQUMsSUFBSSxDQUFDLDZCQUE2QixFQUFFO1lBQzVFLE1BQU0sRUFBQyxjQUFjLEVBQUUsWUFBWSxFQUFDLEdBQUcsSUFBQSxnREFBd0IsRUFBQyxRQUFRLENBQUMsT0FBTyxDQUFDLENBQUM7WUFDbEYsSUFBSSxDQUFDLDJCQUEyQixHQUFHLElBQUksQ0FBQywyQkFBMkIsSUFBSSxZQUFZLENBQUM7WUFDcEYsSUFBSSxDQUFDLDZCQUE2QixHQUFHLElBQUksQ0FBQyw2QkFBNkIsSUFBSSxjQUFjLENBQUM7U0FDM0Y7SUFDSCxDQUFDO0lBRVEsU0FBUyxDQUFDLElBQWE7UUFDOUIsSUFBSSxDQUFDLG1CQUFtQixDQUFDLElBQUksQ0FBQyxDQUFDO1FBQy9CLElBQUksQ0FBQywyQkFBMkIsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUN2QyxJQUFJLENBQUMsOEJBQThCLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDMUMsSUFBSSxDQUFDLGlDQUFpQyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQzdDLElBQUksQ0FBQyw4QkFBOEIsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUM1QyxDQUFDO0lBRVEsWUFBWTtRQUNuQixxRUFBcUU7UUFDckUsOENBQThDO1FBQzlDLE1BQU0sMkJBQTJCLEdBQUcsSUFBSSxDQUFDLDRCQUE0QixDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUM3RSxJQUFJLENBQUMsaUNBQWlDLENBQUMsQ0FBQyxDQUFDLENBQzFDLENBQUM7UUFDRixNQUFNLGNBQWMsR0FBRyxJQUFJLENBQUMsNkJBQTZCLElBQUksSUFBSSxDQUFDLDJCQUEyQixDQUFDO1FBRTlGOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztVQXVCRTtRQUVGLElBQUksMkJBQTJCLEVBQUU7WUFDL0Isa0ZBQWtGO1lBQ2xGLHVCQUF1QixDQUFDLGdCQUFnQixHQUFHLElBQUksQ0FBQztZQUNoRCxJQUFJLENBQUMsY0FBYyxJQUFJLElBQUksQ0FBQyx3QkFBd0IsQ0FBQyxNQUFNLEVBQUU7Z0JBQzNELDRFQUE0RTtnQkFDNUUsaUZBQWlGO2dCQUNqRixvRkFBb0Y7Z0JBQ3BGLElBQUksQ0FBQyxpQ0FBaUMsRUFBRSxDQUFDO2dCQUN6QyxJQUFJLENBQUMsU0FBUyxDQUNaLDZFQUE2RTtvQkFDM0UsaUZBQWlGO29CQUNqRiwrRUFBK0U7b0JBQy9FLHlFQUF5RTtvQkFDekUsNktBQTZLLENBQ2hMLENBQUM7YUFDSDtpQkFBTSxJQUFJLGNBQWMsSUFBSSxJQUFJLENBQUMsd0JBQXdCLENBQUMsTUFBTSxFQUFFO2dCQUNqRSxxRkFBcUY7Z0JBQ3JGLG1GQUFtRjtnQkFDbkYsZ0ZBQWdGO2dCQUNoRiw2RUFBNkU7Z0JBQzdFLElBQUksQ0FBQyxTQUFTLENBQ1osNkVBQTZFO29CQUMzRSxpRkFBaUY7b0JBQ2pGLDRFQUE0RTtvQkFDNUUsZ0ZBQWdGO29CQUNoRiw2S0FBNkssQ0FDaEwsQ0FBQzthQUNIO1NBQ0Y7YUFBTSxJQUFJLElBQUksQ0FBQyxjQUFjLElBQUksY0FBYyxFQUFFO1lBQ2hELGlGQUFpRjtZQUNqRixzRkFBc0Y7WUFDdEYsbUZBQW1GO1lBQ25GLHlCQUF5QjtZQUN6Qix1QkFBdUIsQ0FBQyxnQkFBZ0IsR0FBRyxJQUFJLENBQUM7WUFFaEQsd0ZBQXdGO1lBQ3hGLHFGQUFxRjtZQUNyRixJQUFJLENBQUMsY0FBYyxFQUFFO2dCQUNuQixJQUFJLENBQUMsaUNBQWlDLEVBQUUsQ0FBQztnQkFDekMsSUFBSSxDQUFDLDZCQUE2QixFQUFFLENBQUM7YUFDdEM7aUJBQU0sSUFBSSxJQUFJLENBQUMsNkJBQTZCLElBQUksQ0FBQyxJQUFJLENBQUMsMkJBQTJCLEVBQUU7Z0JBQ2xGLElBQUksQ0FBQyw4QkFBOEIsRUFBRSxDQUFDO2FBQ3ZDO2lCQUFNO2dCQUNMLElBQUksQ0FBQyw0QkFBNEIsRUFBRSxDQUFDO2FBQ3JDO1NBQ0Y7YUFBTTtZQUNMLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxDQUFDO1NBQzNCO1FBRUQsaUZBQWlGO1FBQ2pGLHVGQUF1RjtRQUN2RixJQUFJLENBQUMsY0FBYyxDQUFDLGFBQWEsRUFBRSxDQUFDO1FBRXBDLGlGQUFpRjtRQUNqRiw4RUFBOEU7UUFDOUUsbUVBQW1FO1FBQ25FLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLEdBQUcsSUFBSSxDQUFDLHdCQUF3QixFQUFFLENBQUMsQ0FBQztRQUV2RCxpRkFBaUY7UUFDakYsb0ZBQW9GO1FBQ3BGLHFGQUFxRjtRQUNyRiwwRkFBMEY7UUFDMUYsSUFBSSxDQUFDLDJCQUEyQixJQUFJLENBQUMsSUFBSSxDQUFDLGNBQWMsSUFBSSxjQUFjLEVBQUU7WUFDMUUsSUFBSSxDQUFDLFNBQVMsQ0FDWixnRUFBZ0U7Z0JBQzlELHVGQUF1RjtnQkFDdkYsaUZBQWlGO2dCQUNqRiw2S0FBNkssQ0FDaEwsQ0FBQztTQUNIO0lBQ0gsQ0FBQztJQUVEOzs7Ozs7OztPQVFHO0lBQ0ssNEJBQTRCO1FBQ2xDLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDO1FBQ3JDLE1BQU0sVUFBVSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxVQUFVLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQy9FLE1BQU0sYUFBYSxHQUFHLElBQUEsV0FBSSxFQUFDLFVBQVUsRUFBRSxJQUFJLENBQUMsa0NBQWtDLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQztRQUU1RixxREFBcUQ7UUFDckQsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQ3BCLGFBQWEsRUFDYixJQUFBLGlCQUFZLEVBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyw0QkFBNEIsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxDQUNwRSxDQUFDO1FBRUYsbUVBQW1FO1FBQ25FLCtCQUErQjtRQUMvQixJQUFJLENBQUMsd0JBQXdCLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFO1lBQ3hDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsYUFBYSxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDMUUsT0FBTyxJQUFJLENBQUMsOEJBQThCLENBQ3hDLENBQUMsRUFDRCx5QkFBeUIsRUFDekIsa0JBQWtCLENBQUMsYUFBYSxFQUFFLFFBQVEsQ0FBQyxDQUM1QyxDQUFDO1FBQ0osQ0FBQyxDQUFDLENBQUM7UUFFSCw4RUFBOEU7UUFDOUUsK0VBQStFO1FBQy9FLG9FQUFvRTtRQUNwRSxJQUFJLENBQUMsa0NBQWtDLENBQUMsYUFBYSxDQUFDLENBQUM7UUFDdkQsSUFBSSxDQUFDLDhCQUE4QixFQUFFLENBQUM7SUFDeEMsQ0FBQztJQUVEOzs7T