publish-public-gitee-snapshot.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import { existsSync } from 'node:fs'
  2. import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
  3. import { dirname, join, resolve } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { fileURLToPath } from 'node:url'
  6. import { readJson } from './lib/ecosystem-packages.mjs'
  7. const scriptDir = dirname(fileURLToPath(import.meta.url))
  8. const sourceRoot = resolve(scriptDir, '..')
  9. const args = process.argv.slice(2)
  10. function readArg(name, fallback) {
  11. const index = args.indexOf(name)
  12. return index >= 0 ? args[index + 1] : fallback
  13. }
  14. const publicRepoDir = resolve(
  15. sourceRoot,
  16. readArg('--public-repo-dir', process.env.FILE_VIEWER_PUBLIC_REPO_DIR || '../file-viewer-public')
  17. )
  18. const snapshotDir = resolve(
  19. sourceRoot,
  20. readArg('--snapshot-dir', process.env.FILE_VIEWER_PUBLIC_GITEE_SNAPSHOT_DIR || '.release/public-gitee-snapshot')
  21. )
  22. const branch = readArg('--branch', 'main')
  23. const shouldPush = args.includes('--push')
  24. const keepSnapshot = args.includes('--keep')
  25. const confirmRewriteHistory = args.includes('--confirm-rewrite-history')
  26. const branchRoles = await readJson(join(sourceRoot, 'ecosystem', 'branch-roles.json'))
  27. const remoteUrl = readArg('--remote-url', branchRoles.publicMainRepository.gitee)
  28. const message = readArg('--message', 'chore: publish open-source main snapshot mirror')
  29. const packageMetadataFiles = ['package.json', 'README.md', 'README.en.md', 'LICENSE']
  30. function run(command, commandArgs, options = {}) {
  31. const result = spawnSync(command, commandArgs, {
  32. cwd: options.cwd || sourceRoot,
  33. encoding: 'utf8',
  34. stdio: options.capture ? 'pipe' : 'inherit',
  35. // The open-source aggregate carries many offline assets. Captured commands
  36. // such as `git ls-files -z` can exceed Node's default 1 MiB buffer.
  37. maxBuffer: 256 * 1024 * 1024
  38. })
  39. if (result.status !== 0 && !options.allowFailure) {
  40. throw new Error(
  41. `Command failed in ${options.cwd || sourceRoot}: ${[command, ...commandArgs].join(' ')}\n${
  42. result.stderr || result.stdout || ''
  43. }`
  44. )
  45. }
  46. return (result.stdout || '').trim()
  47. }
  48. function assertCleanGitRepo(cwd, label) {
  49. if (!existsSync(join(cwd, '.git'))) {
  50. throw new Error(`${label} is not a git repository: ${cwd}`)
  51. }
  52. const status = run('git', ['status', '--porcelain'], { cwd, capture: true })
  53. if (status) {
  54. throw new Error(`${label} has uncommitted changes. Commit or clean it first:\n${status}`)
  55. }
  56. }
  57. function firstRefHash(output) {
  58. return output.split('\n').find(Boolean)?.split(/\s+/)[0] || ''
  59. }
  60. function lsRemoteHead(url, branchName) {
  61. return firstRefHash(
  62. run('git', ['ls-remote', url, `refs/heads/${branchName}`], {
  63. capture: true,
  64. allowFailure: true
  65. })
  66. )
  67. }
  68. async function copyTrackedFiles(from, to) {
  69. const trackedFiles = run('git', ['ls-files', '-z'], { cwd: from, capture: true })
  70. .split('\0')
  71. .filter(Boolean)
  72. for (const file of trackedFiles) {
  73. const source = join(from, file)
  74. const target = join(to, file)
  75. await mkdir(dirname(target), { recursive: true })
  76. await cp(source, target, { force: true, preserveTimestamps: true })
  77. }
  78. return trackedFiles.length
  79. }
  80. async function pathExists(path) {
  81. try {
  82. await stat(path)
  83. return true
  84. } catch (error) {
  85. if (error?.code === 'ENOENT') {
  86. return false
  87. }
  88. throw error
  89. }
  90. }
  91. async function copyIfExists(source, target) {
  92. if (!(await pathExists(source))) {
  93. return false
  94. }
  95. await mkdir(dirname(target), { recursive: true })
  96. await cp(source, target, {
  97. recursive: true,
  98. force: true,
  99. preserveTimestamps: true
  100. })
  101. return true
  102. }
  103. async function listPackageDirs(section) {
  104. const root = join(sourceRoot, 'packages', section)
  105. if (!(await pathExists(root))) {
  106. return []
  107. }
  108. const entries = await readdir(root, { withFileTypes: true })
  109. const dirs = []
  110. for (const entry of entries) {
  111. if (!entry.isDirectory()) {
  112. continue
  113. }
  114. const relativeDir = `packages/${section}/${entry.name}`
  115. if (await pathExists(join(sourceRoot, relativeDir, 'package.json'))) {
  116. dirs.push(relativeDir)
  117. }
  118. }
  119. return dirs
  120. }
  121. async function packageDirs() {
  122. return [
  123. 'packages/core',
  124. ...(await listPackageDirs('renderers')),
  125. ...(await listPackageDirs('components')),
  126. ...(await listPackageDirs('compat'))
  127. ]
  128. }
  129. function normalizeRuntimePackageJson(packageJson) {
  130. delete packageJson.scripts
  131. delete packageJson.devDependencies
  132. delete packageJson.publishConfig
  133. if (packageJson.name === '@file-viewer/svelte') {
  134. delete packageJson.svelte
  135. packageJson.main = './dist/index.js'
  136. packageJson.module = './dist/index.js'
  137. packageJson.types = './dist/index.d.ts'
  138. packageJson.exports = {
  139. '.': {
  140. types: './dist/index.d.ts',
  141. import: './dist/index.js',
  142. default: './dist/index.js'
  143. },
  144. './action': {
  145. types: './dist/index.d.ts',
  146. import: './dist/index.js',
  147. default: './dist/index.js'
  148. },
  149. './types': {
  150. types: './dist/index.d.ts',
  151. import: './dist/index.js',
  152. default: './dist/index.js'
  153. },
  154. './package.json': './package.json'
  155. }
  156. packageJson.files = ['dist', 'README.md', 'README.en.md']
  157. }
  158. return packageJson
  159. }
  160. async function copyRuntimePackage(relativePackageDir, snapshotRoot) {
  161. const sourcePackageDir = join(sourceRoot, relativePackageDir)
  162. const targetPackageDir = join(snapshotRoot, relativePackageDir)
  163. await rm(targetPackageDir, { recursive: true, force: true })
  164. await mkdir(targetPackageDir, { recursive: true })
  165. for (const filename of packageMetadataFiles) {
  166. await copyIfExists(join(sourcePackageDir, filename), join(targetPackageDir, filename))
  167. }
  168. const packageJsonPath = join(targetPackageDir, 'package.json')
  169. const packageJson = normalizeRuntimePackageJson(JSON.parse(await readFile(packageJsonPath, 'utf8')))
  170. await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
  171. await copyIfExists(join(sourcePackageDir, 'dist'), join(targetPackageDir, 'dist'))
  172. await copyIfExists(join(sourcePackageDir, 'vendor'), join(targetPackageDir, 'vendor'))
  173. await copyIfExists(join(sourcePackageDir, 'viewer'), join(targetPackageDir, 'viewer'))
  174. await copyIfExists(
  175. join(sourcePackageDir, 'scripts', 'copy-assets.mjs'),
  176. join(targetPackageDir, 'scripts', 'copy-assets.mjs')
  177. )
  178. }
  179. async function replacePackagesWithRuntimeDist(snapshotRoot) {
  180. const dirs = await packageDirs()
  181. await rm(join(snapshotRoot, 'packages'), { recursive: true, force: true })
  182. for (const relativePackageDir of dirs) {
  183. await copyRuntimePackage(relativePackageDir, snapshotRoot)
  184. }
  185. return dirs.length
  186. }
  187. async function rewriteSnapshotRootPackage(snapshotRoot) {
  188. const packagePath = join(snapshotRoot, 'package.json')
  189. const packageJson = JSON.parse(await readFile(packagePath, 'utf8'))
  190. packageJson.scripts = {
  191. dev: packageJson.scripts?.dev || 'pnpm --filter @flyfish-group/file-viewer-demo dev',
  192. 'site:dev': packageJson.scripts?.['site:dev'] || 'pnpm --filter @flyfish-group/file-viewer-site dev',
  193. 'site:build': packageJson.scripts?.['site:build'] || 'pnpm --filter @flyfish-group/file-viewer-site build',
  194. 'site:preview': packageJson.scripts?.['site:preview'] || 'pnpm --filter @flyfish-group/file-viewer-site preview',
  195. build: packageJson.scripts?.['build:demo'] || 'pnpm --filter @flyfish-group/file-viewer-demo build',
  196. 'build:demo': packageJson.scripts?.['build:demo'] || 'pnpm --filter @flyfish-group/file-viewer-demo build',
  197. 'build:component-demo':
  198. packageJson.scripts?.['build:component-demo'] ||
  199. 'pnpm --filter @flyfish-group/file-viewer-component-demo build',
  200. 'docs:dev': packageJson.scripts?.['docs:dev'] || 'vitepress dev docs',
  201. 'docs:build': packageJson.scripts?.['docs:build'] || 'vitepress build docs',
  202. 'docs:preview': packageJson.scripts?.['docs:preview'] || 'vitepress preview docs'
  203. }
  204. await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
  205. }
  206. if (shouldPush && !confirmRewriteHistory) {
  207. throw new Error(
  208. 'Refusing to rewrite the Gitee open-source main mirror without --confirm-rewrite-history. ' +
  209. 'This command intentionally publishes a shallow snapshot branch to avoid oversized historical packs.'
  210. )
  211. }
  212. assertCleanGitRepo(publicRepoDir, 'Open-source main repository')
  213. const publicHead = run('git', ['rev-parse', 'HEAD'], { cwd: publicRepoDir, capture: true })
  214. const publicTree = run('git', ['rev-parse', 'HEAD^{tree}'], { cwd: publicRepoDir, capture: true })
  215. const remoteHead = lsRemoteHead(remoteUrl, branch)
  216. await rm(snapshotDir, { recursive: true, force: true })
  217. await mkdir(snapshotDir, { recursive: true })
  218. const fileCount = await copyTrackedFiles(publicRepoDir, snapshotDir)
  219. const packageCount = await replacePackagesWithRuntimeDist(snapshotDir)
  220. await rewriteSnapshotRootPackage(snapshotDir)
  221. run('git', ['init', '-b', branch], { cwd: snapshotDir })
  222. run('git', ['config', 'user.name', 'Flyfish Release Bot'], { cwd: snapshotDir })
  223. run('git', ['config', 'user.email', 'release@flyfish.dev'], { cwd: snapshotDir })
  224. run('git', ['add', '-A'], { cwd: snapshotDir })
  225. run(
  226. 'git',
  227. [
  228. 'commit',
  229. '-m',
  230. message,
  231. '-m',
  232. `Snapshot source: ${publicHead}`,
  233. '-m',
  234. `Snapshot tree: ${publicTree}`
  235. ],
  236. { cwd: snapshotDir }
  237. )
  238. run('git', ['remote', 'add', 'gitee', remoteUrl], { cwd: snapshotDir })
  239. const snapshotHead = run('git', ['rev-parse', 'HEAD'], { cwd: snapshotDir, capture: true })
  240. const snapshotTree = run('git', ['rev-parse', 'HEAD^{tree}'], { cwd: snapshotDir, capture: true })
  241. console.log(`Prepared Gitee snapshot mirror in ${snapshotDir}`)
  242. console.log(`Tracked files: ${fileCount}`)
  243. console.log(`Runtime package directories: ${packageCount}`)
  244. console.log(`Public source commit: ${publicHead}`)
  245. console.log(`Snapshot commit: ${snapshotHead}`)
  246. console.log(`Public tree: ${publicTree}`)
  247. console.log(`Snapshot tree: ${snapshotTree}`)
  248. console.log(`Gitee ${branch}: ${remoteHead || '(missing)'}`)
  249. if (shouldPush) {
  250. const pushArgs = ['push', 'gitee', `HEAD:refs/heads/${branch}`]
  251. if (remoteHead) {
  252. pushArgs.push(`--force-with-lease=refs/heads/${branch}:${remoteHead}`)
  253. }
  254. run('git', pushArgs, { cwd: snapshotDir })
  255. console.log(`Published shallow snapshot mirror to ${remoteUrl} ${branch}.`)
  256. } else {
  257. console.log(
  258. 'Dry run complete. To publish the shallow Gitee mirror, rerun with --push --confirm-rewrite-history.'
  259. )
  260. }
  261. if (!keepSnapshot) {
  262. await rm(snapshotDir, { recursive: true, force: true })
  263. }