|
| 1 | +import assert from 'node:assert/strict'; |
| 2 | +import fs from 'node:fs'; |
| 3 | +import os from 'node:os'; |
| 4 | +import path from 'node:path'; |
| 5 | +import { execFileSync } from 'node:child_process'; |
| 6 | +import test from 'node:test'; |
| 7 | + |
| 8 | +import { |
| 9 | + buildDocsBundle, |
| 10 | + buildIndexLine, |
| 11 | + derivePublicUrl, |
| 12 | + listDocPages, |
| 13 | + parseFrontmatter |
| 14 | +} from './build-docs-bundle.mjs'; |
| 15 | + |
| 16 | +test('parseFrontmatter reads quoted and unquoted fields', () => { |
| 17 | + const text = [ |
| 18 | + '---', |
| 19 | + 'title: "飞书/Lark"', |
| 20 | + 'description: "通过集成飞书自建应用"', |
| 21 | + 'url: https://status.flashcat.cloud', |
| 22 | + '---', |
| 23 | + '', |
| 24 | + '# body' |
| 25 | + ].join('\n'); |
| 26 | + |
| 27 | + assert.deepEqual(parseFrontmatter(text), { |
| 28 | + title: '飞书/Lark', |
| 29 | + description: '通过集成飞书自建应用', |
| 30 | + url: 'https://status.flashcat.cloud' |
| 31 | + }); |
| 32 | +}); |
| 33 | + |
| 34 | +test('parseFrontmatter returns no fields without a frontmatter block', () => { |
| 35 | + assert.deepEqual(parseFrontmatter('# just a heading\n'), {}); |
| 36 | +}); |
| 37 | + |
| 38 | +test('derivePublicUrl prefers frontmatter url, else derives from the path', () => { |
| 39 | + assert.equal( |
| 40 | + derivePublicUrl('zh/on-call/integration/instant-messaging/lark.mdx', {}), |
| 41 | + 'https://docs.flashduty.com/zh/on-call/integration/instant-messaging/lark' |
| 42 | + ); |
| 43 | + assert.equal( |
| 44 | + derivePublicUrl('zh/stsatuspage.mdx', { url: 'https://status.flashcat.cloud' }), |
| 45 | + 'https://status.flashcat.cloud' |
| 46 | + ); |
| 47 | +}); |
| 48 | + |
| 49 | +test('buildIndexLine omits the description separator when there is none', () => { |
| 50 | + assert.equal( |
| 51 | + buildIndexLine('zh/foo.mdx', { title: '标题', description: '描述' }), |
| 52 | + '- `zh/foo.mdx` — 标题: 描述 https://docs.flashduty.com/zh/foo' |
| 53 | + ); |
| 54 | + assert.equal( |
| 55 | + buildIndexLine('zh/foo.mdx', { title: '标题' }), |
| 56 | + '- `zh/foo.mdx` — 标题 https://docs.flashduty.com/zh/foo' |
| 57 | + ); |
| 58 | +}); |
| 59 | + |
| 60 | +test('listDocPages selects only .md/.mdx pages, excludes index pages and media', () => { |
| 61 | + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-bundle-pages-')); |
| 62 | + fs.mkdirSync(path.join(repoRoot, 'zh', 'on-call'), { recursive: true }); |
| 63 | + fs.writeFileSync(path.join(repoRoot, 'zh', 'a.mdx'), '# a\n'); |
| 64 | + fs.writeFileSync(path.join(repoRoot, 'zh', 'on-call', 'b.md'), '# b\n'); |
| 65 | + fs.writeFileSync(path.join(repoRoot, 'zh', 'index.mdx'), '# index\n'); |
| 66 | + fs.writeFileSync(path.join(repoRoot, 'zh', 'on-call', 'diagram.png'), 'not a page'); |
| 67 | + fs.writeFileSync(path.join(repoRoot, 'zh', '.DS_Store'), 'junk'); |
| 68 | + |
| 69 | + assert.deepEqual(listDocPages(repoRoot, 'zh'), ['zh/a.mdx', 'zh/on-call/b.md']); |
| 70 | +}); |
| 71 | + |
| 72 | +function hasTar() { |
| 73 | + try { |
| 74 | + execFileSync('tar', ['--version']); |
| 75 | + return true; |
| 76 | + } catch { |
| 77 | + return false; |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +test('buildDocsBundle produces the fixed archive layout with no media files', { skip: !hasTar() }, () => { |
| 82 | + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-bundle-repo-')); |
| 83 | + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'docs-bundle-work-')); |
| 84 | + |
| 85 | + fs.mkdirSync(path.join(repoRoot, 'zh', 'on-call'), { recursive: true }); |
| 86 | + fs.mkdirSync(path.join(repoRoot, 'en'), { recursive: true }); |
| 87 | + fs.mkdirSync(path.join(repoRoot, 'api-reference'), { recursive: true }); |
| 88 | + |
| 89 | + fs.writeFileSync( |
| 90 | + path.join(repoRoot, 'zh', 'on-call', 'lark.mdx'), |
| 91 | + '---\ntitle: "飞书/Lark"\ndescription: "接入飞书"\n---\n\nbody\n' |
| 92 | + ); |
| 93 | + fs.writeFileSync(path.join(repoRoot, 'zh', 'on-call', 'diagram.png'), 'not a page'); |
| 94 | + fs.writeFileSync( |
| 95 | + path.join(repoRoot, 'en', 'lark.mdx'), |
| 96 | + '---\ntitle: "Lark"\n---\n\nbody\n' |
| 97 | + ); |
| 98 | + fs.writeFileSync(path.join(repoRoot, 'api-reference', 'on-call.openapi.zh.json'), '{}\n'); |
| 99 | + fs.writeFileSync(path.join(repoRoot, 'api-reference', 'README.md'), 'not json\n'); |
| 100 | + fs.writeFileSync(path.join(repoRoot, 'glossary.md'), '# glossary\n'); |
| 101 | + |
| 102 | + const stagingDir = path.join(work, 'staging'); |
| 103 | + const outDir = path.join(work, 'out'); |
| 104 | + const result = buildDocsBundle({ repoRoot, stagingDir, outDir }); |
| 105 | + |
| 106 | + assert.equal(result.zhPages, 1); |
| 107 | + assert.equal(result.enPages, 1); |
| 108 | + assert.equal(result.apiReferenceFiles, 1); |
| 109 | + assert.ok(result.size > 0); |
| 110 | + |
| 111 | + const extractDir = path.join(work, 'extract'); |
| 112 | + fs.mkdirSync(extractDir, { recursive: true }); |
| 113 | + execFileSync('tar', ['-xzf', result.tarPath, '-C', extractDir]); |
| 114 | + |
| 115 | + assert.deepEqual(fs.readdirSync(extractDir).sort(), ['api-reference', 'en', 'glossary.md', 'zh']); |
| 116 | + assert.deepEqual(fs.readdirSync(path.join(extractDir, 'api-reference')), ['on-call.openapi.zh.json']); |
| 117 | + assert.ok(fs.existsSync(path.join(extractDir, 'zh', 'INDEX.md'))); |
| 118 | + assert.ok(fs.existsSync(path.join(extractDir, 'en', 'INDEX.md'))); |
| 119 | + assert.ok(!fs.existsSync(path.join(extractDir, 'zh', 'on-call', 'diagram.png'))); |
| 120 | + |
| 121 | + const zhIndex = fs.readFileSync(path.join(extractDir, 'zh', 'INDEX.md'), 'utf8'); |
| 122 | + assert.match(zhIndex, /`zh\/on-call\/lark\.mdx` — 飞书\/Lark: 接入飞书 https:\/\/docs\.flashduty\.com\/zh\/on-call\/lark/); |
| 123 | +}); |
0 commit comments