Skip to content
On this page

Changesets

Changesets 是一个用于 Monorepo 项目下版本以及 Changelog 文件管理的工具。目前一些比较火的 Monorepo 仓库都在使用该工具进行项目的发包例如 pnpm、mobx 等。

Add
在修改某个问题后,还不想发布一个新版本。只需要在根目录 npm run changeset ,根据可交互界面选择需要更改的包以及对应的版本信息,再填上这次修改的简要内容。即可在.changeset 的文件中生成一个.md 文件。
这个文件本质上是做个信息的预存储,在该文件被消耗之前,用于是可以自定义修改的。随着不同开发者的迭代积累,changeset 文件是可以在一个周期之内进行累积的。

Version
想发布版本后在这之前需要手动去修改包的版本,特别是对于互相依赖的包来说这一操作特别繁琐。
利用 Changesets后只需要在根目录运行 pnpm run change:version 命令。
这里本质上做的工作是消耗 changeset 文件并且修改对应包版本以及依赖该包的包版本,同时会根据之前
.changeset 文件里面的信息来生成对应项目的 CHANGELOG 信息。

为了更加直观看到效果,我在运行之前增加一个 @mom/utils 的 changeset。这个包被 @mom/components @mom/lowcode-core @mom/lowcode-ui 依赖正常来说都需要手动去更改这些包的版本信息。

Publish
!!! 上面的 pnpm run change:version只会更改依赖包的 package.jsonversion。但是需要在项目中去 run build 构建一次最新产物再 pnpm run release。这里可以运行 pnpm run build 去构建对应更改项目。

选择勾选了 all 会打包下面四个项目,否则打包对应勾选的项目。
发包的 changeset publish 本质上就是对 npm publish 做了一次封装,同时会检查对应的 registry 上有没有对应包的版本,如果已经存在了,就不会再发包了,如果不存在会对对应的包版本执行一次 npm publish。

这里有一个很有意思的点。就是比如我用 pnpm workspace那我就要用pnpm publish来替换掉workspace:*的版本。那changesets是如何知道我们是用哪个包管理器的呢?

如何识别当前用什么包管理器

changesets的源码按照工程拆分到不同的包中,所以要寻找对应的功能可以直接从 clicommand注册里开始找。

说回发布用什么包,那肯定是跟发布有关publish.

typescript
// packages/cli/src/commands/publish/npm-utils.ts

async function internalPublish(
  pkgName: string,
  opts: { cwd: string; access?: string; tag: string },
  twoFactorState: TwoFactorState
): Promise<{ published: boolean }> {
  // 获取到当前的发布工具是什么
  let publishTool = await getPublishTool(opts.cwd);
  // 如果是pnpm 加上参数 不走git方面的检查
  if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
    publishFlags.push("--no-git-checks");
  }

  const envOverride = {
    npm_config_registry: getCorrectRegistry(),
  };
  let { code, stdout, stderr } = await spawn(
    publishTool.name,
    ["publish", opts.cwd, "--json", ...publishFlags],
    {
      env: Object.assign({}, process.env, envOverride),
    }
  );
  }
  return { published: true };
}

发现由getPublishTool这个方法得到项目所用包管理器,那就再来看看这个方法的实现。

javascript
// packages/cli/src/commands/publish/npm-utils.ts
async function getPublishTool(
  cwd: string
): Promise<{ name: "npm" } | { name: "pnpm", shouldAddNoGitChecks: boolean }> {
  const pm = await preferredPM(cwd);
  if (!pm || pm.name !== "pnpm") return { name: "npm" };
  // 判断pnpm的不同版本增加 no-git-checks参数,如果失败就降级处理
  try {
    let result = await spawn("pnpm", ["--version"], { cwd });
    let version = result.stdout.toString().trim();
    let parsed = semver.parse(version);
    return {
      name: "pnpm",
      shouldAddNoGitChecks:
        parsed?.major === undefined ? false : parsed.major >= 5,
    };
  } catch (e) {
    return {
      name: "pnpm",
      shouldAddNoGitChecks: false,
    };
  }
}

从上面可得知重点还在 preferredPM这个函数上,这个函数由preferred-pm提供。
image.png
嗯。没错就是开发pnpm那位大佬在两年前写的一个小工具库,来看看他是如何实现的。

javascript
module.exports = async function preferredPM(pkgPath) {
  if (typeof pkgPath !== "string") {
    throw new TypeError(`pkgPath should be a string, got ${typeof pkgPath}`);
  }
  if (await pathExists(path.join(pkgPath, "package-lock.json"))) {
    return {
      name: "npm",
      version: ">=5",
    };
  }
  if (await pathExists(path.join(pkgPath, "yarn.lock"))) {
    return {
      name: "yarn",
      version: "*",
    };
  }
  if (await pathExists(path.join(pkgPath, "pnpm-lock.yaml"))) {
    return {
      name: "pnpm",
      version: ">=3",
    };
  }
  if (await pathExists(path.join(pkgPath, "shrinkwrap.yaml"))) {
    return {
      name: "pnpm",
      version: "1 || 2",
    };
  }
  if (await findUp("pnpm-lock.yaml", { cwd: pkgPath })) {
    return {
      name: "pnpm",
      version: ">=3",
    };
  }
  try {
    if (typeof findYarnWorkspaceRoot(pkgPath) === "string") {
      return {
        name: "yarn",
        version: "*",
      };
    }
  } catch (err) {}
  const pm = await whichPM(pkgPath);
  return pm && { name: pm.name, version: pm.version || "*" };
};

没想到大道至简往往高端的工具采用最朴素的写法。就是在项目目录中检查有哪个packjson.lock。其中优先级最高的是 npm>yarn>pnpm

也就是说 如果你项目除了含有pnpm.lock.yaml之外的其他锁定版本的文件并且你刚好又用了**pnpm workspace**changesetsrun publish的时候都会使用npm run publish来发布。这就会导致别人在装包的时候不认识 workspace:*关键字导致npm报错