mirror of
https://github.com/coollabsio/coolify-docs.git
synced 2026-06-19 07:35:55 +00:00
245 lines
5.7 KiB
JavaScript
245 lines
5.7 KiB
JavaScript
import { readdir, readFile } from 'node:fs/promises'
|
|
import { existsSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
export const servicesDir = path.join(root, 'content/docs/services')
|
|
export const imagesDir = path.join(root, 'public/images/services')
|
|
|
|
const ignoredServiceFiles = new Set([
|
|
'all.mdx',
|
|
'introduction.mdx',
|
|
'overview.mdx',
|
|
])
|
|
|
|
const imageExtensions = new Set(['.svg', '.png', '.webp', '.jpg', '.jpeg'])
|
|
const iconSuffixes = ['-logo', '_logo', 'logo']
|
|
|
|
/**
|
|
* @typedef {Object} ServiceRecord
|
|
* @property {string} name
|
|
* @property {string} slug
|
|
* @property {string} icon
|
|
* @property {string} description
|
|
* @property {string} category
|
|
* @property {boolean} [disabled]
|
|
*/
|
|
|
|
/**
|
|
* Parse a scalar frontmatter value into a plain JavaScript value.
|
|
*
|
|
* @param {string} value
|
|
* @returns {string | boolean}
|
|
*/
|
|
function parseScalar(value) {
|
|
const trimmed = value.trim()
|
|
|
|
if (!trimmed) {
|
|
return ''
|
|
}
|
|
|
|
if (
|
|
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
) {
|
|
return trimmed.slice(1, -1)
|
|
}
|
|
|
|
if (trimmed === 'true') {
|
|
return true
|
|
}
|
|
|
|
if (trimmed === 'false') {
|
|
return false
|
|
}
|
|
|
|
return trimmed
|
|
}
|
|
|
|
/**
|
|
* Extract top-level scalar frontmatter keys from a markdown document.
|
|
*
|
|
* @param {string} markdown
|
|
* @param {string} file
|
|
* @returns {Record<string, string | boolean>}
|
|
*/
|
|
function parseFrontmatter(markdown, file) {
|
|
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
|
|
if (!match) {
|
|
throw new Error(`${file} is missing frontmatter`)
|
|
}
|
|
|
|
const frontmatter = {}
|
|
|
|
for (const line of match[1].split(/\r?\n/)) {
|
|
const valueMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)
|
|
|
|
if (!valueMatch) {
|
|
continue
|
|
}
|
|
|
|
const [, key, rawValue] = valueMatch
|
|
frontmatter[key] = parseScalar(rawValue)
|
|
}
|
|
|
|
return frontmatter
|
|
}
|
|
|
|
/**
|
|
* Normalize text for filename and slug comparisons.
|
|
*
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
function normalize(value) {
|
|
return value.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
}
|
|
|
|
/**
|
|
* Build a searchable index of available service image assets.
|
|
*
|
|
* @param {string[]} files
|
|
* @returns {{ file: string, normalizedBasename: string }[]}
|
|
*/
|
|
function buildImageIndex(files) {
|
|
return files
|
|
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
|
|
.map((file) => {
|
|
const extension = path.extname(file)
|
|
const basename = path.basename(file, extension)
|
|
|
|
return {
|
|
file,
|
|
normalizedBasename: normalize(basename),
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Resolve a service icon path from known image assets.
|
|
*
|
|
* @param {string} slug
|
|
* @param {string} title
|
|
* @param {{ file: string, normalizedBasename: string }[]} imageIndex
|
|
* @returns {string}
|
|
*/
|
|
function resolveIcon(slug, title, imageIndex) {
|
|
const candidates = [
|
|
...iconSuffixes.map((suffix) => `${slug}${suffix}`),
|
|
slug,
|
|
`${title}-logo`,
|
|
title,
|
|
].map(normalize)
|
|
|
|
for (const candidate of candidates) {
|
|
const match = imageIndex.find((image) => image.normalizedBasename === candidate)
|
|
|
|
if (match) {
|
|
return `/docs/images/services/${match.file}`
|
|
}
|
|
}
|
|
|
|
const fuzzy = imageIndex.find((image) => {
|
|
const normalizedSlug = normalize(slug)
|
|
|
|
return (
|
|
image.normalizedBasename.startsWith(`${normalizedSlug}logo`) ||
|
|
image.normalizedBasename.startsWith(normalizedSlug)
|
|
)
|
|
})
|
|
|
|
if (fuzzy) {
|
|
return `/docs/images/services/${fuzzy.file}`
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Fallback icon extraction from the service markdown body.
|
|
*
|
|
* @param {string} markdown
|
|
* @returns {string}
|
|
*/
|
|
function extractMarkdownIcon(markdown) {
|
|
const imageMatch =
|
|
markdown.match(/<ZoomableImage[^>]+src=["']([^"']+)["']/) ||
|
|
markdown.match(/!\[[^\]]*\]\(([^)\s]+)[^)]*\)/)
|
|
|
|
if (!imageMatch) {
|
|
return ''
|
|
}
|
|
|
|
const src = imageMatch[1]
|
|
|
|
if (src.startsWith('/docs/images/services/')) {
|
|
return src
|
|
}
|
|
|
|
if (src.startsWith('/images/services/')) {
|
|
return `/docs${src}`
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Determine whether a service should be marked as disabled in generated data.
|
|
*
|
|
* @param {Record<string, string | boolean>} frontmatter
|
|
* @param {string} markdown
|
|
* @returns {boolean}
|
|
*/
|
|
function isDisabledService(frontmatter, markdown) {
|
|
if (frontmatter.disabled === true) {
|
|
return true
|
|
}
|
|
|
|
return /SERVICE (HIDDEN|NOT AVAILABLE|REMOVED FROM COOLIFY|TEMPORARILY DISABLED)/i.test(markdown)
|
|
}
|
|
|
|
/**
|
|
* Collect normalized service metadata from service markdown files.
|
|
*
|
|
* @returns {Promise<ServiceRecord[]>}
|
|
*/
|
|
export async function collectServices() {
|
|
const [serviceFiles, imageFiles] = await Promise.all([
|
|
readdir(servicesDir),
|
|
existsSync(imagesDir) ? readdir(imagesDir) : [],
|
|
])
|
|
|
|
const imageIndex = buildImageIndex(imageFiles)
|
|
const services = []
|
|
|
|
for (const file of serviceFiles.sort()) {
|
|
if (!file.endsWith('.mdx') || ignoredServiceFiles.has(file)) {
|
|
continue
|
|
}
|
|
|
|
const slug = file.replace(/\.mdx$/, '')
|
|
const markdown = await readFile(path.join(servicesDir, file), 'utf8')
|
|
const frontmatter = parseFrontmatter(markdown, file)
|
|
const title = frontmatter.title || slug
|
|
const description = frontmatter.description || ''
|
|
const category = frontmatter.category || 'Uncategorized'
|
|
const icon = frontmatter.icon || resolveIcon(slug, title, imageIndex) || extractMarkdownIcon(markdown)
|
|
const disabled = isDisabledService(frontmatter, markdown)
|
|
|
|
services.push({
|
|
name: title,
|
|
slug,
|
|
icon,
|
|
description,
|
|
category,
|
|
...(disabled ? { disabled } : {}),
|
|
})
|
|
}
|
|
|
|
services.sort((a, b) => a.name.localeCompare(b.name, 'en'))
|
|
|
|
return services
|
|
}
|