@angular/material
Version:
Angular Material
813 lines • 132 kB
JavaScript
"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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGFtbWVyLWdlc3R1cmVzLW1pZ3JhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3NyYy9tYXRlcmlhbC9zY2hlbWF0aWNzL25nLXVwZGF0ZS9taWdyYXRpb25zL2hhbW1lci1nZXN0dXJlcy12OS9oYW1tZXItZ2VzdHVyZXMtbWlncmF0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7Ozs7O0dBTUc7OztBQUVILCtDQUFtRTtBQUVuRSx3REFhaUM7QUFDakMsK0RBQWdFO0FBQ2hFLDJCQUFnQztBQUNoQyxpQ0FBaUM7QUFFakMsdUVBQXlFO0FBQ3pFLHlEQUE0RDtBQUM1RCxtRUFBaUU7QUFDakUscURBQStDO0FBQy9DLGlFQUF3RTtBQUN4RSx5RUFBaUU7QUFFakUsTUFBTSx5QkFBeUIsR0FBRyxlQUFlLENBQUM7QUFDbEQsTUFBTSx3QkFBd0IsR0FBRyxnQkFBZ0IsQ0FBQztBQUNsRCxNQUFNLDRCQUE0QixHQUFHLDJCQUEyQixDQUFDO0FBRWpFLE1BQU0sd0JBQXdCLEdBQUcsdUJBQXVCLENBQUM7QUFDekQsTUFBTSwwQkFBMEIsR0FBRywyQkFBMkIsQ0FBQztBQUUvRCxNQUFNLGtCQUFrQixHQUFHLGNBQWMsQ0FBQztBQUMxQyxNQUFNLG9CQUFvQixHQUFHLDJCQUEyQixDQUFDO0FBRXpELE1BQU0sdUJBQXVCLEdBQUcsVUFBVSxDQUFDO0FBRTNDLE1BQU0sNkJBQTZCLEdBQUcscUVBQXFFLENBQUM7QUFZNUcsTUFBYSx1QkFBd0IsU0FBUSw0QkFBcUI7SUFBbEU7O1FBQ0Usb0ZBQW9GO1FBQ3BGLHdGQUF3RjtRQUN4RixzRkFBc0Y7UUFDdEYsZ0NBQWdDO1FBQ2hDLFlBQU8sR0FDTCx1QkFBdUIsQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsYUFBYSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksQ0FBQztRQUV0RixhQUFRLEdBQUcsRUFB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