velxio/frontend/src/utils/libraryManifest.ts

83 lines
3.2 KiB
TypeScript

/**
* Library-manifest helpers — pure functions, no store imports.
*
* Why this exists (2026-08 library-contamination investigation): 79% of
* projects compile with an EMPTY manifest, which forces the backend into
* scan-all resolution over ~1000 shared libraries — the mode where an
* unrelated library can be dragged into the build. Rather than ever
* breaking those projects, the backend now reports the libraries a
* successful scan-all build actually used (`manifest_suggested_libraries`,
* shape { header: [candidateNames] }), and the frontend folds them into the
* board's declared manifest here. One green build = one migrated project.
*/
/** Library-name normalization, mirroring the backend's `_norm_lib_name`:
* lowercase, alphanumerics only, so "Adafruit GFX Library",
* "Adafruit_GFX_Library" and "adafruitgfxlibrary" all compare equal. */
export function normalizeLibName(name: string): string {
return (name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
}
/** Strip a trailing "@version" from a spec ("DHT sensor library@1.4.6"). */
export function bareLibName(spec: string): string {
const at = spec.lastIndexOf('@');
return at > 0 ? spec.slice(0, at) : spec;
}
/**
* Merge the backend's suggested libraries into the current manifest.
*
* Conservative by design:
* - only entries with EXACTLY ONE candidate are taken (an ambiguous
* suggestion must never guess its way into the manifest);
* - names already present (normalized comparison) are skipped;
* - returns `null` when there is nothing new, so callers can cheaply
* detect "no change" and avoid a store write / autosave churn.
*/
export function mergeSuggestedLibraries(
current: string[] | undefined,
suggested: Record<string, string[]> | null | undefined,
): string[] | null {
if (!suggested) return null;
const have = new Set((current ?? []).map(normalizeLibName));
const additions: string[] = [];
for (const candidates of Object.values(suggested)) {
if (!Array.isArray(candidates) || candidates.length !== 1) continue;
const name = String(candidates[0] ?? '').trim();
if (!name) continue;
const norm = normalizeLibName(name);
if (!norm || have.has(norm)) continue;
have.add(norm);
additions.push(name);
}
if (!additions.length) return null;
return [...(current ?? []), ...additions];
}
/**
* Add one just-installed library to a manifest (install-declares flow).
* Returns `null` when it is already declared.
*/
export function addLibraryToManifest(
current: string[] | undefined,
spec: string,
): string[] | null {
const name = bareLibName(spec).trim();
if (!name) return null;
const norm = normalizeLibName(name);
const have = new Set((current ?? []).map(normalizeLibName));
if (have.has(norm)) return null;
return [...(current ?? []), name];
}
/** Remove a library from a manifest (uninstall flow). Null = no change. */
export function removeLibraryFromManifest(
current: string[] | undefined,
spec: string,
): string[] | null {
if (!current?.length) return null;
const norm = normalizeLibName(bareLibName(spec));
const next = current.filter((n) => normalizeLibName(n) !== norm);
return next.length === current.length ? null : next;
}