expo-tiddlywiki-filesystem-android-external-storage 2.11.0 → 2.12.1

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
@@ -118,6 +118,18 @@ Deletes a directory recursively.
118
118
  ### `isExternalStorageManager(): Promise<boolean>`
119
119
  Checks if the `MANAGE_EXTERNAL_STORAGE` permission is granted (Android 11+).
120
120
 
121
+ ## JGit Version Note
122
+
123
+ This module bundles **JGit 6.2.x** (not the latest 6.10.x) for Android compatibility.
124
+
125
+ Newer JGit versions (6.3+) call Java 9+ APIs (`InputStream.readNBytes`, `InputStream.transferTo`) that require `coreLibraryDesugaringEnabled` — Android's `desugar_jdk_libs` provides these at build time. The config plugin included in this package automatically enables desugaring in the generated `android/app/build.gradle`.
126
+
127
+ If you encounter `NoSuchMethodError` for these methods at runtime, verify that the config plugin ran during `expo prebuild` (check that `android/app/build.gradle` contains `coreLibraryDesugaringEnabled true` and `desugar_jdk_libs`).
128
+
129
+ JGit 6.2.x is the latest release that avoids these Java 9+ API calls while still providing all the Git primitives needed by this module (clone, fetch, push, status, diff, log, etc.). One API trade-off: `CloneCommand.setDepth()` (shallow clone) was introduced in JGit 6.3, so this module does not support `--depth` on clone.
130
+
131
+ If you need a newer JGit, ensure your app's `minSdkVersion` and desugaring configuration are verified on all target devices.
132
+
121
133
  ## License
122
134
 
123
135
  MIT
@@ -14,12 +14,14 @@ android {
14
14
  }
15
15
  // JGit needs Java 11+
16
16
  compileOptions {
17
+ coreLibraryDesugaringEnabled true
17
18
  sourceCompatibility JavaVersion.VERSION_11
18
19
  targetCompatibility JavaVersion.VERSION_11
19
20
  }
20
21
  }
21
22
 
22
23
  dependencies {
23
- implementation 'org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r'
24
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
25
+ implementation 'org.eclipse.jgit:org.eclipse.jgit:6.2.0.202206071550-r'
24
26
  implementation "com.squareup.okhttp3:okhttp:4.12.0"
25
27
  }
@@ -133,10 +133,6 @@ internal object GitNetwork {
133
133
  cloneCommand.setBranchesToClone(listOf("refs/heads/$branch"))
134
134
  }
135
135
 
136
- if (depth > 0) {
137
- cloneCommand.setDepth(depth)
138
- }
139
-
140
136
  if (noTags) {
141
137
  cloneCommand.setNoTags()
142
138
  }
package/app.plugin.js CHANGED
@@ -1,7 +1,34 @@
1
- const { withAndroidManifest } = require('@expo/config-plugins');
1
+ const { withAndroidManifest, withAppBuildGradle } = require('@expo/config-plugins');
2
+
3
+ const DESUGAR_DEPENDENCY = "coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'";
4
+
5
+ function applyCoreLibraryDesugaring(contents) {
6
+ if (!contents.includes('coreLibraryDesugaringEnabled')) {
7
+ if (contents.includes('compileOptions {')) {
8
+ contents = contents.replace(
9
+ /(compileOptions\s*\{)/,
10
+ '$1\n coreLibraryDesugaringEnabled true',
11
+ );
12
+ } else {
13
+ contents = contents.replace(
14
+ /^(android\s*\{)/m,
15
+ '$1\n compileOptions {\n coreLibraryDesugaringEnabled true\n }',
16
+ );
17
+ }
18
+ }
19
+
20
+ if (!contents.includes('desugar_jdk_libs')) {
21
+ contents = contents.replace(
22
+ /^(dependencies\s*\{)/m,
23
+ `$1\n ${DESUGAR_DEPENDENCY}`,
24
+ );
25
+ }
26
+
27
+ return contents;
28
+ }
2
29
 
3
30
  const withExternalStoragePermission = (config) => {
4
- return withAndroidManifest(config, async (config) => {
31
+ config = withAndroidManifest(config, async (config) => {
5
32
  const androidManifest = config.modResults;
6
33
 
7
34
  if (!androidManifest.manifest['uses-permission']) {
@@ -20,6 +47,13 @@ const withExternalStoragePermission = (config) => {
20
47
 
21
48
  return config;
22
49
  });
50
+
51
+ config = withAppBuildGradle(config, (config) => {
52
+ config.modResults.contents = applyCoreLibraryDesugaring(config.modResults.contents);
53
+ return config;
54
+ });
55
+
56
+ return config;
23
57
  };
24
58
 
25
59
  module.exports = withExternalStoragePermission;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-tiddlywiki-filesystem-android-external-storage",
3
- "version": "2.11.0",
3
+ "version": "2.12.1",
4
4
  "description": "Expo native module for TidGi-Mobile: filesystem I/O + TiddlyWiki .tid/.meta/.json batch parsing + git status in Kotlin (Android) and Swift (iOS)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -9,8 +9,8 @@
9
9
  "build": "tsc -p tsconfig.json",
10
10
  "clean": "expo-module clean",
11
11
  "lint": "expo-module lint",
12
- "test": "expo-module test",
13
- "prepare": "tsc -p tsconfig.json"
12
+ "test": "jest --config jest.config.js",
13
+ "prepack": "tsc -p tsconfig.json"
14
14
  },
15
15
  "keywords": [
16
16
  "react-native",
@@ -27,13 +27,16 @@
27
27
  "react-native": "*"
28
28
  },
29
29
  "devDependencies": {
30
+ "@types/jest": "29.5.14",
30
31
  "@types/react": "^18.2.0",
31
32
  "@types/react-native": "^0.73.0",
32
33
  "expo": "^50.0.0",
33
34
  "expo-module-scripts": "^3.0.0",
34
35
  "expo-modules-core": "^1.11.0",
36
+ "jest": "29.7.0",
35
37
  "react": "^18.2.0",
36
38
  "react-native": "^0.73.0",
39
+ "ts-jest": "~29.0.5",
37
40
  "typescript": "^5.0.0"
38
41
  },
39
42
  "files": [
@@ -42,6 +45,7 @@
42
45
  "ios",
43
46
  "expo-module.config.json",
44
47
  "src",
48
+ "tsconfig.json",
45
49
  "plugin.js",
46
50
  "app.plugin.js"
47
51
  ],
package/plugin.js CHANGED
@@ -1,7 +1,34 @@
1
- const { withAndroidManifest } = require('@expo/config-plugins');
1
+ const { withAndroidManifest, withAppBuildGradle } = require('@expo/config-plugins');
2
+
3
+ const DESUGAR_DEPENDENCY = "coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'";
4
+
5
+ function applyCoreLibraryDesugaring(contents) {
6
+ if (!contents.includes('coreLibraryDesugaringEnabled')) {
7
+ if (contents.includes('compileOptions {')) {
8
+ contents = contents.replace(
9
+ /(compileOptions\s*\{)/,
10
+ '$1\n coreLibraryDesugaringEnabled true',
11
+ );
12
+ } else {
13
+ contents = contents.replace(
14
+ /^(android\s*\{)/m,
15
+ '$1\n compileOptions {\n coreLibraryDesugaringEnabled true\n }',
16
+ );
17
+ }
18
+ }
19
+
20
+ if (!contents.includes('desugar_jdk_libs')) {
21
+ contents = contents.replace(
22
+ /^(dependencies\s*\{)/m,
23
+ `$1\n ${DESUGAR_DEPENDENCY}`,
24
+ );
25
+ }
26
+
27
+ return contents;
28
+ }
2
29
 
3
30
  const withExternalStoragePermission = (config) => {
4
- return withAndroidManifest(config, async (config) => {
31
+ config = withAndroidManifest(config, async (config) => {
5
32
  const androidManifest = config.modResults;
6
33
 
7
34
  if (!androidManifest.manifest['uses-permission']) {
@@ -20,6 +47,13 @@ const withExternalStoragePermission = (config) => {
20
47
 
21
48
  return config;
22
49
  });
50
+
51
+ config = withAppBuildGradle(config, (config) => {
52
+ config.modResults.contents = applyCoreLibraryDesugaring(config.modResults.contents);
53
+ return config;
54
+ });
55
+
56
+ return config;
23
57
  };
24
58
 
25
59
  module.exports = withExternalStoragePermission;
@@ -0,0 +1,57 @@
1
+ type ExternalStorageModule = typeof import('../index');
2
+
3
+ function loadModuleWithPlatform(
4
+ platformOS: string,
5
+ nativeModule: Record<string, unknown> = {},
6
+ ): { module: ExternalStorageModule; requireNativeModule: jest.Mock } {
7
+ jest.resetModules();
8
+ const requireNativeModule = jest.fn(() => nativeModule);
9
+
10
+ jest.doMock('react-native', () => ({
11
+ Platform: {
12
+ OS: platformOS,
13
+ },
14
+ }));
15
+ jest.doMock('expo-modules-core', () => ({
16
+ requireNativeModule,
17
+ }));
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const module = require('../index') as ExternalStorageModule;
21
+ return { module, requireNativeModule };
22
+ }
23
+
24
+ describe('toPlainPath', () => {
25
+ it('strips only the file URI scheme from filesystem paths', () => {
26
+ const { module } = loadModuleWithPlatform('android');
27
+
28
+ expect(module.toPlainPath('file:///storage/emulated/0/TidGi/wiki')).toBe('/storage/emulated/0/TidGi/wiki');
29
+ expect(module.toPlainPath('/storage/emulated/0/TidGi/wiki')).toBe('/storage/emulated/0/TidGi/wiki');
30
+ expect(module.toPlainPath('content://com.android.externalstorage.documents/tree/primary%3ATidGi')).toBe('content://com.android.externalstorage.documents/tree/primary%3ATidGi');
31
+ });
32
+ });
33
+
34
+ describe('ExternalStorage proxy', () => {
35
+ it('loads the native module lazily when a method is accessed', () => {
36
+ const nativeExists = jest.fn();
37
+ const nativeMkdir = jest.fn();
38
+ const { module, requireNativeModule } = loadModuleWithPlatform('android', {
39
+ exists: nativeExists,
40
+ mkdir: nativeMkdir,
41
+ });
42
+
43
+ expect(requireNativeModule).not.toHaveBeenCalled();
44
+ expect(module.ExternalStorage.exists).toBe(nativeExists);
45
+ expect(module.ExternalStorage.mkdir).toBe(nativeMkdir);
46
+ expect(requireNativeModule).toHaveBeenCalledTimes(1);
47
+ expect(requireNativeModule).toHaveBeenCalledWith('ExternalStorage');
48
+ });
49
+
50
+ it('throws only when the native proxy is accessed on unsupported platforms', () => {
51
+ const { module, requireNativeModule } = loadModuleWithPlatform('web');
52
+
53
+ expect(module.toPlainPath('file:///tmp/wiki')).toBe('/tmp/wiki');
54
+ expect(requireNativeModule).not.toHaveBeenCalled();
55
+ expect(() => module.ExternalStorage.exists).toThrow('ExternalStorage native module is only available on Android and iOS');
56
+ });
57
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "expo-module-scripts/tsconfig.base",
3
+ "compilerOptions": {
4
+ "outDir": "./build",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["src/**/__tests__/**", "src/**/*.test.ts"]
9
+ }