Files
supabase/apps/docs/features/docs/Troubleshooting.utils.common.mjs
T
Charis 75567e4075 chore(docs): remove unnecessary toml package (#35970)
We have two packages for handling toml, which is a bit unnecessary.
Settle on smol-toml as it is under more active maintenance.
2025-05-27 11:57:27 -04:00

199 lines
5.4 KiB
JavaScript

// @ts-check
/**
* This file is for utils needed in both the Next.js app build and the
* troubleshooting sync script. Because of unsolved problems with imports, the
* script is a mjs file instead of a ts file. Any dependencies that are needed
* in both places are defined here, and then typed in Troubleshooting.utils.ts
* as required.
*/
import matter from 'gray-matter'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm'
import { mdxFromMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { gfm } from 'micromark-extension-gfm'
import { mdxjs } from 'micromark-extension-mdxjs'
import { readdir, readFile, stat } from 'node:fs/promises'
import { join, sep } from 'node:path'
import { parse } from 'smol-toml'
import { visit } from 'unist-util-visit'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
export const TROUBLESHOOTING_DIRECTORY = join(process.cwd(), 'content/troubleshooting')
/**
* @typedef {Object} TroubleshootingEntry
* @property {string} filePath
* @property {string} content
* @property {string} contentWithoutJsx
* @property {TroubleshootingMetadata} data
*/
/**
* @typedef {Object} TroubleshootingMetadata
* @property {string} title
* @property {string[]} topics
* @property {string[]} [keywords]
* @property {Object} [api]
* @property {string[]} [api.sdk]
* @property {string[]} [api.management_api]
* @property {string[]} [api.cli]
* @property {AssociatedError[]} [errors]
* @property {string} database_id
* @property {string} [github_url]
* @property {Date} [date_created]
*/
/**
* @typedef {Object} AssociatedError
* @property {number} [http_status_code]
* @property {string} [code]
* @property {string} [message]
*/
export const TroubleshootingSchema = z
.object({
title: z.string(),
topics: z.array(
z.enum([
'ai',
'auth',
'branching',
'cli',
'database',
'functions',
'platform',
'realtime',
'self-hosting',
'storage',
'studio',
'supavisor',
'terraform',
])
),
keywords: z.array(z.string()).optional(),
api: z
.object({
sdk: z.array(z.string()).optional(),
management_api: z.array(z.string()).optional(),
cli: z.array(z.string()).optional(),
})
.strict()
.optional(),
errors: z
.array(
z
.object({
http_status_code: z.number().optional(),
code: z.string().optional(),
message: z.string().optional(),
})
.strict()
)
.optional(),
database_id: z.string().default(`pseudo-${uuidv4()}`),
github_url: z.string().url().optional(),
date_created: z.date({ coerce: true }).optional(),
})
.strict()
/**
* @param {unknown} troubleshootingMetadata
*/
function validateTroubleshootingMetadata(troubleshootingMetadata) {
return TroubleshootingSchema.safeParse(troubleshootingMetadata)
}
export async function getAllTroubleshootingEntriesInternal() {
const troubleshootingDirectoryContents = await readdir(TROUBLESHOOTING_DIRECTORY, {
recursive: true,
})
const troubleshootingFiles = troubleshootingDirectoryContents.map(async (entry) => {
const isHidden = entry.startsWith('_')
if (isHidden) return null
const filePath = join(TROUBLESHOOTING_DIRECTORY, entry)
const isFile = (await stat(filePath)).isFile()
if (!isFile) return null
const fileContents = await readFile(filePath, 'utf-8')
const { content, data: frontmatter } = matter(fileContents, {
language: 'toml',
engines: { toml: parse },
})
const parseResult = validateTroubleshootingMetadata(frontmatter)
if ('error' in parseResult) {
console.error(
`Error validating troubleshooting metadata\nEntry:%O\nError:%O`,
frontmatter,
parseResult.error
)
return null
}
const mdxTree = fromMarkdown(content, {
extensions: [gfm(), mdxjs()],
mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()],
})
visit(mdxTree, (node) => {
if ('children' in node) {
node.children = node.children.filter(
(child) =>
![
'mdxJsxFlowExpression',
'mdxJsxTextExpression',
'mdxFlowExpression',
'mdxTextExpression',
'mdxJsxFlowElement',
'mdxJsxTextElement',
'mdxJsxExpressionAttribute',
'mdxJsxAttribute',
'mdxJsxAttributeValueExpression',
'mdxjsEsm',
].includes(child.type)
)
}
if (node.type === 'link' || node.type === 'image') {
canonicalizeUrl(node)
}
})
const contentWithoutJsx = toMarkdown(mdxTree, {
extensions: [gfmToMarkdown()],
})
return {
filePath,
content,
contentWithoutJsx,
data: parseResult.data,
}
})
return (await Promise.all(troubleshootingFiles)).filter((x) => x != null)
}
/**
*
* @param {import('mdast').Image | import('mdast').Link} node
*/
function canonicalizeUrl(node) {
if (node.url.startsWith('/')) {
node.url === 'https://supabase.com' + node.url
}
}
/**
* @param {TroubleshootingEntry} entry
*/
export function getArticleSlug(entry) {
const parts = entry.filePath.split(sep)
return parts[parts.length - 1].replace(/\.mdx$/, '')
}