search-web-api 1.0.13
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/README.md +307 -0
- package/demo/index.ts +38 -0
- package/demo/openapi.ts +402 -0
- package/demo/routes/autocomplete.ts +124 -0
- package/demo/routes/search.ts +61 -0
- package/docs/AUTOCOMPLETE.md +296 -0
- package/docs/CATEGORY_SEARCH.md +265 -0
- package/package.json +32 -0
- package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
- package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
- package/src/category-registry.ts +6 -0
- package/src/config/search-engine-constants.ts +216 -0
- package/src/constants.ts +6 -0
- package/src/engine-descriptions.ts +7 -0
- package/src/engine-status.ts +7 -0
- package/src/engine.ts +6 -0
- package/src/registry/search-engine-category-registry.ts +200 -0
- package/src/registry/search-engine-descriptions.ts +121 -0
- package/src/registry/search-engine-status-tracker.ts +240 -0
- package/src/result-container.ts +8 -0
- package/src/search/search-engines-registry-list.ts +205 -0
- package/src/search/search-query-executor.ts +168 -0
- package/src/search/search-result-container.ts +421 -0
- package/src/search-web-types.ts +7 -0
- package/src/search.ts +10 -0
- package/src/sources/academic/arxiv.ts +72 -0
- package/src/sources/academic/core.ts +65 -0
- package/src/sources/academic/crossref.ts +75 -0
- package/src/sources/academic/doaj.ts +63 -0
- package/src/sources/academic/google_scholar.ts +53 -0
- package/src/sources/academic/openalex.ts +75 -0
- package/src/sources/academic/pubmed.ts +96 -0
- package/src/sources/academic/semantic_scholar.ts +77 -0
- package/src/sources/academic/wikidata.ts +44 -0
- package/src/sources/general/baidu.ts +59 -0
- package/src/sources/general/bing.ts +30 -0
- package/src/sources/general/brave.ts +54 -0
- package/src/sources/general/duckduckgo.ts +49 -0
- package/src/sources/general/google.ts +68 -0
- package/src/sources/general/mojeek.ts +56 -0
- package/src/sources/general/qwant.ts +37 -0
- package/src/sources/general/startpage.ts +44 -0
- package/src/sources/general/yahoo.ts +41 -0
- package/src/sources/general/yandex.ts +56 -0
- package/src/sources/images/bing_images.ts +68 -0
- package/src/sources/images/deviantart.ts +71 -0
- package/src/sources/images/flickr.ts +132 -0
- package/src/sources/images/google_images.ts +101 -0
- package/src/sources/images/imgur.ts +58 -0
- package/src/sources/images/openclipart.ts +52 -0
- package/src/sources/images/pixabay.ts +56 -0
- package/src/sources/images/unsplash.ts +38 -0
- package/src/sources/images/wallhaven.ts +50 -0
- package/src/sources/it/crates.ts +41 -0
- package/src/sources/it/dockerhub.ts +47 -0
- package/src/sources/it/github.ts +37 -0
- package/src/sources/it/gitlab.ts +55 -0
- package/src/sources/it/npm.ts +36 -0
- package/src/sources/it/packagist.ts +43 -0
- package/src/sources/it/pypi.ts +30 -0
- package/src/sources/it/rubygems.ts +43 -0
- package/src/sources/it/stackoverflow.ts +39 -0
- package/src/sources/maps/apple_maps.ts +105 -0
- package/src/sources/maps/openstreetmap.ts +34 -0
- package/src/sources/maps/photon.ts +77 -0
- package/src/sources/news/bing_news.ts +95 -0
- package/src/sources/news/google_news.ts +80 -0
- package/src/sources/news/hackernews.ts +94 -0
- package/src/sources/news/yahoo_news.ts +77 -0
- package/src/sources/shopping/ebay.ts +96 -0
- package/src/sources/social/mastodon.ts +46 -0
- package/src/sources/social/medium.ts +52 -0
- package/src/sources/social/reddit.ts +48 -0
- package/src/sources/social/soundcloud.ts +64 -0
- package/src/sources/social/twitter.ts +56 -0
- package/src/sources/specialized/annas_archive.ts +97 -0
- package/src/sources/specialized/archive.ts +48 -0
- package/src/sources/specialized/genius.ts +43 -0
- package/src/sources/specialized/goodreads.ts +62 -0
- package/src/sources/specialized/imdb.ts +55 -0
- package/src/sources/specialized/openlibrary.ts +59 -0
- package/src/sources/specialized/wikipedia.ts +37 -0
- package/src/sources/specialized/wttr.ts +98 -0
- package/src/sources/torrents/1337x.ts +43 -0
- package/src/sources/torrents/eztv.ts +47 -0
- package/src/sources/torrents/kickass.ts +63 -0
- package/src/sources/torrents/nyaa.ts +53 -0
- package/src/sources/torrents/solidtorrents.ts +68 -0
- package/src/sources/torrents/thepiratebay.ts +57 -0
- package/src/sources/torrents/yts.ts +54 -0
- package/src/sources/videos/bing_videos.ts +91 -0
- package/src/sources/videos/dailymotion.ts +101 -0
- package/src/sources/videos/invidious.ts +86 -0
- package/src/sources/videos/peertube.ts +76 -0
- package/src/sources/videos/vimeo.ts +67 -0
- package/src/sources/videos/youtube.ts +78 -0
- package/src/suggest-next-words/autocomplete-ai.ts +38 -0
- package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
- package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
- package/src/types/search-engine-interface.ts +27 -0
- package/src/types/search-result-types.ts +405 -0
- package/test/api.test.ts +128 -0
- package/test/autocomplete-ai.test.ts +20 -0
- package/test/autocomplete-engines.test.ts +131 -0
- package/test/engine-health-suite.test.ts +350 -0
- package/test/search.test.ts +69 -0
- package/test/sources-unit.test.ts +1152 -0
- package/test/sources.test.ts +182 -0
- package/test/test-utils.ts +81 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for all search-web/sources engines
|
|
3
|
+
*
|
|
4
|
+
* Tests each engine function directly to verify it returns
|
|
5
|
+
* valid JSON results with the expected structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { EngineResult } from "../src/search-web-types.js";
|
|
10
|
+
import { extractResponseData } from "../src/engine.js";
|
|
11
|
+
import grab from "grab-url";
|
|
12
|
+
|
|
13
|
+
grab("", {
|
|
14
|
+
setDefaults: true,
|
|
15
|
+
headers: {
|
|
16
|
+
"User-Agent":
|
|
17
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Import all engines from the exported list
|
|
22
|
+
import { ALL_ENGINES } from "../src/search.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validates that results are a non-empty array of objects with
|
|
26
|
+
* at minimum title and content strings.
|
|
27
|
+
*/
|
|
28
|
+
function assertValidResults(results: EngineResult[], engineName: string) {
|
|
29
|
+
expect(Array.isArray(results), `${engineName} should return an array`).toBe(
|
|
30
|
+
true,
|
|
31
|
+
);
|
|
32
|
+
expect(
|
|
33
|
+
results.length,
|
|
34
|
+
`${engineName} should return at least 1 result`,
|
|
35
|
+
).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
for (const r of results) {
|
|
38
|
+
expect(typeof r.title, `${engineName} result title should be string`).toBe(
|
|
39
|
+
"string",
|
|
40
|
+
);
|
|
41
|
+
expect(
|
|
42
|
+
r.title.length,
|
|
43
|
+
`${engineName} result title should not be empty`,
|
|
44
|
+
).toBeGreaterThan(0);
|
|
45
|
+
expect(
|
|
46
|
+
typeof r.content,
|
|
47
|
+
`${engineName} result content should be string`,
|
|
48
|
+
).toBe("string");
|
|
49
|
+
if (r.url) {
|
|
50
|
+
expect(typeof r.url).toBe("string");
|
|
51
|
+
}
|
|
52
|
+
if (r.engine) {
|
|
53
|
+
expect(typeof r.engine).toBe("string");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_QUERIES: Record<string, string> = {
|
|
59
|
+
general: "typescript",
|
|
60
|
+
it: "typescript",
|
|
61
|
+
images: "sunset",
|
|
62
|
+
videos: "tutorial",
|
|
63
|
+
news: "technology",
|
|
64
|
+
academic: "machine%20learning",
|
|
65
|
+
torrents: "ubuntu",
|
|
66
|
+
social: "programming",
|
|
67
|
+
maps: "New%20York",
|
|
68
|
+
shopping: "laptop",
|
|
69
|
+
specialized: "books",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SPECIFIC_QUERIES: Record<string, string> = {
|
|
73
|
+
npm: "express",
|
|
74
|
+
crates: "serde",
|
|
75
|
+
dockerhub: "nginx",
|
|
76
|
+
pypi: "requests",
|
|
77
|
+
packagist: "laravel",
|
|
78
|
+
rubygems: "rails",
|
|
79
|
+
imgur: "cats",
|
|
80
|
+
pixabay: "mountains",
|
|
81
|
+
wallhaven: "landscape",
|
|
82
|
+
deviantart: "art",
|
|
83
|
+
openclipart: "arrow",
|
|
84
|
+
vimeo: "documentary",
|
|
85
|
+
dailymotion: "music",
|
|
86
|
+
peertube: "linux",
|
|
87
|
+
arxiv: "neural%20networks",
|
|
88
|
+
wikidata: "python",
|
|
89
|
+
semantic_scholar: "deep%20learning",
|
|
90
|
+
crossref: "quantum%20computing",
|
|
91
|
+
pubmed: "cancer",
|
|
92
|
+
nyaa: "one%20piece",
|
|
93
|
+
yts: "inception",
|
|
94
|
+
eztv: "breaking%20bad",
|
|
95
|
+
medium: "javascript",
|
|
96
|
+
soundcloud: "lofi",
|
|
97
|
+
photon: "Paris",
|
|
98
|
+
apple_maps: "Tokyo",
|
|
99
|
+
wikipedia: "javascript",
|
|
100
|
+
imdb: "inception",
|
|
101
|
+
genius: "bohemian%20rhapsody",
|
|
102
|
+
archive: "books",
|
|
103
|
+
openlibrary: "tolkien",
|
|
104
|
+
wttr: "London",
|
|
105
|
+
annas_archive: "python",
|
|
106
|
+
goodreads: "dune",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// All engines with their query and category for organized testing
|
|
110
|
+
const engines = ALL_ENGINES.map((eng) => ({
|
|
111
|
+
name: eng.name,
|
|
112
|
+
fn: eng.fn,
|
|
113
|
+
category: eng.categories[0] || "general",
|
|
114
|
+
query:
|
|
115
|
+
SPECIFIC_QUERIES[eng.name] ||
|
|
116
|
+
DEFAULT_QUERIES[eng.categories[0] || "general"] ||
|
|
117
|
+
"test",
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// Group engines by category for organized describe blocks
|
|
121
|
+
const byCategory = engines.reduce(
|
|
122
|
+
(acc, e) => {
|
|
123
|
+
(acc[e.category] ??= []).push(e);
|
|
124
|
+
return acc;
|
|
125
|
+
},
|
|
126
|
+
{} as Record<string, typeof engines>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Track results across all tests for summary
|
|
130
|
+
const testResults: Array<{
|
|
131
|
+
name: string;
|
|
132
|
+
category: string;
|
|
133
|
+
status: "pass" | "fail";
|
|
134
|
+
resultCount: number;
|
|
135
|
+
timeMs: number;
|
|
136
|
+
error?: string;
|
|
137
|
+
}> = [];
|
|
138
|
+
|
|
139
|
+
describe("Source Engine Unit Tests", () => {
|
|
140
|
+
for (const [category, categoryEngines] of Object.entries(byCategory)) {
|
|
141
|
+
describe(`${category}`, () => {
|
|
142
|
+
for (const eng of categoryEngines) {
|
|
143
|
+
it(`${eng.name} returns valid JSON results`, async () => {
|
|
144
|
+
const start = Date.now();
|
|
145
|
+
let results: EngineResult[];
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
let rawResults = await eng.fn(eng.query, 1);
|
|
149
|
+
results = extractResponseData(rawResults) as EngineResult[];
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const errMsg =
|
|
152
|
+
error instanceof Error ? error.message : String(error);
|
|
153
|
+
testResults.push({
|
|
154
|
+
name: eng.name,
|
|
155
|
+
category: eng.category,
|
|
156
|
+
status: "fail",
|
|
157
|
+
resultCount: 0,
|
|
158
|
+
timeMs: Date.now() - start,
|
|
159
|
+
error: errMsg,
|
|
160
|
+
});
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const timeMs = Date.now() - start;
|
|
165
|
+
|
|
166
|
+
// Allow null/undefined to be treated as empty array
|
|
167
|
+
if (!results) results = [];
|
|
168
|
+
|
|
169
|
+
testResults.push({
|
|
170
|
+
name: eng.name,
|
|
171
|
+
category: eng.category,
|
|
172
|
+
status: results.length > 0 ? "pass" : "fail",
|
|
173
|
+
resultCount: results.length,
|
|
174
|
+
timeMs,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assertValidResults(results, eng.name);
|
|
178
|
+
}, 20000);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { EngineResult } from "../src/engine";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock HTML response helper
|
|
5
|
+
*/
|
|
6
|
+
export function createMockHtml(content: string): string {
|
|
7
|
+
return content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mock JSON response helper
|
|
12
|
+
*/
|
|
13
|
+
export function createMockJson(data: any): any {
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate engine result structure
|
|
19
|
+
*/
|
|
20
|
+
export function validateEngineResult(result: EngineResult): boolean {
|
|
21
|
+
return (
|
|
22
|
+
typeof result.title === "string" &&
|
|
23
|
+
result.title.length > 0 &&
|
|
24
|
+
typeof result.content === "string" &&
|
|
25
|
+
(result.url === undefined || typeof result.url === "string") &&
|
|
26
|
+
(result.link === undefined || typeof result.link === "string") &&
|
|
27
|
+
(result.thumbnail === undefined || typeof result.thumbnail === "string") &&
|
|
28
|
+
(result.engine === undefined || typeof result.engine === "string")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate all results in an array
|
|
34
|
+
*/
|
|
35
|
+
export function validateEngineResults(results: EngineResult[]): boolean {
|
|
36
|
+
if (!Array.isArray(results)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return results.every(validateEngineResult);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Common test queries for different categories
|
|
44
|
+
*/
|
|
45
|
+
export const TEST_QUERIES = {
|
|
46
|
+
general: ["javascript", "python", "linux", "machine learning"],
|
|
47
|
+
code: ["react hooks", "typescript generics", "nodejs express"],
|
|
48
|
+
torrent: ["ubuntu", "debian", "linux mint"],
|
|
49
|
+
anime: ["one piece", "naruto", "attack on titan"],
|
|
50
|
+
movies: ["inception", "interstellar", "the matrix"],
|
|
51
|
+
tv: ["breaking bad", "game of thrones", "the office"],
|
|
52
|
+
music: ["lofi hip hop", "jazz", "classical music"],
|
|
53
|
+
social: ["javascript news", "tech trends", "programming"],
|
|
54
|
+
academic: ["quantum computing", "neural networks", "cryptography"],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Wait helper for rate limiting
|
|
59
|
+
*/
|
|
60
|
+
export async function wait(ms: number): Promise<void> {
|
|
61
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Retry helper for flaky network requests
|
|
66
|
+
*/
|
|
67
|
+
export async function retry<T>(
|
|
68
|
+
fn: () => Promise<T>,
|
|
69
|
+
retries: number = 3,
|
|
70
|
+
delay: number = 1000
|
|
71
|
+
): Promise<T> {
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (retries <= 0) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
await wait(delay);
|
|
79
|
+
return retry(fn, retries - 1, delay * 2);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"src/**/*",
|
|
14
|
+
"test/**/*"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
reporter: ['text', 'json', 'html'],
|
|
10
|
+
exclude: [
|
|
11
|
+
'node_modules/**',
|
|
12
|
+
'dist/**',
|
|
13
|
+
'**/*.test.ts',
|
|
14
|
+
'**/__tests__/**'
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
testTimeout: 30000, // 30 seconds for network requests
|
|
18
|
+
hookTimeout: 30000
|
|
19
|
+
}
|
|
20
|
+
});
|