@smileid/usesmileid_mlkit_face 12.0.0 → 12.0.2
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 +8 -0
- package/README.md +26 -2
- package/android/build.gradle +8 -3
- package/app.plugin.js +3 -0
- package/package.json +16 -7
- package/plugin/build/index.d.ts +11 -0
- package/plugin/build/index.js +27 -0
- package/plugin/build/index.js.map +1 -0
- package/plugin/build/lib/mlkit_manifest.d.ts +13 -0
- package/plugin/build/lib/mlkit_manifest.js +54 -0
- package/plugin/build/lib/mlkit_manifest.js.map +1 -0
- package/plugin/build/lib/root_project.d.ts +19 -0
- package/plugin/build/lib/root_project.js +109 -0
- package/plugin/build/lib/root_project.js.map +1 -0
- package/plugin/src/__tests__/mirrored_sources.test.ts +57 -0
- package/plugin/src/__tests__/mlkit_manifest.test.ts +117 -0
- package/plugin/src/__tests__/root_project.test.ts +195 -0
- package/plugin/src/index.ts +47 -0
- package/plugin/src/lib/mlkit_manifest.ts +88 -0
- package/plugin/src/lib/root_project.ts +160 -0
- package/plugin/tsconfig.json +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 12.0.2
|
|
2
|
+
|
|
3
|
+
See the repository CHANGELOG for the 12.0.2 release notes.
|
|
4
|
+
|
|
5
|
+
## 12.0.1
|
|
6
|
+
|
|
7
|
+
See the repository CHANGELOG for the 12.0.1 release notes.
|
|
8
|
+
|
|
1
9
|
## 12.0.0
|
|
2
10
|
|
|
3
11
|
First stable release of the Smile ID v12 SDK. Provides the Android Google ML Kit face detection module for `usesmileid`.
|
package/README.md
CHANGED
|
@@ -20,8 +20,32 @@ No ML logic is reimplemented in this package — it's a pure passthrough.
|
|
|
20
20
|
pnpm add @smileid/usesmileid_mlkit_face
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Expo's autolinking picks up `expo-module.config.json` at app build time
|
|
24
|
-
|
|
23
|
+
Expo's autolinking picks up `expo-module.config.json` at app build time, but the
|
|
24
|
+
build settings the AAR needs belong to the root Gradle project, so add the
|
|
25
|
+
package to `expo.plugins` in `app.json` and run `npx expo prebuild --clean`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"expo": {
|
|
30
|
+
"plugins": [
|
|
31
|
+
[
|
|
32
|
+
"@smileid/usesmileid_mlkit_face",
|
|
33
|
+
{ "additionalModelDependencies": ["barcode_ui"] }
|
|
34
|
+
]
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The plugin pins `compileSdk`, the Kotlin/KSP versions the AAR was compiled with,
|
|
41
|
+
and the `androidx.core` version that resolves under Expo's Android Gradle Plugin.
|
|
42
|
+
It also claims the `com.google.mlkit.vision.DEPENDENCIES` meta-data on your
|
|
43
|
+
`<application>` with `tools:replace`: this AAR declares that key as `face`, so
|
|
44
|
+
without the override a second ML Kit consumer declaring its own value (for
|
|
45
|
+
instance `expo-camera`'s barcode scanner, which declares `barcode_ui`) fails the
|
|
46
|
+
Android manifest merger. `additionalModelDependencies` is optional — name the
|
|
47
|
+
hints your other consumers declare so their models are prefetched at install
|
|
48
|
+
time rather than downloaded on first use.
|
|
25
49
|
|
|
26
50
|
## Usage
|
|
27
51
|
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,7 @@ plugins {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
group = 'com.usesmileid.mlkit.face'
|
|
20
|
-
version = '12.0.
|
|
20
|
+
version = '12.0.2'
|
|
21
21
|
|
|
22
22
|
android {
|
|
23
23
|
namespace 'com.usesmileid.mlkit.face'
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
// android/build.gradle and the rest of the @expo/* modules.
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 1
|
|
32
|
-
versionName '12.0.
|
|
32
|
+
versionName '12.0.2'
|
|
33
33
|
// R8 keep-rules to bundle into partner apps that depend on this
|
|
34
34
|
// module. Without these, R8 may rename the Expo module entry
|
|
35
35
|
// point and the JS-bridged member functions, breaking autolinking.
|
|
@@ -41,7 +41,7 @@ dependencies {
|
|
|
41
41
|
// Existing Smile ID native AAR — the entire ML pipeline lives here.
|
|
42
42
|
// This Expo Module is a thin Kotlin shim that constructs the
|
|
43
43
|
// session and bridges its four methods to JS via Expo Modules.
|
|
44
|
-
implementation 'com.usesmileid:mlkit-face:12.0.
|
|
44
|
+
implementation 'com.usesmileid:mlkit-face:12.0.2'
|
|
45
45
|
|
|
46
46
|
// VisionCamera frame-processor plugin classes. The host app provides
|
|
47
47
|
// this dependency via RN autolinking — react-native-vision-camera >= 4.7.3
|
|
@@ -55,4 +55,9 @@ dependencies {
|
|
|
55
55
|
// app's VisionCamera provides the runtime classes (its camera-core is
|
|
56
56
|
// 1.5.0-alpha03; the `ImageProxy` surface used here is stable since 1.0).
|
|
57
57
|
compileOnly 'androidx.camera:camera-core:1.5.0-alpha03'
|
|
58
|
+
|
|
59
|
+
// `FaceDetectorAnalyzer()` takes a defaulted `FaceDetector` argument, so the compiler
|
|
60
|
+
// needs that type to emit the call. The AAR above declares ML Kit at runtime
|
|
61
|
+
// scope only — enough to execute, not to compile against.
|
|
62
|
+
compileOnly 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
|
|
58
63
|
}
|
package/app.plugin.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/usesmileid_mlkit_face",
|
|
3
|
-
"version": "12.0.
|
|
3
|
+
"version": "12.0.2",
|
|
4
4
|
"description": "Android MLKit face detection implementation for the UseSmileID React Native (Expo) SDK.",
|
|
5
5
|
"homepage": "https://usesmileid.com",
|
|
6
6
|
"repository": {
|
|
@@ -31,20 +31,25 @@
|
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
32
|
"import": "./dist/index.js",
|
|
33
33
|
"require": "./dist/index.js"
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"./app.plugin.js": "./app.plugin.js"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"dist",
|
|
38
39
|
"src",
|
|
39
40
|
"android",
|
|
41
|
+
"plugin/build",
|
|
42
|
+
"plugin/src",
|
|
43
|
+
"plugin/tsconfig.json",
|
|
44
|
+
"app.plugin.js",
|
|
40
45
|
"expo-module.config.json",
|
|
41
46
|
"README.md",
|
|
42
47
|
"LICENSE",
|
|
43
48
|
"CHANGELOG.md"
|
|
44
49
|
],
|
|
45
50
|
"dependencies": {
|
|
46
|
-
"@smileid/usesmileid_bridge": "12.0.
|
|
47
|
-
"@smileid/usesmileid_platform_interface": "12.0.
|
|
51
|
+
"@smileid/usesmileid_bridge": "12.0.2",
|
|
52
|
+
"@smileid/usesmileid_platform_interface": "12.0.2"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
55
|
"expo": ">=56.0.0",
|
|
@@ -52,6 +57,8 @@
|
|
|
52
57
|
"react-native": ">=0.83.0"
|
|
53
58
|
},
|
|
54
59
|
"devDependencies": {
|
|
60
|
+
"@types/node": "^25.9.1",
|
|
61
|
+
"expo": "^56.0.9",
|
|
55
62
|
"expo-modules-core": "^56.0.15",
|
|
56
63
|
"react-native": "^0.85.3",
|
|
57
64
|
"typescript": "^6.0.3"
|
|
@@ -60,10 +67,12 @@
|
|
|
60
67
|
"access": "public"
|
|
61
68
|
},
|
|
62
69
|
"scripts": {
|
|
63
|
-
"build": "
|
|
64
|
-
"
|
|
70
|
+
"build": "pnpm run build:lib && pnpm run build:plugin",
|
|
71
|
+
"build:lib": "tsc -p tsconfig.json",
|
|
72
|
+
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
73
|
+
"typecheck": "tsc -b && tsc -p plugin/tsconfig.json --noEmit",
|
|
65
74
|
"test": "vitest run",
|
|
66
75
|
"test:watch": "vitest",
|
|
67
|
-
"clean": "rm -rf dist .turbo coverage"
|
|
76
|
+
"clean": "rm -rf dist plugin/build .turbo coverage"
|
|
68
77
|
}
|
|
69
78
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
import { type MlkitManifestMergeOptions } from "./lib/mlkit_manifest";
|
|
3
|
+
export interface MlkitFaceOptions extends MlkitManifestMergeOptions {
|
|
4
|
+
/** Overrides the default Kotlin pin (must read the AARs' metadata). */
|
|
5
|
+
kotlinVersion?: string;
|
|
6
|
+
/** Opt-in: only when Expo's lookup table lacks the pinned Kotlin. */
|
|
7
|
+
kspVersion?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const withMlkitFace: ConfigPlugin<MlkitFaceOptions | void>;
|
|
10
|
+
export default withMlkitFace;
|
|
11
|
+
export type { MlkitManifestMergeOptions };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The module is autolinked as a Gradle subproject, so it inherits the host's
|
|
3
|
+
// Kotlin compiler and `compileSdk`; Expo's defaults for both are lower than the
|
|
4
|
+
// AARs accept and a library cannot raise a setting the root project owns.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const root_project_1 = require("./lib/root_project");
|
|
7
|
+
const mlkit_manifest_1 = require("./lib/mlkit_manifest");
|
|
8
|
+
// Lowest Kotlin that reads the AARs' 2.4.x metadata AND has Expo
|
|
9
|
+
// compiler-plugin (pika) artifacts; higher pins break expo-modules-core.
|
|
10
|
+
// KSP is left to Expo's own kotlin→ksp lookup table.
|
|
11
|
+
const DEFAULT_KOTLIN_VERSION = "2.3.20";
|
|
12
|
+
const COMPILE_SDK_VERSION = 37;
|
|
13
|
+
const FORCED_DEPENDENCIES = [
|
|
14
|
+
"androidx.core:core:1.18.0",
|
|
15
|
+
"androidx.core:core-ktx:1.18.0",
|
|
16
|
+
];
|
|
17
|
+
const withMlkitFace = (config, options) => {
|
|
18
|
+
const { kotlinVersion, kspVersion, ...manifestOptions } = options ?? {};
|
|
19
|
+
return (0, mlkit_manifest_1.withMlkitManifestMerge)((0, root_project_1.withAndroidRootProjectPins)(config, {
|
|
20
|
+
kotlinVersion: kotlinVersion ?? DEFAULT_KOTLIN_VERSION,
|
|
21
|
+
...(kspVersion !== undefined ? { kspVersion } : {}),
|
|
22
|
+
compileSdkVersion: COMPILE_SDK_VERSION,
|
|
23
|
+
forcedDependencies: FORCED_DEPENDENCIES,
|
|
24
|
+
}), manifestOptions);
|
|
25
|
+
};
|
|
26
|
+
exports.default = withMlkitFace;
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,6EAA6E;AAC7E,gFAAgF;AAChF,0EAA0E;;AAI1E,qDAAgE;AAChE,yDAG8B;AAE9B,iEAAiE;AACjE,yEAAyE;AACzE,qDAAqD;AACrD,MAAM,sBAAsB,GAAG,QAAQ,CAAC;AACxC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAC/B,MAAM,mBAAmB,GAAG;IAC1B,2BAA2B;IAC3B,+BAA+B;CAChC,CAAC;AASF,MAAM,aAAa,GAA0C,CAC3D,MAAM,EACN,OAAO,EACP,EAAE;IACF,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;IACxE,OAAO,IAAA,uCAAsB,EAC3B,IAAA,yCAA0B,EAAC,MAAM,EAAE;QACjC,aAAa,EAAE,aAAa,IAAI,sBAAsB;QACtD,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,iBAAiB,EAAE,mBAAmB;QACtC,kBAAkB,EAAE,mBAAmB;KACxC,CAAC,EACF,eAAe,CAChB,CAAC;AACJ,CAAC,CAAC;AAEF,kBAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AndroidConfig, type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
type AndroidManifest = AndroidConfig.Manifest.AndroidManifest;
|
|
3
|
+
export interface MlkitManifestMergeOptions {
|
|
4
|
+
/** Hints your app's other ML Kit consumers declare, so their models prefetch at install time rather than on first use. */
|
|
5
|
+
readonly additionalModelDependencies?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
/** Provider hint first, then the caller's, deduplicated and order-stable. */
|
|
8
|
+
export declare function mergedModelDependencies(additional: readonly string[]): string;
|
|
9
|
+
/** Declares the merged `com.google.mlkit.vision.DEPENDENCIES` on the host `<application>` with `tools:replace`. */
|
|
10
|
+
export declare const withMlkitManifestMerge: ConfigPlugin<MlkitManifestMergeOptions | void>;
|
|
11
|
+
/** Pure form of the edit. Idempotent; a re-run replaces a stale value. */
|
|
12
|
+
export declare function applyMlkitManifestMerge(manifest: AndroidManifest, value: string): AndroidManifest;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The AAR declares this meta-data without `tools:replace`, so only the host `<application>` can claim it for every ML Kit consumer at once.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.withMlkitManifestMerge = void 0;
|
|
5
|
+
exports.mergedModelDependencies = mergedModelDependencies;
|
|
6
|
+
exports.applyMlkitManifestMerge = applyMlkitManifestMerge;
|
|
7
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
8
|
+
const META_DATA_NAME = "com.google.mlkit.vision.DEPENDENCIES";
|
|
9
|
+
const TOOLS_NAMESPACE = "http://schemas.android.com/tools";
|
|
10
|
+
/** The hint this package's AAR declares; always part of the merged value. */
|
|
11
|
+
const PROVIDER_MODEL_DEPENDENCY = "face";
|
|
12
|
+
// ML Kit's own hint vocabulary — lowercase words and underscores.
|
|
13
|
+
const MODEL_DEPENDENCY = /^[a-z0-9_]+$/;
|
|
14
|
+
/** Provider hint first, then the caller's, deduplicated and order-stable. */
|
|
15
|
+
function mergedModelDependencies(additional) {
|
|
16
|
+
for (const dependency of additional) {
|
|
17
|
+
if (!MODEL_DEPENDENCY.test(dependency)) {
|
|
18
|
+
throw new Error(`[smileid-android] '${dependency}' is not a valid ML Kit model dependency. ` +
|
|
19
|
+
"Expected a lowercase hint such as 'barcode_ui', as printed in the manifest-merger error.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return [...new Set([PROVIDER_MODEL_DEPENDENCY, ...additional])].join(",");
|
|
23
|
+
}
|
|
24
|
+
/** Declares the merged `com.google.mlkit.vision.DEPENDENCIES` on the host `<application>` with `tools:replace`. */
|
|
25
|
+
const withMlkitManifestMerge = (config, options) => (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
26
|
+
config.modResults = applyMlkitManifestMerge(config.modResults, mergedModelDependencies(options?.additionalModelDependencies ?? []));
|
|
27
|
+
return config;
|
|
28
|
+
});
|
|
29
|
+
exports.withMlkitManifestMerge = withMlkitManifestMerge;
|
|
30
|
+
/** Pure form of the edit. Idempotent; a re-run replaces a stale value. */
|
|
31
|
+
function applyMlkitManifestMerge(manifest, value) {
|
|
32
|
+
const application = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
33
|
+
application["meta-data"] ??= [];
|
|
34
|
+
const entries = application["meta-data"];
|
|
35
|
+
const existing = entries.find((entry) => entry?.$?.["android:name"] === META_DATA_NAME);
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.$["android:value"] = value;
|
|
38
|
+
existing.$["tools:replace"] = "android:value";
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
entries.push({
|
|
42
|
+
$: {
|
|
43
|
+
"android:name": META_DATA_NAME,
|
|
44
|
+
"android:value": value,
|
|
45
|
+
"tools:replace": "android:value",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Without the namespace on <manifest>, `tools:replace` merges as an unknown attribute.
|
|
50
|
+
manifest.manifest.$ ??= {};
|
|
51
|
+
manifest.manifest.$["xmlns:tools"] ??= TOOLS_NAMESPACE;
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=mlkit_manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mlkit_manifest.js","sourceRoot":"","sources":["../../src/lib/mlkit_manifest.ts"],"names":[],"mappings":";AAAA,4IAA4I;;;AAgC5I,0DAUC;AAeD,0DA8BC;AArFD,wDAI6B;AAW7B,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAC9D,MAAM,eAAe,GAAG,kCAAkC,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEzC,kEAAkE;AAClE,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAOxC,6EAA6E;AAC7E,SAAgB,uBAAuB,CAAC,UAA6B;IACnE,KAAK,MAAM,UAAU,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CACb,sBAAsB,UAAU,4CAA4C;gBAC1E,0FAA0F,CAC7F,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,yBAAyB,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5E,CAAC;AAED,mHAAmH;AAC5G,MAAM,sBAAsB,GAE/B,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CACtB,IAAA,oCAAmB,EAAC,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;IACrC,MAAM,CAAC,UAAU,GAAG,uBAAuB,CACzC,MAAM,CAAC,UAAU,EACjB,uBAAuB,CAAC,OAAO,EAAE,2BAA2B,IAAI,EAAE,CAAC,CACpE,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC,CAAC;AATQ,QAAA,sBAAsB,0BAS9B;AAEL,0EAA0E;AAC1E,SAAgB,uBAAuB,CACrC,QAAyB,EACzB,KAAa;IAEb,MAAM,WAAW,GACf,8BAAa,CAAC,QAAQ,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAC7D,WAAW,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAA0B,CAAC;IAElE,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAC3B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,cAAc,CAAC,KAAK,cAAc,CACzD,CAAC;IACF,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC;QACpC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,eAAe,CAAC;IAChD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC;YACX,CAAC,EAAE;gBACD,cAAc,EAAE,cAAc;gBAC9B,eAAe,EAAE,KAAK;gBACtB,eAAe,EAAE,eAAe;aACjC;SACF,CAAC,CAAC;IACL,CAAC;IAED,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAsC,CAAC;IAC/D,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,eAAe,CAAC;IAEvD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ConfigPlugin } from "expo/config-plugins";
|
|
2
|
+
export interface AndroidRootProjectPins {
|
|
3
|
+
/**
|
|
4
|
+
* Opt-in: only for hosts whose Kotlin predates the AARs' compiler. Current
|
|
5
|
+
* Expo defaults read the metadata, and forcing a Kotlin that Expo ships no
|
|
6
|
+
* compiler-plugin artifacts for breaks expo-modules-core.
|
|
7
|
+
*/
|
|
8
|
+
kotlinVersion?: string;
|
|
9
|
+
/** Only alongside `kotlinVersion` — Expo's own lookup table may carry no entry for it. */
|
|
10
|
+
kspVersion?: string;
|
|
11
|
+
/** The AARs declare it as a consumer minimum. Independent of `minSdk`, so device support is unchanged. */
|
|
12
|
+
compileSdkVersion: number;
|
|
13
|
+
/** `group:name:version` coordinates forced everywhere, for deps whose newest release the host toolchain cannot consume. */
|
|
14
|
+
forcedDependencies: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/** Declares the pins ahead of `apply plugin: "expo-root-project"`. */
|
|
17
|
+
export declare const withAndroidRootProjectPins: ConfigPlugin<AndroidRootProjectPins>;
|
|
18
|
+
/** Pure form of the edit. Idempotent; a re-run replaces stale values rather than appending. */
|
|
19
|
+
export declare function applyAndroidRootProjectPins(contents: string, pins: AndroidRootProjectPins): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `expo-root-project` resolves these through `extra.setIfNotExist`, so they only
|
|
3
|
+
// take effect when declared before that plugin applies.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.withAndroidRootProjectPins = void 0;
|
|
6
|
+
exports.applyAndroidRootProjectPins = applyAndroidRootProjectPins;
|
|
7
|
+
const config_plugins_1 = require("expo/config-plugins");
|
|
8
|
+
const BEGIN_MARKER = "// === BEGIN smileid root-project pins ===";
|
|
9
|
+
const END_MARKER = "// === END smileid root-project pins ===";
|
|
10
|
+
const APPLY_ROOT_PROJECT = /^apply\s+plugin:\s*["']expo-root-project["']/m;
|
|
11
|
+
// Bumping `ext.kotlinVersion` moves the dependency catalog but not the compiler, so
|
|
12
|
+
// the classpath line needs pinning too. Version and parentheses are optional so an
|
|
13
|
+
// already-pinned line and Groovy's space-call form still match.
|
|
14
|
+
const KGP_CLASSPATH = /classpath\s*(?:\(\s*)?['"]org\.jetbrains\.kotlin:kotlin-gradle-plugin(?::[^'"]*)?['"](?:\s*\))?/;
|
|
15
|
+
/** Declares the pins ahead of `apply plugin: "expo-root-project"`. */
|
|
16
|
+
const withAndroidRootProjectPins = (config, pins) => (0, config_plugins_1.withProjectBuildGradle)(config, (config) => {
|
|
17
|
+
if (config.modResults.language !== "groovy") {
|
|
18
|
+
throw new Error("[smileid-android] Expected android/build.gradle to be Groovy DSL; refusing to edit " +
|
|
19
|
+
`a ${config.modResults.language} build script.`);
|
|
20
|
+
}
|
|
21
|
+
config.modResults.contents = applyAndroidRootProjectPins(config.modResults.contents, pins);
|
|
22
|
+
return config;
|
|
23
|
+
});
|
|
24
|
+
exports.withAndroidRootProjectPins = withAndroidRootProjectPins;
|
|
25
|
+
/** Pure form of the edit. Idempotent; a re-run replaces stale values rather than appending. */
|
|
26
|
+
function applyAndroidRootProjectPins(contents, pins) {
|
|
27
|
+
if (pins.kspVersion !== undefined && pins.kotlinVersion === undefined) {
|
|
28
|
+
throw new Error("[smileid-android] `kspVersion` only makes sense alongside `kotlinVersion`.");
|
|
29
|
+
}
|
|
30
|
+
validateGroovySafe(pins);
|
|
31
|
+
const pinned = pins.kotlinVersion === undefined
|
|
32
|
+
? contents
|
|
33
|
+
: pinKotlinGradlePlugin(contents, pins.kotlinVersion);
|
|
34
|
+
return upsertPinBlock(pinned, renderPinBlock(pins));
|
|
35
|
+
}
|
|
36
|
+
// Every value lands in a single-quoted Groovy literal; reject anything that could close one.
|
|
37
|
+
function validateGroovySafe(pins) {
|
|
38
|
+
const values = [
|
|
39
|
+
pins.kotlinVersion,
|
|
40
|
+
pins.kspVersion,
|
|
41
|
+
...pins.forcedDependencies,
|
|
42
|
+
].filter((value) => value !== undefined);
|
|
43
|
+
for (const value of values) {
|
|
44
|
+
if (/['\\\n\r\t$]/.test(value)) {
|
|
45
|
+
throw new Error(`[smileid-android] Refusing to edit android/build.gradle: '${value}' contains an unsafe character.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function pinKotlinGradlePlugin(contents, kotlinVersion) {
|
|
50
|
+
const pinned = `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}')`;
|
|
51
|
+
if (!KGP_CLASSPATH.test(contents)) {
|
|
52
|
+
throw new Error("[smileid-android] Could not locate the kotlin-gradle-plugin classpath line in " +
|
|
53
|
+
"android/build.gradle. Refusing to silently no-op — the Smile ID analyzer modules do not " +
|
|
54
|
+
`compile unless the Kotlin compiler is pinned to ${kotlinVersion}.`);
|
|
55
|
+
}
|
|
56
|
+
return contents.replace(KGP_CLASSPATH, pinned);
|
|
57
|
+
}
|
|
58
|
+
// Adding a further root-project pin means adding an entry here.
|
|
59
|
+
function renderPinBlock(pins) {
|
|
60
|
+
const extProperties = [];
|
|
61
|
+
if (pins.kotlinVersion !== undefined) {
|
|
62
|
+
extProperties.push(["kotlinVersion", `'${pins.kotlinVersion}'`]);
|
|
63
|
+
}
|
|
64
|
+
if (pins.kspVersion !== undefined) {
|
|
65
|
+
extProperties.push(["kspVersion", `'${pins.kspVersion}'`]);
|
|
66
|
+
}
|
|
67
|
+
extProperties.push(["compileSdkVersion", String(pins.compileSdkVersion)]);
|
|
68
|
+
const lines = extProperties.map(([name, value]) => `ext.${name} = ${value}`);
|
|
69
|
+
return [
|
|
70
|
+
BEGIN_MARKER,
|
|
71
|
+
...lines,
|
|
72
|
+
...renderForcedDependencies(pins.forcedDependencies),
|
|
73
|
+
END_MARKER,
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
// Its own `allprojects` block, which Gradle merges — editing the template's would
|
|
77
|
+
// break the moment its shape changes.
|
|
78
|
+
function renderForcedDependencies(coordinates) {
|
|
79
|
+
if (coordinates.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return [
|
|
83
|
+
"",
|
|
84
|
+
"// Forced to the newest release that resolves under the AGP Expo generates.",
|
|
85
|
+
"allprojects {",
|
|
86
|
+
" configurations.all {",
|
|
87
|
+
" resolutionStrategy {",
|
|
88
|
+
...coordinates.map((coordinate) => ` force '${coordinate}'`),
|
|
89
|
+
" }",
|
|
90
|
+
" }",
|
|
91
|
+
"}",
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
function upsertPinBlock(contents, block) {
|
|
95
|
+
const existing = new RegExp(`${escapeRegExp(BEGIN_MARKER)}[\\s\\S]*?${escapeRegExp(END_MARKER)}`, "m");
|
|
96
|
+
if (existing.test(contents)) {
|
|
97
|
+
return contents.replace(existing, block);
|
|
98
|
+
}
|
|
99
|
+
if (!APPLY_ROOT_PROJECT.test(contents)) {
|
|
100
|
+
throw new Error('[smileid-android] Could not locate `apply plugin: "expo-root-project"` in ' +
|
|
101
|
+
"android/build.gradle. Refusing to silently no-op — these pins only take effect when " +
|
|
102
|
+
"declared before that plugin applies.");
|
|
103
|
+
}
|
|
104
|
+
return contents.replace(APPLY_ROOT_PROJECT, (match) => `${block}\n\n${match}`);
|
|
105
|
+
}
|
|
106
|
+
function escapeRegExp(value) {
|
|
107
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=root_project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root_project.js","sourceRoot":"","sources":["../../src/lib/root_project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
// Both packages write the same marked region of `android/build.gradle`, so if
|
|
6
|
+
// their pins diverge the plugin Expo runs last silently wins.
|
|
7
|
+
|
|
8
|
+
const workspaceRoot = fileURLToPath(new URL("../../../../", import.meta.url));
|
|
9
|
+
|
|
10
|
+
function read(relativePath: string): string {
|
|
11
|
+
return readFileSync(`${workspaceRoot}${relativePath}`, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("mirrored plugin sources", () => {
|
|
15
|
+
// `plugin/src/index.ts` is deliberately absent: only the face package composes the manifest-merge plugin.
|
|
16
|
+
it.each([
|
|
17
|
+
"plugin/src/lib/root_project.ts",
|
|
18
|
+
"plugin/tsconfig.json",
|
|
19
|
+
"app.plugin.js",
|
|
20
|
+
])("keeps the mlkit packages' %s in step", (relativePath) => {
|
|
21
|
+
const face = read(`usesmileid_mlkit_face/${relativePath}`);
|
|
22
|
+
const document = read(`usesmileid_mlkit_document/${relativePath}`);
|
|
23
|
+
|
|
24
|
+
// The entry points differ only in the analyzer they name.
|
|
25
|
+
expect(normalise(face)).toBe(normalise(document));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
"plugin/src/lib/podfile.ts",
|
|
30
|
+
"plugin/src/lib/deployment_target.ts",
|
|
31
|
+
"plugin/tsconfig.json",
|
|
32
|
+
"app.plugin.js",
|
|
33
|
+
])("keeps the vision packages' %s in step", (relativePath) => {
|
|
34
|
+
expect(read(`usesmileid_vision_face/${relativePath}`)).toBe(
|
|
35
|
+
read(`usesmileid_vision_document/${relativePath}`),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("pins the same Android versions in both mlkit plugins", () => {
|
|
40
|
+
const pins =
|
|
41
|
+
/const (DEFAULT_KOTLIN_VERSION|COMPILE_SDK_VERSION|FORCED_DEPENDENCIES)[\s\S]*?;/g;
|
|
42
|
+
const face = read("usesmileid_mlkit_face/plugin/src/index.ts").match(pins);
|
|
43
|
+
const document = read(
|
|
44
|
+
"usesmileid_mlkit_document/plugin/src/index.ts",
|
|
45
|
+
).match(pins);
|
|
46
|
+
|
|
47
|
+
expect(face).not.toBeNull();
|
|
48
|
+
expect(face).toHaveLength(3);
|
|
49
|
+
expect(face).toStrictEqual(document);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function normalise(source: string): string {
|
|
54
|
+
return source
|
|
55
|
+
.replace(/face/gi, "<analyzer>")
|
|
56
|
+
.replace(/document/gi, "<analyzer>");
|
|
57
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AndroidConfig } from "expo/config-plugins";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
applyMlkitManifestMerge,
|
|
6
|
+
mergedModelDependencies,
|
|
7
|
+
} from "../lib/mlkit_manifest";
|
|
8
|
+
|
|
9
|
+
type AndroidManifest = AndroidConfig.Manifest.AndroidManifest;
|
|
10
|
+
|
|
11
|
+
const META_DATA_NAME = "com.google.mlkit.vision.DEPENDENCIES";
|
|
12
|
+
|
|
13
|
+
// xml2js form, as `withAndroidManifest` hands the prebuild manifest over.
|
|
14
|
+
function template(): AndroidManifest {
|
|
15
|
+
return {
|
|
16
|
+
manifest: {
|
|
17
|
+
$: { "xmlns:android": "http://schemas.android.com/apk/res/android" },
|
|
18
|
+
application: [
|
|
19
|
+
{
|
|
20
|
+
$: { "android:name": ".MainApplication" },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
} as unknown as AndroidManifest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function metaData(manifest: AndroidManifest) {
|
|
28
|
+
const application =
|
|
29
|
+
AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
30
|
+
return (application["meta-data"] ?? []).find(
|
|
31
|
+
(entry) => entry?.$?.["android:name"] === META_DATA_NAME,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("mergedModelDependencies", () => {
|
|
36
|
+
it("always declares the hint this package's AAR carries", () => {
|
|
37
|
+
expect(mergedModelDependencies([])).toBe("face");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("appends the host app's other ML Kit hints", () => {
|
|
41
|
+
expect(mergedModelDependencies(["barcode_ui"])).toBe("face,barcode_ui");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("deduplicates so a caller repeating the provider hint is harmless", () => {
|
|
45
|
+
expect(mergedModelDependencies(["face", "barcode_ui", "barcode_ui"])).toBe(
|
|
46
|
+
"face,barcode_ui",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects a value that could not be an ML Kit hint", () => {
|
|
51
|
+
expect(() => mergedModelDependencies(['face" />'])).toThrow(
|
|
52
|
+
/not a valid ML Kit model dependency/,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("applyMlkitManifestMerge", () => {
|
|
58
|
+
it("claims the attribute for every ML Kit library in the app", () => {
|
|
59
|
+
const manifest = applyMlkitManifestMerge(template(), "face,barcode_ui");
|
|
60
|
+
const entry = metaData(manifest);
|
|
61
|
+
|
|
62
|
+
expect(entry?.$["android:value"]).toBe("face,barcode_ui");
|
|
63
|
+
expect(entry?.$["tools:replace"]).toBe("android:value");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("declares the tools namespace the tools:replace attribute needs", () => {
|
|
67
|
+
const manifest = applyMlkitManifestMerge(template(), "face");
|
|
68
|
+
|
|
69
|
+
expect(manifest.manifest.$["xmlns:tools"]).toBe(
|
|
70
|
+
"http://schemas.android.com/tools",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("leaves a namespace the host already declared alone", () => {
|
|
75
|
+
const withNamespace = template();
|
|
76
|
+
withNamespace.manifest.$["xmlns:tools"] =
|
|
77
|
+
"http://schemas.android.com/tools";
|
|
78
|
+
|
|
79
|
+
const manifest = applyMlkitManifestMerge(withNamespace, "face");
|
|
80
|
+
|
|
81
|
+
expect(manifest.manifest.$["xmlns:tools"]).toBe(
|
|
82
|
+
"http://schemas.android.com/tools",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("replaces a stale value instead of declaring the key twice", () => {
|
|
87
|
+
const once = applyMlkitManifestMerge(template(), "face");
|
|
88
|
+
const twice = applyMlkitManifestMerge(once, "face,barcode_ui");
|
|
89
|
+
|
|
90
|
+
const application = AndroidConfig.Manifest.getMainApplicationOrThrow(twice);
|
|
91
|
+
const entries = (application["meta-data"] ?? []).filter(
|
|
92
|
+
(entry) => entry?.$?.["android:name"] === META_DATA_NAME,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(entries).toHaveLength(1);
|
|
96
|
+
expect(entries[0]?.$["android:value"]).toBe("face,barcode_ui");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps unrelated meta-data the host declared", () => {
|
|
100
|
+
const withOther = template();
|
|
101
|
+
const application =
|
|
102
|
+
AndroidConfig.Manifest.getMainApplicationOrThrow(withOther);
|
|
103
|
+
application["meta-data"] = [
|
|
104
|
+
{ $: { "android:name": "com.example.OTHER", "android:value": "keep" } },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const manifest = applyMlkitManifestMerge(withOther, "face");
|
|
108
|
+
const merged =
|
|
109
|
+
AndroidConfig.Manifest.getMainApplicationOrThrow(manifest)["meta-data"];
|
|
110
|
+
|
|
111
|
+
expect(merged).toHaveLength(2);
|
|
112
|
+
expect(
|
|
113
|
+
merged?.find((entry) => entry.$["android:name"] === "com.example.OTHER")
|
|
114
|
+
?.$["android:value"],
|
|
115
|
+
).toBe("keep");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyAndroidRootProjectPins,
|
|
5
|
+
type AndroidRootProjectPins,
|
|
6
|
+
} from "../lib/root_project";
|
|
7
|
+
|
|
8
|
+
const PINS: AndroidRootProjectPins = {
|
|
9
|
+
kotlinVersion: "2.4.10",
|
|
10
|
+
kspVersion: "2.3.10",
|
|
11
|
+
compileSdkVersion: 37,
|
|
12
|
+
forcedDependencies: [
|
|
13
|
+
"androidx.core:core:1.18.0",
|
|
14
|
+
"androidx.core:core-ktx:1.18.0",
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Shaped like the `android/build.gradle` Expo prebuild generates.
|
|
19
|
+
const TEMPLATE = `buildscript {
|
|
20
|
+
repositories {
|
|
21
|
+
google()
|
|
22
|
+
mavenCentral()
|
|
23
|
+
}
|
|
24
|
+
dependencies {
|
|
25
|
+
classpath('com.android.tools.build:gradle')
|
|
26
|
+
classpath('com.facebook.react:react-native-gradle-plugin')
|
|
27
|
+
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
allprojects {
|
|
32
|
+
repositories {
|
|
33
|
+
google()
|
|
34
|
+
mavenCentral()
|
|
35
|
+
maven { url 'https://www.jitpack.io' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
apply plugin: "expo-root-project"
|
|
40
|
+
apply plugin: "com.facebook.react.rootproject"
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
describe("applyAndroidRootProjectPins", () => {
|
|
44
|
+
it("declares every pin before expo-root-project applies", () => {
|
|
45
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, PINS);
|
|
46
|
+
|
|
47
|
+
expect(result).toContain("ext.kotlinVersion = '2.4.10'");
|
|
48
|
+
expect(result).toContain("ext.kspVersion = '2.3.10'");
|
|
49
|
+
expect(result).toContain("ext.compileSdkVersion = 37");
|
|
50
|
+
expect(result.indexOf("ext.kotlinVersion")).toBeLessThan(
|
|
51
|
+
result.indexOf('apply plugin: "expo-root-project"'),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("pins the Kotlin Gradle Plugin classpath so the compiler moves too", () => {
|
|
56
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, PINS);
|
|
57
|
+
|
|
58
|
+
expect(result).toContain(
|
|
59
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10')",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// A template using Groovy's space form must not abort prebuild.
|
|
64
|
+
it.each([
|
|
65
|
+
[
|
|
66
|
+
"space-call, double quotes",
|
|
67
|
+
'classpath "org.jetbrains.kotlin:kotlin-gradle-plugin"',
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"space-call, single quotes",
|
|
71
|
+
"classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin'",
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
"already versioned",
|
|
75
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20')",
|
|
76
|
+
],
|
|
77
|
+
])("pins the classpath written as %s", (_label, line) => {
|
|
78
|
+
const template = TEMPLATE.replace(
|
|
79
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')",
|
|
80
|
+
line,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const result = applyAndroidRootProjectPins(template, PINS);
|
|
84
|
+
|
|
85
|
+
expect(result).toContain(
|
|
86
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10')",
|
|
87
|
+
);
|
|
88
|
+
expect(result).not.toContain("kotlin-gradle-plugin:2.1.20");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("is idempotent across prebuilds", () => {
|
|
92
|
+
const once = applyAndroidRootProjectPins(TEMPLATE, PINS);
|
|
93
|
+
|
|
94
|
+
expect(applyAndroidRootProjectPins(once, PINS)).toBe(once);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("replaces stale versions instead of appending a second block", () => {
|
|
98
|
+
const stale = applyAndroidRootProjectPins(TEMPLATE, {
|
|
99
|
+
kotlinVersion: "2.3.20",
|
|
100
|
+
kspVersion: "2.3.6",
|
|
101
|
+
compileSdkVersion: 36,
|
|
102
|
+
forcedDependencies: ["androidx.core:core:1.17.0"],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = applyAndroidRootProjectPins(stale, PINS);
|
|
106
|
+
|
|
107
|
+
expect(result).not.toContain("2.3.20");
|
|
108
|
+
expect(result).not.toContain("ext.compileSdkVersion = 36");
|
|
109
|
+
expect(result).not.toContain("androidx.core:core:1.17.0");
|
|
110
|
+
expect(result.match(/ext\.kotlinVersion/g)).toHaveLength(1);
|
|
111
|
+
expect(result.match(/resolutionStrategy/g)).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("forces the pinned dependency versions across every module", () => {
|
|
115
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, PINS);
|
|
116
|
+
|
|
117
|
+
expect(result).toContain("allprojects {");
|
|
118
|
+
expect(result).toContain("force 'androidx.core:core:1.18.0'");
|
|
119
|
+
expect(result).toContain("force 'androidx.core:core-ktx:1.18.0'");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("emits no resolution block when nothing is forced", () => {
|
|
123
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, {
|
|
124
|
+
...PINS,
|
|
125
|
+
forcedDependencies: [],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result).not.toContain("resolutionStrategy");
|
|
129
|
+
expect(result).toContain("ext.compileSdkVersion = 37");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("leaves the template's own allprojects block intact", () => {
|
|
133
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, PINS);
|
|
134
|
+
|
|
135
|
+
expect(result).toContain("mavenCentral()");
|
|
136
|
+
expect(result.match(/allprojects \{/g)).toHaveLength(2);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("rejects a coordinate that could break out of the Groovy literal", () => {
|
|
140
|
+
expect(() =>
|
|
141
|
+
applyAndroidRootProjectPins(TEMPLATE, {
|
|
142
|
+
...PINS,
|
|
143
|
+
forcedDependencies: [
|
|
144
|
+
"androidx.core:core:1.18.0'; new File('/').delete()",
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
).toThrow(/unsafe character/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("fails loudly when the Kotlin Gradle Plugin classpath is absent", () => {
|
|
151
|
+
const withoutKotlin = TEMPLATE.replace(
|
|
152
|
+
" classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')\n",
|
|
153
|
+
"",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(() => applyAndroidRootProjectPins(withoutKotlin, PINS)).toThrow(
|
|
157
|
+
/kotlin-gradle-plugin classpath/,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("leaves the host's Kotlin untouched when no kotlinVersion is given", () => {
|
|
162
|
+
const result = applyAndroidRootProjectPins(TEMPLATE, {
|
|
163
|
+
compileSdkVersion: 37,
|
|
164
|
+
forcedDependencies: PINS.forcedDependencies,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result).not.toContain("ext.kotlinVersion");
|
|
168
|
+
expect(result).not.toContain("ext.kspVersion");
|
|
169
|
+
expect(result).toContain(
|
|
170
|
+
"classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')",
|
|
171
|
+
);
|
|
172
|
+
expect(result).toContain("ext.compileSdkVersion = 37");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("rejects kspVersion without kotlinVersion", () => {
|
|
176
|
+
expect(() =>
|
|
177
|
+
applyAndroidRootProjectPins(TEMPLATE, {
|
|
178
|
+
kspVersion: "2.3.10",
|
|
179
|
+
compileSdkVersion: 37,
|
|
180
|
+
forcedDependencies: [],
|
|
181
|
+
}),
|
|
182
|
+
).toThrow(/kspVersion/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("fails loudly when expo-root-project is absent", () => {
|
|
186
|
+
const withoutRootProject = TEMPLATE.replace(
|
|
187
|
+
'apply plugin: "expo-root-project"\n',
|
|
188
|
+
"",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(() => applyAndroidRootProjectPins(withoutRootProject, PINS)).toThrow(
|
|
192
|
+
/expo-root-project/,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The module is autolinked as a Gradle subproject, so it inherits the host's
|
|
2
|
+
// Kotlin compiler and `compileSdk`; Expo's defaults for both are lower than the
|
|
3
|
+
// AARs accept and a library cannot raise a setting the root project owns.
|
|
4
|
+
|
|
5
|
+
import { type ConfigPlugin } from "expo/config-plugins";
|
|
6
|
+
|
|
7
|
+
import { withAndroidRootProjectPins } from "./lib/root_project";
|
|
8
|
+
import {
|
|
9
|
+
withMlkitManifestMerge,
|
|
10
|
+
type MlkitManifestMergeOptions,
|
|
11
|
+
} from "./lib/mlkit_manifest";
|
|
12
|
+
|
|
13
|
+
// Lowest Kotlin that reads the AARs' 2.4.x metadata AND has Expo
|
|
14
|
+
// compiler-plugin (pika) artifacts; higher pins break expo-modules-core.
|
|
15
|
+
// KSP is left to Expo's own kotlin→ksp lookup table.
|
|
16
|
+
const DEFAULT_KOTLIN_VERSION = "2.3.20";
|
|
17
|
+
const COMPILE_SDK_VERSION = 37;
|
|
18
|
+
const FORCED_DEPENDENCIES = [
|
|
19
|
+
"androidx.core:core:1.18.0",
|
|
20
|
+
"androidx.core:core-ktx:1.18.0",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface MlkitFaceOptions extends MlkitManifestMergeOptions {
|
|
24
|
+
/** Overrides the default Kotlin pin (must read the AARs' metadata). */
|
|
25
|
+
kotlinVersion?: string;
|
|
26
|
+
/** Opt-in: only when Expo's lookup table lacks the pinned Kotlin. */
|
|
27
|
+
kspVersion?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const withMlkitFace: ConfigPlugin<MlkitFaceOptions | void> = (
|
|
31
|
+
config,
|
|
32
|
+
options,
|
|
33
|
+
) => {
|
|
34
|
+
const { kotlinVersion, kspVersion, ...manifestOptions } = options ?? {};
|
|
35
|
+
return withMlkitManifestMerge(
|
|
36
|
+
withAndroidRootProjectPins(config, {
|
|
37
|
+
kotlinVersion: kotlinVersion ?? DEFAULT_KOTLIN_VERSION,
|
|
38
|
+
...(kspVersion !== undefined ? { kspVersion } : {}),
|
|
39
|
+
compileSdkVersion: COMPILE_SDK_VERSION,
|
|
40
|
+
forcedDependencies: FORCED_DEPENDENCIES,
|
|
41
|
+
}),
|
|
42
|
+
manifestOptions,
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default withMlkitFace;
|
|
47
|
+
export type { MlkitManifestMergeOptions };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The AAR declares this meta-data without `tools:replace`, so only the host `<application>` can claim it for every ML Kit consumer at once.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AndroidConfig,
|
|
5
|
+
type ConfigPlugin,
|
|
6
|
+
withAndroidManifest,
|
|
7
|
+
} from "expo/config-plugins";
|
|
8
|
+
|
|
9
|
+
type AndroidManifest = AndroidConfig.Manifest.AndroidManifest;
|
|
10
|
+
|
|
11
|
+
// Expo's meta-data attribute type covers only the `android:*` names.
|
|
12
|
+
type ReplaceableMetaData = {
|
|
13
|
+
$: AndroidConfig.Manifest.ManifestMetaData["$"] & {
|
|
14
|
+
"tools:replace"?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const META_DATA_NAME = "com.google.mlkit.vision.DEPENDENCIES";
|
|
19
|
+
const TOOLS_NAMESPACE = "http://schemas.android.com/tools";
|
|
20
|
+
|
|
21
|
+
/** The hint this package's AAR declares; always part of the merged value. */
|
|
22
|
+
const PROVIDER_MODEL_DEPENDENCY = "face";
|
|
23
|
+
|
|
24
|
+
// ML Kit's own hint vocabulary — lowercase words and underscores.
|
|
25
|
+
const MODEL_DEPENDENCY = /^[a-z0-9_]+$/;
|
|
26
|
+
|
|
27
|
+
export interface MlkitManifestMergeOptions {
|
|
28
|
+
/** Hints your app's other ML Kit consumers declare, so their models prefetch at install time rather than on first use. */
|
|
29
|
+
readonly additionalModelDependencies?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Provider hint first, then the caller's, deduplicated and order-stable. */
|
|
33
|
+
export function mergedModelDependencies(additional: readonly string[]): string {
|
|
34
|
+
for (const dependency of additional) {
|
|
35
|
+
if (!MODEL_DEPENDENCY.test(dependency)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`[smileid-android] '${dependency}' is not a valid ML Kit model dependency. ` +
|
|
38
|
+
"Expected a lowercase hint such as 'barcode_ui', as printed in the manifest-merger error.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...new Set([PROVIDER_MODEL_DEPENDENCY, ...additional])].join(",");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Declares the merged `com.google.mlkit.vision.DEPENDENCIES` on the host `<application>` with `tools:replace`. */
|
|
46
|
+
export const withMlkitManifestMerge: ConfigPlugin<
|
|
47
|
+
MlkitManifestMergeOptions | void
|
|
48
|
+
> = (config, options) =>
|
|
49
|
+
withAndroidManifest(config, (config) => {
|
|
50
|
+
config.modResults = applyMlkitManifestMerge(
|
|
51
|
+
config.modResults,
|
|
52
|
+
mergedModelDependencies(options?.additionalModelDependencies ?? []),
|
|
53
|
+
);
|
|
54
|
+
return config;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Pure form of the edit. Idempotent; a re-run replaces a stale value. */
|
|
58
|
+
export function applyMlkitManifestMerge(
|
|
59
|
+
manifest: AndroidManifest,
|
|
60
|
+
value: string,
|
|
61
|
+
): AndroidManifest {
|
|
62
|
+
const application =
|
|
63
|
+
AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
|
|
64
|
+
application["meta-data"] ??= [];
|
|
65
|
+
const entries = application["meta-data"] as ReplaceableMetaData[];
|
|
66
|
+
|
|
67
|
+
const existing = entries.find(
|
|
68
|
+
(entry) => entry?.$?.["android:name"] === META_DATA_NAME,
|
|
69
|
+
);
|
|
70
|
+
if (existing) {
|
|
71
|
+
existing.$["android:value"] = value;
|
|
72
|
+
existing.$["tools:replace"] = "android:value";
|
|
73
|
+
} else {
|
|
74
|
+
entries.push({
|
|
75
|
+
$: {
|
|
76
|
+
"android:name": META_DATA_NAME,
|
|
77
|
+
"android:value": value,
|
|
78
|
+
"tools:replace": "android:value",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Without the namespace on <manifest>, `tools:replace` merges as an unknown attribute.
|
|
84
|
+
manifest.manifest.$ ??= {} as AndroidManifest["manifest"]["$"];
|
|
85
|
+
manifest.manifest.$["xmlns:tools"] ??= TOOLS_NAMESPACE;
|
|
86
|
+
|
|
87
|
+
return manifest;
|
|
88
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// `expo-root-project` resolves these through `extra.setIfNotExist`, so they only
|
|
2
|
+
// take effect when declared before that plugin applies.
|
|
3
|
+
|
|
4
|
+
import { type ConfigPlugin, withProjectBuildGradle } from "expo/config-plugins";
|
|
5
|
+
|
|
6
|
+
export interface AndroidRootProjectPins {
|
|
7
|
+
/**
|
|
8
|
+
* Opt-in: only for hosts whose Kotlin predates the AARs' compiler. Current
|
|
9
|
+
* Expo defaults read the metadata, and forcing a Kotlin that Expo ships no
|
|
10
|
+
* compiler-plugin artifacts for breaks expo-modules-core.
|
|
11
|
+
*/
|
|
12
|
+
kotlinVersion?: string;
|
|
13
|
+
/** Only alongside `kotlinVersion` — Expo's own lookup table may carry no entry for it. */
|
|
14
|
+
kspVersion?: string;
|
|
15
|
+
/** The AARs declare it as a consumer minimum. Independent of `minSdk`, so device support is unchanged. */
|
|
16
|
+
compileSdkVersion: number;
|
|
17
|
+
/** `group:name:version` coordinates forced everywhere, for deps whose newest release the host toolchain cannot consume. */
|
|
18
|
+
forcedDependencies: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BEGIN_MARKER = "// === BEGIN smileid root-project pins ===";
|
|
22
|
+
const END_MARKER = "// === END smileid root-project pins ===";
|
|
23
|
+
|
|
24
|
+
const APPLY_ROOT_PROJECT = /^apply\s+plugin:\s*["']expo-root-project["']/m;
|
|
25
|
+
|
|
26
|
+
// Bumping `ext.kotlinVersion` moves the dependency catalog but not the compiler, so
|
|
27
|
+
// the classpath line needs pinning too. Version and parentheses are optional so an
|
|
28
|
+
// already-pinned line and Groovy's space-call form still match.
|
|
29
|
+
const KGP_CLASSPATH =
|
|
30
|
+
/classpath\s*(?:\(\s*)?['"]org\.jetbrains\.kotlin:kotlin-gradle-plugin(?::[^'"]*)?['"](?:\s*\))?/;
|
|
31
|
+
|
|
32
|
+
/** Declares the pins ahead of `apply plugin: "expo-root-project"`. */
|
|
33
|
+
export const withAndroidRootProjectPins: ConfigPlugin<
|
|
34
|
+
AndroidRootProjectPins
|
|
35
|
+
> = (config, pins) =>
|
|
36
|
+
withProjectBuildGradle(config, (config) => {
|
|
37
|
+
if (config.modResults.language !== "groovy") {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"[smileid-android] Expected android/build.gradle to be Groovy DSL; refusing to edit " +
|
|
40
|
+
`a ${config.modResults.language} build script.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
config.modResults.contents = applyAndroidRootProjectPins(
|
|
44
|
+
config.modResults.contents,
|
|
45
|
+
pins,
|
|
46
|
+
);
|
|
47
|
+
return config;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Pure form of the edit. Idempotent; a re-run replaces stale values rather than appending. */
|
|
51
|
+
export function applyAndroidRootProjectPins(
|
|
52
|
+
contents: string,
|
|
53
|
+
pins: AndroidRootProjectPins,
|
|
54
|
+
): string {
|
|
55
|
+
if (pins.kspVersion !== undefined && pins.kotlinVersion === undefined) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"[smileid-android] `kspVersion` only makes sense alongside `kotlinVersion`.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
validateGroovySafe(pins);
|
|
61
|
+
const pinned =
|
|
62
|
+
pins.kotlinVersion === undefined
|
|
63
|
+
? contents
|
|
64
|
+
: pinKotlinGradlePlugin(contents, pins.kotlinVersion);
|
|
65
|
+
return upsertPinBlock(pinned, renderPinBlock(pins));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Every value lands in a single-quoted Groovy literal; reject anything that could close one.
|
|
69
|
+
function validateGroovySafe(pins: AndroidRootProjectPins): void {
|
|
70
|
+
const values = [
|
|
71
|
+
pins.kotlinVersion,
|
|
72
|
+
pins.kspVersion,
|
|
73
|
+
...pins.forcedDependencies,
|
|
74
|
+
].filter((value): value is string => value !== undefined);
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
if (/['\\\n\r\t$]/.test(value)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`[smileid-android] Refusing to edit android/build.gradle: '${value}' contains an unsafe character.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function pinKotlinGradlePlugin(
|
|
85
|
+
contents: string,
|
|
86
|
+
kotlinVersion: string,
|
|
87
|
+
): string {
|
|
88
|
+
const pinned = `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}')`;
|
|
89
|
+
if (!KGP_CLASSPATH.test(contents)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"[smileid-android] Could not locate the kotlin-gradle-plugin classpath line in " +
|
|
92
|
+
"android/build.gradle. Refusing to silently no-op — the Smile ID analyzer modules do not " +
|
|
93
|
+
`compile unless the Kotlin compiler is pinned to ${kotlinVersion}.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return contents.replace(KGP_CLASSPATH, pinned);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Adding a further root-project pin means adding an entry here.
|
|
100
|
+
function renderPinBlock(pins: AndroidRootProjectPins): string {
|
|
101
|
+
const extProperties: Array<[string, string]> = [];
|
|
102
|
+
if (pins.kotlinVersion !== undefined) {
|
|
103
|
+
extProperties.push(["kotlinVersion", `'${pins.kotlinVersion}'`]);
|
|
104
|
+
}
|
|
105
|
+
if (pins.kspVersion !== undefined) {
|
|
106
|
+
extProperties.push(["kspVersion", `'${pins.kspVersion}'`]);
|
|
107
|
+
}
|
|
108
|
+
extProperties.push(["compileSdkVersion", String(pins.compileSdkVersion)]);
|
|
109
|
+
const lines = extProperties.map(([name, value]) => `ext.${name} = ${value}`);
|
|
110
|
+
return [
|
|
111
|
+
BEGIN_MARKER,
|
|
112
|
+
...lines,
|
|
113
|
+
...renderForcedDependencies(pins.forcedDependencies),
|
|
114
|
+
END_MARKER,
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Its own `allprojects` block, which Gradle merges — editing the template's would
|
|
119
|
+
// break the moment its shape changes.
|
|
120
|
+
function renderForcedDependencies(coordinates: readonly string[]): string[] {
|
|
121
|
+
if (coordinates.length === 0) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
"",
|
|
126
|
+
"// Forced to the newest release that resolves under the AGP Expo generates.",
|
|
127
|
+
"allprojects {",
|
|
128
|
+
" configurations.all {",
|
|
129
|
+
" resolutionStrategy {",
|
|
130
|
+
...coordinates.map((coordinate) => ` force '${coordinate}'`),
|
|
131
|
+
" }",
|
|
132
|
+
" }",
|
|
133
|
+
"}",
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function upsertPinBlock(contents: string, block: string): string {
|
|
138
|
+
const existing = new RegExp(
|
|
139
|
+
`${escapeRegExp(BEGIN_MARKER)}[\\s\\S]*?${escapeRegExp(END_MARKER)}`,
|
|
140
|
+
"m",
|
|
141
|
+
);
|
|
142
|
+
if (existing.test(contents)) {
|
|
143
|
+
return contents.replace(existing, block);
|
|
144
|
+
}
|
|
145
|
+
if (!APPLY_ROOT_PROJECT.test(contents)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'[smileid-android] Could not locate `apply plugin: "expo-root-project"` in ' +
|
|
148
|
+
"android/build.gradle. Refusing to silently no-op — these pins only take effect when " +
|
|
149
|
+
"declared before that plugin applies.",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return contents.replace(
|
|
153
|
+
APPLY_ROOT_PROJECT,
|
|
154
|
+
(match) => `${block}\n\n${match}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function escapeRegExp(value: string): string {
|
|
159
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
160
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./build",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"module": "commonjs",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"ignoreDeprecations": "6.0",
|
|
9
|
+
"target": "ES2021",
|
|
10
|
+
"lib": ["ES2022"],
|
|
11
|
+
"types": ["node"],
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": false,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"composite": false
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"src/**/*.test.ts",
|
|
20
|
+
"src/**/__tests__/**",
|
|
21
|
+
"build",
|
|
22
|
+
"node_modules"
|
|
23
|
+
]
|
|
24
|
+
}
|