@ruan-cat/vercel-deploy-tool 1.2.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import picomatch from "picomatch";
3
+ import { consola } from "consola";
4
+ import type { DeployTarget } from "../config/schema";
5
+
6
+ /**
7
+ * git diff 检测结果
8
+ * @description
9
+ * - `string[]` - git 执行成功,返回变更文件列表(可能为空数组,表示确实无变更)
10
+ * - `null` - git 不可用或执行失败,调用方应降级为全量部署
11
+ */
12
+ export type ChangedFilesResult = string[] | null;
13
+
14
+ /**
15
+ * 获取 git diff 变更的文件列表
16
+ * @description
17
+ * 执行 `git diff --name-only <diffBase> HEAD`,返回相对于 monorepo 根目录的变更文件路径列表。
18
+ *
19
+ * 返回值说明:
20
+ * - `string[]` - git 执行成功,返回变更文件列表(空数组表示确实无变更)
21
+ * - `null` - git 命令不可用、ref 无效或执行失败,调用方应降级为全量部署
22
+ */
23
+ export function getChangedFiles(diffBase: string): ChangedFilesResult {
24
+ if (!diffBase) {
25
+ consola.warn("未提供 diffBase,无法检测变更文件,将降级为全量部署");
26
+ return null;
27
+ }
28
+
29
+ const result = spawnSync("git", ["diff", "--name-only", diffBase, "HEAD"], {
30
+ encoding: "utf-8",
31
+ });
32
+
33
+ if (result.error) {
34
+ consola.warn(`git 命令不可用或执行失败: ${result.error.message},将降级为全量部署`);
35
+ return null;
36
+ }
37
+
38
+ if (result.status !== 0) {
39
+ consola.warn(`git diff 返回非零退出码 ${result.status},将降级为全量部署`);
40
+ if (result.stderr?.trim()) {
41
+ consola.warn(result.stderr.trim());
42
+ }
43
+ return null;
44
+ }
45
+
46
+ const output = result.stdout?.trim();
47
+ if (!output) {
48
+ return [];
49
+ }
50
+
51
+ return output.split("\n").filter(Boolean);
52
+ }
53
+
54
+ /** 过滤结果 */
55
+ export interface FilterResult {
56
+ /** 需要部署的目标 */
57
+ deploy: DeployTarget[];
58
+ /** 跳过的目标 */
59
+ skipped: DeployTarget[];
60
+ }
61
+
62
+ /**
63
+ * 根据 git diff 变更文件列表过滤部署目标
64
+ * @description
65
+ * 对每个 DeployTarget,若配置了 watchPaths,则检查变更文件中是否有匹配 watchPaths 的文件。
66
+ * 有匹配则纳入部署列表,无匹配则跳过。
67
+ * 未配置 watchPaths 的目标,默认纳入部署列表(向后兼容)。
68
+ */
69
+ export function filterTargetsByDiff(targets: DeployTarget[], changedFiles: string[]): FilterResult {
70
+ const deploy: DeployTarget[] = [];
71
+ const skipped: DeployTarget[] = [];
72
+
73
+ for (const target of targets) {
74
+ if (!target.watchPaths || target.watchPaths.length === 0) {
75
+ deploy.push(target);
76
+ continue;
77
+ }
78
+
79
+ const isMatch = picomatch(target.watchPaths);
80
+ const hasChange = changedFiles.some((file) => isMatch(file));
81
+
82
+ if (hasChange) {
83
+ deploy.push(target);
84
+ } else {
85
+ skipped.push(target);
86
+ }
87
+ }
88
+
89
+ return { deploy, skipped };
90
+ }
@@ -5,6 +5,7 @@ import { task, executeSequential } from "../executor";
5
5
  import type { VercelDeployConfig } from "../../config/schema";
6
6
  import { isDeployTargetWithUserCommands, isNeedVercelBuild, getIsCopyDist } from "../../utils/type-guards";
7
7
  import { VERCEL_NULL_CONFIG, VERCEL_NULL_CONFIG_PATH } from "../../utils/vercel-null-config";
8
+ import { getChangedFiles, filterTargetsByDiff } from "../git-diff-filter";
8
9
  import { createLinkTask } from "./link";
9
10
  import { createBuildTask } from "./build";
10
11
  import { createAfterBuildTasks } from "./after-build";
@@ -23,6 +24,24 @@ async function generateVercelNullConfig() {
23
24
  consola.success(`生成 Vercel 空配置文件: ${VERCEL_NULL_CONFIG_PATH}`);
24
25
  }
25
26
 
27
+ /** 部署工作流的可选参数 */
28
+ export interface DeploymentWorkflowOptions {
29
+ /**
30
+ * Git ref,与 HEAD 对比检测变更文件
31
+ * @description
32
+ * 提供后,会通过 git diff 过滤出只有 watchPaths 匹配的目标才被部署。
33
+ * 未提供则不做过滤,全量部署。
34
+ */
35
+ diffBase?: string;
36
+
37
+ /**
38
+ * 强制部署所有目标
39
+ * @description
40
+ * 为 true 时忽略 watchPaths 过滤,即使提供了 diffBase 也全量部署。
41
+ */
42
+ forceAll?: boolean;
43
+ }
44
+
26
45
  /**
27
46
  * 执行 Vercel 部署工作流
28
47
  * @description
@@ -35,7 +54,7 @@ async function generateVercelNullConfig() {
35
54
  * 4. UserCommands + CopyDist 阶段(并行目标,串行步骤)
36
55
  * 5. Deploy + Alias 阶段(并行目标,串行步骤)
37
56
  */
38
- export async function executeDeploymentWorkflow(config: VercelDeployConfig) {
57
+ export async function executeDeploymentWorkflow(config: VercelDeployConfig, options?: DeploymentWorkflowOptions) {
39
58
  // 0. 生成 Vercel 空配置文件
40
59
  await generateVercelNullConfig();
41
60
 
@@ -59,16 +78,53 @@ export async function executeDeploymentWorkflow(config: VercelDeployConfig) {
59
78
  }
60
79
 
61
80
  await task("Vercel 部署工作流", async ({ task }) => {
81
+ // 0. 检测变更范围,确定最终部署目标
82
+ const { result: finalTargets } = await task("0. 检测变更范围", async ({ setTitle }) => {
83
+ if (options?.forceAll) {
84
+ setTitle("0. 强制全量部署(--force-all)");
85
+ return availableTargets;
86
+ }
87
+
88
+ if (!options?.diffBase) {
89
+ setTitle("0. 全量部署(未指定 --diff-base)");
90
+ return availableTargets;
91
+ }
92
+
93
+ const changedFiles = getChangedFiles(options.diffBase);
94
+
95
+ if (changedFiles === null) {
96
+ setTitle("0. Git 不可用,已降级为全量部署");
97
+ return availableTargets;
98
+ }
99
+
100
+ if (changedFiles.length === 0) {
101
+ setTitle("0. 无文件变更,跳过所有部署");
102
+ return [];
103
+ }
104
+
105
+ const { deploy, skipped } = filterTargetsByDiff(availableTargets, changedFiles);
106
+
107
+ skipped.forEach((t) => consola.info(`跳过未变更目标: ${t.targetCWD}`));
108
+
109
+ setTitle(`0. 精确部署: ${deploy.length} / ${availableTargets.length} 个目标`);
110
+ return deploy;
111
+ });
112
+
113
+ if (finalTargets.length === 0) {
114
+ consola.success("所有目标均无变更,无需部署");
115
+ return;
116
+ }
117
+
62
118
  // 1. Link 阶段(并行)
63
119
  await task("1. Link 项目", async () => {
64
- const linkTasks = availableTargets.map((target) => createLinkTask(config, target));
120
+ const linkTasks = finalTargets.map((target) => createLinkTask(config, target));
65
121
 
66
122
  await task.group((task) => linkTasks.map((t) => task(t.name, t.fn)));
67
123
  });
68
124
 
69
125
  // 2. Build 阶段(并行)
70
126
  await task("2. 构建项目", async () => {
71
- const buildTasks = availableTargets.filter(isNeedVercelBuild).map((target) => createBuildTask(config, target));
127
+ const buildTasks = finalTargets.filter(isNeedVercelBuild).map((target) => createBuildTask(config, target));
72
128
 
73
129
  if (buildTasks.length === 0) {
74
130
  consola.warn("没有需要执行 build 的目标");
@@ -87,7 +143,7 @@ export async function executeDeploymentWorkflow(config: VercelDeployConfig) {
87
143
 
88
144
  // 4. UserCommands + CopyDist 阶段(并行目标,串行步骤)
89
145
  await task("4. 执行用户命令与文件复制", async () => {
90
- const targetTasks = availableTargets.map((target) => ({
146
+ const targetTasks = finalTargets.map((target) => ({
91
147
  name: `处理目标: ${target.targetCWD}`,
92
148
  fn: async () => {
93
149
  // 如果不是 userCommands 类型,跳过
@@ -116,7 +172,7 @@ export async function executeDeploymentWorkflow(config: VercelDeployConfig) {
116
172
 
117
173
  // 5. Deploy + Alias 阶段(并行目标,串行步骤)
118
174
  await task("5. 部署与设置别名", async () => {
119
- const deployAliasTasks = availableTargets.map((target) => ({
175
+ const deployAliasTasks = finalTargets.map((target) => ({
120
176
  name: `部署与别名: ${target.targetCWD}`,
121
177
  fn: async () => {
122
178
  // 5.1 部署