Files
santifer--career-ops/providers/_registry.mjs
T
wehub-resource-sync d083df1fdb
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 2s
Web CI / web typecheck + build (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
CodeQL Analysis / Analyze (go) (push) Failing after 16s
chore: import upstream snapshot with attribution
2026-07-13 12:02:43 +08:00

95 lines
3.4 KiB
JavaScript

// Provider registry — loading and routing shared by the scanner and the portal
// health check. Files prefixed with _ are never loaded as providers by scan.mjs,
// so this helper module lives safely alongside the provider plugins.
//
// Extracted from scan.mjs (#1451-era inline definitions) so verify-portals.mjs
// can route non-ATS boards through the SAME provider layer the scanner uses,
// without importing scan.mjs itself (which has top-level side effects and would
// form an import cycle via classifyFetchError).
import { existsSync, readdirSync } from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
/**
* Load every provider plugin in a directory into an id→provider Map.
*
* Alphabetical order so detect() priority is deterministic across machines.
* Malformed modules (wrong shape, duplicate id, import error) are logged and
* skipped, never fatal.
*
* @param {string} dir - Absolute path to the providers directory.
* @returns {Promise<Map<string, object>>}
*/
export async function loadProviders(dir) {
const providers = new Map();
if (!existsSync(dir)) return providers;
const entries = readdirSync(dir)
.filter(f => f.endsWith('.mjs') && !f.startsWith('_'))
.sort();
for (const file of entries) {
const full = path.join(dir, file);
let mod;
try {
mod = await import(pathToFileURL(full).href);
} catch (err) {
console.error(`⚠️ ${file}: failed to load — ${err.message}`);
continue;
}
const p = mod.default;
if (!p || typeof p.fetch !== 'function' || !p.id) {
console.error(`⚠️ ${file}: skipping — default export must be { id, fetch }`);
continue;
}
if (providers.has(p.id)) {
console.error(`⚠️ ${file}: duplicate provider id "${p.id}" — keeping first`);
continue;
}
providers.set(p.id, p);
}
return providers;
}
/**
* Resolve which provider handles a tracked_companies entry.
* 1. Explicit `provider:` field wins (skips detect()).
* 2. local-parser when parser.command + script are configured (before API detect).
* 3. Otherwise each provider's detect() runs in load order; first hit wins.
*
* @param {object} entry - tracked_companies entry.
* @param {Map<string, object>} providers - id→provider Map from loadProviders().
* @param {{skipIds?: string[]}} [opts] - Provider ids to skip (e.g. 'local-parser'
* so a network-only health check never execs a configured local command).
* @returns {{provider: object}|{error: string}|null}
*/
export function resolveProvider(entry, providers, { skipIds = [] } = {}) {
if (entry.provider) {
const p = providers.get(entry.provider);
if (!p) return { error: `unknown provider: ${entry.provider}` };
return { provider: p };
}
const localParser = providers.get('local-parser');
if (localParser && !skipIds.includes('local-parser')) {
try {
const hit = localParser.detect?.(entry);
if (hit) return { provider: localParser };
} catch (err) {
console.error(`⚠️ local-parser: detect() threw for "${entry.name}" — ${err.message}`);
}
}
for (const p of providers.values()) {
if (skipIds.includes(p.id)) continue;
let hit;
try {
hit = p.detect?.(entry);
} catch (err) {
console.error(`⚠️ ${p.id}: detect() threw for "${entry.name}" — ${err.message}`);
continue;
}
if (hit) return { provider: p };
}
return null;
}