react-native-store-version 1.4.1 → 2.0.0-beta.1

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.
Files changed (149) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +511 -51
  3. package/dist/checkVersion.d.ts +73 -0
  4. package/dist/checkVersion.d.ts.map +1 -0
  5. package/dist/checkVersion.js +193 -0
  6. package/dist/cli/check.js +38 -0
  7. package/dist/cli/checks/ids.js +111 -0
  8. package/dist/cli/checks/installed.js +51 -0
  9. package/dist/cli/checks/peers.js +31 -0
  10. package/dist/cli/checks/rollout.js +141 -0
  11. package/dist/cli/checks/store.js +71 -0
  12. package/dist/cli/checks/versions.js +83 -0
  13. package/dist/cli/index.js +43 -0
  14. package/dist/cli/main.js +182 -0
  15. package/dist/cli/output.js +77 -0
  16. package/dist/cli/packages.js +40 -0
  17. package/dist/cli/project/android.js +60 -0
  18. package/dist/cli/project/expo.js +194 -0
  19. package/dist/cli/project/files.js +42 -0
  20. package/dist/cli/project/gradle.js +145 -0
  21. package/dist/cli/project/index.js +10 -0
  22. package/dist/cli/project/ios.js +190 -0
  23. package/dist/cli/project/jsx.js +327 -0
  24. package/dist/cli/project/plist.js +166 -0
  25. package/dist/cli/project/sources.js +86 -0
  26. package/dist/cli/result.js +12 -0
  27. package/dist/compare.d.ts +31 -0
  28. package/dist/compare.d.ts.map +1 -0
  29. package/dist/compare.js +68 -0
  30. package/dist/detect.d.ts +68 -0
  31. package/dist/detect.d.ts.map +1 -0
  32. package/dist/detect.js +103 -0
  33. package/dist/errors.d.ts +33 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +23 -0
  36. package/dist/fetchers/android.d.ts +53 -0
  37. package/dist/fetchers/android.d.ts.map +1 -0
  38. package/dist/fetchers/android.js +91 -0
  39. package/dist/fetchers/custom.d.ts +29 -0
  40. package/dist/fetchers/custom.d.ts.map +1 -0
  41. package/dist/fetchers/custom.js +41 -0
  42. package/dist/fetchers/ios.d.ts +61 -0
  43. package/dist/fetchers/ios.d.ts.map +1 -0
  44. package/dist/fetchers/ios.js +79 -0
  45. package/dist/index.d.ts +15 -14
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +9 -58
  48. package/dist/openStore.d.ts +29 -0
  49. package/dist/openStore.d.ts.map +1 -0
  50. package/dist/openStore.js +46 -0
  51. package/dist/policy.d.ts +75 -0
  52. package/dist/policy.d.ts.map +1 -0
  53. package/dist/policy.js +128 -0
  54. package/dist/react/DefaultForceModal.d.ts +26 -0
  55. package/dist/react/DefaultForceModal.d.ts.map +1 -0
  56. package/dist/react/DefaultForceModal.js +58 -0
  57. package/dist/react/DefaultWarningSheet.d.ts +26 -0
  58. package/dist/react/DefaultWarningSheet.d.ts.map +1 -0
  59. package/dist/react/DefaultWarningSheet.js +66 -0
  60. package/dist/react/StoreVersionModal.d.ts +52 -0
  61. package/dist/react/StoreVersionModal.d.ts.map +1 -0
  62. package/dist/react/StoreVersionModal.js +250 -0
  63. package/dist/react/StoreVersionProvider.d.ts +51 -0
  64. package/dist/react/StoreVersionProvider.d.ts.map +1 -0
  65. package/dist/react/StoreVersionProvider.js +69 -0
  66. package/dist/react/defaultUi.d.ts +72 -0
  67. package/dist/react/defaultUi.d.ts.map +1 -0
  68. package/dist/react/defaultUi.js +118 -0
  69. package/dist/react/engine.d.ts +146 -0
  70. package/dist/react/engine.d.ts.map +1 -0
  71. package/dist/react/engine.js +541 -0
  72. package/dist/react/texts.d.ts +43 -0
  73. package/dist/react/texts.d.ts.map +1 -0
  74. package/dist/react/texts.js +57 -0
  75. package/dist/react/useIsomorphicLayoutEffect.d.ts +3 -0
  76. package/dist/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
  77. package/dist/react/useIsomorphicLayoutEffect.js +6 -0
  78. package/dist/react/useStoreVersion.d.ts +121 -0
  79. package/dist/react/useStoreVersion.d.ts.map +1 -0
  80. package/dist/react/useStoreVersion.js +23 -0
  81. package/dist/request.d.ts +78 -0
  82. package/dist/request.d.ts.map +1 -0
  83. package/dist/request.js +227 -0
  84. package/dist/resolve.d.ts +98 -0
  85. package/dist/resolve.d.ts.map +1 -0
  86. package/dist/resolve.js +218 -0
  87. package/dist/texts/ja.d.ts +8 -0
  88. package/dist/texts/ja.d.ts.map +1 -0
  89. package/dist/texts/ja.js +18 -0
  90. package/dist/vendor/compare-versions.d.ts +52 -0
  91. package/dist/vendor/compare-versions.d.ts.map +1 -0
  92. package/dist/vendor/compare-versions.js +111 -0
  93. package/package.json +84 -45
  94. package/src/checkVersion.ts +273 -0
  95. package/src/cli/check.ts +59 -0
  96. package/src/cli/checks/ids.ts +163 -0
  97. package/src/cli/checks/installed.ts +75 -0
  98. package/src/cli/checks/peers.ts +34 -0
  99. package/src/cli/checks/rollout.ts +183 -0
  100. package/src/cli/checks/store.ts +105 -0
  101. package/src/cli/checks/versions.ts +108 -0
  102. package/src/cli/index.ts +51 -0
  103. package/src/cli/main.ts +229 -0
  104. package/src/cli/output.ts +122 -0
  105. package/src/cli/packages.ts +43 -0
  106. package/src/cli/project/android.ts +96 -0
  107. package/src/cli/project/expo.ts +232 -0
  108. package/src/cli/project/files.ts +56 -0
  109. package/src/cli/project/gradle.ts +181 -0
  110. package/src/cli/project/index.ts +18 -0
  111. package/src/cli/project/ios.ts +284 -0
  112. package/src/cli/project/jsx.ts +354 -0
  113. package/src/cli/project/plist.ts +179 -0
  114. package/src/cli/project/sources.ts +118 -0
  115. package/src/cli/result.ts +34 -0
  116. package/src/compare.ts +101 -0
  117. package/src/detect.ts +178 -0
  118. package/src/errors.ts +63 -0
  119. package/src/fetchers/android.ts +136 -0
  120. package/src/fetchers/custom.ts +84 -0
  121. package/src/fetchers/ios.ts +160 -0
  122. package/src/index.ts +37 -0
  123. package/src/openStore.ts +74 -0
  124. package/src/policy.ts +223 -0
  125. package/src/react/DefaultForceModal.tsx +100 -0
  126. package/src/react/DefaultWarningSheet.tsx +115 -0
  127. package/src/react/StoreVersionModal.tsx +365 -0
  128. package/src/react/StoreVersionProvider.tsx +149 -0
  129. package/src/react/defaultUi.tsx +205 -0
  130. package/src/react/engine.ts +768 -0
  131. package/src/react/texts.ts +119 -0
  132. package/src/react/useIsomorphicLayoutEffect.ts +8 -0
  133. package/src/react/useStoreVersion.ts +153 -0
  134. package/src/request.ts +328 -0
  135. package/src/resolve.ts +388 -0
  136. package/src/texts/ja.ts +26 -0
  137. package/src/vendor/compare-versions.ts +128 -0
  138. package/texts/ja.d.ts +2 -0
  139. package/texts/ja.js +3 -0
  140. package/.eslintrc.js +0 -51
  141. package/.github/FUNDING.yml +0 -3
  142. package/.github/workflows/pr.yml +0 -24
  143. package/.github/workflows/release.yml +0 -31
  144. package/.prettierrc.js +0 -6
  145. package/babel.config.js +0 -3
  146. package/dist/android.d.ts +0 -1
  147. package/dist/android.js +0 -17
  148. package/dist/ios.d.ts +0 -1
  149. package/dist/ios.js +0 -18
@@ -0,0 +1,73 @@
1
+ import { type CompareDetail, type CompareResult } from './compare.ts';
2
+ /** {@link checkVersion} の引数。v2 ではすべて省略できる */
3
+ export type CheckVersionParams = {
4
+ /**
5
+ * アプリのバージョン。必ずネイティブのバージョン(iOS の `CFBundleShortVersionString` / Android の `versionName`)を渡す。
6
+ * `Constants.expoConfig.version` や `package.json` の値は JS バンドルに含まれ、OTA 更新で変わってしまう
7
+ * (ネイティブは古いのに「更新済み」と誤判定する)ので使わないこと。
8
+ *
9
+ * 省略(空文字も含む)すると、iOS / Android では expo-application の `nativeApplicationVersion` →
10
+ * react-native-device-info の `getVersion()` の順に取る。取れなければ `local version is not set.` を throw する。
11
+ * Expo Go の中では Expo Go 自体のバージョンが取れてしまうので、Expo Go で試すときは明示的に渡すこと
12
+ */
13
+ version?: string;
14
+ /**
15
+ * App Store の URL(`https://apps.apple.com/jp/app/github/id1477376905` など。`id<数字>` から App ID を取る)。
16
+ * 省略(空文字も含む)すると、iOS ではアプリの bundleId(expo-application の `applicationId` →
17
+ * react-native-device-info の `getBundleId()`)で引く。取れなければ `iosStoreURL is not set.` を throw する。
18
+ * Expo Go の中で省略すると Expo Go 自体(`host.exp.Exponent`)を引いてしまうので、Expo Go で試すときは URL も明示すること
19
+ */
20
+ iosStoreURL?: string;
21
+ /**
22
+ * Google Play の URL(`https://play.google.com/store/apps/details?id=<package>`)。
23
+ * 省略(空文字も含む)すると、Android ではアプリの package name(expo-application の `applicationId` →
24
+ * react-native-device-info の `getBundleId()`)から URL を組み立てる。取れなければ `androidStoreURL is not set.` を throw する。
25
+ * Expo Go の中で省略すると Expo Go 自体(`host.exp.exponent`)を引いてしまうので、Expo Go で試すときは URL も明示すること
26
+ */
27
+ androidStoreURL?: string;
28
+ /** iTunes Lookup の国コード(iOS のみ)。省略(空文字も含む)すると `jp`(v1 と同じ) */
29
+ country?: string;
30
+ /**
31
+ * ストアへの問い合わせのタイムアウト(ms)。省略するとタイムアウトしない(v1 と同じ)。
32
+ * 正の有限の数のときだけ有効(0・負の数・NaN・Infinity はタイムアウトなし)。
33
+ * 時間切れのときは `StoreVersionError`(`code: 'TIMEOUT'`)を throw する
34
+ */
35
+ timeoutMs?: number;
36
+ };
37
+ /** {@link checkVersion} の戻り値(v1 と同じ形) */
38
+ export type CheckVersionResponse = {
39
+ /** アプリのバージョン(渡した値か、自動取得した値) */
40
+ local: string;
41
+ /** ストアのバージョン */
42
+ remote: string;
43
+ /** `remote` が新しければ `new`、古ければ `old`、同じなら `equal` */
44
+ result: CompareResult;
45
+ detail: CompareDetail;
46
+ };
47
+ /**
48
+ * v1 互換のバージョン確認。ストアのバージョンを取得して、`version` と比べた結果を返す。
49
+ *
50
+ * 引数をすべて渡した場合は v1 と同じ結果・同じエラーメッセージになる(`country` の既定も `jp`)。ただし次の 2 点は v1 と違う:
51
+ * - バージョンの比較は compare-versions 6.1.1 の挙動(プレリリースの一部や、先頭の `^` / `~` / `=` / 大文字の `V` などの扱い)
52
+ * - Android の版は v2 の抽出(構造に錨を下ろしたパターン → v1 のパターンの順)で取る。v1 では取れなかった
53
+ * `5` / `1.0.0-beta.1` のような版も返し、版の位置より前にある古い版は拾わない。版の位置が空なら `can't get android app version.`
54
+ *
55
+ * 省略した引数は、iOS / Android では自動取得する(expo-application か react-native-device-info が必要)。
56
+ * `Platform.OS` が ios / android 以外(web など)のときは v1 と同じく Android の経路で `androidStoreURL` を取得し、自動取得はしない。
57
+ *
58
+ * 失敗は `Error` で reject する(メッセージは v1 と同じ)。`timeoutMs` の時間切れだけは `StoreVersionError`(`code: 'TIMEOUT'`)。
59
+ * Expo Go や web でも判定を省かない(v1 と同じ)。Expo Go の中で version / URL を省略すると Expo Go 自体の値を使ってしまうので、
60
+ * Expo Go で試すときはすべて明示すること。v2 の Provider / hook と違い、起動を止めない仕組みは持たないので、
61
+ * 呼び出し側で catch すること
62
+ *
63
+ * @example
64
+ * import { checkVersion } from 'react-native-store-version';
65
+ *
66
+ * const { result } = await checkVersion({
67
+ * version: '1.0.0', // ネイティブのバージョン
68
+ * iosStoreURL: 'https://apps.apple.com/jp/app/github/id1477376905',
69
+ * androidStoreURL: 'https://play.google.com/store/apps/details?id=com.github.android',
70
+ * });
71
+ */
72
+ export declare const checkVersion: (params?: CheckVersionParams) => Promise<CheckVersionResponse>;
73
+ //# sourceMappingURL=checkVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkVersion.d.ts","sourceRoot":"","sources":["../src/checkVersion.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,aAAa,EAGnB,MAAM,cAAc,CAAC;AAOtB,6CAA6C;AAC7C,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,yCAAyC;AACzC,MAAM,MAAM,oBAAoB,GAAG;IACjC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAgHF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,YAAY,YACf,kBAAkB,KACzB,OAAO,CAAC,oBAAoB,CAoE9B,CAAC"}
@@ -0,0 +1,193 @@
1
+ // v1 互換の checkVersion。取得の層(request / fetcher / detect)の上に載せ替えたもの。
2
+ // 同じ入力(v1 で必須だった引数をすべて渡した場合)なら v1 と同じ戻り値・エラーメッセージを返す。
3
+ // 例外は compare-versions 6.1.1 の比較の差と、Android の版の抽出が v2 の抽出(構造に錨を下ろしたパターン → v1 のパターンの順)になること。
4
+ // v1 で throw していた「引数が無い」場面だけ、自動取得に回す。
5
+ // v2 の判定の層(resolve)は使わない。policy / getter が無く、エラーを v1 のメッセージに戻すには取得の層を直接呼ぶ必要があり、
6
+ // Expo Go や ios / android 以外の OS の skip(v2 の判定)も checkVersion では行わない(v1 と同じく判定する)ため
7
+ import { Platform } from 'react-native';
8
+ import { compareVersion, toDetail, } from './compare.js';
9
+ import { nativeAppId, nativeVersion } from './detect.js';
10
+ import { StoreVersionError } from './errors.js';
11
+ import { extractPlayVersion, playStoreUrl } from './fetchers/android.js';
12
+ import { appIdFromStoreUrl, lookupIOS } from './fetchers/ios.js';
13
+ import { failedResponseStatusOf, requestText } from './request.js';
14
+ /*
15
+ * v1 のエラーメッセージへの対応表(v1 の src/index.ts / ios.ts / android.ts の全経路)。
16
+ * 取得の段階で起きたものは、v1 と同じく `new Error(e.message)`(Error でなければ `can't get ${Platform.OS} version`)で
17
+ * 投げ直す。v2 の層の `StoreVersionError` は {@link toV1Error} で v1 のメッセージに戻す。
18
+ *
19
+ * | 経路(v1) | v1 のメッセージ | v2 の層での扱い | checkVersion が投げるもの |
20
+ * |-----------------------------------------------------|---------------------------------------|---------------------------------------------------------|---------------------------------------|
21
+ * | `version` が falsy | `local version is not set.` | ios / android は自動取得。取れなければ v1 と同じ | 同じ |
22
+ * | iOS で `iosStoreURL` が falsy | `iosStoreURL is not set.` | bundleId を自動取得。取れなければ v1 と同じ | 同じ |
23
+ * | Android で `androidStoreURL` が falsy | `androidStoreURL is not set.` | package name を自動取得。取れなければ v1 と同じ | 同じ |
24
+ * | iOS の URL が正規表現に合わない | `iosStoreURL is invalid.` | `appIdFromStoreUrl` が null | 同じ |
25
+ * | fetch / `text()` が Error で reject | 元のメッセージ | `requestText` の `NETWORK`(cause に元のエラー) | `new Error(cause.message)` |
26
+ * | fetch / `text()` が Error でないもので reject | `can't get ${Platform.OS} version` | `NETWORK`(cause が Error でない) | 同じ |
27
+ * | iOS の本文が JSON でない(status は見ない) | `JSON.parse` のメッセージ | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause に SyntaxError) | `new Error(SyntaxError の message)` |
28
+ * | iOS の `results` が無い・空(`null` の応答も) | `appID(<id>) is not released.` | URL: v1 と同じ処理 / bundleId: `NOT_FOUND` | 同じ(bundleId は `appID(<bundleId>)`)|
29
+ * | iOS の `results[0]` が null など(TypeError) | TypeError のメッセージ | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
30
+ * | iOS の `results[0].version` が文字列でない | (そのまま remote にして比較で throw) | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
31
+ * | iOS の 200 以外 | (見ない。本文で上のどれかになる) | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
32
+ * | Android の URL が正規表現に合わない(ios 以外で省略も) | `androidStoreURL is invalid.` | checkVersion で検証 | 同じ |
33
+ * | Android の 200 以外 | `androidStoreURL is invalid.` | `requestText` の status を checkVersion で見る | 同じ(package name の経路も) |
34
+ * | Android の 200 以外で本文の読み込みが失敗 | `androidStoreURL is invalid.`(v1 は本文を読まない) | `NETWORK`(`failedResponseStatusOf` で status が分かる) | 同じ(package name の経路も) |
35
+ * | Android の 200 で本文の読み込みが失敗 | 元のメッセージ | `NETWORK`(cause に元のエラー) | `new Error(cause.message)` |
36
+ * | Android の版が抽出できない | `can't get android app version.` | `extractPlayVersion` の `PARSE_FAILED` / `INVALID_VERSION`(Varies・比較できない版) | 同じ |
37
+ * | 比較できない local(取得の後) | compare-versions のメッセージ | `compareVersion`(try の外。包まない) | 同じ |
38
+ * | (v1 に無い)`timeoutMs` の時間切れ | — | `TIMEOUT` | `StoreVersionError('TIMEOUT')` そのまま |
39
+ *
40
+ * iOS の URL 方式は `lookupIOS` を使わず `requestText` で v1 と同じ処理をする。`lookupIOS` は 200 以外を `NETWORK` にし、
41
+ * `country` を encode し、`results[0].version` が文字列でないと `NETWORK` にするので、v1 の結果(本文の JSON を読む)を
42
+ * 再現できないため。Android は 200 以外のメッセージ(`androidStoreURL is invalid.`)を通信の失敗と分けるために、
43
+ * `fetchAndroidVersion` ではなく `requestText` と `extractPlayVersion`(v2 の抽出)を使う。
44
+ * Android の抽出は URL を渡した経路でも v2 のもの(v1 と同じ結果を保つ方針の例外)で、v1 と結果が分かれるのは、版の位置に v1 のパターン
45
+ * (数字とドットだけ)に合わない比較できる版(`5` / `1.0.0-beta.1` / `v2.1` など)がある場合(v1 は抽出できず throw)、
46
+ * 版の位置より前に v1 のパターン(`[[["x.y.z"]],`)の形の別の要素がある場合(v1 はそれを拾う)、版の位置が空なのにほかの場所に
47
+ * v1 のパターンが当たる場合(v1 はそれを拾う)。2026-11-08 の実ページでは v1 のパターンに当たるのは版の位置だけで、v1 と結果は同じだった
48
+ */
49
+ /** v1 の `getAndroidVersion` の URL の検証 */
50
+ const V1_ANDROID_STORE_URL = /^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[0-9a-zA-Z.]+/;
51
+ /** 取得の段階で起きたエラーを v1 のメッセージに戻す(上の表) */
52
+ const toV1Error = (error, os, appId) => {
53
+ if (error instanceof StoreVersionError) {
54
+ switch (error.code) {
55
+ // v1 にはタイムアウトが無い(`timeoutMs` を渡したときだけ起きる)ので、code で区別できるようにそのまま投げる
56
+ case 'TIMEOUT':
57
+ return error;
58
+ // 通信の失敗は、v1 と同じく元のエラーのメッセージで投げ直す
59
+ case 'NETWORK':
60
+ return error.cause instanceof Error
61
+ ? new Error(error.cause.message)
62
+ : new Error(`can't get ${os} version`);
63
+ case 'NOT_FOUND':
64
+ return new Error(`appID(${appId}) is not released.`);
65
+ case 'PARSE_FAILED':
66
+ case 'INVALID_VERSION':
67
+ return new Error("can't get android app version.");
68
+ }
69
+ }
70
+ return error instanceof Error
71
+ ? new Error(error.message)
72
+ : new Error(`can't get ${os} version`);
73
+ };
74
+ /** v1 の `getIOSVersion` と同じ処理(status は見ず、本文を JSON として読む)。取得だけ `requestText` に載せる */
75
+ const iosVersionByStoreUrl = async (appId, country, timeoutMs) => {
76
+ // v1 と同じく country は encode しない。タイムスタンプと no-cache は requestText が付ける
77
+ const { body } = await requestText(`https://itunes.apple.com/lookup?id=${appId}&country=${country}`, { noCache: true, timeoutMs });
78
+ const response = JSON.parse(body);
79
+ // v1 の `!response || !response.results || ...` と同じ(null / false / 0 などの応答も未リリース扱い)
80
+ if (!response?.results || response.results.length === 0) {
81
+ throw new Error(`appID(${appId}) is not released.`);
82
+ }
83
+ return response.results[0].version;
84
+ };
85
+ /** Play Store のページを取得して版を返す。200 以外は v1 と同じく `androidStoreURL is invalid.` */
86
+ const androidVersionByUrl = async (url, timeoutMs) => {
87
+ let response;
88
+ try {
89
+ response = await requestText(url, { timeoutMs });
90
+ }
91
+ catch (error) {
92
+ // v1 は status を先に見て、200 以外なら本文を読まない。v2 の request は本文を読むので、その読み込みに失敗しても
93
+ // 200 以外なら v1 と同じメッセージにする
94
+ const status = failedResponseStatusOf(error);
95
+ if (status !== undefined && status !== 200) {
96
+ throw new Error('androidStoreURL is invalid.');
97
+ }
98
+ throw error;
99
+ }
100
+ if (response.status !== 200) {
101
+ throw new Error('androidStoreURL is invalid.');
102
+ }
103
+ return extractPlayVersion(response.body);
104
+ };
105
+ /**
106
+ * v1 互換のバージョン確認。ストアのバージョンを取得して、`version` と比べた結果を返す。
107
+ *
108
+ * 引数をすべて渡した場合は v1 と同じ結果・同じエラーメッセージになる(`country` の既定も `jp`)。ただし次の 2 点は v1 と違う:
109
+ * - バージョンの比較は compare-versions 6.1.1 の挙動(プレリリースの一部や、先頭の `^` / `~` / `=` / 大文字の `V` などの扱い)
110
+ * - Android の版は v2 の抽出(構造に錨を下ろしたパターン → v1 のパターンの順)で取る。v1 では取れなかった
111
+ * `5` / `1.0.0-beta.1` のような版も返し、版の位置より前にある古い版は拾わない。版の位置が空なら `can't get android app version.`
112
+ *
113
+ * 省略した引数は、iOS / Android では自動取得する(expo-application か react-native-device-info が必要)。
114
+ * `Platform.OS` が ios / android 以外(web など)のときは v1 と同じく Android の経路で `androidStoreURL` を取得し、自動取得はしない。
115
+ *
116
+ * 失敗は `Error` で reject する(メッセージは v1 と同じ)。`timeoutMs` の時間切れだけは `StoreVersionError`(`code: 'TIMEOUT'`)。
117
+ * Expo Go や web でも判定を省かない(v1 と同じ)。Expo Go の中で version / URL を省略すると Expo Go 自体の値を使ってしまうので、
118
+ * Expo Go で試すときはすべて明示すること。v2 の Provider / hook と違い、起動を止めない仕組みは持たないので、
119
+ * 呼び出し側で catch すること
120
+ *
121
+ * @example
122
+ * import { checkVersion } from 'react-native-store-version';
123
+ *
124
+ * const { result } = await checkVersion({
125
+ * version: '1.0.0', // ネイティブのバージョン
126
+ * iosStoreURL: 'https://apps.apple.com/jp/app/github/id1477376905',
127
+ * androidStoreURL: 'https://play.google.com/store/apps/details?id=com.github.android',
128
+ * });
129
+ */
130
+ export const checkVersion = async (params = {}) => {
131
+ const os = Platform.OS;
132
+ // 自動取得は ios / android だけ(ほかの OS は v1 と同じ挙動のまま。ネイティブの値がストアの版と対応しないため)
133
+ const autoDetect = os === 'ios' || os === 'android';
134
+ // v1 が `local version is not set.` を投げていた条件(falsy)のときだけ自動取得に回す
135
+ const version = params.version || (autoDetect ? nativeVersion() : null);
136
+ if (!version) {
137
+ throw new Error('local version is not set.');
138
+ }
139
+ let fetchRemote;
140
+ // NOT_FOUND を v1 の `appID(<id>) is not released.` に戻すときの id
141
+ let appId = '';
142
+ if (os === 'ios') {
143
+ const country = params.country || 'jp';
144
+ if (params.iosStoreURL) {
145
+ const storeUrl = params.iosStoreURL;
146
+ fetchRemote = async () => {
147
+ const id = appIdFromStoreUrl(storeUrl);
148
+ if (id === null) {
149
+ throw new Error('iosStoreURL is invalid.');
150
+ }
151
+ return iosVersionByStoreUrl(id, country, params.timeoutMs);
152
+ };
153
+ }
154
+ else {
155
+ const bundleId = nativeAppId();
156
+ if (bundleId === null) {
157
+ throw new Error('iosStoreURL is not set.');
158
+ }
159
+ appId = bundleId;
160
+ fetchRemote = async () => (await lookupIOS({ bundleId }, country, {
161
+ timeoutMs: params.timeoutMs,
162
+ })).version;
163
+ }
164
+ }
165
+ else if (os === 'android' && !params.androidStoreURL) {
166
+ const packageName = nativeAppId();
167
+ if (packageName === null) {
168
+ throw new Error('androidStoreURL is not set.');
169
+ }
170
+ fetchRemote = () => androidVersionByUrl(playStoreUrl(packageName), params.timeoutMs);
171
+ }
172
+ else {
173
+ // v1 は ios 以外をすべて Android の経路に進める(web などで省略された場合は `androidStoreURL is invalid.`)。
174
+ // v1 の `getAndroidVersion(storeURL = '')` の既定引数と同じく、undefined のときだけ空文字にする
175
+ const storeUrl = params.androidStoreURL === undefined ? '' : params.androidStoreURL;
176
+ fetchRemote = async () => {
177
+ if (!storeUrl.match(V1_ANDROID_STORE_URL)) {
178
+ throw new Error('androidStoreURL is invalid.');
179
+ }
180
+ return androidVersionByUrl(storeUrl, params.timeoutMs);
181
+ };
182
+ }
183
+ let remote;
184
+ try {
185
+ remote = await fetchRemote();
186
+ }
187
+ catch (error) {
188
+ throw toV1Error(error, os, appId);
189
+ }
190
+ // v1 と同じく比較は try の外(比較できない local は compare-versions のメッセージがそのまま出る)
191
+ const result = compareVersion(version, remote);
192
+ return { local: version, remote, result, detail: toDetail(result) };
193
+ };
@@ -0,0 +1,38 @@
1
+ // `check` サブコマンドのチェックを順に回す
2
+ import { resolveIds } from './checks/ids.js';
3
+ import { checkInstalled } from './checks/installed.js';
4
+ import { checkPeers } from './checks/peers.js';
5
+ import { checkRollout } from './checks/rollout.js';
6
+ import { checkStore } from './checks/store.js';
7
+ import { checkVersions } from './checks/versions.js';
8
+ import { readProject } from './project/index.js';
9
+ /** アプリの構成(Expo の設定・ネイティブのプロジェクト)を使うチェック */
10
+ const PROJECT_CHECKS = ['C2', 'C3', 'C4', 'C8'];
11
+ /** チェックを C1 → C2 → C3 → C4 → C7 → C8 の順に回す。`ignore` にあるものは回さない */
12
+ export const runChecks = async ({ cwd, country, ignore, }) => {
13
+ const enabled = (id) => !ignore.has(id);
14
+ const entries = [];
15
+ // C3(ID で引く)・C8(Android か)は C2 が特定した ID を使うので、`--ignore C2` でも ID の特定はする(C2 の行だけを出さない)。
16
+ // Expo の設定の評価(アプリの app.config.* を動かす)は、どれも使わないときはしない
17
+ const project = PROJECT_CHECKS.some(enabled) ? readProject(cwd) : null;
18
+ const ids = project === null ? null : resolveIds(project);
19
+ if (enabled('C1')) {
20
+ entries.push(...checkInstalled(cwd));
21
+ }
22
+ if (ids !== null && enabled('C2')) {
23
+ entries.push(...ids.entries);
24
+ }
25
+ if (ids !== null && enabled('C3')) {
26
+ entries.push(...(await checkStore({ ios: ids.ios, android: ids.android }, { country })));
27
+ }
28
+ if (project !== null && enabled('C4')) {
29
+ entries.push(...checkVersions(project));
30
+ }
31
+ if (enabled('C7')) {
32
+ entries.push(...checkPeers(cwd));
33
+ }
34
+ if (ids !== null && enabled('C8')) {
35
+ entries.push(...checkRollout(cwd, ids.android));
36
+ }
37
+ return entries;
38
+ };
@@ -0,0 +1,111 @@
1
+ const IOS = {
2
+ name: 'iOS',
3
+ idName: 'bundleId',
4
+ dir: 'ios/',
5
+ expoKey: 'ios.bundleIdentifier',
6
+ };
7
+ const ANDROID = {
8
+ name: 'Android',
9
+ idName: 'package',
10
+ dir: 'android/',
11
+ expoKey: 'android.package',
12
+ };
13
+ /**
14
+ * 1 つのプラットフォームの ID を決める。ネイティブのディレクトリがあればその値(Expo の設定とずれていれば info)、
15
+ * ネイティブから特定できなければ Expo の設定の値。ネイティブのディレクトリが無ければ(CNG・bare RN で片方だけなど)Expo の設定の値
16
+ */
17
+ const resolvePlatform = (spec, project, expoValue, native) => {
18
+ const { name, idName, dir, expoKey } = spec;
19
+ const expoFile = project.expo?.config?.file;
20
+ const fromExpo = expoValue !== null && expoFile !== undefined
21
+ ? `${expoKey} in ${expoFile}`
22
+ : null;
23
+ if (native?.value != null) {
24
+ const notes = fromExpo !== null && expoValue !== native.value
25
+ ? [
26
+ `the ${name} ${idName} differs between ${native.label} "${native.value}" (${native.file}) and ${expoKey} "${expoValue}" (${expoFile}); using the native one, "${native.value}"`,
27
+ ]
28
+ : [];
29
+ return {
30
+ id: native.value,
31
+ message: `${name} ${idName} "${native.value}" (${native.label} in ${native.file})`,
32
+ notes,
33
+ };
34
+ }
35
+ if (native !== null) {
36
+ // ネイティブのディレクトリはあるが、値が特定できない
37
+ if (fromExpo !== null && expoValue !== null) {
38
+ return {
39
+ id: expoValue,
40
+ message: `${name} ${idName} "${expoValue}" (${fromExpo})`,
41
+ notes: [
42
+ `could not determine the ${name} ${idName} from ${dir} (${native.reason}); using ${expoKey} "${expoValue}" (${expoFile})`,
43
+ ],
44
+ };
45
+ }
46
+ return { id: null, reason: native.reason, notes: [] };
47
+ }
48
+ if (fromExpo !== null && expoValue !== null) {
49
+ return {
50
+ id: expoValue,
51
+ message: `${name} ${idName} "${expoValue}" (${fromExpo})`,
52
+ notes: [],
53
+ };
54
+ }
55
+ if (expoFile !== undefined) {
56
+ return {
57
+ id: null,
58
+ reason: `${expoKey} is not set in ${expoFile} and there is no ${dir} directory`,
59
+ notes: [],
60
+ };
61
+ }
62
+ return {
63
+ id: null,
64
+ reason: project.expo === null
65
+ ? `there is no ${dir} directory and expo is not installed`
66
+ : `the Expo config could not be read and there is no ${dir} directory`,
67
+ notes: [],
68
+ };
69
+ };
70
+ /**
71
+ * C2。両方のプラットフォームが特定できなければ error、片方だけなら、もう片方は info(C3 はそのプラットフォームを飛ばす)。
72
+ * Expo の設定の評価の失敗・複数の .xcodeproj / アプリのターゲット・flavor の無視・Expo の設定とネイティブのずれは info で添える
73
+ */
74
+ export const resolveIds = (project) => {
75
+ const config = project.expo?.config ?? null;
76
+ const ios = resolvePlatform(IOS, project, config?.bundleIdentifier ?? null, project.ios?.bundleId ?? null);
77
+ const android = resolvePlatform(ANDROID, project, config?.package ?? null, project.android?.packageName ?? null);
78
+ const entries = [];
79
+ if (ios.id === null && android.id === null) {
80
+ entries.push({
81
+ id: 'C2',
82
+ level: 'error',
83
+ message: `could not determine either the iOS bundleId or the Android package. iOS: ${ios.reason}. Android: ${android.reason}. Check that --cwd is the app directory`,
84
+ });
85
+ }
86
+ else {
87
+ for (const [spec, resolved] of [
88
+ [IOS, ios],
89
+ [ANDROID, android],
90
+ ]) {
91
+ entries.push(resolved.id === null
92
+ ? {
93
+ id: 'C2',
94
+ level: 'info',
95
+ message: `could not determine the ${spec.name} ${spec.idName}: ${resolved.reason}. C3 skips ${spec.name}`,
96
+ }
97
+ : { id: 'C2', level: 'ok', message: resolved.message });
98
+ }
99
+ }
100
+ const notes = [
101
+ ...(project.expo?.notes ?? []),
102
+ ...(project.ios?.notes ?? []),
103
+ ...ios.notes,
104
+ ...(project.android?.notes ?? []),
105
+ ...android.notes,
106
+ ];
107
+ for (const message of notes) {
108
+ entries.push({ id: 'C2', level: 'info', message });
109
+ }
110
+ return { ios: ios.id, android: android.id, entries };
111
+ };
@@ -0,0 +1,51 @@
1
+ // C1: react-native-store-version がインストールされていて v2 以上か
2
+ import { readFileSync } from 'node:fs';
3
+ import { compareVersion } from '../../compare.js';
4
+ import { findPackageJson } from '../packages.js';
5
+ const PACKAGE_NAME = 'react-native-store-version';
6
+ /**
7
+ * v2 の最初の版(プレリリースを含む)。`2.0.0-beta.x` も `2.0.0` もこれ以上になる
8
+ */
9
+ const V2_FLOOR = '2.0.0-0';
10
+ /**
11
+ * C1。`<cwd>` から `react-native-store-version/package.json` を探して({@link findPackageJson})`name` と `version` を読む。
12
+ * `npm:react-native-store-version@1` のようなエイリアス名で入れたもの(`react-native-store-version-v1` など)は見ない。
13
+ *
14
+ * - 見つからない / 読めない / 別のパッケージ / version が比較できない → error
15
+ * - v2 より前(`2.0.0-0` より小さい)→ info
16
+ */
17
+ export const checkInstalled = (cwd) => {
18
+ const entry = (level, message) => [
19
+ { id: 'C1', level, message },
20
+ ];
21
+ const path = findPackageJson(cwd, PACKAGE_NAME);
22
+ if (path === null) {
23
+ return entry('error', `${PACKAGE_NAME} is not installed (no node_modules/${PACKAGE_NAME}/package.json in ${cwd} or its parents)`);
24
+ }
25
+ let json;
26
+ try {
27
+ json = JSON.parse(readFileSync(path, 'utf8'));
28
+ }
29
+ catch (error) {
30
+ return entry('error', `could not read ${path}: ${error instanceof Error ? error.message : String(error)}`);
31
+ }
32
+ const { name, version } = (json ?? {});
33
+ if (name !== PACKAGE_NAME) {
34
+ return entry('error', `${path} is not ${PACKAGE_NAME} (name: ${JSON.stringify(name)})`);
35
+ }
36
+ let result;
37
+ try {
38
+ if (typeof version !== 'string') {
39
+ throw new TypeError('not a string');
40
+ }
41
+ // compareVersion(local, remote) は remote の方が新しいときに 'new'。つまり version < 2.0.0-0
42
+ result = compareVersion(version, V2_FLOOR);
43
+ }
44
+ catch {
45
+ return entry('error', `could not read the version of ${PACKAGE_NAME} in ${path} (version: ${JSON.stringify(version)})`);
46
+ }
47
+ if (result === 'new') {
48
+ return entry('info', `${PACKAGE_NAME} ${version} is v1. StoreVersionProvider and this check are for v2 or later`);
49
+ }
50
+ return entry('ok', `${PACKAGE_NAME} ${version} is installed`);
51
+ };
@@ -0,0 +1,31 @@
1
+ // C7: version / ID の自動取得に使う optional peer があるか。パッケージの解決だけを見て、
2
+ // アプリのソースは検索しない(Provider に値を渡しているかは見ない)
3
+ import { isInstalled } from '../packages.js';
4
+ /** 自動取得に使う optional peer(src/detect.ts が読む順) */
5
+ export const AUTO_DETECT_PEERS = [
6
+ 'expo-application',
7
+ 'react-native-device-info',
8
+ ];
9
+ /**
10
+ * C7。どちらかが `<cwd>` から見つかれば({@link isInstalled})ok、どちらも無ければ info
11
+ * (`version` / `bundleId` / `packageName` を Provider に渡していれば不要なので、warning にはしない)
12
+ */
13
+ export const checkPeers = (cwd) => {
14
+ const found = AUTO_DETECT_PEERS.filter((name) => isInstalled(cwd, name));
15
+ if (found.length > 0) {
16
+ return [
17
+ {
18
+ id: 'C7',
19
+ level: 'ok',
20
+ message: `${found.join(' and ')} found for detecting the app version and ID`,
21
+ },
22
+ ];
23
+ }
24
+ return [
25
+ {
26
+ id: 'C7',
27
+ level: 'info',
28
+ message: `neither ${AUTO_DETECT_PEERS.join(' nor ')} is installed. Pass version / bundleId / packageName to StoreVersionProvider, or install one of them`,
29
+ },
30
+ ];
31
+ };
@@ -0,0 +1,141 @@
1
+ // C8: Android の段階公開の注意。Google Play の段階公開では、公開対象外のユーザーは新しい版を入手できない。
2
+ // ストアの版との差分で force にしていると、その人たちはアプリを使えなくなる。ソースの簡易なテキスト検索で、
3
+ // その設定になっていそうなときに注意を出す(誤検知はありうるので、--ignore C8 で抑止できることを書く)
4
+ import { displayPath } from '../project/files.js';
5
+ import { findOpeningTags, maskSource, readObjectLiteral, } from '../project/jsx.js';
6
+ import { SCAN_LIMITS, scanSources, } from '../project/sources.js';
7
+ const PROVIDER = 'StoreVersionProvider';
8
+ /** ストアの差分によらず版を渡す関数。どこかで使っていれば、force をストアの差分だけで決めていない */
9
+ const VERSION_GETTERS = /\b(getLatestVersion|getMinimumVersion)\b/;
10
+ /** 一覧に出すファイルの数(多いと 1 行が長くなりすぎる) */
11
+ const MAX_LISTED = 5;
12
+ const files = (n) => `${n} file${n === 1 ? '' : 's'}`;
13
+ const listFiles = (files) => files.length > MAX_LISTED
14
+ ? `${files.slice(0, MAX_LISTED).join(', ')} and ${files.length - MAX_LISTED} more`
15
+ : files.join(', ');
16
+ const ROLLOUT_NOTE = "If you use a staged rollout on Google Play, users outside the rollout cannot get the new version, so a force update from the store version can leave them unable to use the app. When Platform.OS === 'android', set major: 'warning' in the policy, or control force with getMinimumVersion. This is a simple text search and may be a false positive (a policy that already uses major: 'warning' only on Android still has 'force' in it); if so, suppress it with --ignore C8";
17
+ /** 理由の文言(`<StoreVersionProvider> …` に続く) */
18
+ const REASONS = {
19
+ noPolicy: "has no policy prop (the default policy has major: 'force')",
20
+ spread: 'has no policy prop (it may come from a spread)',
21
+ force: "has 'force' in its policy",
22
+ noMajor: "does not set major in its policy, so major updates use the default 'force'",
23
+ defaultPolicy: (expression) => `has policy={${expression}}, so the default policy (major: 'force') is used`,
24
+ };
25
+ /** null / undefined は省略と同じで既定値になる */
26
+ const isDefault = (expression) => expression === 'undefined' || expression === 'null';
27
+ /**
28
+ * policy 属性の値(`{…}` / `"…"` の書かれたまま)から、ストアの差分で force になりそうな理由。
29
+ * `'force'` / `"force"` があるか、`{undefined}` / `{null}`(既定の policy)か、オブジェクトリテラルで `major` が無い・
30
+ * null / undefined(既定の major は force)。変数や関数の policy は中身が分からないので、`'force'` の有無だけを見る。
31
+ * `...base` を含むオブジェクトは major がそこから来うるので、major の有無は見ない
32
+ */
33
+ const policyReasons = (value) => {
34
+ if (value === null) {
35
+ // 値の無い `policy`(true)。型の誤りで、判定の材料にならない
36
+ return [];
37
+ }
38
+ const reasons = [];
39
+ if (/'force'|"force"/.test(value)) {
40
+ reasons.push(REASONS.force);
41
+ }
42
+ if (!value.startsWith('{') || !value.endsWith('}')) {
43
+ return reasons;
44
+ }
45
+ const expression = maskSource(value.slice(1, -1), { strings: false }).trim();
46
+ if (isDefault(expression)) {
47
+ return [REASONS.defaultPolicy(expression)];
48
+ }
49
+ const object = readObjectLiteral(expression);
50
+ if (object !== null && !object.spread) {
51
+ const major = object.properties.get('major');
52
+ if (major === undefined || (major !== null && isDefault(major))) {
53
+ reasons.push(REASONS.noMajor);
54
+ }
55
+ }
56
+ return reasons;
57
+ };
58
+ /**
59
+ * 1 つのファイルの `<StoreVersionProvider …>` の開始タグのうち、ストアの差分で force になりそうなものの理由
60
+ * (policy 属性が無い = デフォルトの major が force。{@link policyReasons})。重複は 1 つにする
61
+ */
62
+ const forceReasons = (source) => {
63
+ const reasons = new Set();
64
+ for (const tag of findOpeningTags(source, PROVIDER)) {
65
+ const policy = tag.attributes.find(({ name }) => name === 'policy');
66
+ if (policy === undefined) {
67
+ reasons.add(tag.spreads > 0 ? REASONS.spread : REASONS.noPolicy);
68
+ continue;
69
+ }
70
+ for (const reason of policyReasons(policy.value)) {
71
+ reasons.add(reason);
72
+ }
73
+ }
74
+ return [...reasons];
75
+ };
76
+ /**
77
+ * C8。Android の package が特定できたときだけ(無ければ skipped)。`<cwd>` の下のソースを走査し、
78
+ * `<StoreVersionProvider>` の開始タグが force になりそうなファイルごとに warning。ただし `getLatestVersion` /
79
+ * `getMinimumVersion` が走査したどこかのファイルにあれば、どれも出さない。ファイル数の上限を超えたら判定せずに info
80
+ */
81
+ export const checkRollout = (cwd, androidPackage, limits = SCAN_LIMITS) => {
82
+ if (androidPackage === null) {
83
+ return [
84
+ {
85
+ id: 'C8',
86
+ level: 'skipped',
87
+ message: 'no Android package was determined, so the staged rollout note does not apply',
88
+ },
89
+ ];
90
+ }
91
+ const flagged = [];
92
+ let getterFile = null;
93
+ const scan = scanSources(cwd, (path, source) => {
94
+ // コメントの中の名前(`// TODO: getMinimumVersion を使う`)は使っていることにならない
95
+ if (getterFile === null &&
96
+ VERSION_GETTERS.test(maskSource(source, { strings: false }))) {
97
+ getterFile = displayPath(cwd, path);
98
+ }
99
+ if (source.includes(PROVIDER)) {
100
+ const reasons = forceReasons(source);
101
+ if (reasons.length > 0) {
102
+ flagged.push({ file: displayPath(cwd, path), reasons });
103
+ }
104
+ }
105
+ }, limits);
106
+ if (scan.truncated) {
107
+ return [
108
+ {
109
+ id: 'C8',
110
+ level: 'info',
111
+ message: `stopped scanning the sources at ${files(limits.maxFiles)} (.ts / .tsx / .js / .jsx outside node_modules, ios, android, and build output), so the staged rollout note was not checked. Check --cwd, or suppress this with --ignore C8`,
112
+ },
113
+ ];
114
+ }
115
+ const entries = [];
116
+ if (scan.tooLarge.length > 0) {
117
+ entries.push({
118
+ id: 'C8',
119
+ level: 'info',
120
+ message: `did not scan ${scan.tooLarge.length} source ${scan.tooLarge.length === 1 ? 'file' : 'files'} larger than ${limits.maxFileBytes} bytes: ${listFiles(scan.tooLarge.map((path) => displayPath(cwd, path)))}`,
121
+ });
122
+ }
123
+ if (getterFile !== null || flagged.length === 0) {
124
+ entries.push({
125
+ id: 'C8',
126
+ level: 'ok',
127
+ message: getterFile !== null
128
+ ? `getLatestVersion / getMinimumVersion is used (${getterFile}), so force updates do not depend only on the store version`
129
+ : `no <${PROVIDER}> that forces updates from the store version was found (${files(scan.scanned)} scanned)`,
130
+ });
131
+ return entries;
132
+ }
133
+ for (const { file, reasons } of flagged) {
134
+ entries.push({
135
+ id: 'C8',
136
+ level: 'warning',
137
+ message: `${file}: <${PROVIDER}> ${reasons.join(' / ')}, and getLatestVersion / getMinimumVersion is not used in the sources. ${ROLLOUT_NOTE}`,
138
+ });
139
+ }
140
+ return entries;
141
+ };