expo-updates 0.24.9 → 0.24.10

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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 0.24.10 — 2024-02-06
14
+
15
+ ### 🎉 New features
16
+
17
+ - Add assets:verify command to CLI. ([#26756](https://github.com/expo/expo/pull/26756) by [@douglowder](https://github.com/douglowder))
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - Fix assets:verify command for images with multiple scales. ([#26940](https://github.com/expo/expo/pull/26940) by [@douglowder](https://github.com/douglowder))
22
+
13
23
  ## 0.24.9 — 2024-01-26
14
24
 
15
25
  ### 🐛 Bug fixes
@@ -4,7 +4,7 @@ apply plugin: 'kotlin-kapt'
4
4
  apply plugin: 'maven-publish'
5
5
 
6
6
  group = 'host.exp.exponent'
7
- version = '0.24.9'
7
+ version = '0.24.10'
8
8
 
9
9
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
10
  if (expoModulesCorePlugin.exists()) {
@@ -122,7 +122,7 @@ android {
122
122
  namespace "expo.modules.updates"
123
123
  defaultConfig {
124
124
  versionCode 31
125
- versionName '0.24.9'
125
+ versionName '0.24.10'
126
126
  consumerProguardFiles("proguard-rules.pro")
127
127
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
128
128
 
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from './cli';
3
+ export declare const expoAssetsVerify: Command;
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.expoAssetsVerify = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const path_1 = tslib_1.__importDefault(require("path"));
8
+ const assetsVerifyAsync_1 = require("./assetsVerifyAsync");
9
+ const assetsVerifyTypes_1 = require("./assetsVerifyTypes");
10
+ const args_1 = require("./utils/args");
11
+ const Log = tslib_1.__importStar(require("./utils/log"));
12
+ const debug = require('debug')('expo-updates:assets:verify');
13
+ const expoAssetsVerify = async (argv) => {
14
+ const args = (0, args_1.assertArgs)({
15
+ // Types
16
+ '--asset-map-path': String,
17
+ '--exported-manifest-path': String,
18
+ '--build-manifest-path': String,
19
+ '--platform': String,
20
+ '--help': Boolean,
21
+ // Aliases
22
+ '-a': '--asset-map-path',
23
+ '-e': '--exported-manifest-path',
24
+ '-b': '--build-manifest-path',
25
+ '-p': '--platform',
26
+ '-h': '--help',
27
+ }, argv !== null && argv !== void 0 ? argv : []);
28
+ if (args['--help']) {
29
+ Log.exit((0, chalk_1.default) `
30
+ {bold Description}
31
+ Verify that all static files in an exported bundle are in either the export or an embedded bundle
32
+
33
+ {bold Usage}
34
+ {dim $} npx expo-updates assets:verify {dim <dir>}
35
+
36
+ Options
37
+ <dir> Directory of the Expo project. Default: Current working directory
38
+ -a, --asset-map-path <path> Path to the \`assetmap.json\` in an export produced by the command \`npx expo export --dump-assetmap\`
39
+ -e, --exported-manifest-path <path> Path to the \`metadata.json\` in an export produced by the command \`npx expo export --dump-assetmap\`
40
+ -b, --build-manifest-path <path> Path to the \`app.manifest\` file created by expo-updates in an Expo application build (either ios or android)
41
+ -p, --platform <platform> Options: ${JSON.stringify(assetsVerifyTypes_1.validPlatforms)}
42
+ -h, --help Usage info
43
+ `, 0);
44
+ }
45
+ return (async () => {
46
+ const projectRoot = (0, args_1.getProjectRoot)(args);
47
+ const validatedArgs = resolveOptions(projectRoot, args);
48
+ debug(`Validated params: ${JSON.stringify(validatedArgs, null, 2)}`);
49
+ const { buildManifestPath, exportedManifestPath, assetMapPath, platform } = validatedArgs;
50
+ const missingAssets = await (0, assetsVerifyAsync_1.getMissingAssetsAsync)(buildManifestPath, exportedManifestPath, assetMapPath, platform);
51
+ if (missingAssets.length > 0) {
52
+ throw new Error(`${missingAssets.length} assets not found in either embedded manifest or in exported bundle:${JSON.stringify(missingAssets, null, 2)}`);
53
+ }
54
+ else {
55
+ Log.log(`All resolved assets found in either embedded manifest or in exported bundle.`);
56
+ }
57
+ process.exit(0);
58
+ })().catch((e) => {
59
+ Log.log(`${e}`);
60
+ process.exit(1);
61
+ });
62
+ };
63
+ exports.expoAssetsVerify = expoAssetsVerify;
64
+ function resolveOptions(projectRoot, args) {
65
+ const exportedManifestPath = validatedPathFromArgument(projectRoot, args, '--exported-manifest-path');
66
+ const buildManifestPath = validatedPathFromArgument(projectRoot, args, '--build-manifest-path');
67
+ const assetMapPath = validatedPathFromArgument(projectRoot, args, '--asset-map-path');
68
+ const platform = args['--platform'];
69
+ if (!(0, assetsVerifyTypes_1.isValidPlatform)(platform)) {
70
+ throw new Error(`Platform must be one of ${JSON.stringify(assetsVerifyTypes_1.validPlatforms)}`);
71
+ }
72
+ return {
73
+ exportedManifestPath,
74
+ buildManifestPath,
75
+ assetMapPath,
76
+ platform,
77
+ };
78
+ }
79
+ function validatedPathFromArgument(projectRoot, args, key) {
80
+ const maybeRelativePath = args[key];
81
+ if (!maybeRelativePath) {
82
+ throw new Error(`No value for ${key}`);
83
+ }
84
+ if (maybeRelativePath.indexOf('/') === 0) {
85
+ return maybeRelativePath; // absolute path
86
+ }
87
+ return path_1.default.resolve(projectRoot, maybeRelativePath);
88
+ }
@@ -0,0 +1,59 @@
1
+ import { BuildManifest, ExportedMetadata, FullAssetDump, FullAssetDumpEntry, MissingAsset, Platform } from './assetsVerifyTypes';
2
+ /**
3
+ * Finds any assets that will be missing from an app given a build and an exported update bundle.
4
+ *
5
+ * @param buildManifestPath Path to the `app.manifest` file created by expo-updates in an Expo application build (either ios or android)
6
+ * @param exportMetadataPath Path to the `metadata.json` in an export produced by the command `npx expo export --dump-assetmap`
7
+ * @param assetMapPath Path to the `assetmap.json` in an export produced by the command `npx expo export --dump-assetmap`
8
+ * @param projectRoot The project root path
9
+ * @returns An array containing any assets that are found in the Metro asset dump, but not included in either app.manifest or the exported bundle
10
+ */
11
+ export declare function getMissingAssetsAsync(buildManifestPath: string, exportMetadataPath: string, assetMapPath: string, platform: Platform): Promise<MissingAsset[]>;
12
+ /**
13
+ * Reads and returns the embedded manifest (app.manifest) for a build.
14
+ *
15
+ * @param buildManifestPath Path to the build folder
16
+ * @param platform Either 'android' or 'ios'
17
+ * @param projectRoot The project root path
18
+ * @returns the JSON structure of the manifest
19
+ */
20
+ export declare function getBuildManifestAsync(buildManifestPath: string): Promise<BuildManifest>;
21
+ /**
22
+ * Extracts the set of asset hashes from a build manifest.
23
+ *
24
+ * @param buildManifest The build manifest
25
+ * @returns The set of asset hashes contained in the build manifest
26
+ */
27
+ export declare function getBuildManifestHashSet(buildManifest: BuildManifest): Set<string>;
28
+ /**
29
+ * Reads and extracts the asset dump for an exported bundle.
30
+ *
31
+ * @param assetMapPath The path to the exported assetmap.json.
32
+ * @returns The asset dump as an object.
33
+ */
34
+ export declare function getFullAssetDumpAsync(assetMapPath: string): Promise<FullAssetDump>;
35
+ /**
36
+ * Extracts the set of asset hashes from an asset dump.
37
+ *
38
+ * @param assetDump
39
+ * @returns The set of asset hashes in the asset dump, and a map of hash to asset
40
+ */
41
+ export declare function getFullAssetDumpHashSet(assetDump: FullAssetDump): {
42
+ fullAssetHashSet: Set<string>;
43
+ fullAssetHashMap: Map<string, FullAssetDumpEntry>;
44
+ };
45
+ /**
46
+ * Reads and extracts the metadata.json from an exported bundle.
47
+ *
48
+ * @param exportedMetadataPath Path to the exported metadata.json.
49
+ * @returns The metadata of the bundle.
50
+ */
51
+ export declare function getExportedMetadataAsync(exportedMetadataPath: string): Promise<ExportedMetadata>;
52
+ /**
53
+ * Extracts the set of asset hashes from an exported bundle's metadata for a given platform.
54
+ *
55
+ * @param metadata The metadata from the exported bundle
56
+ * @param platform Either 'android' or 'ios'
57
+ * @returns the set of asset hashes
58
+ */
59
+ export declare function getExportedMetadataHashSet(metadata: ExportedMetadata, platform: Platform): Set<string>;
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getExportedMetadataHashSet = exports.getExportedMetadataAsync = exports.getFullAssetDumpHashSet = exports.getFullAssetDumpAsync = exports.getBuildManifestHashSet = exports.getBuildManifestAsync = exports.getMissingAssetsAsync = void 0;
4
+ const fs_1 = require("fs");
5
+ const debug = require('debug')('expo-updates:assets:verify');
6
+ /**
7
+ * Finds any assets that will be missing from an app given a build and an exported update bundle.
8
+ *
9
+ * @param buildManifestPath Path to the `app.manifest` file created by expo-updates in an Expo application build (either ios or android)
10
+ * @param exportMetadataPath Path to the `metadata.json` in an export produced by the command `npx expo export --dump-assetmap`
11
+ * @param assetMapPath Path to the `assetmap.json` in an export produced by the command `npx expo export --dump-assetmap`
12
+ * @param projectRoot The project root path
13
+ * @returns An array containing any assets that are found in the Metro asset dump, but not included in either app.manifest or the exported bundle
14
+ */
15
+ async function getMissingAssetsAsync(buildManifestPath, exportMetadataPath, assetMapPath, platform) {
16
+ const buildManifestHashSet = getBuildManifestHashSet(await getBuildManifestAsync(buildManifestPath));
17
+ const fullAssetDump = await getFullAssetDumpAsync(assetMapPath);
18
+ const { fullAssetHashSet, fullAssetHashMap } = getFullAssetDumpHashSet(fullAssetDump);
19
+ const exportedAssetSet = getExportedMetadataHashSet(await getExportedMetadataAsync(exportMetadataPath), platform);
20
+ debug(`Assets in build: ${JSON.stringify([...buildManifestHashSet], null, 2)}`);
21
+ debug(`Assets in exported bundle: ${JSON.stringify([...exportedAssetSet], null, 2)}`);
22
+ debug(`All assets resolved by Metro: ${JSON.stringify([...fullAssetHashSet], null, 2)}`);
23
+ const buildAssetsPlusExportedAssets = new Set(buildManifestHashSet);
24
+ exportedAssetSet.forEach((hash) => buildAssetsPlusExportedAssets.add(hash));
25
+ const missingAssets = [];
26
+ fullAssetHashSet.forEach((hash) => {
27
+ if (!buildAssetsPlusExportedAssets.has(hash)) {
28
+ const asset = fullAssetHashMap.get(hash);
29
+ asset === null || asset === void 0 ? void 0 : asset.fileHashes.forEach((fileHash, index) => {
30
+ if ((asset === null || asset === void 0 ? void 0 : asset.fileHashes[index]) === hash) {
31
+ missingAssets.push({
32
+ hash,
33
+ path: asset === null || asset === void 0 ? void 0 : asset.files[index],
34
+ });
35
+ }
36
+ });
37
+ }
38
+ });
39
+ return missingAssets;
40
+ }
41
+ exports.getMissingAssetsAsync = getMissingAssetsAsync;
42
+ /**
43
+ * Reads and returns the embedded manifest (app.manifest) for a build.
44
+ *
45
+ * @param buildManifestPath Path to the build folder
46
+ * @param platform Either 'android' or 'ios'
47
+ * @param projectRoot The project root path
48
+ * @returns the JSON structure of the manifest
49
+ */
50
+ async function getBuildManifestAsync(buildManifestPath) {
51
+ const buildManifestString = await fs_1.promises.readFile(buildManifestPath, { encoding: 'utf-8' });
52
+ const buildManifest = JSON.parse(buildManifestString);
53
+ return buildManifest;
54
+ }
55
+ exports.getBuildManifestAsync = getBuildManifestAsync;
56
+ /**
57
+ * Extracts the set of asset hashes from a build manifest.
58
+ *
59
+ * @param buildManifest The build manifest
60
+ * @returns The set of asset hashes contained in the build manifest
61
+ */
62
+ function getBuildManifestHashSet(buildManifest) {
63
+ var _a;
64
+ return new Set(((_a = buildManifest.assets) !== null && _a !== void 0 ? _a : []).map((asset) => asset.packagerHash));
65
+ }
66
+ exports.getBuildManifestHashSet = getBuildManifestHashSet;
67
+ /**
68
+ * Reads and extracts the asset dump for an exported bundle.
69
+ *
70
+ * @param assetMapPath The path to the exported assetmap.json.
71
+ * @returns The asset dump as an object.
72
+ */
73
+ async function getFullAssetDumpAsync(assetMapPath) {
74
+ const assetMapString = await fs_1.promises.readFile(assetMapPath, { encoding: 'utf-8' });
75
+ const assetMap = new Map(Object.entries(JSON.parse(assetMapString)));
76
+ return assetMap;
77
+ }
78
+ exports.getFullAssetDumpAsync = getFullAssetDumpAsync;
79
+ /**
80
+ * Extracts the set of asset hashes from an asset dump.
81
+ *
82
+ * @param assetDump
83
+ * @returns The set of asset hashes in the asset dump, and a map of hash to asset
84
+ */
85
+ function getFullAssetDumpHashSet(assetDump) {
86
+ const fullAssetHashSet = new Set();
87
+ const fullAssetHashMap = new Map();
88
+ assetDump.forEach((asset) => asset.fileHashes.forEach((hash) => {
89
+ fullAssetHashSet.add(hash);
90
+ fullAssetHashMap.set(hash, asset);
91
+ }));
92
+ return {
93
+ fullAssetHashSet,
94
+ fullAssetHashMap,
95
+ };
96
+ }
97
+ exports.getFullAssetDumpHashSet = getFullAssetDumpHashSet;
98
+ /**
99
+ * Reads and extracts the metadata.json from an exported bundle.
100
+ *
101
+ * @param exportedMetadataPath Path to the exported metadata.json.
102
+ * @returns The metadata of the bundle.
103
+ */
104
+ async function getExportedMetadataAsync(exportedMetadataPath) {
105
+ const metadataString = await fs_1.promises.readFile(exportedMetadataPath, { encoding: 'utf-8' });
106
+ const metadata = JSON.parse(metadataString);
107
+ return metadata;
108
+ }
109
+ exports.getExportedMetadataAsync = getExportedMetadataAsync;
110
+ /**
111
+ * Extracts the set of asset hashes from an exported bundle's metadata for a given platform.
112
+ *
113
+ * @param metadata The metadata from the exported bundle
114
+ * @param platform Either 'android' or 'ios'
115
+ * @returns the set of asset hashes
116
+ */
117
+ function getExportedMetadataHashSet(metadata, platform) {
118
+ var _a;
119
+ const fileMetadata = platform === 'android' ? metadata.fileMetadata.android : metadata.fileMetadata.ios;
120
+ if (!fileMetadata) {
121
+ throw new Error(`Exported bundle was not exported for platform ${platform}`);
122
+ }
123
+ const assets = (_a = fileMetadata === null || fileMetadata === void 0 ? void 0 : fileMetadata.assets) !== null && _a !== void 0 ? _a : [];
124
+ // Asset paths in the export metadata are of the form 'assets/<hash string>'
125
+ return new Set(assets.map((asset) => asset.path.substring(7, asset.path.length)));
126
+ }
127
+ exports.getExportedMetadataHashSet = getExportedMetadataHashSet;
@@ -0,0 +1,45 @@
1
+ export declare const validPlatforms: string[];
2
+ export type Platform = (typeof validPlatforms)[number];
3
+ export declare const isValidPlatform: (p: any) => boolean;
4
+ export interface ValidatedOptions {
5
+ exportedManifestPath: string;
6
+ buildManifestPath: string;
7
+ assetMapPath: string;
8
+ platform: Platform;
9
+ }
10
+ export type FullAssetDumpEntry = {
11
+ files: string[];
12
+ hash: string;
13
+ name: string;
14
+ type: string;
15
+ fileHashes: string[];
16
+ };
17
+ export type FullAssetDump = Map<string, FullAssetDumpEntry>;
18
+ export type BuildManifestAsset = {
19
+ name: string;
20
+ type: string;
21
+ packagerHash: string;
22
+ };
23
+ export type BuildManifest = {
24
+ assets: BuildManifestAsset[];
25
+ } & {
26
+ [key: string]: any;
27
+ };
28
+ export type ExportedMetadataAsset = {
29
+ path: string;
30
+ ext: string;
31
+ };
32
+ export type FileMetadata = {
33
+ bundle: string;
34
+ assets: ExportedMetadataAsset[];
35
+ };
36
+ export type ExportedMetadata = {
37
+ fileMetadata: {
38
+ ios?: FileMetadata;
39
+ android?: FileMetadata;
40
+ };
41
+ };
42
+ export type MissingAsset = {
43
+ hash: string;
44
+ path: string;
45
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ // Types for the options passed into the command
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.isValidPlatform = exports.validPlatforms = void 0;
5
+ exports.validPlatforms = ['android', 'ios'];
6
+ const isValidPlatform = (p) => exports.validPlatforms.includes(p);
7
+ exports.isValidPlatform = isValidPlatform;
package/build-cli/cli.js CHANGED
@@ -4,11 +4,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const tslib_1 = require("tslib");
5
5
  const arg_1 = tslib_1.__importDefault(require("arg"));
6
6
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
+ const debug_1 = tslib_1.__importDefault(require("debug"));
8
+ const getenv_1 = require("getenv");
7
9
  const Log = tslib_1.__importStar(require("./utils/log"));
10
+ // Setup before requiring `debug`.
11
+ if ((0, getenv_1.boolish)('EXPO_DEBUG', false)) {
12
+ debug_1.default.enable('expo-updates:*');
13
+ }
14
+ else if (debug_1.default.enabled('expo-updates:')) {
15
+ process.env.EXPO_DEBUG = '1';
16
+ }
8
17
  const commands = {
9
18
  // Add a new command here
10
19
  'codesigning:generate': () => import('./generateCodeSigning.js').then((i) => i.generateCodeSigning),
11
20
  'codesigning:configure': () => import('./configureCodeSigning.js').then((i) => i.configureCodeSigning),
21
+ 'assets:verify': () => import('./assetsVerify.js').then((i) => i.expoAssetsVerify),
12
22
  };
13
23
  const args = (0, arg_1.default)({
14
24
  // Types
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import arg from 'arg';
3
+ import chalk from 'chalk';
4
+ import path from 'path';
5
+
6
+ import { getMissingAssetsAsync } from './assetsVerifyAsync';
7
+ import {
8
+ isValidPlatform,
9
+ validPlatforms,
10
+ type ValidatedOptions,
11
+ type Platform,
12
+ } from './assetsVerifyTypes';
13
+ import { Command } from './cli';
14
+ import { assertArgs, getProjectRoot } from './utils/args';
15
+ import * as Log from './utils/log';
16
+
17
+ const debug = require('debug')('expo-updates:assets:verify') as typeof console.log;
18
+
19
+ export const expoAssetsVerify: Command = async (argv) => {
20
+ const args = assertArgs(
21
+ {
22
+ // Types
23
+ '--asset-map-path': String,
24
+ '--exported-manifest-path': String,
25
+ '--build-manifest-path': String,
26
+ '--platform': String,
27
+ '--help': Boolean,
28
+ // Aliases
29
+ '-a': '--asset-map-path',
30
+ '-e': '--exported-manifest-path',
31
+ '-b': '--build-manifest-path',
32
+ '-p': '--platform',
33
+ '-h': '--help',
34
+ },
35
+ argv ?? []
36
+ );
37
+
38
+ if (args['--help']) {
39
+ Log.exit(
40
+ chalk`
41
+ {bold Description}
42
+ Verify that all static files in an exported bundle are in either the export or an embedded bundle
43
+
44
+ {bold Usage}
45
+ {dim $} npx expo-updates assets:verify {dim <dir>}
46
+
47
+ Options
48
+ <dir> Directory of the Expo project. Default: Current working directory
49
+ -a, --asset-map-path <path> Path to the \`assetmap.json\` in an export produced by the command \`npx expo export --dump-assetmap\`
50
+ -e, --exported-manifest-path <path> Path to the \`metadata.json\` in an export produced by the command \`npx expo export --dump-assetmap\`
51
+ -b, --build-manifest-path <path> Path to the \`app.manifest\` file created by expo-updates in an Expo application build (either ios or android)
52
+ -p, --platform <platform> Options: ${JSON.stringify(validPlatforms)}
53
+ -h, --help Usage info
54
+ `,
55
+ 0
56
+ );
57
+ }
58
+
59
+ return (async () => {
60
+ const projectRoot = getProjectRoot(args);
61
+
62
+ const validatedArgs = resolveOptions(projectRoot, args);
63
+ debug(`Validated params: ${JSON.stringify(validatedArgs, null, 2)}`);
64
+
65
+ const { buildManifestPath, exportedManifestPath, assetMapPath, platform } = validatedArgs;
66
+
67
+ const missingAssets = await getMissingAssetsAsync(
68
+ buildManifestPath,
69
+ exportedManifestPath,
70
+ assetMapPath,
71
+ platform
72
+ );
73
+
74
+ if (missingAssets.length > 0) {
75
+ throw new Error(
76
+ `${
77
+ missingAssets.length
78
+ } assets not found in either embedded manifest or in exported bundle:${JSON.stringify(
79
+ missingAssets,
80
+ null,
81
+ 2
82
+ )}`
83
+ );
84
+ } else {
85
+ Log.log(`All resolved assets found in either embedded manifest or in exported bundle.`);
86
+ }
87
+ process.exit(0);
88
+ })().catch((e) => {
89
+ Log.log(`${e}`);
90
+ process.exit(1);
91
+ });
92
+ };
93
+
94
+ function resolveOptions(projectRoot: string, args: arg.Result<arg.Spec>): ValidatedOptions {
95
+ const exportedManifestPath = validatedPathFromArgument(
96
+ projectRoot,
97
+ args,
98
+ '--exported-manifest-path'
99
+ );
100
+ const buildManifestPath = validatedPathFromArgument(projectRoot, args, '--build-manifest-path');
101
+ const assetMapPath = validatedPathFromArgument(projectRoot, args, '--asset-map-path');
102
+
103
+ const platform = args['--platform'] as unknown as Platform;
104
+ if (!isValidPlatform(platform)) {
105
+ throw new Error(`Platform must be one of ${JSON.stringify(validPlatforms)}`);
106
+ }
107
+
108
+ return {
109
+ exportedManifestPath,
110
+ buildManifestPath,
111
+ assetMapPath,
112
+ platform,
113
+ };
114
+ }
115
+
116
+ function validatedPathFromArgument(projectRoot: string, args: arg.Result<arg.Spec>, key: string) {
117
+ const maybeRelativePath = args[key] as unknown as string;
118
+ if (!maybeRelativePath) {
119
+ throw new Error(`No value for ${key}`);
120
+ }
121
+ if (maybeRelativePath.indexOf('/') === 0) {
122
+ return maybeRelativePath; // absolute path
123
+ }
124
+ return path.resolve(projectRoot, maybeRelativePath);
125
+ }
@@ -0,0 +1,153 @@
1
+ import { promises as fs } from 'fs';
2
+
3
+ import {
4
+ BuildManifest,
5
+ ExportedMetadata,
6
+ ExportedMetadataAsset,
7
+ FullAssetDump,
8
+ FullAssetDumpEntry,
9
+ MissingAsset,
10
+ Platform,
11
+ } from './assetsVerifyTypes';
12
+
13
+ const debug = require('debug')('expo-updates:assets:verify') as typeof console.log;
14
+
15
+ /**
16
+ * Finds any assets that will be missing from an app given a build and an exported update bundle.
17
+ *
18
+ * @param buildManifestPath Path to the `app.manifest` file created by expo-updates in an Expo application build (either ios or android)
19
+ * @param exportMetadataPath Path to the `metadata.json` in an export produced by the command `npx expo export --dump-assetmap`
20
+ * @param assetMapPath Path to the `assetmap.json` in an export produced by the command `npx expo export --dump-assetmap`
21
+ * @param projectRoot The project root path
22
+ * @returns An array containing any assets that are found in the Metro asset dump, but not included in either app.manifest or the exported bundle
23
+ */
24
+ export async function getMissingAssetsAsync(
25
+ buildManifestPath: string,
26
+ exportMetadataPath: string,
27
+ assetMapPath: string,
28
+ platform: Platform
29
+ ) {
30
+ const buildManifestHashSet = getBuildManifestHashSet(
31
+ await getBuildManifestAsync(buildManifestPath)
32
+ );
33
+
34
+ const fullAssetDump = await getFullAssetDumpAsync(assetMapPath);
35
+ const { fullAssetHashSet, fullAssetHashMap } = getFullAssetDumpHashSet(fullAssetDump);
36
+
37
+ const exportedAssetSet = getExportedMetadataHashSet(
38
+ await getExportedMetadataAsync(exportMetadataPath),
39
+ platform
40
+ );
41
+
42
+ debug(`Assets in build: ${JSON.stringify([...buildManifestHashSet], null, 2)}`);
43
+ debug(`Assets in exported bundle: ${JSON.stringify([...exportedAssetSet], null, 2)}`);
44
+ debug(`All assets resolved by Metro: ${JSON.stringify([...fullAssetHashSet], null, 2)}`);
45
+
46
+ const buildAssetsPlusExportedAssets = new Set(buildManifestHashSet);
47
+ exportedAssetSet.forEach((hash) => buildAssetsPlusExportedAssets.add(hash));
48
+
49
+ const missingAssets: MissingAsset[] = [];
50
+
51
+ fullAssetHashSet.forEach((hash) => {
52
+ if (!buildAssetsPlusExportedAssets.has(hash)) {
53
+ const asset = fullAssetHashMap.get(hash);
54
+ asset?.fileHashes.forEach((fileHash, index) => {
55
+ if (asset?.fileHashes[index] === hash) {
56
+ missingAssets.push({
57
+ hash,
58
+ path: asset?.files[index],
59
+ });
60
+ }
61
+ });
62
+ }
63
+ });
64
+
65
+ return missingAssets;
66
+ }
67
+
68
+ /**
69
+ * Reads and returns the embedded manifest (app.manifest) for a build.
70
+ *
71
+ * @param buildManifestPath Path to the build folder
72
+ * @param platform Either 'android' or 'ios'
73
+ * @param projectRoot The project root path
74
+ * @returns the JSON structure of the manifest
75
+ */
76
+ export async function getBuildManifestAsync(buildManifestPath: string) {
77
+ const buildManifestString = await fs.readFile(buildManifestPath, { encoding: 'utf-8' });
78
+ const buildManifest: BuildManifest = JSON.parse(buildManifestString);
79
+ return buildManifest;
80
+ }
81
+
82
+ /**
83
+ * Extracts the set of asset hashes from a build manifest.
84
+ *
85
+ * @param buildManifest The build manifest
86
+ * @returns The set of asset hashes contained in the build manifest
87
+ */
88
+ export function getBuildManifestHashSet(buildManifest: BuildManifest) {
89
+ return new Set((buildManifest.assets ?? []).map((asset) => asset.packagerHash));
90
+ }
91
+
92
+ /**
93
+ * Reads and extracts the asset dump for an exported bundle.
94
+ *
95
+ * @param assetMapPath The path to the exported assetmap.json.
96
+ * @returns The asset dump as an object.
97
+ */
98
+ export async function getFullAssetDumpAsync(assetMapPath: string) {
99
+ const assetMapString = await fs.readFile(assetMapPath, { encoding: 'utf-8' });
100
+ const assetMap: FullAssetDump = new Map(Object.entries(JSON.parse(assetMapString)));
101
+ return assetMap;
102
+ }
103
+
104
+ /**
105
+ * Extracts the set of asset hashes from an asset dump.
106
+ *
107
+ * @param assetDump
108
+ * @returns The set of asset hashes in the asset dump, and a map of hash to asset
109
+ */
110
+ export function getFullAssetDumpHashSet(assetDump: FullAssetDump) {
111
+ const fullAssetHashSet = new Set<string>();
112
+ const fullAssetHashMap = new Map<string, FullAssetDumpEntry>();
113
+ assetDump.forEach((asset) =>
114
+ asset.fileHashes.forEach((hash) => {
115
+ fullAssetHashSet.add(hash);
116
+ fullAssetHashMap.set(hash, asset);
117
+ })
118
+ );
119
+ return {
120
+ fullAssetHashSet,
121
+ fullAssetHashMap,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Reads and extracts the metadata.json from an exported bundle.
127
+ *
128
+ * @param exportedMetadataPath Path to the exported metadata.json.
129
+ * @returns The metadata of the bundle.
130
+ */
131
+ export async function getExportedMetadataAsync(exportedMetadataPath: string) {
132
+ const metadataString = await fs.readFile(exportedMetadataPath, { encoding: 'utf-8' });
133
+ const metadata: ExportedMetadata = JSON.parse(metadataString);
134
+ return metadata;
135
+ }
136
+
137
+ /**
138
+ * Extracts the set of asset hashes from an exported bundle's metadata for a given platform.
139
+ *
140
+ * @param metadata The metadata from the exported bundle
141
+ * @param platform Either 'android' or 'ios'
142
+ * @returns the set of asset hashes
143
+ */
144
+ export function getExportedMetadataHashSet(metadata: ExportedMetadata, platform: Platform) {
145
+ const fileMetadata =
146
+ platform === 'android' ? metadata.fileMetadata.android : metadata.fileMetadata.ios;
147
+ if (!fileMetadata) {
148
+ throw new Error(`Exported bundle was not exported for platform ${platform}`);
149
+ }
150
+ const assets: ExportedMetadataAsset[] = fileMetadata?.assets ?? [];
151
+ // Asset paths in the export metadata are of the form 'assets/<hash string>'
152
+ return new Set(assets.map((asset) => asset.path.substring(7, asset.path.length)));
153
+ }
@@ -0,0 +1,64 @@
1
+ // Types for the options passed into the command
2
+
3
+ export const validPlatforms = ['android', 'ios'];
4
+
5
+ export type Platform = (typeof validPlatforms)[number];
6
+
7
+ export const isValidPlatform = (p: any) => validPlatforms.includes(p);
8
+
9
+ export interface ValidatedOptions {
10
+ exportedManifestPath: string;
11
+ buildManifestPath: string;
12
+ assetMapPath: string;
13
+ platform: Platform;
14
+ }
15
+
16
+ // Types for the full asset map (npx expo export --dump-assets)
17
+
18
+ export type FullAssetDumpEntry = {
19
+ files: string[];
20
+ hash: string;
21
+ name: string;
22
+ type: string;
23
+ fileHashes: string[];
24
+ };
25
+
26
+ export type FullAssetDump = Map<string, FullAssetDumpEntry>;
27
+
28
+ // Types for the embedded manifest created by expo-updates
29
+
30
+ export type BuildManifestAsset = {
31
+ name: string;
32
+ type: string;
33
+ packagerHash: string;
34
+ };
35
+
36
+ export type BuildManifest = {
37
+ assets: BuildManifestAsset[];
38
+ } & { [key: string]: any };
39
+
40
+ // Types for the metadata exported by npx expo export
41
+
42
+ export type ExportedMetadataAsset = {
43
+ path: string;
44
+ ext: string;
45
+ };
46
+
47
+ export type FileMetadata = {
48
+ bundle: string;
49
+ assets: ExportedMetadataAsset[];
50
+ };
51
+
52
+ export type ExportedMetadata = {
53
+ fileMetadata: {
54
+ ios?: FileMetadata;
55
+ android?: FileMetadata;
56
+ };
57
+ };
58
+
59
+ // Type for the missing asset array returned by getMissingAssetsAsync
60
+
61
+ export type MissingAsset = {
62
+ hash: string;
63
+ path: string;
64
+ };
package/cli/cli.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import arg from 'arg';
3
3
  import chalk from 'chalk';
4
+ import Debug from 'debug';
5
+ import { boolish } from 'getenv';
4
6
 
5
7
  import * as Log from './utils/log';
6
8
 
9
+ // Setup before requiring `debug`.
10
+ if (boolish('EXPO_DEBUG', false)) {
11
+ Debug.enable('expo-updates:*');
12
+ } else if (Debug.enabled('expo-updates:')) {
13
+ process.env.EXPO_DEBUG = '1';
14
+ }
15
+
7
16
  export type Command = (argv?: string[]) => void;
8
17
 
9
18
  const commands: { [command: string]: () => Promise<Command> } = {
@@ -12,6 +21,7 @@ const commands: { [command: string]: () => Promise<Command> } = {
12
21
  import('./generateCodeSigning.js').then((i) => i.generateCodeSigning),
13
22
  'codesigning:configure': () =>
14
23
  import('./configureCodeSigning.js').then((i) => i.configureCodeSigning),
24
+ 'assets:verify': () => import('./assetsVerify.js').then((i) => i.expoAssetsVerify),
15
25
  };
16
26
 
17
27
  const args = arg(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-updates",
3
- "version": "0.24.9",
3
+ "version": "0.24.10",
4
4
  "description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,11 +58,12 @@
58
58
  "expo-module-scripts": "^3.0.0",
59
59
  "express": "^4.17.2",
60
60
  "form-data": "^4.0.0",
61
+ "fs-extra": "~8.1.0",
61
62
  "memfs": "^3.2.0",
62
63
  "xstate": "^4.37.2"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "expo": "*"
66
67
  },
67
- "gitHead": "82b69cba5b2e967806ca03413c3277f1d53bff8d"
68
+ "gitHead": "c4956bb901ddaffc2ad04ed22fd85aa36b8f3435"
68
69
  }