Files
marketplaces/src/app/widgets/registry/widget-manifest.service.ts
sdarbinyan ce63931bc2
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
feat: dead-config sweep, test suite foundation, widget settingsSchema validation
Sprint G: audited every BootstrapConfig field for a real runtime consumer
(docs/DEAD-CONFIG-AUDIT.md). Wired 3 previously-dead editable fields:
footer.logoUrl, company.address.street/contacts.phone, catalog.suggestionsEnabled.
Remaining dead fields needing a business/design decision tracked in
PRODUCT_BACKLOG.md/KNOWN-ISSUES.md, not silently left.

Sprint H: 6 new spec files (test count 57 -> 83), covering ProjectEditorFacade
(undo/redo, draft persistence, publish gating), AdminAnalyticsFacade
(never-fabricate-a-number contract), and regression coverage for this
session's carousel/hero/profile-toggle fixes.

Sprint I: widget settingsSchema (declared in widget-manifest.json, never
validated) now enforced via a new lightweight schema check in
ProjectValidator, surfaced through the existing issuesByField pipeline.
Same check reused in diagnostics so editor and diagnostics can't disagree.

Verification: tsc clean, ng build clean, 83/83 tests pass, barry-cache
validate clean (2 pre-existing unrelated warnings only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 20:47:13 +04:00

64 lines
2.4 KiB
TypeScript

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, map, of, shareReplay, switchMap, take, tap } from 'rxjs';
import { WidgetManifestEntry, WidgetManifestFile } from '../contracts/widget-manifest.contract';
import { ConfigService } from '../../core/config/config.service';
@Injectable({ providedIn: 'root' })
export class WidgetManifestService {
private readonly fallbackManifestUrl = '/assets/mock/bootstrap/widget-manifest.json';
private readonly manifestByUrl = new Map<string, Observable<WidgetManifestFile>>();
/** Last manifest resolved by getManifest(), for synchronous readers (e.g. ProjectValidator) that can't await an Observable. Mirrors ConfigService.getBootstrapSnapshot(). */
private manifestSnapshot: WidgetManifestFile | null = null;
constructor(
private readonly http: HttpClient,
private readonly configService: ConfigService
) {}
getManifest(): Observable<WidgetManifestFile> {
return this.resolveManifestUrl().pipe(
switchMap((manifestUrl) => {
const cached = this.manifestByUrl.get(manifestUrl);
if (cached) {
return cached;
}
const manifest$ = this.http.get<WidgetManifestFile>(manifestUrl).pipe(
tap(manifest => { this.manifestSnapshot = manifest; }),
shareReplay({ bufferSize: 1, refCount: true }),
catchError(() => of({ widgets: [] }))
);
this.manifestByUrl.set(manifestUrl, manifest$);
return manifest$;
})
);
}
getWidgets(): Observable<WidgetManifestEntry[]> {
return this.getManifest().pipe(map((manifest) => manifest.widgets ?? []));
}
getWidget(type: string): Observable<WidgetManifestEntry | undefined> {
return this.getWidgets().pipe(map((widgets) => widgets.find((widget) => widget.type === type)));
}
/** Synchronous accessor for the last-resolved manifest, or null before it's loaded once. */
getManifestSnapshot(): WidgetManifestFile | null {
return this.manifestSnapshot;
}
private resolveManifestUrl(): Observable<string> {
const snapshotUrl = this.configService.getBootstrapSnapshot()?.widgetRegistry?.manifestUrl;
if (snapshotUrl) {
return of(snapshotUrl);
}
return this.configService.loadBootstrap().pipe(
take(1),
map((bootstrap) => bootstrap.widgetRegistry?.manifestUrl || this.fallbackManifestUrl),
catchError(() => of(this.fallbackManifestUrl))
);
}
}