Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 80 additions & 12 deletions scripts/check-missing-sdk-samples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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(
Expand Down
55 changes: 46 additions & 9 deletions scripts/check-unused-sdk-samples.mjs
Original file line number Diff line number Diff line change
@@ -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_<key>.mdx).
*
* Samples that are unused can be removed from the SDKs.
*
* Exits with code 1 if any unused SDK samples are found.
*/
Expand Down Expand Up @@ -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_<key>.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}`);
Expand All @@ -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}`);
Expand All @@ -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);
24 changes: 12 additions & 12 deletions scripts/generate-code-sample-snippets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
{
Expand Down Expand Up @@ -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'}`
);

Expand All @@ -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();
Expand All @@ -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);

Expand Down Expand Up @@ -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}
Expand Down