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,181 @@
|
|
|
1
|
+
// android/app/build.gradle(.kts)(Groovy / Kotlin の DSL)の簡易な読み取り。Gradle を動かさずに、ブロック
|
|
2
|
+
// (`defaultConfig { … }`)の範囲と、その直下の `name "literal"` / `name = "literal"` の形の値だけを読む。
|
|
3
|
+
//
|
|
4
|
+
// 範囲は波括弧の対応で取る。文字列とコメントの中の波括弧・キーワードを数えないよう、先に「マスク」(コメントを空白に、
|
|
5
|
+
// 文字列の中身を空白にした同じ長さの写し。引用符と改行は残す)を作り、探すのはマスクの上、値を読むのは元の文字列の上で行う。
|
|
6
|
+
// 限界: Groovy の slashy string(`/…/`・`$/…/$`)と、`"${ … }"` の中にさらに `"` を書いたものは文字列として扱えない
|
|
7
|
+
// (アプリの build.gradle の defaultConfig / android の周りではまず使われない)
|
|
8
|
+
|
|
9
|
+
/** 文字列の範囲(`start` から `end` の手前まで) */
|
|
10
|
+
export type Range = { start: number; end: number };
|
|
11
|
+
|
|
12
|
+
const blank = (text: string): string => text.replace(/[^\n]/g, ' ');
|
|
13
|
+
|
|
14
|
+
/** {@link maskGradle} の 1 つの区間(コメント・文字列)の終わり */
|
|
15
|
+
const endOf = (source: string, i: number): { end: number; kind: string } => {
|
|
16
|
+
const two = source.slice(i, i + 2);
|
|
17
|
+
if (two === '//') {
|
|
18
|
+
const nl = source.indexOf('\n', i);
|
|
19
|
+
return { end: nl === -1 ? source.length : nl, kind: 'comment' };
|
|
20
|
+
}
|
|
21
|
+
if (two === '/*') {
|
|
22
|
+
const close = source.indexOf('*/', i + 2);
|
|
23
|
+
return { end: close === -1 ? source.length : close + 2, kind: 'comment' };
|
|
24
|
+
}
|
|
25
|
+
const three = source.slice(i, i + 3);
|
|
26
|
+
if (three === '"""' || three === "'''") {
|
|
27
|
+
const close = source.indexOf(three, i + 3);
|
|
28
|
+
return { end: close === -1 ? source.length : close + 3, kind: three };
|
|
29
|
+
}
|
|
30
|
+
const quote = source[i];
|
|
31
|
+
if (quote === '"' || quote === "'") {
|
|
32
|
+
let j = i + 1;
|
|
33
|
+
while (j < source.length && source[j] !== quote && source[j] !== '\n') {
|
|
34
|
+
j += source[j] === '\\' ? 2 : 1;
|
|
35
|
+
}
|
|
36
|
+
// 閉じが無い(行末まで)ものも、その行で終わった文字列として扱う
|
|
37
|
+
return { end: Math.min(j + 1, source.length), kind: quote };
|
|
38
|
+
}
|
|
39
|
+
return { end: i, kind: '' };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* コメントを空白に、文字列の中身を空白にした、`source` と同じ長さの文字列。位置はそのまま `source` に対応する
|
|
44
|
+
*/
|
|
45
|
+
export const maskGradle = (source: string): string => {
|
|
46
|
+
let out = '';
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < source.length) {
|
|
49
|
+
const { end, kind } = endOf(source, i);
|
|
50
|
+
if (kind === '') {
|
|
51
|
+
out += source[i];
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const part = source.slice(i, end);
|
|
56
|
+
if (kind === 'comment') {
|
|
57
|
+
out += blank(part);
|
|
58
|
+
} else {
|
|
59
|
+
// 引用符(1 文字または 3 文字)は残し、中身だけ消す
|
|
60
|
+
const q = kind.length;
|
|
61
|
+
const closed = part.length >= q * 2 && part.endsWith(kind);
|
|
62
|
+
out += closed
|
|
63
|
+
? kind + blank(part.slice(q, part.length - q)) + kind
|
|
64
|
+
: kind + blank(part.slice(q));
|
|
65
|
+
}
|
|
66
|
+
i = end;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** `masked` の `from` から `to` の手前までの、波括弧の深さの増減 */
|
|
72
|
+
const depthBetween = (masked: string, from: number, to: number): number => {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
for (let i = from; i < to; i += 1) {
|
|
75
|
+
if (masked[i] === '{') {
|
|
76
|
+
depth += 1;
|
|
77
|
+
} else if (masked[i] === '}') {
|
|
78
|
+
depth -= 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return depth;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `range` の中で最初の `<name> {` のブロックの中身の範囲(`{` の次から、対応する `}` の手前まで)。
|
|
86
|
+
* 無い・閉じていなければ null
|
|
87
|
+
*/
|
|
88
|
+
export const findBlock = (
|
|
89
|
+
masked: string,
|
|
90
|
+
name: string,
|
|
91
|
+
range: Range = { start: 0, end: masked.length }
|
|
92
|
+
): Range | null => {
|
|
93
|
+
const pattern = new RegExp(`\\b${name}\\s*\\{`, 'g');
|
|
94
|
+
pattern.lastIndex = range.start;
|
|
95
|
+
const match = pattern.exec(masked);
|
|
96
|
+
if (match === null || match.index >= range.end) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const start = match.index + match[0].length;
|
|
100
|
+
let depth = 1;
|
|
101
|
+
for (let i = start; i < range.end; i += 1) {
|
|
102
|
+
if (masked[i] === '{') {
|
|
103
|
+
depth += 1;
|
|
104
|
+
} else if (masked[i] === '}') {
|
|
105
|
+
depth -= 1;
|
|
106
|
+
if (depth === 0) {
|
|
107
|
+
return { start, end: i };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** {@link findStatement} の結果。文字列のリテラルなら `literal`、そうでなければ書かれている式(1 行・長いものは切る) */
|
|
115
|
+
export type Statement =
|
|
116
|
+
| { literal: string; expression?: undefined }
|
|
117
|
+
| { literal?: undefined; expression: string };
|
|
118
|
+
|
|
119
|
+
const MAX_EXPRESSION = 80;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `range` の直下(入れ子のブロックの中は見ない)で、文の頭にある最初の `<name> <value>` / `<name> = <value>` の値。
|
|
123
|
+
* `<value>` が `"…"`(`$` による埋め込みを含まない)か `'…'` だけならリテラル。無ければ null
|
|
124
|
+
*/
|
|
125
|
+
export const findStatement = (
|
|
126
|
+
source: string,
|
|
127
|
+
masked: string,
|
|
128
|
+
name: string,
|
|
129
|
+
range: Range
|
|
130
|
+
): Statement | null => {
|
|
131
|
+
const pattern = new RegExp(`\\b${name}\\b`, 'g');
|
|
132
|
+
pattern.lastIndex = range.start;
|
|
133
|
+
for (
|
|
134
|
+
let match = pattern.exec(masked);
|
|
135
|
+
match !== null && match.index < range.end;
|
|
136
|
+
match = pattern.exec(masked)
|
|
137
|
+
) {
|
|
138
|
+
// 文の頭(行頭か、`;` / `{` の後)にあるものだけ。`buildConfigField …, applicationId` のような式の中の名前は見ない
|
|
139
|
+
const lineBefore = masked.slice(
|
|
140
|
+
masked.lastIndexOf('\n', match.index - 1) + 1,
|
|
141
|
+
match.index
|
|
142
|
+
);
|
|
143
|
+
const atHead = /(^|[;{])[ \t]*$/.test(lineBefore);
|
|
144
|
+
let i = match.index + name.length;
|
|
145
|
+
// `applicationId.foo` のようなプロパティの参照は値の設定ではない
|
|
146
|
+
if (
|
|
147
|
+
!atHead ||
|
|
148
|
+
masked[i] === '.' ||
|
|
149
|
+
depthBetween(masked, range.start, match.index) !== 0
|
|
150
|
+
) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
i += /^[ \t]*(=[ \t]*)?/.exec(masked.slice(i))?.[0].length ?? 0;
|
|
154
|
+
// 文の終わり(改行・`;`・ブロックの閉じ)。行末のコメントはマスクでは空白なので落とす
|
|
155
|
+
const rest = masked.slice(i, range.end);
|
|
156
|
+
const stop = rest.search(/[\n;}]/);
|
|
157
|
+
const value = source.slice(
|
|
158
|
+
i,
|
|
159
|
+
i + rest.slice(0, stop === -1 ? rest.length : stop).trimEnd().length
|
|
160
|
+
);
|
|
161
|
+
if (value === '') {
|
|
162
|
+
// 値の無い名前だけの行(次の行に値を書く形は読まない)
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const literal = /^"([^"\\$\n]*)"$|^'([^'\\\n]*)'$/.exec(value);
|
|
166
|
+
if (literal) {
|
|
167
|
+
return { literal: literal[1] ?? literal[2] };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
expression:
|
|
171
|
+
value.length > MAX_EXPRESSION
|
|
172
|
+
? `${value.slice(0, MAX_EXPRESSION)}…`
|
|
173
|
+
: value,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** マスクの上で `name` という語があるか(コメント・文字列の中は数えない) */
|
|
180
|
+
export const hasWord = (masked: string, name: string): boolean =>
|
|
181
|
+
new RegExp(`\\b${name}\\b`).test(masked);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// アプリの構成(Expo の設定・iOS / Android のネイティブのプロジェクト)を 1 回だけ読む。C2 / C3 / C4 / C8 が共有する
|
|
2
|
+
// (`--ignore C2` でも C3 / C4 / C8 は ID と版を使うので、読むのは C2 の結果を出すかどうかと別にしている)
|
|
3
|
+
import { type AndroidProject, readAndroidProject } from './android.ts';
|
|
4
|
+
import { type ExpoProject, readExpoProject } from './expo.ts';
|
|
5
|
+
import { type IosProject, readIosProject } from './ios.ts';
|
|
6
|
+
|
|
7
|
+
/** アプリの構成。それぞれ、無い(Expo でない・`ios/` が無い・`android/` が無い)なら null */
|
|
8
|
+
export type ProjectInfo = {
|
|
9
|
+
expo: ExpoProject | null;
|
|
10
|
+
ios: IosProject | null;
|
|
11
|
+
android: AndroidProject | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const readProject = (cwd: string): ProjectInfo => ({
|
|
15
|
+
expo: readExpoProject(cwd),
|
|
16
|
+
ios: readIosProject(cwd),
|
|
17
|
+
android: readAndroidProject(cwd),
|
|
18
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// iOS のネイティブのプロジェクト(`ios/`)から bundleId(C2)と版(C4)を読む。
|
|
2
|
+
// `ios/*.xcodeproj/project.pbxproj` のアプリのターゲットの Release の build settings と、そのターゲットの Info.plist を見る
|
|
3
|
+
import { readdirSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
displayPath,
|
|
8
|
+
hasVariable,
|
|
9
|
+
isDirectory,
|
|
10
|
+
type Lookup,
|
|
11
|
+
readText,
|
|
12
|
+
} from './files.ts';
|
|
13
|
+
import {
|
|
14
|
+
isDict,
|
|
15
|
+
type PlistDict,
|
|
16
|
+
type PlistValue,
|
|
17
|
+
parsePlist,
|
|
18
|
+
} from './plist.ts';
|
|
19
|
+
|
|
20
|
+
/** `ios/` から読んだ値。`notes` は C2 が info で出す注意(複数の .xcodeproj・複数のアプリのターゲット) */
|
|
21
|
+
export type IosProject = {
|
|
22
|
+
bundleId: Lookup;
|
|
23
|
+
version: Lookup;
|
|
24
|
+
notes: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const APPLICATION = 'com.apple.product-type.application';
|
|
28
|
+
|
|
29
|
+
const str = (value: PlistValue | undefined): string | null =>
|
|
30
|
+
typeof value === 'string' ? value : null;
|
|
31
|
+
|
|
32
|
+
const listOfIds = (value: PlistValue | undefined): string[] =>
|
|
33
|
+
Array.isArray(value)
|
|
34
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
/** CocoaPods が作る Pods.xcodeproj はアプリのプロジェクトではない */
|
|
38
|
+
const findXcodeprojs = (iosDir: string): string[] => {
|
|
39
|
+
try {
|
|
40
|
+
return readdirSync(iosDir, { withFileTypes: true })
|
|
41
|
+
.filter(
|
|
42
|
+
(entry) =>
|
|
43
|
+
entry.isDirectory() &&
|
|
44
|
+
entry.name.endsWith('.xcodeproj') &&
|
|
45
|
+
entry.name !== 'Pods.xcodeproj'
|
|
46
|
+
)
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type AppTarget = { name: string; release: PlistDict | null };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* アプリのターゲット(`productType` がちょうど `com.apple.product-type.application` の PBXNativeTarget)を、
|
|
58
|
+
* PBXProject の `targets` の順(Xcode のターゲット一覧の順)に返す。テスト(`bundle.unit-test` など)・extension
|
|
59
|
+
* (`app-extension`)・旧来の watch アプリ(`application.watchapp2`)は productType が違うので入らない。Xcode 14 以降の
|
|
60
|
+
* watchOS 単体のアプリは productType が同じなので、Release の `SDKROOT` が watchOS のものも除く
|
|
61
|
+
*/
|
|
62
|
+
const findAppTargets = (root: PlistDict): AppTarget[] => {
|
|
63
|
+
const objects = isDict(root.objects) ? root.objects : null;
|
|
64
|
+
if (objects === null) {
|
|
65
|
+
throw new Error('no "objects" in the project');
|
|
66
|
+
}
|
|
67
|
+
const object = (id: string): PlistDict | null => {
|
|
68
|
+
const value = objects[id];
|
|
69
|
+
return isDict(value) ? value : null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const rootObject = object(str(root.rootObject) ?? '');
|
|
73
|
+
// rootObject が辿れない壊れ方でも、objects の並び(ID 順)でターゲットを探す
|
|
74
|
+
const targetIds = rootObject
|
|
75
|
+
? listOfIds(rootObject.targets)
|
|
76
|
+
: Object.keys(objects);
|
|
77
|
+
|
|
78
|
+
const releaseOf = (target: PlistDict): PlistDict | null => {
|
|
79
|
+
const list = object(str(target.buildConfigurationList) ?? '');
|
|
80
|
+
for (const id of listOfIds(list?.buildConfigurations)) {
|
|
81
|
+
const config = object(id);
|
|
82
|
+
if (
|
|
83
|
+
config !== null &&
|
|
84
|
+
str(config.isa) === 'XCBuildConfiguration' &&
|
|
85
|
+
str(config.name) === 'Release'
|
|
86
|
+
) {
|
|
87
|
+
return isDict(config.buildSettings)
|
|
88
|
+
? config.buildSettings
|
|
89
|
+
: Object.create(null);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const targets: AppTarget[] = [];
|
|
96
|
+
for (const id of targetIds) {
|
|
97
|
+
const target = object(id);
|
|
98
|
+
if (
|
|
99
|
+
target === null ||
|
|
100
|
+
str(target.isa) !== 'PBXNativeTarget' ||
|
|
101
|
+
str(target.productType) !== APPLICATION
|
|
102
|
+
) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const release = releaseOf(target);
|
|
106
|
+
if (/watch/i.test(str(release?.SDKROOT) ?? '')) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
targets.push({ name: str(target.name) ?? id, release });
|
|
110
|
+
}
|
|
111
|
+
return targets;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Info.plist(XML)の `key` の値の分類:
|
|
116
|
+
* - `setting`: 無い・空・build setting `setting` への参照そのもの(`$(PRODUCT_BUNDLE_IDENTIFIER)` / `${…}`)→ build settings の値を使う
|
|
117
|
+
* - `literal`: 変数を含まない値 → それを使う
|
|
118
|
+
* - `variable`: それ以外の変数を含む(`com.example.$(SUFFIX)` / `$(PRODUCT_BUNDLE_IDENTIFIER).dev`)→ ビルド時にしか決まらない
|
|
119
|
+
*/
|
|
120
|
+
type PlistEntry =
|
|
121
|
+
| { kind: 'setting' }
|
|
122
|
+
| { kind: 'literal' | 'variable'; value: string };
|
|
123
|
+
|
|
124
|
+
const readPlistEntry = (
|
|
125
|
+
plist: string | null,
|
|
126
|
+
key: string,
|
|
127
|
+
setting: string
|
|
128
|
+
): PlistEntry => {
|
|
129
|
+
const match =
|
|
130
|
+
plist === null
|
|
131
|
+
? null
|
|
132
|
+
: new RegExp(
|
|
133
|
+
`<key>\\s*${key}\\s*</key>\\s*<string>([^<]*)</string>`
|
|
134
|
+
).exec(plist);
|
|
135
|
+
const value = match ? match[1].trim() : '';
|
|
136
|
+
if (value === '' || value === `$(${setting})` || value === `\${${setting}}`) {
|
|
137
|
+
return { kind: 'setting' };
|
|
138
|
+
}
|
|
139
|
+
return { kind: hasVariable(value) ? 'variable' : 'literal', value };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* ターゲットの Info.plist のパス。Release の `INFOPLIST_FILE`(`$(SRCROOT)` / `$(PROJECT_DIR)` は .xcodeproj のある
|
|
144
|
+
* ディレクトリ)、無ければ `<ターゲット名>/Info.plist`(React Native / Expo のテンプレートの置き場所)。
|
|
145
|
+
* ほかの変数を含んで場所が決まらなければ null
|
|
146
|
+
*/
|
|
147
|
+
const infoPlistPath = (
|
|
148
|
+
projectDir: string,
|
|
149
|
+
target: AppTarget
|
|
150
|
+
): string | null => {
|
|
151
|
+
const setting = str(target.release?.INFOPLIST_FILE);
|
|
152
|
+
if (setting === null || setting === '') {
|
|
153
|
+
return join(projectDir, target.name, 'Info.plist');
|
|
154
|
+
}
|
|
155
|
+
const path = setting.replace(/^\$[({](SRCROOT|PROJECT_DIR)[)}]\/?/, '');
|
|
156
|
+
if (hasVariable(path)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return resolve(projectDir, path);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** `ios/` が無ければ null(CNG)。読めない・特定できないものは {@link Lookup} の reason にする */
|
|
163
|
+
export const readIosProject = (cwd: string): IosProject | null => {
|
|
164
|
+
const iosDir = join(cwd, 'ios');
|
|
165
|
+
if (!isDirectory(iosDir)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const notes: string[] = [];
|
|
169
|
+
const unknown = (reason: string): IosProject => ({
|
|
170
|
+
bundleId: { value: null, reason },
|
|
171
|
+
version: { value: null, reason },
|
|
172
|
+
notes,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const xcodeprojs = findXcodeprojs(iosDir);
|
|
176
|
+
if (xcodeprojs.length === 0) {
|
|
177
|
+
return unknown(
|
|
178
|
+
'there is no .xcodeproj in ios/ (other than Pods.xcodeproj)'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (xcodeprojs.length > 1) {
|
|
182
|
+
notes.push(
|
|
183
|
+
`ios/ has ${xcodeprojs.length} .xcodeproj (${xcodeprojs.join(', ')}); using the first one, ios/${xcodeprojs[0]}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const pbxprojPath = join(iosDir, xcodeprojs[0], 'project.pbxproj');
|
|
187
|
+
const pbxproj = displayPath(cwd, pbxprojPath);
|
|
188
|
+
const text = readText(pbxprojPath);
|
|
189
|
+
if (text === null) {
|
|
190
|
+
return unknown(`could not read ${pbxproj}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let targets: AppTarget[];
|
|
194
|
+
try {
|
|
195
|
+
const root = parsePlist(text);
|
|
196
|
+
if (!isDict(root)) {
|
|
197
|
+
throw new Error('the root is not a dictionary');
|
|
198
|
+
}
|
|
199
|
+
targets = findAppTargets(root);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return unknown(
|
|
202
|
+
`could not parse ${pbxproj}: ${error instanceof Error ? error.message : String(error)}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const [target] = targets;
|
|
207
|
+
if (target === undefined) {
|
|
208
|
+
return unknown(`there is no application target in ${pbxproj}`);
|
|
209
|
+
}
|
|
210
|
+
if (targets.length > 1) {
|
|
211
|
+
notes.push(
|
|
212
|
+
`${pbxproj} has ${targets.length} application targets (${targets.map((t) => `"${t.name}"`).join(', ')}); using the first one, "${target.name}"`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const { release } = target;
|
|
216
|
+
if (release === null) {
|
|
217
|
+
return unknown(
|
|
218
|
+
`target "${target.name}" in ${pbxproj} has no Release build configuration`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Release の build settings の値。無い・変数を含むなら reason */
|
|
223
|
+
const setting = (key: string): Lookup => {
|
|
224
|
+
const value = str(release[key]);
|
|
225
|
+
if (value === null || value === '') {
|
|
226
|
+
return {
|
|
227
|
+
value: null,
|
|
228
|
+
reason: `${key} is not set in the Release build settings of target "${target.name}" in ${pbxproj}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (hasVariable(value)) {
|
|
232
|
+
return {
|
|
233
|
+
value: null,
|
|
234
|
+
reason: `${key} of target "${target.name}" (Release) in ${pbxproj} is "${value}", which depends on other build settings`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { value, label: key, file: pbxproj };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// SRCROOT は .xcodeproj のあるディレクトリ(ios/)
|
|
241
|
+
const plistPath = infoPlistPath(iosDir, target);
|
|
242
|
+
const plist = plistPath === null ? null : readText(plistPath);
|
|
243
|
+
const plistFile = plistPath === null ? '' : displayPath(cwd, plistPath);
|
|
244
|
+
|
|
245
|
+
// bundleId・版とも、Info.plist の値がリテラルならそれ(Xcode はビルドの結果に Info.plist の値を入れる)、
|
|
246
|
+
// `$(PRODUCT_BUNDLE_IDENTIFIER)` / `$(MARKETING_VERSION)` そのものなら build settings の値。ほかの変数を含めば特定できない
|
|
247
|
+
const fromPlist = (
|
|
248
|
+
entry: PlistEntry,
|
|
249
|
+
key: string,
|
|
250
|
+
fallback: () => Lookup
|
|
251
|
+
): Lookup =>
|
|
252
|
+
entry.kind === 'setting'
|
|
253
|
+
? fallback()
|
|
254
|
+
: entry.kind === 'variable'
|
|
255
|
+
? {
|
|
256
|
+
value: null,
|
|
257
|
+
reason: `${key} "${entry.value}" in ${plistFile} depends on other build settings`,
|
|
258
|
+
}
|
|
259
|
+
: { value: entry.value, label: key, file: plistFile };
|
|
260
|
+
|
|
261
|
+
const plistId = readPlistEntry(
|
|
262
|
+
plist,
|
|
263
|
+
'CFBundleIdentifier',
|
|
264
|
+
'PRODUCT_BUNDLE_IDENTIFIER'
|
|
265
|
+
);
|
|
266
|
+
const productId = setting('PRODUCT_BUNDLE_IDENTIFIER');
|
|
267
|
+
if (
|
|
268
|
+
plistId.kind === 'literal' &&
|
|
269
|
+
productId.value !== null &&
|
|
270
|
+
productId.value !== plistId.value
|
|
271
|
+
) {
|
|
272
|
+
notes.push(
|
|
273
|
+
`${plistFile} has CFBundleIdentifier "${plistId.value}", but PRODUCT_BUNDLE_IDENTIFIER of target "${target.name}" (Release) in ${pbxproj} is "${productId.value}"; using the Info.plist one, "${plistId.value}"`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const bundleId = fromPlist(plistId, 'CFBundleIdentifier', () => productId);
|
|
277
|
+
const version = fromPlist(
|
|
278
|
+
readPlistEntry(plist, 'CFBundleShortVersionString', 'MARKETING_VERSION'),
|
|
279
|
+
'CFBundleShortVersionString',
|
|
280
|
+
() => setting('MARKETING_VERSION')
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return { bundleId, version, notes };
|
|
284
|
+
};
|