@forthtilliath/expo-config-plugins 0.1.0 → 0.3.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/README.md CHANGED
@@ -19,7 +19,8 @@ npm install --save-dev @forthtilliath/expo-config-plugins
19
19
  { "propertyPrefix": "MYAPP" }
20
20
  ],
21
21
  "@forthtilliath/expo-config-plugins/withAndroidAbis",
22
- "@forthtilliath/expo-config-plugins/withShortCxxPath"
22
+ "@forthtilliath/expo-config-plugins/withShortCxxPath",
23
+ "@forthtilliath/expo-config-plugins/withKotlinJvmTarget"
23
24
  ]
24
25
  }
25
26
  }
@@ -59,9 +60,33 @@ On Windows, release builds can overflow the 260-character path limit inside `and
59
60
  | ------------ | -------- | --------------------------------- |
60
61
  | `stagingDir` | `string` | `C:/rncxx` on Windows, else unset |
61
62
 
63
+ ## `withKotlinJvmTarget` — one JVM target for Kotlin
64
+
65
+ Some native modules (e.g. `expo-dynamic-app-icon`) compile their Kotlin code for another JVM target than the project's Java code, and the build fails with "Inconsistent JVM-target compatibility". This plugin appends a block to `android/build.gradle` forcing every Kotlin compile task, in every subproject, to the same target.
66
+
67
+ | Option | Type | Default |
68
+ | ----------- | -------- | ------- |
69
+ | `jvmTarget` | `string` | `"17"` |
70
+
71
+ ## `withReleaseRemovedPermissions` — a release APK that can't go online
72
+
73
+ For an app that works fully offline (personal, health or intimate data), removing the `INTERNET` permission from the **release** APK makes Android itself guarantee that nothing can leave the phone — whatever a dependency (an SDK, a library's telemetry) tries. Debug builds keep the permission: Metro needs the network.
74
+
75
+ ```json
76
+ "@forthtilliath/expo-config-plugins/withReleaseRemovedPermissions"
77
+ ```
78
+
79
+ The plugin writes `android/app/src/release/AndroidManifest.xml`, merged by Gradle over the main manifest and every library's, with `tools:node="remove"` on each permission: it's dropped whoever declared it. It refuses to overwrite a release manifest it didn't generate, and fails the prebuild on anything but permission names (they end up in XML). Check the result with `aapt dump permissions app-release.apk`.
80
+
81
+ | Option | Type | Default | Notes |
82
+ | ------------- | ---------- | -------------- | ----------------------------------------------------------------------------------------- |
83
+ | `permissions` | `string[]` | `["INTERNET"]` | Short (`ACCESS_NETWORK_STATE`) or full (`com.google.android.gms.permission.AD_ID`) names. |
84
+
85
+ Remove it the day a feature really needs the network (update check, Drive backup…): without `INTERNET`, every request fails.
86
+
62
87
  ## Testing your own setup
63
88
 
64
- The transforms behind the plugins are exported as pure functions from `@forthtilliath/expo-config-plugins/transforms` (`applyReleaseSigning`, `applyAndroidAbis`, `applyShortCxxPath`).
89
+ The transforms behind the plugins are exported as pure functions from `@forthtilliath/expo-config-plugins/transforms` (`applyReleaseSigning`, `applyAndroidAbis`, `applyShortCxxPath`, `applyKotlinJvmTarget`, `buildReleaseRemovedPermissionsManifest`).
65
90
 
66
91
  ## Scripts
67
92
 
@@ -38,6 +38,20 @@ export declare function applyAndroidAbis(properties: GradlePropertiesItem[], abi
38
38
  * @throws When the `namespace` line to anchor on isn't found.
39
39
  */
40
40
  export declare function applyShortCxxPath(contents: string, stagingDir: string): string;
41
+ /**
42
+ * Appends to the root `android/build.gradle` a block forcing every Kotlin
43
+ * compile task, in every subproject, to target `jvmTarget` — the Java code's
44
+ * target: a native module compiling its Kotlin for another one (e.g.
45
+ * expo-dynamic-app-icon) fails the build with "Inconsistent JVM-target
46
+ * compatibility". Already-patched contents are returned as is.
47
+ *
48
+ * @throws When `jvmTarget` isn't a JVM version such as `17` or `1.8`.
49
+ */
50
+ export declare function applyKotlinJvmTarget(contents: string, jvmTarget?: string): string;
51
+ export interface KotlinJvmTargetOptions {
52
+ /** JVM version Kotlin compiles for, e.g. `"21"`. @defaultValue `"17"` */
53
+ jvmTarget?: string;
54
+ }
41
55
  export interface ReleaseSigningOptions {
42
56
  /**
43
57
  * Prefix of the Gradle properties holding the upload key, e.g. `MYAPP` for
@@ -57,4 +71,23 @@ export interface ShortCxxPathOptions {
57
71
  */
58
72
  stagingDir?: string;
59
73
  }
74
+ export interface ReleaseRemovedPermissionsOptions {
75
+ /**
76
+ * Android permissions to strip from the release APK only, short (`INTERNET`)
77
+ * or full (`android.permission.INTERNET`) names.
78
+ *
79
+ * @defaultValue `["INTERNET"]`
80
+ */
81
+ permissions?: string[];
82
+ }
83
+ /**
84
+ * The `android/app/src/release/AndroidManifest.xml` removing `permissions`
85
+ * from the release build: merged over the main manifest and every library's,
86
+ * `tools:node="remove"` drops the permission whoever declared it. Debug
87
+ * builds keep it (Metro needs `INTERNET`).
88
+ *
89
+ * @throws When the list is empty or holds anything but permission names —
90
+ * the names end up in XML, nothing else is let through.
91
+ */
92
+ export declare function buildReleaseRemovedPermissionsManifest(permissions?: readonly string[]): string;
60
93
  //# sourceMappingURL=transforms.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../src/transforms.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,MAAM,CAmBR;AAED,qFAAqF;AACrF,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,4CAA4C;AAC5C,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAiC,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,oBAAoB,EAAE,EAClC,IAAI,GAAE,SAAS,MAAM,EAAa,GACjC,oBAAoB,EAAE,CAcxB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAiBR;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../src/transforms.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,MAAM,CAmBR;AAED,qFAAqF;AACrF,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,4CAA4C;AAC5C,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAiC,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,oBAAoB,EAAE,EAClC,IAAI,GAAE,SAAS,MAAM,EAAa,GACjC,oBAAoB,EAAE,CAcxB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAiBR;AAID;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAO,GACf,MAAM,CAkBR;AAED,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gCAAgC;IAC/C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAID;;;;;;;;GAQG;AACH,wBAAgB,sCAAsC,CACpD,WAAW,GAAE,SAAS,MAAM,EAAiB,GAC5C,MAAM,CA0BR"}
@@ -6,6 +6,8 @@ exports.ARM_ABIS = void 0;
6
6
  exports.applyReleaseSigning = applyReleaseSigning;
7
7
  exports.applyAndroidAbis = applyAndroidAbis;
8
8
  exports.applyShortCxxPath = applyShortCxxPath;
9
+ exports.applyKotlinJvmTarget = applyKotlinJvmTarget;
10
+ exports.buildReleaseRemovedPermissionsManifest = buildReleaseRemovedPermissionsManifest;
9
11
  /** The `signingConfig signingConfigs.debug` of the `release` build type (not the `debug` one's). */
10
12
  const RELEASE_USES_DEBUG_KEY = /(buildTypes\s*\{[\s\S]*?release\s*\{[^}]*?)signingConfig signingConfigs\.debug/;
11
13
  function releaseSigningConfig(prefix) {
@@ -87,3 +89,63 @@ function applyShortCxxPath(contents, stagingDir) {
87
89
 
88
90
  namespace`);
89
91
  }
92
+ const KOTLIN_JVM_TARGET_MARKER = "withKotlinJvmTarget";
93
+ /**
94
+ * Appends to the root `android/build.gradle` a block forcing every Kotlin
95
+ * compile task, in every subproject, to target `jvmTarget` — the Java code's
96
+ * target: a native module compiling its Kotlin for another one (e.g.
97
+ * expo-dynamic-app-icon) fails the build with "Inconsistent JVM-target
98
+ * compatibility". Already-patched contents are returned as is.
99
+ *
100
+ * @throws When `jvmTarget` isn't a JVM version such as `17` or `1.8`.
101
+ */
102
+ function applyKotlinJvmTarget(contents, jvmTarget = "17") {
103
+ if (!/^(1\.8|\d{1,2})$/.test(jvmTarget)) {
104
+ throw new Error(`withKotlinJvmTarget: invalid jvmTarget option "${jvmTarget}" (e.g. 17)`);
105
+ }
106
+ if (contents.includes(KOTLIN_JVM_TARGET_MARKER))
107
+ return contents;
108
+ return `${contents.trimEnd()}
109
+
110
+ // ${KOTLIN_JVM_TARGET_MARKER}: Kotlin targets the same JVM as Java (avoids "Inconsistent JVM-target compatibility").
111
+ subprojects {
112
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
113
+ compilerOptions {
114
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_${jvmTarget.replace(".", "_")})
115
+ }
116
+ }
117
+ }
118
+ `;
119
+ }
120
+ const PERMISSION_NAME = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)*$/;
121
+ /**
122
+ * The `android/app/src/release/AndroidManifest.xml` removing `permissions`
123
+ * from the release build: merged over the main manifest and every library's,
124
+ * `tools:node="remove"` drops the permission whoever declared it. Debug
125
+ * builds keep it (Metro needs `INTERNET`).
126
+ *
127
+ * @throws When the list is empty or holds anything but permission names —
128
+ * the names end up in XML, nothing else is let through.
129
+ */
130
+ function buildReleaseRemovedPermissionsManifest(permissions = ["INTERNET"]) {
131
+ if (permissions.length === 0) {
132
+ throw new Error("withReleaseRemovedPermissions: empty permissions list");
133
+ }
134
+ const names = permissions.map((permission) => {
135
+ if (!PERMISSION_NAME.test(permission)) {
136
+ throw new Error(`withReleaseRemovedPermissions: invalid permission name "${permission}"`);
137
+ }
138
+ return permission.includes(".")
139
+ ? permission
140
+ : `android.permission.${permission}`;
141
+ });
142
+ const lines = [...new Set(names)].map((name) => ` <uses-permission android:name="${name}" tools:node="remove"/>`);
143
+ return `<?xml version="1.0" encoding="utf-8"?>
144
+ <!-- Generated by @forthtilliath/expo-config-plugins/withReleaseRemovedPermissions -->
145
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
146
+ xmlns:tools="http://schemas.android.com/tools">
147
+
148
+ ${lines.join("\n")}
149
+ </manifest>
150
+ `;
151
+ }
@@ -0,0 +1,14 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ import { type KotlinJvmTargetOptions } from "./transforms.js";
3
+ /**
4
+ * Makes every Kotlin compile task target the same JVM as the Java code (17
5
+ * by default): some native modules (e.g. expo-dynamic-app-icon) compile
6
+ * their Kotlin for another target, and the build fails with "Inconsistent
7
+ * JVM-target compatibility".
8
+ *
9
+ * `"@forthtilliath/expo-config-plugins/withKotlinJvmTarget"`, or
10
+ * `["@forthtilliath/expo-config-plugins/withKotlinJvmTarget", { "jvmTarget": "21" }]`
11
+ */
12
+ declare const withKotlinJvmTarget: ConfigPlugin<KotlinJvmTargetOptions | undefined>;
13
+ export = withKotlinJvmTarget;
14
+ //# sourceMappingURL=withKotlinJvmTarget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withKotlinJvmTarget.d.ts","sourceRoot":"","sources":["../src/withKotlinJvmTarget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAA0B,MAAM,qBAAqB,CAAC;AAEhF,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;GAQG;AACH,QAAA,MAAM,mBAAmB,EAAE,YAAY,CAAC,sBAAsB,GAAG,SAAS,CAUtE,CAAC;AAEL,SAAS,mBAAmB,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ const config_plugins_1 = require("expo/config-plugins");
3
+ const transforms_js_1 = require("./transforms.js");
4
+ /**
5
+ * Makes every Kotlin compile task target the same JVM as the Java code (17
6
+ * by default): some native modules (e.g. expo-dynamic-app-icon) compile
7
+ * their Kotlin for another target, and the build fails with "Inconsistent
8
+ * JVM-target compatibility".
9
+ *
10
+ * `"@forthtilliath/expo-config-plugins/withKotlinJvmTarget"`, or
11
+ * `["@forthtilliath/expo-config-plugins/withKotlinJvmTarget", { "jvmTarget": "21" }]`
12
+ */
13
+ const withKotlinJvmTarget = (config, options) => (0, config_plugins_1.withProjectBuildGradle)(config, (mod) => {
14
+ mod.modResults.contents = (0, transforms_js_1.applyKotlinJvmTarget)(mod.modResults.contents, options?.jvmTarget);
15
+ return mod;
16
+ });
17
+ module.exports = withKotlinJvmTarget;
@@ -0,0 +1,17 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ import { type ReleaseRemovedPermissionsOptions } from "./transforms.js";
3
+ /**
4
+ * Strips permissions from the **release** APK only — `INTERNET` by default:
5
+ * for an app that works fully offline, Android itself then guarantees that
6
+ * no data can leave the phone, whatever a dependency tries. Debug builds
7
+ * keep them (Metro needs the network).
8
+ *
9
+ * Writes `android/app/src/release/AndroidManifest.xml`; refuses to overwrite
10
+ * one it didn't generate.
11
+ *
12
+ * `"@forthtilliath/expo-config-plugins/withReleaseRemovedPermissions"`, or
13
+ * `["@forthtilliath/expo-config-plugins/withReleaseRemovedPermissions", { "permissions": ["INTERNET", "ACCESS_NETWORK_STATE"] }]`
14
+ */
15
+ declare const withReleaseRemovedPermissions: ConfigPlugin<ReleaseRemovedPermissionsOptions | undefined>;
16
+ export = withReleaseRemovedPermissions;
17
+ //# sourceMappingURL=withReleaseRemovedPermissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withReleaseRemovedPermissions.d.ts","sourceRoot":"","sources":["../src/withReleaseRemovedPermissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAoB,MAAM,qBAAqB,CAAC;AAI1E,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,iBAAiB,CAAC;AAYzB;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,6BAA6B,EAAE,YAAY,CAC/C,gCAAgC,GAAG,SAAS,CAqB1C,CAAC;AAEL,SAAS,6BAA6B,CAAC"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ const config_plugins_1 = require("expo/config-plugins");
3
+ const promises_1 = require("node:fs/promises");
4
+ const node_path_1 = require("node:path");
5
+ const transforms_js_1 = require("./transforms.js");
6
+ const GENERATED_MARK = "withReleaseRemovedPermissions";
7
+ async function readIfExists(path) {
8
+ try {
9
+ return await (0, promises_1.readFile)(path, "utf8");
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ /**
16
+ * Strips permissions from the **release** APK only — `INTERNET` by default:
17
+ * for an app that works fully offline, Android itself then guarantees that
18
+ * no data can leave the phone, whatever a dependency tries. Debug builds
19
+ * keep them (Metro needs the network).
20
+ *
21
+ * Writes `android/app/src/release/AndroidManifest.xml`; refuses to overwrite
22
+ * one it didn't generate.
23
+ *
24
+ * `"@forthtilliath/expo-config-plugins/withReleaseRemovedPermissions"`, or
25
+ * `["@forthtilliath/expo-config-plugins/withReleaseRemovedPermissions", { "permissions": ["INTERNET", "ACCESS_NETWORK_STATE"] }]`
26
+ */
27
+ const withReleaseRemovedPermissions = (config, options) => (0, config_plugins_1.withDangerousMod)(config, [
28
+ "android",
29
+ async (mod) => {
30
+ // Built first: invalid options fail the prebuild before any write.
31
+ const manifest = (0, transforms_js_1.buildReleaseRemovedPermissionsManifest)(options?.permissions);
32
+ const dir = (0, node_path_1.join)(mod.modRequest.platformProjectRoot, "app/src/release");
33
+ const path = (0, node_path_1.join)(dir, "AndroidManifest.xml");
34
+ const existing = await readIfExists(path);
35
+ if (existing !== null && !existing.includes(GENERATED_MARK)) {
36
+ throw new Error(`withReleaseRemovedPermissions: ${path} already exists and wasn't generated by this plugin`);
37
+ }
38
+ await (0, promises_1.mkdir)(dir, { recursive: true });
39
+ await (0, promises_1.writeFile)(path, manifest);
40
+ return mod;
41
+ },
42
+ ]);
43
+ module.exports = withReleaseRemovedPermissions;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forthtilliath/expo-config-plugins",
3
- "version": "0.1.0",
4
- "description": "Expo config plugins for sideloaded Android apps: release signing with your own key, ARM-only native code, short CMake paths on Windows.",
3
+ "version": "0.3.0",
4
+ "description": "Expo config plugins for sideloaded Android apps: release signing with your own key, ARM-only native code, short CMake paths on Windows, one JVM target for Kotlin, permissions removed from release builds.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -20,6 +20,8 @@
20
20
  "./withReleaseSigning": "./dist/withReleaseSigning.js",
21
21
  "./withAndroidAbis": "./dist/withAndroidAbis.js",
22
22
  "./withShortCxxPath": "./dist/withShortCxxPath.js",
23
+ "./withKotlinJvmTarget": "./dist/withKotlinJvmTarget.js",
24
+ "./withReleaseRemovedPermissions": "./dist/withReleaseRemovedPermissions.js",
23
25
  "./transforms": "./dist/transforms.js",
24
26
  "./package.json": "./package.json"
25
27
  },
@@ -27,14 +29,14 @@
27
29
  "expo": ">=54.0.0"
28
30
  },
29
31
  "devDependencies": {
30
- "@types/node": "^26.2.0",
31
- "@vitest/coverage-v8": "5.0.2",
32
- "eslint": "10.7.0",
33
- "expo": "~57.0.9",
32
+ "@types/node": "^26.6.6",
33
+ "@vitest/coverage-v8": "5.0.5",
34
+ "eslint": "10.14.0",
35
+ "expo": "~57.0.31",
34
36
  "typescript": "^5.9.3",
35
- "vitest": "^5.0.2",
36
- "@forthtilliath/eslint-config": "0.9.0",
37
- "@forthtilliath/typescript-config": "0.1.1"
37
+ "vitest": "^5.0.5",
38
+ "@forthtilliath/typescript-config": "0.1.1",
39
+ "@forthtilliath/eslint-config": "0.10.1"
38
40
  },
39
41
  "scripts": {
40
42
  "lint": "eslint . --max-warnings 0",