• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Lumieducation / H5P-Nodejs-library / 9a31ed68-3f83-4d67-b267-111e27055454

18 Jan 2025 07:02PM UTC coverage: 69.408% (+4.0%) from 65.36%
9a31ed68-3f83-4d67-b267-111e27055454

push

circleci

web-flow
fix(deps): update dependency simple-redis-mutex to v2 (#3822)

* fix(deps): update dependency simple-redis-mutex to v2

* refactor(h5p-redis-lock): switched to redis package instead of ioredis

* refactor: removed redundancies in tsconfigs

* refactor: build now emits ES2022

BREAKING CHANGE: packaged build files are now ES2022, h5p-redis-lock now uses redis instead of ioredis

2531 of 4214 branches covered (60.06%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

874 existing lines in 50 files now uncovered.

5834 of 7838 relevant lines covered (74.43%)

5716.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.37
/packages/h5p-server/src/H5PPlayer.ts
1
import LibraryName from './LibraryName';
16✔
2
import {
3
    ContentId,
4
    ContentParameters,
5
    IAssets,
6
    IContentMetadata,
7
    IContentStorage,
8
    IContentUserDataStorage,
9
    IH5PConfig,
10
    IH5PPlayerOptions,
11
    IInstalledLibrary,
12
    IIntegration,
13
    ILibraryName,
14
    ILibraryStorage,
15
    IPlayerModel,
16
    IUrlGenerator,
17
    ILibraryMetadata,
18
    IUser,
19
    ITranslationFunction
20
} from './types';
21
import UrlGenerator from './UrlGenerator';
16✔
22
import Logger from './helpers/Logger';
16✔
23
import { ContentMetadata } from './ContentMetadata';
16✔
24

25
import defaultClientStrings from '../assets/defaultClientStrings.json';
16✔
26
import englishClientStrings from '../assets/translations/client/en.json';
16✔
27
import playerAssetList from './playerAssetList.json';
16✔
28
import player from './renderers/player';
16✔
29
import H5pError from './helpers/H5pError';
16✔
30
import LibraryManager from './LibraryManager';
16✔
31
import SemanticsLocalizer from './SemanticsLocalizer';
16✔
32
import SimpleTranslator from './helpers/SimpleTranslator';
16✔
33
import ContentUserDataManager from './ContentUserDataManager';
16✔
34
import ContentManager from './ContentManager';
16✔
35
import { LaissezFairePermissionSystem } from './implementation/LaissezFairePermissionSystem';
16✔
36

37
const log = new Logger('Player');
16✔
38

39
export default class H5PPlayer {
16✔
40
    /**
41
     *
42
     * @param libraryStorage the storage for libraries (can be read only)
43
     * @param contentStorage the storage for content (can be read only)
44
     * @param config the configuration object
45
     * @param integrationObjectDefaults (optional) the default values to use for
46
     * the integration object
47
     * @param urlGenerator creates url strings for files, can be used to
48
     * customize the paths in an implementation application
49
     * @param translationCallback a function that is called to retrieve
50
     * translations of keys in a certain language; the keys use the i18next
51
     * format (e.g. namespace:key). See the ITranslationFunction documentation
52
     * for more details.
53
     * @param options more options to customize the behavior of the player; see
54
     * IH5PPlayerOptions documentation for more details
55
     */
56
    constructor(
57
        private libraryStorage: ILibraryStorage,
50✔
58
        private contentStorage: IContentStorage,
50✔
59
        private config: IH5PConfig,
50✔
60
        private integrationObjectDefaults?: IIntegration,
50✔
61
        private urlGenerator: IUrlGenerator = new UrlGenerator(config),
50✔
62
        translationCallback: ITranslationFunction = new SimpleTranslator({
25✔
63
            // We use a simplistic translation function that is hard-wired to
64
            // English if the implementation does not pass us a proper one.
65
            client: englishClientStrings
66
        }).t,
67
        private options?: IH5PPlayerOptions,
50✔
68
        contentUserDataStorage?: IContentUserDataStorage
69
    ) {
70
        log.info('initialize');
50✔
71
        this.renderer = player;
50✔
72
        this.libraryManager = new LibraryManager(
50✔
73
            libraryStorage,
74
            urlGenerator.libraryFile,
75
            undefined,
76
            undefined,
77
            undefined,
78
            this.options?.lockProvider,
79
            this.config
80
        );
81

82
        const permissionSystem =
83
            options?.permissionSystem ?? new LaissezFairePermissionSystem();
50✔
84

85
        this.contentUserDataManager = new ContentUserDataManager(
50✔
86
            contentUserDataStorage,
87
            permissionSystem
88
        );
89

90
        this.contentManager = new ContentManager(
50✔
91
            contentStorage,
92
            permissionSystem,
93
            contentUserDataStorage
94
        );
95

96
        this.globalCustomScripts =
50✔
97
            this.options?.customization?.global?.scripts || [];
48✔
98
        if (this.config.customization?.global?.player?.scripts) {
50✔
99
            this.globalCustomScripts = this.globalCustomScripts.concat(
50✔
100
                this.config.customization.global.player.scripts
101
            );
102
        }
103

104
        this.globalCustomStyles =
50✔
105
            this.options?.customization?.global?.styles || [];
48✔
106
        if (this.config.customization?.global?.player?.styles) {
50✔
107
            this.globalCustomStyles = this.globalCustomStyles.concat(
50✔
108
                this.config.customization.global.player.styles
109
            );
110
        }
111

112
        this.semanticsLocalizer = new SemanticsLocalizer(translationCallback);
50✔
113
    }
114
    private semanticsLocalizer: SemanticsLocalizer;
115
    private globalCustomScripts: string[] = [];
50✔
116
    private globalCustomStyles: string[] = [];
50✔
117
    private libraryManager: LibraryManager;
118
    private contentManager: ContentManager;
119
    private contentUserDataManager: ContentUserDataManager;
120
    private renderer: (model: IPlayerModel) => string | any;
121

122
    /**
123
     * Creates a frame for displaying H5P content. You can customize this frame
124
     * by calling setRenderer(...). It normally is enough to call this method
125
     * with the content id. Only call it with parameters and metadata if don't
126
     * want to use the IContentStorage object passed into the constructor.
127
     * @param contentId the content id
128
     * @param actingUser the user who wants to access the content
129
     * @param options.ignoreUserPermission (optional) If set to true, the user
130
     * object won't be passed to the storage classes for permission checks. You
131
     * can use this option if you have already checked the user's permission in
132
     * a different layer.
133
     * @param options.parametersOverride (optional) the parameters of a piece of
134
     * content (=content.json); if you use this option, the parameters won't be
135
     * loaded from storage
136
     * @param options.metadataOverride (optional) the metadata of a piece of
137
     * content (=h5p.json); if you use this option, the parameters won't be
138
     * loaded from storage
139
     * @param options.contextId (optional) allows implementations to have
140
     * multiple content states for a single content object and user tuple
141
     * @param options.asUserId (optional) allows you to impersonate another
142
     * user. You will see their user state instead of yours.
143
     * @param options.readOnlyState (optional) allows you to disable saving of
144
        the user state. You will still see the state, but changes won't be
145
        persisted. This is useful if you want to review other users' states by
146
        setting `asUserId` and don't want to change their state. Note that the
147
        H5P doesn't support this behavior and we use a workaround to implement
148
        it. The workaround includes setting the query parameter `ignorePost=yes`
149
        in the URL of the content state Ajax call. The h5p-express adapter
150
        ignores posts that have this query parameter. You should, however, still
151
        prevent malicious users from writing other users' states in the
152
        permission system! 
153
     * @returns a HTML string that you can insert into your page
154
     */
155
    public async render(
156
        contentId: ContentId,
157
        actingUser: IUser,
158
        language: string = 'en',
×
159
        options?: {
160
            ignoreUserPermissions?: boolean;
161
            metadataOverride?: ContentMetadata;
162
            parametersOverride?: ContentParameters;
163
            showCopyButton?: boolean;
164
            showDownloadButton?: boolean;
165
            showEmbedButton?: boolean;
166
            showFrame?: boolean;
167
            showH5PIcon?: boolean;
168
            showLicenseButton?: boolean;
169
            contextId?: string;
170
            asUserId?: string; // the user for which the content state should be displayed;
171
            readOnlyState?: boolean;
172
        }
173
    ): Promise<string | any> {
174
        log.debug(`rendering page for ${contentId} in language ${language}`);
52✔
175
        if (options?.asUserId) {
52✔
176
            log.debug(`Personifying ${options.asUserId}`);
6✔
177
        }
178

179
        let parameters: ContentParameters;
180
        if (!options?.parametersOverride) {
52!
UNCOV
181
            try {
×
182
                parameters = await this.contentManager.getContentParameters(
×
183
                    contentId,
184
                    options?.ignoreUserPermissions ? undefined : actingUser
×
185
                );
186
            } catch (error) {
187
                throw new H5pError('h5p-player:content-missing', {}, 404);
×
188
            }
189
        } else {
190
            parameters = options.parametersOverride;
52✔
191
        }
192

193
        let metadata: ContentMetadata;
194
        if (!options?.metadataOverride) {
52!
UNCOV
195
            try {
×
196
                metadata = await this.contentManager.getContentMetadata(
×
197
                    contentId,
198
                    options?.ignoreUserPermissions ? undefined : actingUser
×
199
                );
200
            } catch (error) {
201
                throw new H5pError('h5p-player:content-missing', {}, 404);
×
202
            }
203
        } else {
204
            metadata = options.metadataOverride;
52✔
205
        }
206

207
        log.debug('Getting list of installed addons.');
52✔
208
        let installedAddons: ILibraryMetadata[] = [];
52✔
209
        if (this.libraryStorage?.listAddons) {
52!
210
            installedAddons = await this.libraryStorage.listAddons();
×
211
        }
212
        // We remove duplicates from the dependency list by converting it to
213
        // a set and then back.
214
        const dependencies = Array.from(
52✔
215
            new Set(
216
                (metadata.preloadedDependencies || [])
43✔
217
                    .concat(
218
                        await this.getAddonsByParameters(
219
                            parameters,
220
                            installedAddons
221
                        )
222
                    )
223
                    .concat(
224
                        await this.getAddonsByLibrary(
225
                            metadata.mainLibrary,
226
                            installedAddons
227
                        )
228
                    )
229
            )
230
        );
231

232
        // Getting lists of scripts and styles needed for the main library.
233
        const libraries = await this.getMetadataRecursive(dependencies);
52✔
234
        const assets = this.aggregateAssetsRecursive(dependencies, libraries);
52✔
235

236
        const mainLibrarySupportsFullscreen = !metadata.mainLibrary
52✔
237
            ? false
238
            : libraries[
239
                  LibraryName.toUberName(
240
                      metadata.preloadedDependencies.find(
241
                          (dep) => dep.machineName === metadata.mainLibrary
20✔
242
                      )
243
                  )
244
              ].fullscreen === 1;
245

246
        const model: IPlayerModel = {
52✔
247
            contentId,
248
            dependencies,
249
            downloadPath: this.getDownloadPath(contentId),
250
            integration: await this.generateIntegration(
251
                contentId,
252
                parameters,
253
                metadata,
254
                assets,
255
                mainLibrarySupportsFullscreen,
256
                actingUser,
257
                language,
258
                {
259
                    showCopyButton: options?.showCopyButton ?? false,
52✔
260
                    showDownloadButton: options?.showDownloadButton ?? false,
52✔
261
                    showEmbedButton: options?.showEmbedButton ?? false,
52✔
262
                    showFrame: options?.showFrame ?? false,
52✔
263
                    showH5PIcon: options?.showH5PIcon ?? false,
52✔
264
                    showLicenseButton: options?.showLicenseButton ?? false
52✔
265
                },
266
                options?.contextId,
267
                options?.asUserId,
268
                options?.readOnlyState
269
            ),
270
            scripts: this.listCoreScripts().concat(assets.scripts),
271
            styles: this.listCoreStyles().concat(assets.styles),
272
            translations: {},
273
            embedTypes: metadata.embedTypes, // TODO: check if the library supports the embed type!
274
            user: actingUser
275
        };
276

277
        return this.renderer(model);
50✔
278
    }
279

280
    /**
281
     * Overrides the default renderer.
282
     * @param renderer
283
     */
284
    public setRenderer(
285
        renderer: (model: IPlayerModel) => string | any
286
    ): H5PPlayer {
287
        log.info('changing renderer');
46✔
288
        this.renderer = renderer;
46✔
289
        return this;
46✔
290
    }
291

292
    /**
293
     *
294
     * @param dependencies
295
     * @param libraries
296
     * @param assets
297
     * @param loaded
298
     * @returns aggregated asset lists
299
     */
300
    private aggregateAssetsRecursive(
301
        dependencies: ILibraryName[],
302
        libraries: { [ubername: string]: IInstalledLibrary },
303
        assets: IAssets = { scripts: [], styles: [], translations: {} },
26✔
304
        loaded: { [ubername: string]: boolean } = {}
26✔
305
    ): IAssets {
306
        log.verbose(
92✔
307
            `loading assets from dependencies: ${dependencies
308
                .map((dep) => LibraryName.toUberName(dep))
44✔
309
                .join(', ')}`
310
        );
311
        dependencies.forEach((dependency) => {
92✔
312
            const key = LibraryName.toUberName(dependency);
44✔
313
            if (key in loaded) return;
44✔
314

315
            loaded[key] = true;
40✔
316
            const lib = libraries[key];
40✔
317
            if (lib) {
40✔
318
                this.aggregateAssetsRecursive(
40✔
319
                    lib.preloadedDependencies || [],
32✔
320
                    libraries,
321
                    assets,
322
                    loaded
323
                );
324
                let cssFiles: string[] =
325
                    lib.preloadedCss?.map((f) => f.path) || [];
44✔
326
                let jsFiles: string[] =
327
                    lib.preloadedJs?.map((f) => f.path) || [];
44✔
328

329
                // If configured in the options, we call a hook to change the files
330
                // included for certain libraries.
331
                if (this.options?.customization?.alterLibraryFiles) {
40✔
332
                    log.debug('Calling alterLibraryFiles hook');
4✔
333
                    const alteredFiles =
334
                        this.options.customization.alterLibraryFiles(
4✔
335
                            lib,
336
                            jsFiles,
337
                            cssFiles
338
                        );
339
                    jsFiles = alteredFiles?.scripts;
4✔
340
                    cssFiles = alteredFiles?.styles;
4✔
341
                }
342
                (cssFiles || []).forEach((style) =>
40!
343
                    assets.styles.push(
44✔
344
                        this.urlGenerator.libraryFile(lib, style)
345
                    )
346
                );
347
                (jsFiles || []).forEach((script) =>
40!
348
                    assets.scripts.push(
50✔
349
                        this.urlGenerator.libraryFile(lib, script)
350
                    )
351
                );
352
            }
353
        });
354
        return assets;
92✔
355
    }
356

357
    /**
358
     * Scans the parameters for occurances of the regex pattern in any string
359
     * property.
360
     * @param parameters the parameters (= content.json)
361
     * @param regex the regex to look for
362
     * @returns true if the regex occurs in a string inside the parametres
363
     */
364
    private checkIfRegexIsInParameters(
365
        parameters: any,
366
        regex: RegExp
367
    ): boolean {
368
        const type = typeof parameters;
×
369
        if (type === 'string') {
×
370
            if (regex.test(parameters)) {
×
371
                return true;
×
372
            }
373
        } else if (type === 'object') {
×
374
            // eslint-disable-next-line guard-for-in
375
            for (const property in parameters) {
×
376
                const found = this.checkIfRegexIsInParameters(
×
377
                    parameters[property],
378
                    regex
379
                );
380
                if (found) {
×
381
                    return true;
×
382
                }
383
            }
384
        }
385
        return false;
×
386
    }
387

388
    private async generateIntegration(
389
        contentId: ContentId,
390
        parameters: ContentParameters,
391
        metadata: IContentMetadata,
392
        assets: IAssets,
393
        supportsFullscreen: boolean,
394
        actingUser: IUser,
395
        language: string,
396
        displayOptions: {
397
            showCopyButton: boolean;
398
            showDownloadButton: boolean;
399
            showEmbedButton: boolean;
400
            showFrame: boolean;
401
            showH5PIcon: boolean;
402
            showLicenseButton: boolean;
403
        },
404
        contextId: string,
405
        asUserId?: string,
406
        readOnlyState?: boolean
407
    ): Promise<IIntegration> {
408
        // see https://h5p.org/creating-your-own-h5p-plugin
409
        log.info(`generating integration for ${contentId}`);
52✔
410

411
        return {
52✔
412
            ajax: {
413
                contentUserData: this.urlGenerator.contentUserData(
414
                    actingUser,
415
                    contextId,
416
                    asUserId,
417
                    { readonly: readOnlyState }
418
                ),
419
                setFinished: this.urlGenerator.setFinished(actingUser)
420
            },
421
            ajaxPath: this.urlGenerator.ajaxEndpoint(actingUser),
422
            contents: {
423
                [`cid-${contentId}`]: {
424
                    displayOptions: {
425
                        copy: displayOptions.showCopyButton,
426
                        copyright: displayOptions.showLicenseButton,
427
                        embed: displayOptions.showEmbedButton,
428
                        export: displayOptions.showDownloadButton,
429
                        frame: displayOptions.showFrame,
430
                        icon: displayOptions.showH5PIcon
431
                    },
432
                    fullScreen: supportsFullscreen ? '1' : '0',
26!
433
                    jsonContent: JSON.stringify(parameters),
434
                    library: ContentMetadata.toUbername(metadata),
435
                    contentUrl: this.urlGenerator.contentFilesUrl(contentId),
436
                    contentUserData:
437
                        await this.contentUserDataManager.generateContentUserDataIntegration(
438
                            contentId,
439
                            actingUser,
440
                            contextId,
441
                            asUserId
442
                        ),
443
                    metadata: {
444
                        license: metadata.license || 'U',
50✔
445
                        title: metadata.title || '',
50✔
446
                        defaultLanguage: metadata.language || 'en',
50✔
447
                        authors: metadata.authors,
448
                        changes: metadata.changes,
449
                        contentType: metadata.contentType,
450
                        licenseExtras: metadata.licenseExtras,
451
                        a11yTitle: metadata.a11yTitle,
452
                        authorComments: metadata.authorComments,
453
                        licenseVersion: metadata.licenseVersion,
454
                        source: metadata.source,
455
                        yearFrom: metadata.yearFrom,
456
                        yearTo: metadata.yearTo
457
                    },
458
                    scripts: assets.scripts,
459
                    styles: assets.styles,
460
                    url: this.urlGenerator.uniqueContentUrl(contentId),
461
                    exportUrl: this.urlGenerator.downloadPackage(contentId)
462
                }
463
            },
464
            core: {
465
                scripts: this.listCoreScripts(),
466
                styles: this.listCoreStyles()
467
            },
468
            l10n: {
469
                H5P: this.semanticsLocalizer.localize(
470
                    defaultClientStrings,
471
                    language,
472
                    true
473
                )
474
            },
475
            libraryConfig: this.config.libraryConfig,
476
            postUserStatistics: this.config.setFinishedEnabled,
477
            saveFreq: this.getSaveFreq(readOnlyState),
478
            url: this.urlGenerator.baseUrl(),
479
            hubIsEnabled: true,
480
            fullscreenDisabled: this.config.disableFullscreen ? 1 : 0,
25!
481
            ...this.integrationObjectDefaults,
482
            user: {
483
                name: actingUser.name,
484
                mail: actingUser.email,
485
                id: actingUser.id
486
            }
487
        };
488
    }
489

490
    private getSaveFreq(readOnlyState: boolean): number | boolean {
491
        if (readOnlyState) {
50!
492
            return Number.MAX_SAFE_INTEGER;
×
493
        }
494
        if (this.config.contentUserStateSaveInterval !== false) {
50✔
495
            return (
50✔
496
                Math.round(
25!
497
                    Number(this.config.contentUserStateSaveInterval) / 1000
498
                ) || 1
499
            );
500
        }
501
        return false;
×
502
    }
503

504
    /**
505
     * Finds out which adds should be added to the library due to the settings
506
     * in the global configuration or in the library metadata.
507
     * @param machineName the machine name of the library to which addons should
508
     * be added
509
     * @param installedAddons a list of installed addons on the system
510
     * @returns the list of addons to install
511
     */
512
    private async getAddonsByLibrary(
513
        machineName: string,
514
        installedAddons: ILibraryMetadata[]
515
    ): Promise<ILibraryMetadata[]> {
516
        const neededAddons: ILibraryMetadata[] = [];
52✔
517
        // add addons that are required by the H5P library metadata extension
518
        for (const installedAddon of installedAddons) {
52✔
519
            // The property addTo.player.machineNames is a custom
520
            // h5p-nodejs-library extension.
521
            if (
×
522
                installedAddon.addTo?.player?.machineNames?.includes(
×
523
                    machineName
524
                ) ||
525
                installedAddon.addTo?.player?.machineNames?.includes('*')
526
            ) {
527
                log.debug(
×
528
                    `Addon ${LibraryName.toUberName(
529
                        installedAddon
530
                    )} will be added to the player.`
531
                );
532
                neededAddons.push(installedAddon);
×
533
            }
534
        }
535

536
        // add addons that are required by the server configuration
537
        const configRequestedAddons = [
52✔
538
            ...(this.config.playerAddons?.[machineName] ?? []),
52✔
539
            ...(this.config.playerAddons?.['*'] ?? [])
52✔
540
        ];
541
        for (const addonMachineName of configRequestedAddons) {
52✔
542
            const installedAddonVersions =
543
                await this.libraryManager.listInstalledLibraries(
×
544
                    addonMachineName
545
                );
546
            if (
×
547
                !neededAddons
×
548
                    .map((a) => a.machineName)
×
549
                    .includes(addonMachineName) &&
550
                installedAddonVersions[addonMachineName] !== undefined
551
            ) {
552
                log.debug(
×
553
                    `Addon ${addonMachineName} will be added to the player.`
554
                );
555

556
                neededAddons.push(
×
557
                    installedAddonVersions[addonMachineName].sort()[
558
                        installedAddonVersions[addonMachineName].length - 1
559
                    ]
560
                );
561
            }
562
        }
563

564
        return neededAddons;
52✔
565
    }
566

567
    /**
568
     * Determines which addons should be used for the parameters. It will scan
569
     * the parameters for patterns specified by installed addons.
570
     * @param parameters the parameters to scan
571
     * @param installedAddons a list of addons installed on the system
572
     * @returns a list of addons that should be used
573
     */
574
    private async getAddonsByParameters(
575
        parameters: any,
576
        installedAddons: ILibraryMetadata[]
577
    ): Promise<ILibraryMetadata[]> {
578
        log.debug('Checking which of the addons must be used for the content.');
52✔
579
        const addonsToAdd: { [key: string]: ILibraryMetadata } = {};
52✔
580
        for (const installedAddon of installedAddons) {
52✔
581
            if (!installedAddon.addTo?.content?.types) {
×
582
                continue;
×
583
            }
584

585
            for (const types of installedAddon.addTo.content.types) {
×
586
                if (types.text) {
×
587
                    // The regex pattern in the metadata is specified like this:
588
                    // /mypattern/ or /mypattern/g
589
                    // Because of this we must extract the actual pattern and
590
                    // the flags and pass them to the constructor of RegExp.
591
                    const matches = /^\/(.+?)\/([gimy]+)?$/.exec(
×
592
                        types.text.regex
593
                    );
594
                    if (matches.length < 1) {
×
595
                        log.error(
×
596
                            `The addon ${LibraryName.toUberName(
597
                                installedAddon
598
                            )} contains an invalid regexp pattern in the addTo selector: ${
599
                                types.text.regex
600
                            }. This will be silently ignored, but the addon will never be used!`
601
                        );
602
                        continue;
×
603
                    }
604

605
                    if (
×
606
                        this.checkIfRegexIsInParameters(
607
                            parameters,
608
                            new RegExp(matches[1], matches[2])
609
                        )
610
                    ) {
611
                        log.debug(
×
612
                            `Adding addon ${LibraryName.toUberName(
613
                                installedAddon
614
                            )} to dependencies as the regexp pattern ${
615
                                types.text.regex
616
                            } was found in the parameters.`
617
                        );
618
                        addonsToAdd[installedAddon.machineName] =
×
619
                            installedAddon;
620
                    }
621
                }
622
            }
623
        }
624
        return Object.values(addonsToAdd);
52✔
625
    }
626

627
    private getDownloadPath(contentId: ContentId): string {
628
        return this.urlGenerator.downloadPackage(contentId);
52✔
629
    }
630

631
    private async getMetadata(
632
        machineName: string,
633
        majorVersion: number,
634
        minorVersion: number
635
    ): Promise<IInstalledLibrary> {
636
        log.verbose(
44✔
637
            `loading library ${machineName}-${majorVersion}.${minorVersion}`
638
        );
639
        return this.libraryStorage.getLibrary(
44✔
640
            new LibraryName(machineName, majorVersion, minorVersion)
641
        );
642
    }
643

644
    /**
645
     *
646
     * @param dependencies
647
     * @param loaded can be left out in initial call
648
     */
649
    private async getMetadataRecursive(
650
        dependencies: ILibraryName[],
651
        loaded: { [ubername: string]: IInstalledLibrary } = {}
26✔
652
    ): Promise<{ [ubername: string]: IInstalledLibrary }> {
653
        log.verbose(
96✔
654
            `loading libraries from dependencies: ${dependencies
655
                .map((dep) => LibraryName.toUberName(dep))
44✔
656
                .join(', ')}`
657
        );
658
        await Promise.all(
96✔
659
            dependencies.map(async (dependency) => {
660
                const name = dependency.machineName;
44✔
661
                const majVer = dependency.majorVersion;
44✔
662
                const minVer = dependency.minorVersion;
44✔
663

664
                const key = LibraryName.toUberName(dependency);
44✔
665
                if (key in loaded) {
44!
666
                    return;
×
667
                }
668
                let lib;
669
                try {
44✔
670
                    lib = await this.getMetadata(name, majVer, minVer);
44✔
671
                } catch {
672
                    log.info(
×
673
                        `Could not find library ${name}-${majVer}.${minVer} in storage. Silently ignoring...`
674
                    );
675
                }
676
                if (lib) {
44✔
677
                    loaded[key] = lib;
44✔
678
                    await this.getMetadataRecursive(
44✔
679
                        lib.preloadedDependencies || [],
36✔
680
                        loaded
681
                    );
682
                }
683
            })
684
        );
685
        return loaded;
96✔
686
    }
687

688
    private listCoreScripts(): string[] {
689
        return playerAssetList.scripts.core
100✔
690
            .map(this.urlGenerator.coreFile)
691
            .concat(this.globalCustomScripts);
692
    }
693

694
    private listCoreStyles(): string[] {
695
        return playerAssetList.styles.core
100✔
696
            .map(this.urlGenerator.coreFile)
697
            .concat(this.globalCustomStyles);
698
    }
699
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc