import type {
  ProjectConfiguration,
  TargetConfiguration,
  Tree,
} from '@nx/devkit';
import {
  readJson,
  readNxJson,
  readProjectConfiguration,
  writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import type { MigrationProjectConfiguration } from '../../utilities/types';
import { AppMigrator } from './app.migrator';

type AngularCliProjectConfiguration = Omit<ProjectConfiguration, 'targets'> & {
  architect?: {
    [targetName: string]: Omit<TargetConfiguration, 'executor'> & {
      builder: string;
    };
  };
};

const mockedLogger = { warn: jest.fn() };

describe('app migrator', () => {
  let tree: Tree;

  function addProject(
    name: string,
    config: AngularCliProjectConfiguration
  ): MigrationProjectConfiguration {
    config.projectType = 'application';
    const angularJson = readJson(tree, 'angular.json');
    angularJson.projects[name] = config;
    writeJson(tree, 'angular.json', angularJson);
    tree.write(`${config.root}/README.md`, '');
    tree.write(`${config.sourceRoot}/main.ts`, '');

    return { config: readProjectConfiguration(tree, name), name };
  }

  beforeEach(() => {
    tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });

    // when this migrator is invoked, some of the workspace migration has
    // already been run, so we make some adjustments to match that state
    writeJson(tree, 'angular.json', { version: 2, projects: {} });

    jest.clearAllMocks();
  });

  describe('validation', () => {
    it('should fail validation when the project root is not specified', async () => {
      const project = addProject('app1', {} as any);
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].message).toBe(
        'The project root is not defined in the project configuration. The project will be skipped.'
      );
      expect(result[0].hint).toBe(
        'Make sure to manually migrate its configuration and files or remove the project if it is not valid. Alternatively, you could revert the migration, ensure the value for "projects.app1.root" is set and run the migration again.'
      );
    });

    it('should fail validation when the project root is not valid', async () => {
      const project = addProject('app1', { root: '' });
      const migrator = new AppMigrator(
        tree,
        {},
        {
          ...project,
          config: { ...project.config, root: 'wrong-root' },
        }
      );

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].message).toBe(
        'The project root "wrong-root" could not be found. The project will be skipped.'
      );
      expect(result[0].hint).toBe(
        'Make sure to manually migrate its configuration and files or remove the project if it is not valid. Alternatively, you could revert the migration, ensure the value for "projects.app1.root" is correct and run the migration again.'
      );
    });

    it('should fail validation when the project source root is specified and it is not valid', async () => {
      const project = addProject('app1', { root: '', sourceRoot: 'src' });
      const migrator = new AppMigrator(
        tree,
        {},
        {
          ...project,
          config: { ...project.config, sourceRoot: 'wrong-src' },
        }
      );

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].message).toBe(
        'The project source root "wrong-src" could not be found. The project will be skipped.'
      );
      expect(result[0].hint).toBe(
        'Make sure to manually migrate its configuration and files or remove the project if it is not valid. Alternatively, you could revert the migration, ensure the value for "projects.app1.sourceRoot" is correct and run the migration again.'
      );
    });

    it('should fail validation when the project is using unsupported builders', async () => {
      const project = addProject('app1', {
        root: '',
        architect: { build: { builder: '@not/supported:builder' } },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].messageGroup.title).toBe('Unsupported builders');
      expect(result[0].messageGroup.messages).toStrictEqual([
        `The "build" target is using a builder "@not/supported:builder" that's not currently supported by the automated migration. The target will be skipped.`,
      ]);
      expect(result[0].hint).toMatchInlineSnapshot(
        `"Make sure to manually migrate the target configuration and any possible associated files. Alternatively, you could revert the migration, change the builder to one of the builders supported by the automated migration ("@angular-devkit/build-angular:browser", "@angular-devkit/build-angular:protractor", "@cypress/schematic:cypress", "@angular-devkit/build-angular:extract-i18n", "@nguniversal/builders:prerender", "@angular-devkit/build-angular:dev-server", "@angular-devkit/build-angular:server", "@nguniversal/builders:ssr-dev-server", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint"), and run the migration again."`
      );
    });

    it('should fail validation with an error message per target using an unsupported builder', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build: { builder: '@not/supported:builder' },
          test: { builder: '@other/not-supported:builder' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].messageGroup.title).toBe('Unsupported builders');
      expect(result[0].messageGroup.messages).toStrictEqual([
        `The "build" target is using a builder "@not/supported:builder" that's not currently supported by the automated migration. The target will be skipped.`,
        `The "test" target is using a builder "@other/not-supported:builder" that's not currently supported by the automated migration. The target will be skipped.`,
      ]);
      expect(result[0].hint).toMatchInlineSnapshot(
        `"Make sure to manually migrate the target configuration and any possible associated files. Alternatively, you could revert the migration, change the builder to one of the builders supported by the automated migration ("@angular-devkit/build-angular:browser", "@angular-devkit/build-angular:protractor", "@cypress/schematic:cypress", "@angular-devkit/build-angular:extract-i18n", "@nguniversal/builders:prerender", "@angular-devkit/build-angular:dev-server", "@angular-devkit/build-angular:server", "@nguniversal/builders:ssr-dev-server", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint"), and run the migration again."`
      );
    });

    it('should still fail validation when the target name is not a common one', async () => {
      const project = addProject('app1', {
        root: '',
        architect: { 'my-build': { builder: '@not/supported:builder' } },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].messageGroup.title).toBe('Unsupported builders');
      expect(result[0].messageGroup.messages).toStrictEqual([
        `The "my-build" target is using a builder "@not/supported:builder" that's not currently supported by the automated migration. The target will be skipped.`,
      ]);
      expect(result[0].hint).toMatchInlineSnapshot(
        `"Make sure to manually migrate the target configuration and any possible associated files. Alternatively, you could revert the migration, change the builder to one of the builders supported by the automated migration ("@angular-devkit/build-angular:browser", "@angular-devkit/build-angular:protractor", "@cypress/schematic:cypress", "@angular-devkit/build-angular:extract-i18n", "@nguniversal/builders:prerender", "@angular-devkit/build-angular:dev-server", "@angular-devkit/build-angular:server", "@nguniversal/builders:ssr-dev-server", "@angular-devkit/build-angular:karma" and "@angular-eslint/builder:lint"), and run the migration again."`
      );
    });

    it('should fail validation when there are multiple targets for the same builder', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build1: { builder: '@angular-devkit/build-angular:browser' },
          build2: { builder: '@angular-devkit/build-angular:browser' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(1);
      expect(result[0].message).toBe(
        'There is more than one target using a builder that is used to build the project ("build1" and "build2"). This is not currently supported by the automated migration. These targets will be skipped.'
      );
      expect(result[0].hint).toBe(
        'Make sure to manually migrate their configuration and any possible associated files.'
      );
    });

    it('should fail validation with multiple errors when there are multiple targets for the same builders', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build1: { builder: '@angular-devkit/build-angular:browser' },
          build2: { builder: '@angular-devkit/build-angular:browser' },
          lint1: { builder: '@angular-eslint/builder:lint' },
          lint2: { builder: '@angular-eslint/builder:lint' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toHaveLength(2);
      expect(result[0].message).toBe(
        'There is more than one target using a builder that is used to build the project ("build1" and "build2"). This is not currently supported by the automated migration. These targets will be skipped.'
      );
      expect(result[0].hint).toBe(
        'Make sure to manually migrate their configuration and any possible associated files.'
      );
      expect(result[1].message).toBe(
        'There is more than one target using the builder "@angular-eslint/builder:lint": "lint1" and "lint2". This is not currently supported by the automated migration. These targets will be skipped.'
      );
      expect(result[1].hint).toBe(
        'Make sure to manually migrate their configuration and any possible associated files.'
      );
    });

    it('should succeed validation', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: { builder: '@angular-devkit/build-angular:browser' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      const result = migrator.validate();

      expect(result).toBeNull();
    });
  });

  describe('warnings', () => {
    it('should warn when "architect" is not set', async () => {
      const project = addProject('app1', { root: '' });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The project does not have any targets configured. Skipping updating targets.'
      );
    });

    it('should warn when there are no targets', async () => {
      const project = addProject('app1', { root: '', architect: {} });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The project does not have any targets configured. Skipping updating targets.'
      );
    });

    it('should warn when there is no build target', async () => {
      const project = addProject('app1', {
        root: '',
        architect: { test: { builder: '@angular-devkit/build-angular:karma' } },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'There is no build target in the project configuration. Skipping updating the build target configuration.'
      );
    });

    it('should warn when the build target does not have any options and configurations', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build: { builder: '@angular-devkit/build-angular:browser' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The target "build" is not specifying any options or configurations. Skipping updating the target configuration.'
      );
    });

    it('should warn when there is no tsConfig specified in the build target options or development configuration', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: {},
            configurations: { development: {} },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The "build" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.'
      );
    });

    it('should warn when the specified tsConfig in the build target does not exist', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: { tsConfig: 'tsconfig.app.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The tsConfig file "tsconfig.app.json" specified in the "build" target could not be found. Skipping updating the tsConfig file.'
      );
    });

    it('should warn when the lint target does not have any options', async () => {
      const project = addProject('app1', {
        root: '',
        architect: { lint: { builder: '@angular-eslint/builder:lint' } },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The target "lint" is not specifying any options. Skipping updating the target configuration.',
      ]);
    });

    it('should warn when the specified eslintConfig in the lint target does not exist', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: { eslintConfig: '.non-existent-eslintrc.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The ESLint config file ".non-existent-eslintrc.json" could not be found. Skipping updating the file.',
      ]);
    });

    it('should warn when eslintConfig is not specified and the ".eslintrc.json" file at the project root does not exist', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          lint: { builder: '@angular-eslint/builder:lint', options: {} },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The ESLint config file ".eslintrc.json" could not be found. Skipping updating the file.',
      ]);
    });

    it('should warn when a specified lint file pattern is not contained within the project', async () => {
      const project = addProject('app1', {
        root: 'projects/app1',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: { lintFilePatterns: ['not-within-project/**/*.ts'] },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The lint file pattern "not-within-project/**/*.ts" specified in the "lint" target is not contained in the project root or source root. The pattern will not be updated.',
      ]);
    });

    it('should warn when the server target does not have any options and configurations', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          server: { builder: '@angular-devkit/build-angular:server' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The target "server" is not specifying any options or configurations. Skipping updating the target configuration.'
      );
    });

    it('should warn when there is no tsConfig specified in the server target options or development configuration', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          server: {
            builder: '@angular-devkit/build-angular:server',
            options: {},
            configurations: { development: {} },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The "server" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.'
      );
    });

    it('should warn when the specified tsConfig in the server target does not exist', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          server: {
            builder: '@angular-devkit/build-angular:server',
            options: { tsConfig: 'tsconfig.server.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The tsConfig file "tsconfig.server.json" specified in the "server" target could not be found. Skipping updating the tsConfig file.'
      );
    });

    it('should warn when the test target does not have any options', async () => {
      const project = addProject('app1', {
        root: '',
        architect: { test: { builder: '@angular-devkit/build-angular:karma' } },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The target "test" is not specifying any options. Skipping updating the target configuration.',
      ]);
    });

    it('should warn when there is no tsConfig specified in the test target options', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          test: { builder: '@angular-devkit/build-angular:karma', options: {} },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn.mock.calls).toContainEqual([
        'The "test" target does not have the "tsConfig" option configured. Skipping updating the tsConfig file.',
      ]);
    });

    it('should warn when the specified tsConfig in the test target does not exist', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          test: {
            builder: '@angular-devkit/build-angular:karma',
            options: { tsConfig: 'tsconfig.spec.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The tsConfig file "tsconfig.spec.json" specified in the "test" target could not be found. Skipping updating the tsConfig file.'
      );
    });

    it('should warn when the prerender target does not have any options', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          prerender: { builder: '@nguniversal/builders:prerender' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The target "prerender" is not specifying any options. Skipping updating the target configuration.'
      );
    });

    it('should warn when the serveSsr target does not have any options and configurations', async () => {
      const project = addProject('app1', {
        root: '',
        architect: {
          'serve-ssr': { builder: '@nguniversal/builders:ssr-dev-server' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project, mockedLogger as any);

      await expect(migrator.migrate()).resolves.not.toThrow();

      expect(mockedLogger.warn).toHaveBeenCalledWith(
        'The target "serve-ssr" is not specifying any options or configurations. Skipping updating the target configuration.'
      );
    });
  });

  describe('files', () => {
    it('should move project files to the right destination when the app is at the workspace root', async () => {
      tree.write('.browserslistrc', '');
      writeJson(tree, '.eslintrc.json', {});
      writeJson(tree, 'tsconfig.app.json', {});
      writeJson(tree, 'tsconfig.spec.json', {});
      tree.write('src/app/app.module.ts', '// content');
      writeJson(tree, 'e2e/tsconfig.json', {});
      tree.write('e2e/protractor.conf.js', '// content');
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: { tsConfig: 'tsconfig.app.json' },
          },
          test: {
            builder: '@angular-devkit/build-angular:karma',
            options: { tsConfig: 'tsconfig.spec.json' },
          },
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {},
          },
          e2e: {
            builder: '@angular-devkit/build-angular:protractor',
            options: { protractorConfig: 'e2e/protractor.conf.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(tree.exists('apps/app1/.browserslistrc')).toBe(true);
      expect(tree.exists('apps/app1/.eslintrc.json')).toBe(true);
      expect(tree.exists('apps/app1/tsconfig.app.json')).toBe(true);
      expect(tree.exists('apps/app1/tsconfig.spec.json')).toBe(true);
      expect(tree.exists('apps/app1/src/app/app.module.ts')).toBe(true);
      expect(tree.exists('apps/app1-e2e/tsconfig.json')).toBe(true);
      expect(tree.exists('apps/app1-e2e/protractor.conf.js')).toBe(true);
    });

    it('should move project files to the right destination when the app is at the projects folder', async () => {
      tree.write('projects/app1/.browserslistrc', '');
      writeJson(tree, 'projects/app1/.eslintrc.json', {});
      writeJson(tree, 'projects/app1/tsconfig.app.json', {});
      writeJson(tree, 'projects/app1/tsconfig.spec.json', {});
      tree.write('projects/app1/src/app/app.module.ts', '// content');
      writeJson(tree, 'projects/app1/e2e/tsconfig.json', {});
      tree.write('projects/app1/e2e/protractor.conf.js', '// content');
      const project = addProject('app1', {
        root: 'projects/app1',
        sourceRoot: 'projects/app1/src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: { tsConfig: 'projects/app1/tsconfig.app.json' },
          },
          test: {
            builder: '@angular-devkit/build-angular:karma',
            options: { tsConfig: 'projects/app1/tsconfig.spec.json' },
          },
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {},
          },
          e2e: {
            builder: '@angular-devkit/build-angular:protractor',
            options: {
              protractorConfig: 'projects/app1/e2e/protractor.conf.json',
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(tree.exists('apps/app1/.browserslistrc')).toBe(true);
      expect(tree.exists('apps/app1/.eslintrc.json')).toBe(true);
      expect(tree.exists('apps/app1/tsconfig.app.json')).toBe(true);
      expect(tree.exists('apps/app1/tsconfig.spec.json')).toBe(true);
      expect(tree.exists('apps/app1/src/app/app.module.ts')).toBe(true);
      expect(tree.exists('apps/app1-e2e/tsconfig.json')).toBe(true);
      expect(tree.exists('apps/app1-e2e/protractor.conf.js')).toBe(true);
    });
  });

  describe('project configuration', () => {
    it('should convert the project configuration to standalone', async () => {
      const project = addProject('app1', { root: '', sourceRoot: 'src' });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(tree.exists('apps/app1/project.json')).toBe(true);
      const { projects } = readJson(tree, 'angular.json');
      expect(projects.app1).toBeUndefined();
    });

    it('should update project root and source root', async () => {
      const project = addProject('app1', { root: '', sourceRoot: 'src' });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { root, sourceRoot } = readProjectConfiguration(tree, 'app1');
      expect(root).toBe('apps/app1');
      expect(sourceRoot).toBe('apps/app1/src');
    });

    it('should set source root when it was not set', async () => {
      const project = addProject('app1', { root: '' });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { sourceRoot } = readProjectConfiguration(tree, 'app1');
      expect(sourceRoot).toBe('apps/app1/src');
    });

    it('should update build target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: {
              outputPath: 'dist/app1',
              index: 'src/index.html',
              main: 'src/main.ts',
              polyfills: 'src/polyfills.ts',
              tsConfig: 'tsconfig.app.json',
              assets: ['src/favicon.ico', 'src/assets'],
              styles: ['src/styles.css'],
              scripts: [],
            },
            configurations: {
              production: {
                fileReplacements: [
                  {
                    replace: 'src/environments/environment.ts',
                    with: 'src/environments/environment.prod.ts',
                  },
                ],
              },
              development: {},
            },
            defaultConfiguration: 'production',
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.build).toStrictEqual({
        executor: '@angular-devkit/build-angular:browser',
        options: {
          outputPath: 'dist/apps/app1',
          index: 'apps/app1/src/index.html',
          main: 'apps/app1/src/main.ts',
          polyfills: 'apps/app1/src/polyfills.ts',
          tsConfig: 'apps/app1/tsconfig.app.json',
          assets: ['apps/app1/src/favicon.ico', 'apps/app1/src/assets'],
          styles: ['apps/app1/src/styles.css'],
          scripts: [],
        },
        configurations: {
          production: {
            fileReplacements: [
              {
                replace: 'apps/app1/src/environments/environment.ts',
                with: 'apps/app1/src/environments/environment.prod.ts',
              },
            ],
          },
          development: {},
        },
        defaultConfiguration: 'production',
      });
    });

    it('should update build target with array of polyfills', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: {
              outputPath: 'dist/app1',
              index: 'src/index.html',
              main: 'src/main.ts',
              polyfills: ['zone.js'],
              tsConfig: 'tsconfig.app.json',
              assets: ['src/favicon.ico', 'src/assets'],
              styles: ['src/styles.css'],
              scripts: [],
            },
            configurations: {
              production: {},
              development: {},
            },
            defaultConfiguration: 'production',
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.build).toStrictEqual({
        executor: '@angular-devkit/build-angular:browser',
        options: {
          outputPath: 'dist/apps/app1',
          index: 'apps/app1/src/index.html',
          main: 'apps/app1/src/main.ts',
          polyfills: ['zone.js'],
          tsConfig: 'apps/app1/tsconfig.app.json',
          assets: ['apps/app1/src/favicon.ico', 'apps/app1/src/assets'],
          styles: ['apps/app1/src/styles.css'],
          scripts: [],
        },
        configurations: {
          production: {},
          development: {},
        },
        defaultConfiguration: 'production',
      });
    });

    it('should update build target when using a target name different than "build"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomBuildTarget: {
            builder: '@angular-devkit/build-angular:browser',
            options: {
              outputPath: 'dist/app1',
              index: 'src/index.html',
              main: 'src/main.ts',
              polyfills: 'src/polyfills.ts',
              tsConfig: 'tsconfig.app.json',
              assets: ['src/favicon.ico', 'src/assets'],
              styles: ['src/styles.css'],
              scripts: [],
            },
            configurations: {
              production: {
                fileReplacements: [
                  {
                    replace: 'src/environments/environment.ts',
                    with: 'src/environments/environment.prod.ts',
                  },
                ],
              },
              development: {},
            },
            defaultConfiguration: 'production',
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomBuildTarget).toStrictEqual({
        executor: '@angular-devkit/build-angular:browser',
        options: {
          outputPath: 'dist/apps/app1',
          index: 'apps/app1/src/index.html',
          main: 'apps/app1/src/main.ts',
          polyfills: 'apps/app1/src/polyfills.ts',
          tsConfig: 'apps/app1/tsconfig.app.json',
          assets: ['apps/app1/src/favicon.ico', 'apps/app1/src/assets'],
          styles: ['apps/app1/src/styles.css'],
          scripts: [],
        },
        configurations: {
          production: {
            fileReplacements: [
              {
                replace: 'apps/app1/src/environments/environment.ts',
                with: 'apps/app1/src/environments/environment.prod.ts',
              },
            ],
          },
          development: {},
        },
        defaultConfiguration: 'production',
      });
    });

    it('should update the outputPath option of the build target with a "browser" segment at the end when there is a server target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: { outputPath: 'dist/app1' },
          },
          server: { builder: '@angular-devkit/build-angular:server' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.build).toStrictEqual({
        executor: '@angular-devkit/build-angular:browser',
        options: { outputPath: 'dist/apps/app1/browser' },
      });
    });

    it('should update lint target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.lint).toStrictEqual({
        executor: '@nx/linter:eslint',
        options: {
          lintFilePatterns: ['apps/app1/**/*.ts', 'apps/app1/**/*.html'],
        },
      });
    });

    it('should update lint target when using a name different than "lint"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomLintTarget: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomLintTarget).toStrictEqual({
        executor: '@nx/linter:eslint',
        options: {
          lintFilePatterns: ['apps/app1/**/*.ts', 'apps/app1/**/*.html'],
        },
      });
    });

    it('should update the eslintConfig option when specified', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              eslintConfig: '.eslintrc.json',
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.lint).toStrictEqual({
        executor: '@nx/linter:eslint',
        options: {
          eslintConfig: 'apps/app1/.eslintrc.json',
          lintFilePatterns: ['apps/app1/**/*.ts', 'apps/app1/**/*.html'],
        },
      });
    });

    it('should set hasTypeAwareRules when there are rules requiring type checking', async () => {
      writeJson(tree, '.eslintrc.json', {
        root: true,
        ignorePatterns: ['projects/**/*'],
        overrides: [
          {
            files: ['*.ts', '*.tsx'],
            extends: ['plugin:@nx/typescript'],
            rules: { '@typescript-eslint/await-thenable': 'error' },
          },
        ],
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              eslintConfig: '.eslintrc.json',
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.lint).toStrictEqual({
        executor: '@nx/linter:eslint',
        options: {
          eslintConfig: 'apps/app1/.eslintrc.json',
          hasTypeAwareRules: true,
          lintFilePatterns: ['apps/app1/**/*.ts', 'apps/app1/**/*.html'],
        },
      });
    });

    it('should update server target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          server: {
            builder: '@angular-devkit/build-angular:server',
            options: {
              outputPath: 'dist/app1/server',
              main: 'server.ts',
              tsConfig: 'tsconfig.server.json',
            },
            configurations: {
              production: {
                outputHashing: 'media',
                fileReplacements: [
                  {
                    replace: 'src/environments/environment.ts',
                    with: 'src/environments/environment.prod.ts',
                  },
                ],
              },
              development: {},
            },
            defaultConfiguration: 'production',
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.server).toStrictEqual({
        executor: '@angular-devkit/build-angular:server',
        options: {
          outputPath: 'dist/apps/app1/server',
          main: 'apps/app1/server.ts',
          tsConfig: 'apps/app1/tsconfig.server.json',
        },
        configurations: {
          production: {
            outputHashing: 'media',
            fileReplacements: [
              {
                replace: 'apps/app1/src/environments/environment.ts',
                with: 'apps/app1/src/environments/environment.prod.ts',
              },
            ],
          },
          development: {},
        },
        defaultConfiguration: 'production',
      });
    });

    it('should update server target when using a target name different than "server"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomServerTarget: {
            builder: '@angular-devkit/build-angular:server',
            options: {
              outputPath: 'dist/app1/server',
              main: 'server.ts',
              tsConfig: 'tsconfig.server.json',
            },
            configurations: {
              production: {
                outputHashing: 'media',
                fileReplacements: [
                  {
                    replace: 'src/environments/environment.ts',
                    with: 'src/environments/environment.prod.ts',
                  },
                ],
              },
              development: {},
            },
            defaultConfiguration: 'production',
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomServerTarget).toStrictEqual({
        executor: '@angular-devkit/build-angular:server',
        options: {
          outputPath: 'dist/apps/app1/server',
          main: 'apps/app1/server.ts',
          tsConfig: 'apps/app1/tsconfig.server.json',
        },
        configurations: {
          production: {
            outputHashing: 'media',
            fileReplacements: [
              {
                replace: 'apps/app1/src/environments/environment.ts',
                with: 'apps/app1/src/environments/environment.prod.ts',
              },
            ],
          },
          development: {},
        },
        defaultConfiguration: 'production',
      });
    });

    it('should update test target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          test: {
            builder: '@angular-devkit/build-angular:karma',
            options: {
              main: 'src/test.ts',
              tsConfig: 'tsconfig.spec.json',
              karmaConfig: 'karma.conf.js',
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.test).toStrictEqual({
        executor: '@angular-devkit/build-angular:karma',
        options: {
          main: 'apps/app1/src/test.ts',
          tsConfig: 'apps/app1/tsconfig.spec.json',
          karmaConfig: 'apps/app1/karma.conf.js',
        },
      });
    });

    it('should update test target when using a different name than "test"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomTestTarget: {
            builder: '@angular-devkit/build-angular:karma',
            options: {
              main: 'src/test.ts',
              tsConfig: 'tsconfig.spec.json',
              karmaConfig: 'karma.conf.js',
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomTestTarget).toStrictEqual({
        executor: '@angular-devkit/build-angular:karma',
        options: {
          main: 'apps/app1/src/test.ts',
          tsConfig: 'apps/app1/tsconfig.spec.json',
          karmaConfig: 'apps/app1/karma.conf.js',
        },
      });
    });

    it('should update prerender target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          prerender: {
            builder: '@nguniversal/builders:prerender',
            options: { routesFile: './server-routes-files.txt' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.prerender).toStrictEqual({
        executor: '@nguniversal/builders:prerender',
        options: { routesFile: 'apps/app1/server-routes-files.txt' },
      });
    });

    it('should update prerender target when using a different name than "prerender"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomPrerenderTarget: {
            builder: '@nguniversal/builders:prerender',
            options: { routesFile: './server-routes-files.txt' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomPrerenderTarget).toStrictEqual({
        executor: '@nguniversal/builders:prerender',
        options: { routesFile: 'apps/app1/server-routes-files.txt' },
      });
    });

    it('should update serve-ssr target', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          'serve-ssr': {
            builder: '@nguniversal/builders:ssr-dev-server',
            options: {
              sslKey: './ssl/ssl.key',
              sslCert: './ssl/ssl.cert',
              proxyConfig: './proxy.conf.js',
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets['serve-ssr']).toStrictEqual({
        executor: '@nguniversal/builders:ssr-dev-server',
        options: {
          sslKey: 'apps/app1/ssl/ssl.key',
          sslCert: 'apps/app1/ssl/ssl.cert',
          proxyConfig: 'apps/app1/proxy.conf.js',
        },
      });
    });

    it('should update serve-ssr target when using a different name than "serve-ssr"', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomServeSsr: {
            builder: '@nguniversal/builders:ssr-dev-server',
            options: {
              sslKey: './ssl/ssl.key',
              sslCert: './ssl/ssl.cert',
              proxyConfig: './proxy.conf.js',
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { targets } = readProjectConfiguration(tree, 'app1');
      expect(targets.myCustomServeSsr).toStrictEqual({
        executor: '@nguniversal/builders:ssr-dev-server',
        options: {
          sslKey: 'apps/app1/ssl/ssl.key',
          sslCert: 'apps/app1/ssl/ssl.cert',
          proxyConfig: 'apps/app1/proxy.conf.js',
        },
      });
    });
  });

  describe('tsConfig', () => {
    it('should update tsConfig file specified in the build target options', async () => {
      writeJson(tree, 'tsconfig.app.json', {
        extends: './tsconfig.json',
        compilerOptions: {
          outDir: './out-tsc/app',
          types: [],
        },
        files: ['src/main.ts', 'src/polyfills.ts'],
        include: ['src/**/*.d.ts'],
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            options: { tsConfig: 'tsconfig.app.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/tsconfig.app.json')).toStrictEqual({
        extends: '../../tsconfig.base.json',
        compilerOptions: {
          outDir: '../../dist/out-tsc',
          types: [],
        },
        files: ['src/main.ts', 'src/polyfills.ts'],
        include: ['src/**/*.d.ts'],
      });
    });

    it('should update tsConfig file specified in the build target development configuration', async () => {
      writeJson(tree, 'tsconfig.app.json', {
        extends: './tsconfig.json',
        compilerOptions: {
          outDir: './out-tsc/app',
          types: [],
        },
        files: ['src/main.ts', 'src/polyfills.ts'],
        include: ['src/**/*.d.ts'],
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
            configurations: {
              development: { tsConfig: 'tsconfig.app.json' },
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/tsconfig.app.json')).toStrictEqual({
        extends: '../../tsconfig.base.json',
        compilerOptions: {
          outDir: '../../dist/out-tsc',
          types: [],
        },
        files: ['src/main.ts', 'src/polyfills.ts'],
        include: ['src/**/*.d.ts'],
      });
    });

    it('should update tsConfig file specified in the test target options', async () => {
      writeJson(tree, 'tsconfig.spec.json', {
        extends: './tsconfig.json',
        compilerOptions: {
          outDir: './out-tsc/spec',
          types: ['jasmine'],
        },
        files: ['src/test.ts', 'src/polyfills.ts'],
        include: ['src/**/*.spec.ts', 'src/**/*.d.ts'],
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          test: {
            builder: '@angular-devkit/build-angular:karma',
            options: { tsConfig: 'tsconfig.spec.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/tsconfig.spec.json')).toStrictEqual({
        extends: '../../tsconfig.base.json',
        compilerOptions: {
          outDir: '../../dist/out-tsc',
          types: ['jasmine'],
        },
        files: ['src/test.ts', 'src/polyfills.ts'],
        include: ['src/**/*.spec.ts', 'src/**/*.d.ts'],
      });
    });

    it('should update tsConfig file specified in the server target options', async () => {
      writeJson(tree, 'tsconfig.server.json', {
        extends: './tsconfig.app.json',
        compilerOptions: {
          outDir: './out-tsc/server',
          target: 'es2019',
          types: ['node'],
        },
        files: ['src/main.server.ts', 'server.ts'],
        angularCompilerOptions: {
          entryModule: './src/app/app.server.module#AppServerModule',
        },
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          server: {
            builder: '@angular-devkit/build-angular:server',
            options: { tsConfig: 'tsconfig.server.json' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/tsconfig.server.json')).toStrictEqual({
        extends: './tsconfig.app.json',
        compilerOptions: {
          outDir: '../../dist/out-tsc',
          target: 'es2019',
          types: ['node'],
        },
        files: ['src/main.server.ts', 'server.ts'],
        angularCompilerOptions: {
          entryModule: './src/app/app.server.module#AppServerModule',
        },
      });
    });

    it('should update tsConfig file specified in the build target development configuration', async () => {
      writeJson(tree, 'tsconfig.server.json', {
        extends: './tsconfig.app.json',
        compilerOptions: {
          outDir: './out-tsc/server',
          target: 'es2019',
          types: ['node'],
        },
        files: ['src/main.server.ts', 'server.ts'],
        angularCompilerOptions: {
          entryModule: './src/app/app.server.module#AppServerModule',
        },
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          server: {
            builder: '@angular-devkit/build-angular:server',
            configurations: {
              development: { tsConfig: 'tsconfig.server.json' },
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/tsconfig.server.json')).toStrictEqual({
        extends: './tsconfig.app.json',
        compilerOptions: {
          outDir: '../../dist/out-tsc',
          target: 'es2019',
          types: ['node'],
        },
        files: ['src/main.server.ts', 'server.ts'],
        angularCompilerOptions: {
          entryModule: './src/app/app.server.module#AppServerModule',
        },
      });
    });
  });

  describe('eslint', () => {
    it('should update the extends path correctly', async () => {
      writeJson(tree, 'projects/parent/app1/.eslintrc.json', {
        extends: '../../../.eslintrc.json',
      });
      const project = addProject('app1', {
        root: 'projects/parent/app1',
        sourceRoot: 'projects/parent/app1/src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: [
                'projects/parent/app1/**/*.ts',
                'projects/parent/app1/**/*.html',
              ],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/.eslintrc.json').extends).toBe(
        '../../.eslintrc.json'
      );
    });

    it('should set the extends paths to extend from the root config when is not set', async () => {
      writeJson(tree, '.eslintrc.json', {});
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/.eslintrc.json').extends).toBe(
        '../../.eslintrc.json'
      );
    });

    it('should update the extends paths correctly when is an array', async () => {
      writeJson(tree, 'projects/parent/app1/.eslintrc.json', {
        extends: ['../../../.eslintrc.json'],
      });
      const project = addProject('app1', {
        root: 'projects/parent/app1',
        sourceRoot: 'projects/parent/app1/src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: [
                'projects/parent/app1/**/*.ts',
                'projects/parent/app1/**/*.html',
              ],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/.eslintrc.json').extends).toStrictEqual([
        '../../.eslintrc.json',
      ]);
    });

    it('should update the extends paths to extend from root when is an array and is not extending from root', async () => {
      writeJson(tree, 'projects/parent/app1/.eslintrc.json', {
        extends: ['./.eslintrc.json'],
      });
      const project = addProject('app1', {
        root: 'projects/parent/app1',
        sourceRoot: 'projects/parent/app1/src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: [
                'projects/parent/app1/**/*.ts',
                'projects/parent/app1/**/*.html',
              ],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/.eslintrc.json').extends).toStrictEqual([
        '../../.eslintrc.json',
        './.eslintrc.json',
      ]);
    });

    it('should update parserOptions project files', async () => {
      writeJson(tree, '.eslintrc.json', {
        overrides: [
          {
            files: ['*.ts'],
            parserOptions: {
              project: ['tsconfig.app.json', 'tsconfig.spec.json'],
              createDefaultProgram: true,
            },
          },
        ],
      });
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          lint: {
            builder: '@angular-eslint/builder:lint',
            options: {
              lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'],
            },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      expect(readJson(tree, 'apps/app1/.eslintrc.json')).toStrictEqual({
        extends: '../../.eslintrc.json',
        ignorePatterns: ['!**/*'],
        overrides: [
          {
            files: ['*.ts'],
            parserOptions: {
              project: ['apps/app1/tsconfig.*?.json'],
              createDefaultProgram: true,
            },
          },
        ],
      });
    });
  });

  describe('cacheable operations', () => {
    it('should add custom target names to the cacheable operations', async () => {
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          myCustomBuild: {
            builder: '@angular-devkit/build-angular:browser',
          },
          myCustomLint: { builder: '@angular-eslint/builder:lint' },
          myCustomTest: { builder: '@angular-devkit/build-angular:karma' },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { tasksRunnerOptions } = readNxJson(tree);
      expect(
        tasksRunnerOptions.default.options.cacheableOperations
      ).toStrictEqual([
        'build',
        'lint',
        'test',
        'e2e',
        'myCustomTest',
        'myCustomLint',
        'myCustomBuild',
      ]);
    });

    it('should not duplicate cacheable operations', async () => {
      tree.write('protractor.conf.js', '');
      writeJson(tree, 'e2e/tsconfig.json', {});
      const project = addProject('app1', {
        root: '',
        sourceRoot: 'src',
        architect: {
          build: {
            builder: '@angular-devkit/build-angular:browser',
          },
          lint: { builder: '@angular-eslint/builder:lint' },
          myCustomTest: { builder: '@angular-devkit/build-angular:karma' },
          e2e: {
            builder: '@angular-devkit/build-angular:protractor',
            options: { protractorConfig: 'protractor.conf.js' },
          },
        },
      });
      const migrator = new AppMigrator(tree, {}, project);

      await migrator.migrate();

      const { tasksRunnerOptions } = readNxJson(tree);
      expect(
        tasksRunnerOptions.default.options.cacheableOperations
      ).toStrictEqual(['build', 'lint', 'test', 'e2e', 'myCustomTest']);
    });
  });
});
