project-doc-site.spec.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /** Tests for the documentation website projection adapter. */
  2. import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { docsPages, type DocsPage } from '../website/docs.ts'
  7. import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
  8. const roots: string[] = []
  9. afterEach(() => {
  10. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  11. })
  12. function fixture(): { root: string; pages: DocsPage[] } {
  13. const root = mkdtempSync(join(tmpdir(), 'dsh-doc-site-'))
  14. roots.push(root)
  15. mkdirSync(join(root, 'docs'), { recursive: true })
  16. mkdirSync(join(root, 'packages'), { recursive: true })
  17. writeFileSync(join(root, 'docs/a.md'), '# A\n')
  18. writeFileSync(join(root, 'docs/b.md'), '# B\n')
  19. writeFileSync(join(root, 'docs/x(y).md'), '# Parentheses\n')
  20. writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n')
  21. writeFileSync(join(root, 'packages/logo.svg'), '<svg/>\n')
  22. return {
  23. root,
  24. pages: [
  25. { locale: 'root', contentLocale: 'en-US', source: 'docs/a.md', route: 'a.md', label: 'A', sidebar: 'zh-reference', section: 'Test', order: 1 },
  26. { locale: 'root', contentLocale: 'en-US', source: 'docs/b.md', route: 'reference-root/b.md', label: 'B', sidebar: 'zh-reference', section: 'Test', order: 2 },
  27. { locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-reference', section: 'Test', order: 1 },
  28. { locale: 'en', contentLocale: 'en-US', source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-reference', section: 'Test', order: 2 },
  29. ],
  30. }
  31. }
  32. describe('rewriteMarkdown', () => {
  33. it('maps published pages and pins unpublished source links', () => {
  34. const { root, pages } = fixture()
  35. const source = '[B](b.md#part) [source](../packages/tool.ts:2) [web](https://example.com)\n'
  36. expect(rewriteMarkdown(source, {
  37. locale: 'en',
  38. sourcePath: 'docs/a.md',
  39. route: 'en/a.md',
  40. pages,
  41. repoRoot: root,
  42. repositoryRef: 'abc123',
  43. })).toBe(
  44. '[B](./reference/b.md#part) '
  45. + '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
  46. + '[web](https://example.com)\n',
  47. )
  48. })
  49. it('selects the published target in the current site locale', () => {
  50. const { root, pages } = fixture()
  51. expect(rewriteMarkdown('[B](b.md)\n', {
  52. locale: 'root',
  53. sourcePath: 'docs/a.md',
  54. route: 'a.md',
  55. pages,
  56. repoRoot: root,
  57. repositoryRef: 'abc123',
  58. })).toBe('[B](./reference-root/b.md)\n')
  59. })
  60. it('uses raw GitHub content for unpublished images', () => {
  61. const { root, pages } = fixture()
  62. expect(rewriteMarkdown('![logo](../packages/logo.svg)\n', {
  63. locale: 'en',
  64. sourcePath: 'docs/a.md',
  65. route: 'en/a.md',
  66. pages,
  67. repoRoot: root,
  68. repositoryRef: 'abc123',
  69. })).toBe('![logo](https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/abc123/packages/logo.svg)\n')
  70. })
  71. it('does not rewrite Markdown-looking text inside code fences', () => {
  72. const { root, pages } = fixture()
  73. const source = '```md\n[B](b.md)\n```\n'
  74. expect(rewriteMarkdown(source, {
  75. locale: 'en',
  76. sourcePath: 'docs/a.md',
  77. route: 'en/a.md',
  78. pages,
  79. repoRoot: root,
  80. repositoryRef: 'abc123',
  81. })).toBe(source)
  82. })
  83. it('replaces the destination token without changing repeated titles or escapes', () => {
  84. const { root, pages } = fixture()
  85. const source = '[title](b.md "b.md") [escaped](x\\(y\\).md)\n'
  86. expect(rewriteMarkdown(source, {
  87. locale: 'en',
  88. sourcePath: 'docs/a.md',
  89. route: 'en/a.md',
  90. pages,
  91. repoRoot: root,
  92. repositoryRef: 'abc123',
  93. })).toBe(
  94. '[title](./reference/b.md "b.md") '
  95. + '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
  96. )
  97. })
  98. it('routes a pair switcher across locales while ordinary links stay in locale', () => {
  99. const { root, pages } = fixture()
  100. writeFileSync(join(root, 'docs/a.zh.md'), '# A\n')
  101. const paired = pages.filter(page => page.source !== 'docs/a.md')
  102. paired.push(
  103. {
  104. locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'],
  105. route: 'guide/a.md', label: 'A', sidebar: 'zh-guide', section: 'Test', order: 1,
  106. },
  107. {
  108. locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', sourceAliases: ['docs/a.zh.md'],
  109. route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1,
  110. },
  111. )
  112. expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', {
  113. locale: 'root',
  114. sourcePath: 'docs/a.zh.md',
  115. route: 'guide/a.md',
  116. pages: paired,
  117. repoRoot: root,
  118. repositoryRef: 'abc123',
  119. })).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n')
  120. })
  121. it('fails loud when a relative target is missing', () => {
  122. const { root, pages } = fixture()
  123. expect(() => rewriteMarkdown('[missing](missing.md)\n', {
  124. locale: 'en',
  125. sourcePath: 'docs/a.md',
  126. route: 'en/a.md',
  127. pages,
  128. repoRoot: root,
  129. repositoryRef: 'abc123',
  130. })).toThrow('links to missing path "missing.md"')
  131. })
  132. })
  133. describe('docsPages locale routes', () => {
  134. it('publishes every route in both locales and selects paired user sources', () => {
  135. const byRoute = new Map(docsPages.map(page => [page.route, page]))
  136. for (const page of docsPages.filter(page => page.locale === 'root')) {
  137. const counterpart = byRoute.get(`en/${page.route}`)
  138. expect(counterpart, page.route).toBeDefined()
  139. expect(counterpart?.locale).toBe('en')
  140. if (page.source.startsWith('docs/user/')) {
  141. expect(page.source).toMatch(/\.zh\.md$/)
  142. expect(page.contentLocale).toBe('zh-CN')
  143. expect(counterpart?.source).toBe(page.source.replace(/\.zh\.md$/, '.md'))
  144. expect(counterpart?.contentLocale).toBe('en-US')
  145. } else {
  146. expect(counterpart?.source).toBe(page.source)
  147. expect(counterpart?.contentLocale).toBe(page.contentLocale)
  148. }
  149. }
  150. })
  151. it('publishes the Cordis core API under matching locale structures', () => {
  152. const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md']
  153. for (const file of files) {
  154. const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`)
  155. const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`)
  156. expect(root?.source).toBe(`docs/cordis-catalog/core/${file}`)
  157. expect(root?.section).toBe('Cordis API')
  158. expect(english?.source).toBe(root?.source)
  159. expect(english?.section).toBe('Cordis Core API')
  160. }
  161. })
  162. })
  163. describe('addProjectionFrontmatter', () => {
  164. it('adds frontmatter to an ordinary Markdown page', () => {
  165. expect(addProjectionFrontmatter('# Guide\n', 'docs/guide.md')).toBe(
  166. '---\neditSource: "docs/guide.md"\n---\n\n# Guide\n',
  167. )
  168. })
  169. it('extends existing VitePress frontmatter', () => {
  170. expect(addProjectionFrontmatter('---\nlayout: home\n---\n', 'docs/index.md')).toBe(
  171. '---\neditSource: "docs/index.md"\nlayout: home\n---\n',
  172. )
  173. })
  174. })
  175. describe('projectedPageContent', () => {
  176. const page = (sidebar: DocsPage['sidebar']): DocsPage => ({
  177. locale: 'root',
  178. contentLocale: 'zh-CN',
  179. source: 'docs/index.zh.md',
  180. route: 'index.md',
  181. label: 'Home',
  182. sidebar,
  183. section: 'Home',
  184. order: 0,
  185. })
  186. it('omits the source-only body from locale home pages', () => {
  187. expect(projectedPageContent(
  188. '---\nlayout: home\nhero:\n name: Harness\n---\n\n# Harness\n\n[English](index.md) | 中文\n',
  189. page(null),
  190. )).toBe('---\nlayout: home\nhero:\n name: Harness\n---\n')
  191. })
  192. it('keeps the full body for ordinary pages', () => {
  193. const markdown = '---\ntitle: Guide\n---\n\n# Guide\n'
  194. expect(projectedPageContent(markdown, page('zh-guide'))).toBe(markdown)
  195. })
  196. it('rejects a locale home source without frontmatter', () => {
  197. expect(() => projectedPageContent('# Harness\n', page(null)))
  198. .toThrow('locale home source "docs/index.zh.md" must start with YAML frontmatter')
  199. })
  200. })