Skip to content

Commit 87c3284

Browse files
committed
Improve CI scripts related code samples
1 parent 4c7c6a6 commit 87c3284

File tree

3 files changed

+138
-33
lines changed

3 files changed

+138
-33
lines changed

scripts/check-missing-sdk-samples.mjs

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
/**
33
* Informational check – never fails.
44
*
5-
* Compares the sample keys in the local .code-samples.meilisearch.yaml with
6-
* each SDK's .code-samples.meilisearch.yaml (fetched from GitHub).
5+
* Collects code sample keys from doc imports that start with "CodeSamples"
6+
* (e.g. "import CodeSamplesTenantTokenGuideSearchSdk1 from '...'"), then
7+
* compares with each SDK's .code-samples.meilisearch.yaml (fetched from GitHub).
78
*
8-
* Lists, per SDK, which local sample keys are missing on the SDK side.
9+
* Lists, per SDK, which imported sample keys are missing on the SDK side.
910
*/
1011

1112
import fs from 'fs';
@@ -26,14 +27,79 @@ const SDK = [
2627
{ label: 'Dart', project: 'meilisearch-dart' },
2728
];
2829

29-
const LOCAL_YAML = path.join(
30-
process.cwd(),
31-
'.code-samples.meilisearch.yaml'
32-
);
30+
/** Keys that are not expected in SDKs (e.g. no-SDK / curl-only samples). Never reported as missing. */
31+
const NOT_MISSING_IN_SDK = new Set(['tenant_token_guide_search_no_sdk_1']);
32+
33+
/**
34+
* Convert a CodeSamples* component name to the YAML key (snake_case).
35+
* e.g. CodeSamplesTenantTokenGuideSearchSdk1 → tenant_token_guide_search_sdk_1
36+
* CodeSamplesCreateAKey1 → create_a_key_1
37+
*/
38+
function componentNameToKey(name) {
39+
if (!name.startsWith('CodeSamples') || name.length <= 'CodeSamples'.length) {
40+
return null;
41+
}
42+
const rest = name.slice('CodeSamples'.length);
43+
let out = '';
44+
for (let i = 0; i < rest.length; i++) {
45+
const c = rest[i];
46+
const prev = i > 0 ? rest[i - 1] : '';
47+
const prevLetter = (prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z');
48+
const currUpper = c >= 'A' && c <= 'Z';
49+
const currDigit = c >= '0' && c <= '9';
50+
if (i > 0) {
51+
if (currUpper) out += '_'; // _ before every uppercase (e.g. CreateAKey → create_a_key)
52+
else if (currDigit && prevLetter) out += '_'; // _ before digit after letter (e.g. Sdk1 → sdk_1)
53+
}
54+
out += c.toLowerCase();
55+
}
56+
return out;
57+
}
58+
59+
/**
60+
* Collect code sample keys from doc imports starting with "CodeSamples".
61+
* Scans .mdx and .md files for import lines and extracts component names.
62+
*/
63+
function getCodeSampleKeysFromDocImports() {
64+
const keys = new Set();
65+
const componentRe = /CodeSamples[A-Za-z0-9]+/g;
66+
const root = process.cwd();
67+
const skipDirs = new Set(['node_modules', 'generated-code-samples', '.git']);
68+
69+
function walk(dir) {
70+
const entries = fs.readdirSync(dir, { withFileTypes: true });
71+
for (const e of entries) {
72+
const full = path.join(dir, e.name);
73+
if (e.isDirectory()) {
74+
if (!skipDirs.has(e.name)) walk(full);
75+
continue;
76+
}
77+
if (!e.name.endsWith('.mdx') && !e.name.endsWith('.md')) continue;
78+
const content = fs.readFileSync(full, 'utf-8');
79+
const lines = content.split('\n');
80+
for (const line of lines) {
81+
if (!line.includes('import') || !line.includes('from')) continue;
82+
let m;
83+
while ((m = componentRe.exec(line)) !== null) {
84+
const key = componentNameToKey(m[0]);
85+
if (key) keys.add(key);
86+
}
87+
}
88+
}
89+
}
3390

34-
// Load local sample keys
35-
const localSamples = yaml.load(fs.readFileSync(LOCAL_YAML, 'utf-8'));
36-
const localKeys = Object.keys(localSamples).sort();
91+
walk(root);
92+
return [...keys].sort();
93+
}
94+
95+
const expectedKeys = getCodeSampleKeysFromDocImports();
96+
97+
if (expectedKeys.length === 0) {
98+
console.warn(
99+
'⚠ No "CodeSamples*" imports found in the documentation.'
100+
);
101+
process.exit(0);
102+
}
37103

38104
async function fetchYaml(url) {
39105
const res = await fetch(url);
@@ -42,7 +108,7 @@ async function fetchYaml(url) {
42108
}
43109

44110
console.log(
45-
`Local .code-samples.meilisearch.yaml contains ${localKeys.length} sample(s).\n`
111+
`Doc imports (CodeSamples*) reference ${expectedKeys.length} code sample key(s).\n`
46112
);
47113

48114
// Per-sample tracking: which SDKs are missing each sample
@@ -62,7 +128,9 @@ for (const sdk of SDK) {
62128
}
63129

64130
const remoteKeys = new Set(Object.keys(remoteSamples));
65-
const missing = localKeys.filter((key) => !remoteKeys.has(key));
131+
const missing = expectedKeys.filter(
132+
(key) => !NOT_MISSING_IN_SDK.has(key) && !remoteKeys.has(key)
133+
);
66134

67135
if (missing.length > 0) {
68136
console.log(

scripts/check-unused-sdk-samples.mjs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env node
22
/**
33
* For each SDK, fetches its .code-samples.meilisearch.yaml from GitHub and
4-
* checks if it contains sample keys that do NOT exist in the local
5-
* .code-samples.meilisearch.yaml.
4+
* checks if it contains sample keys that are neither in the local
5+
* .code-samples.meilisearch.yaml nor used in the docs as a snippet.
66
*
7-
* Such samples are useless (the documentation does not reference them) and
8-
* can be removed from the SDK.
7+
* A sample is considered used if it is:
8+
* - present in the local .code-samples.meilisearch.yaml, OR
9+
* - referenced in the documentation (import or path to code_samples_<key>.mdx).
10+
*
11+
* Samples that are unused can be removed from the SDKs.
912
*
1013
* Exits with code 1 if any unused SDK samples are found.
1114
*/
@@ -37,6 +40,40 @@ const LOCAL_YAML = path.join(
3740
const localSamples = yaml.load(fs.readFileSync(LOCAL_YAML, 'utf-8'));
3841
const localKeys = new Set(Object.keys(localSamples));
3942

43+
/**
44+
* Collect all code sample keys referenced in the documentation (snippets).
45+
* Scans .mdx and .md files for paths like code_samples_<key>.mdx.
46+
*/
47+
function getDocSnippetKeys() {
48+
const keys = new Set();
49+
const snippetRe = /code_samples_([a-z0-9_]+)\.mdx/gi;
50+
const root = process.cwd();
51+
const skipDirs = new Set(['node_modules', 'generated-code-samples', '.git']);
52+
53+
function walk(dir) {
54+
const entries = fs.readdirSync(dir, { withFileTypes: true });
55+
for (const e of entries) {
56+
const full = path.join(dir, e.name);
57+
if (e.isDirectory()) {
58+
if (!skipDirs.has(e.name)) walk(full);
59+
continue;
60+
}
61+
if (!e.name.endsWith('.mdx') && !e.name.endsWith('.md')) continue;
62+
const content = fs.readFileSync(full, 'utf-8');
63+
let m;
64+
while ((m = snippetRe.exec(content)) !== null) {
65+
keys.add(m[1]);
66+
}
67+
}
68+
}
69+
70+
walk(root);
71+
return keys;
72+
}
73+
74+
const docSnippetKeys = getDocSnippetKeys();
75+
const usedKeys = new Set([...localKeys, ...docSnippetKeys]);
76+
4077
async function fetchYaml(url) {
4178
const res = await fetch(url);
4279
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
@@ -57,12 +94,12 @@ for (const sdk of SDK) {
5794
}
5895

5996
const remoteKeys = Object.keys(remoteSamples);
60-
const unused = remoteKeys.filter((key) => !localKeys.has(key));
97+
const unused = remoteKeys.filter((key) => !usedKeys.has(key));
6198

6299
if (unused.length > 0) {
63100
hasUnused = true;
64101
console.error(
65-
`\n${sdk.label} (${sdk.project}): ${unused.length} sample(s) not in local .code-samples.meilisearch.yaml:`
102+
`\n${sdk.label} (${sdk.project}): ${unused.length} unused sample(s) (not in local .code-samples.meilisearch.yaml nor referenced as snippet in docs):`
66103
);
67104
for (const key of unused.sort()) {
68105
console.error(` - ${key}`);
@@ -72,13 +109,13 @@ for (const sdk of SDK) {
72109

73110
if (!hasUnused) {
74111
console.log(
75-
'OK: All SDK code samples exist in the local .code-samples.meilisearch.yaml.'
112+
'OK: All SDK code samples are used (in local .code-samples.meilisearch.yaml or referenced as snippet in docs).'
76113
);
77114
process.exit(0);
78115
}
79116

80117
console.error(
81-
'\nThe samples listed above exist in SDK repos but NOT in the local ' +
82-
'.code-samples.meilisearch.yaml. They are unused and can be removed from the SDKs.'
118+
'\nThe samples listed above exist in SDK repos but are neither in the local ' +
119+
'.code-samples.meilisearch.yaml nor referenced as snippets in the docs. They can be removed from the SDKs.'
83120
);
84121
process.exit(1);

scripts/generate-code-sample-snippets.mjs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ const SDK = [
1010
project: 'documentation'
1111
},
1212
{
13-
language: 'javascript',
13+
language: 'javascript',
1414
label: 'JS',
1515
project: 'meilisearch-js'
1616
},
1717
{
1818
language: 'python',
19-
label: 'Python',
19+
label: 'Python',
2020
project: 'meilisearch-python'
2121
},
2222
{
@@ -61,7 +61,7 @@ const SDK = [
6161
}
6262
];
6363

64-
const REPOS = SDK.map(sdk =>
64+
const REPOS = SDK.map(sdk =>
6565
`https://raw.githubusercontent.com/meilisearch/${sdk.project}/main/${sdk.source || '.code-samples.meilisearch.yaml'}`
6666
);
6767

@@ -81,17 +81,17 @@ function cleanSnippets() {
8181
console.log(`Cleaned ${files.length} existing code sample snippets.`);
8282
}
8383

84+
function loadLocalYaml(filePath) {
85+
const content = fs.readFileSync(filePath, 'utf-8');
86+
return yaml.load(content);
87+
}
88+
8489
async function fetchYaml(url) {
8590
const response = await fetch(url);
8691
if (!response.ok) throw new Error(`Failed to fetch samples for ${url}`);
8792
return yaml.load(await response.text());
8893
}
8994

90-
function loadLocalYaml(filePath) {
91-
const content = fs.readFileSync(filePath, 'utf-8');
92-
return yaml.load(content);
93-
}
94-
9595
async function buildSnippets() {
9696
// Step 1: Clean existing snippets
9797
cleanSnippets();
@@ -107,8 +107,8 @@ async function buildSnippets() {
107107
const sdkInfo = SDK[i];
108108

109109
try {
110-
const isLocal = sdkInfo.project === 'documentation';
111-
const snippets = isLocal
110+
// Read local file for cURL samples (documentation project), fetch remote for all other SDKs
111+
const snippets = sdkInfo.project === 'documentation'
112112
? loadLocalYaml(path.join(process.cwd(), sdkInfo.source || '.code-samples.meilisearch.yaml'))
113113
: await fetchYaml(repoUrl);
114114

@@ -137,12 +137,12 @@ async function buildSnippets() {
137137
${snippets.map(snippet => {
138138
// Split content into description and code if it contains a nested code block
139139
const parts = snippet.content.split('```');
140-
140+
141141
if (parts.length > 1) { // handle samples with nested code blocks
142142
// Has description and code blocks
143143
const description = parts[0].trim();
144144
const codeBlocks = parts.slice(1);
145-
145+
146146
// Join all parts back together, keeping the description at the top
147147
return `
148148
\`\`\`text ${snippet.label}

0 commit comments

Comments
 (0)