npm-release.mjs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import { execFile } from 'node:child_process'
  2. import { createHash } from 'node:crypto'
  3. import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs'
  4. import { join, sep } from 'node:path'
  5. export const NPM_PACKAGE = '@lsamien/yaoyao'
  6. export const NPM_RELEASE_SOURCE = `npm:${NPM_PACKAGE}`
  7. const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
  8. export function isNpmInstallation(root) {
  9. if (!root.includes(`${sep}node_modules${sep}`) || existsSync(join(root, '.git'))) return false
  10. try { return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name === NPM_PACKAGE }
  11. catch { return false }
  12. }
  13. export function parseNpmRelease(value) {
  14. if (!value || value.name !== NPM_PACKAGE || typeof value.version !== 'string' || !versionPattern.test(value.version))
  15. throw new Error('npm 发布包名称或版本无效')
  16. const { integrity, tarball } = value.dist ?? {}
  17. if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity))
  18. throw new Error('npm 发布包缺少 SHA-512 完整性信息')
  19. let url
  20. try { url = new URL(tarball) } catch { throw new Error('npm 下载地址无效') }
  21. if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password)
  22. throw new Error('npm 下载地址无效')
  23. return { version: value.version, integrity, tarball: url.href }
  24. }
  25. export function inspectNpmRelease(cwd) {
  26. return new Promise((resolve, reject) => {
  27. execFile('npm', ['view', `${NPM_PACKAGE}@latest`, '--json'], {
  28. cwd, encoding: 'utf8', timeout: 60000, maxBuffer: 4 * 1024 * 1024,
  29. }, (error, stdout) => {
  30. if (error) { reject(new Error('无法读取 npm 最新版本,请检查仓库连接和 npm 配置')); return }
  31. try { resolve(parseNpmRelease(JSON.parse(stdout))) } catch (cause) { reject(cause) }
  32. })
  33. })
  34. }
  35. /** Hash the published application's files. npm installs dependencies separately;
  36. * these files must remain identical between preparation and activation. */
  37. export function npmRuntimeDigest(root, version) {
  38. const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
  39. const release = JSON.parse(readFileSync(join(root, 'release.json'), 'utf8'))
  40. const build = JSON.parse(readFileSync(join(root, 'build-info.json'), 'utf8'))
  41. if (packageJson.name !== NPM_PACKAGE || packageJson.version !== version
  42. || release.schemaVersion !== 1 || release.releaseVersion !== version || release.webVersion !== version || release.gitTag !== `v${version}`)
  43. throw new Error('npm 包与目标发布版本不一致')
  44. if (!/^[a-f0-9]{40,64}$/.test(build.commit) || !Number.isSafeInteger(build.buildNumber))
  45. throw new Error('npm 包缺少有效的构建信息')
  46. for (const file of ['dist/index.html', 'dist-server/server/index.js', 'bin/hermes-yaoyao.mjs', 'bin/hermes-yaoyao-updater.mjs']) {
  47. if (!lstatSync(join(root, file)).isFile()) throw new Error(`npm 包缺少服务文件:${file}`)
  48. }
  49. const hash = createHash('sha256')
  50. function visit(directory, prefix = '') {
  51. for (const name of readdirSync(directory).sort()) {
  52. if (name === 'node_modules') continue
  53. const path = join(directory, name), relative = `${prefix}${name}`, stat = lstatSync(path)
  54. if (stat.isDirectory()) visit(path, `${relative}/`)
  55. else if (stat.isFile()) hash.update(relative).update('\0').update(createHash('sha256').update(readFileSync(path)).digest())
  56. else throw new Error('npm 服务文件包含不支持的链接或文件')
  57. }
  58. }
  59. visit(root)
  60. return hash.digest('hex')
  61. }