1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-25521] Move importer metadata to native code (#16695)

* Add importer metadata to native code

* Impl napi code in ts

* Impl napi code in ts

* Fix clippy

* Fix clippy

* remove ts util tests

* Check for installed browsers

* PR fixes

* test fix

* fix clippy

* fix tests

* Bug fix

* clippy fix

* Correct tests

* fix clippy

* fix clippy

* Correct tests

* Correct tests

* [PM-25521] Wire up loading metadata on desktop (#16813)

* Initial commit

* Fix issues regarding now unused feature flag

* Fixed ts-strict issues

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: adudek-bw <adudek@bitwarden.com>

* Remove logic to skip Brave as that now happens via the native code

* Define default capabilities which can be overwritten by specifc client/platform

* Fix DI issues

* Do not overwrite existing importers, just add new ones or update existing ones

* feat: [PM-25521] return metadata directly (not as JSON) (#16882)

* feat: return metadata directly (not as JSON)

* Fix broken builds

Move getMetaData into chromium_importer
Remove chromium_importer_metadata and any related service
Parse object from native instead of json

* Run cargo fmt

* Fix cargo dependency sort order

* Use exposed type from NAPI instead of redefining it.

* Run cargo fmt

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>

* Only enable chromium loader for installed and supported browsers

---------

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
adudek-bw
2025-10-17 09:46:10 -04:00
committed by GitHub
parent 9ba1de702e
commit 7015663c38
32 changed files with 641 additions and 337 deletions

View File

@@ -72,7 +72,11 @@ import {
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
import { ImportOption, ImportResult, ImportType } from "../models";
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
import {
ImportCollectionServiceAbstraction,
ImportMetadataServiceAbstraction,
ImportServiceAbstraction,
} from "../services";
import { ImportChromeComponent } from "./chrome";
import {
@@ -236,6 +240,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
protected accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService,
private destroyRef: DestroyRef,
protected importMetadataService: ImportMetadataServiceAbstraction,
) {}
protected get importBlockedByPolicy(): boolean {
@@ -254,9 +259,11 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
}
async ngOnInit() {
await this.importMetadataService.init();
this.setImportOptions();
this.importService
this.importMetadataService
.metadata$(this.formGroup.controls.format.valueChanges)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({

View File

@@ -37,7 +37,9 @@ import {
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
// to a general module.
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>(
"SystemServices",
);
/** Import service factories */
export const ImporterProviders: SafeProvider[] = [
@@ -85,7 +87,6 @@ export const ImporterProviders: SafeProvider[] = [
PinServiceAbstraction,
AccountService,
RestrictedItemTypesService,
SYSTEM_SERVICE_PROVIDER,
],
}),
];

View File

@@ -1,3 +1,4 @@
export * from "./dialog";
export * from "./importer-providers";
export { ImportComponent } from "./import.component";

View File

@@ -1,4 +1,5 @@
export * from "./models";
export * from "./metadata";
export * from "./services";
export { Importer } from "./importers/importer";

View File

@@ -2,26 +2,32 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
import { ImportType } from "../models";
import { Loader, Instructions } from "./data";
import { Instructions, Loader } from "./data";
import { ImporterMetadata } from "./types";
// FIXME: load this data from rust code
export type ImportersMetadata = Partial<Record<ImportType, ImporterMetadata>>;
/** List of all supported importers and their default capabilities
* Note: the loaders listed here are the ones that are supported in all clients.
* Specific clients may have additional loaders available based on platform capabilities.
*/
const importers = [
{ id: "bitwardenjson", loaders: [Loader.file], instructions: Instructions.unique },
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
{ id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium },
{ id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
{ id: "operacsv", loaders: [Loader.file], instructions: Instructions.chromium },
{
id: "vivaldicsv",
loaders: [Loader.file, Loader.chromium],
loaders: [Loader.file],
instructions: Instructions.chromium,
},
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
{ id: "bravecsv", loaders: [Loader.file], instructions: Instructions.chromium },
{ id: "edgecsv", loaders: [Loader.file], instructions: Instructions.chromium },
// FIXME: add other formats and remove `Partial` from export
] as const;
/** Describes which loaders are available for each import type */
export const Importers: Partial<Record<ImportType, ImporterMetadata>> = deepFreeze(
export const Importers: ImportersMetadata = deepFreeze(
Object.fromEntries(importers.map((i) => [i.id, i])),
);

View File

@@ -0,0 +1,51 @@
import { map, Observable } from "rxjs";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata";
import { ImportType } from "../models/import-options";
import { availableLoaders } from "../util";
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
export class DefaultImportMetadataService implements ImportMetadataServiceAbstraction {
protected importers: ImportersMetadata = Importers;
private logger: SemanticLogger;
constructor(protected system: SystemServiceProvider) {
this.logger = system.log({ type: "ImportMetadataService" });
}
async init(): Promise<void> {
// no-op for default implementation
}
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
const client = this.system.environment.getClientType();
const capabilities$ = type$.pipe(
map((type) => {
if (!this.importers) {
return { type, loaders: [] };
}
const loaders = availableLoaders(this.importers, type, client);
if (!loaders || loaders.length === 0) {
return { type, loaders: [] };
}
const capabilities: ImporterMetadata = { type, loaders };
if (type in this.importers) {
capabilities.instructions = this.importers[type]?.instructions;
}
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
return capabilities;
}),
);
return capabilities$;
}
}

View File

@@ -0,0 +1,11 @@
import { Observable } from "rxjs";
import { ImporterMetadata } from "../metadata";
import { ImportType } from "../models/import-options";
export abstract class ImportMetadataServiceAbstraction {
abstract init(): Promise<void>;
/** describes the features supported by a format */
abstract metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
}

View File

@@ -0,0 +1,110 @@
import { mock, MockProxy } from "jest-mock-extended";
import { Subject, firstValueFrom } from "rxjs";
import { ClientType } from "@bitwarden/client-type";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { ImporterMetadata, Instructions } from "../metadata";
import { ImportType } from "../models";
import { DefaultImportMetadataService } from "./default-import-metadata.service";
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
describe("ImportMetadataService", () => {
let sut: ImportMetadataServiceAbstraction;
let systemServiceProvider: MockProxy<SystemServiceProvider>;
beforeEach(() => {
const configService = mock<ConfigService>();
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
});
sut = new DefaultImportMetadataService(systemServiceProvider);
});
describe("metadata$", () => {
let typeSubject: Subject<ImportType>;
let mockLogger: { debug: jest.Mock };
beforeEach(() => {
typeSubject = new Subject<ImportType>();
mockLogger = { debug: jest.fn() };
const configService = mock<ConfigService>();
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue(mockLogger),
});
// Recreate the service with the updated mocks for logging tests
sut = new DefaultImportMetadataService(systemServiceProvider);
});
afterEach(() => {
typeSubject.complete();
});
it("should emit metadata when type$ emits", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(sut.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result).toEqual({
type: testType,
loaders: expect.any(Array),
instructions: Instructions.chromium,
});
expect(result.type).toBe(testType);
});
it("should update when type$ changes", async () => {
const emissions: ImporterMetadata[] = [];
const subscription = sut.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
typeSubject.next("chromecsv");
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].type).toBe("chromecsv");
expect(emissions[1].type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should log debug information with correct data", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(sut.metadata$(typeSubject));
typeSubject.next(testType);
await metadataPromise;
expect(mockLogger.debug).toHaveBeenCalledWith(
{ importType: testType, capabilities: expect.any(Object) },
"capabilities updated",
);
});
});
});

View File

@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
@@ -8,7 +7,6 @@ import { CollectionView } from "@bitwarden/admin-console/common";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { Importer } from "../importers/importer";
import { ImporterMetadata } from "../metadata";
import { ImportOption, ImportType } from "../models/import-options";
import { ImportResult } from "../models/import-result";
@@ -17,9 +15,6 @@ export abstract class ImportServiceAbstraction {
regularImportOptions: readonly ImportOption[];
getImportOptions: () => ImportOption[];
/** describes the features supported by a format */
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
import: (
importer: Importer,
fileContents: string,

View File

@@ -1,20 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, Subject, firstValueFrom } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ClientType } from "@bitwarden/client-type";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -25,8 +18,6 @@ import { KeyService } from "@bitwarden/key-management";
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
import { Importer } from "../importers/importer";
import { ImporterMetadata, Instructions, Loader } from "../metadata";
import { ImportType } from "../models";
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
@@ -44,7 +35,6 @@ describe("ImportService", () => {
let pinService: MockProxy<PinServiceAbstraction>;
let accountService: MockProxy<AccountService>;
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
let systemServiceProvider: MockProxy<SystemServiceProvider>;
beforeEach(() => {
cipherService = mock<CipherService>();
@@ -57,18 +47,6 @@ describe("ImportService", () => {
pinService = mock<PinServiceAbstraction>();
restrictedItemTypesService = mock<RestrictedItemTypesService>();
const configService = mock<ConfigService>();
configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false));
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
});
importService = new ImportService(
cipherService,
folderService,
@@ -80,7 +58,6 @@ describe("ImportService", () => {
pinService,
accountService,
restrictedItemTypesService,
systemServiceProvider,
);
});
@@ -268,136 +245,6 @@ describe("ImportService", () => {
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
});
});
describe("metadata$", () => {
let featureFlagSubject: BehaviorSubject<boolean>;
let typeSubject: Subject<ImportType>;
let mockLogger: { debug: jest.Mock };
beforeEach(() => {
featureFlagSubject = new BehaviorSubject(false);
typeSubject = new Subject<ImportType>();
mockLogger = { debug: jest.fn() };
const configService = mock<ConfigService>();
configService.getFeatureFlag$.mockReturnValue(featureFlagSubject);
const environment = mock<PlatformUtilsService>();
environment.getClientType.mockReturnValue(ClientType.Desktop);
systemServiceProvider = mock<SystemServiceProvider>({
configService,
environment,
log: jest.fn().mockReturnValue(mockLogger),
});
// Recreate the service with the updated mocks for logging tests
importService = new ImportService(
cipherService,
folderService,
importApiService,
i18nService,
collectionService,
keyService,
encryptService,
pinService,
accountService,
restrictedItemTypesService,
systemServiceProvider,
);
});
afterEach(() => {
featureFlagSubject.complete();
typeSubject.complete();
});
it("should emit metadata when type$ emits", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result).toEqual({
type: testType,
loaders: expect.any(Array),
instructions: Instructions.chromium,
});
expect(result.type).toBe(testType);
});
it("should include all loaders when chromium feature flag is enabled", async () => {
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
featureFlagSubject.next(true);
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
const result = await metadataPromise;
expect(result.loaders).toContain(Loader.chromium);
expect(result.loaders).toContain(Loader.file);
});
it("should update when type$ changes", async () => {
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
typeSubject.next("chromecsv");
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions).toHaveLength(2);
expect(emissions[0].type).toBe("chromecsv");
expect(emissions[1].type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should update when both type$ and feature flag change", async () => {
const emissions: ImporterMetadata[] = [];
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
emissions.push(metadata);
});
// Initial emission
typeSubject.next("chromecsv");
// Change both at the same time
featureFlagSubject.next(true);
typeSubject.next("bravecsv");
// Wait for emissions
await new Promise((resolve) => setTimeout(resolve, 0));
expect(emissions.length).toBeGreaterThanOrEqual(2);
const lastEmission = emissions[emissions.length - 1];
expect(lastEmission.type).toBe("bravecsv");
subscription.unsubscribe();
});
it("should log debug information with correct data", async () => {
const testType: ImportType = "chromecsv";
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
typeSubject.next(testType);
await metadataPromise;
expect(mockLogger.debug).toHaveBeenCalledWith(
{ importType: testType, capabilities: expect.any(Object) },
"capabilities updated",
);
});
});
});
function createCipher(options: Partial<CipherView> = {}) {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
import { firstValueFrom, map } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
@@ -11,7 +11,6 @@ import {
} from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { DeviceType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
@@ -20,8 +19,6 @@ import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SemanticLogger } from "@bitwarden/common/tools/log";
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@@ -98,7 +95,6 @@ import {
PasswordDepot17XmlImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import { ImporterMetadata, Importers, Loader } from "../metadata";
import {
featuredImportOptions,
ImportOption,
@@ -108,15 +104,12 @@ import {
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
import { availableLoaders as availableLoaders } from "../util";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
regularImportOptions = regularImportOptions as readonly ImportOption[];
private logger: SemanticLogger;
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -128,55 +121,12 @@ export class ImportService implements ImportServiceAbstraction {
private pinService: PinServiceAbstraction,
private accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService,
private system: SystemServiceProvider,
) {
this.logger = system.log({ type: "ImportService" });
}
) {}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
const client = this.system.environment.getClientType();
const capabilities$ = combineLatest([type$]).pipe(
map(([type]) => {
let loaders = availableLoaders(type, client);
// Mac App Store is currently disabled due to sandboxing.
let isUnsupported = this.system.environment.isMacAppStore();
// disable the chromium loader for Brave on Windows only
if (type === "bravecsv") {
try {
const device = this.system.environment.getDevice();
const isWindowsDesktop = device === DeviceType.WindowsDesktop;
if (isWindowsDesktop) {
isUnsupported = true;
}
} catch {
isUnsupported = true;
}
}
// If the browser is unsupported, remove the chromium loader
if (isUnsupported) {
loaders = loaders?.filter((loader) => loader !== Loader.chromium);
}
const capabilities: ImporterMetadata = { type, loaders };
if (type in Importers) {
capabilities.instructions = Importers[type].instructions;
}
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
return capabilities;
}),
);
return capabilities$;
}
async import(
importer: Importer,
fileContents: string,

View File

@@ -4,4 +4,7 @@ export { ImportApiService } from "./import-api.service";
export { ImportServiceAbstraction } from "./import.service.abstraction";
export { ImportService } from "./import.service";
export { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
export { DefaultImportMetadataService } from "./default-import-metadata.service";
export { ImportCollectionServiceAbstraction } from "./import-collection.service.abstraction";

View File

@@ -1,60 +0,0 @@
import { ClientType } from "@bitwarden/client-type";
import { Loader } from "./metadata";
import { availableLoaders } from "./util";
describe("availableLoaders", () => {
describe("given valid import types", () => {
it("returns available loaders when client supports all loaders", () => {
const result = availableLoaders("operacsv", ClientType.Desktop);
expect(result).toEqual([Loader.file, Loader.chromium]);
});
it("returns filtered loaders when client supports some loaders", () => {
const result = availableLoaders("operacsv", ClientType.Browser);
expect(result).toEqual([Loader.file]);
});
it("returns single loader for import types with one loader", () => {
const result = availableLoaders("chromecsv", ClientType.Desktop);
expect(result).toEqual([Loader.file]);
});
it("returns all supported loaders for multi-loader import types", () => {
const result = availableLoaders("bravecsv", ClientType.Desktop);
expect(result).toEqual([Loader.file, Loader.chromium]);
});
});
describe("given unknown import types", () => {
it("returns undefined when import type is not found in metadata", () => {
const result = availableLoaders("nonexistent" as any, ClientType.Desktop);
expect(result).toBeUndefined();
});
});
describe("given different client types", () => {
it("returns appropriate loaders for Browser client", () => {
const result = availableLoaders("operacsv", ClientType.Browser);
expect(result).toEqual([Loader.file]);
});
it("returns appropriate loaders for Web client", () => {
const result = availableLoaders("chromecsv", ClientType.Web);
expect(result).toEqual([Loader.file]);
});
it("returns appropriate loaders for CLI client", () => {
const result = availableLoaders("vivaldicsv", ClientType.Cli);
expect(result).toEqual([Loader.file]);
});
});
});

View File

@@ -1,6 +1,6 @@
import { ClientType } from "@bitwarden/client-type";
import { LoaderAvailability, Importers } from "./metadata";
import { LoaderAvailability, ImportersMetadata } from "./metadata";
import { ImportType } from "./models";
/** Lookup the loaders supported by a specific client.
@@ -8,12 +8,20 @@ import { ImportType } from "./models";
* @returns `undefined` when metadata is not defined for the type, or
* an array identifying the supported clients.
*/
export function availableLoaders(type: ImportType, client: ClientType) {
if (!(type in Importers)) {
export function availableLoaders(
importersMetadata: ImportersMetadata,
type: ImportType,
client: ClientType,
) {
if (!importersMetadata) {
return undefined;
}
const capabilities = Importers[type]?.loaders ?? [];
if (!(type in importersMetadata)) {
return undefined;
}
const capabilities = importersMetadata[type]?.loaders ?? [];
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
return available;
}