@wwkit/shared 1.0.4 → 1.0.6
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.
- package/package.json +1 -1
- package/src/config-loader.js +87 -2
- package/src/index.js +2 -1
- package/src/release.js +6 -6
- package/src/shell.js +164 -0
package/package.json
CHANGED
package/src/config-loader.js
CHANGED
|
@@ -2,9 +2,76 @@ import fs from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import process from 'node:process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
5
7
|
import JSON5 from 'json5'
|
|
6
8
|
|
|
7
|
-
const ENV_PATTERN = /\{env:(\w+)\}/
|
|
9
|
+
const ENV_PATTERN = /\{env:(\w+)\}/g
|
|
10
|
+
const PKG_PATTERN = /\{pkg:([^}]+)\}/g
|
|
11
|
+
|
|
12
|
+
const _require = createRequire(import.meta.url)
|
|
13
|
+
const _selfDir = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 向上查找 pnpm 工作区根目录
|
|
17
|
+
* @param {string} startDir
|
|
18
|
+
* @returns {string|null}
|
|
19
|
+
*/
|
|
20
|
+
function findWorkspaceRoot(startDir) {
|
|
21
|
+
let dir = startDir
|
|
22
|
+
while (dir !== path.dirname(dir)) {
|
|
23
|
+
if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return dir
|
|
24
|
+
const packagesDir = path.join(dir, 'packages')
|
|
25
|
+
if (fs.existsSync(packagesDir) && fs.existsSync(path.join(dir, 'package.json'))) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
28
|
+
if (pkg.private && pkg.workspaces) return dir
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
dir = path.dirname(dir)
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析 npm 包的安装目录路径
|
|
38
|
+
* 策略:1) require.resolve(已发布/已安装) 2) 工作区 packages/ 回退
|
|
39
|
+
* @param {string} pkgName - 包名,如 @wwkit/harness
|
|
40
|
+
* @returns {string} 目录路径,找不到时返回空串
|
|
41
|
+
*/
|
|
42
|
+
function resolvePkgPath(pkgName) {
|
|
43
|
+
try {
|
|
44
|
+
const pkgJsonPath = _require.resolve(`${pkgName}/package.json`)
|
|
45
|
+
return path.dirname(pkgJsonPath)
|
|
46
|
+
} catch {}
|
|
47
|
+
try {
|
|
48
|
+
const entry = _require.resolve(pkgName)
|
|
49
|
+
let dir = path.dirname(entry)
|
|
50
|
+
while (dir !== path.dirname(dir)) {
|
|
51
|
+
const pkgJson = path.join(dir, 'package.json')
|
|
52
|
+
if (fs.existsSync(pkgJson)) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
|
|
55
|
+
if (pkg.name === pkgName) return dir
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
dir = path.dirname(dir)
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
const wsRoot = findWorkspaceRoot(_selfDir)
|
|
62
|
+
if (wsRoot) {
|
|
63
|
+
const seg = pkgName.includes('/') ? pkgName.split('/').pop() : pkgName
|
|
64
|
+
const candidate = path.join(wsRoot, 'packages', seg)
|
|
65
|
+
const pkgJson = path.join(candidate, 'package.json')
|
|
66
|
+
if (fs.existsSync(pkgJson)) {
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, 'utf8'))
|
|
69
|
+
if (pkg.name === pkgName) return candidate
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return ''
|
|
74
|
+
}
|
|
8
75
|
|
|
9
76
|
/**
|
|
10
77
|
* 递归合并对象,override 优先,嵌套对象深合并(数组/标量直接覆盖)。
|
|
@@ -40,6 +107,24 @@ export function resolveEnvVars(value, env = process.env) {
|
|
|
40
107
|
return value
|
|
41
108
|
}
|
|
42
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 递归解析字符串中的 {pkg:NAME} 占位符,替换为包安装目录(找不到则为空串)。
|
|
112
|
+
*/
|
|
113
|
+
export function resolvePkgVars(value) {
|
|
114
|
+
if (typeof value === 'string') {
|
|
115
|
+
return value.replace(PKG_PATTERN, (_, name) => resolvePkgPath(name.trim()))
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return value.map((v) => resolvePkgVars(v))
|
|
119
|
+
}
|
|
120
|
+
if (value && typeof value === 'object') {
|
|
121
|
+
const out = {}
|
|
122
|
+
for (const [k, v] of Object.entries(value)) out[k] = resolvePkgVars(v)
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
43
128
|
/**
|
|
44
129
|
* 多层配置加载器:内置 config.json5 + 用户 config.json5 + 环境变量注入内容(优先级从低到高)。
|
|
45
130
|
*
|
|
@@ -88,7 +173,7 @@ export function createConfigLoader({ builtinConfigPath, userConfigDir, envConten
|
|
|
88
173
|
if (envContentVar) {
|
|
89
174
|
merged = deepMerge(merged, loadContent(process.env[envContentVar]))
|
|
90
175
|
}
|
|
91
|
-
return resolveEnvVars(merged)
|
|
176
|
+
return resolvePkgVars(resolveEnvVars(merged))
|
|
92
177
|
}
|
|
93
178
|
|
|
94
179
|
function copyBuiltinConfig() {
|
package/src/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { createConfigLoader, getXdgConfigDir, deepMerge, resolveEnvVars } from './config-loader.js'
|
|
1
|
+
export { createConfigLoader, getXdgConfigDir, deepMerge, resolveEnvVars, resolvePkgVars } from './config-loader.js'
|
|
2
|
+
export { Shell } from './shell.js'
|
package/src/release.js
CHANGED
|
@@ -41,16 +41,16 @@ function resolvePublishRegistry(pkgDir) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// 发布前检查: git
|
|
44
|
+
// 发布前检查: git 工作区干净(不干净仅告警)+ npm 已登录(未登录抛出 Error)。
|
|
45
45
|
// pkgDir 给定时,针对该包 publishConfig.registry 检查登录(而非 .npmrc 里可能指向镜像的 registry)。
|
|
46
46
|
export function preflight({ dryRun = false, pkgDir = null } = {}) {
|
|
47
47
|
const gitStatus = runQuiet('git status --porcelain')
|
|
48
48
|
if (gitStatus) {
|
|
49
|
-
console.
|
|
50
|
-
console.
|
|
51
|
-
|
|
49
|
+
console.warn('⚠ Git 工作区不干净(仅告警,继续发布)')
|
|
50
|
+
console.warn(gitStatus)
|
|
51
|
+
} else {
|
|
52
|
+
console.log(' ✓ Git 工作区干净')
|
|
52
53
|
}
|
|
53
|
-
console.log(' ✓ Git 工作区干净')
|
|
54
54
|
const registry = pkgDir ? resolvePublishRegistry(pkgDir) : null
|
|
55
55
|
const whoamiCmd = registry ? `npm whoami --registry=${registry}` : 'npm whoami'
|
|
56
56
|
try {
|
|
@@ -75,7 +75,7 @@ export function releasePackage({ pkgDir, dryRun = false } = {}) {
|
|
|
75
75
|
console.log(`━━━ 发布 ${name}@${newVer}(原 ${oldVer})━━━`)
|
|
76
76
|
|
|
77
77
|
// 1. bump version (no git tag — scoped tag created manually below)
|
|
78
|
-
run('pnpm version patch --no-git-tag-version', { cwd: pkgDir, dryRun })
|
|
78
|
+
run('pnpm version patch --no-git-tag-version --no-git-checks', { cwd: pkgDir, dryRun })
|
|
79
79
|
|
|
80
80
|
// 2. publish (prepublishOnly hook runs tests automatically)
|
|
81
81
|
run('pnpm publish --no-git-checks', { cwd: pkgDir, dryRun })
|
package/src/shell.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台 shell / 系统环境处理
|
|
3
|
+
*
|
|
4
|
+
* 封装 Windows / macOS / Linux 的 shell 与系统环境差异,供 @wwkit 各包复用:
|
|
5
|
+
* - 平台检测(isWindows / isMac / isLinux)
|
|
6
|
+
* - bash 解析(nvm / phpenv 等 bash 工具需要;Windows 下找 Git Bash / WSL)
|
|
7
|
+
* - execSync / spawnSync 的 shell 选项选择
|
|
8
|
+
* - shell profile 文件路径检测与写入(.zshrc / .bashrc / .profile)
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* import { Shell } from '@wwkit/shared'
|
|
12
|
+
* const shell = new Shell()
|
|
13
|
+
* execSync(cmd, { shell: shell.shellForExec(true) })
|
|
14
|
+
* shell.writeProfile('NVM_DIR', 'nvm', ['export NVM_DIR=...'])
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import os from 'node:os'
|
|
19
|
+
import path from 'node:path'
|
|
20
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
21
|
+
|
|
22
|
+
export class Shell {
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ platform?: string, env?: Record<string,string> }} [opts]
|
|
25
|
+
* platform / env 注入用于测试;默认取 process.platform / process.env
|
|
26
|
+
*/
|
|
27
|
+
constructor({ platform = process.platform, env = process.env } = {}) {
|
|
28
|
+
this.platform = platform
|
|
29
|
+
this.env = env
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 是否为 Windows */
|
|
33
|
+
get isWindows() {
|
|
34
|
+
return this.platform === 'win32'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 是否为 macOS */
|
|
38
|
+
get isMac() {
|
|
39
|
+
return this.platform === 'darwin'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 是否为 Linux */
|
|
43
|
+
get isLinux() {
|
|
44
|
+
return this.platform === 'linux'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 解析可用的 bash 路径
|
|
49
|
+
*
|
|
50
|
+
* Unix: 优先 /bin/bash(原行为),回退 /usr/bin/bash,再回退 PATH 中的 bash
|
|
51
|
+
* Windows: 在 PATH 中查找 bash(Git Bash / WSL 提供);找不到则抛错
|
|
52
|
+
*
|
|
53
|
+
* @returns {string} bash 可执行文件路径
|
|
54
|
+
* @throws {Error} 找不到 bash 时抛出(含平台相关提示)
|
|
55
|
+
*/
|
|
56
|
+
resolveBash() {
|
|
57
|
+
if (!this.isWindows) {
|
|
58
|
+
for (const c of ['/bin/bash', '/usr/bin/bash']) {
|
|
59
|
+
try {
|
|
60
|
+
fs.accessSync(c, fs.constants.X_OK)
|
|
61
|
+
return c
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const cmd = this.isWindows ? 'where bash' : 'command -v bash'
|
|
68
|
+
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
|
69
|
+
if (out) return out.split(/\r?\n/)[0]
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
'bash not found: nvm/phpenv require bash. ' +
|
|
74
|
+
(this.isWindows ? 'Install Git Bash or run inside WSL.' : 'Install bash.')
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 返回 execSync / spawnSync 的 shell 选项
|
|
80
|
+
*
|
|
81
|
+
* needsBash=true → 解析并返回 bash 路径(nvm/phpenv 等 bash 工具)
|
|
82
|
+
* needsBash=false → 返回 true(使用系统默认 shell:Unix /bin/sh,Windows cmd.exe)
|
|
83
|
+
*
|
|
84
|
+
* @param {boolean} needsBash - 命令是否需要 bash
|
|
85
|
+
* @returns {string|boolean}
|
|
86
|
+
*/
|
|
87
|
+
shellForExec(needsBash) {
|
|
88
|
+
return needsBash ? this.resolveBash() : true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 检测当前 shell 的 profile 文件路径
|
|
93
|
+
*
|
|
94
|
+
* Unix: $SHELL 含 zsh → ~/.zshrc;含 bash → ~/.bashrc;否则 ~/.profile
|
|
95
|
+
* Windows: 无 $SHELL,默认 ~/.bashrc(Git Bash 约定)
|
|
96
|
+
*
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
getProfile() {
|
|
100
|
+
const shell = this.env.SHELL || ''
|
|
101
|
+
if (shell.includes('zsh')) {
|
|
102
|
+
return path.join(os.homedir(), '.zshrc')
|
|
103
|
+
}
|
|
104
|
+
if (shell.includes('bash')) {
|
|
105
|
+
return path.join(os.homedir(), '.bashrc')
|
|
106
|
+
}
|
|
107
|
+
if (this.isWindows) {
|
|
108
|
+
return path.join(os.homedir(), '.bashrc')
|
|
109
|
+
}
|
|
110
|
+
return path.join(os.homedir(), '.profile')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 将 shell 初始化行写入 profile(按 marker 去重)
|
|
115
|
+
*
|
|
116
|
+
* @param {string} marker - 若 profile 已含该标记则跳过
|
|
117
|
+
* @param {string} comment - 写入块注释
|
|
118
|
+
* @param {string[]} lines - 要追加的行
|
|
119
|
+
* @returns {boolean} 是否写入了新行
|
|
120
|
+
*/
|
|
121
|
+
writeProfile(marker, comment, lines) {
|
|
122
|
+
const profilePath = this.getProfile()
|
|
123
|
+
let existing = ''
|
|
124
|
+
try {
|
|
125
|
+
existing = fs.readFileSync(profilePath, 'utf8')
|
|
126
|
+
} catch {}
|
|
127
|
+
if (existing.includes(marker)) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
const append = `\n# ${comment}\n${lines.join('\n')}\n`
|
|
131
|
+
fs.appendFileSync(profilePath, append, 'utf8')
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 检测系统命令是否可用
|
|
137
|
+
* Windows: where <cmd>;Unix: which <cmd>
|
|
138
|
+
*
|
|
139
|
+
* @param {string} cmd - 命令名
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
isCommandAvailable(cmd) {
|
|
143
|
+
const checker = this.isWindows ? 'where' : 'which'
|
|
144
|
+
try {
|
|
145
|
+
const result = spawnSync(checker, [cmd], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
146
|
+
return result.status === 0
|
|
147
|
+
} catch {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 按优先级检测可用的网络探测工具
|
|
154
|
+
* 顺序: ping > curl > wget
|
|
155
|
+
*
|
|
156
|
+
* @returns {string|null} 'ping' | 'curl' | 'wget' | null
|
|
157
|
+
*/
|
|
158
|
+
getNetProbeTool() {
|
|
159
|
+
for (const cmd of ['ping', 'curl', 'wget']) {
|
|
160
|
+
if (this.isCommandAvailable(cmd)) return cmd
|
|
161
|
+
}
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
}
|