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

electrode-io / electrode-native / 7581

24 Apr 2026 09:20AM UTC coverage: 56.608% (-11.2%) from 67.856%
7581

push

Azure Pipelines

web-flow
Merge pull request #1924 from electrode-io/npm-security-audit-fix

3600 of 7762 branches covered (46.38%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 1 file covered. (0.0%)

1659 existing lines in 112 files now uncovered.

9425 of 15247 relevant lines covered (61.82%)

523.13 hits per line

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

8.7
/ern-container-gen-android/src/AndroidGenerator.ts
1
import {
3✔
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 {
3✔
23
  ContainerGenerator,
24
  ContainerGeneratorConfig,
25
  ContainerGenResult,
26
  generateContainer,
27
  generatePluginsMustacheViews,
28
  populateApiImplMustacheView,
29
} from 'ern-container-gen';
30

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

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

41
const PATH_TO_TEMPLATES_DIR = path.join(__dirname, 'templates');
3✔
42
const PATH_TO_HULL_DIR = path.join(__dirname, 'hull');
3✔
43
const ERN_CUSTOM_REACT_NATIVE_AAR_PATCH_VERSION = 100;
3✔
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 {
3✔
54
  HERMES,
3✔
55
  JSC,
3✔
56
}
57

58
export default class AndroidGenerator implements ContainerGenerator {
3✔
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> {
UNCOV
70
    return generateContainer(config, {
×
71
      fillContainerHull: this.fillContainerHull.bind(this),
72
    });
73
  }
74

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

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

UNCOV
94
    shell.cp('-R', copyFromPath, config.outDir);
×
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.
UNCOV
97
    shell.mv(`${config.outDir}/gitignore`, `${config.outDir}/.gitignore`);
×
98

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

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

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

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

UNCOV
127
    if (electrodeBridgePlugin) {
×
UNCOV
128
      mustacheView.hasElectrodeBridgePlugin = true;
×
129
    }
130

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

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

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

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

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

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

UNCOV
161
    let isKotlinEnabled = false;
×
162

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

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

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

179
      let pathToPluginProject;
180

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

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

UNCOV
199
      shell.pushd(pathToPluginProject);
×
UNCOV
200
      try {
×
UNCOV
201
        if (await coreUtils.isDependencyPathNativeApiImpl(pluginSourcePath)) {
×
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)
UNCOV
204
          const relPathToApiImplSource = path.normalize(
×
205
            'lib/src/main/java/com/ern',
206
          );
UNCOV
207
          const absPathToCopyPluginSourceTo = path.join(
×
208
            config.outDir,
209
            'lib/src/main/java/com',
210
          );
UNCOV
211
          shell.cp('-R', relPathToApiImplSource, absPathToCopyPluginSourceTo);
×
212
        } else {
UNCOV
213
          const relPathToPluginSource = pluginConfig!.moduleName
×
214
            ? path.join(pluginConfig!.moduleName, 'src/main/java')
×
215
            : path.join('src/main/java');
UNCOV
216
          const absPathToCopyPluginSourceTo = path.join(
×
217
            config.outDir,
218
            'lib/src/main',
219
          );
220

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

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

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

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

UNCOV
252
        if (replaceInFile && Array.isArray(replaceInFile)) {
×
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

UNCOV
269
        if (applyPatch) {
×
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

UNCOV
284
        if (dependencies) {
×
UNCOV
285
          const transitivePrefix = 'transitive:';
×
UNCOV
286
          const filesPrefix = 'files';
×
UNCOV
287
          const annotationProcessorPrefix = 'annotationProcessor:';
×
UNCOV
288
          for (const dependency of dependencies) {
×
UNCOV
289
            if (dependency.startsWith(transitivePrefix)) {
×
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
              );
UNCOV
298
            } else if (dependency.startsWith(filesPrefix)) {
×
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
              );
UNCOV
305
            } else if (dependency.startsWith(annotationProcessorPrefix)) {
×
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
              );
UNCOV
314
            } else if (/^[^:\s'(]+:[^:]+:[^:]+$/.test(dependency)) {
×
UNCOV
315
              androidDependencies.regular.push(dependency);
×
316
            } else {
317
              androidDependencies.raw.push(dependency);
×
318
            }
319
          }
320
        }
321

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

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

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

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

UNCOV
346
    mustacheView.isKotlinEnabled = isKotlinEnabled;
×
347

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

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

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

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

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

UNCOV
379
    injectPluginsKaxTask.succeed(injectPluginsTaskMsg);
×
380

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

UNCOV
388
    log.debug('Patching hull');
×
UNCOV
389
    const files = readDir(
×
390
      config.outDir,
391
      (f) =>
UNCOV
392
        !f.endsWith('.jar') &&
×
393
        !f.endsWith('.aar') &&
394
        !f.endsWith('.git') &&
395
        f !== '.gradle' &&
396
        f !== 'build',
397
    );
UNCOV
398
    const pathLibSrcMain = path.normalize('lib/src/main');
×
UNCOV
399
    const pathLibSrcMainJniLibs = path.normalize('lib/src/main/jniLibs');
×
UNCOV
400
    const pathLibSrcMainAssets = path.normalize('lib/src/main/assets');
×
UNCOV
401
    const pathLibSrcMainJavaCom = path.join(pathLibSrcMain, 'java/com');
×
UNCOV
402
    const pathLibSrcMainRes = path.join(pathLibSrcMain, 'res');
×
UNCOV
403
    const pathLibSrcMainJavaComWalmartlabsErnContainer = path.join(
×
404
      pathLibSrcMainJavaCom,
405
      'walmartlabs/ern/container',
406
    );
UNCOV
407
    for (const file of files) {
×
UNCOV
408
      if (
×
409
        (file.startsWith(pathLibSrcMainJavaCom) &&
×
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)
UNCOV
418
        continue;
×
419
      }
UNCOV
420
      log.debug(`Mustaching ${file}`);
×
UNCOV
421
      const pathToFile = path.join(config.outDir, file);
×
UNCOV
422
      await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile(
×
423
        pathToFile,
424
        mustacheView,
425
        pathToFile,
426
        partialProxy,
427
      );
428
    }
429

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

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

UNCOV
454
    for (const perform of replacements) {
×
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
  ) {
UNCOV
475
    const result: any[] = [];
×
476

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

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

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

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

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

UNCOV
571
        if (semver.gte(rnVersion, '0.60.0')) {
×
UNCOV
572
          const filesConverted = this.convertToAndroidX(
×
573
            pathToCopyPluginConfigHookTo,
574
          );
UNCOV
575
          if (filesConverted > 0) {
×
UNCOV
576
            log.info(
×
577
              `${plugin.name} contains source files with references to the Android Support Library (android.support.*)`,
578
            );
UNCOV
579
            log.info(
×
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[] = [];
2✔
594
    shell.pushd(dir);
2✔
595
    shell
2✔
596
      .ls('-R', '.')
597
      .filter((file) => file.match(/\.(java|kt)$/))
6✔
598
      .forEach((file) => {
599
        if (shell.grep('android.support', file).trim().length !== 0) {
4✔
600
          filesWithSupportLib.push(file);
2✔
601
        }
602
      });
603

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

608
    shell.popd();
2✔
609

610
    return filesWithSupportLib.length;
2✔
611
  }
612

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