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,71 @@
1
+ // C3: その ID でストアから引けるか。ストアの取得はライブラリ本体の fetcher をそのまま使う
2
+ // (アプリが起動時に使うのと同じ取得先・同じ抽出)
3
+ import { StoreVersionError } from '../../errors.js';
4
+ import { fetchAndroidVersion } from '../../fetchers/android.js';
5
+ import { lookupIOS } from '../../fetchers/ios.js';
6
+ /** C3 の 1 回の取得のタイムアウト(ms) */
7
+ export const STORE_TIMEOUT_MS = 10_000;
8
+ const describeError = (error) => error instanceof StoreVersionError
9
+ ? `${error.code}: ${error.message}`
10
+ : error instanceof Error
11
+ ? error.message
12
+ : String(error);
13
+ const androidFailure = (packageName, error) => {
14
+ const code = error instanceof StoreVersionError ? error.code : undefined;
15
+ if (code === 'NOT_FOUND') {
16
+ return `package "${packageName}" is not found on Google Play: not published yet, a wrong ID, or not available in the country of this network`;
17
+ }
18
+ // ページは取れたが版を比較できない(Varies with device / `50616-r-2026-11-03` のような版)。アプリの起動時の判定でも
19
+ // ストアの版は使えない(INVALID_VERSION で onError になり UI を出さない)ので、取り直しても直らない失敗として分ける
20
+ if (code === 'INVALID_VERSION') {
21
+ return `package "${packageName}" is on Google Play, but its version cannot be compared (${error.message}). The store version cannot drive force / warning updates, so use getLatestVersion / getMinimumVersion`;
22
+ }
23
+ return `could not get package "${packageName}" from Google Play (access from CI can be unstable): ${describeError(error)}`;
24
+ };
25
+ /**
26
+ * C3。ID ごとにストアを引き、見つかれば ok(ストアの版を添える)、失敗はすべて warning
27
+ * (`NOT_FOUND` は「ストアに見つからない」。ほかの失敗も、CI からのストア、特に Play Store へのアクセスは不安定なので error にしない)。
28
+ * Android の `INVALID_VERSION`(Google Play にはあるが版を比較できない)は、getLatestVersion / getMinimumVersion を勧める文面にする
29
+ * (iOS の lookupIOS は版を検証しないので INVALID_VERSION にならない)。ID が 1 つも無ければ skipped
30
+ */
31
+ export const checkStore = async (ids, { country }) => {
32
+ const tasks = [];
33
+ if (ids.ios) {
34
+ const bundleId = ids.ios;
35
+ const where = `the App Store (${country})`;
36
+ tasks.push(lookupIOS({ bundleId }, country, { timeoutMs: STORE_TIMEOUT_MS }).then(({ version }) => ({
37
+ id: 'C3',
38
+ level: 'ok',
39
+ message: `bundleId "${bundleId}" is on ${where} (version ${version})`,
40
+ }), (error) => ({
41
+ id: 'C3',
42
+ level: 'warning',
43
+ message: error instanceof StoreVersionError && error.code === 'NOT_FOUND'
44
+ ? `bundleId "${bundleId}" is not found on ${where}: not published yet, a wrong ID, or not available in that country`
45
+ : `could not look up bundleId "${bundleId}" on ${where}: ${describeError(error)}`,
46
+ })));
47
+ }
48
+ if (ids.android) {
49
+ const packageName = ids.android;
50
+ tasks.push(fetchAndroidVersion({ packageName }, { timeoutMs: STORE_TIMEOUT_MS }).then(({ version }) => ({
51
+ id: 'C3',
52
+ level: 'ok',
53
+ message: `package "${packageName}" is on Google Play (version ${version})`,
54
+ }), (error) => ({
55
+ id: 'C3',
56
+ level: 'warning',
57
+ message: androidFailure(packageName, error),
58
+ })));
59
+ }
60
+ if (tasks.length === 0) {
61
+ return [
62
+ {
63
+ id: 'C3',
64
+ level: 'skipped',
65
+ message: 'no iOS bundleId or Android package to look up',
66
+ },
67
+ ];
68
+ }
69
+ // 各 task は reject しない(失敗は then の第 2 引数で warning にしている)
70
+ return Promise.all(tasks);
71
+ };
@@ -0,0 +1,83 @@
1
+ const describe = (name, { label, value, file }) => `${name} ${label} ${value} (${file})`;
2
+ /**
3
+ * C4。ネイティブのディレクトリ(`ios/` / `android/`)がどちらも無い(CNG)なら skipped(指摘ではないので注釈にしない)。
4
+ * iOS と Android(ネイティブ同士)、Expo の設定の version と各ネイティブを比べ、ずれている組ごとに warning。
5
+ * 特定できないネイティブの値は、その値を含む比較を飛ばして info(Expo の version が書かれていなければ黙って比べない)。比べる組が無ければ skipped。比較は文字列の一致
6
+ * (`1.0` と `1.0.0` もずれとして出す。ストアに出る値の書き方を揃えるため)
7
+ */
8
+ export const checkVersions = (project) => {
9
+ if (project.ios === null && project.android === null) {
10
+ return [
11
+ {
12
+ id: 'C4',
13
+ level: 'skipped',
14
+ message: 'there is no ios/ or android/ directory (Continuous Native Generation), so there are no native versions to compare',
15
+ },
16
+ ];
17
+ }
18
+ const sources = [];
19
+ for (const [name, lookup] of [
20
+ ['iOS', project.ios?.version],
21
+ ['Android', project.android?.version],
22
+ ]) {
23
+ if (lookup !== undefined) {
24
+ sources.push(lookup.value === null
25
+ ? { name, found: null, reason: lookup.reason }
26
+ : { name, found: lookup, reason: '' });
27
+ }
28
+ }
29
+ // Expo の設定の version は任意(書かなければ Expo が package.json の値で補う。expo.ts の writtenVersion)なので、
30
+ // 書かれていないときは比べず、info も出さない(bare RN に expo を入れただけのアプリで毎回出るのを避ける)
31
+ const config = project.expo?.config;
32
+ if (config && config.version !== null) {
33
+ sources.push({
34
+ name: 'Expo',
35
+ found: { label: 'version', value: config.version, file: config.file },
36
+ reason: '',
37
+ });
38
+ }
39
+ const entries = [];
40
+ for (const { name, found, reason } of sources) {
41
+ if (found === null) {
42
+ entries.push({
43
+ id: 'C4',
44
+ level: 'info',
45
+ message: `could not determine the ${name} version (${reason}), so it is not compared`,
46
+ });
47
+ }
48
+ }
49
+ const known = sources.filter((source) => source.found !== null);
50
+ if (known.length < 2) {
51
+ if (entries.length === 0) {
52
+ entries.push({
53
+ id: 'C4',
54
+ level: 'skipped',
55
+ message: `only one version was found (${known.map(({ name, found }) => describe(name, found)).join('')}), so there is nothing to compare`,
56
+ });
57
+ }
58
+ return entries;
59
+ }
60
+ let differs = false;
61
+ for (let i = 0; i < known.length; i += 1) {
62
+ for (let j = i + 1; j < known.length; j += 1) {
63
+ const a = known[i];
64
+ const b = known[j];
65
+ if (a.found.value !== b.found.value) {
66
+ differs = true;
67
+ entries.push({
68
+ id: 'C4',
69
+ level: 'warning',
70
+ message: `${describe(a.name, a.found)} differs from ${describe(b.name, b.found)}`,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ if (!differs) {
76
+ entries.push({
77
+ id: 'C4',
78
+ level: 'ok',
79
+ message: `the versions match: ${known.map(({ name, found }) => describe(name, found)).join(', ')}`,
80
+ });
81
+ }
82
+ return entries;
83
+ };
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // CLI の入口(package.json の bin。Node 専用で、アプリのバンドルには入らない。exports からは辿れない)。
3
+ // ここでは Node の版だけを確かめ、満たすときだけ本体(main.ts)を読み込む。ESM の static import は巻き上げられて
4
+ // この判定より先に評価される(古い Node では本体が使う API の読み込みで先に落ちうる)ので、ほかのモジュールを
5
+ // static に import しない(__tests__/cli/index.test.ts でソースを読んで確かめている)
6
+ /** CLI が動く Node の最低の版(package.json の engines には書かない) */
7
+ export const MIN_NODE_VERSION = '22.14.0';
8
+ /**
9
+ * `process.versions.node`(`22.14.0` など)が {@link MIN_NODE_VERSION} 以上か。major / minor だけを見る。
10
+ * 比較のモジュールも import しないため、手で分ける。読めない値は満たさない扱い
11
+ */
12
+ export const isSupportedNode = (version) => {
13
+ const [major, minor] = version.split('.').map((part) => Number(part));
14
+ if (!Number.isInteger(major) || !Number.isInteger(minor)) {
15
+ return false;
16
+ }
17
+ return major > 22 || (major === 22 && minor >= 14);
18
+ };
19
+ const start = async () => {
20
+ const current = process.versions.node;
21
+ if (!isSupportedNode(current)) {
22
+ const message = `the CLI requires Node.js ${MIN_NODE_VERSION} or later (current: ${current})`;
23
+ process.stderr.write(`react-native-store-version: ${message}\n`);
24
+ // GitHub Actions 上では、ログの stderr だけでは理由が PR の画面に出ないので注釈も出す(main.ts の失敗と同じ title)。
25
+ // output.ts の escapeData は import できない(上のとおり)ので、同じ最小限のエスケープ(`%` を最初に)をここで行う
26
+ if (process.env.GITHUB_ACTIONS === 'true') {
27
+ const escaped = message
28
+ .replace(/%/g, '%25')
29
+ .replace(/\r/g, '%0D')
30
+ .replace(/\n/g, '%0A');
31
+ process.stderr.write(`::error title=react-native-store-version check::${escaped}\n`);
32
+ }
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const { main } = await import('./main.js');
37
+ await main();
38
+ };
39
+ // main() は自分で失敗を受けて終了コードにするので、ここに来るのは本体の読み込みの失敗だけ
40
+ start().catch((error) => {
41
+ process.stderr.write(`react-native-store-version: ${error instanceof Error ? error.message : String(error)}\n`);
42
+ process.exitCode = 1;
43
+ });
@@ -0,0 +1,182 @@
1
+ // CLI の本体(index.ts が Node の版を確かめてから読み込む)。引数の解析・チェックの実行・出力・終了コード。
2
+ // Node の標準モジュールと、react-native を読まないライブラリ本体のファイルだけを使う(CLI のための依存は足さない)
3
+ import { appendFileSync, statSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { runChecks } from './check.js';
7
+ import { formatGithub, formatGithubFailure, formatJson, formatSummary, formatText, oneLine, } from './output.js';
8
+ import { countEntries, exitCodeOf } from './result.js';
9
+ /** C3 の App Store の国の既定 */
10
+ export const DEFAULT_COUNTRY = 'us';
11
+ /** `--help` の出力 */
12
+ export const USAGE = `Usage: react-native-store-version check [options]
13
+
14
+ Checks the setup of react-native-store-version in an app.
15
+
16
+ Options:
17
+ --cwd <dir> the app directory (default: the current directory)
18
+ --json print the result as JSON
19
+ --strict exit with 1 on warnings too
20
+ --ignore <ids> comma-separated checks not to run (e.g. C4,C8)
21
+ --country <xx> the App Store country for C3 (default: ${DEFAULT_COUNTRY})
22
+ -h, --help show this help
23
+ `;
24
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
25
+ /** 引数の誤り。使い方と一緒に stderr に出し、終了コード 1 にする */
26
+ class UsageError extends Error {
27
+ }
28
+ const parseIgnore = (values) => {
29
+ const ids = new Set();
30
+ for (const value of values) {
31
+ for (const part of value.split(',')) {
32
+ const id = part.trim().toUpperCase();
33
+ if (id === '') {
34
+ continue;
35
+ }
36
+ // ID の形だけを見る(無い ID は何も止めないだけなので、誤りにはしない)
37
+ if (!/^C\d+$/.test(id)) {
38
+ throw new UsageError(`--ignore: ${JSON.stringify(part.trim())} is not a check ID (e.g. C1)`);
39
+ }
40
+ ids.add(id);
41
+ }
42
+ }
43
+ return ids;
44
+ };
45
+ const parse = (argv) => parseArgs({
46
+ args: [...argv],
47
+ allowPositionals: true,
48
+ strict: true,
49
+ options: {
50
+ cwd: { type: 'string' },
51
+ json: { type: 'boolean' },
52
+ strict: { type: 'boolean' },
53
+ ignore: { type: 'string', multiple: true },
54
+ country: { type: 'string' },
55
+ help: { type: 'boolean', short: 'h' },
56
+ },
57
+ });
58
+ /** 引数を解析する。`--help` なら null */
59
+ const parseCheckArgs = (argv, context) => {
60
+ let parsed;
61
+ try {
62
+ parsed = parse(argv);
63
+ }
64
+ catch (error) {
65
+ // parseArgs の誤り(不明なオプション・値の無い --cwd など)
66
+ throw new UsageError(errorMessage(error));
67
+ }
68
+ const { values, positionals } = parsed;
69
+ if (values.help) {
70
+ return null;
71
+ }
72
+ const [command, ...rest] = positionals;
73
+ if (command === undefined) {
74
+ throw new UsageError('no command given');
75
+ }
76
+ if (command !== 'check') {
77
+ throw new UsageError(`unknown command: ${command}`);
78
+ }
79
+ if (rest.length > 0) {
80
+ throw new UsageError(`unexpected argument: ${rest[0]}`);
81
+ }
82
+ const cwd = resolve(context.cwd, values.cwd ?? '.');
83
+ let isDirectory = false;
84
+ try {
85
+ isDirectory = statSync(cwd).isDirectory();
86
+ }
87
+ catch {
88
+ // 無いパスも、下でディレクトリでない扱いにする
89
+ }
90
+ if (!isDirectory) {
91
+ throw new UsageError(`--cwd: ${cwd} is not a directory`);
92
+ }
93
+ const country = values.country ?? DEFAULT_COUNTRY;
94
+ // iTunes Lookup の country は 2 文字の国コード
95
+ if (!/^[A-Za-z]{2}$/.test(country)) {
96
+ throw new UsageError(`--country: ${JSON.stringify(country)} is not a 2-letter country code`);
97
+ }
98
+ return {
99
+ cwd,
100
+ json: values.json ?? false,
101
+ strict: values.strict ?? false,
102
+ ignore: parseIgnore(values.ignore ?? []),
103
+ country: country.toLowerCase(),
104
+ };
105
+ };
106
+ /**
107
+ * CLI を引数 `argv`(`process.argv.slice(2)`)で動かし、終了コードを返す。例外は投げない
108
+ * (引数の誤り・想定外の失敗は stderr に出して 1。GitHub Actions 上では stderr に `::error::` の注釈も出し、
109
+ * stdout には何も出さない(`--json` の stdout を JSON 以外で汚さないため。runner は stderr のコマンドも処理する))
110
+ */
111
+ export const run = async (argv, context) => {
112
+ const onGithub = context.env.GITHUB_ACTIONS === 'true';
113
+ const fail = (message, usage) => {
114
+ // GitHub Actions 上では、message の改行で次の行がワークフローコマンドにならないよう 1 行にする
115
+ const line = onGithub ? oneLine(message) : message;
116
+ context.stderr(`error: ${line}\n${usage ? `\n${USAGE}` : ''}`);
117
+ if (onGithub) {
118
+ context.stderr(formatGithubFailure(message));
119
+ }
120
+ return 1;
121
+ };
122
+ let args;
123
+ try {
124
+ args = parseCheckArgs(argv, context);
125
+ }
126
+ catch (error) {
127
+ return fail(errorMessage(error), true);
128
+ }
129
+ if (args === null) {
130
+ context.stdout(USAGE);
131
+ return 0;
132
+ }
133
+ let report;
134
+ try {
135
+ const entries = await runChecks({
136
+ cwd: args.cwd,
137
+ country: args.country,
138
+ ignore: args.ignore,
139
+ });
140
+ const counts = countEntries(entries);
141
+ report = {
142
+ cwd: args.cwd,
143
+ entries,
144
+ counts,
145
+ exitCode: exitCodeOf(counts, args.strict),
146
+ };
147
+ }
148
+ catch (error) {
149
+ return fail(errorMessage(error), false);
150
+ }
151
+ if (args.json) {
152
+ // stdout は JSON だけにする(GitHub Actions 上でも注釈を混ぜない)
153
+ context.stdout(formatJson(report));
154
+ }
155
+ else if (onGithub) {
156
+ context.stdout(formatGithub(report));
157
+ }
158
+ else {
159
+ context.stdout(formatText(report));
160
+ }
161
+ // Job Summary はファイルへの追記で stdout を汚さないので、--json でも書く
162
+ const summary = context.env.GITHUB_STEP_SUMMARY;
163
+ if (onGithub && summary) {
164
+ try {
165
+ appendFileSync(summary, formatSummary(report));
166
+ }
167
+ catch (error) {
168
+ // Job Summary は付け足しの表示なので、書けなくてもチェックの結果(終了コード)は変えない
169
+ context.stderr(`warning: could not write the job summary: ${errorMessage(error)}\n`);
170
+ }
171
+ }
172
+ return report.exitCode;
173
+ };
174
+ /** `process` につないで {@link run} を動かす(index.ts から呼ぶ) */
175
+ export const main = async () => {
176
+ process.exitCode = await run(process.argv.slice(2), {
177
+ cwd: process.cwd(),
178
+ env: process.env,
179
+ stdout: (text) => process.stdout.write(text),
180
+ stderr: (text) => process.stderr.write(text),
181
+ });
182
+ };
@@ -0,0 +1,77 @@
1
+ const COMMAND = 'react-native-store-version check';
2
+ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
3
+ const countsLine = ({ error, warning, info }) => `${plural(error, 'error')}, ${plural(warning, 'warning')}, ${info} info`;
4
+ /**
5
+ * 1 行に出す値の `\r` / `\n` を空白にする。GitHub Actions 上では、値の中の改行の後ろが `::` で始まると
6
+ * ワークフローコマンドとして解釈されてしまうため(注釈にする行は escapeData で改行をエスケープする)
7
+ */
8
+ export const oneLine = (value) => value.replace(/[\r\n]/g, ' ');
9
+ const headerLine = (cwd) => `${COMMAND}: ${oneLine(cwd)}`;
10
+ const textLine = ({ id, level, message }) => ` ${level.padEnd(8)} ${id} ${oneLine(message)}`;
11
+ /** テキストの出力(末尾の改行まで) */
12
+ export const formatText = (report) => [
13
+ headerLine(report.cwd),
14
+ ...(report.entries.length === 0
15
+ ? [' (no checks were run)']
16
+ : report.entries.map(textLine)),
17
+ countsLine(report.counts),
18
+ '',
19
+ ].join('\n');
20
+ /** `--json` の出力(末尾の改行まで)。entries は `results` に、出したときと同じ順で入る */
21
+ export const formatJson = (report) => `${JSON.stringify({
22
+ cwd: report.cwd,
23
+ results: report.entries,
24
+ counts: report.counts,
25
+ exitCode: report.exitCode,
26
+ }, null, 2)}\n`;
27
+ /** ワークフローコマンドの message のエスケープ(`%` を最初に置き換える。後の置き換えで入る `%` を二重にしないため) */
28
+ export const escapeData = (value) => value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
29
+ /** ワークフローコマンドの property(`title` など)のエスケープ。message のエスケープに `:` と `,` を足す */
30
+ export const escapeProperty = (value) => escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
31
+ const ANNOTATION = {
32
+ error: 'error',
33
+ warning: 'warning',
34
+ info: 'notice',
35
+ };
36
+ /**
37
+ * GitHub Actions 上の出力(末尾の改行まで)。error / warning / info は `::error::` / `::warning::` / `::notice::` の注釈にし
38
+ * (ログにも本文が出る)、ok / skipped はテキストと同じ行で出す
39
+ */
40
+ export const formatGithub = (report) => [
41
+ headerLine(report.cwd),
42
+ ...report.entries.map((entry) => entry.level === 'ok' || entry.level === 'skipped'
43
+ ? textLine(entry)
44
+ : `::${ANNOTATION[entry.level]} title=${escapeProperty(`${COMMAND} ${entry.id}`)}::${escapeData(entry.message)}`),
45
+ countsLine(report.counts),
46
+ '',
47
+ ].join('\n');
48
+ /**
49
+ * チェックの結果が出せなかったとき(引数の誤り・想定外の失敗)の GitHub Actions の注釈(末尾の改行まで)。
50
+ * ログの stderr だけでは失敗の理由が PR の画面に出ないため
51
+ */
52
+ export const formatGithubFailure = (message) => `::error title=${escapeProperty(COMMAND)}::${escapeData(message)}\n`;
53
+ /** Markdown の表のセルに入れる文字列。HTML として読まれないようにし、`|` と改行で表が崩れないようにする */
54
+ const escapeCell = (value) => value
55
+ .replace(/&/g, '&amp;')
56
+ .replace(/</g, '&lt;')
57
+ .replace(/>/g, '&gt;')
58
+ .replace(/\|/g, '\\|')
59
+ .replace(/\r?\n/g, '<br>');
60
+ /** Job Summary(`GITHUB_STEP_SUMMARY` のファイル)に追記する Markdown(前後を空行で区切る) */
61
+ export const formatSummary = (report) => [
62
+ '',
63
+ `### ${COMMAND}`,
64
+ '',
65
+ `Directory: <code>${escapeCell(report.cwd)}</code>`,
66
+ '',
67
+ ...(report.entries.length === 0
68
+ ? ['No checks were run.']
69
+ : [
70
+ '| Check | Result | Message |',
71
+ '| --- | --- | --- |',
72
+ ...report.entries.map(({ id, level, message }) => `| ${id} | ${level} | ${escapeCell(message)} |`),
73
+ ]),
74
+ '',
75
+ countsLine(report.counts),
76
+ '',
77
+ ].join('\n');
@@ -0,0 +1,40 @@
1
+ // `<cwd>` から見たパッケージの探し方。アプリ(Metro)と同じく、`<cwd>` から親へ辿った各ディレクトリの node_modules/<name> を見る
2
+ // (monorepo の巻き上げも拾う)。
3
+ //
4
+ // createRequire の require.resolve は使わない。Node の解決は node_modules の連鎖のほかに NODE_PATH と
5
+ // グローバルのフォルダ(~/.node_modules など)も見るが、pnpm の bin の shim は NODE_PATH に node_modules/.pnpm/node_modules
6
+ // (巻き上げた推移的な依存)や、この CLI 自身のパッケージがある node_modules/.pnpm/react-native-store-version@x/node_modules を
7
+ // 入れて起動する。そのため `pnpm exec` / `pnpm dlx` から動かすと、アプリに入っていないパッケージ(この CLI 自身を含む)も
8
+ // 見つかってしまう。Metro は NODE_PATH を見ないので、ここも見ない
9
+ import { statSync } from 'node:fs';
10
+ import { basename, dirname, join, resolve } from 'node:path';
11
+ const isFile = (path) => {
12
+ try {
13
+ return statSync(path).isFile();
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ };
19
+ /**
20
+ * `<cwd>` から親へ辿り、最初に見つかった `node_modules/<name>/package.json` のパス(symlink は辿らないままのパス)。
21
+ * 無ければ null。Node と同じく、`node_modules` という名前のディレクトリの下に node_modules は探さない
22
+ */
23
+ export const findPackageJson = (cwd, name) => {
24
+ let dir = resolve(cwd);
25
+ for (;;) {
26
+ if (basename(dir) !== 'node_modules') {
27
+ const candidate = join(dir, 'node_modules', name, 'package.json');
28
+ if (isFile(candidate)) {
29
+ return candidate;
30
+ }
31
+ }
32
+ const parent = dirname(dir);
33
+ if (parent === dir) {
34
+ return null;
35
+ }
36
+ dir = parent;
37
+ }
38
+ };
39
+ /** `<cwd>` から `name` のパッケージが見つかるか({@link findPackageJson}) */
40
+ export const isInstalled = (cwd, name) => findPackageJson(cwd, name) !== null;
@@ -0,0 +1,60 @@
1
+ // Android のネイティブのプロジェクト(`android/`)から package(C2)と版(C4)を読む。
2
+ // `android/app/build.gradle`(無ければ `build.gradle.kts`)の最初の `defaultConfig { … }` と `android { namespace … }` を見る
3
+ import { join } from 'node:path';
4
+ import { displayPath, isDirectory, isFile, readText, } from './files.js';
5
+ import { findBlock, findStatement, hasWord, maskGradle, } from './gradle.js';
6
+ const GRADLE_FILES = ['build.gradle', 'build.gradle.kts'];
7
+ /** `android/` が無ければ null(CNG)。読めない・特定できないものは {@link Lookup} の reason にする */
8
+ export const readAndroidProject = (cwd) => {
9
+ const androidDir = join(cwd, 'android');
10
+ if (!isDirectory(androidDir)) {
11
+ return null;
12
+ }
13
+ const notes = [];
14
+ const unknown = (reason) => ({
15
+ packageName: { value: null, reason },
16
+ version: { value: null, reason },
17
+ notes,
18
+ });
19
+ const path = GRADLE_FILES.map((name) => join(androidDir, 'app', name)).find(isFile);
20
+ if (path === undefined) {
21
+ return unknown('there is no android/app/build.gradle or android/app/build.gradle.kts');
22
+ }
23
+ const file = displayPath(cwd, path);
24
+ const source = readText(path);
25
+ if (source === null) {
26
+ return unknown(`could not read ${file}`);
27
+ }
28
+ const masked = maskGradle(source);
29
+ const defaultConfig = findBlock(masked, 'defaultConfig');
30
+ /** `range` の直下の `key` を読む。無ければ null */
31
+ const statement = (key, range) => {
32
+ const found = range && findStatement(source, masked, key, range);
33
+ if (!found) {
34
+ return null;
35
+ }
36
+ if (found.literal === undefined) {
37
+ return {
38
+ value: null,
39
+ reason: `${key} in ${file} is not a string literal (${found.expression})`,
40
+ };
41
+ }
42
+ return { value: found.literal, label: key, file };
43
+ };
44
+ // applicationId が無ければ namespace(AGP は applicationId の既定に namespace を使う)。applicationId が変数なら
45
+ // namespace と同じとは限らないので、namespace には戻らず「特定できない」にする
46
+ const packageName = statement('applicationId', defaultConfig) ??
47
+ statement('namespace', findBlock(masked, 'android')) ?? {
48
+ value: null,
49
+ reason: `there is no applicationId in defaultConfig or namespace in android { } of ${file}`,
50
+ };
51
+ const flavors = ['productFlavors', 'applicationIdSuffix'].filter((word) => hasWord(masked, word));
52
+ if (flavors.length > 0) {
53
+ notes.push(`${file} has ${flavors.join(' and ')}; ${flavors.length > 1 ? 'they are' : 'it is'} ignored, and the package without a flavor or suffix is used`);
54
+ }
55
+ const version = statement('versionName', defaultConfig) ?? {
56
+ value: null,
57
+ reason: `there is no versionName in defaultConfig of ${file}`,
58
+ };
59
+ return { packageName, version, notes };
60
+ };