@infly/libs 2.0.53 → 2.1.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.
@@ -1,46 +1,77 @@
1
1
  // CLI 交互选择适配,业务选项由调用方注入。
2
2
  const EXIT_SELECTION = "__exit__";
3
-
4
- function isReadlineClosedError(error) {
5
- return error?.code === "ERR_USE_AFTER_CLOSE";
6
- }
7
-
8
- function makePromptCancellationSafe(prompt) {
9
- const unsafeStop = prompt.stop;
10
- if (typeof unsafeStop !== "function") return;
11
-
12
- prompt.removeListener("close", unsafeStop);
13
- const safeStop = () => {
14
- try {
15
- unsafeStop();
16
- } catch (error) {
17
- if (!isReadlineClosedError(error)) throw error;
18
- }
19
- };
20
- prompt.stop = safeStop;
21
- prompt.once("close", safeStop);
3
+
4
+ function isReadlineClosedError(error) {
5
+ return error?.code === "ERR_USE_AFTER_CLOSE";
6
+ }
7
+
8
+ function makePromptCancellationSafe(prompt) {
9
+ const unsafeStop = prompt.stop;
10
+ if (typeof unsafeStop !== "function") return;
11
+
12
+ prompt.removeListener("close", unsafeStop);
13
+ const safeStop = () => {
14
+ try {
15
+ unsafeStop();
16
+ } catch (error) {
17
+ if (!isReadlineClosedError(error)) throw error;
18
+ }
19
+ };
20
+ prompt.stop = safeStop;
21
+ prompt.once("close", safeStop);
22
+ }
23
+
24
+ async function selectOption(message, choices, dependencies = {}) {
25
+ const stdin = dependencies.stdin || process.stdin;
26
+ const stdout = dependencies.stdout || process.stdout;
27
+ if (!stdin.isTTY || !stdout.isTTY) {
28
+ throw new Error(`${message}: interactive selection requires a TTY; pass an explicit option.`);
29
+ }
30
+
31
+ const Select = dependencies.Select || require("enquirer").Select;
32
+ const prompt = new Select({
33
+ name: "selection",
34
+ message,
35
+ stdin,
36
+ stdout,
37
+ choices: [
38
+ ...choices.map((choice) => ({
39
+ name: choice.value,
40
+ message: choice.label,
41
+ })),
42
+ { name: EXIT_SELECTION, message: "退出" },
43
+ ],
44
+ });
45
+
46
+ prompt.once("start", makePromptCancellationSafe);
47
+ try {
48
+ return await prompt.run();
49
+ } catch (error) {
50
+ if (!error || error.name === "CancelPromptError" || isReadlineClosedError(error)) {
51
+ return EXIT_SELECTION;
52
+ }
53
+ throw error;
54
+ }
22
55
  }
23
56
 
24
- async function selectOption(message, choices, dependencies = {}) {
57
+ async function selectOptions(message, choices, dependencies = {}) {
25
58
  const stdin = dependencies.stdin || process.stdin;
26
59
  const stdout = dependencies.stdout || process.stdout;
27
60
  if (!stdin.isTTY || !stdout.isTTY) {
28
61
  throw new Error(`${message}: interactive selection requires a TTY; pass an explicit option.`);
29
62
  }
30
63
 
31
- const Select = dependencies.Select || require("enquirer").Select;
32
- const prompt = new Select({
33
- name: "selection",
64
+ const MultiSelect = dependencies.MultiSelect || require("enquirer").MultiSelect;
65
+ const prompt = new MultiSelect({
66
+ name: "selections",
34
67
  message,
35
68
  stdin,
36
69
  stdout,
37
- choices: [
38
- ...choices.map((choice) => ({
39
- name: choice.value,
40
- message: choice.label,
41
- })),
42
- { name: EXIT_SELECTION, message: "退出" },
43
- ],
70
+ min: 1,
71
+ choices: choices.map((choice) => ({
72
+ name: choice.value,
73
+ message: choice.label,
74
+ })),
44
75
  });
45
76
 
46
77
  prompt.once("start", makePromptCancellationSafe);
@@ -58,4 +89,5 @@ module.exports = {
58
89
  EXIT_SELECTION,
59
90
  makePromptCancellationSafe,
60
91
  selectOption,
92
+ selectOptions,
61
93
  };
@@ -1,31 +1,31 @@
1
1
  /**
2
2
  * 框架无关的异步并发控制。
3
- * 异步并发限流器。
4
- * 对 items 数组中的每一项执行 worker,同时最多运行 concurrency 个 worker。
5
- *
6
- * @param {Array} items - 待处理项
7
- * @param {number} concurrency - 最大并发数
8
- * @param {Function} worker - 处理函数 (item, index) => Promise
9
- * @returns {Promise<Array>} 结果数组,索引与 items 一一对应
10
- */
11
- async function runWithConcurrency(items, concurrency, worker) {
12
- if (!Number.isInteger(concurrency) || concurrency < 1) {
13
- throw new TypeError("concurrency must be a positive integer");
14
- }
15
-
16
- const results = new Array(items.length);
17
- let nextIndex = 0;
18
- const runWorker = async () => {
19
- while (nextIndex < items.length) {
20
- const index = nextIndex;
21
- nextIndex += 1;
22
- results[index] = await worker(items[index], index);
23
- }
24
- };
25
-
26
- const workerCount = Math.min(concurrency, items.length);
27
- await Promise.all(Array.from({ length: workerCount }, runWorker));
28
- return results;
29
- }
30
-
31
- module.exports = { runWithConcurrency };
3
+ * 异步并发限流器。
4
+ * 对 items 数组中的每一项执行 worker,同时最多运行 concurrency 个 worker。
5
+ *
6
+ * @param {Array} items - 待处理项
7
+ * @param {number} concurrency - 最大并发数
8
+ * @param {Function} worker - 处理函数 (item, index) => Promise
9
+ * @returns {Promise<Array>} 结果数组,索引与 items 一一对应
10
+ */
11
+ async function runWithConcurrency(items, concurrency, worker) {
12
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
13
+ throw new TypeError("concurrency must be a positive integer");
14
+ }
15
+
16
+ const results = new Array(items.length);
17
+ let nextIndex = 0;
18
+ const runWorker = async () => {
19
+ while (nextIndex < items.length) {
20
+ const index = nextIndex;
21
+ nextIndex += 1;
22
+ results[index] = await worker(items[index], index);
23
+ }
24
+ };
25
+
26
+ const workerCount = Math.min(concurrency, items.length);
27
+ await Promise.all(Array.from({ length: workerCount }, runWorker));
28
+ return results;
29
+ }
30
+
31
+ module.exports = { runWithConcurrency };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infly/libs",
3
- "version": "2.0.53",
3
+ "version": "2.1.0",
4
4
  "description": "不受前端框架限制的独立工具库",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -59,7 +59,7 @@
59
59
  "author": "Kahal",
60
60
  "license": "ISC",
61
61
  "dependencies": {
62
- "@infly/ts-libs": "^0.1.12",
62
+ "@infly/ts-libs": "^0.1.15",
63
63
  "connect": "^3.7.0",
64
64
  "dotenv": "16.6.1",
65
65
  "enquirer": "^2.4.1",
@@ -3,20 +3,20 @@ const fs = require("fs");
3
3
  function normalizeRelativePath(value) {
4
4
  return String(value).replace(/\\/g, "/");
5
5
  }
6
-
7
- /**
8
- * 从 .gitmodules 中解析所有子模块路径
9
- */
10
- function parseGitmodulesPaths(rootDir) {
11
- const p = require("path").join(rootDir, ".gitmodules");
12
- if (!fs.existsSync(p)) return [];
13
- const content = fs.readFileSync(p, "utf8");
14
- const paths = [];
15
- content.split(/\[submodule/).forEach((block) => {
16
- const m = block.match(/path\s*=\s*(.+)/);
17
- if (m) paths.push(m[1].trim());
18
- });
19
- return paths;
20
- }
21
-
6
+
7
+ /**
8
+ * 从 .gitmodules 中解析所有子模块路径
9
+ */
10
+ function parseGitmodulesPaths(rootDir) {
11
+ const p = require("path").join(rootDir, ".gitmodules");
12
+ if (!fs.existsSync(p)) return [];
13
+ const content = fs.readFileSync(p, "utf8");
14
+ const paths = [];
15
+ content.split(/\[submodule/).forEach((block) => {
16
+ const m = block.match(/path\s*=\s*(.+)/);
17
+ if (m) paths.push(m[1].trim());
18
+ });
19
+ return paths;
20
+ }
21
+
22
22
  module.exports = { normalizeRelativePath, parseGitmodulesPaths };
@@ -1,146 +1,146 @@
1
1
  // workspace 配置读取与模式展开。
2
2
  const fs = require("fs");
3
- const path = require("path");
4
- const YAML = require("yaml");
5
-
6
- function readJson(filePath) {
7
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
8
- }
9
-
10
- function readWorkspaceList(content, sectionName) {
11
- let workspaceConfig;
12
-
13
- try {
14
- workspaceConfig = YAML.parse(content);
15
- } catch (error) {
16
- throw new Error(`无法解析 pnpm-workspace.yaml: ${error.message}`, { cause: error });
17
- }
18
-
19
- if (workspaceConfig == null) {
20
- return [];
21
- }
22
-
23
- if (typeof workspaceConfig !== "object" || Array.isArray(workspaceConfig)) {
24
- throw new TypeError("pnpm-workspace.yaml 的根节点必须是对象");
25
- }
26
-
27
- const patterns = workspaceConfig[sectionName];
28
-
29
- if (patterns == null) {
30
- return [];
31
- }
32
-
33
- if (
34
- !Array.isArray(patterns)
35
- || patterns.some((pattern) => typeof pattern !== "string" || pattern.trim().length === 0)
36
- ) {
37
- throw new TypeError(`pnpm-workspace.yaml 的 ${sectionName} 必须是非空字符串数组`);
38
- }
39
-
40
- return patterns;
41
- }
42
-
43
- function readWorkspaceSection(workspaceFile, sectionName) {
44
- return readWorkspaceList(fs.readFileSync(workspaceFile, "utf8"), sectionName);
45
- }
46
-
47
- function readWorkspaceSectionWithFallback(workspaceFile, sectionName, fallbackSectionName) {
48
- const patterns = readWorkspaceSection(workspaceFile, sectionName);
49
-
50
- if (patterns.length > 0) {
51
- return patterns;
52
- }
53
-
54
- return readWorkspaceSection(workspaceFile, fallbackSectionName);
55
- }
56
-
57
- function expandWorkspacePattern(rootDir, pattern) {
58
- const normalizedPattern = pattern.replace(/\\/g, "/");
59
-
60
- if (!normalizedPattern.includes("*")) {
61
- return [path.join(rootDir, normalizedPattern)];
62
- }
63
-
64
- const starIndex = normalizedPattern.indexOf("*");
65
- const basePart = normalizedPattern.slice(0, starIndex);
66
- const suffixPart = normalizedPattern.slice(starIndex + 1);
67
- const baseDir = path.join(rootDir, basePart);
68
-
69
- if (!fs.existsSync(baseDir)) {
70
- return [];
71
- }
72
-
73
- const suffixIsNamePattern = suffixPart && !suffixPart.startsWith("/");
74
-
75
- return fs
76
- .readdirSync(baseDir, { withFileTypes: true })
77
- .filter((entry) => entry.isDirectory())
78
- .filter((entry) => !suffixIsNamePattern || entry.name.endsWith(suffixPart))
79
- .map((entry) => (suffixIsNamePattern
80
- ? path.join(baseDir, entry.name)
81
- : path.join(baseDir, entry.name, suffixPart)))
82
- .filter((packageDir) => fs.existsSync(path.join(packageDir, "package.json")));
83
- }
84
-
85
- function getWorkspacePackages(rootDir, workspaceFile, sectionName, fallbackSectionName, scriptName) {
86
- const patterns = fallbackSectionName
87
- ? readWorkspaceSectionWithFallback(workspaceFile, sectionName, fallbackSectionName)
88
- : readWorkspaceSection(workspaceFile, sectionName);
89
- const included = new Set();
90
- patterns.forEach((pattern) => {
91
- const excluded = pattern.startsWith("!");
92
- const resolvedPattern = excluded ? pattern.slice(1) : pattern;
93
- expandWorkspacePattern(rootDir, resolvedPattern).forEach((packageDir) => {
94
- const resolvedDir = path.resolve(packageDir);
95
- if (excluded) included.delete(resolvedDir);
96
- else included.add(resolvedDir);
97
- });
98
- });
99
- const packageDirs = [...included];
100
- const seen = new Set();
101
-
102
- return packageDirs
103
- .map((packageDir) => {
104
- const packageJsonPath = path.join(packageDir, "package.json");
105
-
106
- if (!fs.existsSync(packageJsonPath)) {
107
- return null;
108
- }
109
-
110
- const packageJson = readJson(packageJsonPath);
111
- const scripts = packageJson.scripts || {};
112
-
113
- if (!packageJson.name || seen.has(packageJson.name) || (scriptName && !scripts[scriptName])) {
114
- return null;
115
- }
116
-
117
- seen.add(packageJson.name);
118
-
119
- return {
120
- name: packageJson.name,
121
- dir: path.relative(rootDir, packageDir),
122
- packageJson,
123
- };
124
- })
125
- .filter(Boolean);
126
- }
127
-
128
- function toTurboFilter(value) {
129
- const normalized = value.replace(/\\/g, "/");
130
-
131
- if (normalized.includes("/") && !normalized.startsWith("./") && !normalized.startsWith("../")) {
132
- return `./${normalized}`;
133
- }
134
-
135
- return normalized;
136
- }
137
-
138
- module.exports = {
139
- expandWorkspacePattern,
140
- getWorkspacePackages,
141
- readJson,
142
- readWorkspaceList,
143
- readWorkspaceSection,
144
- readWorkspaceSectionWithFallback,
145
- toTurboFilter,
146
- };
3
+ const path = require("path");
4
+ const YAML = require("yaml");
5
+
6
+ function readJson(filePath) {
7
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
8
+ }
9
+
10
+ function readWorkspaceList(content, sectionName) {
11
+ let workspaceConfig;
12
+
13
+ try {
14
+ workspaceConfig = YAML.parse(content);
15
+ } catch (error) {
16
+ throw new Error(`无法解析 pnpm-workspace.yaml: ${error.message}`, { cause: error });
17
+ }
18
+
19
+ if (workspaceConfig == null) {
20
+ return [];
21
+ }
22
+
23
+ if (typeof workspaceConfig !== "object" || Array.isArray(workspaceConfig)) {
24
+ throw new TypeError("pnpm-workspace.yaml 的根节点必须是对象");
25
+ }
26
+
27
+ const patterns = workspaceConfig[sectionName];
28
+
29
+ if (patterns == null) {
30
+ return [];
31
+ }
32
+
33
+ if (
34
+ !Array.isArray(patterns)
35
+ || patterns.some((pattern) => typeof pattern !== "string" || pattern.trim().length === 0)
36
+ ) {
37
+ throw new TypeError(`pnpm-workspace.yaml 的 ${sectionName} 必须是非空字符串数组`);
38
+ }
39
+
40
+ return patterns;
41
+ }
42
+
43
+ function readWorkspaceSection(workspaceFile, sectionName) {
44
+ return readWorkspaceList(fs.readFileSync(workspaceFile, "utf8"), sectionName);
45
+ }
46
+
47
+ function readWorkspaceSectionWithFallback(workspaceFile, sectionName, fallbackSectionName) {
48
+ const patterns = readWorkspaceSection(workspaceFile, sectionName);
49
+
50
+ if (patterns.length > 0) {
51
+ return patterns;
52
+ }
53
+
54
+ return readWorkspaceSection(workspaceFile, fallbackSectionName);
55
+ }
56
+
57
+ function expandWorkspacePattern(rootDir, pattern) {
58
+ const normalizedPattern = pattern.replace(/\\/g, "/");
59
+
60
+ if (!normalizedPattern.includes("*")) {
61
+ return [path.join(rootDir, normalizedPattern)];
62
+ }
63
+
64
+ const starIndex = normalizedPattern.indexOf("*");
65
+ const basePart = normalizedPattern.slice(0, starIndex);
66
+ const suffixPart = normalizedPattern.slice(starIndex + 1);
67
+ const baseDir = path.join(rootDir, basePart);
68
+
69
+ if (!fs.existsSync(baseDir)) {
70
+ return [];
71
+ }
72
+
73
+ const suffixIsNamePattern = suffixPart && !suffixPart.startsWith("/");
74
+
75
+ return fs
76
+ .readdirSync(baseDir, { withFileTypes: true })
77
+ .filter((entry) => entry.isDirectory())
78
+ .filter((entry) => !suffixIsNamePattern || entry.name.endsWith(suffixPart))
79
+ .map((entry) => (suffixIsNamePattern
80
+ ? path.join(baseDir, entry.name)
81
+ : path.join(baseDir, entry.name, suffixPart)))
82
+ .filter((packageDir) => fs.existsSync(path.join(packageDir, "package.json")));
83
+ }
84
+
85
+ function getWorkspacePackages(rootDir, workspaceFile, sectionName, fallbackSectionName, scriptName) {
86
+ const patterns = fallbackSectionName
87
+ ? readWorkspaceSectionWithFallback(workspaceFile, sectionName, fallbackSectionName)
88
+ : readWorkspaceSection(workspaceFile, sectionName);
89
+ const included = new Set();
90
+ patterns.forEach((pattern) => {
91
+ const excluded = pattern.startsWith("!");
92
+ const resolvedPattern = excluded ? pattern.slice(1) : pattern;
93
+ expandWorkspacePattern(rootDir, resolvedPattern).forEach((packageDir) => {
94
+ const resolvedDir = path.resolve(packageDir);
95
+ if (excluded) included.delete(resolvedDir);
96
+ else included.add(resolvedDir);
97
+ });
98
+ });
99
+ const packageDirs = [...included];
100
+ const seen = new Set();
101
+
102
+ return packageDirs
103
+ .map((packageDir) => {
104
+ const packageJsonPath = path.join(packageDir, "package.json");
105
+
106
+ if (!fs.existsSync(packageJsonPath)) {
107
+ return null;
108
+ }
109
+
110
+ const packageJson = readJson(packageJsonPath);
111
+ const scripts = packageJson.scripts || {};
112
+
113
+ if (!packageJson.name || seen.has(packageJson.name) || (scriptName && !scripts[scriptName])) {
114
+ return null;
115
+ }
116
+
117
+ seen.add(packageJson.name);
118
+
119
+ return {
120
+ name: packageJson.name,
121
+ dir: path.relative(rootDir, packageDir),
122
+ packageJson,
123
+ };
124
+ })
125
+ .filter(Boolean);
126
+ }
127
+
128
+ function toTurboFilter(value) {
129
+ const normalized = value.replace(/\\/g, "/");
130
+
131
+ if (normalized.includes("/") && !normalized.startsWith("./") && !normalized.startsWith("../")) {
132
+ return `./${normalized}`;
133
+ }
134
+
135
+ return normalized;
136
+ }
137
+
138
+ module.exports = {
139
+ expandWorkspacePattern,
140
+ getWorkspacePackages,
141
+ readJson,
142
+ readWorkspaceList,
143
+ readWorkspaceSection,
144
+ readWorkspaceSectionWithFallback,
145
+ toTurboFilter,
146
+ };