@cosmocoder/mcp-web-docs 1.0.0
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/LICENSE +22 -0
- package/README.md +368 -0
- package/build/__mocks__/embeddings.d.ts +17 -0
- package/build/__mocks__/embeddings.js +66 -0
- package/build/__mocks__/embeddings.js.map +1 -0
- package/build/config.d.ts +44 -0
- package/build/config.js +158 -0
- package/build/config.js.map +1 -0
- package/build/config.test.d.ts +1 -0
- package/build/config.test.js +165 -0
- package/build/config.test.js.map +1 -0
- package/build/crawler/auth.d.ts +128 -0
- package/build/crawler/auth.js +546 -0
- package/build/crawler/auth.js.map +1 -0
- package/build/crawler/auth.test.d.ts +1 -0
- package/build/crawler/auth.test.js +174 -0
- package/build/crawler/auth.test.js.map +1 -0
- package/build/crawler/base.d.ts +24 -0
- package/build/crawler/base.js +149 -0
- package/build/crawler/base.js.map +1 -0
- package/build/crawler/base.test.d.ts +1 -0
- package/build/crawler/base.test.js +234 -0
- package/build/crawler/base.test.js.map +1 -0
- package/build/crawler/browser-config.d.ts +2 -0
- package/build/crawler/browser-config.js +29 -0
- package/build/crawler/browser-config.js.map +1 -0
- package/build/crawler/browser-config.test.d.ts +1 -0
- package/build/crawler/browser-config.test.js +56 -0
- package/build/crawler/browser-config.test.js.map +1 -0
- package/build/crawler/cheerio.d.ts +11 -0
- package/build/crawler/cheerio.js +134 -0
- package/build/crawler/cheerio.js.map +1 -0
- package/build/crawler/chromium.d.ts +21 -0
- package/build/crawler/chromium.js +596 -0
- package/build/crawler/chromium.js.map +1 -0
- package/build/crawler/content-extractor-types.d.ts +25 -0
- package/build/crawler/content-extractor-types.js +2 -0
- package/build/crawler/content-extractor-types.js.map +1 -0
- package/build/crawler/content-extractors.d.ts +9 -0
- package/build/crawler/content-extractors.js +9 -0
- package/build/crawler/content-extractors.js.map +1 -0
- package/build/crawler/content-utils.d.ts +2 -0
- package/build/crawler/content-utils.js +22 -0
- package/build/crawler/content-utils.js.map +1 -0
- package/build/crawler/content-utils.test.d.ts +1 -0
- package/build/crawler/content-utils.test.js +99 -0
- package/build/crawler/content-utils.test.js.map +1 -0
- package/build/crawler/crawlee-crawler.d.ts +63 -0
- package/build/crawler/crawlee-crawler.js +342 -0
- package/build/crawler/crawlee-crawler.js.map +1 -0
- package/build/crawler/crawlee-crawler.test.d.ts +1 -0
- package/build/crawler/crawlee-crawler.test.js +280 -0
- package/build/crawler/crawlee-crawler.test.js.map +1 -0
- package/build/crawler/default-extractor.d.ts +4 -0
- package/build/crawler/default-extractor.js +26 -0
- package/build/crawler/default-extractor.js.map +1 -0
- package/build/crawler/default-extractor.test.d.ts +1 -0
- package/build/crawler/default-extractor.test.js +200 -0
- package/build/crawler/default-extractor.test.js.map +1 -0
- package/build/crawler/default.d.ts +11 -0
- package/build/crawler/default.js +138 -0
- package/build/crawler/default.js.map +1 -0
- package/build/crawler/docs-crawler.d.ts +26 -0
- package/build/crawler/docs-crawler.js +97 -0
- package/build/crawler/docs-crawler.js.map +1 -0
- package/build/crawler/docs-crawler.test.d.ts +1 -0
- package/build/crawler/docs-crawler.test.js +185 -0
- package/build/crawler/docs-crawler.test.js.map +1 -0
- package/build/crawler/factory.d.ts +6 -0
- package/build/crawler/factory.js +83 -0
- package/build/crawler/factory.js.map +1 -0
- package/build/crawler/github-pages-extractor.d.ts +4 -0
- package/build/crawler/github-pages-extractor.js +33 -0
- package/build/crawler/github-pages-extractor.js.map +1 -0
- package/build/crawler/github-pages-extractor.test.d.ts +1 -0
- package/build/crawler/github-pages-extractor.test.js +184 -0
- package/build/crawler/github-pages-extractor.test.js.map +1 -0
- package/build/crawler/github.d.ts +20 -0
- package/build/crawler/github.js +181 -0
- package/build/crawler/github.js.map +1 -0
- package/build/crawler/github.test.d.ts +1 -0
- package/build/crawler/github.test.js +326 -0
- package/build/crawler/github.test.js.map +1 -0
- package/build/crawler/puppeteer.d.ts +16 -0
- package/build/crawler/puppeteer.js +191 -0
- package/build/crawler/puppeteer.js.map +1 -0
- package/build/crawler/queue-manager.d.ts +43 -0
- package/build/crawler/queue-manager.js +169 -0
- package/build/crawler/queue-manager.js.map +1 -0
- package/build/crawler/queue-manager.test.d.ts +1 -0
- package/build/crawler/queue-manager.test.js +509 -0
- package/build/crawler/queue-manager.test.js.map +1 -0
- package/build/crawler/site-rules.d.ts +11 -0
- package/build/crawler/site-rules.js +104 -0
- package/build/crawler/site-rules.js.map +1 -0
- package/build/crawler/site-rules.test.d.ts +1 -0
- package/build/crawler/site-rules.test.js +139 -0
- package/build/crawler/site-rules.test.js.map +1 -0
- package/build/crawler/storybook-extractor.d.ts +34 -0
- package/build/crawler/storybook-extractor.js +767 -0
- package/build/crawler/storybook-extractor.js.map +1 -0
- package/build/crawler/storybook-extractor.test.d.ts +1 -0
- package/build/crawler/storybook-extractor.test.js +491 -0
- package/build/crawler/storybook-extractor.test.js.map +1 -0
- package/build/embeddings/fastembed.d.ts +25 -0
- package/build/embeddings/fastembed.js +188 -0
- package/build/embeddings/fastembed.js.map +1 -0
- package/build/embeddings/fastembed.test.d.ts +1 -0
- package/build/embeddings/fastembed.test.js +307 -0
- package/build/embeddings/fastembed.test.js.map +1 -0
- package/build/embeddings/openai.d.ts +8 -0
- package/build/embeddings/openai.js +56 -0
- package/build/embeddings/openai.js.map +1 -0
- package/build/embeddings/types.d.ts +4 -0
- package/build/embeddings/types.js +2 -0
- package/build/embeddings/types.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +1007 -0
- package/build/index.js.map +1 -0
- package/build/index.test.d.ts +1 -0
- package/build/index.test.js +364 -0
- package/build/index.test.js.map +1 -0
- package/build/indexing/queue-manager.d.ts +36 -0
- package/build/indexing/queue-manager.js +86 -0
- package/build/indexing/queue-manager.js.map +1 -0
- package/build/indexing/queue-manager.test.d.ts +1 -0
- package/build/indexing/queue-manager.test.js +257 -0
- package/build/indexing/queue-manager.test.js.map +1 -0
- package/build/indexing/status.d.ts +39 -0
- package/build/indexing/status.js +207 -0
- package/build/indexing/status.js.map +1 -0
- package/build/indexing/status.test.d.ts +1 -0
- package/build/indexing/status.test.js +246 -0
- package/build/indexing/status.test.js.map +1 -0
- package/build/processor/content.d.ts +16 -0
- package/build/processor/content.js +286 -0
- package/build/processor/content.js.map +1 -0
- package/build/processor/content.test.d.ts +1 -0
- package/build/processor/content.test.js +369 -0
- package/build/processor/content.test.js.map +1 -0
- package/build/processor/markdown.d.ts +11 -0
- package/build/processor/markdown.js +256 -0
- package/build/processor/markdown.js.map +1 -0
- package/build/processor/markdown.test.d.ts +1 -0
- package/build/processor/markdown.test.js +312 -0
- package/build/processor/markdown.test.js.map +1 -0
- package/build/processor/metadata-parser.d.ts +37 -0
- package/build/processor/metadata-parser.js +245 -0
- package/build/processor/metadata-parser.js.map +1 -0
- package/build/processor/metadata-parser.test.d.ts +1 -0
- package/build/processor/metadata-parser.test.js +357 -0
- package/build/processor/metadata-parser.test.js.map +1 -0
- package/build/processor/processor.d.ts +8 -0
- package/build/processor/processor.js +190 -0
- package/build/processor/processor.js.map +1 -0
- package/build/processor/processor.test.d.ts +1 -0
- package/build/processor/processor.test.js +357 -0
- package/build/processor/processor.test.js.map +1 -0
- package/build/rag/cache.d.ts +10 -0
- package/build/rag/cache.js +10 -0
- package/build/rag/cache.js.map +1 -0
- package/build/rag/code-generator.d.ts +11 -0
- package/build/rag/code-generator.js +30 -0
- package/build/rag/code-generator.js.map +1 -0
- package/build/rag/context-assembler.d.ts +23 -0
- package/build/rag/context-assembler.js +113 -0
- package/build/rag/context-assembler.js.map +1 -0
- package/build/rag/docs-search.d.ts +55 -0
- package/build/rag/docs-search.js +380 -0
- package/build/rag/docs-search.js.map +1 -0
- package/build/rag/pipeline.d.ts +26 -0
- package/build/rag/pipeline.js +91 -0
- package/build/rag/pipeline.js.map +1 -0
- package/build/rag/query-processor.d.ts +14 -0
- package/build/rag/query-processor.js +57 -0
- package/build/rag/query-processor.js.map +1 -0
- package/build/rag/reranker.d.ts +55 -0
- package/build/rag/reranker.js +210 -0
- package/build/rag/reranker.js.map +1 -0
- package/build/rag/response-generator.d.ts +20 -0
- package/build/rag/response-generator.js +101 -0
- package/build/rag/response-generator.js.map +1 -0
- package/build/rag/retriever.d.ts +19 -0
- package/build/rag/retriever.js +111 -0
- package/build/rag/retriever.js.map +1 -0
- package/build/rag/validator.d.ts +22 -0
- package/build/rag/validator.js +128 -0
- package/build/rag/validator.js.map +1 -0
- package/build/rag/version-manager.d.ts +23 -0
- package/build/rag/version-manager.js +98 -0
- package/build/rag/version-manager.js.map +1 -0
- package/build/setupTests.d.ts +4 -0
- package/build/setupTests.js +50 -0
- package/build/setupTests.js.map +1 -0
- package/build/storage/storage.d.ts +38 -0
- package/build/storage/storage.js +700 -0
- package/build/storage/storage.js.map +1 -0
- package/build/storage/storage.test.d.ts +1 -0
- package/build/storage/storage.test.js +338 -0
- package/build/storage/storage.test.js.map +1 -0
- package/build/types/rag.d.ts +27 -0
- package/build/types/rag.js +2 -0
- package/build/types/rag.js.map +1 -0
- package/build/types.d.ts +120 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/util/content-utils.d.ts +31 -0
- package/build/util/content-utils.js +120 -0
- package/build/util/content-utils.js.map +1 -0
- package/build/util/content.d.ts +1 -0
- package/build/util/content.js +16 -0
- package/build/util/content.js.map +1 -0
- package/build/util/docs.d.ts +1 -0
- package/build/util/docs.js +26 -0
- package/build/util/docs.js.map +1 -0
- package/build/util/docs.test.d.ts +1 -0
- package/build/util/docs.test.js +49 -0
- package/build/util/docs.test.js.map +1 -0
- package/build/util/favicon.d.ts +6 -0
- package/build/util/favicon.js +88 -0
- package/build/util/favicon.js.map +1 -0
- package/build/util/favicon.test.d.ts +1 -0
- package/build/util/favicon.test.js +140 -0
- package/build/util/favicon.test.js.map +1 -0
- package/build/util/logger.d.ts +17 -0
- package/build/util/logger.js +72 -0
- package/build/util/logger.js.map +1 -0
- package/build/util/logger.test.d.ts +1 -0
- package/build/util/logger.test.js +46 -0
- package/build/util/logger.test.js.map +1 -0
- package/build/util/security.d.ts +312 -0
- package/build/util/security.js +719 -0
- package/build/util/security.js.map +1 -0
- package/build/util/security.test.d.ts +1 -0
- package/build/util/security.test.js +524 -0
- package/build/util/security.test.js.map +1 -0
- package/build/util/site-detector.d.ts +22 -0
- package/build/util/site-detector.js +42 -0
- package/build/util/site-detector.js.map +1 -0
- package/package.json +112 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { detectDefaultBrowser, AuthManager } from './auth.js';
|
|
2
|
+
const { mockDefaultBrowser, mockMkdir, mockReadFile, mockWriteFile, mockAccess, mockChmod, mockUnlink, mockChromiumLaunch } = vi.hoisted(() => ({
|
|
3
|
+
mockDefaultBrowser: vi.fn(),
|
|
4
|
+
mockMkdir: vi.fn(),
|
|
5
|
+
mockReadFile: vi.fn(),
|
|
6
|
+
mockWriteFile: vi.fn(),
|
|
7
|
+
mockAccess: vi.fn(),
|
|
8
|
+
mockChmod: vi.fn(),
|
|
9
|
+
mockUnlink: vi.fn(),
|
|
10
|
+
mockChromiumLaunch: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('default-browser', () => ({
|
|
13
|
+
default: mockDefaultBrowser,
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('node:fs/promises', () => ({
|
|
16
|
+
mkdir: mockMkdir,
|
|
17
|
+
readFile: mockReadFile,
|
|
18
|
+
writeFile: mockWriteFile,
|
|
19
|
+
access: mockAccess,
|
|
20
|
+
chmod: mockChmod,
|
|
21
|
+
unlink: mockUnlink,
|
|
22
|
+
}));
|
|
23
|
+
vi.mock('playwright', () => ({
|
|
24
|
+
chromium: {
|
|
25
|
+
launch: mockChromiumLaunch,
|
|
26
|
+
},
|
|
27
|
+
firefox: {
|
|
28
|
+
launch: vi.fn(),
|
|
29
|
+
},
|
|
30
|
+
webkit: {
|
|
31
|
+
launch: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
describe('Auth Module', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
describe('detectDefaultBrowser', () => {
|
|
39
|
+
it('should detect Firefox', async () => {
|
|
40
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Firefox', id: 'firefox' });
|
|
41
|
+
const result = await detectDefaultBrowser();
|
|
42
|
+
expect(result).toBe('firefox');
|
|
43
|
+
});
|
|
44
|
+
it('should detect Chrome', async () => {
|
|
45
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Google Chrome', id: 'com.google.chrome' });
|
|
46
|
+
const result = await detectDefaultBrowser();
|
|
47
|
+
expect(result).toBe('chrome');
|
|
48
|
+
});
|
|
49
|
+
it('should detect Edge', async () => {
|
|
50
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Microsoft Edge', id: 'microsoft-edge' });
|
|
51
|
+
const result = await detectDefaultBrowser();
|
|
52
|
+
expect(result).toBe('edge');
|
|
53
|
+
});
|
|
54
|
+
it('should detect Safari as webkit', async () => {
|
|
55
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Safari', id: 'com.apple.safari' });
|
|
56
|
+
const result = await detectDefaultBrowser();
|
|
57
|
+
expect(result).toBe('webkit');
|
|
58
|
+
});
|
|
59
|
+
it('should detect Chromium', async () => {
|
|
60
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Chromium', id: 'chromium-browser' });
|
|
61
|
+
const result = await detectDefaultBrowser();
|
|
62
|
+
expect(result).toBe('chromium');
|
|
63
|
+
});
|
|
64
|
+
it('should fall back to chromium for unknown browser', async () => {
|
|
65
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'Unknown Browser', id: 'unknown' });
|
|
66
|
+
const result = await detectDefaultBrowser();
|
|
67
|
+
expect(result).toBe('chromium');
|
|
68
|
+
});
|
|
69
|
+
it('should fall back to chromium on error', async () => {
|
|
70
|
+
mockDefaultBrowser.mockRejectedValue(new Error('Detection failed'));
|
|
71
|
+
const result = await detectDefaultBrowser();
|
|
72
|
+
expect(result).toBe('chromium');
|
|
73
|
+
});
|
|
74
|
+
it('should be case-insensitive', async () => {
|
|
75
|
+
mockDefaultBrowser.mockResolvedValue({ name: 'FIREFOX', id: 'FIREFOX' });
|
|
76
|
+
const result = await detectDefaultBrowser();
|
|
77
|
+
expect(result).toBe('firefox');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('AuthManager', () => {
|
|
81
|
+
let authManager;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
authManager = new AuthManager('/tmp/test-data');
|
|
84
|
+
mockMkdir.mockResolvedValue(undefined);
|
|
85
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
86
|
+
mockChmod.mockResolvedValue(undefined);
|
|
87
|
+
});
|
|
88
|
+
describe('initialize', () => {
|
|
89
|
+
it('should create sessions directory', async () => {
|
|
90
|
+
await authManager.initialize();
|
|
91
|
+
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('sessions'), { recursive: true });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('hasSession', () => {
|
|
95
|
+
it('should return true when session file exists', async () => {
|
|
96
|
+
mockAccess.mockResolvedValue(undefined);
|
|
97
|
+
const result = await authManager.hasSession('https://example.com/docs');
|
|
98
|
+
expect(result).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('should return false when session file does not exist', async () => {
|
|
101
|
+
mockAccess.mockRejectedValue(new Error('ENOENT'));
|
|
102
|
+
const result = await authManager.hasSession('https://example.com/docs');
|
|
103
|
+
expect(result).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('should use domain for session path', async () => {
|
|
106
|
+
mockAccess.mockResolvedValue(undefined);
|
|
107
|
+
await authManager.hasSession('https://docs.example.com/path/page');
|
|
108
|
+
expect(mockAccess).toHaveBeenCalledWith(expect.stringContaining('docs.example.com'));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('loadSession', () => {
|
|
112
|
+
it('should return null when session does not exist', async () => {
|
|
113
|
+
mockReadFile.mockRejectedValue(new Error('ENOENT'));
|
|
114
|
+
const result = await authManager.loadSession('https://example.com');
|
|
115
|
+
expect(result).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
it('should return null for invalid session data', async () => {
|
|
118
|
+
mockReadFile.mockResolvedValue('invalid json');
|
|
119
|
+
const result = await authManager.loadSession('https://example.com');
|
|
120
|
+
expect(result).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
it('should return null for session with invalid structure', async () => {
|
|
123
|
+
mockReadFile.mockResolvedValue(JSON.stringify({ invalid: 'structure' }));
|
|
124
|
+
const result = await authManager.loadSession('https://example.com');
|
|
125
|
+
expect(result).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('clearSession', () => {
|
|
129
|
+
it('should delete session file', async () => {
|
|
130
|
+
mockUnlink.mockResolvedValue(undefined);
|
|
131
|
+
await authManager.clearSession('https://example.com');
|
|
132
|
+
expect(mockUnlink).toHaveBeenCalledWith(expect.stringContaining('example.com'));
|
|
133
|
+
});
|
|
134
|
+
it('should not throw when session does not exist', async () => {
|
|
135
|
+
mockUnlink.mockRejectedValue(new Error('ENOENT'));
|
|
136
|
+
await expect(authManager.clearSession('https://nonexistent.com')).resolves.not.toThrow();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('getSessionPath (security)', () => {
|
|
140
|
+
it('should sanitize domain names', async () => {
|
|
141
|
+
mockAccess.mockResolvedValue(undefined);
|
|
142
|
+
await authManager.hasSession('https://example.com/../../../etc/passwd');
|
|
143
|
+
// Should not contain path traversal sequences
|
|
144
|
+
const calledPath = mockAccess.mock.calls[0][0];
|
|
145
|
+
expect(calledPath).not.toContain('..');
|
|
146
|
+
expect(calledPath).not.toContain('etc');
|
|
147
|
+
expect(calledPath).not.toContain('passwd');
|
|
148
|
+
});
|
|
149
|
+
it('should reject invalid domain names', async () => {
|
|
150
|
+
// The URL constructor will throw for completely invalid URLs
|
|
151
|
+
await expect(authManager.hasSession('not-a-url')).rejects.toThrow();
|
|
152
|
+
});
|
|
153
|
+
it('should convert domain to lowercase', async () => {
|
|
154
|
+
mockAccess.mockResolvedValue(undefined);
|
|
155
|
+
await authManager.hasSession('https://EXAMPLE.COM');
|
|
156
|
+
expect(mockAccess).toHaveBeenCalledWith(expect.stringContaining('example.com'));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('cleanup', () => {
|
|
160
|
+
it('should not throw when no active browser', async () => {
|
|
161
|
+
await expect(authManager.cleanup()).resolves.not.toThrow();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('BrowserType', () => {
|
|
166
|
+
it('should accept valid browser types', () => {
|
|
167
|
+
const validTypes = ['chromium', 'chrome', 'firefox', 'webkit', 'edge'];
|
|
168
|
+
validTypes.forEach((type) => {
|
|
169
|
+
expect(typeof type).toBe('string');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=auth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../../src/crawler/auth.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CrawlResult } from '../types.js';
|
|
2
|
+
export declare abstract class BaseCrawler {
|
|
3
|
+
protected readonly maxDepth: number;
|
|
4
|
+
protected readonly maxRequestsPerCrawl: number;
|
|
5
|
+
protected seenUrls: Set<string>;
|
|
6
|
+
protected isAborting: boolean;
|
|
7
|
+
private requestCount;
|
|
8
|
+
private lastRequestTime;
|
|
9
|
+
protected totalUrls: number;
|
|
10
|
+
protected processedUrls: number;
|
|
11
|
+
protected onProgress?: (progress: number, description: string) => void;
|
|
12
|
+
constructor(maxDepth?: number, maxRequestsPerCrawl?: number, onProgress?: (progress: number, description: string) => void);
|
|
13
|
+
protected updateProgress(description: string): void;
|
|
14
|
+
protected addDiscoveredUrls(count: number): void;
|
|
15
|
+
protected markUrlProcessed(url: string): void;
|
|
16
|
+
abstract crawl(url: string, maxDepth?: number): AsyncGenerator<CrawlResult, void, unknown>;
|
|
17
|
+
protected shouldCrawl(urlString: string): boolean;
|
|
18
|
+
protected markUrlAsSeen(url: string): void;
|
|
19
|
+
protected getPathFromUrl(urlString: string): string;
|
|
20
|
+
protected normalizeUrl(urlString: string): string;
|
|
21
|
+
protected rateLimit(): Promise<void>;
|
|
22
|
+
protected retryWithBackoff<T>(operation: () => Promise<T>, maxRetries?: number, baseDelay?: number): Promise<T>;
|
|
23
|
+
abort(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { URL } from 'url';
|
|
2
|
+
import { IGNORED_PATHS, RATE_LIMIT } from '../config.js';
|
|
3
|
+
import { logger } from '../util/logger.js';
|
|
4
|
+
export class BaseCrawler {
|
|
5
|
+
maxDepth;
|
|
6
|
+
maxRequestsPerCrawl;
|
|
7
|
+
seenUrls;
|
|
8
|
+
isAborting;
|
|
9
|
+
requestCount;
|
|
10
|
+
lastRequestTime;
|
|
11
|
+
totalUrls;
|
|
12
|
+
processedUrls;
|
|
13
|
+
onProgress;
|
|
14
|
+
constructor(maxDepth = 4, maxRequestsPerCrawl = 1000, onProgress) {
|
|
15
|
+
this.maxDepth = maxDepth;
|
|
16
|
+
this.maxRequestsPerCrawl = maxRequestsPerCrawl;
|
|
17
|
+
this.seenUrls = new Set();
|
|
18
|
+
this.isAborting = false;
|
|
19
|
+
this.requestCount = 0;
|
|
20
|
+
this.lastRequestTime = 0;
|
|
21
|
+
this.totalUrls = 1; // Start with 1 for the initial URL
|
|
22
|
+
this.processedUrls = 0;
|
|
23
|
+
this.onProgress = onProgress;
|
|
24
|
+
}
|
|
25
|
+
updateProgress(description) {
|
|
26
|
+
if (this.onProgress) {
|
|
27
|
+
// Calculate progress as percentage (0-100)
|
|
28
|
+
const progress = Math.min(Math.round((this.processedUrls / this.totalUrls) * 100), 100);
|
|
29
|
+
this.onProgress(progress, description);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
addDiscoveredUrls(count) {
|
|
33
|
+
this.totalUrls += count;
|
|
34
|
+
this.updateProgress('Discovering pages...');
|
|
35
|
+
}
|
|
36
|
+
markUrlProcessed(url) {
|
|
37
|
+
this.processedUrls++;
|
|
38
|
+
this.markUrlAsSeen(url);
|
|
39
|
+
this.updateProgress(`Processing page ${this.processedUrls} of ${this.totalUrls}`);
|
|
40
|
+
}
|
|
41
|
+
shouldCrawl(urlString) {
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(urlString);
|
|
44
|
+
// Skip if already seen (using full URL including query params)
|
|
45
|
+
if (this.seenUrls.has(urlString)) {
|
|
46
|
+
logger.debug(`[${this.constructor.name}] Skipping already seen URL: ${urlString}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// Skip fragments only
|
|
50
|
+
if (url.hash) {
|
|
51
|
+
logger.debug(`[${this.constructor.name}] Skipping URL with hash: ${urlString}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Skip non-HTML files only if they have a file extension
|
|
55
|
+
const ext = url.pathname.split('.').pop()?.toLowerCase();
|
|
56
|
+
if (ext && ext !== 'html' && ext !== 'htm') {
|
|
57
|
+
logger.debug(`[${this.constructor.name}] Skipping non-HTML file: ${urlString}`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// Skip ignored paths only if they match exactly
|
|
61
|
+
const path = url.pathname.toLowerCase();
|
|
62
|
+
const isIgnored = IGNORED_PATHS.some((ignored) => {
|
|
63
|
+
// If ignored path ends with /, treat it as a directory
|
|
64
|
+
if (ignored.endsWith('/')) {
|
|
65
|
+
return path.startsWith(ignored);
|
|
66
|
+
}
|
|
67
|
+
// Otherwise match exactly
|
|
68
|
+
return path === `/${ignored}`;
|
|
69
|
+
});
|
|
70
|
+
if (isIgnored) {
|
|
71
|
+
logger.debug(`[${this.constructor.name}] Skipping ignored path: ${urlString}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
logger.debug(`[${this.constructor.name}] Error checking URL: ${urlString}`, error);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
markUrlAsSeen(url) {
|
|
82
|
+
this.seenUrls.add(url);
|
|
83
|
+
}
|
|
84
|
+
getPathFromUrl(urlString) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(urlString);
|
|
87
|
+
return url.pathname + url.search; // Include query params in path
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return urlString;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
normalizeUrl(urlString) {
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(urlString);
|
|
96
|
+
// Remove hash fragment but keep query params
|
|
97
|
+
url.hash = '';
|
|
98
|
+
return url.toString().replace(/\/$/, '');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return urlString;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async rateLimit() {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
this.requestCount++;
|
|
107
|
+
// Reset counter if time window has passed
|
|
108
|
+
if (now - this.lastRequestTime > RATE_LIMIT.timeWindow) {
|
|
109
|
+
this.requestCount = 1;
|
|
110
|
+
this.lastRequestTime = now;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// If we've hit the rate limit, wait until the next window
|
|
114
|
+
if (this.requestCount > RATE_LIMIT.maxRequests) {
|
|
115
|
+
const waitTime = RATE_LIMIT.timeWindow - (now - this.lastRequestTime);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
117
|
+
this.requestCount = 1;
|
|
118
|
+
this.lastRequestTime = Date.now();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Ensure minimum delay between requests
|
|
122
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
123
|
+
if (timeSinceLastRequest < RATE_LIMIT.minDelay) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT.minDelay - timeSinceLastRequest));
|
|
125
|
+
}
|
|
126
|
+
this.lastRequestTime = Date.now();
|
|
127
|
+
}
|
|
128
|
+
async retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
|
|
129
|
+
let lastError;
|
|
130
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
131
|
+
try {
|
|
132
|
+
return await operation();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
lastError = error;
|
|
136
|
+
if (i === maxRetries - 1)
|
|
137
|
+
break;
|
|
138
|
+
// Exponential backoff
|
|
139
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw lastError;
|
|
144
|
+
}
|
|
145
|
+
abort() {
|
|
146
|
+
this.isAborting = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/crawler/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { BaseCrawler } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Concrete implementation of BaseCrawler for testing purposes.
|
|
4
|
+
*
|
|
5
|
+
* Since BaseCrawler is abstract, we create this minimal subclass that:
|
|
6
|
+
* 1. Implements the required abstract `crawl` method
|
|
7
|
+
* 2. Exposes protected BaseCrawler methods via public wrappers
|
|
8
|
+
*
|
|
9
|
+
* The wrapper methods (testShouldCrawl, testMarkUrlAsSeen, etc.) simply call
|
|
10
|
+
* the inherited BaseCrawler methods - we're testing the REAL BaseCrawler
|
|
11
|
+
* implementation, not overridden methods.
|
|
12
|
+
*/
|
|
13
|
+
class TestCrawler extends BaseCrawler {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
+
async *crawl(_url) {
|
|
16
|
+
yield {
|
|
17
|
+
url: 'https://example.com',
|
|
18
|
+
path: '/',
|
|
19
|
+
content: '<h1>Test</h1>',
|
|
20
|
+
title: 'Test',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// Expose protected BaseCrawler methods for testing (these call the real implementations)
|
|
24
|
+
testShouldCrawl(url) {
|
|
25
|
+
return this.shouldCrawl(url);
|
|
26
|
+
}
|
|
27
|
+
testMarkUrlAsSeen(url) {
|
|
28
|
+
this.markUrlAsSeen(url);
|
|
29
|
+
}
|
|
30
|
+
testGetPathFromUrl(url) {
|
|
31
|
+
return this.getPathFromUrl(url);
|
|
32
|
+
}
|
|
33
|
+
testNormalizeUrl(url) {
|
|
34
|
+
return this.normalizeUrl(url);
|
|
35
|
+
}
|
|
36
|
+
async testRateLimit() {
|
|
37
|
+
return this.rateLimit();
|
|
38
|
+
}
|
|
39
|
+
async testRetryWithBackoff(operation, maxRetries) {
|
|
40
|
+
return this.retryWithBackoff(operation, maxRetries);
|
|
41
|
+
}
|
|
42
|
+
testAbort() {
|
|
43
|
+
this.abort();
|
|
44
|
+
}
|
|
45
|
+
get isAbortingFlag() {
|
|
46
|
+
return this.isAborting;
|
|
47
|
+
}
|
|
48
|
+
testUpdateProgress(description) {
|
|
49
|
+
this.updateProgress(description);
|
|
50
|
+
}
|
|
51
|
+
testAddDiscoveredUrls(count) {
|
|
52
|
+
this.addDiscoveredUrls(count);
|
|
53
|
+
}
|
|
54
|
+
testMarkUrlProcessed(url) {
|
|
55
|
+
this.markUrlProcessed(url);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
describe('BaseCrawler', () => {
|
|
59
|
+
let crawler;
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
crawler = new TestCrawler();
|
|
62
|
+
});
|
|
63
|
+
describe('constructor', () => {
|
|
64
|
+
it('should initialize with default values', () => {
|
|
65
|
+
const defaultCrawler = new TestCrawler();
|
|
66
|
+
expect(defaultCrawler).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
it('should accept custom maxDepth', () => {
|
|
69
|
+
const customCrawler = new TestCrawler(10);
|
|
70
|
+
expect(customCrawler).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
it('should accept custom maxRequestsPerCrawl', () => {
|
|
73
|
+
const customCrawler = new TestCrawler(4, 500);
|
|
74
|
+
expect(customCrawler).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
it('should accept progress callback', () => {
|
|
77
|
+
const progressFn = vi.fn();
|
|
78
|
+
const customCrawler = new TestCrawler(4, 1000, progressFn);
|
|
79
|
+
customCrawler.testUpdateProgress('Testing');
|
|
80
|
+
expect(progressFn).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('shouldCrawl', () => {
|
|
84
|
+
it('should return true for valid URLs', () => {
|
|
85
|
+
// URLs with .html extension pass the file extension check
|
|
86
|
+
expect(crawler.testShouldCrawl('https://example.com/docs.html')).toBe(true);
|
|
87
|
+
expect(crawler.testShouldCrawl('https://example.com/api.html')).toBe(true);
|
|
88
|
+
expect(crawler.testShouldCrawl('https://example.com/page.htm')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it('should return false for already seen URLs', () => {
|
|
91
|
+
crawler.testMarkUrlAsSeen('https://example.com/seen');
|
|
92
|
+
expect(crawler.testShouldCrawl('https://example.com/seen')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it('should return false for URLs with hash fragments', () => {
|
|
95
|
+
expect(crawler.testShouldCrawl('https://example.com/page#section')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('should return false for non-HTML files', () => {
|
|
98
|
+
expect(crawler.testShouldCrawl('https://example.com/image.png')).toBe(false);
|
|
99
|
+
expect(crawler.testShouldCrawl('https://example.com/doc.pdf')).toBe(false);
|
|
100
|
+
expect(crawler.testShouldCrawl('https://example.com/script.js')).toBe(false);
|
|
101
|
+
expect(crawler.testShouldCrawl('https://example.com/style.css')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it('should return true for HTML files', () => {
|
|
104
|
+
expect(crawler.testShouldCrawl('https://example.com/page.html')).toBe(true);
|
|
105
|
+
expect(crawler.testShouldCrawl('https://example.com/page.htm')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it('should return true for HTML file extensions', () => {
|
|
108
|
+
expect(crawler.testShouldCrawl('https://example.com/docs.html')).toBe(true);
|
|
109
|
+
expect(crawler.testShouldCrawl('https://example.com/page.htm')).toBe(true);
|
|
110
|
+
expect(crawler.testShouldCrawl('https://example.com/index.HTML')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('should return false for paths without HTML extension', () => {
|
|
113
|
+
// The implementation's file extension check treats all paths without
|
|
114
|
+
// .html/.htm extension as non-HTML files
|
|
115
|
+
expect(crawler.testShouldCrawl('https://example.com/docs')).toBe(false);
|
|
116
|
+
expect(crawler.testShouldCrawl('https://example.com/')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('should return false for invalid URLs', () => {
|
|
119
|
+
expect(crawler.testShouldCrawl('not-a-url')).toBe(false);
|
|
120
|
+
expect(crawler.testShouldCrawl('')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('markUrlAsSeen', () => {
|
|
124
|
+
it('should mark URL as seen', () => {
|
|
125
|
+
const url = 'https://example.com/test.html';
|
|
126
|
+
expect(crawler.testShouldCrawl(url)).toBe(true);
|
|
127
|
+
crawler.testMarkUrlAsSeen(url);
|
|
128
|
+
expect(crawler.testShouldCrawl(url)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('getPathFromUrl', () => {
|
|
132
|
+
it('should extract pathname from URL', () => {
|
|
133
|
+
expect(crawler.testGetPathFromUrl('https://example.com/docs/page')).toBe('/docs/page');
|
|
134
|
+
});
|
|
135
|
+
it('should include query params in path', () => {
|
|
136
|
+
expect(crawler.testGetPathFromUrl('https://example.com/search?q=test')).toBe('/search?q=test');
|
|
137
|
+
});
|
|
138
|
+
it('should return original string for invalid URL', () => {
|
|
139
|
+
expect(crawler.testGetPathFromUrl('not-a-url')).toBe('not-a-url');
|
|
140
|
+
});
|
|
141
|
+
it('should handle root path', () => {
|
|
142
|
+
expect(crawler.testGetPathFromUrl('https://example.com')).toBe('/');
|
|
143
|
+
expect(crawler.testGetPathFromUrl('https://example.com/')).toBe('/');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('normalizeUrl', () => {
|
|
147
|
+
it('should remove hash fragments', () => {
|
|
148
|
+
expect(crawler.testNormalizeUrl('https://example.com/page#section')).toBe('https://example.com/page');
|
|
149
|
+
});
|
|
150
|
+
it('should remove trailing slash', () => {
|
|
151
|
+
expect(crawler.testNormalizeUrl('https://example.com/docs/')).toBe('https://example.com/docs');
|
|
152
|
+
});
|
|
153
|
+
it('should preserve query params', () => {
|
|
154
|
+
expect(crawler.testNormalizeUrl('https://example.com/search?q=test')).toBe('https://example.com/search?q=test');
|
|
155
|
+
});
|
|
156
|
+
it('should return original string for invalid URL', () => {
|
|
157
|
+
expect(crawler.testNormalizeUrl('not-a-url')).toBe('not-a-url');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('abort', () => {
|
|
161
|
+
it('should set isAborting flag to true', () => {
|
|
162
|
+
expect(crawler.isAbortingFlag).toBe(false);
|
|
163
|
+
crawler.testAbort();
|
|
164
|
+
expect(crawler.isAbortingFlag).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('retryWithBackoff', () => {
|
|
168
|
+
it('should return result on successful operation', async () => {
|
|
169
|
+
const operation = vi.fn().mockResolvedValue('success');
|
|
170
|
+
const result = await crawler.testRetryWithBackoff(operation);
|
|
171
|
+
expect(result).toBe('success');
|
|
172
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
173
|
+
});
|
|
174
|
+
it('should retry on failure', async () => {
|
|
175
|
+
const operation = vi.fn().mockRejectedValueOnce(new Error('fail')).mockResolvedValue('success');
|
|
176
|
+
vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
|
|
177
|
+
callback();
|
|
178
|
+
return 0;
|
|
179
|
+
});
|
|
180
|
+
const result = await crawler.testRetryWithBackoff(operation);
|
|
181
|
+
expect(result).toBe('success');
|
|
182
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
183
|
+
vi.restoreAllMocks();
|
|
184
|
+
});
|
|
185
|
+
it('should throw after max retries', async () => {
|
|
186
|
+
const operation = vi.fn().mockRejectedValue(new Error('persistent error'));
|
|
187
|
+
vi.spyOn(global, 'setTimeout').mockImplementation((callback) => {
|
|
188
|
+
callback();
|
|
189
|
+
return 0;
|
|
190
|
+
});
|
|
191
|
+
await expect(crawler.testRetryWithBackoff(operation, 3)).rejects.toThrow('persistent error');
|
|
192
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
193
|
+
vi.restoreAllMocks();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('progress tracking', () => {
|
|
197
|
+
it('should call progress callback with updateProgress', () => {
|
|
198
|
+
const progressFn = vi.fn();
|
|
199
|
+
const progressCrawler = new TestCrawler(4, 1000, progressFn);
|
|
200
|
+
progressCrawler.testUpdateProgress('Processing...');
|
|
201
|
+
expect(progressFn).toHaveBeenCalledWith(expect.any(Number), 'Processing...');
|
|
202
|
+
});
|
|
203
|
+
it('should track discovered URLs', () => {
|
|
204
|
+
const progressFn = vi.fn();
|
|
205
|
+
const progressCrawler = new TestCrawler(4, 1000, progressFn);
|
|
206
|
+
progressCrawler.testAddDiscoveredUrls(5);
|
|
207
|
+
expect(progressFn).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
it('should track processed URLs', () => {
|
|
210
|
+
const progressFn = vi.fn();
|
|
211
|
+
const progressCrawler = new TestCrawler(4, 1000, progressFn);
|
|
212
|
+
progressCrawler.testMarkUrlProcessed('https://example.com/page1');
|
|
213
|
+
expect(progressFn).toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
it('should not throw when progress callback not provided', () => {
|
|
216
|
+
const noCrawler = new TestCrawler();
|
|
217
|
+
expect(() => noCrawler.testUpdateProgress('Test')).not.toThrow();
|
|
218
|
+
expect(() => noCrawler.testAddDiscoveredUrls(5)).not.toThrow();
|
|
219
|
+
expect(() => noCrawler.testMarkUrlProcessed('https://example.com')).not.toThrow();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('crawl', () => {
|
|
223
|
+
it('should yield CrawlResult', async () => {
|
|
224
|
+
const results = [];
|
|
225
|
+
for await (const result of crawler.crawl('https://example.com')) {
|
|
226
|
+
results.push(result);
|
|
227
|
+
}
|
|
228
|
+
expect(results.length).toBe(1);
|
|
229
|
+
expect(results[0].url).toBe('https://example.com');
|
|
230
|
+
expect(results[0].content).toBe('<h1>Test</h1>');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
//# sourceMappingURL=base.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.test.js","sourceRoot":"","sources":["../../src/crawler/base.test.ts"],"names":[],"mappings":"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"}
|