@wwkit/shared 1.0.28 → 1.0.29

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,251 +1,251 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import os from 'node:os'
4
- import process from 'node:process'
5
- import { fileURLToPath } from 'node:url'
6
- import JSON5 from 'json5'
7
-
8
- const ENV_PATTERN = /\{env:(\w+)\}/g
9
- const PKG_PATTERN = /\{pkg:([^}]+)\}/g
10
-
11
- const _selfDir = path.dirname(fileURLToPath(import.meta.url))
12
-
13
- /**
14
- * 向上查找 pnpm 工作区根目录
15
- * @param {string} startDir
16
- * @returns {string|null}
17
- */
18
- function findWorkspaceRoot(startDir) {
19
- let dir = startDir
20
- while (dir !== path.dirname(dir)) {
21
- if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir
22
- const packagesDir = path.join(dir, 'packages')
23
- if (fs.existsSync(packagesDir) && fs.existsSync(path.join(dir, 'package.json'))) {
24
- try {
25
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
26
- if (pkg.private && pkg.workspaces) return dir
27
- } catch {}
28
- }
29
- dir = path.dirname(dir)
30
- }
31
- return null
32
- }
33
-
34
- /**
35
- * 解析 npm 包的安装目录路径
36
- *
37
- * 从当前文件位置向外逐级查找 node_modules 目录,在其中定位目标包。
38
- * {pkg:XX} 的目标包必然与当前包在同一个 node_modules 空间下,
39
- * 因此只需向外找到最近的 node_modules 即可命中。
40
- * 开发环境下回退到工作区 packages/ 目录。
41
- *
42
- * @param {string} pkgName - 包名,如 @wwkit/harness
43
- * @returns {string} 目录路径,找不到时返回空串
44
- */
45
- function resolvePkgPath(pkgName) {
46
- let dir = _selfDir
47
- while (dir !== path.dirname(dir)) {
48
- const pkgJsonPath = path.join(dir, 'node_modules', pkgName, 'package.json')
49
- if (fs.existsSync(pkgJsonPath)) {
50
- try {
51
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
52
- if (pkg.name === pkgName) return path.dirname(pkgJsonPath)
53
- } catch {}
54
- }
55
- dir = path.dirname(dir)
56
- }
57
- const wsRoot = findWorkspaceRoot(_selfDir)
58
- if (wsRoot) {
59
- const seg = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName
60
- const candidate = path.join(wsRoot, 'packages', seg)
61
- const pkgJson = path.join(candidate, 'package.json')
62
- if (fs.existsSync(pkgJson)) {
63
- try {
64
- const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
65
- if (pkg.name === pkgName) return candidate
66
- } catch {}
67
- }
68
- }
69
- return ''
70
- }
71
-
72
- /**
73
- * 递归检查 builtin 是否有 user 缺少的 key(嵌套对象也递归比较)。
74
- * @param {*} builtin
75
- * @param {*} user
76
- * @returns {boolean}
77
- */
78
- function hasMissingKeys(builtin, user) {
79
- if (!builtin || typeof builtin !== 'object' || Array.isArray(builtin)) return false
80
- if (!user || typeof user !== 'object' || Array.isArray(user)) return false
81
- for (const key of Object.keys(builtin)) {
82
- if (!(key in user)) return true
83
- const bv = builtin[key]
84
- const uv = user[key]
85
- if (bv && typeof bv === 'object' && !Array.isArray(bv)
86
- && uv && typeof uv === 'object' && !Array.isArray(uv)) {
87
- if (hasMissingKeys(bv, uv)) return true
88
- }
89
- }
90
- return false
91
- }
92
-
93
- /**
94
- * 递归合并对象,override 优先,嵌套对象深合并(数组/标量直接覆盖)。
95
- */
96
- export function deepMerge(base, override) {
97
- const merged = { ...(base ?? {}) }
98
- for (const [key, value] of Object.entries(override ?? {})) {
99
- if (value && typeof value === 'object' && !Array.isArray(value)
100
- && merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
101
- merged[key] = deepMerge(merged[key], value)
102
- } else {
103
- merged[key] = value
104
- }
105
- }
106
- return merged
107
- }
108
-
109
- /**
110
- * 递归解析字符串中的 {env:XXX} 占位符,替换为环境变量值(未设置则为空串)。
111
- */
112
- export function resolveEnvVars(value, env = process.env) {
113
- if (typeof value === 'string') {
114
- return value.replace(ENV_PATTERN, (_, name) => env[name] ?? '')
115
- }
116
- if (Array.isArray(value)) {
117
- return value.map((v) => resolveEnvVars(v, env))
118
- }
119
- if (value && typeof value === 'object') {
120
- const out = {}
121
- for (const [k, v] of Object.entries(value)) out[k] = resolveEnvVars(v, env)
122
- return out
123
- }
124
- return value
125
- }
126
-
127
- /**
128
- * 递归解析字符串中的 {pkg:NAME} 占位符,替换为包安装目录(找不到则为空串)。
129
- */
130
- export function resolvePkgVars(value) {
131
- if (typeof value === 'string') {
132
- return value.replace(PKG_PATTERN, (_, name) => resolvePkgPath(name.trim()))
133
- }
134
- if (Array.isArray(value)) {
135
- return value.map((v) => resolvePkgVars(v))
136
- }
137
- if (value && typeof value === 'object') {
138
- const out = {}
139
- for (const [k, v] of Object.entries(value)) out[k] = resolvePkgVars(v)
140
- return out
141
- }
142
- return value
143
- }
144
-
145
- /**
146
- * 多层配置加载器:内置 config.json5 + 用户 config.json5 + 环境变量注入内容(优先级从低到高)。
147
- *
148
- * 默认行为(无需配置):
149
- * - 深合并(deepMerge),非浅合并
150
- * - 结果默认解析 {env:XXX} 占位符
151
- *
152
- * 可选层(通过选项开启):
153
- * - envContentVar: 指定环境变量名,其内容作为"注入配置"这一层参与合并(优先级最高,同样走深合并+占位符解析)
154
- * - envDirVar: 指定环境变量名,其值覆盖用户配置目录
155
- *
156
- * 用法:
157
- * const loader = createConfigLoader({
158
- * builtinConfigPath: path.join(__dirname, 'config.json5'),
159
- * userConfigDir: getXdgConfigDir('cft'),
160
- * })
161
- * loader.copyBuiltinConfig() // 复制内置→用户目录(不覆盖已存在)
162
- * const cfg = loader.getConfig()
163
- */
164
- export function createConfigLoader({ builtinConfigPath, userConfigDir, envContentVar = null, envDirVar = null }) {
165
- const getUserDir = () => (envDirVar ? (process.env[envDirVar] ?? userConfigDir) : userConfigDir)
166
- const getUserConfigPath = () => path.join(getUserDir(), 'config.json5')
167
-
168
- function loadJson5(filePath) {
169
- if (!filePath || !fs.existsSync(filePath)) return {}
170
- try {
171
- return JSON5.parse(fs.readFileSync(filePath, 'utf-8'))
172
- } catch {
173
- return {}
174
- }
175
- }
176
-
177
- function loadContent(content) {
178
- if (!content) return {}
179
- try {
180
- return JSON5.parse(content)
181
- } catch {
182
- return {}
183
- }
184
- }
185
-
186
- function getConfig() {
187
- const builtin = loadJson5(builtinConfigPath)
188
- const user = loadJson5(getUserConfigPath())
189
- let merged = deepMerge(builtin, user)
190
- if (envContentVar) {
191
- merged = deepMerge(merged, loadContent(process.env[envContentVar]))
192
- }
193
- return resolvePkgVars(resolveEnvVars(merged))
194
- }
195
-
196
- function copyBuiltinConfig() {
197
- const dir = getUserDir()
198
- if (!fs.existsSync(dir)) {
199
- fs.mkdirSync(dir, { recursive: true })
200
- }
201
- const userPath = getUserConfigPath()
202
- if (!fs.existsSync(userPath) && fs.existsSync(builtinConfigPath)) {
203
- fs.copyFileSync(builtinConfigPath, userPath)
204
- return true
205
- }
206
- return false
207
- }
208
-
209
- /**
210
- * 合并内置配置到用户配置文件:
211
- * 用户文件不存在 → 复制 builtin;已存在 → 追加缺失 key(不覆盖已有值)。
212
- * @returns {boolean} 是否写入了文件
213
- */
214
- function mergeBuiltinConfig() {
215
- const dir = getUserDir()
216
- if (!fs.existsSync(dir)) {
217
- fs.mkdirSync(dir, { recursive: true })
218
- }
219
- const userPath = getUserConfigPath()
220
- if (!fs.existsSync(userPath)) {
221
- if (fs.existsSync(builtinConfigPath)) {
222
- fs.copyFileSync(builtinConfigPath, userPath)
223
- return true
224
- }
225
- return false
226
- }
227
- const builtin = loadJson5(builtinConfigPath)
228
- const user = loadJson5(userPath)
229
- if (!hasMissingKeys(builtin, user)) return false
230
- const merged = deepMerge(builtin, user)
231
- fs.writeFileSync(userPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
232
- return true
233
- }
234
-
235
- return {
236
- getConfig,
237
- copyBuiltinConfig,
238
- mergeBuiltinConfig,
239
- userConfigDir: getUserDir(),
240
- userConfigPath: getUserConfigPath(),
241
- }
242
- }
243
-
244
- /**
245
- * 获取 XDG 配置目录下的子路径。
246
- * ~/.config/<subdir> 或 $XDG_CONFIG_HOME/<subdir>
247
- */
248
- export function getXdgConfigDir(subdir) {
249
- const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
250
- return path.join(xdg, subdir)
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import process from 'node:process'
5
+ import { fileURLToPath } from 'node:url'
6
+ import JSON5 from 'json5'
7
+
8
+ const ENV_PATTERN = /\{env:(\w+)\}/g
9
+ const PKG_PATTERN = /\{pkg:([^}]+)\}/g
10
+
11
+ const _selfDir = path.dirname(fileURLToPath(import.meta.url))
12
+
13
+ /**
14
+ * 向上查找 pnpm 工作区根目录
15
+ * @param {string} startDir
16
+ * @returns {string|null}
17
+ */
18
+ function findWorkspaceRoot(startDir) {
19
+ let dir = startDir
20
+ while (dir !== path.dirname(dir)) {
21
+ if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir
22
+ const packagesDir = path.join(dir, 'packages')
23
+ if (fs.existsSync(packagesDir) && fs.existsSync(path.join(dir, 'package.json'))) {
24
+ try {
25
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
26
+ if (pkg.private && pkg.workspaces) return dir
27
+ } catch {}
28
+ }
29
+ dir = path.dirname(dir)
30
+ }
31
+ return null
32
+ }
33
+
34
+ /**
35
+ * 解析 npm 包的安装目录路径
36
+ *
37
+ * 从当前文件位置向外逐级查找 node_modules 目录,在其中定位目标包。
38
+ * {pkg:XX} 的目标包必然与当前包在同一个 node_modules 空间下,
39
+ * 因此只需向外找到最近的 node_modules 即可命中。
40
+ * 开发环境下回退到工作区 packages/ 目录。
41
+ *
42
+ * @param {string} pkgName - 包名,如 @wwkit/harness
43
+ * @returns {string} 目录路径,找不到时返回空串
44
+ */
45
+ function resolvePkgPath(pkgName) {
46
+ let dir = _selfDir
47
+ while (dir !== path.dirname(dir)) {
48
+ const pkgJsonPath = path.join(dir, 'node_modules', pkgName, 'package.json')
49
+ if (fs.existsSync(pkgJsonPath)) {
50
+ try {
51
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
52
+ if (pkg.name === pkgName) return path.dirname(pkgJsonPath)
53
+ } catch {}
54
+ }
55
+ dir = path.dirname(dir)
56
+ }
57
+ const wsRoot = findWorkspaceRoot(_selfDir)
58
+ if (wsRoot) {
59
+ const seg = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName
60
+ const candidate = path.join(wsRoot, 'packages', seg)
61
+ const pkgJson = path.join(candidate, 'package.json')
62
+ if (fs.existsSync(pkgJson)) {
63
+ try {
64
+ const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
65
+ if (pkg.name === pkgName) return candidate
66
+ } catch {}
67
+ }
68
+ }
69
+ return ''
70
+ }
71
+
72
+ /**
73
+ * 递归检查 builtin 是否有 user 缺少的 key(嵌套对象也递归比较)。
74
+ * @param {*} builtin
75
+ * @param {*} user
76
+ * @returns {boolean}
77
+ */
78
+ function hasMissingKeys(builtin, user) {
79
+ if (!builtin || typeof builtin !== 'object' || Array.isArray(builtin)) return false
80
+ if (!user || typeof user !== 'object' || Array.isArray(user)) return false
81
+ for (const key of Object.keys(builtin)) {
82
+ if (!(key in user)) return true
83
+ const bv = builtin[key]
84
+ const uv = user[key]
85
+ if (bv && typeof bv === 'object' && !Array.isArray(bv)
86
+ && uv && typeof uv === 'object' && !Array.isArray(uv)) {
87
+ if (hasMissingKeys(bv, uv)) return true
88
+ }
89
+ }
90
+ return false
91
+ }
92
+
93
+ /**
94
+ * 递归合并对象,override 优先,嵌套对象深合并(数组/标量直接覆盖)。
95
+ */
96
+ export function deepMerge(base, override) {
97
+ const merged = { ...(base ?? {}) }
98
+ for (const [key, value] of Object.entries(override ?? {})) {
99
+ if (value && typeof value === 'object' && !Array.isArray(value)
100
+ && merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
101
+ merged[key] = deepMerge(merged[key], value)
102
+ } else {
103
+ merged[key] = value
104
+ }
105
+ }
106
+ return merged
107
+ }
108
+
109
+ /**
110
+ * 递归解析字符串中的 {env:XXX} 占位符,替换为环境变量值(未设置则为空串)。
111
+ */
112
+ export function resolveEnvVars(value, env = process.env) {
113
+ if (typeof value === 'string') {
114
+ return value.replace(ENV_PATTERN, (_, name) => env[name] ?? '')
115
+ }
116
+ if (Array.isArray(value)) {
117
+ return value.map((v) => resolveEnvVars(v, env))
118
+ }
119
+ if (value && typeof value === 'object') {
120
+ const out = {}
121
+ for (const [k, v] of Object.entries(value)) out[k] = resolveEnvVars(v, env)
122
+ return out
123
+ }
124
+ return value
125
+ }
126
+
127
+ /**
128
+ * 递归解析字符串中的 {pkg:NAME} 占位符,替换为包安装目录(找不到则为空串)。
129
+ */
130
+ export function resolvePkgVars(value) {
131
+ if (typeof value === 'string') {
132
+ return value.replace(PKG_PATTERN, (_, name) => resolvePkgPath(name.trim()))
133
+ }
134
+ if (Array.isArray(value)) {
135
+ return value.map((v) => resolvePkgVars(v))
136
+ }
137
+ if (value && typeof value === 'object') {
138
+ const out = {}
139
+ for (const [k, v] of Object.entries(value)) out[k] = resolvePkgVars(v)
140
+ return out
141
+ }
142
+ return value
143
+ }
144
+
145
+ /**
146
+ * 多层配置加载器:内置 config.json5 + 用户 config.json5 + 环境变量注入内容(优先级从低到高)。
147
+ *
148
+ * 默认行为(无需配置):
149
+ * - 深合并(deepMerge),非浅合并
150
+ * - 结果默认解析 {env:XXX} 占位符
151
+ *
152
+ * 可选层(通过选项开启):
153
+ * - envContentVar: 指定环境变量名,其内容作为"注入配置"这一层参与合并(优先级最高,同样走深合并+占位符解析)
154
+ * - envDirVar: 指定环境变量名,其值覆盖用户配置目录
155
+ *
156
+ * 用法:
157
+ * const loader = createConfigLoader({
158
+ * builtinConfigPath: path.join(__dirname, 'config.json5'),
159
+ * userConfigDir: getXdgConfigDir('cft'),
160
+ * })
161
+ * loader.copyBuiltinConfig() // 复制内置→用户目录(不覆盖已存在)
162
+ * const cfg = loader.getConfig()
163
+ */
164
+ export function createConfigLoader({ builtinConfigPath, userConfigDir, envContentVar = null, envDirVar = null }) {
165
+ const getUserDir = () => (envDirVar ? (process.env[envDirVar] ?? userConfigDir) : userConfigDir)
166
+ const getUserConfigPath = () => path.join(getUserDir(), 'config.json5')
167
+
168
+ function loadJson5(filePath) {
169
+ if (!filePath || !fs.existsSync(filePath)) return {}
170
+ try {
171
+ return JSON5.parse(fs.readFileSync(filePath, 'utf-8'))
172
+ } catch {
173
+ return {}
174
+ }
175
+ }
176
+
177
+ function loadContent(content) {
178
+ if (!content) return {}
179
+ try {
180
+ return JSON5.parse(content)
181
+ } catch {
182
+ return {}
183
+ }
184
+ }
185
+
186
+ function getConfig() {
187
+ const builtin = loadJson5(builtinConfigPath)
188
+ const user = loadJson5(getUserConfigPath())
189
+ let merged = deepMerge(builtin, user)
190
+ if (envContentVar) {
191
+ merged = deepMerge(merged, loadContent(process.env[envContentVar]))
192
+ }
193
+ return resolvePkgVars(resolveEnvVars(merged))
194
+ }
195
+
196
+ function copyBuiltinConfig() {
197
+ const dir = getUserDir()
198
+ if (!fs.existsSync(dir)) {
199
+ fs.mkdirSync(dir, { recursive: true })
200
+ }
201
+ const userPath = getUserConfigPath()
202
+ if (!fs.existsSync(userPath) && fs.existsSync(builtinConfigPath)) {
203
+ fs.copyFileSync(builtinConfigPath, userPath)
204
+ return true
205
+ }
206
+ return false
207
+ }
208
+
209
+ /**
210
+ * 合并内置配置到用户配置文件:
211
+ * 用户文件不存在 → 复制 builtin;已存在 → 追加缺失 key(不覆盖已有值)。
212
+ * @returns {boolean} 是否写入了文件
213
+ */
214
+ function mergeBuiltinConfig() {
215
+ const dir = getUserDir()
216
+ if (!fs.existsSync(dir)) {
217
+ fs.mkdirSync(dir, { recursive: true })
218
+ }
219
+ const userPath = getUserConfigPath()
220
+ if (!fs.existsSync(userPath)) {
221
+ if (fs.existsSync(builtinConfigPath)) {
222
+ fs.copyFileSync(builtinConfigPath, userPath)
223
+ return true
224
+ }
225
+ return false
226
+ }
227
+ const builtin = loadJson5(builtinConfigPath)
228
+ const user = loadJson5(userPath)
229
+ if (!hasMissingKeys(builtin, user)) return false
230
+ const merged = deepMerge(builtin, user)
231
+ fs.writeFileSync(userPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
232
+ return true
233
+ }
234
+
235
+ return {
236
+ getConfig,
237
+ copyBuiltinConfig,
238
+ mergeBuiltinConfig,
239
+ userConfigDir: getUserDir(),
240
+ userConfigPath: getUserConfigPath(),
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 获取 XDG 配置目录下的子路径。
246
+ * ~/.config/<subdir> 或 $XDG_CONFIG_HOME/<subdir>
247
+ */
248
+ export function getXdgConfigDir(subdir) {
249
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
250
+ return path.join(xdg, subdir)
251
251
  }