umijs

430 阅读11分钟

umijs

  • 如何调试umijs
    • 1:下载umijs github.com/umijs/umi
    • 2:在本地的vsCode调试 umijs image.png
      {
              // 使用 IntelliSense 了解相关属性。
              // 悬停以查看现有属性的描述。
              // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
              "version": "0.2.0",
              "configurations": [
                      {
                              "name": "Attach to Remote",
                              "port": 9230,
                              "request": "attach",
                              "type": "node"
                      }
              ]
      }
      
      
    • 3: 新建调试文件 image.png

umijs 前期需要准备的东西

  • esbuild/joi/priates/ tapable/chokider/scripts
  • esbuild 是打包工具
  • joi 配置校验工具
  • pirates 用于处理node加载(require)es6、ts文件
  • tapabll发布订阅插件
  • chokider 监听文件变化
  • scripts 这是umi-scripts 指令是umi内部的指令,bin文件中执行的逻辑大概就是运行umi-scripts [指令],就可以执行scripts下文件名的ts脚本了,例如umi-scripts turbo --cmd build就是执行scripts/turbo.ts

umijs执行循序

  • cli/cli.ts
    • 当我们的umi项目执行umi指令时,会执行文件下的run的函数,这里校验了node版本,设置了一些环境信息,然后如果是dev,则执行dev函数,否则直接运行new Service().run2(),由于dev中也会创建Service,我们跳过这块
  • cli/dev.ts、cli/fork.ts
    • 开启进程去执行forkedDev
  • cli/forkedDev.ts
    • 创建服务,并运行,并在进程关闭前调用onExit方法出发插件的一些行为
  • service/service.ts
    • 在构造函数中初始化了默认preset:'@umijs/preset-umi',默认的插件join(cwd, 'plugin.ts')(即项目根目录下允许创建自定义插件),默认配置文件位置DEFAULT_CONFIG_FILES,应用的启动目录env.APP_ROOT || process.cwd() run2为run的前置函数,目前适配了版本帮助指令的简写

umijs

1. cli文件

import {
  catchUnhandledRejection,
  checkLocal,
  checkVersion as checkNodeVersion,
  logger,
  printHelp,
  setNoDeprecation,
  setNodeTitle,
  yParser,
} from '@umijs/utils';
import { DEV_COMMAND, FRAMEWORK_NAME, MIN_NODE_VERSION } from '../constants';
import { Service } from '../service/service';
import { dev } from './dev';

interface IOpts {
  presets?: string[];
}

catchUnhandledRejection();

export async function run(opts?: IOpts) {
   //检测node版本
  checkNodeVersion(MIN_NODE_VERSION);
  //检测是不是本地环境 dev 还是啥,这个和打印的log日志有关
  checkLocal();
  // 设置 环境变量 umi
  setNodeTitle(FRAMEWORK_NAME);
  setNoDeprecation();
  // 路径解析参数 process.argv
  const args = yParser(process.argv.slice(2), {
    alias: {
      version: ['v'],
      help: ['h'],
    },
    boolean: ['version'],
  });
  // 获取运行时的命令 比如 npm run dev 这里截取出来的就是 dev
  const command = args._[0];
  // 内部存在的命令
  const FEATURE_COMMANDS = ['mfsu', 'setup', 'deadcode'];
  if ([DEV_COMMAND, ...FEATURE_COMMANDS].includes(command)) {
    process.env.NODE_ENV = 'development';
  } else if (command === 'build') {
    process.env.NODE_ENV = 'production';
  }
  if (opts?.presets) {
    process.env[`${FRAMEWORK_NAME}_PRESETS`.toUpperCase()] =
      opts.presets.join(',');
  }
  // 如果是命令是的 dev 就执行dev()
  if (command === DEV_COMMAND) {
    dev();
  } else {
    // 否则就执行 Pro
    try {
      await new Service().run2({
        name: args._[0],
        args,
      });
    } catch (e: any) {
      logger.fatal(e);
      printHelp.exit();
      process.exit(1);
    }
  }
}

2. 运行命令是npm run dev

  • 2.1 dev.ts文件
import fork from './fork';

// npm run dev 执行这个函数
export function dev() {
  // 开启子线程
  // 核心目的是通过 Node.js 的 fork 创建子进程,
  // 并处理父进程(当前脚本)接收到的终止信号(如 SIGINT 和 SIGTERM)来优雅地关闭子进程。
  const child = fork({
    scriptPath: require.resolve('../../bin/forkedDev'), //解析并返回一个文件路径,该文件路径对应于要被子进程执行的脚本
  });

  // 监听 SIGINT 信号
  process.on('SIGINT', () => {
    child.kill('SIGINT');  // 向子进程发送 SIGINT 信号
    process.exit(0);       // 终止父进程(退出时返回状态码 0,表示正常退出)
  });

  // 监听 SIGTERM 信号
  process.on('SIGTERM', () => {
    child.kill('SIGTERM');  // 向子进程发送 SIGTERM 信号
    process.exit(1);        // 终止父进程(退出时返回状态码 1,表示发生错误)
  });
}
  • 2.2 forkedDev.ts 文件
import {
  logger,
  printHelp,
  setNoDeprecation,
  setNodeTitle,
  yParser,
} from '@umijs/utils';
import { DEV_COMMAND, FRAMEWORK_NAME } from '../constants';
import { Service } from '../service/service';
setNodeTitle(`${FRAMEWORK_NAME}-dev`);
setNoDeprecation();

(async () => {
  try {
    const args = yParser(process.argv.slice(2));
    // 开启服务
    const service = new Service();
    await service.run2({
      name: DEV_COMMAND,
      args,
    });

    let closed = false;
    // kill(2) Ctrl-C
    process.once('SIGINT', () => onSignal('SIGINT'));
    // kill(3) Ctrl-\
    process.once('SIGQUIT', () => onSignal('SIGQUIT'));
    // kill(15) default
    process.once('SIGTERM', () => onSignal('SIGTERM'));
    function onSignal(signal: string) {
      if (closed) return;
      closed = true;
      // 退出时触发插件中的 onExit 事件
      service.applyPlugins({
        key: 'onExit',
        args: {
          signal,
        },
      });
      process.exit(0);
    }
  } catch (e: any) {
    logger.fatal(e);
    printHelp.exit();
    process.exit(1);
  }
})();

  • 2.3 src/service/service.ts
import { Service as CoreService } from '@umijs/core';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
import { DEFAULT_CONFIG_FILES, FRAMEWORK_NAME } from '../constants';
import { getCwd } from './cwd';

export class Service extends CoreService {
  constructor(opts?: any) {
    process.env.UMI_DIR = dirname(require.resolve('../../package'));
    const cwd = getCwd();
    // Why?
    // plugin import from umi but don't explicitly depend on it
    // and we may also have old umi installed
    // ref: https://github.com/umijs/umi/issues/8342#issuecomment-1182654076
    require('./requireHook');
    super({
      ...opts,
      env: process.env.NODE_ENV,
      cwd,
      defaultConfigFiles: opts?.defaultConfigFiles || DEFAULT_CONFIG_FILES, // 初始化基本配置项
      frameworkName: opts?.frameworkName || FRAMEWORK_NAME, // 出事配置项查找的文件名 config.ts等等
      presets: [require.resolve('@umijs/preset-umi'), ...(opts?.presets || [])], // 初始化预设文件
      plugins: [
        existsSync(join(cwd, 'plugin.ts')) && join(cwd, 'plugin.ts'),
        existsSync(join(cwd, 'plugin.js')) && join(cwd, 'plugin.js'),
      ].filter(Boolean), // 初始化插件集合
    });
  }

  async run2(opts: { name: string; args?: any }) {
    let name = opts.name;
    if (opts?.args.version || name === 'v') {
      name = 'version';
    } else if (opts?.args.help || !name || name === 'h') {
      name = 'help';
    }

    // TODO
    // initWebpack

    return await this.run({ ...opts, name });
  }
}

umi/core/src/service/service.ts

run() 方法启动

  async run(opts: { name: string; args?: any }) {
    // 获取传入进来的参数 npm run dev 运行命令 dev
    const { name, args = {} } = opts;
    // 初始化args参数
    args._ = args._ || [];
    // shift the command itself
    // 判断如果运行命令 和args的第一个是一样的删除
    if (args._[0] === name) args._.shift();
    this.args = args;
    this.name = name;

    // loadEnv
    // loadEnv({ cwd: this.cwd, envFile: '.env' }):调用 loadEnv 函数加载环境变量。cwd 代表当前工作目录,.env 是环境变量文件。这行代码会根据提供的 cwd 目录加载 .env 文件中的环境变量。
    this.stage = ServiceStage.init;
    // 加载项目根目录下的 .env 环境变量文件
    loadEnv({ cwd: this.cwd, envFile: '.env' });
    // get pkg from package.json
    // 存储package.json
    let pkg: Record<string, string | Record<string, any>> = {};
    // 存储package.json 文件路径
    let pkgPath: string = '';
    try {
      // 赋值操作
      pkg = require(join(this.cwd, 'package.json'));
      pkgPath = join(this.cwd, 'package.json');
    } catch (_e) {
      // APP_ROOT
      // 如果当前运行的命令路径不是根路径 回去主打去根路径下面去找
      if (this.cwd !== process.cwd()) {
        try {
          pkg = require(join(process.cwd(), 'package.json'));
          pkgPath = join(process.cwd(), 'package.json');
        } catch (_e) { }
      }
    }
    // 挂载到当前的这个class 实例上
    this.pkg = pkg;
    this.pkgPath = pkgPath || join(this.cwd, 'package.json');
    // 获取umi 的前缀名
    const prefix = this.frameworkName;
    // 通过前缀名加_env 去过去对应的 process 上设置的环境变量
    const specifiedEnv = process.env[`${prefix}_ENV`.toUpperCase()];

    // 获取 .umirc.ts 配置项
    const configManager = new Config({
      cwd: this.cwd, //传入当前项目根工作路径
      env: this.env, // 开发模式 dev 还是uat 还是prod
      defaultConfigFiles: this.opts.defaultConfigFiles, // 默认的配置文件的 umirc.ts .umirc.js config/config.ts config/config.js'
      specifiedEnv, // .env 里面设置的
    });
    // {
    //   files: [
    //   ],
    //   opts: {
    //     cwd: "F:\\umi\\umi-learn1",
    //     env: "development",
    //     defaultConfigFiles: [
    //       ".umirc.ts",
    //       ".umirc.js",
    //       "config/config.ts",
    //       "config/config.js",
    //     ],
    //     specifiedEnv: undefined,
    //   },
    //   mainConfigFile: "F:\\umi\\umi-learn1\\.umirc.ts",
    //   prevConfig: null,
    // }
    this.configManager = configManager;
    // 用户配置项
    // npmClient:'npm'
    // routes:[]
    this.userConfig = configManager.getUserConfig().config;
    // get paths
    // 抽离成函数,方便后续继承覆盖
    // 获取当前路径
    // absApiRoutesPath:
    // 'F:/umi/umi-learn1/src/api'
    // absNodeModulesPath:
    // 'F:/umi/umi-learn1/node_modules'
    // absOutputPath:
    // 'F:/umi/umi-learn1/dist'
    // absPagesPath:
    // 'F:/umi/umi-learn1/src/pages'
    // absSrcPath:
    // 'F:/umi/umi-learn1/src'
    // absTmpPath:
    // 'F:/umi/umi-learn1/src/.umi'
    // cwd:
    // 'F:\\umi\\umi-learn1'
    this.paths = await this.getPaths();

    // resolve initial presets and plugins
    // 初始化umi 自己内置的插件和预设
    const { plugins, presets } = Plugin.getPluginsAndPresets({
      cwd: this.cwd,
      pkg,
      plugins: [require.resolve('./generatePlugin')].concat(
        this.opts.plugins || [],
      ),
      presets: [require.resolve('./servicePlugin')].concat(
        this.opts.presets || [],
      ),
      userConfig: this.userConfig,
      prefix,
    });
    // register presets and plugins
    this.stage = ServiceStage.initPresets;
    const presetPlugins: Plugin[] = [];
    // 初始化预设
    while (presets.length) {
      await this.initPreset({
        preset: presets.shift()!,
        presets,
        plugins: presetPlugins,
      });
    }
    plugins.unshift(...presetPlugins);
    this.stage = ServiceStage.initPlugins;
    // 初始化插件
    while (plugins.length) {
      await this.initPlugin({ plugin: plugins.shift()!, plugins });
    }
    // 找到要执行的命令 dev
    const command = this.commands[name];
    if (!command) {
      this.commandGuessHelper(Object.keys(this.commands), name);
      throw Error(`Invalid command ${chalk.red(name)}, it's not registered.`);
    }
    // collect configSchemas and configDefaults
    // 手机配置项和默认配置项
    for (const id of Object.keys(this.plugins)) {
      const { config, key } = this.plugins[id];
      if (config.schema) this.configSchemas[key] = config.schema;
      if (config.default !== undefined) {
        this.configDefaults[key] = config.default;
      }
      this.configOnChanges[key] = config.onChange || ConfigChangeType.reload;
    }
    // setup api.config from modifyConfig and modifyDefaultConfig
    this.stage = ServiceStage.resolveConfig;
    //
    const { defaultConfig } = await this.resolveConfig();
    if (this.config.outputPath) {
      this.paths.absOutputPath = isAbsolute(this.config.outputPath)
        ? this.config.outputPath
        : join(this.cwd, this.config.outputPath);
    }
    // 触发插件中的某个生命周期或钩子,并应用插件对特定事件的处理。它允许你通过插件来动态地修改配置、执行额外的操作、或对框架行为进行拓展。
    this.paths = await this.applyPlugins({
      key: 'modifyPaths',
      initialValue: this.paths,
    });

    const storage = await this.applyPlugins({
      key: 'modifyTelemetryStorage',
      initialValue: noopStorage,
    });

    this.telemetry.useStorage(storage);
    // applyPlugin collect app data
    // TODO: some data is mutable
    this.stage = ServiceStage.collectAppData;
    // 修改 appData 源数据信息
    this.appData = await this.applyPlugins({
      key: 'modifyAppData',
      initialValue: {
        // base
        cwd: this.cwd,
        pkg,
        pkgPath,
        plugins: this.plugins,
        presets,
        name,
        args,
        // config
        userConfig: this.userConfig,
        mainConfigFile: configManager.mainConfigFile,
        config: this.config,
        defaultConfig: defaultConfig,
        // TODO
        // moduleGraph,
        // routes,
        // npmClient,
        // nodeVersion,
        // gitInfo,
        // gitBranch,
        // debugger info,
        // devPort,
        // devHost,
        // env
      },
    });
    // applyPlugin onCheck
    this.stage = ServiceStage.onCheck;
    await this.applyPlugins({
      key: 'onCheck',
    });
    // applyPlugin onStart
    this.stage = ServiceStage.onStart;
    await this.applyPlugins({
      key: 'onStart',
    });
    // run command
    this.stage = ServiceStage.runCommand;
    // 执行dev 命令
    let ret = await command.fn({ args });
    this._profilePlugins();
    return ret;
  }

defaultConfig

2c15a1cbd681641c59a86aaacc508139.png

import type { BuildResult } from '@umijs/bundler-utils/compiled/esbuild';
import {
  AsyncSeriesWaterfallHook,
  SyncWaterfallHook,
} from '@umijs/bundler-utils/compiled/tapable';
import { chalk, fastestLevenshtein, lodash, yParser } from '@umijs/utils';
import assert from 'assert';
import { existsSync } from 'fs';
import { isAbsolute, join } from 'path';
import { Config } from '../config/config';
import { DEFAULT_FRAMEWORK_NAME } from '../constants';
import {
  ApplyPluginsType,
  ConfigChangeType,
  EnableBy,
  Env,
  IEvent,
  IFrameworkType,
  IModify,
  PluginType,
  ServiceStage,
} from '../types';
import { Command } from './command';
import { loadEnv } from './env';
import { Generator } from './generator';
import { Hook } from './hook';
import { getPaths } from './path';
import { Plugin } from './plugin';
import { PluginAPI } from './pluginAPI';
import { noopStorage, Telemetry } from './telemetry';

interface IOpts {
  cwd: string;
  env: Env;
  plugins?: string[];
  presets?: string[];
  frameworkName?: string;
  defaultConfigFiles?: string[];
}

export class Service {
  private opts: IOpts;
  appData: {
    deps?: Record<
      string,
      {
        version: string;
        matches: string[];
        subpaths: string[];
        external?: boolean;
      }
    >;
    framework?: IFrameworkType;
    prepare?: {
      buildResult: Omit<BuildResult, 'outputFiles'>;
      fileImports?: Record<string, Declaration[]>;
    };
    mpa?: {
      entry?: { [key: string]: string }[];
    };
    bundler?: string;
    [key: string]: any;
  } = {};
  args: yParser.Arguments = { _: [], $0: '' };
  commands: Record<string, Command> = {};
  generators: Record<string, Generator> = {};
  config: Record<string, any> = {};
  configSchemas: Record<string, any> = {};
  configDefaults: Record<string, any> = {};
  configOnChanges: Record<string, any> = {};
  cwd: string;
  env: Env;
  hooks: Record<string, Hook[]> = {};
  name: string = '';
  paths: {
    cwd?: string;
    absSrcPath?: string;
    absPagesPath?: string;
    absApiRoutesPath?: string;
    absTmpPath?: string;
    absNodeModulesPath?: string;
    absOutputPath?: string;
  } = {};
  // preset is plugin with different type
  plugins: Record<string, Plugin> = {};
  keyToPluginMap: Record<string, Plugin> = {};
  pluginMethods: Record<string, { plugin: Plugin; fn: Function }> = {};
  skipPluginIds: Set<string> = new Set<string>();
  stage: ServiceStage = ServiceStage.uninitialized;
  userConfig: Record<string, any> = {};
  configManager: Config | null = null;
  pkg: {
    name?: string;
    version?: string;
    dependencies?: Record<string, string>;
    devDependencies?: Record<string, string>;
    [key: string]: any;
  } = {};
  pkgPath: string = '';
  telemetry = new Telemetry();

  constructor(opts: IOpts) {
    this.cwd = opts.cwd;
    this.env = opts.env;
    this.opts = opts;
    assert(existsSync(this.cwd), `Invalid cwd ${this.cwd}, it's not found.`);
  }

  // overload, for apply event synchronously
  applyPlugins<T>(opts: {
    key: string;
    type?: ApplyPluginsType.event;
    initialValue?: any;
    args?: any;
    sync: true;
  }): typeof opts.initialValue | T;
  applyPlugins<T>(opts: {
    key: string;
    type?: ApplyPluginsType;
    initialValue?: any;
    args?: any;
  }): Promise<typeof opts.initialValue | T>;
  applyPlugins<T>(opts: {
    key: string;
    type?: ApplyPluginsType;
    initialValue?: any;
    args?: any;
    sync?: boolean;
  }): Promise<typeof opts.initialValue | T> | (typeof opts.initialValue | T) {
    const hooks = this.hooks[opts.key] || [];
    let type = opts.type;
    // guess type from key
    if (!type) {
      if (opts.key.startsWith('on')) {
        type = ApplyPluginsType.event;
      } else if (opts.key.startsWith('modify')) {
        type = ApplyPluginsType.modify;
      } else if (opts.key.startsWith('add')) {
        type = ApplyPluginsType.add;
      } else {
        throw new Error(
          `Invalid applyPlugins arguments, type must be supplied for key ${opts.key}.`,
        );
      }
    }
    switch (type) {
      case ApplyPluginsType.add:
        assert(
          !('initialValue' in opts) || Array.isArray(opts.initialValue),
          `applyPlugins failed, opts.initialValue must be Array if opts.type is add.`,
        );
        const tAdd = new AsyncSeriesWaterfallHook(['memo']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook)) continue;
          tAdd.tapPromise(
            {
              name: hook.plugin.key,
              stage: hook.stage || 0,
              before: hook.before,
            },
            async (memo: any) => {
              const dateStart = new Date();
              const items = await hook.fn(opts.args);
              hook.plugin.time.hooks[opts.key] ||= [];
              hook.plugin.time.hooks[opts.key].push(
                new Date().getTime() - dateStart.getTime(),
              );
              return memo.concat(items);
            },
          );
        }
        return tAdd.promise(opts.initialValue || []) as Promise<T>;
      case ApplyPluginsType.modify:
        const tModify = new AsyncSeriesWaterfallHook(['memo']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook)) continue;
          tModify.tapPromise(
            {
              name: hook.plugin.key,
              stage: hook.stage || 0,
              before: hook.before,
            },
            async (memo: any) => {
              const dateStart = new Date();
              const ret = await hook.fn(memo, opts.args);
              hook.plugin.time.hooks[opts.key] ||= [];
              hook.plugin.time.hooks[opts.key].push(
                new Date().getTime() - dateStart.getTime(),
              );
              return ret;
            },
          );
        }
        return tModify.promise(opts.initialValue) as Promise<T>;
      case ApplyPluginsType.event:
        if (opts.sync) {
          const tEvent = new SyncWaterfallHook(['_']);
          hooks.forEach((hook) => {
            if (this.isPluginEnable(hook)) {
              tEvent.tap(
                {
                  name: hook.plugin.key,
                  stage: hook.stage || 0,
                  before: hook.before,
                },
                () => {
                  const dateStart = new Date();
                  hook.fn(opts.args);
                  hook.plugin.time.hooks[opts.key] ||= [];
                  hook.plugin.time.hooks[opts.key].push(
                    new Date().getTime() - dateStart.getTime(),
                  );
                },
              );
            }
          });

          return tEvent.call(1) as T;
        }

        const tEvent = new AsyncSeriesWaterfallHook(['_']);
        for (const hook of hooks) {
          if (!this.isPluginEnable(hook)) continue;
          tEvent.tapPromise(
            {
              name: hook.plugin.key,
              stage: hook.stage || 0,
              before: hook.before,
            },
            async () => {
              const dateStart = new Date();
              await hook.fn(opts.args);
              hook.plugin.time.hooks[opts.key] ||= [];
              hook.plugin.time.hooks[opts.key].push(
                new Date().getTime() - dateStart.getTime(),
              );
            },
          );
        }
        return tEvent.promise(1) as Promise<T>;
      default:
        throw new Error(
          `applyPlugins failed, type is not defined or is not matched, got ${opts.type}.`,
        );
    }
  }

  async run(opts: { name: string; args?: any }) {
    // 获取传入进来的参数 npm run dev 运行命令 dev
    const { name, args = {} } = opts;
    // 初始化args参数
    args._ = args._ || [];
    // shift the command itself
    // 判断如果运行命令 和args的第一个是一样的删除
    if (args._[0] === name) args._.shift();
    this.args = args;
    this.name = name;

    // loadEnv
    // loadEnv({ cwd: this.cwd, envFile: '.env' }):调用 loadEnv 函数加载环境变量。cwd 代表当前工作目录,.env 是环境变量文件。这行代码会根据提供的 cwd 目录加载 .env 文件中的环境变量。
    this.stage = ServiceStage.init;
    // 加载项目根目录下的 .env 环境变量文件
    loadEnv({ cwd: this.cwd, envFile: '.env' });
    // get pkg from package.json
    // 存储package.json
    let pkg: Record<string, string | Record<string, any>> = {};
    // 存储package.json 文件路径
    let pkgPath: string = '';
    try {
      // 赋值操作
      pkg = require(join(this.cwd, 'package.json'));
      pkgPath = join(this.cwd, 'package.json');
    } catch (_e) {
      // APP_ROOT
      // 如果当前运行的命令路径不是根路径 回去主打去根路径下面去找
      if (this.cwd !== process.cwd()) {
        try {
          pkg = require(join(process.cwd(), 'package.json'));
          pkgPath = join(process.cwd(), 'package.json');
        } catch (_e) { }
      }
    }
    // 挂载到当前的这个class 实例上
    this.pkg = pkg;
    this.pkgPath = pkgPath || join(this.cwd, 'package.json');
    // 获取umi 的前缀名
    const prefix = this.frameworkName;
    // 通过前缀名加_env 去过去对应的 process 上设置的环境变量
    const specifiedEnv = process.env[`${prefix}_ENV`.toUpperCase()];

    // 获取 .umirc.ts 配置项
    const configManager = new Config({
      cwd: this.cwd, //传入当前项目根工作路径
      env: this.env, // 开发模式 dev 还是uat 还是prod
      defaultConfigFiles: this.opts.defaultConfigFiles, // 默认的配置文件的 umirc.ts .umirc.js config/config.ts config/config.js'
      specifiedEnv, // .env 里面设置的
    });
    // {
    //   files: [
    //   ],
    //   opts: {
    //     cwd: "F:\\umi\\umi-learn1",
    //     env: "development",
    //     defaultConfigFiles: [
    //       ".umirc.ts",
    //       ".umirc.js",
    //       "config/config.ts",
    //       "config/config.js",
    //     ],
    //     specifiedEnv: undefined,
    //   },
    //   mainConfigFile: "F:\\umi\\umi-learn1\\.umirc.ts",
    //   prevConfig: null,
    // }
    this.configManager = configManager;
    // 用户配置项
    // npmClient:'npm'
    // routes:[]
    this.userConfig = configManager.getUserConfig().config;
    // get paths
    // 抽离成函数,方便后续继承覆盖
    // 获取当前路径
    // absApiRoutesPath:
    // 'F:/umi/umi-learn1/src/api'
    // absNodeModulesPath:
    // 'F:/umi/umi-learn1/node_modules'
    // absOutputPath:
    // 'F:/umi/umi-learn1/dist'
    // absPagesPath:
    // 'F:/umi/umi-learn1/src/pages'
    // absSrcPath:
    // 'F:/umi/umi-learn1/src'
    // absTmpPath:
    // 'F:/umi/umi-learn1/src/.umi'
    // cwd:
    // 'F:\\umi\\umi-learn1'
    this.paths = await this.getPaths();

    // resolve initial presets and plugins
    // 初始化umi 自己内置的插件和预设
    const { plugins, presets } = Plugin.getPluginsAndPresets({
      cwd: this.cwd,
      pkg,
      plugins: [require.resolve('./generatePlugin')].concat(
        this.opts.plugins || [],
      ),
      presets: [require.resolve('./servicePlugin')].concat(
        this.opts.presets || [],
      ),
      userConfig: this.userConfig,
      prefix,
    });
    // register presets and plugins
    this.stage = ServiceStage.initPresets;
    const presetPlugins: Plugin[] = [];
    // 初始化预设
    while (presets.length) {
      await this.initPreset({
        preset: presets.shift()!,
        presets,
        plugins: presetPlugins,
      });
    }
    plugins.unshift(...presetPlugins);
    this.stage = ServiceStage.initPlugins;
    // 初始化插件
    while (plugins.length) {
      await this.initPlugin({ plugin: plugins.shift()!, plugins });
    }
    // 找到要执行的命令 dev
    const command = this.commands[name];
    if (!command) {
      this.commandGuessHelper(Object.keys(this.commands), name);
      throw Error(`Invalid command ${chalk.red(name)}, it's not registered.`);
    }
    // collect configSchemas and configDefaults
    // 手机配置项和默认配置项
    for (const id of Object.keys(this.plugins)) {
      const { config, key } = this.plugins[id];
      if (config.schema) this.configSchemas[key] = config.schema;
      if (config.default !== undefined) {
        this.configDefaults[key] = config.default;
      }
      this.configOnChanges[key] = config.onChange || ConfigChangeType.reload;
    }
    // setup api.config from modifyConfig and modifyDefaultConfig
    this.stage = ServiceStage.resolveConfig;
    //
    const { defaultConfig } = await this.resolveConfig();
    if (this.config.outputPath) {
      this.paths.absOutputPath = isAbsolute(this.config.outputPath)
        ? this.config.outputPath
        : join(this.cwd, this.config.outputPath);
    }
    // 触发插件中的某个生命周期或钩子,并应用插件对特定事件的处理。它允许你通过插件来动态地修改配置、执行额外的操作、或对框架行为进行拓展。
    this.paths = await this.applyPlugins({
      key: 'modifyPaths',
      initialValue: this.paths,
    });

    const storage = await this.applyPlugins({
      key: 'modifyTelemetryStorage',
      initialValue: noopStorage,
    });

    this.telemetry.useStorage(storage);
    // applyPlugin collect app data
    // TODO: some data is mutable
    this.stage = ServiceStage.collectAppData;
    // 修改 appData 源数据信息
    this.appData = await this.applyPlugins({
      key: 'modifyAppData',
      initialValue: {
        // base
        cwd: this.cwd,
        pkg,
        pkgPath,
        plugins: this.plugins,
        presets,
        name,
        args,
        // config
        userConfig: this.userConfig,
        mainConfigFile: configManager.mainConfigFile,
        config: this.config,
        defaultConfig: defaultConfig,
        // TODO
        // moduleGraph,
        // routes,
        // npmClient,
        // nodeVersion,
        // gitInfo,
        // gitBranch,
        // debugger info,
        // devPort,
        // devHost,
        // env
      },
    });
    // applyPlugin onCheck
    this.stage = ServiceStage.onCheck;
    await this.applyPlugins({
      key: 'onCheck',
    });
    // applyPlugin onStart
    this.stage = ServiceStage.onStart;
    await this.applyPlugins({
      key: 'onStart',
    });
    // run command
    this.stage = ServiceStage.runCommand;
    // 执行dev 命令
    let ret = await command.fn({ args });
    this._profilePlugins();
    return ret;
  }

  async getPaths() {
    // get paths
    const paths = getPaths({
      cwd: this.cwd,
      env: this.env,
      prefix: this.frameworkName,
    });
    return paths;
  }
  // async resolveConfig() 异步方法,目的是解析和生成最终的配置。它涉及到插件的应用、配置的获取和合并等操作。
  async resolveConfig() {
    // configManager and paths are not available until the init stage
    assert(
      this.stage > ServiceStage.init,
      `Can't generate final config before init stage`,
    );
    const resolveMode = this.commands[this.name].configResolveMode;
    // applyPlugins 是应用插件的方法,传入的参数包含:
    //   key: 'modifyConfig':指明是应用 modifyConfig 插件,用于修改配置。
    //   initialValue:传递给插件的初始值。在这里,配置的获取方式取决于 resolveMode 的值:
    //   如果 resolveMode 为 'strict',则通过 this.configManager!.getConfig 方法获取并返回配置(可能是包含了架构验证的严格配置)。
    //   否则,通过 this.configManager!.getUserConfig 获取用户配置。
    //   使用 lodash.cloneDeep 是为了深拷贝配置对象,避免直接修改原始配置。
    //   args: { paths: this.paths }:将 this.paths 作为参数传递给插件,插件可能根据这些路径来修改或处理配置
    const config = await this.applyPlugins({
      key: 'modifyConfig',
      // why clone deep?
      // user may change the config in modifyConfig
      // e.g. memo.alias = xxx
      initialValue: lodash.cloneDeep(
        resolveMode === 'strict'
          ? this.configManager!.getConfig({
            schemas: this.configSchemas,
          }).config
          : this.configManager!.getUserConfig().config,
      ),
      args: { paths: this.paths },
    });
    const defaultConfig = await this.applyPlugins({
      key: 'modifyDefaultConfig',
      // 避免 modifyDefaultConfig 时修改 this.configDefaults
      initialValue: lodash.cloneDeep(this.configDefaults),
    });
    this.config = lodash.merge(defaultConfig, config) as Record<string, any>;

    return { config, defaultConfig };
  }
  // 作用是用于分析和输出插件的性能数据,具体来说,是对插件的运行时间进行分析并展示相关的统计信息
  _profilePlugins() {
    if (this.args.profilePlugins) {
      console.log();
      // Object.keys(this.plugins) 获取插件对象 this.plugins 中的所有插件 ID。
      //   对每个插件 ID,使用 map 遍历插件并获取以下信息:
      //   id: 插件的 ID。
      //   total: 该插件的总执行时间,通过 totalTime(plugin) 计算。
      //   register: 插件的注册时间,如果没有则为 0。
      //   hooks: 插件的钩子函数的执行时间。
      Object.keys(this.plugins)
        .map((id) => {
          const plugin = this.plugins[id];
          const total = totalTime(plugin);
          return {
            id,
            total,
            register: plugin.time.register || 0,
            hooks: plugin.time.hooks,
          };
        })
        .filter((time) => {
          // 筛选插件时间大于指定限制的插件
          return time.total > (this.args.profilePluginsLimit ?? 10);
        })
        // 使用 sort 函数将插件按总时间 total 进行降序排序
        .sort((a, b) => (b.total > a.total ? 1 : -1))
        // 输出插件的分析结果
        .forEach((time) => {
          // 使用 forEach 遍历每个插件的性能数据,并将其打印到控制台。
          // 如果启用了 profilePluginsVerbose,则还会打印详细的插件注册时间 register 和钩子函数的执行时间 hooks,其中 hooks 会通过 sortHooks 函数进行排序。
          console.log(chalk.green('plugin'), time.id, time.total);
          if (this.args.profilePluginsVerbose) {
            console.log('      ', chalk.green('register'), time.register);
            console.log(
              '      ',
              chalk.green('hooks'),
              JSON.stringify(sortHooks(time.hooks)),
            );
          }
        });
    }
    // sortHooks 函数用于排序插件的钩子(hooks)。它将钩子按执行时间的总和降序排列。
    // add(hooks[b]) 和 add(hooks[a]) 会计算每个钩子数组的总执行时间,然后按总时间排序。

    function sortHooks(hooks: Record<string, number[]>) {
      const ret: Record<string, number[]> = {};
      Object.keys(hooks)
        .sort((a, b) => {
          return add(hooks[b]) - add(hooks[a]);
        })
        .forEach((key) => {
          ret[key] = hooks[key];
        });
      return ret;
    }
    // totalTime 函数计算一个插件的总执行时间,包括:
    // 插件注册时间 register。
    // 所有钩子函数的执行时间总和(通过 reduce 和 add 函数计算每个钩子的总时间)。
    function totalTime(plugin: Plugin) {
      const time = plugin.time;
      return (
        (time.register || 0) +
        Object.values(time.hooks).reduce<number>((a, b) => a + add(b), 0)
      );
    }
    // add 函数计算一个数字数组的和,用于计算钩子函数的执行时间总和。
    function add(nums: number[]) {
      return nums.reduce((a, b) => a + b, 0);
    }
  }

  async initPreset(opts: {
    preset: Plugin;
    presets: Plugin[];
    plugins: Plugin[];
  }) {
    // opts:该函数接收一个对象作为参数,包含三个字段:
    //   preset:一个插件对象(Plugin 类型),表示当前要初始化的预设插件。
    //   presets:一个插件数组(Plugin[] 类型),存储与当前 preset 相关联的预设插件。
    //   plugins:另一个插件数组(Plugin[] 类型),存储当前已经加载的所有插件。
    const { presets, plugins } = await this.initPlugin({
      plugin: opts.preset,
      presets: opts.presets,
      plugins: opts.plugins,
    });
    opts.presets.unshift(...(presets || []));
    opts.plugins.push(...(plugins || []));
  }
  // opts:函数的参数是一个包含以下字段的对象:
  //   plugin:待初始化的插件对象,类型为 Plugin。这个插件会被注册到插件系统中。
  //   presets:一个可选的预设插件数组,这些插件会在初始化过程中注册到插件系统中。预设插件通常是为了扩展或修改插件行为的插件。
  //   plugins:插件数组,包含当前插件系统中已加载的插件。
  // initPlugin 方法的主要作用是:

  // 注册插件:确保插件没有被重复注册。
  // 初始化插件API:为插件创建 API 交互接口,绑定相关的服务属性和插件功能。
  // 执行插件:调用插件的 apply 方法,执行插件的功能,并记录执行时间。
  // 处理插件返回:检查和处理插件返回的预设插件和其他插件,确保它们正确注册。
  // 唯一性检查:确保每个插件的 key 和 id 在系统中是唯一的,避免重复注册。
  async initPlugin(opts: {
    plugin: Plugin;
    presets?: Plugin[];
    plugins: Plugin[];
  }) {
    // register to this.plugins
    // 判断插件是否已经存在了
    assert(
      !this.plugins[opts.plugin.id],
      `${opts.plugin.type} ${opts.plugin.id} is already registered by ${this.plugins[opts.plugin.id]?.path
      }, ${opts.plugin.type} from ${opts.plugin.path} register failed.`,
    );
    this.plugins[opts.plugin.id] = opts.plugin;

    // apply with PluginAPI
    // 插件的API绑定
    const pluginAPI = new PluginAPI({
      plugin: opts.plugin,
      service: this,
    });
    pluginAPI.registerPresets = pluginAPI.registerPresets.bind(
      pluginAPI,
      opts.presets || [],
    );
    pluginAPI.registerPlugins = pluginAPI.registerPlugins.bind(
      pluginAPI,
      opts.plugins,
    );
    // PluginAPI.proxyPluginAPI:通过 proxyPluginAPI 创建一个代理的插件API实例,这个实例将允许插件访问特定的服务属性(如 appData, config, args 等)。
    // serviceProps 列出了所有需要代理的服务属性。插件将能够通过这些属性与外部服务进行交互。
    // staticProps 提供了一些静态属性,比如 ApplyPluginsType、ConfigChangeType 等,这些是插件执行时需要使用的类型或常量。
    const proxyPluginAPI = PluginAPI.proxyPluginAPI({
      service: this,
      pluginAPI,
      serviceProps: [
        'appData',
        'applyPlugins',
        'args',
        'config',
        'cwd',
        'pkg',
        'pkgPath',
        'name',
        'paths',
        'userConfig',
        'env',
        'isPluginEnable',
      ],
      staticProps: {
        ApplyPluginsType,
        ConfigChangeType,
        EnableBy,
        ServiceStage,
        service: this,
      },
    });
    let dateStart = new Date();
    let ret = await opts.plugin.apply()(proxyPluginAPI);
    // 调用插件的 apply 方法
    opts.plugin.time.register = new Date().getTime() - dateStart.getTime();
    //     // 插件返回值检查:验证插件的返回结果
    if (opts.plugin.type === 'plugin') {
      assert(!ret, `plugin should return nothing`);
    }
    // key should be unique
    // 插件键值检查:确保插件的键唯一
    assert(
      !this.keyToPluginMap[opts.plugin.key],
      `key ${opts.plugin.key} is already registered by ${this.keyToPluginMap[opts.plugin.key]?.path
      }, ${opts.plugin.type} from ${opts.plugin.path} register failed.`,
    );

    this.keyToPluginMap[opts.plugin.key] = opts.plugin;
    // 处理插件返回的预设和插件
    if (ret?.presets) {
      ret.presets = ret.presets.map(
        (preset: string) =>
          new Plugin({
            path: preset,
            type: PluginType.preset,
            cwd: this.cwd,
          }),
      );
    }
    if (ret?.plugins) {
      ret.plugins = ret.plugins.map(
        (plugin: string) =>
          new Plugin({
            path: plugin,
            type: PluginType.plugin,
            cwd: this.cwd,
          }),
      );
    }
    return ret || {};
  }

  isPluginEnable(hook: Hook | string) {
    let plugin: Plugin;
    if ((hook as Hook).plugin) {
      plugin = (hook as Hook).plugin;
    } else {
      plugin = this.keyToPluginMap[hook as string];
      if (!plugin) return false;
    }
    const { id, key, enableBy } = plugin;
    if (this.skipPluginIds.has(id)) return false;
    if (this.userConfig[key] === false) return false;
    if (this.config[key] === false) return false;
    if (enableBy === EnableBy.config) {
      // TODO: 提供单独的命令用于启用插件
      // this.userConfig 中如果存在,启用
      // this.config 好了之后如果存在,启用
      // this.config 在 modifyConfig 和 modifyDefaultConfig 之后才会 ready
      // 这意味着 modifyConfig 和 modifyDefaultConfig 只能判断 api.userConfig
      // 举个具体场景:
      //   - p1 enableBy config, p2 modifyDefaultConfig p1 = {}
      //   - p1 里 modifyConfig 和 modifyDefaultConfig 仅 userConfig 里有 p1 有效,其他 p2 开启时即有效
      //   - p2 里因为用了 modifyDefaultConfig,如果 p2 是 enableBy config,需要 userConfig 里配 p2,p2 和 p1 才有效
      return key in this.userConfig || (this.config && key in this.config);
    }
    if (typeof enableBy === 'function')
      return enableBy({
        userConfig: this.userConfig,
        config: this.config,
        env: this.env,
      });
    // EnableBy.register
    return true;
  }
  // 根据输入的命令(currentCmd)与已有的命令(commands)计算相似度。
  // 找出与输入命令相似度较高的命令,并提供前 3 个作为建议。
  // 输出一个提示,告诉用户是否可能是这些命令之一。
  // 如果有相似命令,输出它们,帮助用户快速发现正确命令。
  commandGuessHelper(commands: string[], currentCmd: string) {
    const altCmds = commands.filter((cmd) => {
      return (
        fastestLevenshtein.distance(currentCmd, cmd) <
        currentCmd.length * 0.6 && currentCmd !== cmd
      );
    });
    const printHelper = altCmds
      .slice(0, 3)
      .map((cmd) => {
        return ` - ${chalk.green(cmd)}`;
      })
      .join('\n');
    if (altCmds.length) {
      console.log();
      console.log(
        [
          chalk.cyan(
            altCmds.length === 1
              ? 'Did you mean this command ?'
              : 'Did you mean one of these commands ?',
          ),
          printHelper,
        ].join('\n'),
      );
      console.log();
    }
  }

  get frameworkName() {
    return this.opts.frameworkName || DEFAULT_FRAMEWORK_NAME;
  }
}

export interface IServicePluginAPI {
  appData: typeof Service.prototype.appData;
  applyPlugins: typeof Service.prototype.applyPlugins;
  args: typeof Service.prototype.args;
  config: typeof Service.prototype.config;
  cwd: typeof Service.prototype.cwd;
  generators: typeof Service.prototype.generators;
  pkg: typeof Service.prototype.pkg;
  pkgPath: typeof Service.prototype.pkgPath;
  name: typeof Service.prototype.name;
  paths: Required<typeof Service.prototype.paths>;
  userConfig: typeof Service.prototype.userConfig;
  env: typeof Service.prototype.env;
  isPluginEnable: typeof Service.prototype.isPluginEnable;

  onCheck: IEvent<null>;
  onStart: IEvent<null>;
  modifyAppData: IModify<typeof Service.prototype.appData, null>;
  modifyConfig: IModify<
    typeof Service.prototype.config,
    { paths: Record<string, string> }
  >;
  modifyDefaultConfig: IModify<typeof Service.prototype.config, null>;
  modifyPaths: IModify<typeof Service.prototype.paths, null>;
  modifyTelemetryStorage: IModify<typeof Service.prototype.telemetry, null>;

  ApplyPluginsType: typeof ApplyPluginsType;
  ConfigChangeType: typeof ConfigChangeType;
  EnableBy: typeof EnableBy;
  ServiceStage: typeof ServiceStage;

  registerPresets: (presets: any[]) => void;
  registerPlugins: (plugins: (Plugin | {})[]) => void;
}

// this is manually copied from @umijs/es-module-parser
type DeclareKind = 'value' | 'type';
type Declaration =
  | {
    type: 'ImportDeclaration';
    source: string;
    specifiers: Array<SimpleImportSpecifier>;
    importKind: DeclareKind;
    start: number;
    end: number;
  }
  | {
    type: 'DynamicImport';
    source: string;
    start: number;
    end: number;
  }
  | {
    type: 'ExportNamedDeclaration';
    source: string;
    specifiers: Array<SimpleExportSpecifier>;
    exportKind: DeclareKind;
    start: number;
    end: number;
  }
  | {
    type: 'ExportAllDeclaration';
    source: string;
    start: number;
    end: number;
  };
type SimpleImportSpecifier =
  | {
    type: 'ImportDefaultSpecifier';
    local: string;
  }
  | {
    type: 'ImportNamespaceSpecifier';
    local: string;
    imported: string;
  }
  | {
    type: 'ImportNamespaceSpecifier';
    local?: string;
  };
type SimpleExportSpecifier =
  | {
    type: 'ExportDefaultSpecifier';
    exported: string;
  }
  | {
    type: 'ExportNamespaceSpecifier';
    exported?: string;
  }
  | {
    type: 'ExportSpecifier';
    exported: string;
    local: string;
  };


umi/preset-umi/src/commands/dev.ts