| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- import { execFile } from 'node:child_process'
- import { createHash } from 'node:crypto'
- import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync } from 'node:fs'
- import { basename, dirname, join, sep } from 'node:path'
- export const NPM_PACKAGE = '@lsamien/yaoyao'
- export const NPM_RELEASE_SOURCE = `npm:${NPM_PACKAGE}`
- const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
- export function isNpmInstallation(root) {
- if (!root.includes(`${sep}node_modules${sep}`) || existsSync(join(root, '.git'))) return false
- try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name === NPM_PACKAGE }
- catch { return false }
- }
- export function parseNpmRelease(value) {
- if (!value || value.name !== NPM_PACKAGE || typeof value.version !== 'string' || !versionPattern.test(value.version))
- throw new Error('npm 发布包名称或版本无效')
- const { integrity, tarball } = value.dist ?? {}
- if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity))
- throw new Error('npm 发布包缺少 SHA-512 完整性信息')
- let url
- try { url = new URL(tarball) } catch { throw new Error('npm 下载地址无效') }
- if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password)
- throw new Error('npm 下载地址无效')
- return { version: value.version, integrity, tarball: url.href }
- }
- export function inspectNpmRelease(cwd) {
- return new Promise((resolve, reject) => {
- execFile('npm', ['view', `${NPM_PACKAGE}@latest`, '--json'], {
- cwd, encoding: 'utf8', timeout: 60000, maxBuffer: 4 * 1024 * 1024,
- }, (error, stdout) => {
- if (error) { reject(new Error('无法读取 npm 最新版本,请检查仓库连接和 npm 配置')); return }
- try { resolve(parseNpmRelease(JSON.parse(stdout))) } catch (cause) { reject(cause) }
- })
- })
- }
- export function npmInstallationLocation(root) {
- root = realpathSync(root)
- const modules = dirname(dirname(root)), prefix = dirname(modules)
- if (basename(modules) !== 'node_modules' || root !== join(modules, NPM_PACKAGE) || !isNpmInstallation(root))
- throw new Error('直接覆盖更新仅支持已核对的 npm 安装目录')
- // Unix global installs use <prefix>/lib/node_modules; managed local installs
- // use <prefix>/node_modules. Preserve the layout and unrelated global tools.
- return basename(prefix) === 'lib' ? { prefix: dirname(prefix), global: true } : { prefix, global: false }
- }
- export function verifyPreparedNpmArchive(home, prepared, npm) {
- const downloads = realpathSync(join(home, 'updates', 'npm'))
- const archive = realpathSync(prepared.archive), directory = basename(dirname(archive))
- if (!/^[0-9a-f-]{36}$/.test(directory) || archive !== join(downloads, directory, 'release.tgz') || !lstatSync(archive).isFile())
- throw new Error('已下载更新的文件位置无效')
- const hash = createHash('sha512'), descriptor = openSync(archive, 'r'), buffer = Buffer.alloc(1024 * 1024)
- try {
- let bytes, received = 0
- while ((bytes = readSync(descriptor, buffer, 0, buffer.length, null)) > 0) {
- received += bytes
- if (received > 512 * 1024 * 1024) throw new Error('npm 安装包超过下载大小限制')
- hash.update(buffer.subarray(0, bytes))
- }
- } finally { closeSync(descriptor) }
- if (`sha512-${hash.digest('base64')}` !== npm.integrity) throw new Error('已下载更新的安装包完整性校验失败')
- return archive
- }
- /** Validate the installed application's version, build and published files. */
- export function npmRuntimeDigest(root, version) {
- const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
- const release = JSON.parse(readFileSync(join(root, 'release.json'), 'utf8'))
- const build = JSON.parse(readFileSync(join(root, 'build-info.json'), 'utf8'))
- if (packageJson.name !== NPM_PACKAGE || packageJson.version !== version
- || release.schemaVersion !== 1 || release.releaseVersion !== version || release.webVersion !== version || release.gitTag !== `v${version}`)
- throw new Error('npm 包与目标发布版本不一致')
- if (!/^[a-f0-9]{40,64}$/.test(build.commit) || !Number.isSafeInteger(build.buildNumber))
- throw new Error('npm 包缺少有效的构建信息')
- for (const file of ['dist/index.html', 'dist-server/server/index.js', 'bin/hermes-yaoyao.mjs', 'bin/hermes-yaoyao-updater.mjs']) {
- if (!lstatSync(join(root, file)).isFile()) throw new Error(`npm 包缺少服务文件:${file}`)
- }
- const hash = createHash('sha256')
- function visit(directory, prefix = '') {
- for (const name of readdirSync(directory).sort()) {
- if (name === 'node_modules') continue
- const path = join(directory, name), relative = `${prefix}${name}`, stat = lstatSync(path)
- if (stat.isDirectory()) visit(path, `${relative}/`)
- else if (stat.isFile()) hash.update(relative).update('\0').update(createHash('sha256').update(readFileSync(path)).digest())
- else throw new Error('npm 服务文件包含不支持的链接或文件')
- }
- }
- visit(root)
- return hash.digest('hex')
- }
|