Farm源码阅读(1)

322 阅读6分钟

最近看到个新的编译工具Farm,编译器还是rust写的,刚好最近在学rust,勾引起我学习的兴趣。下面直接开始学习一下,本篇从入口cli包到core包,都是nodejs这部分的代码。

cli

cli基本可以看作整个项目的入口,包括dev、build。

代码位于farm仓库的packages/cli目录下。

入口代码src/index.ts:

import { cac } from 'cac';
const cli = cac('farm');


// cli基本的参数
cli
  .option('-c, --config <file>', 'use specified config file')
  .option('-m, --mode <mode>', 'set env mode')
  .option('--base <path>', 'public base path')
  .option('--clearScreen', 'allow/disable clear screen when logging', {
    default: true
  });

// farm 或者 farm start 命令的参数和start命令对应的执行逻辑
cli
  .command(
    '[root]',
    'Compile the project in dev mode and serve it with farm dev server'
  )
  .alias('start')
  .option('-l, --lazy', 'lazyCompilation')
  .option('--host <host>', 'specify host')
  .option('--port <port>', 'specify port')
  .option('--open', 'open browser on server start')
  .option('--hmr', 'enable hot module replacement')
  .option('--cors', 'enable cors')
  .option('--strictPort', 'specified port is already in use, exit with error')
  .action(
    async (
      root: string,
      options: FarmCLIServerOptions & GlobalFarmCLIOptions
    ) => {
      // 处理传入的参数,包括过滤重复参数、缩写参数、设置默认值...
      // 引入@farmfe/core,并执行core提供的start方法,启动本地调试的编译环境,类似vite命令
      const { start } = await resolveCore();
      handleAsyncOperationErrors( // handleAsyncOperationErrors 用来 catch 被 reject 的 promise
        start(defaultOptions), // 处理完cli参数,交给start启动编译
        'Failed to start server'
      );
    }
  );

可以看出主要用了cac来作为参数解析库,全部链式操作设置参数提示、回调,非常方便。

同理在后面的代码设置一好build、watch等命令。

// 代理一下process.emit方法,将node的实验提示过滤,其余的提示按原样提示
function preventExperimentalWarning() {
  const defaultEmit = process.emit;
  process.emit = function (...args: any[]) {
    if (args[1].name === 'ExperimentalWarning') {
      return undefined;
    }
    return defaultEmit.call(this, ...args);
  };
}

preventExperimentalWarning()

// 设置help命令提示
cli.help();

// 设置cli版本
cli.version(version);

//开始解析命令和参数
cli.parse();

core

index

core包是编译器的核心部分,看看index.ts:

// 提供编译器接口
export * from './compiler/index.js';
// 提供配置相关的接口,用于提供类型定义、运行config文件传递给编译器
export * from './config/index.js';
// 提供开发服务端,使用koa
export * from './server/index.js';
// 提供一些内置的插件
export * from './plugin/type.js';
// 一些公共工具包
export * from './utils/index.js';

// 省略import

// 上面cli包的start命令解析完后交给这个start开始调用编译和启动开发服务器
export async function start(
  inlineConfig: FarmCLIOptions & UserConfig
): Promise<void> {
  const logger = inlineConfig.logger ?? new Logger();
  setProcessEnv('development');

  try {
    // 解析farm.config.js/ts文件
    const resolvedUserConfig = await resolveConfig(
      inlineConfig,
      logger,
      'development'
    );

    // 使用配置创建编译器实例
    const compiler = await createCompiler(resolvedUserConfig);

    // 使用配置创建开发服务器实例
    const devServer = await createDevServer(
      compiler,
      resolvedUserConfig,
      logger
    );

    // 开始跟踪文件改变
    createFileWatcher(devServer, resolvedUserConfig, inlineConfig, logger);
    // 服务器和文件监控都准备就绪后,调用插件的configureDevServer钩子
    resolvedUserConfig.jsPlugins.forEach((plugin: JsPlugin) =>
      plugin.configureDevServer?.(devServer)
    );
    // 开始监听端口
    await devServer.listen();
  } catch (error) {
    logger.error(`Failed to start the server: ${error.stack}`);
  }
}

export async function build(
  inlineConfig: FarmCLIOptions & UserConfig
): Promise<void> {
  const logger = inlineConfig.logger ?? new Logger();
  setProcessEnv('production');
  const resolvedUserConfig = await resolveConfig(
    inlineConfig,
    logger,
    'production'
  );

  setProcessEnv(resolvedUserConfig.compilation.mode);

  try {
    // 看下面的createBundleHandler实现代码
    await createBundleHandler(resolvedUserConfig);
    // copy resources under publicDir to output.path
    await copyPublicDirectory(resolvedUserConfig, inlineConfig, logger);
  } catch (err) {
    console.log(err);
  }
}


export async function createBundleHandler(
  resolvedUserConfig: ResolvedUserConfig,
  watchMode = false
) {
  const compiler = await createCompiler(resolvedUserConfig);

  // compilerHandler用于按照选项清空控制台、计算编译时间
  await compilerHandler(async () => {
    compiler.removeOutputPathDir();
    await compiler.compile();
    compiler.writeResourcesToDisk();
  }, resolvedUserConfig);

  // 是否配置了watch,则开始监听文件变更
  if (resolvedUserConfig.compilation?.watch || watchMode) {
    const watcher = new FileWatcher(compiler, resolvedUserConfig);
    await watcher.watch();
    return watcher;
  }
}

comolier

先来看看compiler写了什么:

export class Compiler {
  // 通过napi绑定rust的编译器
  private _bindingCompiler: BindingCompiler;
  private _updateQueue: UpdateQueueItem[] = [];
  private _onUpdateFinishQueue: (() => void | Promise<void>)[] = [];

  // 标记是否已经在编译,限制只能运行一个编译任务
  public compiling = false;

  private logger: ILogger;

  constructor(public config: Config) {
    this.logger = new Logger();
    this._bindingCompiler = new BindingCompiler(this.config);
  }

  // 调用编译器完成编译
  async compile() {
    if (this.compiling) {
      this.logger.error('Already compiling', {
        exit: true
      });
    }

    this.compiling = true;
    if (process.env.FARM_PROFILE) {
      this._bindingCompiler.compileSync();
    } else {
      await this._bindingCompiler.compile();
    }
    this.compiling = false;
  }

  async update(
    paths: string[],
    sync = false,
    ignoreCompilingCheck = false
  ): Promise<JsUpdateResult> {
    let resolve: (res: JsUpdateResult) => void;

    const promise = new Promise<JsUpdateResult>((r) => {
      resolve = r;
    });

    // 如果已经有一个更新进程,加到队列里面等待执行
    if (this.compiling && !ignoreCompilingCheck) {
      this._updateQueue.push({ paths, resolve });
      return promise;
    }

    this.compiling = true;
    try {
      const res = this._bindingCompiler.update(
        paths,
        async () => {
          const next = this._updateQueue.shift();

          if (next) {
            // 如果队列里面还有任务,拿出来继续调用本方法
            await this.update(next.paths, true, true).then(next.resolve);
          } else {
            this.compiling = false;
            for (const cb of this._onUpdateFinishQueue) {
              await cb();
            }
            // 执行完回调后清空队列
            this._onUpdateFinishQueue = [];
          }
        },
        sync
      );

      return res as JsUpdateResult;
    } catch (e) {
      this.compiling = false;
      throw e;
    }
  }

  // 编译完后将资源写入到目标目录
  writeResourcesToDisk(base = ''): void {
    const resources = this.resources();
    const configOutputPath = this.config.config.output.path;
    const outputPath = path.isAbsolute(configOutputPath)
      ? configOutputPath
      : path.join(this.config.config.root, configOutputPath);

    for (const [name, resource] of Object.entries(resources)) {
      // remove query params and hash of name
      const nameWithoutQuery = name.split('?')[0];
      const nameWithoutHash = nameWithoutQuery.split('#')[0];

      const filePath = path.join(outputPath, base, nameWithoutHash);

      if (!existsSync(path.dirname(filePath))) {
        mkdirSync(path.dirname(filePath), { recursive: true });
      }

      writeFileSync(filePath, resource);
    }

    // 写入完成后,执行写入完成的回调
    this.callWriteResourcesHook();
  }

  callWriteResourcesHook() {
    for (const jsPlugin of this.config.jsPlugins ?? []) {
      (jsPlugin as JsPlugin).writeResources?.executor?.({
        resourcesMap: this._bindingCompiler.resourcesMap() as Record<
          string,
          Resource
        >,
        config: this.config.config
      });
    }
  }

  compileSync() {
    if (this.compiling) {
      this.logger.error('Already compiling', {
        exit: true
      });
    }
    this.compiling = true;
    this._bindingCompiler.compileSync();
    this.compiling = false;
  }

  // 剩下的一些都是调用compiler方法的一些方法
}

config

config部分主要用来处理用户配置,这部分代码很多,只看上面用到的resolveConfig方法

export async function resolveConfig(
  inlineOptions: FarmCLIOptions, // 命令行传入的cli命令
  logger: Logger,
  mode?: CompilationMode
): Promise<ResolvedUserConfig> {
  // 使用cli命令清空控制台
  checkClearScreen(inlineOptions);
  inlineOptions.mode = inlineOptions.mode ?? mode;

  // 根据cli的值提供默认值
  const getDefaultConfig = async () => {
    const mergedUserConfig = mergeInlineCliOptions({}, inlineOptions);

    const resolvedUserConfig = await resolveMergedUserConfig(
      mergedUserConfig,
      undefined,
      inlineOptions.mode ?? mode
    );
    resolvedUserConfig.server = normalizeDevServerOptions({}, mode);
    resolvedUserConfig.compilation = await normalizeUserCompilationConfig(
      resolvedUserConfig,
      logger,
      mode
    );
    resolvedUserConfig.root = resolvedUserConfig.compilation.root;
    resolvedUserConfig.jsPlugins = [];
    resolvedUserConfig.rustPlugins = [];
    return resolvedUserConfig;
  };
  // configPath 可能是目录或者文件
  const { configPath } = inlineOptions;
  // 如果根据configPath找找不到配置文件,则使用默认配置
  if (!configPath) {
    return getDefaultConfig();
  }

  if (!path.isAbsolute(configPath)) {
    throw new Error('configPath must be an absolute path');
  }

  // loadConfigFile主要逻辑是读取config文件,如果是ts文件就调用上面的compiler对config代码进行编译打包,如果是js直接import默认名导出
  // 如果导出是对象则作为配置内容,如果是方法,先执行然后拿到配置内容。
  const loadedUserConfig = await loadConfigFile(
    configPath,
    inlineOptions,
    logger
  );

  // 没拿到config,就使用默认值
  if (!loadedUserConfig) {
    return getDefaultConfig();
  }

  const { config: userConfig, configFilePath } = loadedUserConfig;

  // 处理rust插件和js插件
  const { jsPlugins, rustPlugins } = await resolveFarmPlugins(userConfig);

  const rawJsPlugins = (await resolveAsyncPlugins(jsPlugins || [])).filter(
    Boolean
  );

  let vitePluginAdapters: JsPlugin[] = [];
  const vitePlugins = (userConfig?.vitePlugins ?? []).filter(Boolean);
  // 如果有配置vite的插件,适配成farm的
  if (vitePlugins.length) {
    vitePluginAdapters = await handleVitePlugins(
      vitePlugins,
      userConfig,
      logger
    );
  }

  // 根据优先级排序
  const sortFarmJsPlugins = getSortedPlugins([
    ...rawJsPlugins,
    ...vitePluginAdapters
  ]);

  const config = await resolveConfigHook(userConfig, sortFarmJsPlugins);

  const mergedUserConfig = mergeInlineCliOptions(config, inlineOptions);

  const resolvedUserConfig = await resolveMergedUserConfig(
    mergedUserConfig,
    configFilePath,
    inlineOptions.mode ?? mode
  );

  // 先处理 server config ,下面normalizeUserCompilationConfig可能会用到
  resolvedUserConfig.server = normalizeDevServerOptions(
    resolvedUserConfig.server,
    mode
  );

  const targetWeb = !(
    userConfig.compilation?.output?.targetEnv === 'node' ||
    mode === 'production'
  );

  try {
  // 检查端口可用性:如果发生冲突,则自动增加端口
    targetWeb &&
      (await Server.resolvePortConflict(resolvedUserConfig.server, logger));
    // eslint-disable-next-line no-empty
  } catch {}

  // 处理config里的compilation参数
  resolvedUserConfig.compilation = await normalizeUserCompilationConfig(
    resolvedUserConfig,
    logger,
    mode
  );
  resolvedUserConfig.root = resolvedUserConfig.compilation.root;
  resolvedUserConfig.jsPlugins = sortFarmJsPlugins;
  resolvedUserConfig.rustPlugins = rustPlugins;

  // 处理完config之后,调用设置的configResolved钩子
  await resolveConfigResolvedHook(resolvedUserConfig, sortFarmJsPlugins);

  return resolvedUserConfig;
}

看似很长,实际上是因为配置文件涉及到的很多的配置,需要对配置项规范化、设置默认值操作。

server

使用koa2提供开发服务端,src/index.ts的start其中调用了该文件里的createDevServer:

export async function createDevServer(
  compiler: Compiler,
  resolvedUserConfig: ResolvedUserConfig,
  logger: Logger
) {
  const server = new Server({ compiler, logger });
  await server.createDevServer(resolvedUserConfig.server);

  return server;
}

然后看看src/server/index.ts


interface ImplDevServer {
  createServer(options: UserServerConfig): void;
  createDevServer(options: UserServerConfig): void;
  createPreviewServer(options: UserServerConfig): void;
  listen(): Promise<void>;
  close(): Promise<void>;
  getCompiler(): Compiler;
}


export class Server implements ImplDevServer {
  private _app: Koa;
  private restart_promise: Promise<void> | null = null;
  private compiler: Compiler | null;
  public logger: Logger;

  ws: WsServer;
  config: NormalizedServerConfig & UserPreviewServerConfig;
  hmrEngine?: HmrEngine;
  server?: httpServer;
  publicDir?: string;
  publicPath?: string;
  resolvedUrls?: ServerUrls;
  watcher: FileWatcher;

  constructor({
    compiler = null,
    logger
  }: {
    compiler?: Compiler | null;
    logger: Logger;
  }) {
    this.compiler = compiler;
    this.logger = logger ?? new Logger();

    this.initializeKoaServer();

    if (!compiler) return;

    this.publicDir = normalizePublicDir(compiler?.config.config.root);

    this.publicPath =
      normalizePublicPath(
        compiler.config.config.output?.publicPath,
        logger,
        false
      ) || '/';
  }
  private initializeKoaServer() {
    this._app = new Koa();
  }
  public async createDevServer(options: NormalizedServerConfig) {
    if (!this.compiler) {
      throw new Error('DevServer requires a compiler for development mode.');
    }

    await this.createServer(options);

    this.hmrEngine = new HmrEngine(this.compiler, this, this.logger);

    this.createWebSocket();

    this.invalidateVite();

    this.applyServerMiddlewares(options.middlewares);
  }
  public async createServer(options: NormalizedServerConfig) {
    const { https, host } = options;
    const protocol = https ? 'https' : 'http';

    const hostname = await resolveHostname(host);

    this.config = {
      ...options,
      protocol,
      hostname
    };

    // 因为要支持https、http2,所以要以http2.createSecureServer然后传入koa的callback这种方法起服务器
    if (https) {
      this.server = http2.createSecureServer(
        {
          ...https,
          allowHTTP1: true
        },
        this._app.callback()
      );
    } else {
      this.server = http.createServer(this._app.callback());
    }
  }
  public createWebSocket() {
    if (!this.server) {
      throw new Error('Websocket requires a server.');
    }
    this.ws = new WsServer(this.server, this.config, this.hmrEngine);
  }
  async listen(): Promise<void> {
    if (!this.server) {
      this.logger.error('HTTP server is not created yet');
      return;
    }
    const { port, open, protocol, hostname } = this.config;

    const start = Date.now();
    // start启动的话,会在这里执行编译,等待编译完成
    await this.compile();
    // 统计和打印编译时间
    bootstrap(Date.now() - start, this.compiler.config);
    // 监听端口,完成服务器的启动
    await this.startServer(this.config);

    !__FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ &&
      (await this.displayServerUrls());

    if (open) {
      const publicPath =
        this.publicPath === '/' ? this.publicPath : `/${this.publicPath}`;
      const serverUrl = `${protocol}://${hostname.name}:${port}${publicPath}`;
      openBrowser(serverUrl);
    }
  }
  async startServer(serverOptions: UserServerConfig) {
    const { port, hostname } = serverOptions;
    const listen = promisify(this.server.listen).bind(this.server);
    try {
      await listen(port, hostname.host);
    } catch (error) {
      this.handleServerError(error, port, hostname.host);
    }
  }
}

可以看到server部分代码还是和compiler部分紧密耦合在一起的,可以说server部分就是在compiler部分外面包一层服务器,方便调用编译。

HmrEngine

在创建DevServer的过程中,还初始化了HmrEngine,看看代码:

export class HmrEngine {
  // 需要更新的模块队列
  private _updateQueue: string[] = [];

  private _compiler: Compiler;
  private _devServer: Server;
  private _onUpdates: ((result: JsUpdateResult) => void)[];
  private _lastModifiedTimestamp: Map<string, string>;

  constructor(compiler: Compiler, devServer: Server, private _logger: Logger) {
    this._compiler = compiler;
    this._devServer = devServer;
    this._lastModifiedTimestamp = new Map();
  }

  callUpdates(result: JsUpdateResult) {
    this._onUpdates?.forEach((cb) => cb(result));
  }

  onUpdateFinish(cb: (result: JsUpdateResult) => void) {
    if (!this._onUpdates) {
      this._onUpdates = [];
    }
    this._onUpdates.push(cb);
  }

  recompileAndSendResult = async (): Promise<JsUpdateResult> => {
    const queue = [...this._updateQueue];

    if (queue.length === 0) {
      return;
    }

    // 处理需要更新的模块的路径
    let updatedFilesStr = queue
      .map((item) => {
        if (isAbsolute(item)) {
          return relative(this._compiler.config.config.root, item);
        } else {
          const resolvedPath = this._compiler.transformModulePath(
            this._compiler.config.config.root,
            item
          );
          return relative(this._compiler.config.config.root, resolvedPath);
        }
      })
      .join(', ');
    if (updatedFilesStr.length >= 100) {
      updatedFilesStr =
        updatedFilesStr.slice(0, 100) + `...(${queue.length} files)`;
    }

    try {
      clearScreen();
      const start = Date.now();
      // 开始调用compiler编译需要更新的模块
      const result = await this._compiler.update(queue);
      // 编译完成
      this._logger.info(
        `${bold(cyan(updatedFilesStr))} updated in ${bold(
          green(`${Date.now() - start}ms`)
        )}`
      );

      // 清理队列里已经编译过的内容,留下没编译的后续再次调用编译
      this._updateQueue = this._updateQueue.filter(
        (item) => !queue.includes(item)
      );

      // 如果有动态资源更新,在这里处理
      let dynamicResourcesMap: Record<string, Resource[]> = null;

      if (result.dynamicResourcesMap) {
        for (const [key, value] of Object.entries(result.dynamicResourcesMap)) {
          if (!dynamicResourcesMap) {
            dynamicResourcesMap = {} as Record<string, Resource[]>;
          }
          dynamicResourcesMap[key] = value.map((r) => ({
            path: r[0],
            type: r[1] as 'script' | 'link'
          }));
        }
      }

      // 将编译好的代码处理成json格式,发送给前端进行更新
      const resultStr = `{
      added: [${result.added
        .map((r) => `'${r.replaceAll('\\', '\\\\')}'`)
        .join(', ')}],
      changed: [${result.changed
        .map((r) => `'${r.replaceAll('\\', '\\\\')}'`)
        .join(', ')}],
      removed: [${result.removed
        .map((r) => `'${r.replaceAll('\\', '\\\\')}'`)
        .join(', ')}],
      immutableModules: ${JSON.stringify(result.immutableModules.trim())},
      mutableModules: ${JSON.stringify(result.mutableModules.trim())},
      boundaries: ${JSON.stringify(result.boundaries)},
      dynamicResourcesMap: ${JSON.stringify(dynamicResourcesMap)}
    }`;

      // 调用callUpdates钩子
      this.callUpdates(result);

      // 发送
      this._devServer.ws.clients.forEach((client: WebSocketClient) => {
        client.rawSend(`
        {
          type: 'farm-update',
          result: ${resultStr}
        }
      `);
      });

      // 调用onUpdateFinish钩子
      this._compiler.onUpdateFinish(async () => {
        // if there are more updates, recompile again
        if (this._updateQueue.length > 0) {
          await this.recompileAndSendResult();
        }
      });
    } catch (e) {
      clearScreen();
      // this._lastAttemptWasError = true;
      throw e;
    }
  };

  // 对外提供的hmr更新接口,传入需要更新的文件路径
  async hmrUpdate(absPath: string | string[], force = false) {
    const paths = Array.isArray(absPath) ? absPath : [absPath];

    for (const path of paths) {
      if (this._compiler.hasModule(path) && !this._updateQueue.includes(path)) {
        const lastModifiedTimestamp = this._lastModifiedTimestamp.get(path);
        const currentTimestamp = (await stat(path)).mtime.toISOString();
        // 只有在文件的时间戳(即文件的最后修改时间)与上次检查时的时间戳不同时,才对文件进行更新。如果文件自上次检查以来没有被修改过,那么就不需要更新它。
        if (!force && lastModifiedTimestamp === currentTimestamp) {
          continue;
        }

        this._lastModifiedTimestamp.set(path, currentTimestamp);
        // 将文件路径加入到需要更新的队列
        this._updateQueue.push(path);
      }
    }

    // 如果现在没有在编译,而且队列有值,才调用上面的更新方法。
    if (!this._compiler.compiling && this._updateQueue.length > 0) {
      try {
        await this.recompileAndSendResult();
      } catch (e) {
        // eslint-disable-next-line no-control-regex
        const serialization = e.message.replace(/\x1b\[[0-9;]*m/g, '');
        const errorStr = `${JSON.stringify({
          message: serialization
        })}`;
        this._devServer.ws.clients.forEach((client: WebSocketClient) => {
          client.rawSend(`
            {
              type: 'error',
              err: ${errorStr}
            }
          `);
        });
        this._logger.error(e);
      }
    }
  }
}

watcher

这部分主要实现了文件修改后,执行编译并通过hmr更新页面。在上面我们看到了start命令调用了:createFileWatcher,文件位于src/index.ts,看看他的实现:

export async function createFileWatcher(
  devServer: Server,
  resolvedUserConfig: ResolvedUserConfig,
  inlineConfig: FarmCLIOptions & UserConfig,
  logger: Logger
) {
  // 需要开启hmr并且不能是production模式启动
  if (
    devServer.config.hmr &&
    resolvedUserConfig.compilation.mode === 'production'
  ) {
    logger.error('HMR cannot be enabled in production mode.');
    return;
  }

  if (!devServer.config.hmr) {
    return;
  }

  const fileWatcher = new FileWatcher(devServer, resolvedUserConfig);
  devServer.watcher = fileWatcher;
  await fileWatcher.watch();

  const configFilePath = await getConfigFilePath(inlineConfig.configPath);
  // 监听config文件是否修改,如果config修改就需要重新启动
  const farmWatcher = new ConfigWatcher({
    ...resolvedUserConfig,
    configFilePath
  });
  farmWatcher.watch(async (files: string[]) => {
    clearScreen();

    devServer.restart(async () => {
      logFileChanges(files, resolvedUserConfig.root, logger);
      farmWatcher?.close();

      await devServer.close();
      __FARM_GLOBAL__.__FARM_RESTART_DEV_SERVER__ = true;
      // 重头开始执行start命令
      await start(inlineConfig);
    });
  });
}

然后是watcher核心代码src/watcher/index.ts


interface ImplFileWatcher {
  watch(): Promise<void>;
}

export class FileWatcher implements ImplFileWatcher {
  private _root: string;
  private _watcher: FSWatcher;
  private _logger: Logger;
  private _close = false;

  constructor(
    public serverOrCompiler: Server | Compiler,
    public options: ResolvedUserConfig
  ) {
    this._root = options.root;
    this._logger = new Logger();
  }

  getInternalWatcher() {
    return this._watcher;
  }

  async watch() {
    // 获取compiler实例
    const compiler = this.getCompilerFromServerOrCompiler(
      this.serverOrCompiler
    );

    // 定义当文件修改之后执行的回调
    const handlePathChange = async (path: string): Promise<void> => {
      if (this._close) {
        return;
      }

      try {
        if (this.serverOrCompiler instanceof Server) {
          await this.serverOrCompiler.hmrEngine.hmrUpdate(path);
        }

        if (
          this.serverOrCompiler instanceof Compiler &&
          this.serverOrCompiler.hasModule(path)
        ) {
          compilerHandler(
            async () => {
              const result = await compiler.update([path], true);
              handleUpdateFinish(result);
              compiler.writeResourcesToDisk();
            },
            this.options,
            { clear: true }
          );
        }
      } catch (error) {
        this._logger.error(error);
      }
    };

    // 需要watch的文件
    const watchedFiles = [
      ...compiler.resolvedModulePaths(this._root),
      ...compiler.resolvedWatchPaths()
    ].filter(
      (file) =>
        !file.startsWith(this.options.root) &&
        !file.includes('node_modules/') &&
        existsSync(file)
    );

    const files = [this.options.root, ...watchedFiles];
    this._watcher = createWatcher(this.options, files);

    this._watcher.on('change', (path) => {
      if (this._close) return;
      handlePathChange(path);
    });

    // 定义hmr更新完成后的回调,作用是处理模块更新完成后的结果,并将新的模块路径添加到监视器中。
    const handleUpdateFinish = (updateResult: JsUpdateResult) => {
      const added = [
        ...updateResult.added,
        ...updateResult.extraWatchResult.add
      ].map((addedModule) => {
        const resolvedPath = compiler.transformModulePath(
          this._root,
          addedModule
        );
        return resolvedPath;
      });
      const filteredAdded = added.filter(
        (file) => !file.startsWith(this.options.root)
      );

      if (filteredAdded.length > 0) {
        this._watcher.add(filteredAdded);
      }
    };

    if (this.serverOrCompiler instanceof Server) {
      this.serverOrCompiler.hmrEngine?.onUpdateFinish(handleUpdateFinish);
    }
  }

  private getCompilerFromServerOrCompiler(
    serverOrCompiler: Server | Compiler
  ): Compiler {
    return serverOrCompiler instanceof Server
      ? serverOrCompiler.getCompiler()
      : serverOrCompiler;
  }

  close() {
    this._close = false;
    this._watcher = null;
    this.serverOrCompiler = null;
  }
}

可以看到这部分代码只是胶水代码,用来实现watcher和server交互的代码,看看watcher核心代码:src/watcher/create-watcher.ts

// 快速、轻量级的文件系统遍历库,用于查找文件和目录
import glob from 'fast-glob';
// 小型而且高效的跨平台文件监视库
import chokidar, { FSWatcher, WatchOptions } from 'chokidar';

function resolveChokidarOptions(
  config: ResolvedUserConfig,
  insideChokidarOptions: WatchOptions
) {
  const { ignored = [], ...userChokidarOptions } =
    config.server?.hmr?.watchOptions ?? {};
  let cacheDir = path.resolve(config.root, 'node_modules', '.farm', 'cache');

  // 如果配置中指定了持久化缓存,并且提供了cacheDir,那么函数会使用配置中的cacheDir。如果cacheDir不是绝对路径,那么函数会将其解析为相对于项目根目录的绝对路径。
  if (
    typeof config.compilation?.persistentCache === 'object' &&
    config.compilation.persistentCache.cacheDir
  ) {
    cacheDir = config.compilation.persistentCache.cacheDir;

    if (!path.isAbsolute(cacheDir)) {
      cacheDir = path.resolve(config.root, cacheDir);
    }
  }

  const options: WatchOptions = {
    // 需要忽略的部分
    ignored: [
      '**/.git/**',
      '**/node_modules/**',
      '**/test-results/**', // Playwright
      glob.escapePath(cacheDir) + '/**',
      glob.escapePath(
        path.resolve(config.root, config.compilation.output.path)
      ) + '/**',
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    // 这意味着在监视开始时,不会触发任何事件
    ignoreInitial: true,
    // 如果监视的文件或目录没有读取权限,chokidar不会抛出错误
    ignorePermissionErrors: true,
    // 它只有在当前平台不是Linux时才会被设置。这个对象定义了在写入文件完成之前,chokidar需要等待的时间和检查的间隔。
    awaitWriteFinish:
      process.platform === 'linux'
        ? undefined
        : {
            stabilityThreshold: 10,
            pollInterval: 10
          },
    ...userChokidarOptions,
    ...insideChokidarOptions
  };

  return options;
}

// 创建watcher
export function createWatcher(
  config: ResolvedUserConfig,
  files: string[],
  chokidarOptions?: WatchOptions
): FSWatcher {
  const options = resolveChokidarOptions(config, chokidarOptions);

  return chokidar.watch(files, options);
}

以上就是core部分的核心代码解析,下一篇继续看rust部分的core和compiler代码。