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

electrode-io / electrode-native / 7563

08 Apr 2026 06:30AM UTC coverage: 67.856% (+7.8%) from 60.065%
7563

push

Azure Pipelines

r0h0gg6
Update lerna publish to avoid npm classic token deprec

4529 of 7762 branches covered (58.35%)

Branch coverage included in aggregate %.

11084 of 15247 relevant lines covered (72.7%)

666.52 hits per line

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

75.65
/ern-container-gen-android/src/AndroidGenerator.ts
1
import {
90✔
2
  android,
3
  AndroidResolvedVersions,
4
  BundlingResult,
5
  createTmpDir,
6
  gitApply,
7
  handleCopyDirective,
8
  HermesCli,
9
  injectReactNativeVersionKeysInObject,
10
  kax,
11
  log,
12
  manifest,
13
  mustacheUtils,
14
  NativePlatform,
15
  PackagePath,
16
  PluginConfig,
17
  readPackageJson,
18
  shell,
19
  utils as coreUtils,
20
  yarn,
21
} from 'ern-core';
22
import {
90✔
23
  ContainerGenerator,
24
  ContainerGeneratorConfig,
25
  ContainerGenResult,
26
  generateContainer,
27
  generatePluginsMustacheViews,
28
  populateApiImplMustacheView,
29
} from 'ern-container-gen';
30

31
import glob from 'glob';
90✔
32
import _ from 'lodash';
90✔
33
import path from 'path';
90✔
34
import fs from 'fs-extra';
90✔
35
import readDir from 'fs-readdir-recursive';
90✔
36
import semver from 'semver';
90✔
37

38
// tslint:disable-next-line:no-var-requires
39
const AdmZip = require('adm-zip');
90✔
40

41
const PATH_TO_TEMPLATES_DIR = path.join(__dirname, 'templates');
90✔
42
const PATH_TO_HULL_DIR = path.join(__dirname, 'hull');
90✔
43
const ERN_CUSTOM_REACT_NATIVE_AAR_PATCH_VERSION = 100;
90✔
44

45
export interface AndroidDependencies {
46
  files: string[];
47
  transitive: string[];
48
  raw: string[];
49
  regular: string[];
50
  annotationProcessor: string[];
51
}
52

53
export enum JavaScriptEngine {
90✔
54
  HERMES,
90✔
55
  JSC,
90✔
56
}
57

58
export default class AndroidGenerator implements ContainerGenerator {
90✔
59
  get name(): string {
60
    return 'AndroidGenerator';
×
61
  }
62

63
  get platform(): NativePlatform {
64
    return 'android';
×
65
  }
66

67
  public async generate(
68
    config: ContainerGeneratorConfig,
69
  ): Promise<ContainerGenResult> {
70
    return generateContainer(config, {
7✔
71
      fillContainerHull: this.fillContainerHull.bind(this),
72
    });
73
  }
74

75
  public async doesDirectoryContainsKotlinSourceFiles(
76
    dir: string,
77
  ): Promise<boolean> {
78
    return new Promise((resolve, reject) => {
20✔
79
      glob(path.join(dir, '**/*.kt'), (err, files) => {
20✔
80
        if (err) {
20!
81
          reject(err);
×
82
        } else {
83
          resolve(files?.length > 0);
20!
84
        }
85
      });
86
    });
87
  }
88

89
  public async fillContainerHull(
90
    config: ContainerGeneratorConfig,
91
  ): Promise<void> {
92
    const copyFromPath = path.join(PATH_TO_HULL_DIR, '{.*,*}');
7✔
93

94
    shell.cp('-R', copyFromPath, config.outDir);
7✔
95

96
    // https://github.com/npm/npm/issues/1862 npm renames .gitignore to .npmignore causing the generated container to emit the .gitignore file. This solution below helps to bypass it.
97
    shell.mv(`${config.outDir}/gitignore`, `${config.outDir}/.gitignore`);
7✔
98

99
    const reactNativePlugin = _.find(
7✔
100
      config.plugins,
101
      (p) => p.name === 'react-native',
7✔
102
    );
103

104
    if (!reactNativePlugin) {
7!
105
      throw new Error('react-native was not found in plugins list !');
×
106
    }
107
    if (!reactNativePlugin.version) {
7!
108
      throw new Error('react-native plugin does not have a version !');
×
109
    }
110

111
    let mustacheView: any = {
7✔
112
      customFeatures: [],
113
      customPermissions: [],
114
      customRepos: [],
115
      permissions: [],
116
    };
117
    injectReactNativeVersionKeysInObject(
7✔
118
      mustacheView,
119
      reactNativePlugin.version,
120
    );
121

122
    const electrodeBridgePlugin = _.find(
7✔
123
      config.plugins,
124
      (p) => p.name === 'react-native-electrode-bridge',
16✔
125
    );
126

127
    if (electrodeBridgePlugin) {
7✔
128
      mustacheView.hasElectrodeBridgePlugin = true;
6✔
129
    }
130

131
    mustacheView.miniApps = await config.composite.getMiniApps();
7✔
132
    mustacheView.jsMainModuleName = config.jsMainModuleName || 'index';
7✔
133

134
    await kax
7✔
135
      .task('Preparing Native Dependencies Injection')
136
      .run(this.buildAndroidPluginsViews(config.plugins, mustacheView));
137

138
    await kax
7✔
139
      .task('Adding Native Dependencies Hooks')
140
      .run(this.addAndroidPluginHookClasses(config.plugins, config.outDir));
141

142
    kax.task('Setting Android tools and libraries versions').succeed();
7✔
143
    const versions = android.resolveAndroidVersions({
7✔
144
      reactNativeVersion: reactNativePlugin.version,
145
      ...config.androidConfig,
146
    });
147
    mustacheView = Object.assign(mustacheView, versions);
7✔
148

149
    const injectPluginsTaskMsg = 'Injecting Native Dependencies';
7✔
150
    const injectPluginsKaxTask = kax.task(injectPluginsTaskMsg);
7✔
151

152
    const replacements: (() => void)[] = [];
7✔
153
    const androidDependencies: AndroidDependencies = {
7✔
154
      annotationProcessor: [],
155
      files: [],
156
      raw: [],
157
      regular: [],
158
      transitive: [],
159
    };
160

161
    let isKotlinEnabled = false;
7✔
162

163
    for (const plugin of config.plugins) {
7✔
164
      if (plugin.name === 'react-native') {
27✔
165
        continue;
7✔
166
      }
167

168
      let pluginConfig: PluginConfig<'android'> | undefined =
169
        await manifest.getPluginConfig(plugin, 'android');
20✔
170
      if (!pluginConfig) {
20!
171
        log.warn(
×
172
          `Skipping ${plugin.name} as it does not have an Android configuration`,
173
        );
174
        continue;
×
175
      }
176

177
      injectPluginsKaxTask.text = `${injectPluginsTaskMsg} [${plugin.name}]`;
20✔
178

179
      let pathToPluginProject;
180

181
      const pluginSourcePath = plugin.basePath;
20✔
182
      if (await coreUtils.isDependencyPathNativeApiImpl(pluginSourcePath)) {
20✔
183
        // For native api implementations, if a 'ern.pluginConfig' object
184
        // exists in its package.json, replace pluginConfig with this one.
185
        const pluginPackageJson = await readPackageJson(pluginSourcePath);
2✔
186
        if (pluginPackageJson.ern.pluginConfig) {
2✔
187
          pluginConfig = pluginPackageJson.ern.pluginConfig.android;
1✔
188
        }
189
        populateApiImplMustacheView(pluginSourcePath, mustacheView, true);
2✔
190
      }
191
      pathToPluginProject = path.join(pluginSourcePath, pluginConfig!.root);
20✔
192

193
      if (!isKotlinEnabled) {
20!
194
        isKotlinEnabled = await this.doesDirectoryContainsKotlinSourceFiles(
20✔
195
          pathToPluginProject,
196
        );
197
      }
198

199
      shell.pushd(pathToPluginProject);
20✔
200
      try {
20✔
201
        if (await coreUtils.isDependencyPathNativeApiImpl(pluginSourcePath)) {
20✔
202
          // Special handling for native api implementation as we don't
203
          // want to copy the API and bridge code (part of native api implementations projects)
204
          const relPathToApiImplSource = path.normalize(
2✔
205
            'lib/src/main/java/com/ern',
206
          );
207
          const absPathToCopyPluginSourceTo = path.join(
2✔
208
            config.outDir,
209
            'lib/src/main/java/com',
210
          );
211
          shell.cp('-R', relPathToApiImplSource, absPathToCopyPluginSourceTo);
2✔
212
        } else {
213
          const relPathToPluginSource = pluginConfig!.moduleName
18✔
214
            ? path.join(pluginConfig!.moduleName, 'src/main/java')
18✔
215
            : path.join('src/main/java');
216
          const absPathToCopyPluginSourceTo = path.join(
18✔
217
            config.outDir,
218
            'lib/src/main',
219
          );
220

221
          if (semver.gte(reactNativePlugin.version, '0.60.0')) {
18!
222
            const convertedFiles = this.convertToAndroidX(
18✔
223
              relPathToPluginSource,
224
            );
225
            if (convertedFiles > 0) {
18✔
226
              log.info(
15✔
227
                `${plugin.name} contains source files with references to the Android Support Library (android.support.*)`,
228
              );
229
              log.info(
15✔
230
                `${convertedFiles} files successfully converted to use AndroidX (androidx.*)`,
231
              );
232
            }
233
          }
234

235
          shell.cp('-R', relPathToPluginSource, absPathToCopyPluginSourceTo);
18✔
236
        }
237

238
        const {
239
          applyPatch,
240
          copy,
241
          dependencies,
242
          features,
243
          permissions,
244
          replaceInFile,
245
          repositories,
246
        } = pluginConfig!;
20✔
247

248
        if (copy) {
20!
249
          handleCopyDirective(pluginSourcePath, config.outDir, copy);
×
250
        }
251

252
        if (replaceInFile && Array.isArray(replaceInFile)) {
20!
253
          for (const r of replaceInFile) {
×
254
            replacements.push(() => {
×
255
              log.debug(`Performing string replacement on ${r.path}`);
×
256
              const pathToFile = path.join(config.outDir, r.path);
×
257
              const fileContent = fs.readFileSync(pathToFile, 'utf8');
×
258
              const patchedFileContent = fileContent.replace(
×
259
                RegExp(r.string, 'g'),
260
                r.replaceWith,
261
              );
262
              fs.writeFileSync(pathToFile, patchedFileContent, {
×
263
                encoding: 'utf8',
264
              });
265
            });
266
          }
267
        }
268

269
        if (applyPatch) {
20!
270
          const { patch, root } = applyPatch;
×
271
          if (!patch) {
×
272
            throw new Error('Missing "patch" property in "applyPatch" object');
×
273
          }
274
          if (!root) {
×
275
            throw new Error('Missing "root" property in "applyPatch" object');
×
276
          }
277
          const [patchFile, rootDir] = [
×
278
            path.join(pluginConfig!.path!, patch),
279
            path.join(config.outDir, root),
280
          ];
281
          await gitApply({ patchFile, rootDir });
×
282
        }
283

284
        if (dependencies) {
20✔
285
          const transitivePrefix = 'transitive:';
2✔
286
          const filesPrefix = 'files';
2✔
287
          const annotationProcessorPrefix = 'annotationProcessor:';
2✔
288
          for (const dependency of dependencies) {
2✔
289
            if (dependency.startsWith(transitivePrefix)) {
4!
290
              androidDependencies.transitive.push(
×
291
                dependency.replace(transitivePrefix, ''),
292
              );
293
              log.warn(
×
294
                `Deprecation warning for ${plugin.name} manifest configuration.
295
${transitivePrefix} dependency prefix has been deprecated and will be removed in a future release.
296
You should replace "${transitivePrefix}:${dependency}" with "implementation ('${dependency}') { transitive = true }"`,
297
              );
298
            } else if (dependency.startsWith(filesPrefix)) {
4!
299
              androidDependencies.files.push(dependency);
×
300
              log.warn(
×
301
                `Deprecation warning for ${plugin.name} manifest configuration.
302
${filesPrefix} dependency prefix has been deprecated and will be removed in a future release.
303
You should replace "${dependency}" with "implementation ${dependency}"`,
304
              );
305
            } else if (dependency.startsWith(annotationProcessorPrefix)) {
4!
306
              androidDependencies.annotationProcessor.push(
×
307
                dependency.replace(annotationProcessorPrefix, ''),
308
              );
309
              log.warn(
×
310
                `Deprecation warning for ${plugin.name} manifest configuration.
311
${annotationProcessorPrefix} dependency prefix has been deprecated and will be removed in a future release.
312
You should replace "${annotationProcessorPrefix}:${dependency}" with "annotationProcessor '${dependency}'"`,
313
              );
314
            } else if (/^[^:\s'(]+:[^:]+:[^:]+$/.test(dependency)) {
4!
315
              androidDependencies.regular.push(dependency);
4✔
316
            } else {
317
              androidDependencies.raw.push(dependency);
×
318
            }
319
          }
320
        }
321

322
        if (repositories) {
20!
323
          mustacheView.customRepos.push(...repositories);
×
324
        }
325

326
        if (permissions) {
20!
327
          mustacheView.customPermissions.push(...permissions);
×
328
        }
329

330
        if (features) {
20!
331
          mustacheView.customFeatures.push(...features);
×
332
        }
333
      } finally {
334
        shell.popd();
20✔
335
      }
336
    }
337

338
    const resPath = path.join(config.outDir, 'lib/src/main/res');
7✔
339
    const resSrcDirs = fs
7✔
340
      .readdirSync(resPath)
341
      .filter((f) => fs.statSync(path.join(resPath, f)).isDirectory())
21✔
342
      .map((d) => `'src/main/res/${d}'`)
21✔
343
      .join(',');
344
    mustacheView.resSrcDirs = resSrcDirs;
7✔
345

346
    mustacheView.isKotlinEnabled = isKotlinEnabled;
7✔
347

348
    // Dedupe repositories and permissions
349
    mustacheView.customRepos = _.uniq(mustacheView.customRepos);
7✔
350
    mustacheView.customPermissions = _.uniq(mustacheView.customPermissions);
7✔
351

352
    androidDependencies.raw.push(
7✔
353
      `api 'com.walmartlabs.ern:react-android:${versions.reactNativeAarVersion}'`,
354
    );
355

356
    if (isKotlinEnabled) {
7✔
357
      androidDependencies.regular.push(
1✔
358
        `org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlinVersion}`,
359
      );
360
    }
361
    mustacheView.implementations = this.buildImplementationStatements(
7✔
362
      androidDependencies,
363
      versions,
364
    );
365

366
    log.debug(
7✔
367
      `Implementation statements to be injected: ${JSON.stringify(
368
        mustacheView.implementations,
369
      )}`,
370
    );
371

372
    if (
7!
373
      semver.patch(versions.reactNativeAarVersion) >=
374
      ERN_CUSTOM_REACT_NATIVE_AAR_PATCH_VERSION
375
    ) {
376
      mustacheView.isCustomReactNativeAar = true;
×
377
    }
378

379
    injectPluginsKaxTask.succeed(injectPluginsTaskMsg);
7✔
380

381
    const partialProxy = (name: string) => {
7✔
382
      return fs.readFileSync(
38✔
383
        path.join(PATH_TO_TEMPLATES_DIR, `${name}.mustache`),
384
        'utf8',
385
      );
386
    };
387

388
    log.debug('Patching hull');
7✔
389
    const files = readDir(
7✔
390
      config.outDir,
391
      (f) =>
392
        !f.endsWith('.jar') &&
787✔
393
        !f.endsWith('.aar') &&
394
        !f.endsWith('.git') &&
395
        f !== '.gradle' &&
396
        f !== 'build',
397
    );
398
    const pathLibSrcMain = path.normalize('lib/src/main');
7✔
399
    const pathLibSrcMainJniLibs = path.normalize('lib/src/main/jniLibs');
7✔
400
    const pathLibSrcMainAssets = path.normalize('lib/src/main/assets');
7✔
401
    const pathLibSrcMainJavaCom = path.join(pathLibSrcMain, 'java/com');
7✔
402
    const pathLibSrcMainRes = path.join(pathLibSrcMain, 'res');
7✔
403
    const pathLibSrcMainJavaComWalmartlabsErnContainer = path.join(
7✔
404
      pathLibSrcMainJavaCom,
405
      'walmartlabs/ern/container',
406
    );
407
    for (const file of files) {
7✔
408
      if (
533✔
409
        (file.startsWith(pathLibSrcMainJavaCom) &&
1,479✔
410
          !file.startsWith(pathLibSrcMainJavaComWalmartlabsErnContainer)) ||
411
        file.startsWith(pathLibSrcMainAssets) ||
412
        file.startsWith(pathLibSrcMainJniLibs) ||
413
        file.startsWith(pathLibSrcMainRes)
414
      ) {
415
        // We don't want to Mustache process library files. It can lead to bad things
416
        // We also don't want to process assets files ...
417
        // We just want to process container specific code (which contains mustache templates)
418
        continue;
393✔
419
      }
420
      log.debug(`Mustaching ${file}`);
140✔
421
      const pathToFile = path.join(config.outDir, file);
140✔
422
      await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile(
140✔
423
        pathToFile,
424
        mustacheView,
425
        pathToFile,
426
        partialProxy,
427
      );
428
    }
429

430
    log.debug('Creating miniapp activities');
7✔
431
    const compositeMiniApps = await config.composite.getMiniApps();
7✔
432
    for (const miniApp of compositeMiniApps) {
7✔
433
      const activityFileName = `${miniApp.pascalCaseName}Activity.java`;
10✔
434

435
      log.debug(`Creating ${activityFileName}`);
10✔
436
      const pathToMiniAppActivityMustacheTemplate = path.join(
10✔
437
        PATH_TO_TEMPLATES_DIR,
438
        'MiniAppActivity.mustache',
439
      );
440
      const pathToOutputActivityFile = path.join(
10✔
441
        config.outDir,
442
        pathLibSrcMainJavaComWalmartlabsErnContainer,
443
        'miniapps',
444
        activityFileName,
445
      );
446
      await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile(
10✔
447
        pathToMiniAppActivityMustacheTemplate,
448
        miniApp,
449
        pathToOutputActivityFile,
450
        partialProxy,
451
      );
452
    }
453

454
    for (const perform of replacements) {
7✔
455
      perform();
×
456
    }
457
  }
458

459
  public getJavaScriptEngine(
460
    config: ContainerGeneratorConfig,
461
  ): JavaScriptEngine {
462
    return config.androidConfig
×
463
      ? config.androidConfig.jsEngine === 'jsc'
×
464
        ? JavaScriptEngine.JSC
×
465
        : config.androidConfig.jsEngine === 'hermes'
466
        ? JavaScriptEngine.HERMES
×
467
        : JavaScriptEngine.JSC
468
      : JavaScriptEngine.JSC;
469
  }
470

471
  public buildImplementationStatements(
472
    dependencies: AndroidDependencies,
473
    androidVersions: AndroidResolvedVersions,
474
  ) {
475
    const result: any[] = [];
7✔
476

477
    // Replace versions of support libraries with set version
478
    dependencies.regular = dependencies.regular.map((d) =>
7✔
479
      d.startsWith('androidx.appcompat:')
5✔
480
        ? `${d.slice(0, d.lastIndexOf(':'))}:${
5✔
481
            androidVersions.androidxAppcompactVersion
482
          }`
483
        : d,
484
    );
485

486
    // Dedupe dependencies with same version
487
    dependencies.regular = _.uniq(dependencies.regular);
7✔
488
    dependencies.files = _.uniq(dependencies.files);
7✔
489
    dependencies.raw = _.uniq(dependencies.raw);
7✔
490
    dependencies.transitive = _.uniq(dependencies.transitive);
7✔
491
    dependencies.annotationProcessor = _.uniq(dependencies.annotationProcessor);
7✔
492

493
    // Use highest versions for regular and transitive
494
    // dependencies with multiple versions
495
    const g = _.groupBy(
7✔
496
      dependencies.regular,
497
      (x) => x.match(/^[^:]+:[^:]+/)![0],
5✔
498
    );
499
    dependencies.regular = Object.keys(g).map((x) => this.highestVersion(g[x]));
7✔
500
    const h = _.groupBy(
7✔
501
      dependencies.transitive,
502
      (x) => x.match(/^[^:]+:[^:]+/)![0],
×
503
    );
504
    dependencies.transitive = Object.keys(h).map((x) =>
7✔
505
      this.highestVersion(h[x]),
×
506
    );
507

508
    // Add dependencies to result
509
    dependencies.regular.forEach((d) => result.push(`implementation '${d}'`));
7✔
510
    dependencies.files.forEach((d) => result.push(`implementation ${d}`));
7✔
511
    dependencies.raw.forEach((d) => {
7✔
512
      result.push(d);
7✔
513
    });
514
    dependencies.transitive.forEach((d) =>
7✔
515
      result.push(`implementation ('${d}') { transitive = true }`),
×
516
    );
517
    dependencies.annotationProcessor.forEach((d) =>
7✔
518
      result.push(`annotationProcessor '${d}'`),
×
519
    );
520
    return result;
7✔
521
  }
522

523
  public highestVersion(d: string[]): string {
524
    if (d.length === 1) {
5!
525
      return d[0];
5✔
526
    }
527
    const name = d[0].match(/^[^:]+:[^:]+/)![0];
×
528
    const version = d
×
529
      .map((x) => x.match(/^[^:]+:[^:]+:(.+)/)![1])
×
530
      // Trick to make highest version lookup as easy
531
      // as peforming a lexical sort
532
      .map((x) => x.replace('+', '999999'))
×
533
      .sort()
534
      .map((x) => x.replace('999999', '+'))
×
535
      .pop();
536
    return `${name}:${version}`;
×
537
  }
538

539
  public async addAndroidPluginHookClasses(
540
    plugins: PackagePath[],
541
    outDir: string,
542
  ): Promise<any> {
543
    const rnVersion = plugins.find((p) => p.name === 'react-native')?.version!;
7!
544
    for (const plugin of plugins) {
7✔
545
      if (plugin.name === 'react-native') {
27✔
546
        continue;
7✔
547
      }
548
      const pluginConfig = await manifest.getPluginConfig(plugin, 'android');
20✔
549
      if (!pluginConfig) {
20!
550
        log.warn(
×
551
          `Skipping ${plugin.name} as it does not have an Android configuration`,
552
        );
553
        continue;
×
554
      }
555
      const androidPluginHook = pluginConfig.pluginHook;
20✔
556
      if (androidPluginHook) {
20✔
557
        log.debug(`Adding ${androidPluginHook.name}.java`);
7✔
558
        if (!pluginConfig.path) {
7!
559
          throw new Error('No plugin config path was set. Cannot proceed.');
×
560
        }
561
        const pathToPluginConfigHook = path.join(
7✔
562
          pluginConfig.path,
563
          `${androidPluginHook.name}.java`,
564
        );
565
        const pathToCopyPluginConfigHookTo = path.join(
7✔
566
          outDir,
567
          'lib/src/main/java/com/walmartlabs/ern/container/plugins',
568
        );
569
        shell.cp(pathToPluginConfigHook, pathToCopyPluginConfigHookTo);
7✔
570

571
        if (semver.gte(rnVersion, '0.60.0')) {
7!
572
          const filesConverted = this.convertToAndroidX(
7✔
573
            pathToCopyPluginConfigHookTo,
574
          );
575
          if (filesConverted > 0) {
7✔
576
            log.info(
6✔
577
              `${plugin.name} contains source files with references to the Android Support Library (android.support.*)`,
578
            );
579
            log.info(
6✔
580
              `${filesConverted} files successfully converted to use AndroidX (androidx.*)`,
581
            );
582
          }
583
        }
584
      }
585
    }
586
  }
587

588
  /**
589
   * Convert files in a directory from support library to AndroidX
590
   * eg: import android.support.annotation.NonNull => import androidx.annotation.NonNull
591
   */
592
  public convertToAndroidX(dir: string): number {
593
    const filesWithSupportLib: string[] = [];
27✔
594
    shell.pushd(dir);
27✔
595
    shell
27✔
596
      .ls('-R', '.')
597
      .filter((file) => file.match(/\.(java|kt)$/))
435✔
598
      .forEach((file) => {
599
        if (shell.grep('android.support', file).trim().length !== 0) {
330✔
600
          filesWithSupportLib.push(file);
226✔
601
        }
602
      });
603

604
    filesWithSupportLib.forEach((file) => {
27✔
605
      shell.sed('-i', 'android.support', 'androidx', file);
226✔
606
    });
607

608
    shell.popd();
27✔
609

610
    return filesWithSupportLib.length;
27✔
611
  }
612

613
  public async buildAndroidPluginsViews(
614
    plugins: PackagePath[],
615
    mustacheView: any,
616
  ): Promise<any> {
617
    mustacheView.plugins = await generatePluginsMustacheViews(
7✔
618
      plugins,
619
      'android',
620
    );
621
    const reactNativeCodePushPlugin = _.find(
7✔
622
      plugins,
623
      (p) => p.name === 'react-native-code-push',
27✔
624
    );
625
    if (reactNativeCodePushPlugin) {
7!
626
      mustacheView.isCodePushPluginIncluded = true;
×
627
    }
628
  }
629
}
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

© 2026 Coveralls, Inc