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,194 @@
|
|
|
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
|
+
import { findPackageJson } from '../packages.js';
|
|
10
|
+
import { displayPath, isFile, readText } from './files.js';
|
|
11
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
const text = (value) => typeof value === 'string' && value !== '' ? value : null;
|
|
13
|
+
/** 失敗の理由を 1 行に詰めたときの長さの上限 */
|
|
14
|
+
const MAX_REASON = 300;
|
|
15
|
+
/**
|
|
16
|
+
* 失敗の理由を 1 行にする。getConfig の失敗は `Error reading Expo config at <path>:` の次の行以降に理由があり、
|
|
17
|
+
* 構文エラーではその後にコードの抜粋(`> 2 | name: ,`)が続く。抜粋の手前までの空でない行をつなぎ、
|
|
18
|
+
* `<cwd>/` を落として(パスは cwd からの相対で出す)、長すぎれば切る
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Error の message。`instanceof Error` では見ない: アプリの設定の評価や、Jest のモジュールの解決が投げるものは
|
|
22
|
+
* 別の realm の Error のことがあり、`String(error)` だと先頭に `Error: ` が付く
|
|
23
|
+
*/
|
|
24
|
+
const messageOf = (error) => typeof error === 'object' &&
|
|
25
|
+
error !== null &&
|
|
26
|
+
typeof error.message === 'string'
|
|
27
|
+
? error.message
|
|
28
|
+
: String(error);
|
|
29
|
+
const errorMessage = (error, cwd) => {
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const line of messageOf(error).split(/\r?\n/)) {
|
|
32
|
+
if (/^\s*>?\s*\d+\s*\|/.test(line)) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (line.trim() !== '') {
|
|
36
|
+
lines.push(line.trim());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let reason = lines.join(' ');
|
|
40
|
+
if (cwd !== undefined) {
|
|
41
|
+
reason = reason.split(`${cwd}${sep}`).join('');
|
|
42
|
+
}
|
|
43
|
+
return reason.length > MAX_REASON
|
|
44
|
+
? `${reason.slice(0, MAX_REASON)}…`
|
|
45
|
+
: reason;
|
|
46
|
+
};
|
|
47
|
+
/** 設定の `expo` の中身(app.json の `expo` キーの下、または app.config の戻り値)から値を取る */
|
|
48
|
+
const valuesOf = (exp, file) => {
|
|
49
|
+
const ios = isObject(exp.ios) ? exp.ios : {};
|
|
50
|
+
const android = isObject(exp.android) ? exp.android : {};
|
|
51
|
+
return {
|
|
52
|
+
file,
|
|
53
|
+
bundleIdentifier: text(ios.bundleIdentifier),
|
|
54
|
+
package: text(android.package),
|
|
55
|
+
version: text(exp.version),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* `fn` の間、stdout への出力を stderr に回す。getConfig はアプリの app.config.ts / .js を評価するので、そこの
|
|
60
|
+
* `console.log` などが stdout に出ると `--json` の stdout が JSON でなくなる。
|
|
61
|
+
* Node の console は書くたびに `process.stdout.write` を引くので、それを差し替えれば `console.log` も回る。
|
|
62
|
+
* console 自体を差し替えたもの(Jest など)のために `console.log` / `info` / `debug` も回す。
|
|
63
|
+
* getConfig は同期なので、戻ってから後に(タイマーなどで)出るものは回らない
|
|
64
|
+
*/
|
|
65
|
+
export const withStdoutToStderr = (fn) => {
|
|
66
|
+
const { stdout, stderr } = process;
|
|
67
|
+
const write = stdout.write;
|
|
68
|
+
const { log, info, debug } = console;
|
|
69
|
+
const toStderr = (...args) => {
|
|
70
|
+
stderr.write(`${format(...args)}\n`);
|
|
71
|
+
};
|
|
72
|
+
stdout.write = stderr.write.bind(stderr);
|
|
73
|
+
console.log = toStderr;
|
|
74
|
+
console.info = toStderr;
|
|
75
|
+
console.debug = toStderr;
|
|
76
|
+
try {
|
|
77
|
+
return fn();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
stdout.write = write;
|
|
81
|
+
console.log = log;
|
|
82
|
+
console.info = info;
|
|
83
|
+
console.debug = debug;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** 動的な設定(app.config.*)があればそれ、無ければ app.json のパス(getConfig の戻り値の名前) */
|
|
87
|
+
const configFileOf = (cwd, result) => {
|
|
88
|
+
const path = text(result.dynamicConfigPath) ?? text(result.staticConfigPath);
|
|
89
|
+
return path === null ? 'the Expo config' : displayPath(cwd, path);
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* getConfig の `exp.version` のうち、設定に書かれた値だけを返す(書かれていなければ null)。
|
|
93
|
+
* getConfig(@expo/config の ensureConfigHasDefaultValues)は version が無いと `<cwd>/package.json` の version
|
|
94
|
+
* (それも無ければ `1.0.0`)で埋めるので、bare RN に expo を入れただけのアプリ(app.json に version が無く、
|
|
95
|
+
* package.json が `0.0.1`)でネイティブの版とのずれを誤って出してしまう。
|
|
96
|
+
*
|
|
97
|
+
* - 静的な設定の生の中身(`rootConfig`。`expo` キーの下か直下)に version があれば、書かれた値
|
|
98
|
+
* - 動的な設定(app.config.*。`dynamicConfigPath`)があれば、`exp.version` が Expo の補う値(戻り値の `pkg.version`。
|
|
99
|
+
* getConfig は `pkg` にも同じ既定値を入れて返す)と違うときだけ書かれた値。app.config に渡す `config` も既定値を
|
|
100
|
+
* 埋めた後のものなので、`({ config }) => ({ ...config, … })` は補った値をそのまま返すため
|
|
101
|
+
* 限界: app.config に package.json と同じ値を明示したときは「書かれていない」と見なす(比べないだけで、誤った warning は出ない)
|
|
102
|
+
* - どちらでもなければ書かれていない
|
|
103
|
+
*/
|
|
104
|
+
const writtenVersion = (result, version) => {
|
|
105
|
+
const root = isObject(result.rootConfig) ? result.rootConfig : {};
|
|
106
|
+
const raw = isObject(root.expo) ? root.expo : root;
|
|
107
|
+
if (text(raw.version) !== null) {
|
|
108
|
+
return version;
|
|
109
|
+
}
|
|
110
|
+
if (text(result.dynamicConfigPath) === null) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const pkg = isObject(result.pkg) ? result.pkg : {};
|
|
114
|
+
const filled = typeof pkg.version === 'string' ? pkg.version : '1.0.0';
|
|
115
|
+
return version === filled ? null : version;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* 見つけた `expo` の `expo/config` を読み込む。createRequire の解決は NODE_PATH も見る(pnpm の bin の shim は
|
|
119
|
+
* `.pnpm/node_modules` を入れる)ので、見つけた expo に `expo/config` が無いと別の expo のものを読みうる。
|
|
120
|
+
* 解決した先が見つけた expo の中かを確かめる
|
|
121
|
+
*/
|
|
122
|
+
const loadExpoConfig = (cwd, expoPackageJson) => {
|
|
123
|
+
const require = createRequire(expoPackageJson);
|
|
124
|
+
const resolved = require.resolve('expo/config');
|
|
125
|
+
const expoDir = dirname(expoPackageJson);
|
|
126
|
+
// expo の下の node_modules(入れ子の別の expo)も、見つけた expo のものではない
|
|
127
|
+
const inside = relative(realpathSync(expoDir), realpathSync(resolved));
|
|
128
|
+
if (inside.startsWith('..') || inside.split(sep).includes('node_modules')) {
|
|
129
|
+
throw new Error(`${displayPath(cwd, expoDir)} does not provide expo/config`);
|
|
130
|
+
}
|
|
131
|
+
return require(resolved);
|
|
132
|
+
};
|
|
133
|
+
/** `expo/config` の getConfig で評価する。失敗は Error を投げる */
|
|
134
|
+
const evaluate = (cwd, expoPackageJson) => {
|
|
135
|
+
const result = withStdoutToStderr(() => {
|
|
136
|
+
const mod = loadExpoConfig(cwd, expoPackageJson);
|
|
137
|
+
const getConfig = isObject(mod) ? mod.getConfig : undefined;
|
|
138
|
+
if (typeof getConfig !== 'function') {
|
|
139
|
+
throw new Error('expo/config does not export getConfig');
|
|
140
|
+
}
|
|
141
|
+
return getConfig(cwd, { skipSDKVersionRequirement: true });
|
|
142
|
+
});
|
|
143
|
+
if (!isObject(result) || !isObject(result.exp)) {
|
|
144
|
+
throw new Error('getConfig did not return { exp }');
|
|
145
|
+
}
|
|
146
|
+
const values = valuesOf(result.exp, configFileOf(cwd, result));
|
|
147
|
+
return { ...values, version: writtenVersion(result, values.version) };
|
|
148
|
+
};
|
|
149
|
+
/** app.json を JSON として読む(`expo` キーがあればその下、無ければ直下)。無い・壊れていれば null と注意 */
|
|
150
|
+
const readAppJson = (cwd) => {
|
|
151
|
+
const path = join(cwd, 'app.json');
|
|
152
|
+
if (!isFile(path)) {
|
|
153
|
+
return { config: null, note: 'there is no app.json' };
|
|
154
|
+
}
|
|
155
|
+
let json;
|
|
156
|
+
try {
|
|
157
|
+
json = JSON.parse(readText(path) ?? '');
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
config: null,
|
|
162
|
+
note: `could not parse app.json: ${errorMessage(error)}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!isObject(json)) {
|
|
166
|
+
return { config: null, note: 'app.json is not a JSON object' };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
config: valuesOf(isObject(json.expo) ? json.expo : json, 'app.json'),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* `<cwd>` から `expo` が見つからなければ null(Expo のアプリでない)。見つかれば getConfig で設定を評価し、
|
|
174
|
+
* 失敗したら理由を notes に入れて app.json の素読みに戻る
|
|
175
|
+
*/
|
|
176
|
+
export const readExpoProject = (cwd) => {
|
|
177
|
+
const expoPackageJson = findPackageJson(cwd, 'expo');
|
|
178
|
+
if (expoPackageJson === null) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return { config: evaluate(cwd, expoPackageJson), notes: [] };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const fallback = readAppJson(cwd);
|
|
186
|
+
return {
|
|
187
|
+
config: fallback.config,
|
|
188
|
+
notes: [
|
|
189
|
+
`could not evaluate the Expo config with expo/config (${errorMessage(error, cwd)}); read app.json directly instead`,
|
|
190
|
+
...(fallback.note === undefined ? [] : [fallback.note]),
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// アプリのファイルを読むときの共通部分。読めないファイル・ディレクトリは例外にせず「無い」として扱う
|
|
2
|
+
// (チェックの材料が無いだけで、CLI 全体は落とさない)
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { relative, sep } from 'node:path';
|
|
5
|
+
export const isFile = (path) => {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(path).isFile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export const isDirectory = (path) => {
|
|
14
|
+
try {
|
|
15
|
+
return statSync(path).isDirectory();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/** UTF-8 のテキストとして読む。無い・読めなければ null */
|
|
22
|
+
export const readText = (path) => {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(path, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* メッセージに出すパス(`<cwd>` からの相対。区切りは `/`)。`<cwd>` の外なら絶対パスのまま。
|
|
32
|
+
* どのファイルの値かを、CI のログでもリポジトリの中の場所として読めるようにする
|
|
33
|
+
*/
|
|
34
|
+
export const displayPath = (cwd, path) => {
|
|
35
|
+
const rel = relative(cwd, path);
|
|
36
|
+
if (rel === '' || rel.startsWith('..')) {
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
return rel.split(sep).join('/');
|
|
40
|
+
};
|
|
41
|
+
/** 値が `$(…)` / `${…}` のようにほかの設定を参照しているか(ビルド時にしか決まらないので「特定できない」にする) */
|
|
42
|
+
export const hasVariable = (value) => /\$[({]/.test(value);
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
const blank = (text) => text.replace(/[^\n]/g, ' ');
|
|
9
|
+
/** {@link maskGradle} の 1 つの区間(コメント・文字列)の終わり */
|
|
10
|
+
const endOf = (source, i) => {
|
|
11
|
+
const two = source.slice(i, i + 2);
|
|
12
|
+
if (two === '//') {
|
|
13
|
+
const nl = source.indexOf('\n', i);
|
|
14
|
+
return { end: nl === -1 ? source.length : nl, kind: 'comment' };
|
|
15
|
+
}
|
|
16
|
+
if (two === '/*') {
|
|
17
|
+
const close = source.indexOf('*/', i + 2);
|
|
18
|
+
return { end: close === -1 ? source.length : close + 2, kind: 'comment' };
|
|
19
|
+
}
|
|
20
|
+
const three = source.slice(i, i + 3);
|
|
21
|
+
if (three === '"""' || three === "'''") {
|
|
22
|
+
const close = source.indexOf(three, i + 3);
|
|
23
|
+
return { end: close === -1 ? source.length : close + 3, kind: three };
|
|
24
|
+
}
|
|
25
|
+
const quote = source[i];
|
|
26
|
+
if (quote === '"' || quote === "'") {
|
|
27
|
+
let j = i + 1;
|
|
28
|
+
while (j < source.length && source[j] !== quote && source[j] !== '\n') {
|
|
29
|
+
j += source[j] === '\\' ? 2 : 1;
|
|
30
|
+
}
|
|
31
|
+
// 閉じが無い(行末まで)ものも、その行で終わった文字列として扱う
|
|
32
|
+
return { end: Math.min(j + 1, source.length), kind: quote };
|
|
33
|
+
}
|
|
34
|
+
return { end: i, kind: '' };
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* コメントを空白に、文字列の中身を空白にした、`source` と同じ長さの文字列。位置はそのまま `source` に対応する
|
|
38
|
+
*/
|
|
39
|
+
export const maskGradle = (source) => {
|
|
40
|
+
let out = '';
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < source.length) {
|
|
43
|
+
const { end, kind } = endOf(source, i);
|
|
44
|
+
if (kind === '') {
|
|
45
|
+
out += source[i];
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const part = source.slice(i, end);
|
|
50
|
+
if (kind === 'comment') {
|
|
51
|
+
out += blank(part);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// 引用符(1 文字または 3 文字)は残し、中身だけ消す
|
|
55
|
+
const q = kind.length;
|
|
56
|
+
const closed = part.length >= q * 2 && part.endsWith(kind);
|
|
57
|
+
out += closed
|
|
58
|
+
? kind + blank(part.slice(q, part.length - q)) + kind
|
|
59
|
+
: kind + blank(part.slice(q));
|
|
60
|
+
}
|
|
61
|
+
i = end;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
};
|
|
65
|
+
/** `masked` の `from` から `to` の手前までの、波括弧の深さの増減 */
|
|
66
|
+
const depthBetween = (masked, from, to) => {
|
|
67
|
+
let depth = 0;
|
|
68
|
+
for (let i = from; i < to; i += 1) {
|
|
69
|
+
if (masked[i] === '{') {
|
|
70
|
+
depth += 1;
|
|
71
|
+
}
|
|
72
|
+
else if (masked[i] === '}') {
|
|
73
|
+
depth -= 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return depth;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* `range` の中で最初の `<name> {` のブロックの中身の範囲(`{` の次から、対応する `}` の手前まで)。
|
|
80
|
+
* 無い・閉じていなければ null
|
|
81
|
+
*/
|
|
82
|
+
export const findBlock = (masked, name, range = { start: 0, end: masked.length }) => {
|
|
83
|
+
const pattern = new RegExp(`\\b${name}\\s*\\{`, 'g');
|
|
84
|
+
pattern.lastIndex = range.start;
|
|
85
|
+
const match = pattern.exec(masked);
|
|
86
|
+
if (match === null || match.index >= range.end) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const start = match.index + match[0].length;
|
|
90
|
+
let depth = 1;
|
|
91
|
+
for (let i = start; i < range.end; i += 1) {
|
|
92
|
+
if (masked[i] === '{') {
|
|
93
|
+
depth += 1;
|
|
94
|
+
}
|
|
95
|
+
else if (masked[i] === '}') {
|
|
96
|
+
depth -= 1;
|
|
97
|
+
if (depth === 0) {
|
|
98
|
+
return { start, end: i };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
const MAX_EXPRESSION = 80;
|
|
105
|
+
/**
|
|
106
|
+
* `range` の直下(入れ子のブロックの中は見ない)で、文の頭にある最初の `<name> <value>` / `<name> = <value>` の値。
|
|
107
|
+
* `<value>` が `"…"`(`$` による埋め込みを含まない)か `'…'` だけならリテラル。無ければ null
|
|
108
|
+
*/
|
|
109
|
+
export const findStatement = (source, masked, name, range) => {
|
|
110
|
+
const pattern = new RegExp(`\\b${name}\\b`, 'g');
|
|
111
|
+
pattern.lastIndex = range.start;
|
|
112
|
+
for (let match = pattern.exec(masked); match !== null && match.index < range.end; match = pattern.exec(masked)) {
|
|
113
|
+
// 文の頭(行頭か、`;` / `{` の後)にあるものだけ。`buildConfigField …, applicationId` のような式の中の名前は見ない
|
|
114
|
+
const lineBefore = masked.slice(masked.lastIndexOf('\n', match.index - 1) + 1, match.index);
|
|
115
|
+
const atHead = /(^|[;{])[ \t]*$/.test(lineBefore);
|
|
116
|
+
let i = match.index + name.length;
|
|
117
|
+
// `applicationId.foo` のようなプロパティの参照は値の設定ではない
|
|
118
|
+
if (!atHead ||
|
|
119
|
+
masked[i] === '.' ||
|
|
120
|
+
depthBetween(masked, range.start, match.index) !== 0) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
i += /^[ \t]*(=[ \t]*)?/.exec(masked.slice(i))?.[0].length ?? 0;
|
|
124
|
+
// 文の終わり(改行・`;`・ブロックの閉じ)。行末のコメントはマスクでは空白なので落とす
|
|
125
|
+
const rest = masked.slice(i, range.end);
|
|
126
|
+
const stop = rest.search(/[\n;}]/);
|
|
127
|
+
const value = source.slice(i, i + rest.slice(0, stop === -1 ? rest.length : stop).trimEnd().length);
|
|
128
|
+
if (value === '') {
|
|
129
|
+
// 値の無い名前だけの行(次の行に値を書く形は読まない)
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const literal = /^"([^"\\$\n]*)"$|^'([^'\\\n]*)'$/.exec(value);
|
|
133
|
+
if (literal) {
|
|
134
|
+
return { literal: literal[1] ?? literal[2] };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
expression: value.length > MAX_EXPRESSION
|
|
138
|
+
? `${value.slice(0, MAX_EXPRESSION)}…`
|
|
139
|
+
: value,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
/** マスクの上で `name` という語があるか(コメント・文字列の中は数えない) */
|
|
145
|
+
export const hasWord = (masked, name) => new RegExp(`\\b${name}\\b`).test(masked);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// アプリの構成(Expo の設定・iOS / Android のネイティブのプロジェクト)を 1 回だけ読む。C2 / C3 / C4 / C8 が共有する
|
|
2
|
+
// (`--ignore C2` でも C3 / C4 / C8 は ID と版を使うので、読むのは C2 の結果を出すかどうかと別にしている)
|
|
3
|
+
import { readAndroidProject } from './android.js';
|
|
4
|
+
import { readExpoProject } from './expo.js';
|
|
5
|
+
import { readIosProject } from './ios.js';
|
|
6
|
+
export const readProject = (cwd) => ({
|
|
7
|
+
expo: readExpoProject(cwd),
|
|
8
|
+
ios: readIosProject(cwd),
|
|
9
|
+
android: readAndroidProject(cwd),
|
|
10
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
import { displayPath, hasVariable, isDirectory, readText, } from './files.js';
|
|
6
|
+
import { isDict, parsePlist, } from './plist.js';
|
|
7
|
+
const APPLICATION = 'com.apple.product-type.application';
|
|
8
|
+
const str = (value) => typeof value === 'string' ? value : null;
|
|
9
|
+
const listOfIds = (value) => Array.isArray(value)
|
|
10
|
+
? value.filter((item) => typeof item === 'string')
|
|
11
|
+
: [];
|
|
12
|
+
/** CocoaPods が作る Pods.xcodeproj はアプリのプロジェクトではない */
|
|
13
|
+
const findXcodeprojs = (iosDir) => {
|
|
14
|
+
try {
|
|
15
|
+
return readdirSync(iosDir, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isDirectory() &&
|
|
17
|
+
entry.name.endsWith('.xcodeproj') &&
|
|
18
|
+
entry.name !== 'Pods.xcodeproj')
|
|
19
|
+
.map((entry) => entry.name)
|
|
20
|
+
.sort();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* アプリのターゲット(`productType` がちょうど `com.apple.product-type.application` の PBXNativeTarget)を、
|
|
28
|
+
* PBXProject の `targets` の順(Xcode のターゲット一覧の順)に返す。テスト(`bundle.unit-test` など)・extension
|
|
29
|
+
* (`app-extension`)・旧来の watch アプリ(`application.watchapp2`)は productType が違うので入らない。Xcode 14 以降の
|
|
30
|
+
* watchOS 単体のアプリは productType が同じなので、Release の `SDKROOT` が watchOS のものも除く
|
|
31
|
+
*/
|
|
32
|
+
const findAppTargets = (root) => {
|
|
33
|
+
const objects = isDict(root.objects) ? root.objects : null;
|
|
34
|
+
if (objects === null) {
|
|
35
|
+
throw new Error('no "objects" in the project');
|
|
36
|
+
}
|
|
37
|
+
const object = (id) => {
|
|
38
|
+
const value = objects[id];
|
|
39
|
+
return isDict(value) ? value : null;
|
|
40
|
+
};
|
|
41
|
+
const rootObject = object(str(root.rootObject) ?? '');
|
|
42
|
+
// rootObject が辿れない壊れ方でも、objects の並び(ID 順)でターゲットを探す
|
|
43
|
+
const targetIds = rootObject
|
|
44
|
+
? listOfIds(rootObject.targets)
|
|
45
|
+
: Object.keys(objects);
|
|
46
|
+
const releaseOf = (target) => {
|
|
47
|
+
const list = object(str(target.buildConfigurationList) ?? '');
|
|
48
|
+
for (const id of listOfIds(list?.buildConfigurations)) {
|
|
49
|
+
const config = object(id);
|
|
50
|
+
if (config !== null &&
|
|
51
|
+
str(config.isa) === 'XCBuildConfiguration' &&
|
|
52
|
+
str(config.name) === 'Release') {
|
|
53
|
+
return isDict(config.buildSettings)
|
|
54
|
+
? config.buildSettings
|
|
55
|
+
: Object.create(null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
const targets = [];
|
|
61
|
+
for (const id of targetIds) {
|
|
62
|
+
const target = object(id);
|
|
63
|
+
if (target === null ||
|
|
64
|
+
str(target.isa) !== 'PBXNativeTarget' ||
|
|
65
|
+
str(target.productType) !== APPLICATION) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const release = releaseOf(target);
|
|
69
|
+
if (/watch/i.test(str(release?.SDKROOT) ?? '')) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
targets.push({ name: str(target.name) ?? id, release });
|
|
73
|
+
}
|
|
74
|
+
return targets;
|
|
75
|
+
};
|
|
76
|
+
const readPlistEntry = (plist, key, setting) => {
|
|
77
|
+
const match = plist === null
|
|
78
|
+
? null
|
|
79
|
+
: new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([^<]*)</string>`).exec(plist);
|
|
80
|
+
const value = match ? match[1].trim() : '';
|
|
81
|
+
if (value === '' || value === `$(${setting})` || value === `\${${setting}}`) {
|
|
82
|
+
return { kind: 'setting' };
|
|
83
|
+
}
|
|
84
|
+
return { kind: hasVariable(value) ? 'variable' : 'literal', value };
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* ターゲットの Info.plist のパス。Release の `INFOPLIST_FILE`(`$(SRCROOT)` / `$(PROJECT_DIR)` は .xcodeproj のある
|
|
88
|
+
* ディレクトリ)、無ければ `<ターゲット名>/Info.plist`(React Native / Expo のテンプレートの置き場所)。
|
|
89
|
+
* ほかの変数を含んで場所が決まらなければ null
|
|
90
|
+
*/
|
|
91
|
+
const infoPlistPath = (projectDir, target) => {
|
|
92
|
+
const setting = str(target.release?.INFOPLIST_FILE);
|
|
93
|
+
if (setting === null || setting === '') {
|
|
94
|
+
return join(projectDir, target.name, 'Info.plist');
|
|
95
|
+
}
|
|
96
|
+
const path = setting.replace(/^\$[({](SRCROOT|PROJECT_DIR)[)}]\/?/, '');
|
|
97
|
+
if (hasVariable(path)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return resolve(projectDir, path);
|
|
101
|
+
};
|
|
102
|
+
/** `ios/` が無ければ null(CNG)。読めない・特定できないものは {@link Lookup} の reason にする */
|
|
103
|
+
export const readIosProject = (cwd) => {
|
|
104
|
+
const iosDir = join(cwd, 'ios');
|
|
105
|
+
if (!isDirectory(iosDir)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const notes = [];
|
|
109
|
+
const unknown = (reason) => ({
|
|
110
|
+
bundleId: { value: null, reason },
|
|
111
|
+
version: { value: null, reason },
|
|
112
|
+
notes,
|
|
113
|
+
});
|
|
114
|
+
const xcodeprojs = findXcodeprojs(iosDir);
|
|
115
|
+
if (xcodeprojs.length === 0) {
|
|
116
|
+
return unknown('there is no .xcodeproj in ios/ (other than Pods.xcodeproj)');
|
|
117
|
+
}
|
|
118
|
+
if (xcodeprojs.length > 1) {
|
|
119
|
+
notes.push(`ios/ has ${xcodeprojs.length} .xcodeproj (${xcodeprojs.join(', ')}); using the first one, ios/${xcodeprojs[0]}`);
|
|
120
|
+
}
|
|
121
|
+
const pbxprojPath = join(iosDir, xcodeprojs[0], 'project.pbxproj');
|
|
122
|
+
const pbxproj = displayPath(cwd, pbxprojPath);
|
|
123
|
+
const text = readText(pbxprojPath);
|
|
124
|
+
if (text === null) {
|
|
125
|
+
return unknown(`could not read ${pbxproj}`);
|
|
126
|
+
}
|
|
127
|
+
let targets;
|
|
128
|
+
try {
|
|
129
|
+
const root = parsePlist(text);
|
|
130
|
+
if (!isDict(root)) {
|
|
131
|
+
throw new Error('the root is not a dictionary');
|
|
132
|
+
}
|
|
133
|
+
targets = findAppTargets(root);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return unknown(`could not parse ${pbxproj}: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
const [target] = targets;
|
|
139
|
+
if (target === undefined) {
|
|
140
|
+
return unknown(`there is no application target in ${pbxproj}`);
|
|
141
|
+
}
|
|
142
|
+
if (targets.length > 1) {
|
|
143
|
+
notes.push(`${pbxproj} has ${targets.length} application targets (${targets.map((t) => `"${t.name}"`).join(', ')}); using the first one, "${target.name}"`);
|
|
144
|
+
}
|
|
145
|
+
const { release } = target;
|
|
146
|
+
if (release === null) {
|
|
147
|
+
return unknown(`target "${target.name}" in ${pbxproj} has no Release build configuration`);
|
|
148
|
+
}
|
|
149
|
+
/** Release の build settings の値。無い・変数を含むなら reason */
|
|
150
|
+
const setting = (key) => {
|
|
151
|
+
const value = str(release[key]);
|
|
152
|
+
if (value === null || value === '') {
|
|
153
|
+
return {
|
|
154
|
+
value: null,
|
|
155
|
+
reason: `${key} is not set in the Release build settings of target "${target.name}" in ${pbxproj}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (hasVariable(value)) {
|
|
159
|
+
return {
|
|
160
|
+
value: null,
|
|
161
|
+
reason: `${key} of target "${target.name}" (Release) in ${pbxproj} is "${value}", which depends on other build settings`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { value, label: key, file: pbxproj };
|
|
165
|
+
};
|
|
166
|
+
// SRCROOT は .xcodeproj のあるディレクトリ(ios/)
|
|
167
|
+
const plistPath = infoPlistPath(iosDir, target);
|
|
168
|
+
const plist = plistPath === null ? null : readText(plistPath);
|
|
169
|
+
const plistFile = plistPath === null ? '' : displayPath(cwd, plistPath);
|
|
170
|
+
// bundleId・版とも、Info.plist の値がリテラルならそれ(Xcode はビルドの結果に Info.plist の値を入れる)、
|
|
171
|
+
// `$(PRODUCT_BUNDLE_IDENTIFIER)` / `$(MARKETING_VERSION)` そのものなら build settings の値。ほかの変数を含めば特定できない
|
|
172
|
+
const fromPlist = (entry, key, fallback) => entry.kind === 'setting'
|
|
173
|
+
? fallback()
|
|
174
|
+
: entry.kind === 'variable'
|
|
175
|
+
? {
|
|
176
|
+
value: null,
|
|
177
|
+
reason: `${key} "${entry.value}" in ${plistFile} depends on other build settings`,
|
|
178
|
+
}
|
|
179
|
+
: { value: entry.value, label: key, file: plistFile };
|
|
180
|
+
const plistId = readPlistEntry(plist, 'CFBundleIdentifier', 'PRODUCT_BUNDLE_IDENTIFIER');
|
|
181
|
+
const productId = setting('PRODUCT_BUNDLE_IDENTIFIER');
|
|
182
|
+
if (plistId.kind === 'literal' &&
|
|
183
|
+
productId.value !== null &&
|
|
184
|
+
productId.value !== plistId.value) {
|
|
185
|
+
notes.push(`${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}"`);
|
|
186
|
+
}
|
|
187
|
+
const bundleId = fromPlist(plistId, 'CFBundleIdentifier', () => productId);
|
|
188
|
+
const version = fromPlist(readPlistEntry(plist, 'CFBundleShortVersionString', 'MARKETING_VERSION'), 'CFBundleShortVersionString', () => setting('MARKETING_VERSION'));
|
|
189
|
+
return { bundleId, version, notes };
|
|
190
|
+
};
|