Skip to content
Open
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
09ba8f9
solved issue #3586, Optimize directory traversal in check-edit-links …
NalinDalal May 25, 2025
9b26d43
solved issue #3586, Optimize directory traversal in check-edit-links …
NalinDalal May 25, 2025
2d18752
Merge branch 'asyncapi:master' into master
NalinDalal May 29, 2025
f2c3261
Merge branch 'asyncapi:master' into master
NalinDalal May 31, 2025
c8d889f
Merge branch 'asyncapi:master' into master
NalinDalal Jun 2, 2025
5d440e6
Merge branch 'asyncapi:master' into master
NalinDalal Jun 6, 2025
2bbf7c7
added code comments for comment convention
NalinDalal Jun 7, 2025
5711324
Merge branch 'asyncapi:master' into master
NalinDalal Jun 8, 2025
7a57eae
Merge branch 'master' into master
NalinDalal Jun 9, 2025
e0d9f71
Merge branch 'asyncapi:master' into master
NalinDalal Jun 14, 2025
269f1c9
Merge branch 'asyncapi:master' into master
NalinDalal Jun 18, 2025
d0f4955
Merge branch 'asyncapi:master' into master
NalinDalal Jun 24, 2025
9dababd
Merge branch 'asyncapi:master' into master
NalinDalal Jun 25, 2025
4f4cf25
Merge branch 'asyncapi:master' into master
NalinDalal Jul 1, 2025
23cb94b
Merge branch 'asyncapi:master' into master
NalinDalal Jul 3, 2025
474bafa
Merge branch 'master' into master
NalinDalal Jul 27, 2025
5e9c460
Merge branch 'asyncapi:master' into master
NalinDalal Jul 28, 2025
dca5523
remove the redundancy
NalinDalal Jul 28, 2025
f4386fd
updated the code and test
NalinDalal Jul 28, 2025
3582f6b
updated the code and test
NalinDalal Jul 28, 2025
9882c1b
Merge branch 'master' into master
NalinDalal Jul 28, 2025
c73319f
Merge branch 'asyncapi:master' into master
NalinDalal Jul 29, 2025
8ab20d5
Merge branch 'asyncapi:master' into master
NalinDalal Aug 3, 2025
78982f8
update the yml file for secrets; hopefully it will work
NalinDalal Aug 4, 2025
f14dd4f
Merge branch 'master' into master
NalinDalal Aug 5, 2025
58c4915
Merge branch 'master' into master
NalinDalal Aug 20, 2025
4c59396
conflict resolve
NalinDalal Aug 20, 2025
a378ea2
Merge branch 'asyncapi:master' into master
NalinDalal Aug 27, 2025
7b08f05
Merge branch 'asyncapi:master' into master
NalinDalal Aug 28, 2025
860917a
Merge branch 'master' into master
NalinDalal Aug 28, 2025
e03f548
Merge branch 'asyncapi:master' into master
NalinDalal Aug 29, 2025
e32dae2
remove unnecessary changes due to lsp
NalinDalal Aug 30, 2025
cdc2514
Merge branch 'master' of https://github.com/NalinDalal/website
NalinDalal Aug 30, 2025
03e08fa
comment conventions updated
NalinDalal Aug 30, 2025
9eb9def
removed excess cnahges, i think now this shouldn't reflect in pr
NalinDalal Aug 30, 2025
e03e1dd
check
NalinDalal Aug 30, 2025
2b46ce7
Merge branch 'master' into master
NalinDalal Aug 31, 2025
e28613d
Merge branch 'asyncapi:master' into master
NalinDalal Sep 4, 2025
4999062
Merge branch 'master' into master
NalinDalal Sep 15, 2025
45f1d44
Merge branch 'asyncapi:master' into master
NalinDalal Sep 16, 2025
a18b604
Merge branch 'master' into master
NalinDalal Sep 17, 2025
5c7a877
Merge branch 'asyncapi:master' into master
NalinDalal Oct 9, 2025
5ff6e72
Merge branch 'master' into master
NalinDalal Oct 15, 2025
b21a14a
hope conventions done
NalinDalal Oct 15, 2025
2c82893
check
NalinDalal Oct 15, 2025
929110f
check
NalinDalal Oct 15, 2025
ca58315
check
NalinDalal Oct 15, 2025
5a2bb34
check
NalinDalal Oct 15, 2025
15fe3b6
check
NalinDalal Oct 15, 2025
add761e
check
NalinDalal Oct 15, 2025
244baba
check
NalinDalal Oct 15, 2025
85b16b2
check
NalinDalal Oct 15, 2025
d5df065
check
NalinDalal Oct 15, 2025
dbc5af1
check
NalinDalal Oct 15, 2025
8f18d61
check
NalinDalal Oct 15, 2025
a233aee
check
NalinDalal Oct 15, 2025
1549a06
check
NalinDalal Oct 15, 2025
4e6f309
check
NalinDalal Oct 15, 2025
047a3a9
check
NalinDalal Oct 15, 2025
369d78e
check
NalinDalal Oct 15, 2025
97c572a
check
NalinDalal Oct 15, 2025
f02d5ec
check
NalinDalal Oct 15, 2025
9cfd551
check
NalinDalal Oct 15, 2025
ea07753
check
NalinDalal Oct 15, 2025
af46332
check
NalinDalal Oct 15, 2025
22d9fd9
check
NalinDalal Oct 15, 2025
cdc7e3f
check
NalinDalal Oct 15, 2025
708ab8a
check
NalinDalal Oct 15, 2025
0a195a1
check
NalinDalal Oct 15, 2025
06edde7
check
NalinDalal Oct 15, 2025
6944a7c
check
NalinDalal Oct 15, 2025
8e03fbd
check
NalinDalal Oct 15, 2025
c6cdfb8
check
NalinDalal Oct 15, 2025
4a0cc96
check
NalinDalal Oct 15, 2025
49e1a9a
check
NalinDalal Oct 15, 2025
d5975ca
check
NalinDalal Oct 15, 2025
76ec73e
check
NalinDalal Oct 15, 2025
fb772a8
check
NalinDalal Oct 15, 2025
5f1537f
check
NalinDalal Oct 15, 2025
8259f24
check
NalinDalal Oct 15, 2025
32cc50a
check
NalinDalal Oct 15, 2025
b018de7
check
NalinDalal Oct 15, 2025
bbde642
check
NalinDalal Oct 15, 2025
f09407f
check
NalinDalal Oct 15, 2025
06a733e
check
NalinDalal Oct 15, 2025
3b346e5
check
NalinDalal Oct 15, 2025
f63a073
check
NalinDalal Oct 15, 2025
b276e00
i think done
NalinDalal Oct 15, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/if-nodejs-pr-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,4 @@ jobs:
fail_ci_if_error: true
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
verbose: true
114 changes: 58 additions & 56 deletions scripts/markdown/check-edit-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,63 @@ interface PathObject {
*
* @throws {Error} If an error occurs during the HTTP HEAD request for any edit link.
*/

// NEW: Async generator for efficient directory traversal
async function* walkDirectory(dir: string, relativePath = ''): AsyncGenerator<{ filePath: string; relativeFilePath: string }> {
const entries = await fs.readdir(dir, { withFileTypes: true });

for (const entry of entries) {
const absPath = path.join(dir, entry.name);
const relPath = path.join(relativePath, entry.name);

if (entry.isDirectory()) {
yield* walkDirectory(absPath, relPath);
} else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== '_section.md') {
yield { filePath: absPath, relativeFilePath: relPath };
}
}
}

/**
* Recursively traverses a folder and collects all Markdown file paths,
* excluding `_section.md` files. For each Markdown file found, it constructs
* the corresponding URL path and determines the appropriate edit link.
*
* This function uses an async generator (`walkDirectory`) to stream file paths
* instead of loading all of them into memory at once, improving performance
* and memory efficiency in large documentation repositories.
*
* @param folderPath - The absolute path to the root folder to traverse.
* @param editOptions - An array of objects used to determine the correct edit link
* for each markdown file. Each object should have a `value` and `href`.
* @returns A promise that resolves to an array of `PathObject`, each containing:
* - `filePath`: Absolute path to the markdown file.
* - `urlPath`: Relative URL path derived from the file's location.
* - `editLink`: Link to edit the file, based on `editOptions`.
*
* @throws Will throw an error if the directory traversal fails.
*/
async function generatePaths(folderPath: string, editOptions: { value: string; href: string }[]): Promise<PathObject[]> {
if (typeof folderPath !== 'string' || !folderPath) {
throw new TypeError('The "path" argument must be a non-empty string.');
}

const result: PathObject[] = [];

try {
for await (const { filePath, relativeFilePath } of walkDirectory(folderPath)) {
const urlPath = relativeFilePath.split(path.sep).join('/').replace(/\.md$/, '');
result.push({filePath,urlPath,editLink: determineEditLink(urlPath, filePath, editOptions)});
}

return result;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
const errorStack = err instanceof Error ? err.stack : '';
throw new Error(`Error walking directory ${folderPath}: ${errorMessage}\n${errorStack}`);
}
}

async function processBatch(batch: PathObject[]): Promise<(PathObject | null)[]> {
const TIMEOUT_MS = Number(process.env.DOCS_LINK_CHECK_TIMEOUT) || 5000;

Expand All @@ -44,7 +101,7 @@ async function processBatch(batch: PathObject[]): Promise<(PathObject | null)[]>

const controller = new AbortController();
timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);

const response = await fetch(editLink, {
method: 'HEAD',
signal: controller.signal
Expand Down Expand Up @@ -134,61 +191,6 @@ function determineEditLink(
return target ? `${target.href}/${path.basename(filePath)}` : null;
}

/**
* Recursively processes markdown files in a directory to generate path objects with corresponding edit links.
*
* This function reads the contents of the specified folder, skipping files named "_section.md", and recursively traverses subdirectories.
* For each markdown file encountered, it constructs a URL path by replacing system path separators with '/' and removing the file extension,
* then generates an edit link based on the provided edit options. The results are accumulated in an array and returned.
*
* @param folderPath - The folder to process.
* @param editOptions - An array of edit link option objects with `value` and `href` properties.
* @param relativePath - Optional relative path for URL generation (default: '').
* @param result - Optional accumulator for results (default: an empty array).
* @returns A promise that resolves with an array of objects, each containing a file path, URL path, and edit link.
* @throws {Error} If an error occurs while reading or processing the directory.
*/
async function generatePaths(
folderPath: string,
editOptions: { value: string; href: string }[],
relativePath = '',
result: PathObject[] = []
): Promise<PathObject[]> {
try {
const files = await fs.readdir(folderPath);

await Promise.all(
files.map(async (file) => {
const filePath = path.join(folderPath, file);
const relativeFilePath = path.join(relativePath, file);

// Skip _section.md files
if (file === '_section.md') {
return;
}

const stats = await fs.stat(filePath);

if (stats.isDirectory()) {
await generatePaths(filePath, editOptions, relativeFilePath, result);
} else if (stats.isFile() && file.endsWith('.md')) {
const urlPath = relativeFilePath.split(path.sep).join('/').replace('.md', '');

result.push({
filePath,
urlPath,
editLink: determineEditLink(urlPath, filePath, editOptions)
});
}
})
);

return result;
} catch (err) {
throw new Error(`Error processing directory ${folderPath}: ${err}`);
}
}

/**
* Executes the main workflow for validating edit links in markdown files.
*
Expand Down
81 changes: 72 additions & 9 deletions tests/markdown/check-edit-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ jest.mock('../../scripts/helpers/logger.ts', () => ({
logger: { info: jest.fn() }
}));
jest.mock('node-fetch-2', () => jest.fn());
function dirent(name: string, isFile = true, isDirectory = false) {
return { name, isFile: () => isFile, isDirectory: () => isDirectory };
}

describe('URL Checker Tests', () => {
const mockFetch = fetch as jest.Mock;
const testDir = path.resolve(__dirname, '../../markdown/docs');

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -64,11 +68,23 @@ describe('URL Checker Tests', () => {

expect(result).toBe(null);
});

it('returns fallback link if editOption.value is empty', () => {
const fallbackOption = [{ value: '', href: 'https://github.com/org/repo/edit/main' }];
expect(
determineEditLink('docs/anything', 'docs/anything.md', fallbackOption),
).toBe('https://github.com/org/repo/edit/main/docs/docs/anything.md');
});

it('returns correct link for specific match', () => {
const options = [{ value: 'special', href: 'https://github.com/org/repo/edit/main' }];
expect(
determineEditLink('docs/special', 'docs/special.md', options),
).toBe('https://github.com/org/repo/edit/main/special.md');
});
});

describe('generatePaths', () => {
const testDir = path.resolve(__dirname, '../../markdown/docs');

it('should generate correct paths for markdown files', async () => {
const paths = await generatePaths(testDir, editOptions);

Expand All @@ -89,13 +105,7 @@ describe('URL Checker Tests', () => {

it('should skip non-markdown files', async () => {
// Create a mock implementation to test the else branch
const mockReaddir = jest.spyOn(fs, 'readdir') as jest.Mock;
const mockStat = jest.spyOn(fs, 'stat') as jest.Mock;

mockReaddir.mockImplementationOnce(() => Promise.resolve(['test.js', 'test.md']));
mockStat.mockImplementationOnce(() => Promise.resolve({ isDirectory: () => false, isFile: () => true }));
mockStat.mockImplementationOnce(() => Promise.resolve({ isDirectory: () => false, isFile: () => true }));

const mockReaddir = jest.spyOn(fs, 'readdir').mockImplementation(async (dir, opts) => [dirent('test.js', true, false),dirent('test.md', true, false)]);
const result = await generatePaths(testDir, editOptions);

// Only the markdown file should be included, not the js file
Expand All @@ -111,6 +121,27 @@ describe('URL Checker Tests', () => {

await expect(generatePaths(invalidDir, editOptions)).rejects.toThrow();
});


it('throws TypeError for invalid folderPath', async () => {
// @ts-expect-error
await expect(generatePaths(undefined, editOptions)).rejects.toThrow(TypeError);
await expect(generatePaths('', editOptions)).rejects.toThrow(TypeError);
});

it('throws error if readdir fails', async () => {
jest.spyOn(fs, 'readdir').mockImplementationOnce(() => {throw new Error('FS error')});
await expect(generatePaths(testDir, editOptions)).rejects.toThrow('FS error');
});

it('handles subdirectory traversal', async () => {
jest.spyOn(fs, 'readdir').mockImplementationOnce(async () => [dirent('subdir', false, true),dirent('main.md', true, false)])
.mockImplementationOnce(async () => [dirent('subfile.md', true, false)]);
const result = await generatePaths(testDir, editOptions);

expect(result.some((f) => f.filePath.endsWith('main.md'))).toBe(true);
expect(result.some((f) => f.filePath.endsWith('subfile.md'))).toBe(true);
});
});

describe('processBatch', () => {
Expand Down Expand Up @@ -163,8 +194,32 @@ describe('URL Checker Tests', () => {
);
await expect(processBatch(testBatch)).rejects.toThrow();
}, 20000);

const batch = [
{filePath: 'file1.md',urlPath: 'docs/file1',editLink: 'https://github.com/org/repo/edit/main/file1.md'},
{filePath: 'reference/specification/v2.x.md',urlPath: 'docs/reference/specification/v2.x',editLink: 'https://github.com/org/repo/edit/main/v2.x.md'},
{ filePath: 'file2.md', urlPath: 'docs/file2', editLink: null } // no editLink
];

it('skips files with no editLink or in ignoreFiles', async () => {
mockFetch.mockImplementation(() => Promise.resolve({ status: 200 }));
const result = await processBatch(batch);
expect(result).toEqual([null, null, null]);
});

it('returns file if editLink is 404', async () => {
mockFetch.mockImplementation(() => Promise.resolve({ status: 404 }));
const result = await processBatch([{filePath: 'file.md', urlPath: 'docs/file',editLink: 'https://github.com/org/repo/edit/main/file.md'}]);
expect(result[0]?.editLink).toContain('file.md');
});

it('rejects on network error', async () => {
mockFetch.mockImplementation(() =>Promise.reject(new Error('Network error')));
await expect(processBatch([{filePath: 'file.md',urlPath: 'docs/file',editLink: 'https://github.com/org/repo/edit/main/file.md'}])).rejects.toThrow('Network error');
});
});

// ----------- checkUrls tests -----------
describe('checkUrls', () => {
it('should process all URLs in batches', async () => {
mockFetch.mockImplementation(() => Promise.resolve({ status: 200 }));
Expand All @@ -184,6 +239,14 @@ describe('URL Checker Tests', () => {

expect(results.length).toBe(2);
});

it('returns only 404s from batch', async () => {
mockFetch.mockImplementation((url) =>Promise.resolve({ status: url.includes('bad') ? 404 : 200 }));
const paths = [{filePath: 'good.md',urlPath: 'docs/good',editLink: 'https://github.com/org/repo/edit/main/good.md'},{filePath: 'bad.md',urlPath: 'docs/bad',editLink: 'https://github.com/org/repo/edit/main/bad.md'}];
const result = await checkUrls(paths);
expect(result.length).toBe(1);
expect(result[0].filePath).toBe('bad.md');
});
});

describe('main', () => {
Expand Down
Loading