diff --git a/README.md b/README.md index 826f2dc..2c15c78 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ pnpm dev wx ``` ```ts -import { join } from 'node:path' // unh.config.ts import { defineConfig } from '@uni-helper/unh' diff --git a/package.json b/package.json index 9090efd..65c9c39 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "kolorist": "catalog:", "local-pkg": "catalog:", "moment": "catalog:", + "pathe": "catalog:", + "std-env": "catalog:", "unconfig": "catalog:" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ee1330..e1ae54e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,12 @@ catalogs: moment: specifier: ^2.30.1 version: 2.30.1 + pathe: + specifier: ^2.0.3 + version: 2.0.3 + std-env: + specifier: ^3.10.0 + version: 3.10.0 unconfig: specifier: ^7.3.3 version: 7.4.1 @@ -117,6 +123,12 @@ importers: moment: specifier: 'catalog:' version: 2.30.1 + pathe: + specifier: 'catalog:' + version: 2.0.3 + std-env: + specifier: 'catalog:' + version: 3.10.0 unconfig: specifier: 'catalog:' version: 7.4.1 @@ -1645,28 +1657,24 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.50': resolution: {integrity: sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.50': resolution: {integrity: sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-beta.50': resolution: {integrity: sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-beta.50': resolution: {integrity: sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==} @@ -1743,67 +1751,56 @@ packages: resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.2': resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.2': resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.2': resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.2': resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.2': resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.2': resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.2': resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.2': resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.2': resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.2': resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.2': resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f3060aa..7d667e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,8 @@ catalog: kolorist: ^1.8.0 local-pkg: ^1.1.2 moment: ^2.30.1 + pathe: ^2.0.3 + std-env: ^3.10.0 unconfig: ^7.3.3 catalogs: cli: diff --git a/src/constants/devtools.ts b/src/constants/devtools.ts new file mode 100644 index 0000000..daa9392 --- /dev/null +++ b/src/constants/devtools.ts @@ -0,0 +1,8 @@ +import type { MPPlatform } from './platform' + +export const DEVTOOLS_BUNDLE_ID = { + 'mp-weixin': { + mac: 'wechatwebdevtools', + windows: '微信开发者工具', + }, +} as Record diff --git a/src/constants/index.ts b/src/constants/index.ts index a962ecb..0df1261 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,2 +1,3 @@ +export * from './devtools' export * from './platform' export * from './terminal' diff --git a/src/logics/devtools.ts b/src/logics/devtools.ts index 10c63ef..f58d5e6 100644 --- a/src/logics/devtools.ts +++ b/src/logics/devtools.ts @@ -1,59 +1,22 @@ import type { MPPlatform } from '@/constants' -import { execFileSync } from 'node:child_process' -import path from 'node:path' import process from 'node:process' import spawn from 'cross-spawn' import { bold, gray, white } from 'kolorist' +import { join, resolve } from 'pathe' import { getCliConfig, getGlobalConfig } from '@/cli/config' import { MP_PLATFORMS } from '@/constants' -import { ensureJsonSync, isMac, isWindows, stripAnsiColors } from '@/utils' +import { ensureJsonSync, stripAnsiColors } from '@/utils' +import { getDevtoolsCliPath } from '@/utils/findSoftware' import { logger } from '@/utils/log' -const DEVTOOLS_BUNDLE_ID = { - mac: { - 'mp-weixin': 'com.tencent.webplusdevtools', - }, - windows: { - 'mp-weixin': 'webplusdevtools.exe', - }, -} - function getDevtoolsPath() { - const platform = getGlobalConfig().platform! + const platform = getGlobalConfig().platform! as MPPlatform const userCliPath = getCliConfig().devtools?.cliPath?.[platform] if (userCliPath) { return userCliPath } - let cliPath = '' - if (isMac()) { - const devtoolsBundleId = DEVTOOLS_BUNDLE_ID.mac[platform as keyof typeof DEVTOOLS_BUNDLE_ID.mac] - if (!devtoolsBundleId) { - return '' - } - - try { - // 添加超时控制,避免长时间等待 - const searchResult = execFileSync( - 'mdfind', - [ - `kMDItemCFBundleIdentifier == "${devtoolsBundleId}"`, - ], - { timeout: 5000 }, // 设置超时时间,避免长时间等待 - ).toString().trim() - - if (searchResult) { - cliPath = path.join(searchResult, 'Contents/MacOS/cli') - } - } - catch (error) { - logger.warn(`搜索开发者工具路径失败: ${error instanceof Error ? error.message : String(error)}`) - return '' - } - } - if (isWindows()) { - // Windows 平台的实现 - } + const cliPath = getDevtoolsCliPath(platform) return cliPath } @@ -71,9 +34,9 @@ export function openDevtools(outputDir: string) { logger.info(` devtools.cliPath.${platform} = "开发者工具路径"`) return } - const finalOutputDir = path.resolve(process.cwd(), stripAnsiColors(outputDir)) + const finalOutputDir = resolve(process.cwd(), stripAnsiColors(outputDir)) - ensureJsonSync(path.join(finalOutputDir, 'project.config.json'), { + ensureJsonSync(join(finalOutputDir, 'project.config.json'), { appid: 'touristappid', projectname: 'empty', }) diff --git a/src/logics/env.ts b/src/logics/env.ts index 5f70db2..cf68693 100644 --- a/src/logics/env.ts +++ b/src/logics/env.ts @@ -1,9 +1,9 @@ import type { UniHelperConfig } from '@/config/types' import fs from 'node:fs' -import path from 'node:path' import { config } from 'dotenv' import { expand } from 'dotenv-expand' import { bold, red } from 'kolorist' +import path from 'pathe' /** 配置类型 */ type EnvConfig = Exclude /** diff --git a/src/logics/files.ts b/src/logics/files.ts index 1299dde..04bb4f1 100644 --- a/src/logics/files.ts +++ b/src/logics/files.ts @@ -1,10 +1,10 @@ import type { BuildPhase } from '@/cli/types' import type { ManifestOptions, UniHelperConfig } from '@/config/types' import fs from 'node:fs' -import path from 'node:path' import process from 'node:process' import { yellow } from 'kolorist' import { isPackageExists } from 'local-pkg' +import path from 'pathe' import { loadConfig } from 'unconfig' export function writeFileSync(path: string, content: string) { diff --git a/src/utils/findSoftware.ts b/src/utils/findSoftware.ts new file mode 100644 index 0000000..2e14d8a --- /dev/null +++ b/src/utils/findSoftware.ts @@ -0,0 +1,150 @@ +import type { MPPlatform } from '@/constants' +import { execSync } from 'node:child_process' +import os from 'node:os' +import fs from 'fs-extra' +import { basename, dirname, join } from 'pathe' +import { isMacOS, isWindows } from 'std-env' +import { DEVTOOLS_BUNDLE_ID } from '@/constants' +import { decodeGbk } from '@/utils' +import { logger } from './log' + +/** + * 查找软件安装目录 + * @param executableName 可执行文件名 (不带可执行后缀,例如 "wechatwebdevtools") + * @param displayName 软件在控制面板显示的名称 (例如 "微信开发者工具"),当前仅作用于 windows 下 + * @returns 软件安装 + */ +export function findSoftwareInstallLocation(platform: MPPlatform) { + const devtoolsBundleId = DEVTOOLS_BUNDLE_ID[platform] + if (!devtoolsBundleId) { + logger.error(`暂不支持 ${platform} 开发者工具的查找,欢迎提交 PR 支持`) + return null + } + if (isWindows) { + const appName = devtoolsBundleId.windows + // 查询标准卸载注册表 (Uninstall Keys),兼容 32位 & 64位 + const installPath = findInUninstallRegistry(appName) + if (installPath) { + return installPath[0] + } + + // 查询兼容性助手 (AppCompatFlags) + // Borrowed from https://github.com/uni-helper/hbuilderx-cli/blob/9e39cbf2f1986ee1f2a3f105b927ec4ee6cf7112/src/utils.ts#L5-L24 + const exeFullName = `${appName}.exe` + const cmd = `reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Compatibility Assistant\\Store" /f "${exeFullName}" /c` + + const resultBuffer = execSync(cmd) + const result = decodeGbk(resultBuffer) + + // match: "C:\Folder\app.exe" REG_BINARY + // eslint-disable-next-line regexp/no-super-linear-backtracking + const regex = /^.*?(\w:\\.*?)\s*REG_BINARY/gm + + let match + // 遍历所有匹配结果,防止只取到第一个错误的包含匹配 + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(result)) !== null) { + if (match && match[1]) { + const fullExePath = match[1].trim() + const fileName = basename(fullExePath) + + if (fileName.toLowerCase() === exeFullName.toLowerCase()) { + return dirname(fullExePath) + } + } + } + } + + if (isMacOS) { + const appName = devtoolsBundleId.mac + const candidates = [ + `/Applications/${appName}.app`, // 系统应用目录 + join(os.homedir(), 'Applications', `${appName}.app`), // 用户应用目录 (~/Applications) + ] + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate + } + } + } + + return null +} + +/** + * 使用 PowerShell 深度遍历 Uninstall 注册表 + * - 兼容 64位 和 32位 + * - 返回 InstallLocation 或 DisplayIcon + * @platform Only `windows` + */ +function findInUninstallRegistry(appName: string): [string, 'INSTALL_LOCATION' | 'DISPLAY_ICON'] | null { + const psScript = ` + $paths = @( + "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*", + "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" + ) + $target = "*${appName}*" + + foreach ($path in $paths) { + Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $target } | ForEach-Object { + if ($_.InstallLocation) { + # 使用 INSTALL_LOCATION| 标识前缀 + Write-Output "INSTALL_LOCATION|$($_.InstallLocation)" + exit + } + if ($_.DisplayIcon) { + # 使用 DISPLAY_ICON| 标识前缀 + # 如果没有 InstallLocation,尝试从 DisplayIcon (通常是 exe 路径) 获取目录 + Write-Output "DISPLAY_ICON|$($_.DisplayIcon)" + exit + } + } + } + ` + + // 编码,防止转义、乱码问题 + // eslint-disable-next-line node/prefer-global/buffer + const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64') + + try { + const resultBuffer = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand "${encodedCommand}"`, { + stdio: ['ignore', 'pipe', 'ignore'], // ignore error + windowsHide: true, + }) + const output = decodeGbk(resultBuffer) + + if (!output) + return null + + const parts = output.split('|', 2) + if (parts.length !== 2) + return null + + const [sourceType, rawPath] = parts + const cleanPath = rawPath.replace(/"/g, '').trim() // 去掉可能的引号并修剪空格 + + if (sourceType === 'INSTALL_LOCATION') { + return [cleanPath, sourceType] + } + else if (sourceType === 'DISPLAY_ICON') { + return [dirname(cleanPath), sourceType] + } + } + catch { /** nothing */ } + + return null +} + +export function getDevtoolsCliPath(platform: MPPlatform) { + const devtoolsPath = findSoftwareInstallLocation(platform) + if (!devtoolsPath) { + return null + } + if (isWindows) { + return join(devtoolsPath, 'cli.bat') + } + if (isMacOS) { + return join(devtoolsPath, 'Contents/MacOS/cli') + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 6caf027..d5d015d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,15 +1,5 @@ -import os from 'node:os' - -import { parse } from 'node:path' import fs from 'fs-extra' - -export function isMac() { - return os.platform() === 'darwin' -} - -export function isWindows() { - return os.platform() === 'win32' -} +import { parse } from 'pathe' /** * 去除字符串中的 ANSI 颜色代码 @@ -38,3 +28,8 @@ export async function ensureJson(path: string, object: any = {}) { await fs.writeJSON(path, object) } } + +export function decodeGbk(input?: NonSharedBuffer) { + const decoder = new TextDecoder('gbk') + return decoder.decode(input) +}