vscode-icons/vscode-icons

View on GitHub
src/pad/projectAutoDetectionManager.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
import { ErrorHandler } from '../common/errorHandler';
import { readFileAsync } from '../common/fsAsync';
import { constants } from '../constants';
import { ManifestReader } from '../iconsManifest';
import * as models from '../models';
import { IPackageManifest } from '../models/packageManifest';
import { Utils } from '../utils';

export class ProjectAutoDetectionManager
  implements models.IProjectAutoDetectionManager
{
  constructor(
    private vscodeManager: models.IVSCodeManager,
    private configManager: models.IConfigManager,
  ) {
    if (!vscodeManager) {
      throw new ReferenceError(`'vscodeManager' not set to an instance`);
    }
    if (!configManager) {
      throw new ReferenceError(`'configManager' not set to an instance`);
    }
  }

  public async detectProjects(
    projects: models.Projects[],
  ): Promise<models.IProjectDetectionResult[]> {
    if (!projects || !projects.length) {
      return null;
    }

    try {
      const results = (await (this.configManager.vsicons.projectDetection
        .disableDetect
        ? Promise.resolve(null)
        : this.vscodeManager.workspace.findFiles(
            '**/package.json',
            '**/node_modules/**',
          ))) as models.IVSCodeUri[];
      return this.detect(results, projects);
    } catch (error: unknown) {
      ErrorHandler.logError(error);
    }
  }

  private async detect(
    results: models.IVSCodeUri[],
    projects: models.Projects[],
  ): Promise<models.IProjectDetectionResult[]> {
    const detectionResults: models.IProjectDetectionResult[] = [];
    if (!results || !results.length) {
      return detectionResults;
    }
    for (const project of projects) {
      const detectionResult: models.IProjectDetectionResult =
        await this.checkForProject(results, project);
      detectionResults.push(detectionResult);
    }
    return detectionResults;
  }

  private async checkForProject(
    results: models.IVSCodeUri[],
    project: models.Projects,
  ): Promise<models.IProjectDetectionResult> {
    const getPresetName = (_project: models.Projects): string => {
      switch (_project) {
        case models.Projects.angular:
          return constants.vsicons.presets.angular;
        case models.Projects.nestjs:
          return constants.vsicons.presets.nestjs;
        default:
          break;
      }
    };

    // We need to check only the 'workspaceValue' ('user' setting should be ignored)
    const getPreset = (proj: models.Projects): boolean =>
      this.configManager.getPreset<boolean>(getPresetName(proj)).workspaceValue;

    const iconsDisabled: boolean = await ManifestReader.iconsDisabled(project);

    // NOTE: User setting (preset) bypasses detection in the following cases:
    // 1. Preset is set to 'false' and icons are not present in the manifest file
    // 2. Preset is set to 'true' and icons are present in the manifest file
    // For this cases PAD will not display a message
    const preset = getPreset(project);
    let bypass =
      preset != null &&
      ((!preset && iconsDisabled) || (preset && !iconsDisabled));

    if (bypass) {
      return { apply: false };
    }

    // We need to mandatory check the following:
    // 1. The project related icons are present in the manifest file
    // 2. It's a detectable project
    // 3. The preset (when it's defined)
    // Use case: User has the preset set but project detection does not detect that project
    const projectInfo: models.IProjectInfo = await this.getProjectInfo(
      results,
      project,
    );
    const enableIcons = iconsDisabled && (!!projectInfo || preset === true);
    const disableIcons = !iconsDisabled && (!projectInfo || preset === false);

    // Don't check for conflicting projects if user setting (preset) is explicitly set
    const conflictingProjects: models.Projects[] = preset
      ? []
      : await this.checkForConflictingProjects(results, project, enableIcons);

    // bypass, if user explicitly has any preset of a conficting project set
    // and those icons are enabled
    for (const cp of conflictingProjects) {
      bypass = getPreset(cp);
      if (bypass) {
        break;
      }
    }

    if (bypass || (!enableIcons && !disableIcons)) {
      return { apply: false };
    }

    const langResourceKey: models.LangResourceKeys = conflictingProjects.length
      ? models.LangResourceKeys.conflictProjectsDetected
      : this.getDetectionLanguageResourseKey(project, enableIcons, preset);

    return {
      apply: true,
      project,
      conflictingProjects,
      langResourceKey,
      value: enableIcons || !disableIcons,
    };
  }

  private async checkForConflictingProjects(
    results: models.IVSCodeUri[],
    project: models.Projects,
    enableIcons: boolean,
  ): Promise<models.Projects[]> {
    if (!enableIcons) {
      return [];
    }
    const conflictingProjects = async (
      conflictProjects: models.Projects[],
    ): Promise<models.Projects[]> => {
      const projectInfos: models.Projects[] = [];
      for (const cp of conflictProjects) {
        const info = await this.getProjectInfo(results, cp);
        if (info) {
          projectInfos.push(cp);
        }
      }
      return projectInfos;
    };

    switch (project) {
      case models.Projects.nestjs:
        return conflictingProjects([models.Projects.angular]);
      case models.Projects.angular:
        return conflictingProjects([models.Projects.nestjs]);
      default:
        return [];
    }
  }

  private getDetectionLanguageResourseKey(
    project: models.Projects,
    enableIcons: boolean,
    preset: boolean,
  ): models.LangResourceKeys {
    switch (project) {
      case models.Projects.angular:
        return this.getNgRelatedMessages(enableIcons, preset);
      case models.Projects.nestjs:
        return this.getNestRelatedMessages(enableIcons, preset);
    }
  }

  private getNgRelatedMessages(enableIcons: boolean, preset: boolean): number {
    return enableIcons
      ? preset === true
        ? models.LangResourceKeys.nonNgDetectedPresetTrue
        : models.LangResourceKeys.ngDetected
      : preset === false
        ? models.LangResourceKeys.ngDetectedPresetFalse
        : models.LangResourceKeys.nonNgDetected;
  }

  private getNestRelatedMessages(
    enableIcons: boolean,
    preset: boolean,
  ): number {
    return enableIcons
      ? preset === true
        ? models.LangResourceKeys.nonNestDetectedPresetTrue
        : models.LangResourceKeys.nestDetected
      : preset === false
        ? models.LangResourceKeys.nestDetectedPresetFalse
        : models.LangResourceKeys.nonNestDetected;
  }

  private async getProjectInfo(
    results: models.IVSCodeUri[],
    project: models.Projects,
  ): Promise<models.IProjectInfo> {
    let projectInfo: models.IProjectInfo = null;
    for (const result of results) {
      const content: string = await readFileAsync(result.fsPath, 'utf8');
      const projectJson: IPackageManifest =
        Utils.parseJSONSafe<IPackageManifest>(content);
      projectInfo = this.getInfo(projectJson, project);
      if (projectInfo) {
        break;
      }
    }
    return projectInfo;
  }

  private getInfo(
    projectJson: IPackageManifest,
    name: models.Projects,
  ): models.IProjectInfo {
    if (!projectJson) {
      return null;
    }

    const getInfo = (key: string): models.IProjectInfo => {
      if (projectJson.dependencies && !!projectJson.dependencies[key]) {
        return { name, version: projectJson.dependencies[key] };
      }
      if (projectJson.devDependencies && !!projectJson.devDependencies[key]) {
        return { name, version: projectJson.devDependencies[key] };
      }
      return null;
    };

    switch (name) {
      case models.Projects.angular:
        return getInfo('@angular/core');
      case models.Projects.nestjs:
        return getInfo('@nestjs/core');
      default:
        return null;
    }
  }
}