spec-up-t 1.1.52 → 1.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/js/insert-trefs.js +1 -1
- package/branches.md +33 -0
- package/package.json +7 -1
- package/src/README.md +98 -0
- package/src/collect-external-references.js +108 -31
- package/src/collect-external-references.test.js +152 -0
- package/src/collectExternalReferences/fetchTermsFromGitHubRepository.js +39 -44
- package/src/collectExternalReferences/fetchTermsFromGitHubRepository.test.js +385 -0
- package/src/collectExternalReferences/matchTerm.js +10 -5
- package/src/collectExternalReferences/matchTerm.test.js +30 -0
- package/src/collectExternalReferences/octokitClient.js +96 -0
- package/src/collectExternalReferences/processXTrefsData.js +2 -0
- package/src/create-term-index.js +3 -1
- package/src/create-term-relations.js +3 -1
- package/src/fix-markdown-files.js +2 -1
- package/src/prepare-tref.js +2 -1
- package/src/utils/file-filter.js +36 -0
- package/src/utils/isLineWithDefinition.js +5 -10
- package/readme.md +0 -10
- package/src/collectExternalReferences/checkRateLimit.js +0 -17
- package/src/collectExternalReferences/setupFetchHeaders.js +0 -14
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This test suite covers the main functionality of the fetchTermsFromGitHubRepository function including:
|
|
3
|
+
|
|
4
|
+
- Retrieving cached search results when available
|
|
5
|
+
- Fetching and caching new results when not in cache
|
|
6
|
+
- Handling API errors gracefully
|
|
7
|
+
- Processing empty search results correctly
|
|
8
|
+
|
|
9
|
+
The tests use Jest's mocking capabilities to avoid actual GitHub API calls and filesystem operations, making the tests fast and reliable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Mock dependencies
|
|
16
|
+
jest.mock('fs');
|
|
17
|
+
jest.mock('path');
|
|
18
|
+
jest.mock('crypto');
|
|
19
|
+
jest.mock('../utils/isLineWithDefinition');
|
|
20
|
+
jest.mock('../config/paths');
|
|
21
|
+
|
|
22
|
+
// Replace your static mock with a configurable one
|
|
23
|
+
jest.mock('./octokitClient', () => {
|
|
24
|
+
// Create mock functions that can be reconfigured in each test
|
|
25
|
+
const mockSearchClient = {
|
|
26
|
+
search: jest.fn()
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const mockContentClient = {
|
|
30
|
+
getContent: jest.fn()
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
getSearchClient: jest.fn().mockResolvedValue(mockSearchClient),
|
|
35
|
+
getContentClient: jest.fn().mockResolvedValue(mockContentClient)
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Import the mocked module to access the mock functions
|
|
40
|
+
const octokitClient = require('./octokitClient');
|
|
41
|
+
|
|
42
|
+
// Import the function to test
|
|
43
|
+
const { fetchTermsFromGitHubRepository } = require('./fetchTermsFromGitHubRepository');
|
|
44
|
+
const { getPath } = require('../config/paths');
|
|
45
|
+
|
|
46
|
+
describe('fetchTermsFromGitHubRepository', () => {
|
|
47
|
+
// Setup common variables
|
|
48
|
+
const mockToken = 'mock-github-token';
|
|
49
|
+
const mockSearchString = 'test-search';
|
|
50
|
+
const mockOwner = 'test-owner';
|
|
51
|
+
const mockRepo = 'test-repo';
|
|
52
|
+
const mockSubdirectory = 'test-dir';
|
|
53
|
+
const mockCachePath = '/mock/cache/path';
|
|
54
|
+
|
|
55
|
+
const mockSearchResponse = {
|
|
56
|
+
data: {
|
|
57
|
+
total_count: 1,
|
|
58
|
+
items: [{
|
|
59
|
+
path: 'test-file.md',
|
|
60
|
+
repository: {
|
|
61
|
+
owner: { login: 'test-owner' },
|
|
62
|
+
name: 'test-repo'
|
|
63
|
+
},
|
|
64
|
+
text_matches: [{
|
|
65
|
+
fragment: '[[def: test-term]]\nContent'
|
|
66
|
+
}]
|
|
67
|
+
}]
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const mockFileContentResponse = {
|
|
72
|
+
data: {
|
|
73
|
+
content: Buffer.from('[[def: test-term]]\nTest file content').toString('base64')
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
// Reset all mocks
|
|
79
|
+
jest.clearAllMocks();
|
|
80
|
+
|
|
81
|
+
// Setup common mock returns
|
|
82
|
+
getPath.mockReturnValue(mockCachePath);
|
|
83
|
+
path.join.mockImplementation((...args) => args.join('/'));
|
|
84
|
+
|
|
85
|
+
// Mock crypto hash
|
|
86
|
+
const mockHash = {
|
|
87
|
+
update: jest.fn().mockReturnThis(),
|
|
88
|
+
digest: jest.fn().mockReturnValue('mock-hash')
|
|
89
|
+
};
|
|
90
|
+
require('crypto').createHash.mockReturnValue(mockHash);
|
|
91
|
+
|
|
92
|
+
// Default file system mocks
|
|
93
|
+
fs.existsSync.mockReturnValue(false);
|
|
94
|
+
fs.mkdirSync.mockReturnValue(undefined);
|
|
95
|
+
fs.writeFileSync.mockReturnValue(undefined);
|
|
96
|
+
fs.readFileSync.mockReturnValue('');
|
|
97
|
+
|
|
98
|
+
// Default isLineWithDefinition behavior
|
|
99
|
+
const { isLineWithDefinition } = require('../utils/isLineWithDefinition');
|
|
100
|
+
isLineWithDefinition.mockImplementation(line => {
|
|
101
|
+
return line.includes('[[def:') && line.includes(']]');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should return cached search results if available', async () => {
|
|
106
|
+
// Setup cache hit
|
|
107
|
+
fs.existsSync.mockReturnValue(true);
|
|
108
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockSearchResponse));
|
|
109
|
+
|
|
110
|
+
// Also set up file content cache
|
|
111
|
+
fs.existsSync.mockImplementation((path) => {
|
|
112
|
+
return true; // Assume all files exist in cache
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Mock content response
|
|
116
|
+
fs.readFileSync.mockImplementation((path) => {
|
|
117
|
+
if (path.includes('.json')) {
|
|
118
|
+
return JSON.stringify(mockSearchResponse);
|
|
119
|
+
} else {
|
|
120
|
+
return '[[def: test-term]]\nContent';
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
125
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(fs.existsSync).toHaveBeenCalled();
|
|
129
|
+
expect(fs.readFileSync).toHaveBeenCalled();
|
|
130
|
+
expect(result).not.toBeNull();
|
|
131
|
+
expect(result.path).toBe('test-file.md');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should fetch and cache search results if not cached', async () => {
|
|
135
|
+
// Set up mocks for API calls
|
|
136
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
137
|
+
mockSearchClient.search.mockResolvedValueOnce(mockSearchResponse);
|
|
138
|
+
|
|
139
|
+
const mockContentClient = await octokitClient.getContentClient();
|
|
140
|
+
mockContentClient.getContent.mockResolvedValueOnce({
|
|
141
|
+
data: {
|
|
142
|
+
content: Buffer.from('[[def: test-term]]\nContent').toString('base64')
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Mock file system for cache miss
|
|
147
|
+
fs.existsSync.mockReturnValue(false);
|
|
148
|
+
|
|
149
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
150
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
154
|
+
expect(result).not.toBeNull();
|
|
155
|
+
expect(result.path).toBe('test-file.md');
|
|
156
|
+
expect(result.content).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should handle API errors gracefully', async () => {
|
|
160
|
+
// Set up mock to throw an error
|
|
161
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
162
|
+
mockSearchClient.search.mockRejectedValueOnce(new Error('API error'));
|
|
163
|
+
|
|
164
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
165
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(result).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should handle empty search results', async () => {
|
|
172
|
+
// Set up mock for empty results
|
|
173
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
174
|
+
mockSearchClient.search.mockResolvedValueOnce({
|
|
175
|
+
data: {
|
|
176
|
+
total_count: 0,
|
|
177
|
+
items: []
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
182
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(result).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should handle invalid GitHub tokens', async () => {
|
|
189
|
+
// Set up mock for authentication error
|
|
190
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
191
|
+
mockSearchClient.search.mockRejectedValueOnce({
|
|
192
|
+
status: 401,
|
|
193
|
+
message: 'Bad credentials'
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
197
|
+
'invalid-token', mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(result).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should handle rate limiting errors', async () => {
|
|
204
|
+
// Set up mock for rate limit error
|
|
205
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
206
|
+
mockSearchClient.search.mockRejectedValueOnce({
|
|
207
|
+
status: 403,
|
|
208
|
+
message: 'API rate limit exceeded',
|
|
209
|
+
headers: {
|
|
210
|
+
'x-ratelimit-reset': (Date.now() / 1000 + 60).toString()
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
215
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(result).toBeNull();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('should handle large files that provide download URLs instead of content', async () => {
|
|
222
|
+
// Set up search response with large file
|
|
223
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
224
|
+
mockSearchClient.search.mockResolvedValueOnce({
|
|
225
|
+
data: {
|
|
226
|
+
total_count: 1,
|
|
227
|
+
items: [{
|
|
228
|
+
path: 'large-file.md',
|
|
229
|
+
repository: {
|
|
230
|
+
owner: { login: mockOwner },
|
|
231
|
+
name: mockRepo
|
|
232
|
+
},
|
|
233
|
+
text_matches: [{
|
|
234
|
+
fragment: '[[def: test-term]]\nContent'
|
|
235
|
+
}]
|
|
236
|
+
}]
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Set up content client for large file (no content)
|
|
241
|
+
const mockContentClient = await octokitClient.getContentClient();
|
|
242
|
+
mockContentClient.getContent.mockResolvedValueOnce({
|
|
243
|
+
data: {
|
|
244
|
+
content: null,
|
|
245
|
+
download_url: 'https://raw.githubusercontent.com/test-owner/test-repo/main/large-file.md'
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
250
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(result).not.toBeNull();
|
|
254
|
+
expect(result.path).toBe('large-file.md');
|
|
255
|
+
expect(result.content).toBe("");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should handle files with multiple definition lines', async () => {
|
|
259
|
+
// Set up search response with file containing multiple definitions
|
|
260
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
261
|
+
mockSearchClient.search.mockResolvedValueOnce({
|
|
262
|
+
data: {
|
|
263
|
+
total_count: 1,
|
|
264
|
+
items: [{
|
|
265
|
+
path: 'multi-def.md',
|
|
266
|
+
repository: {
|
|
267
|
+
owner: { login: mockOwner },
|
|
268
|
+
name: mockRepo
|
|
269
|
+
},
|
|
270
|
+
text_matches: [{
|
|
271
|
+
fragment: '[[def: term1]]\nContent'
|
|
272
|
+
}]
|
|
273
|
+
}]
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// File content with multiple definitions
|
|
278
|
+
const fileContent = `
|
|
279
|
+
[[def: term1, alias1]]\n
|
|
280
|
+
This is definition 1\n
|
|
281
|
+
\n
|
|
282
|
+
[[def: term2, alias2]]\n
|
|
283
|
+
This is definition 2
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
// Set up content client for multi-def file
|
|
287
|
+
const mockContentClient = await octokitClient.getContentClient();
|
|
288
|
+
mockContentClient.getContent.mockResolvedValueOnce({
|
|
289
|
+
data: {
|
|
290
|
+
content: Buffer.from(fileContent).toString('base64')
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
295
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
expect(result).not.toBeNull();
|
|
299
|
+
expect(result.path).toBe('multi-def.md');
|
|
300
|
+
expect(result.content).toContain('term1');
|
|
301
|
+
expect(result.content).toContain('term2');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('should handle file content fetch errors', async () => {
|
|
305
|
+
// Set up search that succeeds
|
|
306
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
307
|
+
mockSearchClient.search.mockResolvedValueOnce({
|
|
308
|
+
data: {
|
|
309
|
+
total_count: 1,
|
|
310
|
+
items: [{
|
|
311
|
+
path: 'error-file.md',
|
|
312
|
+
repository: {
|
|
313
|
+
owner: { login: mockOwner },
|
|
314
|
+
name: mockRepo
|
|
315
|
+
},
|
|
316
|
+
text_matches: [{
|
|
317
|
+
fragment: '[[def: test-term]]\nContent'
|
|
318
|
+
}]
|
|
319
|
+
}]
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Set up content fetch that fails
|
|
324
|
+
const mockContentClient = await octokitClient.getContentClient();
|
|
325
|
+
mockContentClient.getContent.mockRejectedValueOnce(new Error('File not found'));
|
|
326
|
+
|
|
327
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
328
|
+
mockToken, mockSearchString, mockOwner, mockRepo, mockSubdirectory
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// This might be null or might return empty content based on implementation
|
|
332
|
+
if (result) {
|
|
333
|
+
expect(result.content).toBe("");
|
|
334
|
+
} else {
|
|
335
|
+
expect(result).toBeNull();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('should correctly filter by subdirectory', async () => {
|
|
340
|
+
// Set up search with multiple results in different directories
|
|
341
|
+
const mockSearchClient = await octokitClient.getSearchClient();
|
|
342
|
+
mockSearchClient.search.mockResolvedValueOnce({
|
|
343
|
+
data: {
|
|
344
|
+
total_count: 2,
|
|
345
|
+
items: [
|
|
346
|
+
{
|
|
347
|
+
path: 'test-dir/file1.md',
|
|
348
|
+
repository: {
|
|
349
|
+
owner: { login: mockOwner },
|
|
350
|
+
name: mockRepo
|
|
351
|
+
},
|
|
352
|
+
text_matches: [{
|
|
353
|
+
fragment: '[[def: term1]]\nContent'
|
|
354
|
+
}]
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
path: 'other-dir/file2.md',
|
|
358
|
+
repository: {
|
|
359
|
+
owner: { login: mockOwner },
|
|
360
|
+
name: mockRepo
|
|
361
|
+
},
|
|
362
|
+
text_matches: [{
|
|
363
|
+
fragment: '[[def: term2]]\nContent'
|
|
364
|
+
}]
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Set up content client
|
|
371
|
+
const mockContentClient = await octokitClient.getContentClient();
|
|
372
|
+
mockContentClient.getContent.mockResolvedValueOnce({
|
|
373
|
+
data: {
|
|
374
|
+
content: Buffer.from('[[def: term1]]\nContent').toString('base64')
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const result = await fetchTermsFromGitHubRepository(
|
|
379
|
+
mockToken, mockSearchString, mockOwner, mockRepo, 'test-dir'
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(result).not.toBeNull();
|
|
383
|
+
expect(result.path).toBe('test-dir/file1.md');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -2,20 +2,25 @@ const isLineWithDefinition = require('../utils/isLineWithDefinition').isLineWith
|
|
|
2
2
|
|
|
3
3
|
function matchTerm(text, term) {
|
|
4
4
|
if (!text || typeof text !== 'string') {
|
|
5
|
-
|
|
6
|
-
console.log('Nothing to match');
|
|
5
|
+
console.log('Nothing to match for term:', term);
|
|
7
6
|
return false;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
const firstLine = text.split('\n')[0].trim();
|
|
11
10
|
|
|
12
|
-
if (isLineWithDefinition(firstLine) === false) {
|
|
11
|
+
if (isLineWithDefinition(firstLine) === false) {
|
|
13
12
|
console.log('String does not start with `[[def:` or end with `]]`');
|
|
14
13
|
return false;
|
|
15
14
|
};
|
|
16
15
|
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
// Find the closing bracket position instead of assuming it's at the end
|
|
17
|
+
const startPos = firstLine.indexOf('[[def:') + 6;
|
|
18
|
+
const endPos = firstLine.indexOf(']]');
|
|
19
|
+
|
|
20
|
+
if (startPos === -1 || endPos === -1) return false;
|
|
21
|
+
|
|
22
|
+
// Extract text between [[def: and ]]
|
|
23
|
+
let relevantPart = firstLine.substring(startPos, endPos);
|
|
19
24
|
|
|
20
25
|
// Split the string on `,` and trim the array elements
|
|
21
26
|
let termsArray = relevantPart.split(',').map(term => term.trim());
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { matchTerm } = require('./matchTerm');
|
|
2
|
+
|
|
3
|
+
describe('matchTerm', () => {
|
|
4
|
+
test('returns true when the term is found in a correctly formatted definition', () => {
|
|
5
|
+
const text = '[[def: term1, term2]]\nSome additional text';
|
|
6
|
+
expect(matchTerm(text, 'term1')).toBe(true);
|
|
7
|
+
expect(matchTerm(text, 'term2')).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('returns false when the term is not found in the definition', () => {
|
|
11
|
+
const text = '[[def: term1, term2]]\nSome additional text';
|
|
12
|
+
expect(matchTerm(text, 'term3')).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('returns false when the text is null or not a string', () => {
|
|
16
|
+
expect(matchTerm(null, 'term1')).toBe(false);
|
|
17
|
+
expect(matchTerm(123, 'term1')).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns false when the first line is not a valid definition', () => {
|
|
21
|
+
const text = 'Invalid definition line\n[[def: term1, term2]]';
|
|
22
|
+
expect(matchTerm(text, 'term1')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('handles extra spaces correctly', () => {
|
|
26
|
+
const text = '[[def: term1 , term2 ]]';
|
|
27
|
+
expect(matchTerm(text, 'term1')).toBe(true);
|
|
28
|
+
expect(matchTerm(text, 'term2')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Simple wrapper for Octokit functionality
|
|
2
|
+
let octokitClient = null;
|
|
3
|
+
let initPromise = null;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize the Octokit client (only happens once)
|
|
7
|
+
*/
|
|
8
|
+
async function initializeClient(token) {
|
|
9
|
+
if (initPromise) return initPromise;
|
|
10
|
+
|
|
11
|
+
initPromise = (async () => {
|
|
12
|
+
try {
|
|
13
|
+
const { Octokit } = await import('octokit');
|
|
14
|
+
const { throttling } = await import('@octokit/plugin-throttling');
|
|
15
|
+
|
|
16
|
+
const ThrottledOctokit = Octokit.plugin(throttling);
|
|
17
|
+
octokitClient = new ThrottledOctokit({
|
|
18
|
+
auth: token,
|
|
19
|
+
throttle: {
|
|
20
|
+
onRateLimit: (retryAfter, options) => {
|
|
21
|
+
console.warn(`Request quota exhausted for ${options.method} ${options.url}`);
|
|
22
|
+
return options.request.retryCount <= 1; // retry once
|
|
23
|
+
},
|
|
24
|
+
onSecondaryRateLimit: (retryAfter, options) => {
|
|
25
|
+
console.warn(`Secondary rate limit hit for ${options.method} ${options.url}`);
|
|
26
|
+
return options.request.retryCount <= 1; // retry once
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log("Octokit client initialized successfully");
|
|
32
|
+
return octokitClient;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to initialize Octokit:", error);
|
|
35
|
+
initPromise = null; // Reset so we can try again later
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
|
|
40
|
+
return initPromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a GitHub search client
|
|
45
|
+
*/
|
|
46
|
+
async function getSearchClient(token) {
|
|
47
|
+
const client = await initializeClient(token);
|
|
48
|
+
return {
|
|
49
|
+
search: async (query, owner, repo, path) => {
|
|
50
|
+
// First check if the repo is a fork
|
|
51
|
+
let isForked = false;
|
|
52
|
+
try {
|
|
53
|
+
const repoInfo = await client.rest.repos.get({
|
|
54
|
+
owner,
|
|
55
|
+
repo
|
|
56
|
+
});
|
|
57
|
+
isForked = repoInfo.data.fork;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn(`Could not determine fork status: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use appropriate search parameter based on fork status
|
|
63
|
+
const forkParam = isForked ? ' fork:true' : '';
|
|
64
|
+
const searchQuery = `${query} repo:${owner}/${repo} path:${path}${forkParam}`;
|
|
65
|
+
|
|
66
|
+
console.log(`Executing GitHub search: ${searchQuery}`);
|
|
67
|
+
return client.rest.search.code({
|
|
68
|
+
q: searchQuery,
|
|
69
|
+
headers: {
|
|
70
|
+
Accept: "application/vnd.github.v3.text-match+json",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get a GitHub content client
|
|
79
|
+
*/
|
|
80
|
+
async function getContentClient(token) {
|
|
81
|
+
const client = await initializeClient(token);
|
|
82
|
+
return {
|
|
83
|
+
getContent: async (owner, repo, path) => {
|
|
84
|
+
return client.rest.repos.getContent({
|
|
85
|
+
owner,
|
|
86
|
+
repo,
|
|
87
|
+
path
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
getSearchClient,
|
|
95
|
+
getContentClient
|
|
96
|
+
};
|
|
@@ -31,10 +31,12 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
|
|
|
31
31
|
xtref.content = item.content;
|
|
32
32
|
xtref.avatarUrl = item.repository.owner.avatar_url;
|
|
33
33
|
console.log(`✅ Match found for term: ${xtref.term} in ${xtref.externalSpec};`);
|
|
34
|
+
console.log("============================================\n\n");
|
|
34
35
|
} else {
|
|
35
36
|
xtref.commitHash = "not found";
|
|
36
37
|
xtref.content = "This term was not found in the external repository.";
|
|
37
38
|
console.log(`ℹ️ No match found for term: ${xtref.term} in ${xtref.externalSpec};`);
|
|
39
|
+
console.log("============================================\n\n");
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
package/src/create-term-index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* @since 2024-09-02
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const { shouldProcessFile } = require('./utils/file-filter');
|
|
19
|
+
|
|
18
20
|
function createTermIndex() {
|
|
19
21
|
const fs = require('fs-extra');
|
|
20
22
|
const path = require('path');
|
|
@@ -23,7 +25,7 @@ function createTermIndex() {
|
|
|
23
25
|
const specTermDirectoryName = config.specs.map(spec => spec.spec_terms_directory);
|
|
24
26
|
const outputPathJSON = path.join('output', 'term-index.json');
|
|
25
27
|
const files = fs.readdirSync(path.join(specDirectories[0], specTermDirectoryName[0]))
|
|
26
|
-
.filter(
|
|
28
|
+
.filter(shouldProcessFile);
|
|
27
29
|
|
|
28
30
|
const filePaths = files.map(file => specTermDirectoryName[0] + '/' + file);
|
|
29
31
|
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* @since 2024-06-22
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
const fs = require('fs-extra');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const config = fs.readJsonSync('specs.json');
|
|
12
|
+
const { shouldProcessFile } = require('./utils/file-filter');
|
|
11
13
|
|
|
12
14
|
const specTermDirectoryName = config.specs.map(spec => spec.spec_directory + '/' + spec.spec_terms_directory);
|
|
13
15
|
|
|
@@ -41,7 +43,7 @@ function createTermRelations() {
|
|
|
41
43
|
// read directory
|
|
42
44
|
fs.readdirSync(specDirectory).forEach(file => {
|
|
43
45
|
// read file
|
|
44
|
-
if (file
|
|
46
|
+
if (shouldProcessFile(file)) {
|
|
45
47
|
const markdown = fs.readFileSync(`${specDirectory}/${file}`, 'utf8');
|
|
46
48
|
|
|
47
49
|
let regexDef = /\[\[def:.*?\]\]/g;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { shouldProcessFile } = require('./utils/file-filter');
|
|
3
4
|
|
|
4
5
|
// Function to process markdown files in a directory recursively
|
|
5
6
|
function fixMarkdownFiles(directory) {
|
|
@@ -15,7 +16,7 @@ function fixMarkdownFiles(directory) {
|
|
|
15
16
|
if (item.isDirectory()) {
|
|
16
17
|
// If the item is a directory, call processDirectory recursively
|
|
17
18
|
processDirectory(itemPath);
|
|
18
|
-
} else if (item.isFile() &&
|
|
19
|
+
} else if (item.isFile() && shouldProcessFile(item.name)) {
|
|
19
20
|
try {
|
|
20
21
|
// Read the file synchronously
|
|
21
22
|
let data = fs.readFileSync(itemPath, 'utf8');
|
package/src/prepare-tref.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const dedent = require('dedent');
|
|
25
|
+
const { shouldProcessFile } = require('./utils/file-filter');
|
|
25
26
|
|
|
26
27
|
function getLocalXTrefContent(externalSpec, term) {
|
|
27
28
|
const filePath = path.join('output', 'xtrefs-data.json');
|
|
@@ -58,7 +59,7 @@ function prepareTref(directory) {
|
|
|
58
59
|
if (item.isDirectory()) {
|
|
59
60
|
// If the item is a directory, call processDirectory recursively
|
|
60
61
|
processDirectory(itemPath);
|
|
61
|
-
} else if (item.isFile() &&
|
|
62
|
+
} else if (item.isFile() && shouldProcessFile(item.name)) {
|
|
62
63
|
try {
|
|
63
64
|
// Read the file synchronously
|
|
64
65
|
let data = fs.readFileSync(itemPath, 'utf8');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Utility functions for filtering files consistently across the codebase
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a file is a Markdown file (ends with .md)
|
|
7
|
+
* @param {string} filename - The filename to check
|
|
8
|
+
* @returns {boolean} - True if the file is a Markdown file, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
function isMarkdownFile(filename) {
|
|
11
|
+
return filename.endsWith('.md');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a file is hidden/excluded (starts with underscore)
|
|
16
|
+
* @param {string} filename - The filename to check
|
|
17
|
+
* @returns {boolean} - True if the file is hidden/excluded, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
function isNotHiddenFile(filename) {
|
|
20
|
+
return !filename.startsWith('_');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if a file should be processed (is a Markdown file and not hidden)
|
|
25
|
+
* @param {string} filename - The filename to check
|
|
26
|
+
* @returns {boolean} - True if the file should be processed, false otherwise
|
|
27
|
+
*/
|
|
28
|
+
function shouldProcessFile(filename) {
|
|
29
|
+
return isMarkdownFile(filename) && isNotHiddenFile(filename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
isMarkdownFile,
|
|
34
|
+
isNotHiddenFile,
|
|
35
|
+
shouldProcessFile
|
|
36
|
+
};
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
line
|
|
3
|
-
|
|
4
|
-
if
|
|
5
|
-
|
|
6
|
-
return true;
|
|
7
|
-
} else {
|
|
8
|
-
// console.log('String does not start with `[[def:` or end with `]]`');
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
1
|
+
function isLineWithDefinition(line) {
|
|
2
|
+
if (!line || typeof line !== 'string') return false;
|
|
3
|
+
|
|
4
|
+
// Check if the line starts with [[def: and contains ]]
|
|
5
|
+
return line.startsWith('[[def:') && line.includes(']]');
|
|
11
6
|
}
|
|
12
7
|
|
|
13
8
|
exports.isLineWithDefinition = isLineWithDefinition;
|