diff --git a/scripts/check-missing-sdk-samples.mjs b/scripts/check-missing-sdk-samples.mjs index d85d112f1..44c135b0e 100644 --- a/scripts/check-missing-sdk-samples.mjs +++ b/scripts/check-missing-sdk-samples.mjs @@ -2,10 +2,11 @@ /** * Informational check – never fails. * - * Compares the sample keys in the local .code-samples.meilisearch.yaml with - * each SDK's .code-samples.meilisearch.yaml (fetched from GitHub). + * Collects code sample keys from doc imports that start with "CodeSamples" + * (e.g. "import CodeSamplesTenantTokenGuideSearchSdk1 from '...'"), then + * compares with each SDK's .code-samples.meilisearch.yaml (fetched from GitHub). * - * Lists, per SDK, which local sample keys are missing on the SDK side. + * Lists, per SDK, which imported sample keys are missing on the SDK side. */ import fs from 'fs'; @@ -26,14 +27,79 @@ const SDK = [ { label: 'Dart', project: 'meilisearch-dart' }, ]; -const LOCAL_YAML = path.join( - process.cwd(), - '.code-samples.meilisearch.yaml' -); +/** Keys that are not expected in SDKs (e.g. no-SDK / curl-only samples). Never reported as missing. */ +const NOT_MISSING_IN_SDK = new Set(['tenant_token_guide_search_no_sdk_1']); + +/** + * Convert a CodeSamples* component name to the YAML key (snake_case). + * e.g. CodeSamplesTenantTokenGuideSearchSdk1 → tenant_token_guide_search_sdk_1 + * CodeSamplesCreateAKey1 → create_a_key_1 + */ +function componentNameToKey(name) { + if (!name.startsWith('CodeSamples') || name.length <= 'CodeSamples'.length) { + return null; + } + const rest = name.slice('CodeSamples'.length); + let out = ''; + for (let i = 0; i < rest.length; i++) { + const c = rest[i]; + const prev = i > 0 ? rest[i - 1] : ''; + const prevLetter = (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z'); + const currUpper = c >= 'A' && c <= 'Z'; + const currDigit = c >= '0' && c <= '9'; + if (i > 0) { + if (currUpper) out += '_'; // _ before every uppercase (e.g. CreateAKey → create_a_key) + else if (currDigit && prevLetter) out += '_'; // _ before digit after letter (e.g. Sdk1 → sdk_1) + } + out += c.toLowerCase(); + } + return out; +} + +/** + * Collect code sample keys from doc imports starting with "CodeSamples". + * Scans .mdx and .md files for import lines and extracts component names. + */ +function getCodeSampleKeysFromDocImports() { + const keys = new Set(); + const componentRe = /CodeSamples[A-Za-z0-9]+/g; + const root = process.cwd(); + const skipDirs = new Set(['node_modules', 'generated-code-samples', '.git']); + + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + if (!skipDirs.has(e.name)) walk(full); + continue; + } + if (!e.name.endsWith('.mdx') && !e.name.endsWith('.md')) continue; + const content = fs.readFileSync(full, 'utf-8'); + const lines = content.split('\n'); + for (const line of lines) { + if (!line.includes('import') || !line.includes('from')) continue; + let m; + while ((m = componentRe.exec(line)) !== null) { + const key = componentNameToKey(m[0]); + if (key) keys.add(key); + } + } + } + } -// Load local sample keys -const localSamples = yaml.load(fs.readFileSync(LOCAL_YAML, 'utf-8')); -const localKeys = Object.keys(localSamples).sort(); + walk(root); + return [...keys].sort(); +} + +const expectedKeys = getCodeSampleKeysFromDocImports(); + +if (expectedKeys.length === 0) { + console.warn( + '⚠ No "CodeSamples*" imports found in the documentation.' + ); + process.exit(0); +} async function fetchYaml(url) { const res = await fetch(url); @@ -42,7 +108,7 @@ async function fetchYaml(url) { } console.log( - `Local .code-samples.meilisearch.yaml contains ${localKeys.length} sample(s).\n` + `Doc imports (CodeSamples*) reference ${expectedKeys.length} code sample key(s).\n` ); // Per-sample tracking: which SDKs are missing each sample @@ -62,7 +128,9 @@ for (const sdk of SDK) { } const remoteKeys = new Set(Object.keys(remoteSamples)); - const missing = localKeys.filter((key) => !remoteKeys.has(key)); + const missing = expectedKeys.filter( + (key) => !NOT_MISSING_IN_SDK.has(key) && !remoteKeys.has(key) + ); if (missing.length > 0) { console.log( diff --git a/scripts/check-unused-sdk-samples.mjs b/scripts/check-unused-sdk-samples.mjs index 12ca80c4c..8cae040c4 100644 --- a/scripts/check-unused-sdk-samples.mjs +++ b/scripts/check-unused-sdk-samples.mjs @@ -1,11 +1,14 @@ #!/usr/bin/env node /** * For each SDK, fetches its .code-samples.meilisearch.yaml from GitHub and - * checks if it contains sample keys that do NOT exist in the local - * .code-samples.meilisearch.yaml. + * checks if it contains sample keys that are neither in the local + * .code-samples.meilisearch.yaml nor used in the docs as a snippet. * - * Such samples are useless (the documentation does not reference them) and - * can be removed from the SDK. + * A sample is considered used if it is: + * - present in the local .code-samples.meilisearch.yaml, OR + * - referenced in the documentation (import or path to code_samples_.mdx). + * + * Samples that are unused can be removed from the SDKs. * * Exits with code 1 if any unused SDK samples are found. */ @@ -37,6 +40,40 @@ const LOCAL_YAML = path.join( const localSamples = yaml.load(fs.readFileSync(LOCAL_YAML, 'utf-8')); const localKeys = new Set(Object.keys(localSamples)); +/** + * Collect all code sample keys referenced in the documentation (snippets). + * Scans .mdx and .md files for paths like code_samples_.mdx. + */ +function getDocSnippetKeys() { + const keys = new Set(); + const snippetRe = /code_samples_([a-z0-9_]+)\.mdx/gi; + const root = process.cwd(); + const skipDirs = new Set(['node_modules', 'generated-code-samples', '.git']); + + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isDirectory()) { + if (!skipDirs.has(e.name)) walk(full); + continue; + } + if (!e.name.endsWith('.mdx') && !e.name.endsWith('.md')) continue; + const content = fs.readFileSync(full, 'utf-8'); + let m; + while ((m = snippetRe.exec(content)) !== null) { + keys.add(m[1]); + } + } + } + + walk(root); + return keys; +} + +const docSnippetKeys = getDocSnippetKeys(); +const usedKeys = new Set([...localKeys, ...docSnippetKeys]); + async function fetchYaml(url) { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); @@ -57,12 +94,12 @@ for (const sdk of SDK) { } const remoteKeys = Object.keys(remoteSamples); - const unused = remoteKeys.filter((key) => !localKeys.has(key)); + const unused = remoteKeys.filter((key) => !usedKeys.has(key)); if (unused.length > 0) { hasUnused = true; console.error( - `\n${sdk.label} (${sdk.project}): ${unused.length} sample(s) not in local .code-samples.meilisearch.yaml:` + `\n${sdk.label} (${sdk.project}): ${unused.length} unused sample(s) (not in local .code-samples.meilisearch.yaml nor referenced as snippet in docs):` ); for (const key of unused.sort()) { console.error(` - ${key}`); @@ -72,13 +109,13 @@ for (const sdk of SDK) { if (!hasUnused) { console.log( - 'OK: All SDK code samples exist in the local .code-samples.meilisearch.yaml.' + 'OK: All SDK code samples are used (in local .code-samples.meilisearch.yaml or referenced as snippet in docs).' ); process.exit(0); } console.error( - '\nThe samples listed above exist in SDK repos but NOT in the local ' + - '.code-samples.meilisearch.yaml. They are unused and can be removed from the SDKs.' + '\nThe samples listed above exist in SDK repos but are neither in the local ' + + '.code-samples.meilisearch.yaml nor referenced as snippets in the docs. They can be removed from the SDKs.' ); process.exit(1); diff --git a/scripts/generate-code-sample-snippets.mjs b/scripts/generate-code-sample-snippets.mjs index c35b6569f..adad27392 100644 --- a/scripts/generate-code-sample-snippets.mjs +++ b/scripts/generate-code-sample-snippets.mjs @@ -10,13 +10,13 @@ const SDK = [ project: 'documentation' }, { - language: 'javascript', + language: 'javascript', label: 'JS', project: 'meilisearch-js' }, { language: 'python', - label: 'Python', + label: 'Python', project: 'meilisearch-python' }, { @@ -61,7 +61,7 @@ const SDK = [ } ]; -const REPOS = SDK.map(sdk => +const REPOS = SDK.map(sdk => `https://raw.githubusercontent.com/meilisearch/${sdk.project}/main/${sdk.source || '.code-samples.meilisearch.yaml'}` ); @@ -81,17 +81,17 @@ function cleanSnippets() { console.log(`Cleaned ${files.length} existing code sample snippets.`); } +function loadLocalYaml(filePath) { + const content = fs.readFileSync(filePath, 'utf-8'); + return yaml.load(content); +} + async function fetchYaml(url) { const response = await fetch(url); if (!response.ok) throw new Error(`Failed to fetch samples for ${url}`); return yaml.load(await response.text()); } -function loadLocalYaml(filePath) { - const content = fs.readFileSync(filePath, 'utf-8'); - return yaml.load(content); -} - async function buildSnippets() { // Step 1: Clean existing snippets cleanSnippets(); @@ -107,8 +107,8 @@ async function buildSnippets() { const sdkInfo = SDK[i]; try { - const isLocal = sdkInfo.project === 'documentation'; - const snippets = isLocal + // Read local file for cURL samples (documentation project), fetch remote for all other SDKs + const snippets = sdkInfo.project === 'documentation' ? loadLocalYaml(path.join(process.cwd(), sdkInfo.source || '.code-samples.meilisearch.yaml')) : await fetchYaml(repoUrl); @@ -137,12 +137,12 @@ async function buildSnippets() { ${snippets.map(snippet => { // Split content into description and code if it contains a nested code block const parts = snippet.content.split('```'); - + if (parts.length > 1) { // handle samples with nested code blocks // Has description and code blocks const description = parts[0].trim(); const codeBlocks = parts.slice(1); - + // Join all parts back together, keeping the description at the top return ` \`\`\`text ${snippet.label}