@infly/libs 2.0.49 → 2.0.51

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.
@@ -1,74 +1,74 @@
1
- const fs = require("node:fs");
2
- const path = require("node:path");
3
-
4
- const {
5
- EXIT_SELECTION,
6
- selectOption: defaultSelectOption,
7
- } = require("../../tools/select-option");
8
- const { composeRelease: defaultComposeRelease } = require("./compose-release");
9
-
10
- function loadTargetConfig(configPath) {
11
- const resolvedPath = path.resolve(configPath);
12
- delete require.cache[require.resolve(resolvedPath)];
13
- return {
14
- config: require(resolvedPath),
15
- configDir: path.dirname(resolvedPath),
16
- };
17
- }
18
-
19
- function loadPackageTargetConfig(cwd = process.cwd()) {
20
- const packagePath = path.resolve(cwd, "package.json");
21
- if (!fs.existsSync(packagePath)) {
22
- throw new Error(`Package config not found: ${packagePath}`);
23
- }
24
- delete require.cache[require.resolve(packagePath)];
25
- const packageJson = require(packagePath);
26
- return {
27
- config: packageJson.infly?.docker,
28
- configDir: path.dirname(packagePath),
29
- };
30
- }
31
-
32
- async function runComposeCommand(options = {}, dependencies = {}) {
33
- const composeRelease = dependencies.composeRelease || defaultComposeRelease;
34
- const usesDirectConfig = !options.config
35
- && !options.configObject
36
- && (options.projectDir || options.file);
37
- if (usesDirectConfig) return composeRelease(options);
38
-
39
- const loaded = options.config
40
- ? loadTargetConfig(options.config)
41
- : options.configObject
42
- ? { config: options.configObject, configDir: options.configDir || process.cwd() }
43
- : loadPackageTargetConfig(options.cwd || process.cwd());
44
- const targets = loaded.config?.targets;
45
- if (!targets || Object.keys(targets).length === 0) {
46
- throw new Error("Docker target config must define at least one target.");
47
- }
48
-
49
- const selectOption = dependencies.selectOption || defaultSelectOption;
50
- const log = dependencies.log || console.log;
51
- const targetName = options.target || await selectOption(
52
- "请选择 Docker 发布目标",
53
- Object.entries(targets).map(([value, target]) => ({
54
- value,
55
- label: target.label || value,
56
- })),
57
- );
58
- if (targetName === EXIT_SELECTION) {
59
- log("已退出,未执行任何操作。");
60
- return { cancelled: true };
61
- }
62
- const target = targets[targetName];
63
- if (!target) throw new Error(`Unknown Docker target: ${targetName}`);
64
-
65
- return composeRelease({
66
- projectDir: loaded.configDir,
67
- file: target.file,
68
- bump: options.bump || "patch",
69
- push: Boolean(options.push),
70
- dryRun: Boolean(options.dryRun),
71
- });
72
- }
73
-
74
- module.exports = { loadPackageTargetConfig, loadTargetConfig, runComposeCommand };
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const {
5
+ EXIT_SELECTION,
6
+ selectOption: defaultSelectOption,
7
+ } = require("../../cli/select-option");
8
+ const { composeRelease: defaultComposeRelease } = require("./compose-release");
9
+
10
+ function loadTargetConfig(configPath) {
11
+ const resolvedPath = path.resolve(configPath);
12
+ delete require.cache[require.resolve(resolvedPath)];
13
+ return {
14
+ config: require(resolvedPath),
15
+ configDir: path.dirname(resolvedPath),
16
+ };
17
+ }
18
+
19
+ function loadPackageTargetConfig(cwd = process.cwd()) {
20
+ const packagePath = path.resolve(cwd, "package.json");
21
+ if (!fs.existsSync(packagePath)) {
22
+ throw new Error(`Package config not found: ${packagePath}`);
23
+ }
24
+ delete require.cache[require.resolve(packagePath)];
25
+ const packageJson = require(packagePath);
26
+ return {
27
+ config: packageJson.infly?.docker,
28
+ configDir: path.dirname(packagePath),
29
+ };
30
+ }
31
+
32
+ async function runComposeCommand(options = {}, dependencies = {}) {
33
+ const composeRelease = dependencies.composeRelease || defaultComposeRelease;
34
+ const usesDirectConfig = !options.config
35
+ && !options.configObject
36
+ && (options.projectDir || options.file);
37
+ if (usesDirectConfig) return composeRelease(options);
38
+
39
+ const loaded = options.config
40
+ ? loadTargetConfig(options.config)
41
+ : options.configObject
42
+ ? { config: options.configObject, configDir: options.configDir || process.cwd() }
43
+ : loadPackageTargetConfig(options.cwd || process.cwd());
44
+ const targets = loaded.config?.targets;
45
+ if (!targets || Object.keys(targets).length === 0) {
46
+ throw new Error("Docker target config must define at least one target.");
47
+ }
48
+
49
+ const selectOption = dependencies.selectOption || defaultSelectOption;
50
+ const log = dependencies.log || console.log;
51
+ const targetName = options.target || await selectOption(
52
+ "请选择 Docker 发布目标",
53
+ Object.entries(targets).map(([value, target]) => ({
54
+ value,
55
+ label: target.label || value,
56
+ })),
57
+ );
58
+ if (targetName === EXIT_SELECTION) {
59
+ log("已退出,未执行任何操作。");
60
+ return { cancelled: true };
61
+ }
62
+ const target = targets[targetName];
63
+ if (!target) throw new Error(`Unknown Docker target: ${targetName}`);
64
+
65
+ return composeRelease({
66
+ projectDir: loaded.configDir,
67
+ file: target.file,
68
+ bump: options.bump || "patch",
69
+ push: Boolean(options.push),
70
+ dryRun: Boolean(options.dryRun),
71
+ });
72
+ }
73
+
74
+ module.exports = { loadPackageTargetConfig, loadTargetConfig, runComposeCommand };
@@ -1,6 +1,6 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
- const { execFileSync, spawnSync } = require("node:child_process");
3
+ const { execFileSync, spawn, spawnSync } = require("node:child_process");
4
4
  const dotenv = require("dotenv");
5
5
  const { dockerBuildPush } = require("./docker-build-push");
6
6
 
@@ -45,6 +45,25 @@ function run(command, args, label, cwd) {
45
45
  if (result.status !== 0) throw new Error(`${label}失败,退出码: ${result.status}`);
46
46
  }
47
47
 
48
+ function runAsync(command, args, label, cwd) {
49
+ return new Promise((resolve, reject) => {
50
+ console.log(`\n>>> ${label}`);
51
+ const child = spawn(command, args, {
52
+ cwd,
53
+ env: process.env,
54
+ stdio: "inherit",
55
+ shell: false,
56
+ });
57
+ child.once("error", (error) => {
58
+ reject(new Error(`${label}无法启动: ${error.message}`));
59
+ });
60
+ child.once("close", (code) => {
61
+ if (code === 0) resolve();
62
+ else reject(new Error(`${label}失败,退出码: ${code}`));
63
+ });
64
+ });
65
+ }
66
+
48
67
  function runShellCommand(command, label, cwd) {
49
68
  if (/[\r\n&|<>^]/.test(command)) {
50
69
  throw new Error(`${label}包含不安全的 Windows shell 字符`);
@@ -52,16 +71,59 @@ function runShellCommand(command, label, cwd) {
52
71
  run(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], label, cwd);
53
72
  }
54
73
 
55
- function runPnpmScript(scriptName, cwd) {
74
+ function runShellCommandAsync(command, label, cwd) {
75
+ if (/[\r\n&|<>^]/.test(command)) {
76
+ throw new Error(`${label}包含不安全的 Windows shell 字符`);
77
+ }
78
+ return runAsync(
79
+ process.env.ComSpec || "cmd.exe",
80
+ ["/d", "/s", "/c", command],
81
+ label,
82
+ cwd
83
+ );
84
+ }
85
+
86
+ function buildPnpmScriptSpawn(scriptName) {
56
87
  if (!SAFE_SCRIPT_NAME.test(scriptName)) {
57
88
  throw new Error(`dockerRelease.checks 包含非法脚本名: ${scriptName}`);
58
89
  }
59
90
  const label = `项目检查: pnpm run ${scriptName}`;
60
91
  if (process.platform === "win32") {
61
- runShellCommand(`pnpm run ${scriptName}`, label, cwd);
62
- } else {
63
- run("pnpm", ["run", scriptName], label, cwd);
92
+ const command = `pnpm run ${scriptName}`;
93
+ if (/[\r\n&|<>^]/.test(command)) {
94
+ throw new Error(`${label}包含不安全的 Windows shell 字符`);
95
+ }
96
+ return {
97
+ command: process.env.ComSpec || "cmd.exe",
98
+ args: ["/d", "/s", "/c", command],
99
+ label,
100
+ };
64
101
  }
102
+ return { command: "pnpm", args: ["run", scriptName], label };
103
+ }
104
+
105
+ function runPnpmScript(scriptName, cwd) {
106
+ const { command, args, label } = buildPnpmScriptSpawn(scriptName);
107
+ run(command, args, label, cwd);
108
+ }
109
+
110
+ async function runPnpmScriptAsync(scriptName, cwd) {
111
+ const { command, args, label } = buildPnpmScriptSpawn(scriptName);
112
+ return runAsync(command, args, label, cwd);
113
+ }
114
+
115
+ async function runProjectChecks(scriptNames, cwd) {
116
+ const results = await Promise.allSettled(
117
+ scriptNames.map((scriptName) => runPnpmScriptAsync(scriptName, cwd))
118
+ );
119
+ const failures = results
120
+ .map((result, index) => ({ result, scriptName: scriptNames[index] }))
121
+ .filter(({ result }) => result.status === "rejected");
122
+ if (failures.length === 0) return;
123
+ const lines = failures.map(({ result }) => ` - ${result.reason.message}`);
124
+ throw new Error(
125
+ `项目检查未全部通过(失败 ${failures.length}/${scriptNames.length}):\n${lines.join("\n")}`
126
+ );
65
127
  }
66
128
 
67
129
  function runSonarScanner(cwd, projectVersion) {
@@ -168,7 +230,7 @@ async function releaseDocker(options = {}) {
168
230
  console.warn(`紧急原因: ${emergency.emergencyReason}`);
169
231
  console.warn(`目标 commit: ${projectVersion.fullCommit}`);
170
232
  } else {
171
- for (const scriptName of config.checks) runPnpmScript(scriptName, projectRoot);
233
+ await runProjectChecks(config.checks, projectRoot);
172
234
  }
173
235
 
174
236
  if (!emergency.emergency && config.coverageReport) {
@@ -190,10 +252,12 @@ async function releaseDocker(options = {}) {
190
252
 
191
253
  module.exports = {
192
254
  assertCleanProject,
255
+ buildPnpmScriptSpawn,
193
256
  loadLocalReleaseEnv,
194
257
  normalizeEmergencyOptions,
195
258
  readReleaseConfig,
196
259
  releaseDocker,
197
260
  resolveInsideProject,
198
261
  resolveProjectVersion,
262
+ runProjectChecks,
199
263
  };
package/build/index.js ADDED
@@ -0,0 +1,34 @@
1
+ const { run, spawnBackground } = require("./build-utils/command.js");
2
+ const { createBuildContext, parseBuildContext, ENV } = require("./build-utils/env.js");
3
+ const {
4
+ getTargetBranch,
5
+ updatePackageSources,
6
+ updateAppAndDistSources,
7
+ updateAllSources,
8
+ getDistRepoTasks,
9
+ printDryRunSourceUpdatePlan,
10
+ } = require("./build-utils/git.js");
11
+ const { postBatchWebhook } = require("./build-utils/webhook.js");
12
+ const { startBatchPreviews } = require("./build-utils/preview.js");
13
+
14
+ /**
15
+ * Stable public entry for repository-level build orchestration.
16
+ *
17
+ * Keep application selection and build ordering in the caller. This module only
18
+ * exposes framework-neutral lifecycle primitives owned by @infly/libs.
19
+ */
20
+ module.exports = {
21
+ ENV,
22
+ createBuildContext,
23
+ getDistRepoTasks,
24
+ getTargetBranch,
25
+ parseBuildContext,
26
+ postBatchWebhook,
27
+ printDryRunSourceUpdatePlan,
28
+ run,
29
+ spawnBackground,
30
+ startBatchPreviews,
31
+ updateAllSources,
32
+ updateAppAndDistSources,
33
+ updatePackageSources,
34
+ };
@@ -1,52 +1,55 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- /**
5
- * 覆盖写入构建脚本
6
- * @param {String} filePath - 覆盖文件路径
7
- * @param {Array | Object} scriptsList - 写入脚本配置
8
- * @param {String} 更新字段值
9
- */
10
- function scriptWrite(filePath, scriptsList, updateKey = "scripts") {
11
- const pkg = JSON.parse(fs.readFileSync(filePath));
12
- pkg[updateKey] = pkg[updateKey] || {};
13
- if (Array.isArray(scriptsList)) {
14
- scriptsList.forEach(item => {
15
- if (pkg[updateKey][item.key]) {
16
- pkg[updateKey][`origin:${item.key}`] = pkg[updateKey][item.key];
17
- }
18
- pkg[updateKey][item.key] = item.value;
19
- });
20
- } else if (typeof scriptsList === "object") {
21
- pkg[updateKey] = scriptsList;
22
- }
23
-
24
- fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2));
25
- }
26
-
27
- /**
28
- * 获取文件绝对路径
29
- * @param {String} filePath - 文件路径
30
- * @returns
31
- */
32
- function targetProjectFileResolve(filePath) {
33
- // 初始脚本自动执行路径会自动读取node_modules
34
- const projectRoot = process.cwd();
35
- const absoluteProjectRoot = projectRoot.replace(/\\node_modules.*$/g, "");
36
- return path.resolve(absoluteProjectRoot, filePath);
37
- }
38
-
39
- /**
40
- * 获取文件绝对路径
41
- * @param {String} filePath - 文件路径
42
- * @returns
43
- */
44
- function currentProjectFileResolve(filePath) {
45
- return path.join(__dirname, filePath);
46
- }
47
-
48
- module.exports = {
49
- scriptWrite,
50
- targetProjectFileResolve,
51
- currentProjectFileResolve
52
- };
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ // 兼容旧 tools/file-process.js 的 __dirname 路径语义。
5
+ const legacyToolsDir = path.resolve(__dirname, "../tools");
6
+
7
+ /**
8
+ * 覆盖写入构建脚本
9
+ * @param {String} filePath - 覆盖文件路径
10
+ * @param {Array | Object} scriptsList - 写入脚本配置
11
+ * @param {String} 更新字段值
12
+ */
13
+ function scriptWrite(filePath, scriptsList, updateKey = "scripts") {
14
+ const pkg = JSON.parse(fs.readFileSync(filePath));
15
+ pkg[updateKey] = pkg[updateKey] || {};
16
+ if (Array.isArray(scriptsList)) {
17
+ scriptsList.forEach(item => {
18
+ if (pkg[updateKey][item.key]) {
19
+ pkg[updateKey][`origin:${item.key}`] = pkg[updateKey][item.key];
20
+ }
21
+ pkg[updateKey][item.key] = item.value;
22
+ });
23
+ } else if (typeof scriptsList === "object") {
24
+ pkg[updateKey] = scriptsList;
25
+ }
26
+
27
+ fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2));
28
+ }
29
+
30
+ /**
31
+ * 获取文件绝对路径
32
+ * @param {String} filePath - 文件路径
33
+ * @returns
34
+ */
35
+ function targetProjectFileResolve(filePath) {
36
+ // 初始脚本自动执行路径会自动读取node_modules
37
+ const projectRoot = process.cwd();
38
+ const absoluteProjectRoot = projectRoot.replace(/\\node_modules.*$/g, "");
39
+ return path.resolve(absoluteProjectRoot, filePath);
40
+ }
41
+
42
+ /**
43
+ * 获取文件绝对路径
44
+ * @param {String} filePath - 文件路径
45
+ * @returns
46
+ */
47
+ function currentProjectFileResolve(filePath) {
48
+ return path.join(legacyToolsDir, filePath);
49
+ }
50
+
51
+ module.exports = {
52
+ scriptWrite,
53
+ targetProjectFileResolve,
54
+ currentProjectFileResolve
55
+ };
@@ -1,63 +1,67 @@
1
- function formatProgressLine({ label, completed, total, path = "" }) {
2
- const safeTotal = Math.max(total, 1);
3
- const percentage = total === 0 ? 100 : Math.round((completed / safeTotal) * 100);
4
- const filled = Math.round(percentage / 5);
5
- const bar = `${"█".repeat(filled)}${"░".repeat(20 - filled)}`;
6
- return `${label} [${bar}] ${percentage}% (${completed}/${total})${path ? ` ${path}` : ""}`;
7
- }
8
-
9
- function createProgressReporter(options = {}) {
10
- const stream = options.stream || process.stdout;
11
- const quiet = Boolean(options.quiet);
12
- const defaultLabels = {
13
- status: "检查状态",
14
- deinit: "停用模块",
15
- init: "模块加载",
16
- };
17
- const labels = { ...defaultLabels, ...options.labels };
18
- return (event) => {
19
- if (quiet || !labels[event.phase] || event.total === 0) return;
20
- const line = formatProgressLine({
21
- ...event,
22
- label: labels[event.phase],
23
- });
24
- if (stream.isTTY) {
25
- stream.write(`\r${line}\x1b[K${event.done || event.completed >= event.total ? "\n" : ""}`);
26
- return;
27
- }
28
- if (event.completed === 0 || event.done || event.completed >= event.total) stream.write(`${line}\n`);
29
- };
30
- }
31
-
32
- function formatSwitchSummary(name, totalMs, dependencyResult, timings) {
33
- const seconds = (totalMs / 1000).toFixed(1);
34
- const dependencyLabel = dependencyResult.installed ? ",依赖已更新" : "";
35
- const summary = `\n✅ ${name}(${seconds}s${dependencyLabel})`;
36
- if (!timings) return summary;
37
- const labels = {
38
- analyze: "分析",
39
- precheck: "预检",
40
- status: "状态",
41
- deinit: "停用",
42
- init: "加载",
43
- install: "依赖",
44
- };
45
- const phases = Object.entries(labels)
46
- .filter(([phase]) => Number.isFinite(timings[phase]))
47
- .map(([phase, label]) => `${label} ${(timings[phase] / 1000).toFixed(1)}s`);
48
- return phases.length > 0 ? `${summary}\n 阶段: ${phases.join(" | ")}` : summary;
49
- }
50
-
51
- function formatSlowStatusDetails(details, thresholdMs = 2000) {
52
- return details
53
- .filter((detail) => detail.durationMs > thresholdMs)
54
- .sort((left, right) => right.durationMs - left.durationMs)
55
- .map((detail) => `${detail.path} ${detail.durationMs}ms`);
56
- }
57
-
58
- module.exports = {
59
- createProgressReporter,
60
- formatProgressLine,
61
- formatSlowStatusDetails,
62
- formatSwitchSummary,
63
- };
1
+ // CLI 与终端流程的进度呈现能力。
2
+ function formatProgressLine({ label, completed, total, path = "" }) {
3
+ const safeTotal = Math.max(total, 1);
4
+ const percentage = total === 0 ? 100 : Math.round((completed / safeTotal) * 100);
5
+ const filled = Math.round(percentage / 5);
6
+ const bar = `${"█".repeat(filled)}${"░".repeat(20 - filled)}`;
7
+ return `${label} [${bar}] ${percentage}% (${completed}/${total})${path ? ` ${path}` : ""}`;
8
+ }
9
+
10
+ function createProgressReporter(options = {}) {
11
+ const stream = options.stream || process.stdout;
12
+ const quiet = Boolean(options.quiet);
13
+ const defaultLabels = {
14
+ status: "检查状态",
15
+ deinit: "停用模块",
16
+ init: "模块加载",
17
+ };
18
+ const labels = { ...defaultLabels, ...options.labels };
19
+ return (event) => {
20
+ if (quiet || !labels[event.phase] || event.total === 0) return;
21
+ const line = formatProgressLine({
22
+ ...event,
23
+ label: labels[event.phase],
24
+ });
25
+ if (stream.isTTY) {
26
+ stream.write(`\r${line}\x1b[K${event.done || event.completed >= event.total ? "\n" : ""}`);
27
+ return;
28
+ }
29
+ if (event.completed === 0 || event.done || event.completed >= event.total) stream.write(`${line}\n`);
30
+ };
31
+ }
32
+
33
+ function formatSwitchSummary(name, totalMs, dependencyResult, timings) {
34
+ const seconds = (totalMs / 1000).toFixed(1);
35
+ const dependencyLabel = dependencyResult.installed ? ",依赖已更新" : "";
36
+ const skipWarning = dependencyResult.reason === "skipped"
37
+ ? "\n⚠️ 已跳过依赖安装,后续 dev 缺依赖时请手动 pnpm install"
38
+ : "";
39
+ const summary = `\n✅ ${name}(${seconds}s${dependencyLabel})${skipWarning}`;
40
+ if (!timings) return summary;
41
+ const labels = {
42
+ analyze: "分析",
43
+ precheck: "预检",
44
+ status: "状态",
45
+ deinit: "停用",
46
+ init: "加载",
47
+ install: "依赖",
48
+ };
49
+ const phases = Object.entries(labels)
50
+ .filter(([phase]) => Number.isFinite(timings[phase]))
51
+ .map(([phase, label]) => `${label} ${(timings[phase] / 1000).toFixed(1)}s`);
52
+ return phases.length > 0 ? `${summary}\n 阶段: ${phases.join(" | ")}` : summary;
53
+ }
54
+
55
+ function formatSlowStatusDetails(details, thresholdMs = 2000) {
56
+ return details
57
+ .filter((detail) => detail.durationMs > thresholdMs)
58
+ .sort((left, right) => right.durationMs - left.durationMs)
59
+ .map((detail) => `${detail.path} ${detail.durationMs}ms`);
60
+ }
61
+
62
+ module.exports = {
63
+ createProgressReporter,
64
+ formatProgressLine,
65
+ formatSlowStatusDetails,
66
+ formatSwitchSummary,
67
+ };