@expo/repack-app 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,6 @@ exports.decodeApkAsync = decodeApkAsync;
7
7
  exports.rebuildApkAsync = rebuildApkAsync;
8
8
  exports.addBundleAssetsToDecodedApkAsync = addBundleAssetsToDecodedApkAsync;
9
9
  exports.addRenameManifestPackageAsync = addRenameManifestPackageAsync;
10
- const steps_1 = require("@expo/steps");
11
10
  const promises_1 = __importDefault(require("fs/promises"));
12
11
  const glob_1 = require("glob");
13
12
  const node_assert_1 = __importDefault(require("node:assert"));
@@ -18,23 +17,31 @@ let cachedApktoolPath = null;
18
17
  * Decode the APK file using APKTool.
19
18
  */
20
19
  async function decodeApkAsync(apkPath, options) {
21
- const { workingDirectory } = options;
20
+ const { spawnAsync, workingDirectory } = options;
22
21
  const apktoolPath = await getApktoolPathAsync();
23
22
  const outputPath = 'decoded-apk';
24
- await (0, steps_1.spawnAsync)('java', ['-jar', apktoolPath, 'decode', apkPath, '-s', '-o', outputPath], options.verbose
25
- ? { logger: options.logger, stdio: 'pipe', cwd: workingDirectory }
26
- : { cwd: workingDirectory });
23
+ await spawnAsync('java', ['-jar', apktoolPath, 'decode', apkPath, '-s', '-o', outputPath], {
24
+ cwd: workingDirectory,
25
+ });
27
26
  return node_path_1.default.join(workingDirectory, outputPath);
28
27
  }
29
28
  /**
30
29
  * Rebuild the decoded APK file using APKTool.
31
30
  */
32
31
  async function rebuildApkAsync(decodedApkRoot, options) {
33
- const { workingDirectory } = options;
32
+ const { spawnAsync, workingDirectory } = options;
34
33
  const apktoolPath = await getApktoolPathAsync();
35
34
  const apktoolPackedApkName = 'apktool-packed.apk';
36
35
  const apktoolPackedApkPath = node_path_1.default.resolve(workingDirectory, apktoolPackedApkName);
37
- await (0, steps_1.spawnAsync)('java', ['-jar', apktoolPath, 'build', '-o', apktoolPackedApkPath, '--use-aapt2', decodedApkRoot], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
36
+ await spawnAsync('java', [
37
+ '-jar',
38
+ apktoolPath,
39
+ 'build',
40
+ '-o',
41
+ apktoolPackedApkPath,
42
+ '--use-aapt2',
43
+ decodedApkRoot,
44
+ ]);
38
45
  return apktoolPackedApkPath;
39
46
  }
40
47
  /**
@@ -22,4 +22,4 @@ export declare function findLatestBuildToolsDirAsync(): Promise<string | null>;
22
22
  /**
23
23
  * Search for classes in the APK with the given app ID pattern passing to grep.
24
24
  */
25
- export declare function searchDexClassesAsync(unzipApkRoot: string, grepAppIdPattern: string): Promise<Set<string>>;
25
+ export declare function searchDexClassesAsync(unzipApkRoot: string, grepAppIdPattern: string, options: NormalizedOptions): Promise<Set<string>>;
@@ -7,7 +7,6 @@ exports.getAndroidBuildToolsAsync = getAndroidBuildToolsAsync;
7
7
  exports.createResignedApkAsync = createResignedApkAsync;
8
8
  exports.findLatestBuildToolsDirAsync = findLatestBuildToolsDirAsync;
9
9
  exports.searchDexClassesAsync = searchDexClassesAsync;
10
- const steps_1 = require("@expo/steps");
11
10
  const node_assert_1 = __importDefault(require("node:assert"));
12
11
  const promises_1 = __importDefault(require("node:fs/promises"));
13
12
  const node_path_1 = __importDefault(require("node:path"));
@@ -34,17 +33,15 @@ async function getAndroidBuildToolsAsync(options) {
34
33
  * @returns
35
34
  */
36
35
  async function createResignedApkAsync(unzippedApkRoot, options, signingOptions) {
37
- const { workingDirectory } = options;
36
+ const { spawnAsync, workingDirectory } = options;
38
37
  const { apksignerPath, zipalignPath } = await getAndroidBuildToolsAsync(options);
39
38
  const resignedApkPath = node_path_1.default.join(workingDirectory, 'resigned.apk');
40
39
  const resignedApkUnalignedPath = node_path_1.default.resolve(workingDirectory, 'resigned-unaligned.apk');
41
- await (0, steps_1.spawnAsync)('zip', ['-r', '-0', resignedApkUnalignedPath, 'lib', 'resources.arsc'], options.verbose
42
- ? { logger: options.logger, stdio: 'pipe', cwd: unzippedApkRoot }
43
- : { cwd: unzippedApkRoot });
44
- await (0, steps_1.spawnAsync)('zip', ['-r', resignedApkUnalignedPath, '.', '-x', 'lib/*', '-x', 'resources.arsc'], options.verbose
45
- ? { logger: options.logger, stdio: 'pipe', cwd: unzippedApkRoot }
46
- : { cwd: unzippedApkRoot });
47
- await (0, steps_1.spawnAsync)(zipalignPath, ['-v', '-p', '4', resignedApkUnalignedPath, resignedApkPath], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
40
+ await spawnAsync('zip', ['-r', '-0', resignedApkUnalignedPath, 'lib', 'resources.arsc'], {
41
+ cwd: unzippedApkRoot,
42
+ });
43
+ await spawnAsync('zip', ['-r', resignedApkUnalignedPath, '.', '-x', 'lib/*', '-x', 'resources.arsc'], { cwd: unzippedApkRoot });
44
+ await spawnAsync(zipalignPath, ['-v', '-p', '4', resignedApkUnalignedPath, resignedApkPath]);
48
45
  const signerArgs = [
49
46
  'sign',
50
47
  '--ks',
@@ -59,7 +56,7 @@ async function createResignedApkAsync(unzippedApkRoot, options, signingOptions)
59
56
  signerArgs.push('--key-pass', signingOptions.keyPassword);
60
57
  }
61
58
  signerArgs.push(resignedApkPath);
62
- await (0, steps_1.spawnAsync)(apksignerPath, signerArgs, options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
59
+ await spawnAsync(apksignerPath, signerArgs);
63
60
  return resignedApkPath;
64
61
  }
65
62
  /**
@@ -101,10 +98,11 @@ async function findLatestBuildToolsDirAsync() {
101
98
  /**
102
99
  * Search for classes in the APK with the given app ID pattern passing to grep.
103
100
  */
104
- async function searchDexClassesAsync(unzipApkRoot, grepAppIdPattern) {
101
+ async function searchDexClassesAsync(unzipApkRoot, grepAppIdPattern, options) {
102
+ const { spawnAsync } = options;
105
103
  const { dexdumpPath } = await getAndroidBuildToolsAsync();
106
104
  const grepPattern = `"^ Class descriptor : 'L${grepAppIdPattern.replace(/\./g, '/')}\\/"`;
107
- const { stdout } = await (0, steps_1.spawnAsync)(dexdumpPath, ['classes*.dex', '|', 'grep', grepPattern], {
105
+ const { stdout } = await spawnAsync(dexdumpPath, ['classes*.dex', '|', 'grep', grepPattern], {
108
106
  cwd: unzipApkRoot,
109
107
  shell: true,
110
108
  });
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.repackAppAndroidAsync = repackAppAndroidAsync;
7
- const steps_1 = require("@expo/steps");
8
7
  const node_assert_1 = __importDefault(require("node:assert"));
9
8
  const promises_1 = __importDefault(require("node:fs/promises"));
10
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -19,7 +18,7 @@ const utils_1 = require("../utils");
19
18
  */
20
19
  async function repackAppAndroidAsync(_options) {
21
20
  const options = await (0, utils_1.normalizeOptionAsync)(_options);
22
- const { workingDirectory, logger } = options;
21
+ const { logger, spawnAsync, workingDirectory } = options;
23
22
  await promises_1.default.mkdir(workingDirectory, { recursive: true });
24
23
  const { exp } = (0, expo_1.getExpoConfig)(options.projectRoot, {
25
24
  isPublicConfig: true,
@@ -32,22 +31,22 @@ async function repackAppAndroidAsync(_options) {
32
31
  : null;
33
32
  logger.info(picocolors_1.default.dim(`Resolved runtime version: ${updatesRuntimeVersion}`));
34
33
  if (options.exportEmbedOptions != null) {
35
- logger.info(`Generating bundle`);
34
+ logger.time(`Generating bundle`);
36
35
  const { assetRoot, bundleOutputPath } = await (0, expo_1.generateBundleAssetsAsync)(exp, options);
37
- logger.info(`Finished generating bundle ✅`);
38
- logger.info(`Adding bundled resources`);
36
+ logger.timeEnd(`Generating bundle`);
37
+ logger.time(`Adding bundled resources`);
39
38
  await (0, apktool_1.addBundleAssetsToDecodedApkAsync)({
40
39
  decodedApkRoot,
41
40
  assetRoot,
42
41
  bundleOutputPath,
43
42
  });
44
- logger.info(`Finished adding bundled resources ✅`);
43
+ logger.timeEnd(`Adding bundled resources`);
45
44
  }
46
- logger.info(`Updating Androidmanifest.xml`);
45
+ logger.time(`Updating Androidmanifest.xml`);
47
46
  const androidManiestFilePath = node_path_1.default.join(decodedApkRoot, 'AndroidManifest.xml');
48
47
  const androidManiestXml = await (0, resources_1.parseXmlFileAsync)(androidManiestFilePath);
49
48
  const originalAppId = await (0, resources_1.queryAppIdFromManifestAsync)(androidManiestXml);
50
- const dexClasses = await (0, build_tools_1.searchDexClassesAsync)(decodedApkRoot, originalAppId);
49
+ const dexClasses = await (0, build_tools_1.searchDexClassesAsync)(decodedApkRoot, originalAppId, options);
51
50
  await (0, resources_1.updateAndroidManifestAsync)({
52
51
  config: exp,
53
52
  androidManiestXml,
@@ -58,19 +57,19 @@ async function repackAppAndroidAsync(_options) {
58
57
  });
59
58
  await (0, apktool_1.addRenameManifestPackageAsync)(decodedApkRoot, exp.android?.package);
60
59
  await (0, resources_1.buildXmlFileAsync)(androidManiestXml, androidManiestFilePath);
61
- logger.info(`Finished updating Androidmanifest.xml ✅`);
62
- logger.info(`Updating resources`);
60
+ logger.timeEnd(`Updating Androidmanifest.xml`);
61
+ logger.time(`Updating resources`);
63
62
  await (0, resources_1.updateResourcesAsync)({ config: exp, decodedApkRoot });
64
- logger.info(`Finished updating resources ✅`);
65
- logger.info(`Generating app config`);
63
+ logger.timeEnd(`Updating resources`);
64
+ logger.time(`Generating app config`);
66
65
  const appConfigPath = await (0, expo_1.generateAppConfigAsync)(options, exp);
67
66
  await promises_1.default.copyFile(appConfigPath, node_path_1.default.join(decodedApkRoot, 'assets', 'app.config'));
68
- logger.info(`Finished generating app config ✅`);
69
- logger.info(`Creating updated apk`);
67
+ logger.timeEnd(`Generating app config`);
68
+ logger.time(`Creating updated apk`);
70
69
  const apktoolPackedApkPath = await (0, apktool_1.rebuildApkAsync)(decodedApkRoot, options);
71
70
  const unzippedApkRoot = node_path_1.default.join(workingDirectory, 'unzip');
72
71
  await promises_1.default.mkdir(unzippedApkRoot, { recursive: true });
73
- await (0, steps_1.spawnAsync)('unzip', ['-o', apktoolPackedApkPath, '-d', unzippedApkRoot], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
72
+ await spawnAsync('unzip', ['-o', apktoolPackedApkPath, '-d', unzippedApkRoot]);
74
73
  const outputApk = await (0, build_tools_1.createResignedApkAsync)(unzippedApkRoot, options, {
75
74
  keyStorePath: options.androidSigningOptions?.keyStorePath ??
76
75
  node_path_1.default.resolve(__dirname, '../../assets/debug.keystore'),
@@ -78,7 +77,7 @@ async function repackAppAndroidAsync(_options) {
78
77
  keyAlias: options.androidSigningOptions?.keyAlias,
79
78
  keyPassword: options.androidSigningOptions?.keyPassword,
80
79
  });
81
- logger.info(`Finished creating updated apk ✅`);
80
+ logger.timeEnd(`Creating updated apk`);
82
81
  await promises_1.default.rename(outputApk, options.outputPath);
83
82
  if (!options.skipWorkingDirCleanup) {
84
83
  try {
package/build/cli.js CHANGED
@@ -3,16 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const logger_1 = require("@expo/logger");
7
6
  const commander_1 = require("commander");
8
7
  const picocolors_1 = __importDefault(require("picocolors"));
9
8
  const index_1 = require("./index");
9
+ const utils_1 = require("./utils");
10
10
  const program = new commander_1.Command('repack-app')
11
11
  .requiredOption('-p, --platform <platform>', 'Platform to repack the app for')
12
12
  .requiredOption('--source-app <path>', 'Path to the source app file')
13
13
  .option('--android-build-tools-dir <path>', 'Path to the Android build tools directory')
14
14
  .option('-w, --working-directory <path>', 'Path to the working directory')
15
- .option('-o, --output <path>', 'Path to the output APK file')
15
+ .option('-o, --output <path>', 'Path to the output artifact')
16
16
  // Android signing options
17
17
  .option('--ks <path>', 'Path to the keystore file')
18
18
  .option('--ks-pass <password>', 'Keystore password', 'pass:android')
@@ -26,12 +26,13 @@ const program = new commander_1.Command('repack-app')
26
26
  .option('--embed-bundle-assets', 'Whether to execute export:embed to embed new bundle assets')
27
27
  .option('--bundle-assets-sourcemap-output <path>', 'Paired with --embed-bundle-assets and generate the sourcemap to the specified path')
28
28
  // arguments
29
- .argument('<project-root>', 'Path to the project root')
29
+ .argument('[project-root]', 'Path to the project root', process.cwd())
30
+ .version(require('../package.json').version)
30
31
  .parse(process.argv);
31
32
  async function runAsync() {
32
- const logger = (0, logger_1.createLogger)({ name: 'repack-app' });
33
+ const logger = new utils_1.ConsoleLogger();
33
34
  const platform = program.opts().platform;
34
- const projectRoot = program.args[0];
35
+ const projectRoot = program.processedArgs[0];
35
36
  const exportEmbedOptions = program.opts()
36
37
  .embedBundleAssets
37
38
  ? {
package/build/expo.js CHANGED
@@ -8,7 +8,6 @@ exports.isExpoUpdatesInstalled = isExpoUpdatesInstalled;
8
8
  exports.resolveRuntimeVersionAsync = resolveRuntimeVersionAsync;
9
9
  exports.getExpoConfig = getExpoConfig;
10
10
  exports.generateBundleAssetsAsync = generateBundleAssetsAsync;
11
- const steps_1 = require("@expo/steps");
12
11
  const node_assert_1 = __importDefault(require("node:assert"));
13
12
  const promises_1 = __importDefault(require("node:fs/promises"));
14
13
  const node_path_1 = __importDefault(require("node:path"));
@@ -36,13 +35,13 @@ function isExpoUpdatesInstalled(projectRoot) {
36
35
  * Resolve the `runtimeVersion` for expo-updates.
37
36
  */
38
37
  async function resolveRuntimeVersionAsync(options, config) {
39
- const { projectRoot } = options;
38
+ const { projectRoot, spawnAsync } = options;
40
39
  const cli = resolve_from_1.default.silent(projectRoot, 'expo-updates/bin/cli') ??
41
40
  (0, resolve_from_1.default)(projectRoot, 'expo-updates/bin/cli.js');
42
- const proc = await (0, steps_1.spawnAsync)(cli, ['runtimeversion:resolve', '--platform', 'android', '--workflow', 'managed'], {
41
+ const { stdout } = await spawnAsync(cli, ['runtimeversion:resolve', '--platform', 'android', '--workflow', 'managed'], {
43
42
  cwd: projectRoot,
44
43
  });
45
- const runtimeVersion = JSON.parse(proc.stdout).runtimeVersion;
44
+ const runtimeVersion = JSON.parse(stdout).runtimeVersion;
46
45
  return runtimeVersion ?? config.version ?? '1.0.0';
47
46
  }
48
47
  /**
@@ -69,11 +68,11 @@ function getExpoConfig(projectRoot, options) {
69
68
  * Generate JS bundle and assets for the app.
70
69
  */
71
70
  async function generateBundleAssetsAsync(expoConfig, options) {
72
- const { projectRoot, platform, workingDirectory } = options;
71
+ const { projectRoot, platform, spawnAsync, workingDirectory } = options;
73
72
  const bundleAssetRoot = node_path_1.default.resolve(workingDirectory, 'bundles');
74
73
  await promises_1.default.mkdir(bundleAssetRoot, { recursive: true });
75
74
  // [0] Resolve entry point
76
- const { stdout: entryFile } = await (0, steps_1.spawnAsync)('node', ['-e', "require('expo/scripts/resolveAppEntry')", projectRoot, platform, 'absolute'], { cwd: projectRoot });
75
+ const { stdout: entryFile } = await spawnAsync('node', ['-e', "require('expo/scripts/resolveAppEntry')", projectRoot, platform, 'absolute'], { cwd: projectRoot });
77
76
  // [1] Execute export:embed
78
77
  const isEnableHermes = isEnableHermesManaged(expoConfig, platform);
79
78
  const bundleFileName = platform === 'android' ? 'index.android.bundle' : 'main.bundle';
@@ -96,7 +95,7 @@ async function generateBundleAssetsAsync(expoConfig, options) {
96
95
  ];
97
96
  if (isEnableHermes) {
98
97
  exportEmbedArgs.push('--minify', 'false');
99
- exportEmbedArgs.push('--bundle-bytecode', 'true');
98
+ exportEmbedArgs.push('--bytecode', 'true');
100
99
  exportEmbedArgs.push('--unstable-transform-profile', 'hermes');
101
100
  }
102
101
  else {
@@ -105,9 +104,7 @@ async function generateBundleAssetsAsync(expoConfig, options) {
105
104
  if (options.exportEmbedOptions?.sourcemapOutput) {
106
105
  exportEmbedArgs.push('--sourcemap-output', options.exportEmbedOptions.sourcemapOutput);
107
106
  }
108
- await (0, steps_1.spawnAsync)('npx', exportEmbedArgs, options.verbose
109
- ? { logger: options.logger, stdio: 'pipe', cwd: projectRoot }
110
- : { cwd: projectRoot });
107
+ await spawnAsync('npx', exportEmbedArgs, { cwd: projectRoot });
111
108
  return {
112
109
  bundleOutputPath,
113
110
  assetRoot,
@@ -1,13 +1,17 @@
1
1
  import { type ExpoConfig } from '@expo/config';
2
2
  import type { IosSigningOptions, NormalizedOptions } from '../types';
3
3
  /**
4
- * Unzip the IPA file.
4
+ * Extract the given iOS artifact and return the path to the .app directory.
5
5
  */
6
- export declare function unzipIpaAsync(options: NormalizedOptions): Promise<string>;
6
+ export declare function extractIosArtifactAsync(options: NormalizedOptions): Promise<string>;
7
7
  /**
8
8
  * Update some binary files.
9
9
  */
10
10
  export declare function updateFilesAsync(config: ExpoConfig, appWorkingDirectory: string): Promise<string>;
11
+ /**
12
+ * From the given working .app directory, create a new .app directory.
13
+ */
14
+ export declare function createAppAsync(options: NormalizedOptions, appWorkingDirectory: string): Promise<string>;
11
15
  /**
12
16
  * From the given working .app directory, create a new .ipa file.
13
17
  */
@@ -3,48 +3,73 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.unzipIpaAsync = unzipIpaAsync;
6
+ exports.extractIosArtifactAsync = extractIosArtifactAsync;
7
7
  exports.updateFilesAsync = updateFilesAsync;
8
+ exports.createAppAsync = createAppAsync;
8
9
  exports.createIpaAsync = createIpaAsync;
9
10
  exports.createResignLane = createResignLane;
10
- const steps_1 = require("@expo/steps");
11
+ const glob_1 = require("glob");
11
12
  const node_assert_1 = __importDefault(require("node:assert"));
12
13
  const promises_1 = __importDefault(require("node:fs/promises"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const utils_1 = require("../utils");
15
16
  /**
16
- * Unzip the IPA file.
17
+ * Extract the given iOS artifact and return the path to the .app directory.
17
18
  */
18
- async function unzipIpaAsync(options) {
19
- const unzipWorkingDirectory = node_path_1.default.join(options.workingDirectory, 'unzip');
20
- await (0, steps_1.spawnAsync)('unzip', [options.sourceAppPath, '-d', unzipWorkingDirectory], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
21
- const appWorkingDirectory = node_path_1.default.join(unzipWorkingDirectory, 'Payload', 'HelloWorld.app');
22
- (0, node_assert_1.default)(await (0, utils_1.directoryExistsAsync)(appWorkingDirectory));
19
+ async function extractIosArtifactAsync(options) {
20
+ const { spawnAsync, sourceAppPath } = options;
21
+ const extractedWorkingDirectory = node_path_1.default.join(options.workingDirectory, 'extracted');
22
+ await promises_1.default.mkdir(extractedWorkingDirectory, { recursive: true });
23
+ if (sourceAppPath.endsWith('.zip') || sourceAppPath.endsWith('.ipa')) {
24
+ await spawnAsync('unzip', [sourceAppPath, '-d', extractedWorkingDirectory]);
25
+ }
26
+ else if (sourceAppPath.endsWith('.tar.gz')) {
27
+ await spawnAsync('tar', ['-xzf', sourceAppPath, '-C', extractedWorkingDirectory]);
28
+ }
29
+ else if (sourceAppPath.endsWith('.app')) {
30
+ const basename = node_path_1.default.basename(sourceAppPath);
31
+ await (0, utils_1.copyDirAsync)(sourceAppPath, node_path_1.default.join(extractedWorkingDirectory, basename));
32
+ }
33
+ else {
34
+ throw new Error('Unsupported file type. Please provide a .zip, .tar.gz, or .app file.');
35
+ }
36
+ const matches = await (0, glob_1.glob)('{Payload/*.app,*.app}', {
37
+ cwd: extractedWorkingDirectory,
38
+ absolute: true,
39
+ });
40
+ const appWorkingDirectory = matches[0];
41
+ (0, node_assert_1.default)(appWorkingDirectory, `Failed to find the .app directory in the extracted artifact: ${extractedWorkingDirectory}`);
23
42
  return appWorkingDirectory;
24
43
  }
25
44
  /**
26
45
  * Update some binary files.
27
46
  */
28
47
  async function updateFilesAsync(config, appWorkingDirectory) {
29
- const parentDir = node_path_1.default.dirname(appWorkingDirectory);
48
+ const { dir: parentDir, name } = node_path_1.default.parse(appWorkingDirectory);
30
49
  const newAppWorkingDirectory = node_path_1.default.join(parentDir, `${config.name}.app`);
31
50
  // [0] Update the .app directory
32
- await promises_1.default.rename(node_path_1.default.join(parentDir, 'HelloWorld.app'), newAppWorkingDirectory);
51
+ await promises_1.default.rename(node_path_1.default.join(parentDir, `${name}.app`), newAppWorkingDirectory);
33
52
  // [1] Rename the executable
34
- await promises_1.default.rename(node_path_1.default.join(newAppWorkingDirectory, 'HelloWorld'), node_path_1.default.join(newAppWorkingDirectory, config.name));
53
+ await promises_1.default.rename(node_path_1.default.join(newAppWorkingDirectory, name), node_path_1.default.join(newAppWorkingDirectory, config.name));
35
54
  return newAppWorkingDirectory;
36
55
  }
56
+ /**
57
+ * From the given working .app directory, create a new .app directory.
58
+ */
59
+ async function createAppAsync(options, appWorkingDirectory) {
60
+ const outputAppPath = node_path_1.default.resolve(options.workingDirectory, node_path_1.default.basename(appWorkingDirectory));
61
+ await (0, utils_1.copyDirAsync)(appWorkingDirectory, outputAppPath);
62
+ return outputAppPath;
63
+ }
37
64
  /**
38
65
  * From the given working .app directory, create a new .ipa file.
39
66
  */
40
67
  async function createIpaAsync(options, appWorkingDirectory) {
41
- const { workingDirectory } = options;
68
+ const { spawnAsync, workingDirectory } = options;
42
69
  await promises_1.default.mkdir(node_path_1.default.join(workingDirectory, 'Payload'), { recursive: true });
43
70
  await promises_1.default.rename(appWorkingDirectory, node_path_1.default.join(workingDirectory, 'Payload', node_path_1.default.basename(appWorkingDirectory)));
44
71
  const outputIpaPath = node_path_1.default.resolve(workingDirectory, 'repacked.ipa');
45
- await (0, steps_1.spawnAsync)('zip', ['-r', outputIpaPath, 'Payload'], options.verbose
46
- ? { logger: options.logger, stdio: 'pipe', cwd: workingDirectory }
47
- : { cwd: workingDirectory });
72
+ await spawnAsync('zip', ['-r', outputIpaPath, 'Payload'], { cwd: workingDirectory });
48
73
  return outputIpaPath;
49
74
  }
50
75
  /**
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.repackAppIosAsync = repackAppIosAsync;
7
7
  exports.resignIpaAsync = resignIpaAsync;
8
- const steps_1 = require("@expo/steps");
9
8
  const node_assert_1 = __importDefault(require("node:assert"));
10
9
  const promises_1 = __importDefault(require("node:fs/promises"));
11
10
  const node_path_1 = __importDefault(require("node:path"));
@@ -30,43 +29,50 @@ async function repackAppIosAsync(_options) {
30
29
  ? await (0, expo_1.resolveRuntimeVersionAsync)(options, exp)
31
30
  : null;
32
31
  logger.info(picocolors_1.default.dim(`Resolved runtime version: ${updatesRuntimeVersion}`));
33
- logger.info(`Unzipping IPA`);
34
- let appWorkingDirectory = await (0, build_tools_1.unzipIpaAsync)(options);
32
+ logger.time(`Extracting artifact from ${options.sourceAppPath}`);
33
+ let appWorkingDirectory = await (0, build_tools_1.extractIosArtifactAsync)(options);
35
34
  appWorkingDirectory = await (0, build_tools_1.updateFilesAsync)(exp, appWorkingDirectory);
36
35
  const infoPlistPath = node_path_1.default.join(appWorkingDirectory, 'Info.plist');
37
36
  const originalAppId = await (0, resources_1.queryAppIdFromPlistAsync)(infoPlistPath, options);
38
- logger.info(`Finished unzipping IPA ✅`);
37
+ logger.timeEnd(`Extracting artifact from ${options.sourceAppPath}`);
39
38
  if (options.exportEmbedOptions != null) {
40
- logger.info(`Generating bundle`);
39
+ logger.time(`Generating bundle`);
41
40
  const { assetRoot, bundleOutputPath } = await (0, expo_1.generateBundleAssetsAsync)(exp, options);
42
- logger.info(`Finished generating bundle ✅`);
43
- logger.info(`Adding bundled resources`);
41
+ logger.timeEnd(`Generating bundle`);
42
+ logger.time(`Adding bundled resources`);
44
43
  await (0, resources_1.addBundleAssetsAsync)({
45
44
  appWorkingDirectory,
46
45
  assetRoot,
47
46
  bundleOutputPath,
48
47
  });
49
- logger.info(`Finished adding bundled resources ✅`);
48
+ logger.timeEnd(`Adding bundled resources`);
50
49
  }
51
- logger.info(`Updating Info.plist`);
50
+ logger.time(`Updating Info.plist`);
52
51
  await (0, resources_1.updateInfoPlistAsync)({ config: exp, infoPlistPath, originalAppId, options });
53
- logger.info(`Finished updating Info.plist ✅`);
54
- logger.info(`Updating Expo.plist`);
52
+ logger.timeEnd(`Updating Info.plist`);
53
+ logger.time(`Updating Expo.plist`);
55
54
  await (0, resources_1.updateExpoPlistAsync)(exp, node_path_1.default.join(appWorkingDirectory, 'Expo.plist'), updatesRuntimeVersion, options);
56
- logger.info(`Finished updating Expo.plist ✅`);
57
- logger.info(`Generating app.config`);
55
+ logger.timeEnd(`Updating Expo.plist`);
56
+ logger.time(`Generating app.config`);
58
57
  const appConfigPath = await (0, expo_1.generateAppConfigAsync)(options, exp);
59
58
  await promises_1.default.copyFile(appConfigPath, node_path_1.default.join(appWorkingDirectory, 'EXConstants.bundle', 'app.config'));
60
- logger.info(`Finished generating app.config ✅`);
61
- logger.info(`Creating updated ipa`);
62
- const outputIpa = await (0, build_tools_1.createIpaAsync)(options, appWorkingDirectory);
63
- logger.info(`Finished creating updated ipa ✅`);
59
+ logger.timeEnd(`Generating app.config`);
60
+ logger.time(`Creating updated artifact`);
61
+ let outputArtifact;
62
+ if (options.outputPath.endsWith('.app')) {
63
+ outputArtifact = await (0, build_tools_1.createAppAsync)(options, appWorkingDirectory);
64
+ }
65
+ else {
66
+ outputArtifact = await (0, build_tools_1.createIpaAsync)(options, appWorkingDirectory);
67
+ }
68
+ logger.timeEnd(`Creating updated artifact`);
64
69
  if (options.iosSigningOptions) {
65
- logger.info(`Resigning the IPA at ${outputIpa}`);
66
- await resignIpaAsync(outputIpa, options.iosSigningOptions, options);
67
- logger.info(`Finished resigning the IPA ✅`);
70
+ (0, node_assert_1.default)(outputArtifact.endsWith('.ipa'), 'Signing is only supported for .ipa files');
71
+ logger.time(`Resigning the IPA at ${outputArtifact}`);
72
+ await resignIpaAsync(outputArtifact, options.iosSigningOptions, options);
73
+ logger.timeEnd(`Resigning the IPA at ${outputArtifact}`);
68
74
  }
69
- await promises_1.default.rename(outputIpa, options.outputPath);
75
+ await promises_1.default.rename(outputArtifact, options.outputPath);
70
76
  if (!options.skipWorkingDirCleanup) {
71
77
  try {
72
78
  await promises_1.default.rm(workingDirectory, { recursive: true });
@@ -80,13 +86,12 @@ async function repackAppIosAsync(_options) {
80
86
  */
81
87
  async function resignIpaAsync(ipaPath, signingOptions, _options) {
82
88
  const options = await (0, utils_1.normalizeOptionAsync)(_options);
89
+ const { spawnAsync } = options;
83
90
  const resignWorkingDirectory = node_path_1.default.join(options.workingDirectory, 'fastlane');
84
91
  await promises_1.default.mkdir(resignWorkingDirectory, { recursive: true });
85
92
  const fastfileContents = (0, build_tools_1.createResignLane)('resign_ipa', ipaPath, signingOptions);
86
93
  await promises_1.default.writeFile(node_path_1.default.join(resignWorkingDirectory, 'Fastfile'), fastfileContents);
87
- await (0, steps_1.spawnAsync)('fastlane', ['resign_ipa'], {
88
- logger: options.logger,
89
- stdio: 'pipe',
94
+ await spawnAsync('fastlane', ['resign_ipa'], {
90
95
  cwd: resignWorkingDirectory,
91
96
  env: options.env,
92
97
  });
@@ -8,7 +8,6 @@ exports.updateExpoPlistAsync = updateExpoPlistAsync;
8
8
  exports.queryAppIdFromPlistAsync = queryAppIdFromPlistAsync;
9
9
  exports.addBundleAssetsAsync = addBundleAssetsAsync;
10
10
  const plist_1 = __importDefault(require("@expo/plist"));
11
- const steps_1 = require("@expo/steps");
12
11
  const node_assert_1 = __importDefault(require("node:assert"));
13
12
  const promises_1 = __importDefault(require("node:fs/promises"));
14
13
  const node_path_1 = __importDefault(require("node:path"));
@@ -36,15 +35,18 @@ async function updateInfoPlistAsync({ config, infoPlistPath, originalAppId, opti
36
35
  urlType.CFBundleURLSchemes = schemes;
37
36
  }
38
37
  }
39
- const userActivityTypes = data.NSUserActivityTypes.map((activityType) => activityType.replace(originalAppId, bundleIdentifier));
40
- return {
38
+ const result = {
41
39
  ...data,
42
40
  CFBundleDisplayName: config.name,
43
41
  CFBundleName: config.name,
44
42
  CFBundleExecutable: config.name,
45
43
  CFBundleIdentifier: bundleIdentifier,
46
- NSUserActivityTypes: userActivityTypes,
47
44
  };
45
+ const userActivityTypes = data.NSUserActivityTypes?.map((activityType) => activityType.replace(originalAppId, bundleIdentifier));
46
+ if (userActivityTypes) {
47
+ result.NSUserActivityTypes = userActivityTypes;
48
+ }
49
+ return result;
48
50
  });
49
51
  }
50
52
  /**
@@ -88,30 +90,17 @@ async function addBundleAssetsAsync({ appWorkingDirectory, assetRoot, bundleOutp
88
90
  await promises_1.default.copyFile(bundleOutputPath, node_path_1.default.join(appWorkingDirectory, 'main.jsbundle'));
89
91
  // export:embed --assets-dest on iOS uses the app root as target directory,
90
92
  // so we just need to copy the assets to the app root.
91
- await copyDirAsync(assetRoot, appWorkingDirectory);
93
+ await (0, utils_1.copyDirAsync)(assetRoot, appWorkingDirectory);
92
94
  }
93
95
  //#region Internals
94
96
  async function updateBinaryPlistAsync(plistPath, options, updater) {
95
- await (0, steps_1.spawnAsync)('plutil', ['-convert', 'xml1', plistPath], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
97
+ const { spawnAsync } = options;
98
+ await spawnAsync('plutil', ['-convert', 'xml1', plistPath]);
96
99
  const contents = await promises_1.default.readFile(plistPath, 'utf8');
97
100
  const data = await plist_1.default.parse(contents);
98
101
  const updatedData = updater(data);
99
102
  const updatedContents = plist_1.default.build(updatedData);
100
103
  await promises_1.default.writeFile(plistPath, updatedContents);
101
- await (0, steps_1.spawnAsync)('plutil', ['-convert', 'binary1', plistPath], options.verbose ? { logger: options.logger, stdio: 'pipe' } : undefined);
102
- }
103
- async function copyDirAsync(src, dst) {
104
- await promises_1.default.mkdir(dst, { recursive: true });
105
- const entries = await promises_1.default.readdir(src, { withFileTypes: true });
106
- for (const entry of entries) {
107
- const srcPath = node_path_1.default.join(src, entry.name);
108
- const dstPath = node_path_1.default.join(dst, entry.name);
109
- if (entry.isDirectory()) {
110
- await copyDirAsync(srcPath, dstPath);
111
- }
112
- else if (entry.isFile()) {
113
- await promises_1.default.copyFile(srcPath, dstPath);
114
- }
115
- }
104
+ await spawnAsync('plutil', ['-convert', 'binary1', plistPath]);
116
105
  }
117
106
  //#endregion
package/build/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { bunyan } from '@expo/logger';
1
+ import type { ChildProcess, SpawnOptions } from 'child_process';
2
2
  export interface Options {
3
3
  /**
4
4
  * The platform to repack the app for.
@@ -12,10 +12,6 @@ export interface Options {
12
12
  * The prebuilt app file path that acts as a source for repacking.
13
13
  */
14
14
  sourceAppPath: string;
15
- /**
16
- * The bunyan logger instance.
17
- */
18
- logger: bunyan;
19
15
  /**
20
16
  * Working directory.
21
17
  */
@@ -54,10 +50,22 @@ export interface Options {
54
50
  * env variables
55
51
  */
56
52
  env?: Record<string, string | undefined>;
53
+ /**
54
+ * Customize the logger instance.
55
+ * @default `console`
56
+ */
57
+ logger?: Logger;
58
+ /**
59
+ * Customize the spawn process function.
60
+ * @default `@expo/spawn-async`
61
+ */
62
+ spawnAsync?: SpawnProcessAsync;
57
63
  }
58
64
  export interface NormalizedOptions extends Options {
59
65
  workingDirectory: NonNullable<Options['workingDirectory']>;
60
66
  outputPath: NonNullable<Options['outputPath']>;
67
+ logger: NonNullable<Options['logger']>;
68
+ spawnAsync: NonNullable<Options['spawnAsync']>;
61
69
  }
62
70
  export interface AndroidSigningOptions {
63
71
  /**
@@ -109,3 +117,27 @@ export interface ExportEmbedOptions {
109
117
  */
110
118
  sourcemapOutput?: string;
111
119
  }
120
+ export interface Logger {
121
+ debug(...message: any[]): void;
122
+ info(...message: any[]): void;
123
+ warn(...message: any[]): void;
124
+ error(...message: any[]): void;
125
+ time(label: string): void;
126
+ timeEnd(label: string): void;
127
+ }
128
+ export interface SpawnProcessOptions extends SpawnOptions {
129
+ }
130
+ export interface SpawnProcessPromise<T> extends Promise<T> {
131
+ child: ChildProcess;
132
+ }
133
+ export interface SpawnProcessResult {
134
+ pid?: number;
135
+ output: string[];
136
+ stdout: string;
137
+ stderr: string;
138
+ status: number | null;
139
+ signal: string | null;
140
+ }
141
+ export interface SpawnProcessAsync {
142
+ (command: string, args: string[], options?: SpawnProcessOptions): SpawnProcessPromise<SpawnProcessResult>;
143
+ }
package/build/types.js CHANGED
@@ -1,2 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ //#endregion Advanced integrations
package/build/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { NormalizedOptions, Options } from './types';
1
+ import type { Logger, NormalizedOptions, Options } from './types';
2
2
  /**
3
3
  * Check if a directory exists.
4
4
  */
@@ -11,3 +11,15 @@ export declare function requireNotNull<T>(value: T | null | undefined): T;
11
11
  * Normalize the options.
12
12
  */
13
13
  export declare function normalizeOptionAsync(options: Options): Promise<NormalizedOptions>;
14
+ export declare class ConsoleLogger implements Logger {
15
+ debug(...message: any[]): void;
16
+ info(...message: any[]): void;
17
+ warn(...message: any[]): void;
18
+ error(...message: any[]): void;
19
+ time(label: string): void;
20
+ timeEnd(label: string): void;
21
+ }
22
+ /**
23
+ * Copy a directory recursively.
24
+ */
25
+ export declare function copyDirAsync(src: string, dst: string): Promise<void>;
package/build/utils.js CHANGED
@@ -3,9 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ConsoleLogger = void 0;
6
7
  exports.directoryExistsAsync = directoryExistsAsync;
7
8
  exports.requireNotNull = requireNotNull;
8
9
  exports.normalizeOptionAsync = normalizeOptionAsync;
10
+ exports.copyDirAsync = copyDirAsync;
11
+ const spawn_async_1 = __importDefault(require("@expo/spawn-async"));
9
12
  const node_assert_1 = __importDefault(require("node:assert"));
10
13
  const promises_1 = __importDefault(require("node:fs/promises"));
11
14
  const node_path_1 = __importDefault(require("node:path"));
@@ -31,5 +34,53 @@ async function normalizeOptionAsync(options) {
31
34
  ...options,
32
35
  workingDirectory: options.workingDirectory ?? (await promises_1.default.mkdtemp(node_path_1.default.join(require('temp-dir'), 'repack-app-'))),
33
36
  outputPath: options.outputPath ?? node_path_1.default.join(options.projectRoot, `repacked${fileExt}`),
37
+ logger: options.logger ?? new ConsoleLogger(),
38
+ spawnAsync: options.spawnAsync ?? createDefaultSpawnAsync(!!options.verbose),
34
39
  };
35
40
  }
41
+ class ConsoleLogger {
42
+ debug(...message) {
43
+ console.debug(...message);
44
+ }
45
+ info(...message) {
46
+ console.log(...message);
47
+ }
48
+ warn(...message) {
49
+ console.warn(...message);
50
+ }
51
+ error(...message) {
52
+ console.error(...message);
53
+ }
54
+ time(label) {
55
+ console.time(label);
56
+ }
57
+ timeEnd(label) {
58
+ console.timeEnd(label);
59
+ }
60
+ }
61
+ exports.ConsoleLogger = ConsoleLogger;
62
+ function createDefaultSpawnAsync(verbose) {
63
+ return function defaultSpawnAsync(command, args, options) {
64
+ return (0, spawn_async_1.default)(command, args, {
65
+ stdio: verbose ? 'inherit' : 'pipe',
66
+ ...options,
67
+ });
68
+ };
69
+ }
70
+ /**
71
+ * Copy a directory recursively.
72
+ */
73
+ async function copyDirAsync(src, dst) {
74
+ await promises_1.default.mkdir(dst, { recursive: true });
75
+ const entries = await promises_1.default.readdir(src, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ const srcPath = node_path_1.default.join(src, entry.name);
78
+ const dstPath = node_path_1.default.join(dst, entry.name);
79
+ if (entry.isDirectory()) {
80
+ await copyDirAsync(srcPath, dstPath);
81
+ }
82
+ else if (entry.isFile()) {
83
+ await promises_1.default.copyFile(srcPath, dstPath);
84
+ }
85
+ }
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/repack-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Repacking tool for Expo apps",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -25,9 +25,8 @@
25
25
  "author": "650 Industries, Inc.",
26
26
  "license": "BUSL-1.1",
27
27
  "dependencies": {
28
- "@expo/logger": "^1.0.117",
29
28
  "@expo/plist": "^0.2.0",
30
- "@expo/steps": "^1.0.156",
29
+ "@expo/spawn-async": "^1.7.2",
31
30
  "commander": "^13.0.0",
32
31
  "glob": "^11.0.0",
33
32
  "picocolors": "^1.1.1",