@hot-updater/expo 1.0.0-rc.0 → 1.0.0-rc.2

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/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ let node_fs = require("node:fs");
46
46
  let node_stream_promises = require("node:stream/promises");
47
47
  let node_stream = require("node:stream");
48
48
  let node_buffer = require("node:buffer");
49
+ let expo_config_js = require("expo/config.js");
49
50
  let uuidv7 = require("uuidv7");
50
51
  //#region ../../node_modules/.pnpm/is-plain-obj@4.1.0/node_modules/is-plain-obj/index.js
51
52
  function isPlainObject(value) {
@@ -6266,6 +6267,26 @@ const runExpoPrebuild = async ({ platform }) => {
6266
6267
  };
6267
6268
  //#endregion
6268
6269
  //#region src/expo.ts
6270
+ const getHotUpdaterExpoPluginProps = (plugins) => {
6271
+ const entry = plugins?.find((plugin) => Array.isArray(plugin) && (plugin[0] === "@hot-updater/expo" || plugin[0] === "@hot-updater/expo/app.plugin.js"));
6272
+ if (!entry || !Array.isArray(entry)) return void 0;
6273
+ const props = entry[1];
6274
+ return typeof props === "object" && props !== null ? props : void 0;
6275
+ };
6276
+ const getExpoFingerprintExtraSources = (cwd) => {
6277
+ const { exp } = (0, expo_config_js.getConfig)(cwd, { skipSDKVersionRequirement: true });
6278
+ const publicKeyPath = getHotUpdaterExpoPluginProps(exp.plugins)?.publicKeyPath;
6279
+ if (publicKeyPath === void 0) return [];
6280
+ if (typeof publicKeyPath !== "string" || !publicKeyPath.trim()) throw new Error("@hot-updater/expo publicKeyPath must be a non-empty path.");
6281
+ return [publicKeyPath];
6282
+ };
6283
+ const getExpoBundleSigningPublicKey = async (cwd) => {
6284
+ const { exp } = (0, expo_config_js.getConfig)(cwd, { skipSDKVersionRequirement: true });
6285
+ const publicKeyPath = getHotUpdaterExpoPluginProps(exp.plugins)?.publicKeyPath;
6286
+ if (publicKeyPath === void 0) return null;
6287
+ if (typeof publicKeyPath !== "string" || !publicKeyPath.trim()) throw new Error("@hot-updater/expo publicKeyPath must be a non-empty path.");
6288
+ return { publicKey: await (0, _hot_updater_cli_tools.readBundleSigningPublicKeyFile)(publicKeyPath, { cwd }) };
6289
+ };
6269
6290
  const isHermesEnabled = (cwd, platform) => {
6270
6291
  try {
6271
6292
  const appJsonPath = path.default.join(cwd, "app.json");
@@ -6333,9 +6354,13 @@ const expo = (config = {
6333
6354
  }) => ({ cwd }) => {
6334
6355
  const { outDir = "dist", sourcemap = false, resetCache = true } = config;
6335
6356
  return {
6336
- nativeBuild: { prebuild: async ({ platform }) => {
6337
- await runExpoPrebuild({ platform });
6338
- } },
6357
+ nativeBuild: {
6358
+ getBundleSigningPublicKey: () => getExpoBundleSigningPublicKey(cwd),
6359
+ getFingerprintExtraSources: async () => getExpoFingerprintExtraSources(cwd),
6360
+ prebuild: async ({ platform }) => {
6361
+ await runExpoPrebuild({ platform });
6362
+ }
6363
+ },
6339
6364
  build: async ({ platform }) => {
6340
6365
  const buildPath = path.default.join(cwd, outDir);
6341
6366
  await fs.default.promises.rm(buildPath, {
@@ -6361,3 +6386,5 @@ const expo = (config = {
6361
6386
  };
6362
6387
  //#endregion
6363
6388
  exports.expo = expo;
6389
+ exports.getExpoBundleSigningPublicKey = getExpoBundleSigningPublicKey;
6390
+ exports.getExpoFingerprintExtraSources = getExpoFingerprintExtraSources;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { BasePluginArgs, BuildPlugin, BuildPluginConfig } from "@hot-updater/plugin-core";
2
2
 
3
3
  //#region src/expo.d.ts
4
+ declare const getExpoFingerprintExtraSources: (cwd: string) => string[];
5
+ declare const getExpoBundleSigningPublicKey: (cwd: string) => Promise<{
6
+ readonly publicKey: string;
7
+ } | null>;
4
8
  interface ExpoPluginConfig extends BuildPluginConfig {
5
9
  /**
6
10
  * @default false
@@ -17,4 +21,4 @@ declare const expo: (config?: ExpoPluginConfig) => ({
17
21
  cwd
18
22
  }: BasePluginArgs) => BuildPlugin;
19
23
  //#endregion
20
- export { ExpoPluginConfig, expo };
24
+ export { ExpoPluginConfig, expo, getExpoBundleSigningPublicKey, getExpoFingerprintExtraSources };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { BasePluginArgs, BuildPlugin, BuildPluginConfig } from "@hot-updater/plugin-core";
2
2
 
3
3
  //#region src/expo.d.ts
4
+ declare const getExpoFingerprintExtraSources: (cwd: string) => string[];
5
+ declare const getExpoBundleSigningPublicKey: (cwd: string) => Promise<{
6
+ readonly publicKey: string;
7
+ } | null>;
4
8
  interface ExpoPluginConfig extends BuildPluginConfig {
5
9
  /**
6
10
  * @default false
@@ -17,4 +21,4 @@ declare const expo: (config?: ExpoPluginConfig) => ({
17
21
  cwd
18
22
  }: BasePluginArgs) => BuildPlugin;
19
23
  //#endregion
20
- export { ExpoPluginConfig, expo };
24
+ export { ExpoPluginConfig, expo, getExpoBundleSigningPublicKey, getExpoFingerprintExtraSources };
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import fs from "fs";
4
4
  import * as path$2 from "path";
5
5
  import path from "path";
6
6
  import { compileHermes } from "@hot-updater/bare";
7
- import { log, p } from "@hot-updater/cli-tools";
7
+ import { log, p, readBundleSigningPublicKeyFile } from "@hot-updater/cli-tools";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
10
10
  import { StringDecoder } from "node:string_decoder";
@@ -20,6 +20,7 @@ import { appendFileSync, createReadStream, createWriteStream, readFileSync, stat
20
20
  import { finished } from "node:stream/promises";
21
21
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
22
22
  import { Buffer as Buffer$1 } from "node:buffer";
23
+ import { getConfig } from "expo/config.js";
23
24
  import { uuidv7 } from "uuidv7";
24
25
  //#region \0rolldown/runtime.js
25
26
  var __create = Object.create;
@@ -6264,6 +6265,26 @@ const runExpoPrebuild = async ({ platform }) => {
6264
6265
  };
6265
6266
  //#endregion
6266
6267
  //#region src/expo.ts
6268
+ const getHotUpdaterExpoPluginProps = (plugins) => {
6269
+ const entry = plugins?.find((plugin) => Array.isArray(plugin) && (plugin[0] === "@hot-updater/expo" || plugin[0] === "@hot-updater/expo/app.plugin.js"));
6270
+ if (!entry || !Array.isArray(entry)) return void 0;
6271
+ const props = entry[1];
6272
+ return typeof props === "object" && props !== null ? props : void 0;
6273
+ };
6274
+ const getExpoFingerprintExtraSources = (cwd) => {
6275
+ const { exp } = getConfig(cwd, { skipSDKVersionRequirement: true });
6276
+ const publicKeyPath = getHotUpdaterExpoPluginProps(exp.plugins)?.publicKeyPath;
6277
+ if (publicKeyPath === void 0) return [];
6278
+ if (typeof publicKeyPath !== "string" || !publicKeyPath.trim()) throw new Error("@hot-updater/expo publicKeyPath must be a non-empty path.");
6279
+ return [publicKeyPath];
6280
+ };
6281
+ const getExpoBundleSigningPublicKey = async (cwd) => {
6282
+ const { exp } = getConfig(cwd, { skipSDKVersionRequirement: true });
6283
+ const publicKeyPath = getHotUpdaterExpoPluginProps(exp.plugins)?.publicKeyPath;
6284
+ if (publicKeyPath === void 0) return null;
6285
+ if (typeof publicKeyPath !== "string" || !publicKeyPath.trim()) throw new Error("@hot-updater/expo publicKeyPath must be a non-empty path.");
6286
+ return { publicKey: await readBundleSigningPublicKeyFile(publicKeyPath, { cwd }) };
6287
+ };
6267
6288
  const isHermesEnabled = (cwd, platform) => {
6268
6289
  try {
6269
6290
  const appJsonPath = path.join(cwd, "app.json");
@@ -6331,9 +6352,13 @@ const expo = (config = {
6331
6352
  }) => ({ cwd }) => {
6332
6353
  const { outDir = "dist", sourcemap = false, resetCache = true } = config;
6333
6354
  return {
6334
- nativeBuild: { prebuild: async ({ platform }) => {
6335
- await runExpoPrebuild({ platform });
6336
- } },
6355
+ nativeBuild: {
6356
+ getBundleSigningPublicKey: () => getExpoBundleSigningPublicKey(cwd),
6357
+ getFingerprintExtraSources: async () => getExpoFingerprintExtraSources(cwd),
6358
+ prebuild: async ({ platform }) => {
6359
+ await runExpoPrebuild({ platform });
6360
+ }
6361
+ },
6337
6362
  build: async ({ platform }) => {
6338
6363
  const buildPath = path.join(cwd, outDir);
6339
6364
  await fs.promises.rm(buildPath, {
@@ -6358,4 +6383,4 @@ const expo = (config = {
6358
6383
  };
6359
6384
  };
6360
6385
  //#endregion
6361
- export { expo };
6386
+ export { expo, getExpoBundleSigningPublicKey, getExpoFingerprintExtraSources };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/expo",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Hot Updater build and config plugin for Expo",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -28,16 +28,16 @@
28
28
  "@babel/core": "7.26.0",
29
29
  "@babel/types": "7.26.0",
30
30
  "uuidv7": "^1.0.2",
31
- "@hot-updater/bare": "1.0.0-rc.0",
32
- "@hot-updater/cli-tools": "1.0.0-rc.0",
33
- "@hot-updater/plugin-core": "1.0.0-rc.0"
31
+ "@hot-updater/cli-tools": "1.0.0-rc.1",
32
+ "@hot-updater/bare": "1.0.0-rc.1",
33
+ "@hot-updater/plugin-core": "1.0.0-rc.1"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20",
37
37
  "expo": "^50.0.0",
38
38
  "execa": "9.5.2",
39
39
  "vitest": "4.1.4",
40
- "hot-updater": "1.0.0-rc.0"
40
+ "hot-updater": "1.0.0-rc.2"
41
41
  },
42
42
  "exports": {
43
43
  ".": {
@@ -4,28 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getPublicKeyFromConfig = void 0;
7
- const node_crypto_1 = require("node:crypto");
8
- const promises_1 = require("node:fs/promises");
9
- const path_1 = __importDefault(require("path"));
10
7
  const config_plugins_1 = require("expo/config-plugins");
11
8
  const package_json_1 = __importDefault(require("../../package.json"));
12
9
  const transformers_1 = require("./transformers");
13
10
  const loadCliTools = () => import("@hot-updater/cli-tools");
14
11
  const loadHotUpdater = () => import("hot-updater");
15
- const canonicalizeRsaSpkiPublicKey = (publicKeyPem) => {
16
- const trimmed = publicKeyPem.trim();
17
- if (!trimmed.startsWith("-----BEGIN PUBLIC KEY-----") ||
18
- !trimmed.endsWith("-----END PUBLIC KEY-----") ||
19
- trimmed.includes("PRIVATE KEY")) {
20
- throw new Error("not spki");
21
- }
22
- const publicKey = (0, node_crypto_1.createPublicKey)(trimmed);
23
- if (publicKey.asymmetricKeyType !== "rsa" ||
24
- (publicKey.asymmetricKeyDetails?.modulusLength ?? 0) < 2048) {
25
- throw new Error("not rsa");
26
- }
27
- return publicKey.export({ format: "pem", type: "spki" }).toString().trim();
28
- };
29
12
  const ANDROID_META_DATA_KEYS = {
30
13
  channel: "com.hotupdater.CHANNEL",
31
14
  fingerprintHash: "com.hotupdater.FINGERPRINT_HASH",
@@ -60,46 +43,27 @@ const upsertAndroidMetaData = (application, name, value) => {
60
43
  application["meta-data"] = metaData;
61
44
  };
62
45
  let fingerprintCache = null;
63
- const getFingerprint = async () => {
64
- if (fingerprintCache) {
65
- return fingerprintCache;
46
+ const getFingerprint = async (publicKeyPath) => {
47
+ const cached = fingerprintCache;
48
+ if (cached && cached.publicKeyPath === publicKeyPath) {
49
+ return cached.value;
66
50
  }
67
51
  const { createFingerprintJSON, generateFingerprints } = await loadHotUpdater();
68
- fingerprintCache = await generateFingerprints();
69
- await createFingerprintJSON(fingerprintCache);
70
- return fingerprintCache;
52
+ const value = await generateFingerprints(publicKeyPath === undefined ? [] : [publicKeyPath]);
53
+ fingerprintCache = { publicKeyPath, value };
54
+ await createFingerprintJSON(value);
55
+ return value;
71
56
  };
72
- /** Uses public-key-only configuration, or the v0 local key sources when omitted. */
73
- const getPublicKeyFromConfig = async (signingConfig) => {
74
- if (!signingConfig ||
75
- ("enabled" in signingConfig && !signingConfig.enabled)) {
57
+ /** Reads the native trust anchor configured by the Expo app. */
58
+ const getPublicKeyFromConfig = async (publicKeyPath, projectRoot = process.cwd()) => {
59
+ if (publicKeyPath === undefined)
76
60
  return null;
77
- }
78
- // Retain the v0 EAS key source only for local configs without an explicit pin.
79
- if ("enabled" in signingConfig && signingConfig.publicKeyPath === undefined) {
80
- const envPrivateKey = process.env.HOT_UPDATER_PRIVATE_KEY;
81
- if (envPrivateKey) {
82
- try {
83
- const pem = envPrivateKey.includes("-----BEGIN")
84
- ? envPrivateKey
85
- : await (0, promises_1.readFile)(path_1.default.resolve(process.cwd(), envPrivateKey), "utf8");
86
- return canonicalizeRsaSpkiPublicKey((0, node_crypto_1.createPublicKey)((0, node_crypto_1.createPrivateKey)(pem))
87
- .export({ format: "pem", type: "spki" })
88
- .toString());
89
- }
90
- catch {
91
- // As in v0, try the configured local files if the environment source fails.
92
- }
93
- }
94
- }
95
61
  try {
96
- const { getBundleSigningPublicKey } = await loadCliTools();
97
- return ((await getBundleSigningPublicKey(signingConfig, { cwd: process.cwd() }))?.trim() ?? null);
62
+ const { readBundleSigningPublicKeyFile } = await loadCliTools();
63
+ return (await readBundleSigningPublicKeyFile(publicKeyPath, { cwd: projectRoot })).trim();
98
64
  }
99
65
  catch {
100
- throw new Error(signingConfig.publicKeyPath !== undefined
101
- ? "[hot-updater] Failed to load publicKeyPath for bundle signing."
102
- : "[hot-updater] Failed to load public key for bundle signing.");
66
+ throw new Error("[hot-updater] Failed to load publicKeyPath for bundle signing.");
103
67
  }
104
68
  };
105
69
  exports.getPublicKeyFromConfig = getPublicKeyFromConfig;
@@ -138,9 +102,9 @@ const withHotUpdaterConfigAsync = (props) => (config) => {
138
102
  return hotUpdaterConfigPromise;
139
103
  };
140
104
  let publicKeyPromise;
141
- const getPublicKey = async () => {
105
+ const getPublicKey = async (projectRoot) => {
142
106
  if (!publicKeyPromise) {
143
- publicKeyPromise = getHotUpdaterConfig().then(($config) => (0, exports.getPublicKeyFromConfig)($config.signing));
107
+ publicKeyPromise = (0, exports.getPublicKeyFromConfig)(props.publicKeyPath, projectRoot);
144
108
  }
145
109
  return publicKeyPromise;
146
110
  };
@@ -150,11 +114,11 @@ const withHotUpdaterConfigAsync = (props) => (config) => {
150
114
  let fingerprintHash = null;
151
115
  const hotUpdaterConfig = await getHotUpdaterConfig();
152
116
  if (hotUpdaterConfig.updateStrategy !== "appVersion") {
153
- const fingerprint = await getFingerprint();
117
+ const fingerprint = await getFingerprint(props.publicKeyPath);
154
118
  fingerprintHash = fingerprint.ios.hash;
155
119
  }
156
120
  // Load public key if signing is enabled
157
- const publicKey = await getPublicKey();
121
+ const publicKey = await getPublicKey(cfg.modRequest.projectRoot);
158
122
  cfg.modResults.HOT_UPDATER_CHANNEL = channel;
159
123
  if (fingerprintHash) {
160
124
  cfg.modResults.HOT_UPDATER_FINGERPRINT_HASH = fingerprintHash;
@@ -172,11 +136,11 @@ const withHotUpdaterConfigAsync = (props) => (config) => {
172
136
  let fingerprintHash = null;
173
137
  const hotUpdaterConfig = await getHotUpdaterConfig();
174
138
  if (hotUpdaterConfig.updateStrategy !== "appVersion") {
175
- const fingerprint = await getFingerprint();
139
+ const fingerprint = await getFingerprint(props.publicKeyPath);
176
140
  fingerprintHash = fingerprint.android.hash;
177
141
  }
178
142
  // Load public key if signing is enabled
179
- const publicKey = await getPublicKey();
143
+ const publicKey = await getPublicKey(cfg.modRequest.projectRoot);
180
144
  const application = cfg.modResults.manifest.application?.[0];
181
145
  if (!application) {
182
146
  return cfg;
@@ -186,7 +150,7 @@ const withHotUpdaterConfigAsync = (props) => (config) => {
186
150
  upsertAndroidMetaData(application, ANDROID_META_DATA_KEYS.fingerprintHash, fingerprintHash);
187
151
  }
188
152
  if (publicKey) {
189
- upsertAndroidMetaData(application, ANDROID_META_DATA_KEYS.publicKey, publicKey);
153
+ upsertAndroidMetaData(application, ANDROID_META_DATA_KEYS.publicKey, config_plugins_1.XML.escapeAndroidString(publicKey));
190
154
  }
191
155
  else {
192
156
  removeAndroidMetaData(application, ANDROID_META_DATA_KEYS.publicKey);