Skip to main content
Glama
generateEntry.ts8.44 kB
/* -------------------------------------------------------------------------- * docs/tools/generateEntry.ts * * Completely re-written generator that now produces *two* artefacts for every * documentation category (blog, docs, frequent_questions, legal): * 1. A `<category>.entry.ts` file listing every markdown document and * exposing a `localeRecord` that asynchronously reads its content for * any locale. * 2. A `<category>.types.ts` file providing an exhaustive TypeScript type * describing the front-matter metadata of every document for all * locales. * * The generator is invoked from the `prepare` script of the `docs` package * and must therefore be 100 % deterministic. * ------------------------------------------------------------------------- */ import { spawn } from 'node:child_process'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { localeMap } from '@intlayer/core'; import * as fg from 'fast-glob'; import { locales } from '../intlayer.config'; /* -------------------------------------------------------------------------- */ /* TYPES */ /* -------------------------------------------------------------------------- */ interface CategoryConfig { /** camelCase identifier used in variable names */ name: 'blog' | 'docs' | 'frequentQuestions' | 'legal'; /** folder name on disk */ dir: 'blog' | 'docs' | 'frequent_questions' | 'legal'; /** constant exported from the generated entry file */ constName: string; /** path to the generated entry file */ entryFilePath: string; } /* -------------------------------------------------------------------------- */ /* CONFIGURATION */ /* -------------------------------------------------------------------------- */ const categories: CategoryConfig[] = [ { name: 'blog', dir: 'blog', constName: 'blogEntry', entryFilePath: './src/generated/blog.entry.ts', }, { name: 'docs', dir: 'docs', constName: 'docsEntry', entryFilePath: './src/generated/docs.entry.ts', }, { name: 'frequentQuestions', dir: 'frequent_questions', constName: 'frequentQuestionsEntry', entryFilePath: './src/generated/frequentQuestions.entry.ts', }, { name: 'legal', dir: 'legal', constName: 'legalEntry', entryFilePath: './src/generated/legal.entry.ts', }, ]; /* -------------------------------------------------------------------------- */ /* HELPERS – ENTRY FILES GENERATION */ /* -------------------------------------------------------------------------- */ /** * Format content using Biome CLI. Falls back to the original content if Biome * is unavailable or formatting fails for any reason. */ const formatWithBiome = async ( content: string, filePathForConfig: string ): Promise<string> => { try { const child = spawn( 'biome', ['format', '--stdin-file-path', filePathForConfig], { stdio: ['pipe', 'pipe', 'pipe'], } ); let stdout = ''; let stderr = ''; if (child.stdout) { child.stdout.setEncoding('utf8'); child.stdout.on('data', (chunk) => { stdout += chunk as string; }); } if (child.stderr) { child.stderr.setEncoding('utf8'); child.stderr.on('data', (chunk) => { stderr += chunk as string; }); } const completion = new Promise<string>((resolve, reject) => { child.on('error', reject); child.on('close', (code) => { if (code === 0) { resolve(stdout); } else { reject(new Error(stderr || `biome exited with code ${code}`)); } }); }); if (child.stdin) { child.stdin.write(content); child.stdin.end(); } return await completion; } catch { return content; } }; const buildEntryContent = ( { constName, dir }: CategoryConfig, englishFiles: string[] ): string => { const header = [ `/* AUTO-GENERATED – DO NOT EDIT */`, `/* REGENERATE USING \`pnpm prepare\` */`, `import { existsSync } from 'node:fs';`, `import { readFile } from 'node:fs/promises';`, `import { join, dirname as pathDirname } from 'node:path';`, `import { fileURLToPath } from 'node:url';`, `import { getPackageJsonPath, getProjectRequire } from '@intlayer/config';`, `import type { LocalesValues } from '@intlayer/types';`, ``, `// Robustly resolve the base directory of the @intlayer/docs package in both`, `// bundled environments (Next.js) and standalone CLIs (MCP via npx).`, `const currentDir = typeof __dirname !== 'undefined'`, ` ? __dirname`, ` : pathDirname(fileURLToPath(import.meta.url));`, ``, `let baseDir: string;`, `try {`, ` // Prefer resolving from the location of this file (works for CLIs).`, ` const projectRequire = getProjectRequire(currentDir);`, ` const docEntryPath = projectRequire.resolve('@intlayer/docs');`, ` baseDir = getPackageJsonPath(docEntryPath).baseDir;`, `} catch {`, ` try {`, ` // Fallback: resolve from the consumer project (works for apps/bundlers).`, ` const projectRequire = getProjectRequire();`, ` const docEntryPath = projectRequire.resolve('@intlayer/docs');`, ` baseDir = getPackageJsonPath(docEntryPath).baseDir;`, ` } catch {`, ` // Last resort: walk up from currentDir (useful when executed inside @intlayer/docs).`, ` baseDir = getPackageJsonPath(currentDir).baseDir;`, ` }`, `}`, ``, `const readLocale = (relativeAfterLocale: string, locale: LocalesValues): Promise<string> => {`, ` const target1 = join(baseDir, \`./${dir}/\${locale}/\${relativeAfterLocale}\`);`, ` if (existsSync(target1)) {`, ` return readFile(target1, 'utf8');`, ` }`, ` const target2 = join(baseDir, \`./${dir}/en/\${relativeAfterLocale}\`);`, ` if (existsSync(target2)) {`, ` return readFile(target2, 'utf8');`, ` }`, ``, ` return Promise.reject(new Error(\`[docs] File not found: \${relativeAfterLocale} - locale: \${locale} - path: \${target1} - path: \${target2}\`));`, `};`, ``, `\nexport const ${constName} = {\n`, ].join('\n'); const lines = englishFiles .sort() .map((file) => { const relativeAfterLocale = file.replace(`./${dir}/en/`, ''); const localeList = localeMap( ({ locale }) => `\n '${locale}': readLocale('${relativeAfterLocale}', '${locale}')`, locales ); return ` '${file}': {${localeList.join(',')}\n } as unknown as Record<LocalesValues, Promise<string>>,`; }) .join('\n'); const footer = `\n} as const;\n`; return header + lines + footer; }; /* -------------------------------------------------------------------------- */ /* MAIN */ /* -------------------------------------------------------------------------- */ const generate = async () => { console.log('🔄 Generating entry & type files…'); for (const cfg of categories) { /* ----------------------------- entry file ------------------------------ */ const englishPattern = `./${cfg.dir}/en/**/*.md`; const englishFiles = fg.sync(englishPattern, { ignore: ['**/_*'], }); const entryContent = buildEntryContent(cfg, englishFiles); await mkdir(dirname(cfg.entryFilePath), { recursive: true }); /* ----------------------------- format with Biome ----------------------------- */ const formattedEntryContent = await formatWithBiome( entryContent, cfg.entryFilePath ); try { const currentContent = await readFile(cfg.entryFilePath, 'utf-8'); if (currentContent !== formattedEntryContent) { await writeFile(cfg.entryFilePath, formattedEntryContent, 'utf-8'); console.log(`✍️ Updated ${cfg.entryFilePath}`); } } catch { // No existing file – write it await writeFile(cfg.entryFilePath, formattedEntryContent, 'utf-8'); console.log(`✍️ Created ${cfg.entryFilePath}`); } } console.log('🎉 Done!'); }; generate().catch((error) => { console.error(error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server