react-native-store-version 1.4.2 → 2.0.0
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 +28 -0
- package/README.md +514 -43
- 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 +83 -44
- 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 -48
- package/.github/workflows/release.yml +0 -259
- 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,122 @@
|
|
|
1
|
+
// `check` の結果の出力: テキスト / `--json` / GitHub Actions の注釈と Job Summary
|
|
2
|
+
import type { CheckEntry, Counts } from './result.ts';
|
|
3
|
+
|
|
4
|
+
/** 出力に渡す結果 */
|
|
5
|
+
export type Report = {
|
|
6
|
+
/** 確かめたディレクトリ(絶対パス) */
|
|
7
|
+
cwd: string;
|
|
8
|
+
entries: readonly CheckEntry[];
|
|
9
|
+
counts: Counts;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const COMMAND = 'react-native-store-version check';
|
|
14
|
+
|
|
15
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
16
|
+
|
|
17
|
+
const countsLine = ({ error, warning, info }: Counts) =>
|
|
18
|
+
`${plural(error, 'error')}, ${plural(warning, 'warning')}, ${info} info`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 1 行に出す値の `\r` / `\n` を空白にする。GitHub Actions 上では、値の中の改行の後ろが `::` で始まると
|
|
22
|
+
* ワークフローコマンドとして解釈されてしまうため(注釈にする行は escapeData で改行をエスケープする)
|
|
23
|
+
*/
|
|
24
|
+
export const oneLine = (value: string): string => value.replace(/[\r\n]/g, ' ');
|
|
25
|
+
|
|
26
|
+
const headerLine = (cwd: string) => `${COMMAND}: ${oneLine(cwd)}`;
|
|
27
|
+
|
|
28
|
+
const textLine = ({ id, level, message }: CheckEntry) =>
|
|
29
|
+
` ${level.padEnd(8)} ${id} ${oneLine(message)}`;
|
|
30
|
+
|
|
31
|
+
/** テキストの出力(末尾の改行まで) */
|
|
32
|
+
export const formatText = (report: Report): string =>
|
|
33
|
+
[
|
|
34
|
+
headerLine(report.cwd),
|
|
35
|
+
...(report.entries.length === 0
|
|
36
|
+
? [' (no checks were run)']
|
|
37
|
+
: report.entries.map(textLine)),
|
|
38
|
+
countsLine(report.counts),
|
|
39
|
+
'',
|
|
40
|
+
].join('\n');
|
|
41
|
+
|
|
42
|
+
/** `--json` の出力(末尾の改行まで)。entries は `results` に、出したときと同じ順で入る */
|
|
43
|
+
export const formatJson = (report: Report): string =>
|
|
44
|
+
`${JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
cwd: report.cwd,
|
|
47
|
+
results: report.entries,
|
|
48
|
+
counts: report.counts,
|
|
49
|
+
exitCode: report.exitCode,
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2
|
|
53
|
+
)}\n`;
|
|
54
|
+
|
|
55
|
+
/** ワークフローコマンドの message のエスケープ(`%` を最初に置き換える。後の置き換えで入る `%` を二重にしないため) */
|
|
56
|
+
export const escapeData = (value: string): string =>
|
|
57
|
+
value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
58
|
+
|
|
59
|
+
/** ワークフローコマンドの property(`title` など)のエスケープ。message のエスケープに `:` と `,` を足す */
|
|
60
|
+
export const escapeProperty = (value: string): string =>
|
|
61
|
+
escapeData(value).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
62
|
+
|
|
63
|
+
const ANNOTATION = {
|
|
64
|
+
error: 'error',
|
|
65
|
+
warning: 'warning',
|
|
66
|
+
info: 'notice',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* GitHub Actions 上の出力(末尾の改行まで)。error / warning / info は `::error::` / `::warning::` / `::notice::` の注釈にし
|
|
71
|
+
* (ログにも本文が出る)、ok / skipped はテキストと同じ行で出す
|
|
72
|
+
*/
|
|
73
|
+
export const formatGithub = (report: Report): string =>
|
|
74
|
+
[
|
|
75
|
+
headerLine(report.cwd),
|
|
76
|
+
...report.entries.map((entry) =>
|
|
77
|
+
entry.level === 'ok' || entry.level === 'skipped'
|
|
78
|
+
? textLine(entry)
|
|
79
|
+
: `::${ANNOTATION[entry.level]} title=${escapeProperty(`${COMMAND} ${entry.id}`)}::${escapeData(entry.message)}`
|
|
80
|
+
),
|
|
81
|
+
countsLine(report.counts),
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* チェックの結果が出せなかったとき(引数の誤り・想定外の失敗)の GitHub Actions の注釈(末尾の改行まで)。
|
|
87
|
+
* ログの stderr だけでは失敗の理由が PR の画面に出ないため
|
|
88
|
+
*/
|
|
89
|
+
export const formatGithubFailure = (message: string): string =>
|
|
90
|
+
`::error title=${escapeProperty(COMMAND)}::${escapeData(message)}\n`;
|
|
91
|
+
|
|
92
|
+
/** Markdown の表のセルに入れる文字列。HTML として読まれないようにし、`|` と改行で表が崩れないようにする */
|
|
93
|
+
const escapeCell = (value: string): string =>
|
|
94
|
+
value
|
|
95
|
+
.replace(/&/g, '&')
|
|
96
|
+
.replace(/</g, '<')
|
|
97
|
+
.replace(/>/g, '>')
|
|
98
|
+
.replace(/\|/g, '\\|')
|
|
99
|
+
.replace(/\r?\n/g, '<br>');
|
|
100
|
+
|
|
101
|
+
/** Job Summary(`GITHUB_STEP_SUMMARY` のファイル)に追記する Markdown(前後を空行で区切る) */
|
|
102
|
+
export const formatSummary = (report: Report): string =>
|
|
103
|
+
[
|
|
104
|
+
'',
|
|
105
|
+
`### ${COMMAND}`,
|
|
106
|
+
'',
|
|
107
|
+
`Directory: <code>${escapeCell(report.cwd)}</code>`,
|
|
108
|
+
'',
|
|
109
|
+
...(report.entries.length === 0
|
|
110
|
+
? ['No checks were run.']
|
|
111
|
+
: [
|
|
112
|
+
'| Check | Result | Message |',
|
|
113
|
+
'| --- | --- | --- |',
|
|
114
|
+
...report.entries.map(
|
|
115
|
+
({ id, level, message }) =>
|
|
116
|
+
`| ${id} | ${level} | ${escapeCell(message)} |`
|
|
117
|
+
),
|
|
118
|
+
]),
|
|
119
|
+
'',
|
|
120
|
+
countsLine(report.counts),
|
|
121
|
+
'',
|
|
122
|
+
].join('\n');
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
|
|
12
|
+
const isFile = (path: string): boolean => {
|
|
13
|
+
try {
|
|
14
|
+
return statSync(path).isFile();
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `<cwd>` から親へ辿り、最初に見つかった `node_modules/<name>/package.json` のパス(symlink は辿らないままのパス)。
|
|
22
|
+
* 無ければ null。Node と同じく、`node_modules` という名前のディレクトリの下に node_modules は探さない
|
|
23
|
+
*/
|
|
24
|
+
export const findPackageJson = (cwd: string, name: string): string | null => {
|
|
25
|
+
let dir = resolve(cwd);
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (basename(dir) !== 'node_modules') {
|
|
28
|
+
const candidate = join(dir, 'node_modules', name, 'package.json');
|
|
29
|
+
if (isFile(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** `<cwd>` から `name` のパッケージが見つかるか({@link findPackageJson}) */
|
|
42
|
+
export const isInstalled = (cwd: string, name: string): boolean =>
|
|
43
|
+
findPackageJson(cwd, name) !== null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Android のネイティブのプロジェクト(`android/`)から package(C2)と版(C4)を読む。
|
|
2
|
+
// `android/app/build.gradle`(無ければ `build.gradle.kts`)の最初の `defaultConfig { … }` と `android { namespace … }` を見る
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
displayPath,
|
|
7
|
+
isDirectory,
|
|
8
|
+
isFile,
|
|
9
|
+
type Lookup,
|
|
10
|
+
readText,
|
|
11
|
+
} from './files.ts';
|
|
12
|
+
import {
|
|
13
|
+
findBlock,
|
|
14
|
+
findStatement,
|
|
15
|
+
hasWord,
|
|
16
|
+
maskGradle,
|
|
17
|
+
type Range,
|
|
18
|
+
} from './gradle.ts';
|
|
19
|
+
|
|
20
|
+
/** `android/` から読んだ値。`notes` は C2 が info で出す注意(productFlavors / applicationIdSuffix を無視したこと) */
|
|
21
|
+
export type AndroidProject = {
|
|
22
|
+
packageName: Lookup;
|
|
23
|
+
version: Lookup;
|
|
24
|
+
notes: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const GRADLE_FILES = ['build.gradle', 'build.gradle.kts'];
|
|
28
|
+
|
|
29
|
+
/** `android/` が無ければ null(CNG)。読めない・特定できないものは {@link Lookup} の reason にする */
|
|
30
|
+
export const readAndroidProject = (cwd: string): AndroidProject | null => {
|
|
31
|
+
const androidDir = join(cwd, 'android');
|
|
32
|
+
if (!isDirectory(androidDir)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const notes: string[] = [];
|
|
36
|
+
const unknown = (reason: string): AndroidProject => ({
|
|
37
|
+
packageName: { value: null, reason },
|
|
38
|
+
version: { value: null, reason },
|
|
39
|
+
notes,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const path = GRADLE_FILES.map((name) => join(androidDir, 'app', name)).find(
|
|
43
|
+
isFile
|
|
44
|
+
);
|
|
45
|
+
if (path === undefined) {
|
|
46
|
+
return unknown(
|
|
47
|
+
'there is no android/app/build.gradle or android/app/build.gradle.kts'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const file = displayPath(cwd, path);
|
|
51
|
+
const source = readText(path);
|
|
52
|
+
if (source === null) {
|
|
53
|
+
return unknown(`could not read ${file}`);
|
|
54
|
+
}
|
|
55
|
+
const masked = maskGradle(source);
|
|
56
|
+
const defaultConfig = findBlock(masked, 'defaultConfig');
|
|
57
|
+
|
|
58
|
+
/** `range` の直下の `key` を読む。無ければ null */
|
|
59
|
+
const statement = (key: string, range: Range | null): Lookup | null => {
|
|
60
|
+
const found = range && findStatement(source, masked, key, range);
|
|
61
|
+
if (!found) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (found.literal === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
value: null,
|
|
67
|
+
reason: `${key} in ${file} is not a string literal (${found.expression})`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { value: found.literal, label: key, file };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// applicationId が無ければ namespace(AGP は applicationId の既定に namespace を使う)。applicationId が変数なら
|
|
74
|
+
// namespace と同じとは限らないので、namespace には戻らず「特定できない」にする
|
|
75
|
+
const packageName = statement('applicationId', defaultConfig) ??
|
|
76
|
+
statement('namespace', findBlock(masked, 'android')) ?? {
|
|
77
|
+
value: null,
|
|
78
|
+
reason: `there is no applicationId in defaultConfig or namespace in android { } of ${file}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const flavors = ['productFlavors', 'applicationIdSuffix'].filter((word) =>
|
|
82
|
+
hasWord(masked, word)
|
|
83
|
+
);
|
|
84
|
+
if (flavors.length > 0) {
|
|
85
|
+
notes.push(
|
|
86
|
+
`${file} has ${flavors.join(' and ')}; ${flavors.length > 1 ? 'they are' : 'it is'} ignored, and the package without a flavor or suffix is used`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const version = statement('versionName', defaultConfig) ?? {
|
|
91
|
+
value: null,
|
|
92
|
+
reason: `there is no versionName in defaultConfig of ${file}`,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { packageName, version, notes };
|
|
96
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Expo の設定(app.json / app.config.ts / app.config.js)から bundleIdentifier / package / version を読む(C2 / C4)。
|
|
2
|
+
// アプリに入っている `expo` の `expo/config`(@expo/config の再 export)の getConfig で評価する。pnpm の isolated では
|
|
3
|
+
// アプリから @expo/config を直接は解決できないので、expo のディレクトリを起点に読む。CLI のための依存は足さないので、
|
|
4
|
+
// 型は import せず、返ってきた値(unknown)から必要な形を確かめる
|
|
5
|
+
import { realpathSync } from 'node:fs';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
8
|
+
import { format } from 'node:util';
|
|
9
|
+
|
|
10
|
+
import { findPackageJson } from '../packages.ts';
|
|
11
|
+
import { displayPath, isFile, readText } from './files.ts';
|
|
12
|
+
|
|
13
|
+
/** Expo の設定から読んだ値。無い・文字列でないものは null */
|
|
14
|
+
export type ExpoConfig = {
|
|
15
|
+
/** 値を読んだ設定ファイル({@link displayPath}) */
|
|
16
|
+
file: string;
|
|
17
|
+
bundleIdentifier: string | null;
|
|
18
|
+
package: string | null;
|
|
19
|
+
version: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `expo` が入っているアプリの設定。`config` は読めなかったら null。`notes` は C2 が info で出す注意
|
|
24
|
+
* (getConfig が失敗して app.json の素読みに戻ったことなど)
|
|
25
|
+
*/
|
|
26
|
+
export type ExpoProject = {
|
|
27
|
+
config: ExpoConfig | null;
|
|
28
|
+
notes: string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Obj = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
const isObject = (value: unknown): value is Obj =>
|
|
34
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
|
|
36
|
+
const text = (value: unknown): string | null =>
|
|
37
|
+
typeof value === 'string' && value !== '' ? value : null;
|
|
38
|
+
|
|
39
|
+
/** 失敗の理由を 1 行に詰めたときの長さの上限 */
|
|
40
|
+
const MAX_REASON = 300;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 失敗の理由を 1 行にする。getConfig の失敗は `Error reading Expo config at <path>:` の次の行以降に理由があり、
|
|
44
|
+
* 構文エラーではその後にコードの抜粋(`> 2 | name: ,`)が続く。抜粋の手前までの空でない行をつなぎ、
|
|
45
|
+
* `<cwd>/` を落として(パスは cwd からの相対で出す)、長すぎれば切る
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Error の message。`instanceof Error` では見ない: アプリの設定の評価や、Jest のモジュールの解決が投げるものは
|
|
49
|
+
* 別の realm の Error のことがあり、`String(error)` だと先頭に `Error: ` が付く
|
|
50
|
+
*/
|
|
51
|
+
const messageOf = (error: unknown): string =>
|
|
52
|
+
typeof error === 'object' &&
|
|
53
|
+
error !== null &&
|
|
54
|
+
typeof (error as { message?: unknown }).message === 'string'
|
|
55
|
+
? (error as { message: string }).message
|
|
56
|
+
: String(error);
|
|
57
|
+
|
|
58
|
+
const errorMessage = (error: unknown, cwd?: string) => {
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
for (const line of messageOf(error).split(/\r?\n/)) {
|
|
61
|
+
if (/^\s*>?\s*\d+\s*\|/.test(line)) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (line.trim() !== '') {
|
|
65
|
+
lines.push(line.trim());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let reason = lines.join(' ');
|
|
69
|
+
if (cwd !== undefined) {
|
|
70
|
+
reason = reason.split(`${cwd}${sep}`).join('');
|
|
71
|
+
}
|
|
72
|
+
return reason.length > MAX_REASON
|
|
73
|
+
? `${reason.slice(0, MAX_REASON)}…`
|
|
74
|
+
: reason;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** 設定の `expo` の中身(app.json の `expo` キーの下、または app.config の戻り値)から値を取る */
|
|
78
|
+
const valuesOf = (exp: Obj, file: string): ExpoConfig => {
|
|
79
|
+
const ios = isObject(exp.ios) ? exp.ios : {};
|
|
80
|
+
const android = isObject(exp.android) ? exp.android : {};
|
|
81
|
+
return {
|
|
82
|
+
file,
|
|
83
|
+
bundleIdentifier: text(ios.bundleIdentifier),
|
|
84
|
+
package: text(android.package),
|
|
85
|
+
version: text(exp.version),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `fn` の間、stdout への出力を stderr に回す。getConfig はアプリの app.config.ts / .js を評価するので、そこの
|
|
91
|
+
* `console.log` などが stdout に出ると `--json` の stdout が JSON でなくなる。
|
|
92
|
+
* Node の console は書くたびに `process.stdout.write` を引くので、それを差し替えれば `console.log` も回る。
|
|
93
|
+
* console 自体を差し替えたもの(Jest など)のために `console.log` / `info` / `debug` も回す。
|
|
94
|
+
* getConfig は同期なので、戻ってから後に(タイマーなどで)出るものは回らない
|
|
95
|
+
*/
|
|
96
|
+
export const withStdoutToStderr = <T>(fn: () => T): T => {
|
|
97
|
+
const { stdout, stderr } = process;
|
|
98
|
+
const write = stdout.write;
|
|
99
|
+
const { log, info, debug } = console;
|
|
100
|
+
const toStderr = (...args: unknown[]) => {
|
|
101
|
+
stderr.write(`${format(...args)}\n`);
|
|
102
|
+
};
|
|
103
|
+
stdout.write = stderr.write.bind(stderr) as typeof stdout.write;
|
|
104
|
+
console.log = toStderr;
|
|
105
|
+
console.info = toStderr;
|
|
106
|
+
console.debug = toStderr;
|
|
107
|
+
try {
|
|
108
|
+
return fn();
|
|
109
|
+
} finally {
|
|
110
|
+
stdout.write = write;
|
|
111
|
+
console.log = log;
|
|
112
|
+
console.info = info;
|
|
113
|
+
console.debug = debug;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** 動的な設定(app.config.*)があればそれ、無ければ app.json のパス(getConfig の戻り値の名前) */
|
|
118
|
+
const configFileOf = (cwd: string, result: Obj): string => {
|
|
119
|
+
const path = text(result.dynamicConfigPath) ?? text(result.staticConfigPath);
|
|
120
|
+
return path === null ? 'the Expo config' : displayPath(cwd, path);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* getConfig の `exp.version` のうち、設定に書かれた値だけを返す(書かれていなければ null)。
|
|
125
|
+
* getConfig(@expo/config の ensureConfigHasDefaultValues)は version が無いと `<cwd>/package.json` の version
|
|
126
|
+
* (それも無ければ `1.0.0`)で埋めるので、bare RN に expo を入れただけのアプリ(app.json に version が無く、
|
|
127
|
+
* package.json が `0.0.1`)でネイティブの版とのずれを誤って出してしまう。
|
|
128
|
+
*
|
|
129
|
+
* - 静的な設定の生の中身(`rootConfig`。`expo` キーの下か直下)に version があれば、書かれた値
|
|
130
|
+
* - 動的な設定(app.config.*。`dynamicConfigPath`)があれば、`exp.version` が Expo の補う値(戻り値の `pkg.version`。
|
|
131
|
+
* getConfig は `pkg` にも同じ既定値を入れて返す)と違うときだけ書かれた値。app.config に渡す `config` も既定値を
|
|
132
|
+
* 埋めた後のものなので、`({ config }) => ({ ...config, … })` は補った値をそのまま返すため
|
|
133
|
+
* 限界: app.config に package.json と同じ値を明示したときは「書かれていない」と見なす(比べないだけで、誤った warning は出ない)
|
|
134
|
+
* - どちらでもなければ書かれていない
|
|
135
|
+
*/
|
|
136
|
+
const writtenVersion = (result: Obj, version: string | null): string | null => {
|
|
137
|
+
const root = isObject(result.rootConfig) ? result.rootConfig : {};
|
|
138
|
+
const raw = isObject(root.expo) ? root.expo : root;
|
|
139
|
+
if (text(raw.version) !== null) {
|
|
140
|
+
return version;
|
|
141
|
+
}
|
|
142
|
+
if (text(result.dynamicConfigPath) === null) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const pkg = isObject(result.pkg) ? result.pkg : {};
|
|
146
|
+
const filled = typeof pkg.version === 'string' ? pkg.version : '1.0.0';
|
|
147
|
+
return version === filled ? null : version;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 見つけた `expo` の `expo/config` を読み込む。createRequire の解決は NODE_PATH も見る(pnpm の bin の shim は
|
|
152
|
+
* `.pnpm/node_modules` を入れる)ので、見つけた expo に `expo/config` が無いと別の expo のものを読みうる。
|
|
153
|
+
* 解決した先が見つけた expo の中かを確かめる
|
|
154
|
+
*/
|
|
155
|
+
const loadExpoConfig = (cwd: string, expoPackageJson: string): unknown => {
|
|
156
|
+
const require = createRequire(expoPackageJson);
|
|
157
|
+
const resolved = require.resolve('expo/config');
|
|
158
|
+
const expoDir = dirname(expoPackageJson);
|
|
159
|
+
// expo の下の node_modules(入れ子の別の expo)も、見つけた expo のものではない
|
|
160
|
+
const inside = relative(realpathSync(expoDir), realpathSync(resolved));
|
|
161
|
+
if (inside.startsWith('..') || inside.split(sep).includes('node_modules')) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${displayPath(cwd, expoDir)} does not provide expo/config`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return require(resolved);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** `expo/config` の getConfig で評価する。失敗は Error を投げる */
|
|
170
|
+
const evaluate = (cwd: string, expoPackageJson: string): ExpoConfig => {
|
|
171
|
+
const result = withStdoutToStderr(() => {
|
|
172
|
+
const mod = loadExpoConfig(cwd, expoPackageJson);
|
|
173
|
+
const getConfig = isObject(mod) ? mod.getConfig : undefined;
|
|
174
|
+
if (typeof getConfig !== 'function') {
|
|
175
|
+
throw new Error('expo/config does not export getConfig');
|
|
176
|
+
}
|
|
177
|
+
return getConfig(cwd, { skipSDKVersionRequirement: true }) as unknown;
|
|
178
|
+
});
|
|
179
|
+
if (!isObject(result) || !isObject(result.exp)) {
|
|
180
|
+
throw new Error('getConfig did not return { exp }');
|
|
181
|
+
}
|
|
182
|
+
const values = valuesOf(result.exp, configFileOf(cwd, result));
|
|
183
|
+
return { ...values, version: writtenVersion(result, values.version) };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** app.json を JSON として読む(`expo` キーがあればその下、無ければ直下)。無い・壊れていれば null と注意 */
|
|
187
|
+
const readAppJson = (
|
|
188
|
+
cwd: string
|
|
189
|
+
): { config: ExpoConfig | null; note?: string } => {
|
|
190
|
+
const path = join(cwd, 'app.json');
|
|
191
|
+
if (!isFile(path)) {
|
|
192
|
+
return { config: null, note: 'there is no app.json' };
|
|
193
|
+
}
|
|
194
|
+
let json: unknown;
|
|
195
|
+
try {
|
|
196
|
+
json = JSON.parse(readText(path) ?? '');
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
config: null,
|
|
200
|
+
note: `could not parse app.json: ${errorMessage(error)}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (!isObject(json)) {
|
|
204
|
+
return { config: null, note: 'app.json is not a JSON object' };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
config: valuesOf(isObject(json.expo) ? json.expo : json, 'app.json'),
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `<cwd>` から `expo` が見つからなければ null(Expo のアプリでない)。見つかれば getConfig で設定を評価し、
|
|
213
|
+
* 失敗したら理由を notes に入れて app.json の素読みに戻る
|
|
214
|
+
*/
|
|
215
|
+
export const readExpoProject = (cwd: string): ExpoProject | null => {
|
|
216
|
+
const expoPackageJson = findPackageJson(cwd, 'expo');
|
|
217
|
+
if (expoPackageJson === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
return { config: evaluate(cwd, expoPackageJson), notes: [] };
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const fallback = readAppJson(cwd);
|
|
224
|
+
return {
|
|
225
|
+
config: fallback.config,
|
|
226
|
+
notes: [
|
|
227
|
+
`could not evaluate the Expo config with expo/config (${errorMessage(error, cwd)}); read app.json directly instead`,
|
|
228
|
+
...(fallback.note === undefined ? [] : [fallback.note]),
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// アプリのファイルを読むときの共通部分。読めないファイル・ディレクトリは例外にせず「無い」として扱う
|
|
2
|
+
// (チェックの材料が無いだけで、CLI 全体は落とさない)
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { relative, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const isFile = (path: string): boolean => {
|
|
7
|
+
try {
|
|
8
|
+
return statSync(path).isFile();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isDirectory = (path: string): boolean => {
|
|
15
|
+
try {
|
|
16
|
+
return statSync(path).isDirectory();
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** UTF-8 のテキストとして読む。無い・読めなければ null */
|
|
23
|
+
export const readText = (path: string): string | null => {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(path, 'utf8');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* メッセージに出すパス(`<cwd>` からの相対。区切りは `/`)。`<cwd>` の外なら絶対パスのまま。
|
|
33
|
+
* どのファイルの値かを、CI のログでもリポジトリの中の場所として読めるようにする
|
|
34
|
+
*/
|
|
35
|
+
export const displayPath = (cwd: string, path: string): string => {
|
|
36
|
+
const rel = relative(cwd, path);
|
|
37
|
+
if (rel === '' || rel.startsWith('..')) {
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
return rel.split(sep).join('/');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** 値が `$(…)` / `${…}` のようにほかの設定を参照しているか(ビルド時にしか決まらないので「特定できない」にする) */
|
|
44
|
+
export const hasVariable = (value: string): boolean => /\$[({]/.test(value);
|
|
45
|
+
|
|
46
|
+
/** 1 つのファイルから読んだ値({@link Lookup} の見つかった側) */
|
|
47
|
+
export type Found = {
|
|
48
|
+
value: string;
|
|
49
|
+
/** 値の名前(`PRODUCT_BUNDLE_IDENTIFIER` / `versionName` など) */
|
|
50
|
+
label: string;
|
|
51
|
+
/** 値を読んだファイル({@link displayPath}) */
|
|
52
|
+
file: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** ネイティブのファイルから値を探した結果。見つからなければ、その理由(メッセージにそのまま入る英文) */
|
|
56
|
+
export type Lookup = Found | { value: null; reason: string };
|