) {
const entries = Object.entries(properties).filter(
([key, value]) => !EXCLUDED_PROPERTIES.has(key) && value !== null && value !== undefined && value !== ''
);
if (entries.length === 0) {
return '';
}
const rows = entries
.map(
([key, value]) =>
` ${key}${renderPropertyValue(value)}
`
)
.join('\n');
return `\n${rows}\n
\n\n`;
}
function renderBacklinks(
backlinks: ExportArtifactSet['notes'][number]['backlinks']
) {
if (backlinks.length === 0) {
return '';
}
const items = backlinks
.map(link => ` ${link.title}`)
.join('\n');
return `\n\n\n
LINKS TO THIS PAGE
\n${items}\n
\n`;
}
function rewriteStaticAssetPaths(markdown: string) {
return markdown
.replace(/\]\(((?:\.\.\/)*assets\/[^)]+)\)/g, (_match, assetPath) => {
return `](/${String(assetPath).replace(/^(?:\.\.\/)+/, '')})`;
})
.replace(
/(src|href)="((?:\.\.\/)*assets\/)/g,
(_match, attr, assetPath) => {
return `${attr}="/${String(assetPath).replace(/^(?:\.\.\/)+/, '')}`;
}
);
}
function isFrameworkHandledRoute(route: string) {
return route === '/404';
}
async function ensureCleanDir(dir: string) {
await fs.rm(dir, { recursive: true, force: true });
await fs.mkdir(dir, { recursive: true });
}
async function writeTemplateFiles(outputDir: string) {
await Promise.all(
Object.entries(STARLIGHT_TEMPLATE_FILES).map(
async ([relativePath, content]) => {
const outputPath = path.join(outputDir, relativePath);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, content, 'utf8');
}
)
);
}
async function writeDocs(outputDir: string, artifactSet: ExportArtifactSet) {
const docsDir = path.join(outputDir, DOCS_DIR);
for (const note of artifactSet.notes) {
if (isFrameworkHandledRoute(note.route)) {
continue;
}
if (
artifactSet.site.homepageRoute &&
artifactSet.site.homepageRoute !== '/' &&
note.route === '/'
) {
continue;
}
const relativePath = routeToDocPath(note.route);
const outputPath = path.join(docsDir, relativePath);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(
outputPath,
`${renderFrontmatter(note)}${renderProperties(note.properties)}${rewriteStaticAssetPaths(
stripLeadingH1(stripFrontmatter(note.markdown))
)}${renderBacklinks(note.backlinks)}`,
'utf8'
);
}
if (
artifactSet.site.homepageRoute &&
artifactSet.site.homepageRoute !== '/'
) {
const homepageNote = artifactSet.notes.find(
note => note.route === artifactSet.site.homepageRoute
);
if (homepageNote) {
const outputPath = path.join(docsDir, 'index.md');
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(
outputPath,
`${renderFrontmatter(homepageNote)}${renderProperties(homepageNote.properties)}${rewriteStaticAssetPaths(
stripLeadingH1(homepageNote.markdown)
)}${renderBacklinks(homepageNote.backlinks)}`,
'utf8'
);
}
}
}
async function copyAssets(outputDir: string, artifactSet: ExportArtifactSet) {
const assetsDir = path.join(outputDir, ASSETS_DIR);
for (const asset of artifactSet.assets) {
const relativeOutput = asset.outputPath.replace(/^assets\//, '');
const outputPath = path.join(assetsDir, relativeOutput);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.copyFile(asset.sourceUri.toFsPath(), outputPath);
}
}
async function writeSiteConfig(
outputDir: string,
artifactSet: ExportArtifactSet,
siteUrl?: string
) {
const outputPath = path.join(outputDir, GENERATED_DIR, 'site-config.json');
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(
outputPath,
JSON.stringify(
{
title: artifactSet.site.title ?? DEFAULT_TITLE,
description: artifactSet.site.description ?? DEFAULT_DESCRIPTION,
homepageRoute: artifactSet.site.homepageRoute,
siteUrl,
},
null,
2
),
'utf8'
);
}
async function writeRoutesManifest(
outputDir: string,
artifactSet: ExportArtifactSet
) {
const outputPath = path.join(outputDir, ROUTES_MANIFEST_PATH);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(
outputPath,
JSON.stringify(
artifactSet.routes
.filter(route => !isFrameworkHandledRoute(route.route))
.map(route => ({
sourcePath: route.sourceUri.path,
route: route.route,
})),
null,
2
),
'utf8'
);
}
async function writeGraphData(
outputDir: string,
artifactSet: ExportArtifactSet
) {
const outputPath = path.join(outputDir, GRAPH_DATA_PATH);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(
outputPath,
JSON.stringify(artifactSet.graph, null, 2),
'utf8'
);
}
async function writeGraphBundle(outputDir: string, bundlePath: string) {
const outputPath = path.join(outputDir, GRAPH_BUNDLE_PATH);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.copyFile(bundlePath, outputPath);
}
async function writeFavicon(outputDir: string, faviconPath: string) {
const outputPath = path.join(outputDir, FAVICON_PATH);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.copyFile(faviconPath, outputPath);
}
export async function writeStarlightSite(options: WriteStarlightSiteOptions) {
const includeProjectScaffold = options.includeProjectScaffold ?? true;
const docsDir = path.join(options.outputDir, DOCS_DIR);
const assetsDir = path.join(options.outputDir, ASSETS_DIR);
const generatedDir = path.join(options.outputDir, GENERATED_DIR);
if (includeProjectScaffold) {
await writeTemplateFiles(options.outputDir);
}
await ensureCleanDir(docsDir);
await ensureCleanDir(assetsDir);
await ensureCleanDir(generatedDir);
await writeDocs(options.outputDir, options.artifactSet);
await copyAssets(options.outputDir, options.artifactSet);
await writeGraphData(options.outputDir, options.artifactSet);
if (options.graphBundlePath) {
await writeGraphBundle(options.outputDir, options.graphBundlePath);
}
if (options.faviconPath) {
await writeFavicon(options.outputDir, options.faviconPath);
}
await writeSiteConfig(
options.outputDir,
options.artifactSet,
options.siteUrl
);
await writeRoutesManifest(options.outputDir, options.artifactSet);
}