sync-github-wiki.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import { spawnSync } from 'node:child_process'
  2. import {
  3. existsSync,
  4. mkdirSync,
  5. readFileSync,
  6. rmSync,
  7. writeFileSync
  8. } from 'node:fs'
  9. import { basename, dirname, join, resolve } from 'node:path'
  10. import { fileURLToPath } from 'node:url'
  11. const scriptDir = dirname(fileURLToPath(import.meta.url))
  12. const sourceRoot = resolve(scriptDir, '..')
  13. const docsRoot = join(sourceRoot, 'docs')
  14. const defaultWikiDir = resolve(sourceRoot, '..', 'file-viewer.wiki')
  15. const wikiDir = resolve(process.env.FILE_VIEWER_WIKI_DIR || defaultWikiDir)
  16. const wikiRemote = process.env.FILE_VIEWER_WIKI_REMOTE || 'https://github.com/flyfish-dev/file-viewer.wiki.git'
  17. const shouldPush = process.argv.includes('--push')
  18. const noClone = process.argv.includes('--no-clone')
  19. const pages = [
  20. { source: 'index.md', file: 'Docs-Home.md', title: '文档首页', group: '中文文档' },
  21. { source: 'guide/index.md', file: 'Guide.md', title: '文档导览', group: '中文文档' },
  22. { source: 'guide/overview.md', file: 'Overview.md', title: '概述', group: '中文文档' },
  23. { source: 'guide/quickstart.md', file: 'Quickstart.md', title: '快速开始', group: '中文文档' },
  24. { source: 'guide/quickstart-web.md', file: 'Vanilla-JS.md', title: '纯 JS / Script 标签', group: '生态接入' },
  25. { source: 'guide/quickstart-vue3.md', file: 'Vue3.md', title: 'Vue 3', group: '生态接入' },
  26. { source: 'guide/quickstart-vue2.md', file: 'Vue2.md', title: 'Vue 2.7 / 2.6', group: '生态接入' },
  27. { source: 'guide/quickstart-react.md', file: 'React.md', title: 'React', group: '生态接入' },
  28. { source: 'guide/quickstart-svelte.md', file: 'Svelte.md', title: 'Svelte', group: '生态接入' },
  29. { source: 'guide/ecosystem.md', file: 'Ecosystem.md', title: '生态组件总览', group: '生态接入' },
  30. { source: 'guide/usage.md', file: 'Usage.md', title: '组件用法与 API', group: '生态接入' },
  31. { source: 'guide/on-demand-renderers.md', file: 'On-Demand-Renderers.md', title: '模块化与按需装配', group: '架构与能力' },
  32. { source: 'guide/formats.md', file: 'Supported-Formats.md', title: '支持格式', group: '架构与能力' },
  33. { source: 'guide/format-fidelity.md', file: 'Format-Fidelity.md', title: '格式完整度', group: '架构与能力' },
  34. { source: 'guide/compare.md', file: 'Comparison.md', title: '方案对比', group: '架构与能力' },
  35. { source: 'guide/demo.md', file: 'Demo.md', title: 'Demo 说明', group: '交付与运营' },
  36. { source: 'guide/development.md', file: 'Development.md', title: '本地开发', group: '交付与运营' },
  37. { source: 'guide/docker.md', file: 'Docker.md', title: 'Docker 部署', group: '交付与运营' },
  38. { source: 'guide/distribution.md', file: 'Distribution.md', title: '发布与分发', group: '交付与运营' },
  39. { source: 'guide/faq.md', file: 'FAQ.md', title: '常见问题', group: '交付与运营' },
  40. { source: 'changelog.md', file: 'Changelog.md', title: '更新日志', group: '交付与运营' },
  41. { source: 'donate.md', file: 'Sponsor.md', title: '捐赠与支持', group: '交付与运营' },
  42. { source: 'en/index.md', file: 'EN-Docs-Home.md', title: 'Docs Home', group: 'English Docs' },
  43. { source: 'en/guide/index.md', file: 'EN-Guide.md', title: 'Guide', group: 'English Docs' },
  44. { source: 'en/guide/overview.md', file: 'EN-Overview.md', title: 'Overview', group: 'English Docs' },
  45. { source: 'en/guide/quickstart.md', file: 'EN-Quickstart.md', title: 'Quickstart', group: 'English Docs' },
  46. { source: 'en/guide/quickstart-web.md', file: 'EN-Vanilla-JS.md', title: 'Vanilla JS / Script Tag', group: 'English Ecosystem' },
  47. { source: 'en/guide/quickstart-vue3.md', file: 'EN-Vue3.md', title: 'Vue 3', group: 'English Ecosystem' },
  48. { source: 'en/guide/quickstart-vue2.md', file: 'EN-Vue2.md', title: 'Vue 2.7 / 2.6', group: 'English Ecosystem' },
  49. { source: 'en/guide/quickstart-react.md', file: 'EN-React.md', title: 'React', group: 'English Ecosystem' },
  50. { source: 'en/guide/quickstart-svelte.md', file: 'EN-Svelte.md', title: 'Svelte', group: 'English Ecosystem' },
  51. { source: 'en/guide/ecosystem.md', file: 'EN-Ecosystem.md', title: 'Ecosystem Packages', group: 'English Ecosystem' },
  52. { source: 'en/guide/usage.md', file: 'EN-Usage.md', title: 'Usage And API', group: 'English Ecosystem' },
  53. { source: 'en/guide/on-demand-renderers.md', file: 'EN-On-Demand-Renderers.md', title: 'Modular Assembly', group: 'English Capability' },
  54. { source: 'en/guide/formats.md', file: 'EN-Supported-Formats.md', title: 'Supported Formats', group: 'English Capability' },
  55. { source: 'en/guide/format-fidelity.md', file: 'EN-Format-Fidelity.md', title: 'Format Fidelity', group: 'English Capability' },
  56. { source: 'en/guide/compare.md', file: 'EN-Comparison.md', title: 'Comparison', group: 'English Capability' },
  57. { source: 'en/guide/demo.md', file: 'EN-Demo.md', title: 'Demo Guide', group: 'English Delivery' },
  58. { source: 'en/guide/development.md', file: 'EN-Development.md', title: 'Local Development', group: 'English Delivery' },
  59. { source: 'en/guide/docker.md', file: 'EN-Docker.md', title: 'Docker Deployment', group: 'English Delivery' },
  60. { source: 'en/guide/distribution.md', file: 'EN-Distribution.md', title: 'Distribution', group: 'English Delivery' },
  61. { source: 'en/guide/faq.md', file: 'EN-FAQ.md', title: 'FAQ', group: 'English Delivery' }
  62. ]
  63. const pageByRoute = new Map([
  64. ['/', 'Docs-Home'],
  65. ['/index', 'Docs-Home'],
  66. ['/guide', 'Guide'],
  67. ['/guide/', 'Guide'],
  68. ['/guide/index', 'Guide'],
  69. ['/changelog', 'Changelog'],
  70. ['/donate', 'Sponsor'],
  71. ['/en', 'EN-Docs-Home'],
  72. ['/en/', 'EN-Docs-Home'],
  73. ['/en/index', 'EN-Docs-Home'],
  74. ['/en/guide', 'EN-Guide'],
  75. ['/en/guide/', 'EN-Guide'],
  76. ['/en/guide/index', 'EN-Guide'],
  77. ...pages.map(page => {
  78. const route = `/${page.source.replace(/\.md$/, '').replace(/\/index$/, '')}`
  79. return [route, page.file.replace(/\.md$/, '')]
  80. })
  81. ])
  82. function run(command, args, options = {}) {
  83. const result = spawnSync(command, args, {
  84. cwd: options.cwd || sourceRoot,
  85. encoding: 'utf8',
  86. stdio: options.capture ? 'pipe' : 'inherit'
  87. })
  88. if (result.status !== 0) {
  89. const output = [result.stdout, result.stderr].filter(Boolean).join('\n')
  90. throw new Error(`Command failed: ${command} ${args.join(' ')}${output ? `\n${output}` : ''}`)
  91. }
  92. return options.capture ? (result.stdout || '').trim() : ''
  93. }
  94. function tryRun(command, args, options = {}) {
  95. const result = spawnSync(command, args, {
  96. cwd: options.cwd || sourceRoot,
  97. encoding: 'utf8',
  98. stdio: options.capture ? 'pipe' : 'inherit'
  99. })
  100. return {
  101. ok: result.status === 0,
  102. stdout: (result.stdout || '').trim(),
  103. stderr: (result.stderr || '').trim()
  104. }
  105. }
  106. function ensureWikiRepo() {
  107. if (existsSync(join(wikiDir, '.git'))) {
  108. const existingStatus = run('git', ['status', '--porcelain'], { cwd: wikiDir, capture: true })
  109. if (existingStatus) {
  110. throw new Error(
  111. `GitHub Wiki checkout has uncommitted changes at ${wikiDir}. Commit or clean them before syncing.`
  112. )
  113. }
  114. const remote = tryRun('git', ['ls-remote', '--exit-code', 'origin', 'HEAD'], { cwd: wikiDir, capture: true })
  115. if (!remote.ok) {
  116. console.warn('[wiki] Remote Wiki repository is not initialized yet; updating the local Wiki checkout only.')
  117. return
  118. }
  119. run('git', ['fetch', 'origin'], { cwd: wikiDir })
  120. run('git', ['checkout', 'master'], { cwd: wikiDir })
  121. const pull = tryRun('git', ['pull', '--ff-only', 'origin', 'master'], { cwd: wikiDir })
  122. if (!pull.ok) {
  123. console.warn('[wiki] Local Wiki history differs from the initialized remote; resetting local generated Wiki checkout to origin/master.')
  124. run('git', ['reset', '--hard', 'origin/master'], { cwd: wikiDir })
  125. }
  126. return
  127. }
  128. mkdirSync(dirname(wikiDir), { recursive: true })
  129. if (!noClone) {
  130. const clone = tryRun('git', ['clone', wikiRemote, wikiDir])
  131. if (clone.ok) {
  132. return
  133. }
  134. console.warn(`[wiki] Clone failed, initializing a first Wiki checkout locally: ${clone.stderr || clone.stdout}`)
  135. }
  136. mkdirSync(wikiDir, { recursive: true })
  137. run('git', ['init', '-b', 'master'], { cwd: wikiDir })
  138. run('git', ['remote', 'add', 'origin', wikiRemote], { cwd: wikiDir })
  139. }
  140. function stripFrontmatter(markdown) {
  141. return markdown.replace(/^---\n[\s\S]*?\n---\n+/, '')
  142. }
  143. function wikiPageName(pageFile) {
  144. return pageFile.replace(/\.md$/, '')
  145. }
  146. function rewriteDocLink(target) {
  147. if (/^(https?:|mailto:|#)/.test(target)) {
  148. return target
  149. }
  150. const [pathPart, hashPart] = target.split('#')
  151. const normalized = pathPart
  152. .replace(/\.md$/, '')
  153. .replace(/\/index$/, '')
  154. .replace(/\/$/, '') || '/'
  155. const route = normalized.startsWith('/') ? normalized : `/${normalized}`
  156. const pageName = pageByRoute.get(route)
  157. if (!pageName) {
  158. return target
  159. }
  160. return hashPart ? `${pageName}#${hashPart}` : pageName
  161. }
  162. function transformMarkdown(markdown, page) {
  163. let output = stripFrontmatter(markdown)
  164. output = output.replace(/\]\((\/[^)\s]+)(#[^)]+)?\)/g, (_match, pathPart, hashPart = '') => {
  165. const rewritten = rewriteDocLink(`${pathPart}${hashPart}`)
  166. return `](${rewritten})`
  167. })
  168. output = output.replace(/href="(\/[^"#]+)(#[^"]*)?"/g, (_match, pathPart, hashPart = '') => {
  169. const rewritten = rewriteDocLink(`${pathPart}${hashPart}`)
  170. return `href="${rewritten}"`
  171. })
  172. output = output.replace(/src="\/_media\//g, 'src="https://doc.file-viewer.app/_media/')
  173. output = output.replace(/]\(\/_media\//g, '](https://doc.file-viewer.app/_media/')
  174. return [
  175. `<!-- Generated from docs/${page.source}. Edit the source docs, then run pnpm docs:wiki:sync. -->`,
  176. '',
  177. output.trim(),
  178. ''
  179. ].join('\n')
  180. }
  181. function writeIfChanged(filePath, content) {
  182. if (existsSync(filePath) && readFileSync(filePath, 'utf8') === content) {
  183. return false
  184. }
  185. writeFileSync(filePath, content)
  186. return true
  187. }
  188. function readDocsPage(page) {
  189. const sourcePath = join(docsRoot, page.source)
  190. if (!existsSync(sourcePath)) {
  191. throw new Error(`Missing docs source: docs/${page.source}`)
  192. }
  193. return readFileSync(sourcePath, 'utf8')
  194. }
  195. function buildHome() {
  196. return `# Flyfish Viewer Documentation
  197. Browser-native file preview infrastructure for internal tools, intranet systems, private deployments, and business attachment centers.
  198. ## Start here
  199. - [中文文档首页](Docs-Home)
  200. - [快速开始](Quickstart)
  201. - [支持格式](Supported-Formats)
  202. - [生态组件总览](Ecosystem)
  203. - [发布与分发](Distribution)
  204. - [English documentation](EN-Docs-Home)
  205. ## Official links
  206. - [Official site](https://file-viewer.app)
  207. - [Documentation site](https://doc.file-viewer.app)
  208. - [Live demo](https://demo.file-viewer.app)
  209. - [GitHub repository](https://github.com/flyfish-dev/file-viewer)
  210. - [npm packages](https://www.npmjs.com/org/file-viewer)
  211. This Wiki mirrors the main documentation for GitHub readers. The VitePress documentation site remains the canonical polished documentation experience.
  212. `
  213. }
  214. function buildSidebar() {
  215. const groups = new Map()
  216. for (const page of pages) {
  217. if (!groups.has(page.group)) {
  218. groups.set(page.group, [])
  219. }
  220. groups.get(page.group).push(page)
  221. }
  222. const lines = [
  223. '# Flyfish Viewer',
  224. '',
  225. '- [Home](Home)',
  226. '- [Official Docs](https://doc.file-viewer.app)',
  227. '- [Live Demo](https://demo.file-viewer.app)',
  228. ''
  229. ]
  230. for (const [group, groupPages] of groups) {
  231. lines.push(`## ${group}`)
  232. for (const page of groupPages) {
  233. lines.push(`- [${page.title}](${wikiPageName(page.file)})`)
  234. }
  235. lines.push('')
  236. }
  237. return `${lines.join('\n').trim()}\n`
  238. }
  239. function buildFooter() {
  240. return [
  241. 'Flyfish Viewer · [Official site](https://file-viewer.app) · [Docs](https://doc.file-viewer.app) · [Demo](https://demo.file-viewer.app) · [GitHub](https://github.com/flyfish-dev/file-viewer)',
  242. ''
  243. ].join('\n')
  244. }
  245. ensureWikiRepo()
  246. mkdirSync(wikiDir, { recursive: true })
  247. writeIfChanged(join(wikiDir, 'Home.md'), buildHome())
  248. writeIfChanged(join(wikiDir, '_Sidebar.md'), buildSidebar())
  249. writeIfChanged(join(wikiDir, '_Footer.md'), buildFooter())
  250. for (const page of pages) {
  251. const markdown = transformMarkdown(readDocsPage(page), page)
  252. writeIfChanged(join(wikiDir, page.file), markdown)
  253. }
  254. const status = run('git', ['status', '--short'], { cwd: wikiDir, capture: true })
  255. if (!status) {
  256. if (shouldPush) {
  257. const localHead = run('git', ['rev-parse', 'HEAD'], { cwd: wikiDir, capture: true })
  258. const remoteHeadResult = tryRun('git', ['rev-parse', 'origin/master'], { cwd: wikiDir, capture: true })
  259. if (!remoteHeadResult.ok || remoteHeadResult.stdout !== localHead) {
  260. run('git', ['push', 'origin', 'master'], { cwd: wikiDir })
  261. } else {
  262. console.log(`GitHub Wiki is already up to date at ${wikiDir}`)
  263. }
  264. } else {
  265. console.log(`GitHub Wiki is already up to date at ${wikiDir}`)
  266. }
  267. process.exit(0)
  268. }
  269. console.log(status)
  270. run('git', ['add', '-A'], { cwd: wikiDir })
  271. const commitMessage = `docs: sync wiki documentation from ${basename(sourceRoot)}`
  272. run('git', ['commit', '-m', commitMessage], { cwd: wikiDir })
  273. if (shouldPush) {
  274. run('git', ['push', 'origin', 'master'], { cwd: wikiDir })
  275. } else {
  276. console.log(`Wiki changes committed locally at ${wikiDir}. Run with --push to publish.`)
  277. }