computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
184 lines
5.1 KiB
184 lines
5.1 KiB
import fs from 'node:fs/promises'; |
|
import path from 'node:path'; |
|
import { fileURLToPath } from 'node:url'; |
|
import type { Node } from '@xyflow/react'; |
|
import matter from 'gray-matter'; |
|
import type { RoadmapFrontmatter } from '../src/lib/roadmap'; |
|
import { slugify } from '../src/lib/slugger'; |
|
import { markdownToHtml } from '../src/lib/markdown'; |
|
import { HTMLElement, parse } from 'node-html-parser'; |
|
import { htmlToMarkdown } from '../src/lib/html'; |
|
|
|
const __filename = fileURLToPath(import.meta.url); |
|
const __dirname = path.dirname(__filename); |
|
|
|
export const allowedLinkTypes = [ |
|
'video', |
|
'article', |
|
'opensource', |
|
'course', |
|
'website', |
|
'podcast', |
|
] as const; |
|
|
|
// Directory containing the roadmaps |
|
const ROADMAP_CONTENT_DIR = path.join(__dirname, '../src/data/roadmaps'); |
|
const allRoadmaps = await fs.readdir(ROADMAP_CONTENT_DIR); |
|
|
|
const editorRoadmapIds = new Set<string>(); |
|
for (const roadmapId of allRoadmaps) { |
|
const roadmapFrontmatterDir = path.join( |
|
ROADMAP_CONTENT_DIR, |
|
roadmapId, |
|
`${roadmapId}.md`, |
|
); |
|
const roadmapFrontmatterRaw = await fs.readFile( |
|
roadmapFrontmatterDir, |
|
'utf-8', |
|
); |
|
const { data } = matter(roadmapFrontmatterRaw); |
|
|
|
const roadmapFrontmatter = data as RoadmapFrontmatter; |
|
if (roadmapFrontmatter.renderer === 'editor') { |
|
editorRoadmapIds.add(roadmapId); |
|
} |
|
} |
|
|
|
const publicRoadmapsContentDir = path.join('./public', 'roadmap-content'); |
|
const stats = await fs.stat(publicRoadmapsContentDir).catch(() => null); |
|
if (!stats || !stats.isDirectory()) { |
|
await fs.mkdir(publicRoadmapsContentDir, { recursive: true }); |
|
} |
|
|
|
for (const roadmapId of editorRoadmapIds) { |
|
console.log(`🚀 Starting ${roadmapId}`); |
|
|
|
const roadmapDir = path.join( |
|
ROADMAP_CONTENT_DIR, |
|
roadmapId, |
|
`${roadmapId}.json`, |
|
); |
|
const roadmapContent = await fs.readFile(roadmapDir, 'utf-8'); |
|
let { nodes } = JSON.parse(roadmapContent) as { |
|
nodes: Node[]; |
|
}; |
|
nodes = nodes.filter( |
|
(node) => |
|
node?.type && |
|
['topic', 'subtopic', 'todo'].includes(node.type) && |
|
node.data?.label, |
|
); |
|
|
|
const roadmapContentDir = path.join( |
|
ROADMAP_CONTENT_DIR, |
|
roadmapId, |
|
'content', |
|
); |
|
const stats = await fs.stat(roadmapContentDir).catch(() => null); |
|
if (!stats || !stats.isDirectory()) { |
|
await fs.mkdir(roadmapContentDir, { recursive: true }); |
|
} |
|
|
|
const roadmapContentFiles = await fs.readdir(roadmapContentDir, { |
|
recursive: true, |
|
}); |
|
|
|
const contentMap: Record< |
|
string, |
|
{ |
|
title: string; |
|
description: string; |
|
links: { |
|
title: string; |
|
url: string; |
|
type: string; |
|
}[]; |
|
} |
|
> = {}; |
|
|
|
for (const node of nodes) { |
|
const ndoeDirPatterWithoutExt = `${slugify(node.data.label)}@${node.id}`; |
|
const nodeDirPattern = `${ndoeDirPatterWithoutExt}.md`; |
|
if (!roadmapContentFiles.includes(nodeDirPattern)) { |
|
contentMap[nodeDirPattern] = { |
|
title: node.data.label, |
|
description: '', |
|
links: [], |
|
}; |
|
|
|
continue; |
|
} |
|
|
|
const content = await fs.readFile( |
|
path.join(roadmapContentDir, nodeDirPattern), |
|
'utf-8', |
|
); |
|
const html = markdownToHtml(content, false); |
|
const rootHtml = parse(html); |
|
|
|
let ulWithLinks: HTMLElement | undefined; |
|
rootHtml.querySelectorAll('ul').forEach((ul) => { |
|
const listWithJustLinks = Array.from(ul.querySelectorAll('li')).filter( |
|
(li) => { |
|
const link = li.querySelector('a'); |
|
return link && link.textContent?.trim() === li.textContent?.trim(); |
|
}, |
|
); |
|
|
|
if (listWithJustLinks.length > 0) { |
|
ulWithLinks = ul; |
|
} |
|
}); |
|
|
|
const listLinks = |
|
ulWithLinks !== undefined |
|
? Array.from(ulWithLinks.querySelectorAll('li > a')) |
|
.map((link) => { |
|
const typePattern = /@([a-z.]+)@/; |
|
let linkText = link.textContent || ''; |
|
const linkHref = link.getAttribute('href') || ''; |
|
let linkType = linkText.match(typePattern)?.[1] || 'article'; |
|
linkType = allowedLinkTypes.includes(linkType as any) |
|
? linkType |
|
: 'article'; |
|
|
|
linkText = linkText.replace(typePattern, ''); |
|
|
|
return { |
|
title: linkText, |
|
url: linkHref, |
|
type: linkType, |
|
}; |
|
}) |
|
.sort((a, b) => { |
|
const order = [ |
|
'official', |
|
'opensource', |
|
'article', |
|
'video', |
|
'feed', |
|
]; |
|
return order.indexOf(a.type) - order.indexOf(b.type); |
|
}) |
|
: []; |
|
|
|
const title = rootHtml.querySelector('h1'); |
|
ulWithLinks?.remove(); |
|
title?.remove(); |
|
const htmlStringWithoutLinks = rootHtml.toString(); |
|
const description = htmlToMarkdown(htmlStringWithoutLinks); |
|
|
|
contentMap[node.id] = { |
|
title: node.data.label, |
|
description, |
|
links: listLinks, |
|
}; |
|
} |
|
|
|
await fs.writeFile( |
|
path.join(publicRoadmapsContentDir, `${roadmapId}.json`), |
|
JSON.stringify(contentMap, null, 2), |
|
); |
|
console.log(`✅ Finished ${roadmapId}`); |
|
console.log('-'.repeat(20)); |
|
}
|
|
|