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.
- package/CHANGELOG.md +51 -0
- package/README.md +511 -51
- package/dist/checkVersion.d.ts +73 -0
- package/dist/checkVersion.d.ts.map +1 -0
- package/dist/checkVersion.js +193 -0
- package/dist/cli/check.js +38 -0
- package/dist/cli/checks/ids.js +111 -0
- package/dist/cli/checks/installed.js +51 -0
- package/dist/cli/checks/peers.js +31 -0
- package/dist/cli/checks/rollout.js +141 -0
- package/dist/cli/checks/store.js +71 -0
- package/dist/cli/checks/versions.js +83 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/main.js +182 -0
- package/dist/cli/output.js +77 -0
- package/dist/cli/packages.js +40 -0
- package/dist/cli/project/android.js +60 -0
- package/dist/cli/project/expo.js +194 -0
- package/dist/cli/project/files.js +42 -0
- package/dist/cli/project/gradle.js +145 -0
- package/dist/cli/project/index.js +10 -0
- package/dist/cli/project/ios.js +190 -0
- package/dist/cli/project/jsx.js +327 -0
- package/dist/cli/project/plist.js +166 -0
- package/dist/cli/project/sources.js +86 -0
- package/dist/cli/result.js +12 -0
- package/dist/compare.d.ts +31 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +68 -0
- package/dist/detect.d.ts +68 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +103 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +23 -0
- package/dist/fetchers/android.d.ts +53 -0
- package/dist/fetchers/android.d.ts.map +1 -0
- package/dist/fetchers/android.js +91 -0
- package/dist/fetchers/custom.d.ts +29 -0
- package/dist/fetchers/custom.d.ts.map +1 -0
- package/dist/fetchers/custom.js +41 -0
- package/dist/fetchers/ios.d.ts +61 -0
- package/dist/fetchers/ios.d.ts.map +1 -0
- package/dist/fetchers/ios.js +79 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -58
- package/dist/openStore.d.ts +29 -0
- package/dist/openStore.d.ts.map +1 -0
- package/dist/openStore.js +46 -0
- package/dist/policy.d.ts +75 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +128 -0
- package/dist/react/DefaultForceModal.d.ts +26 -0
- package/dist/react/DefaultForceModal.d.ts.map +1 -0
- package/dist/react/DefaultForceModal.js +58 -0
- package/dist/react/DefaultWarningSheet.d.ts +26 -0
- package/dist/react/DefaultWarningSheet.d.ts.map +1 -0
- package/dist/react/DefaultWarningSheet.js +66 -0
- package/dist/react/StoreVersionModal.d.ts +52 -0
- package/dist/react/StoreVersionModal.d.ts.map +1 -0
- package/dist/react/StoreVersionModal.js +250 -0
- package/dist/react/StoreVersionProvider.d.ts +51 -0
- package/dist/react/StoreVersionProvider.d.ts.map +1 -0
- package/dist/react/StoreVersionProvider.js +69 -0
- package/dist/react/defaultUi.d.ts +72 -0
- package/dist/react/defaultUi.d.ts.map +1 -0
- package/dist/react/defaultUi.js +118 -0
- package/dist/react/engine.d.ts +146 -0
- package/dist/react/engine.d.ts.map +1 -0
- package/dist/react/engine.js +541 -0
- package/dist/react/texts.d.ts +43 -0
- package/dist/react/texts.d.ts.map +1 -0
- package/dist/react/texts.js +57 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/react/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/react/useIsomorphicLayoutEffect.js +6 -0
- package/dist/react/useStoreVersion.d.ts +121 -0
- package/dist/react/useStoreVersion.d.ts.map +1 -0
- package/dist/react/useStoreVersion.js +23 -0
- package/dist/request.d.ts +78 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +227 -0
- package/dist/resolve.d.ts +98 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +218 -0
- package/dist/texts/ja.d.ts +8 -0
- package/dist/texts/ja.d.ts.map +1 -0
- package/dist/texts/ja.js +18 -0
- package/dist/vendor/compare-versions.d.ts +52 -0
- package/dist/vendor/compare-versions.d.ts.map +1 -0
- package/dist/vendor/compare-versions.js +111 -0
- package/package.json +84 -45
- package/src/checkVersion.ts +273 -0
- package/src/cli/check.ts +59 -0
- package/src/cli/checks/ids.ts +163 -0
- package/src/cli/checks/installed.ts +75 -0
- package/src/cli/checks/peers.ts +34 -0
- package/src/cli/checks/rollout.ts +183 -0
- package/src/cli/checks/store.ts +105 -0
- package/src/cli/checks/versions.ts +108 -0
- package/src/cli/index.ts +51 -0
- package/src/cli/main.ts +229 -0
- package/src/cli/output.ts +122 -0
- package/src/cli/packages.ts +43 -0
- package/src/cli/project/android.ts +96 -0
- package/src/cli/project/expo.ts +232 -0
- package/src/cli/project/files.ts +56 -0
- package/src/cli/project/gradle.ts +181 -0
- package/src/cli/project/index.ts +18 -0
- package/src/cli/project/ios.ts +284 -0
- package/src/cli/project/jsx.ts +354 -0
- package/src/cli/project/plist.ts +179 -0
- package/src/cli/project/sources.ts +118 -0
- package/src/cli/result.ts +34 -0
- package/src/compare.ts +101 -0
- package/src/detect.ts +178 -0
- package/src/errors.ts +63 -0
- package/src/fetchers/android.ts +136 -0
- package/src/fetchers/custom.ts +84 -0
- package/src/fetchers/ios.ts +160 -0
- package/src/index.ts +37 -0
- package/src/openStore.ts +74 -0
- package/src/policy.ts +223 -0
- package/src/react/DefaultForceModal.tsx +100 -0
- package/src/react/DefaultWarningSheet.tsx +115 -0
- package/src/react/StoreVersionModal.tsx +365 -0
- package/src/react/StoreVersionProvider.tsx +149 -0
- package/src/react/defaultUi.tsx +205 -0
- package/src/react/engine.ts +768 -0
- package/src/react/texts.ts +119 -0
- package/src/react/useIsomorphicLayoutEffect.ts +8 -0
- package/src/react/useStoreVersion.ts +153 -0
- package/src/request.ts +328 -0
- package/src/resolve.ts +388 -0
- package/src/texts/ja.ts +26 -0
- package/src/vendor/compare-versions.ts +128 -0
- package/texts/ja.d.ts +2 -0
- package/texts/ja.js +3 -0
- package/.eslintrc.js +0 -51
- package/.github/FUNDING.yml +0 -3
- package/.github/workflows/pr.yml +0 -24
- package/.github/workflows/release.yml +0 -31
- package/.prettierrc.js +0 -6
- package/babel.config.js +0 -3
- package/dist/android.d.ts +0 -1
- package/dist/android.js +0 -17
- package/dist/ios.d.ts +0 -1
- package/dist/ios.js +0 -18
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
|
|
9
|
+
import {
|
|
10
|
+
type CompareDetail,
|
|
11
|
+
type CompareResult,
|
|
12
|
+
compareVersion,
|
|
13
|
+
toDetail,
|
|
14
|
+
} from './compare.ts';
|
|
15
|
+
import { nativeAppId, nativeVersion } from './detect.ts';
|
|
16
|
+
import { StoreVersionError } from './errors.ts';
|
|
17
|
+
import { extractPlayVersion, playStoreUrl } from './fetchers/android.ts';
|
|
18
|
+
import { appIdFromStoreUrl, lookupIOS } from './fetchers/ios.ts';
|
|
19
|
+
import { failedResponseStatusOf, requestText } from './request.ts';
|
|
20
|
+
|
|
21
|
+
/** {@link checkVersion} の引数。v2 ではすべて省略できる */
|
|
22
|
+
export type CheckVersionParams = {
|
|
23
|
+
/**
|
|
24
|
+
* アプリのバージョン。必ずネイティブのバージョン(iOS の `CFBundleShortVersionString` / Android の `versionName`)を渡す。
|
|
25
|
+
* `Constants.expoConfig.version` や `package.json` の値は JS バンドルに含まれ、OTA 更新で変わってしまう
|
|
26
|
+
* (ネイティブは古いのに「更新済み」と誤判定する)ので使わないこと。
|
|
27
|
+
*
|
|
28
|
+
* 省略(空文字も含む)すると、iOS / Android では expo-application の `nativeApplicationVersion` →
|
|
29
|
+
* react-native-device-info の `getVersion()` の順に取る。取れなければ `local version is not set.` を throw する。
|
|
30
|
+
* Expo Go の中では Expo Go 自体のバージョンが取れてしまうので、Expo Go で試すときは明示的に渡すこと
|
|
31
|
+
*/
|
|
32
|
+
version?: string;
|
|
33
|
+
/**
|
|
34
|
+
* App Store の URL(`https://apps.apple.com/jp/app/github/id1477376905` など。`id<数字>` から App ID を取る)。
|
|
35
|
+
* 省略(空文字も含む)すると、iOS ではアプリの bundleId(expo-application の `applicationId` →
|
|
36
|
+
* react-native-device-info の `getBundleId()`)で引く。取れなければ `iosStoreURL is not set.` を throw する。
|
|
37
|
+
* Expo Go の中で省略すると Expo Go 自体(`host.exp.Exponent`)を引いてしまうので、Expo Go で試すときは URL も明示すること
|
|
38
|
+
*/
|
|
39
|
+
iosStoreURL?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Google Play の URL(`https://play.google.com/store/apps/details?id=<package>`)。
|
|
42
|
+
* 省略(空文字も含む)すると、Android ではアプリの package name(expo-application の `applicationId` →
|
|
43
|
+
* react-native-device-info の `getBundleId()`)から URL を組み立てる。取れなければ `androidStoreURL is not set.` を throw する。
|
|
44
|
+
* Expo Go の中で省略すると Expo Go 自体(`host.exp.exponent`)を引いてしまうので、Expo Go で試すときは URL も明示すること
|
|
45
|
+
*/
|
|
46
|
+
androidStoreURL?: string;
|
|
47
|
+
/** iTunes Lookup の国コード(iOS のみ)。省略(空文字も含む)すると `jp`(v1 と同じ) */
|
|
48
|
+
country?: string;
|
|
49
|
+
/**
|
|
50
|
+
* ストアへの問い合わせのタイムアウト(ms)。省略するとタイムアウトしない(v1 と同じ)。
|
|
51
|
+
* 正の有限の数のときだけ有効(0・負の数・NaN・Infinity はタイムアウトなし)。
|
|
52
|
+
* 時間切れのときは `StoreVersionError`(`code: 'TIMEOUT'`)を throw する
|
|
53
|
+
*/
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** {@link checkVersion} の戻り値(v1 と同じ形) */
|
|
58
|
+
export type CheckVersionResponse = {
|
|
59
|
+
/** アプリのバージョン(渡した値か、自動取得した値) */
|
|
60
|
+
local: string;
|
|
61
|
+
/** ストアのバージョン */
|
|
62
|
+
remote: string;
|
|
63
|
+
/** `remote` が新しければ `new`、古ければ `old`、同じなら `equal` */
|
|
64
|
+
result: CompareResult;
|
|
65
|
+
detail: CompareDetail;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* v1 のエラーメッセージへの対応表(v1 の src/index.ts / ios.ts / android.ts の全経路)。
|
|
70
|
+
* 取得の段階で起きたものは、v1 と同じく `new Error(e.message)`(Error でなければ `can't get ${Platform.OS} version`)で
|
|
71
|
+
* 投げ直す。v2 の層の `StoreVersionError` は {@link toV1Error} で v1 のメッセージに戻す。
|
|
72
|
+
*
|
|
73
|
+
* | 経路(v1) | v1 のメッセージ | v2 の層での扱い | checkVersion が投げるもの |
|
|
74
|
+
* |-----------------------------------------------------|---------------------------------------|---------------------------------------------------------|---------------------------------------|
|
|
75
|
+
* | `version` が falsy | `local version is not set.` | ios / android は自動取得。取れなければ v1 と同じ | 同じ |
|
|
76
|
+
* | iOS で `iosStoreURL` が falsy | `iosStoreURL is not set.` | bundleId を自動取得。取れなければ v1 と同じ | 同じ |
|
|
77
|
+
* | Android で `androidStoreURL` が falsy | `androidStoreURL is not set.` | package name を自動取得。取れなければ v1 と同じ | 同じ |
|
|
78
|
+
* | iOS の URL が正規表現に合わない | `iosStoreURL is invalid.` | `appIdFromStoreUrl` が null | 同じ |
|
|
79
|
+
* | fetch / `text()` が Error で reject | 元のメッセージ | `requestText` の `NETWORK`(cause に元のエラー) | `new Error(cause.message)` |
|
|
80
|
+
* | fetch / `text()` が Error でないもので reject | `can't get ${Platform.OS} version` | `NETWORK`(cause が Error でない) | 同じ |
|
|
81
|
+
* | iOS の本文が JSON でない(status は見ない) | `JSON.parse` のメッセージ | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause に SyntaxError) | `new Error(SyntaxError の message)` |
|
|
82
|
+
* | iOS の `results` が無い・空(`null` の応答も) | `appID(<id>) is not released.` | URL: v1 と同じ処理 / bundleId: `NOT_FOUND` | 同じ(bundleId は `appID(<bundleId>)`)|
|
|
83
|
+
* | iOS の `results[0]` が null など(TypeError) | TypeError のメッセージ | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
|
|
84
|
+
* | iOS の `results[0].version` が文字列でない | (そのまま remote にして比較で throw) | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
|
|
85
|
+
* | iOS の 200 以外 | (見ない。本文で上のどれかになる) | URL: v1 と同じ処理 / bundleId: `NETWORK`(cause なし) | URL: 同じ / bundleId: `can't get ios version` |
|
|
86
|
+
* | Android の URL が正規表現に合わない(ios 以外で省略も) | `androidStoreURL is invalid.` | checkVersion で検証 | 同じ |
|
|
87
|
+
* | Android の 200 以外 | `androidStoreURL is invalid.` | `requestText` の status を checkVersion で見る | 同じ(package name の経路も) |
|
|
88
|
+
* | Android の 200 以外で本文の読み込みが失敗 | `androidStoreURL is invalid.`(v1 は本文を読まない) | `NETWORK`(`failedResponseStatusOf` で status が分かる) | 同じ(package name の経路も) |
|
|
89
|
+
* | Android の 200 で本文の読み込みが失敗 | 元のメッセージ | `NETWORK`(cause に元のエラー) | `new Error(cause.message)` |
|
|
90
|
+
* | Android の版が抽出できない | `can't get android app version.` | `extractPlayVersion` の `PARSE_FAILED` / `INVALID_VERSION`(Varies・比較できない版) | 同じ |
|
|
91
|
+
* | 比較できない local(取得の後) | compare-versions のメッセージ | `compareVersion`(try の外。包まない) | 同じ |
|
|
92
|
+
* | (v1 に無い)`timeoutMs` の時間切れ | — | `TIMEOUT` | `StoreVersionError('TIMEOUT')` そのまま |
|
|
93
|
+
*
|
|
94
|
+
* iOS の URL 方式は `lookupIOS` を使わず `requestText` で v1 と同じ処理をする。`lookupIOS` は 200 以外を `NETWORK` にし、
|
|
95
|
+
* `country` を encode し、`results[0].version` が文字列でないと `NETWORK` にするので、v1 の結果(本文の JSON を読む)を
|
|
96
|
+
* 再現できないため。Android は 200 以外のメッセージ(`androidStoreURL is invalid.`)を通信の失敗と分けるために、
|
|
97
|
+
* `fetchAndroidVersion` ではなく `requestText` と `extractPlayVersion`(v2 の抽出)を使う。
|
|
98
|
+
* Android の抽出は URL を渡した経路でも v2 のもの(v1 と同じ結果を保つ方針の例外)で、v1 と結果が分かれるのは、版の位置に v1 のパターン
|
|
99
|
+
* (数字とドットだけ)に合わない比較できる版(`5` / `1.0.0-beta.1` / `v2.1` など)がある場合(v1 は抽出できず throw)、
|
|
100
|
+
* 版の位置より前に v1 のパターン(`[[["x.y.z"]],`)の形の別の要素がある場合(v1 はそれを拾う)、版の位置が空なのにほかの場所に
|
|
101
|
+
* v1 のパターンが当たる場合(v1 はそれを拾う)。2026-11-08 の実ページでは v1 のパターンに当たるのは版の位置だけで、v1 と結果は同じだった
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/** v1 の `getAndroidVersion` の URL の検証 */
|
|
105
|
+
const V1_ANDROID_STORE_URL =
|
|
106
|
+
/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[0-9a-zA-Z.]+/;
|
|
107
|
+
|
|
108
|
+
/** 取得の段階で起きたエラーを v1 のメッセージに戻す(上の表) */
|
|
109
|
+
const toV1Error = (error: unknown, os: string, appId: string): Error => {
|
|
110
|
+
if (error instanceof StoreVersionError) {
|
|
111
|
+
switch (error.code) {
|
|
112
|
+
// v1 にはタイムアウトが無い(`timeoutMs` を渡したときだけ起きる)ので、code で区別できるようにそのまま投げる
|
|
113
|
+
case 'TIMEOUT':
|
|
114
|
+
return error;
|
|
115
|
+
// 通信の失敗は、v1 と同じく元のエラーのメッセージで投げ直す
|
|
116
|
+
case 'NETWORK':
|
|
117
|
+
return error.cause instanceof Error
|
|
118
|
+
? new Error(error.cause.message)
|
|
119
|
+
: new Error(`can't get ${os} version`);
|
|
120
|
+
case 'NOT_FOUND':
|
|
121
|
+
return new Error(`appID(${appId}) is not released.`);
|
|
122
|
+
case 'PARSE_FAILED':
|
|
123
|
+
case 'INVALID_VERSION':
|
|
124
|
+
return new Error("can't get android app version.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return error instanceof Error
|
|
128
|
+
? new Error(error.message)
|
|
129
|
+
: new Error(`can't get ${os} version`);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
type IOSLookupResponse = {
|
|
133
|
+
results?: { length?: number; [index: number]: { version: string } };
|
|
134
|
+
} | null;
|
|
135
|
+
|
|
136
|
+
/** v1 の `getIOSVersion` と同じ処理(status は見ず、本文を JSON として読む)。取得だけ `requestText` に載せる */
|
|
137
|
+
const iosVersionByStoreUrl = async (
|
|
138
|
+
appId: string,
|
|
139
|
+
country: string,
|
|
140
|
+
timeoutMs: number | undefined
|
|
141
|
+
): Promise<string> => {
|
|
142
|
+
// v1 と同じく country は encode しない。タイムスタンプと no-cache は requestText が付ける
|
|
143
|
+
const { body } = await requestText(
|
|
144
|
+
`https://itunes.apple.com/lookup?id=${appId}&country=${country}`,
|
|
145
|
+
{ noCache: true, timeoutMs }
|
|
146
|
+
);
|
|
147
|
+
const response = JSON.parse(body) as IOSLookupResponse;
|
|
148
|
+
// v1 の `!response || !response.results || ...` と同じ(null / false / 0 などの応答も未リリース扱い)
|
|
149
|
+
if (!response?.results || response.results.length === 0) {
|
|
150
|
+
throw new Error(`appID(${appId}) is not released.`);
|
|
151
|
+
}
|
|
152
|
+
return response.results[0].version;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Play Store のページを取得して版を返す。200 以外は v1 と同じく `androidStoreURL is invalid.` */
|
|
156
|
+
const androidVersionByUrl = async (
|
|
157
|
+
url: string,
|
|
158
|
+
timeoutMs: number | undefined
|
|
159
|
+
): Promise<string> => {
|
|
160
|
+
let response: { status: number; body: string };
|
|
161
|
+
try {
|
|
162
|
+
response = await requestText(url, { timeoutMs });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// v1 は status を先に見て、200 以外なら本文を読まない。v2 の request は本文を読むので、その読み込みに失敗しても
|
|
165
|
+
// 200 以外なら v1 と同じメッセージにする
|
|
166
|
+
const status = failedResponseStatusOf(error);
|
|
167
|
+
if (status !== undefined && status !== 200) {
|
|
168
|
+
throw new Error('androidStoreURL is invalid.');
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
if (response.status !== 200) {
|
|
173
|
+
throw new Error('androidStoreURL is invalid.');
|
|
174
|
+
}
|
|
175
|
+
return extractPlayVersion(response.body);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* v1 互換のバージョン確認。ストアのバージョンを取得して、`version` と比べた結果を返す。
|
|
180
|
+
*
|
|
181
|
+
* 引数をすべて渡した場合は v1 と同じ結果・同じエラーメッセージになる(`country` の既定も `jp`)。ただし次の 2 点は v1 と違う:
|
|
182
|
+
* - バージョンの比較は compare-versions 6.1.1 の挙動(プレリリースの一部や、先頭の `^` / `~` / `=` / 大文字の `V` などの扱い)
|
|
183
|
+
* - Android の版は v2 の抽出(構造に錨を下ろしたパターン → v1 のパターンの順)で取る。v1 では取れなかった
|
|
184
|
+
* `5` / `1.0.0-beta.1` のような版も返し、版の位置より前にある古い版は拾わない。版の位置が空なら `can't get android app version.`
|
|
185
|
+
*
|
|
186
|
+
* 省略した引数は、iOS / Android では自動取得する(expo-application か react-native-device-info が必要)。
|
|
187
|
+
* `Platform.OS` が ios / android 以外(web など)のときは v1 と同じく Android の経路で `androidStoreURL` を取得し、自動取得はしない。
|
|
188
|
+
*
|
|
189
|
+
* 失敗は `Error` で reject する(メッセージは v1 と同じ)。`timeoutMs` の時間切れだけは `StoreVersionError`(`code: 'TIMEOUT'`)。
|
|
190
|
+
* Expo Go や web でも判定を省かない(v1 と同じ)。Expo Go の中で version / URL を省略すると Expo Go 自体の値を使ってしまうので、
|
|
191
|
+
* Expo Go で試すときはすべて明示すること。v2 の Provider / hook と違い、起動を止めない仕組みは持たないので、
|
|
192
|
+
* 呼び出し側で catch すること
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* import { checkVersion } from 'react-native-store-version';
|
|
196
|
+
*
|
|
197
|
+
* const { result } = await checkVersion({
|
|
198
|
+
* version: '1.0.0', // ネイティブのバージョン
|
|
199
|
+
* iosStoreURL: 'https://apps.apple.com/jp/app/github/id1477376905',
|
|
200
|
+
* androidStoreURL: 'https://play.google.com/store/apps/details?id=com.github.android',
|
|
201
|
+
* });
|
|
202
|
+
*/
|
|
203
|
+
export const checkVersion = async (
|
|
204
|
+
params: CheckVersionParams = {}
|
|
205
|
+
): Promise<CheckVersionResponse> => {
|
|
206
|
+
const os = Platform.OS;
|
|
207
|
+
// 自動取得は ios / android だけ(ほかの OS は v1 と同じ挙動のまま。ネイティブの値がストアの版と対応しないため)
|
|
208
|
+
const autoDetect = os === 'ios' || os === 'android';
|
|
209
|
+
|
|
210
|
+
// v1 が `local version is not set.` を投げていた条件(falsy)のときだけ自動取得に回す
|
|
211
|
+
const version = params.version || (autoDetect ? nativeVersion() : null);
|
|
212
|
+
if (!version) {
|
|
213
|
+
throw new Error('local version is not set.');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let fetchRemote: () => Promise<string>;
|
|
217
|
+
// NOT_FOUND を v1 の `appID(<id>) is not released.` に戻すときの id
|
|
218
|
+
let appId = '';
|
|
219
|
+
if (os === 'ios') {
|
|
220
|
+
const country = params.country || 'jp';
|
|
221
|
+
if (params.iosStoreURL) {
|
|
222
|
+
const storeUrl = params.iosStoreURL;
|
|
223
|
+
fetchRemote = async () => {
|
|
224
|
+
const id = appIdFromStoreUrl(storeUrl);
|
|
225
|
+
if (id === null) {
|
|
226
|
+
throw new Error('iosStoreURL is invalid.');
|
|
227
|
+
}
|
|
228
|
+
return iosVersionByStoreUrl(id, country, params.timeoutMs);
|
|
229
|
+
};
|
|
230
|
+
} else {
|
|
231
|
+
const bundleId = nativeAppId();
|
|
232
|
+
if (bundleId === null) {
|
|
233
|
+
throw new Error('iosStoreURL is not set.');
|
|
234
|
+
}
|
|
235
|
+
appId = bundleId;
|
|
236
|
+
fetchRemote = async () =>
|
|
237
|
+
(
|
|
238
|
+
await lookupIOS({ bundleId }, country, {
|
|
239
|
+
timeoutMs: params.timeoutMs,
|
|
240
|
+
})
|
|
241
|
+
).version;
|
|
242
|
+
}
|
|
243
|
+
} else if (os === 'android' && !params.androidStoreURL) {
|
|
244
|
+
const packageName = nativeAppId();
|
|
245
|
+
if (packageName === null) {
|
|
246
|
+
throw new Error('androidStoreURL is not set.');
|
|
247
|
+
}
|
|
248
|
+
fetchRemote = () =>
|
|
249
|
+
androidVersionByUrl(playStoreUrl(packageName), params.timeoutMs);
|
|
250
|
+
} else {
|
|
251
|
+
// v1 は ios 以外をすべて Android の経路に進める(web などで省略された場合は `androidStoreURL is invalid.`)。
|
|
252
|
+
// v1 の `getAndroidVersion(storeURL = '')` の既定引数と同じく、undefined のときだけ空文字にする
|
|
253
|
+
const storeUrl =
|
|
254
|
+
params.androidStoreURL === undefined ? '' : params.androidStoreURL;
|
|
255
|
+
fetchRemote = async () => {
|
|
256
|
+
if (!storeUrl.match(V1_ANDROID_STORE_URL)) {
|
|
257
|
+
throw new Error('androidStoreURL is invalid.');
|
|
258
|
+
}
|
|
259
|
+
return androidVersionByUrl(storeUrl, params.timeoutMs);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let remote: string;
|
|
264
|
+
try {
|
|
265
|
+
remote = await fetchRemote();
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw toV1Error(error, os, appId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// v1 と同じく比較は try の外(比較できない local は compare-versions のメッセージがそのまま出る)
|
|
271
|
+
const result = compareVersion(version, remote);
|
|
272
|
+
return { local: version, remote, result, detail: toDetail(result) };
|
|
273
|
+
};
|
package/src/cli/check.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// `check` サブコマンドのチェックを順に回す
|
|
2
|
+
import { resolveIds } from './checks/ids.ts';
|
|
3
|
+
import { checkInstalled } from './checks/installed.ts';
|
|
4
|
+
import { checkPeers } from './checks/peers.ts';
|
|
5
|
+
import { checkRollout } from './checks/rollout.ts';
|
|
6
|
+
import { checkStore } from './checks/store.ts';
|
|
7
|
+
import { checkVersions } from './checks/versions.ts';
|
|
8
|
+
import { readProject } from './project/index.ts';
|
|
9
|
+
import type { CheckEntry, CheckId } from './result.ts';
|
|
10
|
+
|
|
11
|
+
/** {@link runChecks} のオプション */
|
|
12
|
+
export type CheckOptions = {
|
|
13
|
+
/** 確かめるアプリのディレクトリ(絶対パス) */
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** C3 の App Store の国 */
|
|
16
|
+
country: string;
|
|
17
|
+
/** 回さない(結果にも出さない)チェック */
|
|
18
|
+
ignore: ReadonlySet<string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** アプリの構成(Expo の設定・ネイティブのプロジェクト)を使うチェック */
|
|
22
|
+
const PROJECT_CHECKS: readonly CheckId[] = ['C2', 'C3', 'C4', 'C8'];
|
|
23
|
+
|
|
24
|
+
/** チェックを C1 → C2 → C3 → C4 → C7 → C8 の順に回す。`ignore` にあるものは回さない */
|
|
25
|
+
export const runChecks = async ({
|
|
26
|
+
cwd,
|
|
27
|
+
country,
|
|
28
|
+
ignore,
|
|
29
|
+
}: CheckOptions): Promise<CheckEntry[]> => {
|
|
30
|
+
const enabled = (id: CheckId) => !ignore.has(id);
|
|
31
|
+
const entries: CheckEntry[] = [];
|
|
32
|
+
|
|
33
|
+
// C3(ID で引く)・C8(Android か)は C2 が特定した ID を使うので、`--ignore C2` でも ID の特定はする(C2 の行だけを出さない)。
|
|
34
|
+
// Expo の設定の評価(アプリの app.config.* を動かす)は、どれも使わないときはしない
|
|
35
|
+
const project = PROJECT_CHECKS.some(enabled) ? readProject(cwd) : null;
|
|
36
|
+
const ids = project === null ? null : resolveIds(project);
|
|
37
|
+
|
|
38
|
+
if (enabled('C1')) {
|
|
39
|
+
entries.push(...checkInstalled(cwd));
|
|
40
|
+
}
|
|
41
|
+
if (ids !== null && enabled('C2')) {
|
|
42
|
+
entries.push(...ids.entries);
|
|
43
|
+
}
|
|
44
|
+
if (ids !== null && enabled('C3')) {
|
|
45
|
+
entries.push(
|
|
46
|
+
...(await checkStore({ ios: ids.ios, android: ids.android }, { country }))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (project !== null && enabled('C4')) {
|
|
50
|
+
entries.push(...checkVersions(project));
|
|
51
|
+
}
|
|
52
|
+
if (enabled('C7')) {
|
|
53
|
+
entries.push(...checkPeers(cwd));
|
|
54
|
+
}
|
|
55
|
+
if (ids !== null && enabled('C8')) {
|
|
56
|
+
entries.push(...checkRollout(cwd, ids.android));
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// C2: iOS の bundleId / Android の package が特定できるか。特定した ID は C3(ストア)と C8(Android か)が使う
|
|
2
|
+
import type { Lookup } from '../project/files.ts';
|
|
3
|
+
import type { ProjectInfo } from '../project/index.ts';
|
|
4
|
+
import type { CheckEntry } from '../result.ts';
|
|
5
|
+
|
|
6
|
+
/** {@link resolveIds} の結果。`entries` は C2 の結果の行(`--ignore C2` のときは出さず、ID だけを使う) */
|
|
7
|
+
export type ResolvedIds = {
|
|
8
|
+
ios: string | null;
|
|
9
|
+
android: string | null;
|
|
10
|
+
entries: CheckEntry[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PlatformSpec = {
|
|
14
|
+
name: string;
|
|
15
|
+
/** ID の呼び名 */
|
|
16
|
+
idName: string;
|
|
17
|
+
/** ネイティブのディレクトリ */
|
|
18
|
+
dir: string;
|
|
19
|
+
/** Expo の設定のキー */
|
|
20
|
+
expoKey: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const IOS: PlatformSpec = {
|
|
24
|
+
name: 'iOS',
|
|
25
|
+
idName: 'bundleId',
|
|
26
|
+
dir: 'ios/',
|
|
27
|
+
expoKey: 'ios.bundleIdentifier',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ANDROID: PlatformSpec = {
|
|
31
|
+
name: 'Android',
|
|
32
|
+
idName: 'package',
|
|
33
|
+
dir: 'android/',
|
|
34
|
+
expoKey: 'android.package',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type Resolved =
|
|
38
|
+
| { id: string; message: string; notes: string[] }
|
|
39
|
+
| { id: null; reason: string; notes: string[] };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 1 つのプラットフォームの ID を決める。ネイティブのディレクトリがあればその値(Expo の設定とずれていれば info)、
|
|
43
|
+
* ネイティブから特定できなければ Expo の設定の値。ネイティブのディレクトリが無ければ(CNG・bare RN で片方だけなど)Expo の設定の値
|
|
44
|
+
*/
|
|
45
|
+
const resolvePlatform = (
|
|
46
|
+
spec: PlatformSpec,
|
|
47
|
+
project: ProjectInfo,
|
|
48
|
+
expoValue: string | null,
|
|
49
|
+
native: Lookup | null
|
|
50
|
+
): Resolved => {
|
|
51
|
+
const { name, idName, dir, expoKey } = spec;
|
|
52
|
+
const expoFile = project.expo?.config?.file;
|
|
53
|
+
const fromExpo =
|
|
54
|
+
expoValue !== null && expoFile !== undefined
|
|
55
|
+
? `${expoKey} in ${expoFile}`
|
|
56
|
+
: null;
|
|
57
|
+
|
|
58
|
+
if (native?.value != null) {
|
|
59
|
+
const notes =
|
|
60
|
+
fromExpo !== null && expoValue !== native.value
|
|
61
|
+
? [
|
|
62
|
+
`the ${name} ${idName} differs between ${native.label} "${native.value}" (${native.file}) and ${expoKey} "${expoValue}" (${expoFile}); using the native one, "${native.value}"`,
|
|
63
|
+
]
|
|
64
|
+
: [];
|
|
65
|
+
return {
|
|
66
|
+
id: native.value,
|
|
67
|
+
message: `${name} ${idName} "${native.value}" (${native.label} in ${native.file})`,
|
|
68
|
+
notes,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (native !== null) {
|
|
72
|
+
// ネイティブのディレクトリはあるが、値が特定できない
|
|
73
|
+
if (fromExpo !== null && expoValue !== null) {
|
|
74
|
+
return {
|
|
75
|
+
id: expoValue,
|
|
76
|
+
message: `${name} ${idName} "${expoValue}" (${fromExpo})`,
|
|
77
|
+
notes: [
|
|
78
|
+
`could not determine the ${name} ${idName} from ${dir} (${native.reason}); using ${expoKey} "${expoValue}" (${expoFile})`,
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { id: null, reason: native.reason, notes: [] };
|
|
83
|
+
}
|
|
84
|
+
if (fromExpo !== null && expoValue !== null) {
|
|
85
|
+
return {
|
|
86
|
+
id: expoValue,
|
|
87
|
+
message: `${name} ${idName} "${expoValue}" (${fromExpo})`,
|
|
88
|
+
notes: [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (expoFile !== undefined) {
|
|
92
|
+
return {
|
|
93
|
+
id: null,
|
|
94
|
+
reason: `${expoKey} is not set in ${expoFile} and there is no ${dir} directory`,
|
|
95
|
+
notes: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
id: null,
|
|
100
|
+
reason:
|
|
101
|
+
project.expo === null
|
|
102
|
+
? `there is no ${dir} directory and expo is not installed`
|
|
103
|
+
: `the Expo config could not be read and there is no ${dir} directory`,
|
|
104
|
+
notes: [],
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* C2。両方のプラットフォームが特定できなければ error、片方だけなら、もう片方は info(C3 はそのプラットフォームを飛ばす)。
|
|
110
|
+
* Expo の設定の評価の失敗・複数の .xcodeproj / アプリのターゲット・flavor の無視・Expo の設定とネイティブのずれは info で添える
|
|
111
|
+
*/
|
|
112
|
+
export const resolveIds = (project: ProjectInfo): ResolvedIds => {
|
|
113
|
+
const config = project.expo?.config ?? null;
|
|
114
|
+
const ios = resolvePlatform(
|
|
115
|
+
IOS,
|
|
116
|
+
project,
|
|
117
|
+
config?.bundleIdentifier ?? null,
|
|
118
|
+
project.ios?.bundleId ?? null
|
|
119
|
+
);
|
|
120
|
+
const android = resolvePlatform(
|
|
121
|
+
ANDROID,
|
|
122
|
+
project,
|
|
123
|
+
config?.package ?? null,
|
|
124
|
+
project.android?.packageName ?? null
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const entries: CheckEntry[] = [];
|
|
128
|
+
if (ios.id === null && android.id === null) {
|
|
129
|
+
entries.push({
|
|
130
|
+
id: 'C2',
|
|
131
|
+
level: 'error',
|
|
132
|
+
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`,
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
for (const [spec, resolved] of [
|
|
136
|
+
[IOS, ios],
|
|
137
|
+
[ANDROID, android],
|
|
138
|
+
] as const) {
|
|
139
|
+
entries.push(
|
|
140
|
+
resolved.id === null
|
|
141
|
+
? {
|
|
142
|
+
id: 'C2',
|
|
143
|
+
level: 'info',
|
|
144
|
+
message: `could not determine the ${spec.name} ${spec.idName}: ${resolved.reason}. C3 skips ${spec.name}`,
|
|
145
|
+
}
|
|
146
|
+
: { id: 'C2', level: 'ok', message: resolved.message }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const notes = [
|
|
152
|
+
...(project.expo?.notes ?? []),
|
|
153
|
+
...(project.ios?.notes ?? []),
|
|
154
|
+
...ios.notes,
|
|
155
|
+
...(project.android?.notes ?? []),
|
|
156
|
+
...android.notes,
|
|
157
|
+
];
|
|
158
|
+
for (const message of notes) {
|
|
159
|
+
entries.push({ id: 'C2', level: 'info', message });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { ios: ios.id, android: android.id, entries };
|
|
163
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// C1: react-native-store-version がインストールされていて v2 以上か
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { compareVersion } from '../../compare.ts';
|
|
5
|
+
import { findPackageJson } from '../packages.ts';
|
|
6
|
+
import type { CheckEntry } from '../result.ts';
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = 'react-native-store-version';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* v2 の最初の版(プレリリースを含む)。`2.0.0-beta.x` も `2.0.0` もこれ以上になる
|
|
12
|
+
*/
|
|
13
|
+
const V2_FLOOR = '2.0.0-0';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* C1。`<cwd>` から `react-native-store-version/package.json` を探して({@link findPackageJson})`name` と `version` を読む。
|
|
17
|
+
* `npm:react-native-store-version@1` のようなエイリアス名で入れたもの(`react-native-store-version-v1` など)は見ない。
|
|
18
|
+
*
|
|
19
|
+
* - 見つからない / 読めない / 別のパッケージ / version が比較できない → error
|
|
20
|
+
* - v2 より前(`2.0.0-0` より小さい)→ info
|
|
21
|
+
*/
|
|
22
|
+
export const checkInstalled = (cwd: string): CheckEntry[] => {
|
|
23
|
+
const entry = (level: CheckEntry['level'], message: string): CheckEntry[] => [
|
|
24
|
+
{ id: 'C1', level, message },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const path = findPackageJson(cwd, PACKAGE_NAME);
|
|
28
|
+
if (path === null) {
|
|
29
|
+
return entry(
|
|
30
|
+
'error',
|
|
31
|
+
`${PACKAGE_NAME} is not installed (no node_modules/${PACKAGE_NAME}/package.json in ${cwd} or its parents)`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let json: unknown;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(readFileSync(path, 'utf8'));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return entry(
|
|
40
|
+
'error',
|
|
41
|
+
`could not read ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const { name, version } = (json ?? {}) as {
|
|
45
|
+
name?: unknown;
|
|
46
|
+
version?: unknown;
|
|
47
|
+
};
|
|
48
|
+
if (name !== PACKAGE_NAME) {
|
|
49
|
+
return entry(
|
|
50
|
+
'error',
|
|
51
|
+
`${path} is not ${PACKAGE_NAME} (name: ${JSON.stringify(name)})`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let result: ReturnType<typeof compareVersion>;
|
|
56
|
+
try {
|
|
57
|
+
if (typeof version !== 'string') {
|
|
58
|
+
throw new TypeError('not a string');
|
|
59
|
+
}
|
|
60
|
+
// compareVersion(local, remote) は remote の方が新しいときに 'new'。つまり version < 2.0.0-0
|
|
61
|
+
result = compareVersion(version, V2_FLOOR);
|
|
62
|
+
} catch {
|
|
63
|
+
return entry(
|
|
64
|
+
'error',
|
|
65
|
+
`could not read the version of ${PACKAGE_NAME} in ${path} (version: ${JSON.stringify(version)})`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (result === 'new') {
|
|
69
|
+
return entry(
|
|
70
|
+
'info',
|
|
71
|
+
`${PACKAGE_NAME} ${version} is v1. StoreVersionProvider and this check are for v2 or later`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return entry('ok', `${PACKAGE_NAME} ${version} is installed`);
|
|
75
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// C7: version / ID の自動取得に使う optional peer があるか。パッケージの解決だけを見て、
|
|
2
|
+
// アプリのソースは検索しない(Provider に値を渡しているかは見ない)
|
|
3
|
+
import { isInstalled } from '../packages.ts';
|
|
4
|
+
import type { CheckEntry } from '../result.ts';
|
|
5
|
+
|
|
6
|
+
/** 自動取得に使う optional peer(src/detect.ts が読む順) */
|
|
7
|
+
export const AUTO_DETECT_PEERS = [
|
|
8
|
+
'expo-application',
|
|
9
|
+
'react-native-device-info',
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* C7。どちらかが `<cwd>` から見つかれば({@link isInstalled})ok、どちらも無ければ info
|
|
14
|
+
* (`version` / `bundleId` / `packageName` を Provider に渡していれば不要なので、warning にはしない)
|
|
15
|
+
*/
|
|
16
|
+
export const checkPeers = (cwd: string): CheckEntry[] => {
|
|
17
|
+
const found = AUTO_DETECT_PEERS.filter((name) => isInstalled(cwd, name));
|
|
18
|
+
if (found.length > 0) {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
id: 'C7',
|
|
22
|
+
level: 'ok',
|
|
23
|
+
message: `${found.join(' and ')} found for detecting the app version and ID`,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
id: 'C7',
|
|
30
|
+
level: 'info',
|
|
31
|
+
message: `neither ${AUTO_DETECT_PEERS.join(' nor ')} is installed. Pass version / bundleId / packageName to StoreVersionProvider, or install one of them`,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
};
|