perf(widget): cut expand/collapse tap latency (native patches + light headless path)
- Upgrade react-native-android-widget 0.20.1 -> 0.21.0 (expedited
WorkManager upstream, clickable-area ripple corner fix)
- patch-package on the lib:
1. WIDGET_CLICK bypasses WorkManager and runs the headless JS task
directly on the ReactHost (goAsync + WorkManager fallback), removing
two Room writes + worker dispatch from every tap and WorkManager
init from the cold path
2. WEBP_LOSSLESS (effort 0) encoding instead of PNG q100 on API 30+,
several times faster for widget-sized bitmaps
3. Copy identical light/dark collection item files instead of
compressing every row bitmap twice when the config is single-mode
- Extract dependency-light src/services/widgetState.ts so the headless
click handler no longer pulls DB/drizzle/date-fns at cold start
- Smoke tests: lock the light import graph + patch/postinstall wiring
- Bump v1.6.5 (versionCode 17) for the preview build
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9ccc9f4a9d
commit
fccd7faf95
10 changed files with 583 additions and 84 deletions
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -132,8 +132,19 @@ Couleurs sombres : fond `#1A1A1A`, surface `#2A2A2A`, bordure `#3A3A3A`, texte `
|
|||
- `widgetSync.ts` lit les tâches depuis SQLite et les cache dans AsyncStorage (`widget:state`)
|
||||
- Le thème est lu depuis AsyncStorage (`simpl-liste-settings` → `state.theme`), résolu si `system` via `Appearance.getColorScheme()`
|
||||
- `widgetTaskHandler.ts` gère le rendu headless (quand l'app n'est pas ouverte) en lisant la clé consolidée `widget:state`
|
||||
- `widgetState.ts` contient l'état widget + accesseurs AsyncStorage. Module volontairement léger (pas de DB/drizzle/date-fns) : c'est le chemin critique du cold start headless. Le smoke test verrouille son graphe d'import.
|
||||
- Les couleurs du widget suivent la même palette que l'app (voir `LIGHT_COLORS` / `DARK_COLORS` dans `TaskListWidget.tsx`)
|
||||
- Un debounce de 2s sur `TOGGLE_EXPAND` empêche les double-taps d'annuler l'expansion
|
||||
- Un debounce de 600ms sur `TOGGLE_EXPAND` empêche les double-taps d'annuler l'expansion
|
||||
|
||||
### Latence des interactions (expand/collapse, toggle)
|
||||
Chemin d'un tap : broadcast → `RNWidgetProvider` → task headless JS → `renderWidget` → bitmap → RemoteViews. Optimisations en place :
|
||||
- **Render optimiste** dans `widgetTaskHandler.ts` : `renderWidget` avant la persistance AsyncStorage et le write DB
|
||||
- **Patchs natifs** (`patches/react-native-android-widget+<version>.patch`, appliqués par `postinstall` → patch-package, donc aussi en build EAS) :
|
||||
1. Les `WIDGET_CLICK` court-circuitent WorkManager (`RNWidgetClickTask.java` : exécution directe sur le ReactHost via goAsync, fallback WorkManager). Les autres actions (UPDATE/ADDED/RESIZED) gardent WorkManager expedited (upstream 0.21.0)
|
||||
2. Compression **WEBP_LOSSLESS effort 0** au lieu de PNG q100 (API 30+) dans `RNWidgetImageProvider.writeImage`
|
||||
3. Dédup light/dark des items de collection quand la config est unique : copie de fichier au lieu d'une seconde compression
|
||||
- En cas d'upgrade de la lib : régénérer le patch (`npx patch-package react-native-android-widget`) après avoir réappliqué les 3 modifications ; le postinstall échoue bruyamment si le patch ne s'applique plus
|
||||
- **Mesurer** (build dev sur appareil) : `adb logcat -s ReactNativeJS | grep '\[widget\]'` — chaque étape du handler est instrumentée en `__DEV__`. Le plancher incompressible reste le cold start du runtime JS headless (~0,5-1,5s si le process est mort) ; à chaud, viser < 300ms
|
||||
|
||||
### Clés AsyncStorage utilisées par le widget
|
||||
| Clé | Contenu |
|
||||
|
|
|
|||
4
app.json
4
app.json
|
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Simpl-Liste",
|
||||
"slug": "simpl-liste",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "simplliste",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"backgroundColor": "#FFF8F0"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"versionCode": 16
|
||||
"versionCode": 17
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
|
|
|
|||
195
package-lock.json
generated
195
package-lock.json
generated
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simpl-liste",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
"react-dom": "19.1.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-android-widget": "^0.21.0",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
|
|
@ -58,6 +59,7 @@
|
|||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.9.2"
|
||||
|
|
@ -3851,6 +3853,13 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
|
|
@ -6674,6 +6683,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-yarn-workspace-root": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
|
||||
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"micromatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/flow-enums-runtime": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
|
||||
|
|
@ -6719,6 +6738,21 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
@ -7464,6 +7498,13 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -7728,6 +7769,26 @@
|
|||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
|
||||
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.8",
|
||||
"call-bound": "^1.0.4",
|
||||
"isarray": "^2.0.5",
|
||||
"jsonify": "^0.0.1",
|
||||
"object-keys": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
|
|
@ -7740,6 +7801,29 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonify": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
|
||||
"dev": true,
|
||||
"license": "Public Domain",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
@ -7749,6 +7833,16 @@
|
|||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/klaw-sync": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
|
|
@ -9229,6 +9323,75 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
|
||||
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@yarnpkg/lockfile": "^1.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"ci-info": "^3.7.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"find-yarn-workspace-root": "^2.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"json-stable-stringify": "^1.0.2",
|
||||
"klaw-sync": "^6.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"open": "^7.4.2",
|
||||
"semver": "^7.5.3",
|
||||
"slash": "^2.0.0",
|
||||
"tmp": "^0.2.4",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"patch-package": "index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">5"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-package/node_modules/slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
|
@ -9859,9 +10022,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-native-android-widget": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-android-widget/-/react-native-android-widget-0.20.1.tgz",
|
||||
"integrity": "sha512-m3akQCyoG6XUH8OLHOyL3/Xxx/XXTXf9ZyFYmWvSQrv1YUaZ7kVjmeLIYnaNz+qQ9VrTAS9OygCxZQptqCGzjQ==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-android-widget/-/react-native-android-widget-0.21.0.tgz",
|
||||
"integrity": "sha512-0HkXYvsXmzEUHts/nBG+lLajgypDOerht7OvDhoAsFzJ+jt6tY8m+wNK/Ng+1cubX+ZEfpBedpPPFe8uBZD8Rg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": ">=54.0.0",
|
||||
|
|
@ -11630,6 +11793,16 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
|
|
@ -11800,6 +11973,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"main": "index.js",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"test": "node tests/smoke.test.cjs",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
"web": "expo start --web",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
"react-dom": "19.1.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-android-widget": "^0.20.1",
|
||||
"react-native-android-widget": "^0.21.0",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"drizzle-kit": "^0.31.9",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.9.2"
|
||||
|
|
|
|||
232
patches/react-native-android-widget+0.21.0.patch
Normal file
232
patches/react-native-android-widget+0.21.0.patch
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
diff --git a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidget.java b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidget.java
|
||||
index a7079f9..470c469 100644
|
||||
--- a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidget.java
|
||||
+++ b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidget.java
|
||||
@@ -107,7 +107,9 @@ public class RNWidget {
|
||||
for (int i = 0; i < Math.min(collectionViews.size(), RNWidgetCollectionService.MAX_COLLECTION_WIDGETS); i++) {
|
||||
CollectionView collectionView = collectionViews.get(i);
|
||||
RNWidgetCollectionService.storeCollection(appContext, widgetId, i, collectionView.getRenderedViews(), "light");
|
||||
- RNWidgetCollectionService.storeCollection(appContext, widgetId, i, collectionView.getRenderedViews(), "dark");
|
||||
+ // PATCH (simpl-liste): both modes are identical here — copy the
|
||||
+ // encoded files instead of compressing every bitmap twice.
|
||||
+ RNWidgetCollectionService.copyCollection(appContext, widgetId, i, collectionView.getRenderedViews().size(), "light", "dark");
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetClickTask.java b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetClickTask.java
|
||||
new file mode 100644
|
||||
index 0000000..10de86e
|
||||
--- /dev/null
|
||||
+++ b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetClickTask.java
|
||||
@@ -0,0 +1,109 @@
|
||||
+package com.reactnativeandroidwidget;
|
||||
+
|
||||
+import android.content.BroadcastReceiver;
|
||||
+import android.content.Context;
|
||||
+import android.os.Handler;
|
||||
+import android.os.Looper;
|
||||
+
|
||||
+import androidx.work.Data;
|
||||
+
|
||||
+import com.facebook.react.ReactApplication;
|
||||
+import com.facebook.react.ReactHost;
|
||||
+import com.facebook.react.ReactInstanceEventListener;
|
||||
+import com.facebook.react.bridge.Arguments;
|
||||
+import com.facebook.react.bridge.ReactContext;
|
||||
+import com.facebook.react.bridge.UiThreadUtil;
|
||||
+import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
||||
+import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
||||
+import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
||||
+
|
||||
+import java.util.HashMap;
|
||||
+import java.util.Map;
|
||||
+import java.util.concurrent.atomic.AtomicBoolean;
|
||||
+
|
||||
+/**
|
||||
+ * PATCH (simpl-liste): runs widget click tasks directly on the ReactHost instead
|
||||
+ * of going through WorkManager. WorkManager adds scheduling latency to every tap
|
||||
+ * (two Room DB writes + worker dispatch, plus full WorkManager init on a cold
|
||||
+ * process). Clicks are user-facing and latency-critical, and fire-and-forget
|
||||
+ * semantics are acceptable for them: the widget state self-heals from
|
||||
+ * AsyncStorage on the next interaction if a click task is ever lost. Non-click
|
||||
+ * paths (WIDGET_UPDATE/ADDED/RESIZED/DELETED) keep using WorkManager through
|
||||
+ * RNWidgetJsCommunication.
|
||||
+ */
|
||||
+class RNWidgetClickTask {
|
||||
+ // Manifest-registered receivers get ~10s before the system gives up on the
|
||||
+ // broadcast; always finish before that even if the JS task hangs.
|
||||
+ private static final long BROADCAST_TIMEOUT_MS = 9000;
|
||||
+
|
||||
+ static void start(Context context, BroadcastReceiver.PendingResult pendingResult, Data data) {
|
||||
+ AtomicBoolean finished = new AtomicBoolean(false);
|
||||
+ Runnable finishBroadcast = () -> {
|
||||
+ if (finished.compareAndSet(false, true)) {
|
||||
+ try {
|
||||
+ pendingResult.finish();
|
||||
+ } catch (Exception ignored) {
|
||||
+ }
|
||||
+ }
|
||||
+ };
|
||||
+ new Handler(Looper.getMainLooper()).postDelayed(finishBroadcast, BROADCAST_TIMEOUT_MS);
|
||||
+
|
||||
+ try {
|
||||
+ ReactHost reactHost = ((ReactApplication) context.getApplicationContext()).getReactHost();
|
||||
+ ReactContext reactContext = reactHost.getCurrentReactContext();
|
||||
+
|
||||
+ Map<String, Object> arguments = new HashMap<>(data.getKeyValueMap());
|
||||
+ arguments.put("screenInfo", RNWidgetUtil.getScreenInfo(context.getApplicationContext()).toHashMap());
|
||||
+
|
||||
+ HeadlessJsTaskConfig taskConfig = new HeadlessJsTaskConfig(
|
||||
+ "RNWidgetBackgroundTask",
|
||||
+ Arguments.makeNativeMap(arguments),
|
||||
+ 30 * 1000,
|
||||
+ true
|
||||
+ );
|
||||
+
|
||||
+ if (reactContext == null) {
|
||||
+ reactHost.addReactInstanceEventListener(new ReactInstanceEventListener() {
|
||||
+ @Override
|
||||
+ public void onReactContextInitialized(ReactContext initializedContext) {
|
||||
+ reactHost.removeReactInstanceEventListener(this);
|
||||
+ runTask(initializedContext, taskConfig, finishBroadcast);
|
||||
+ }
|
||||
+ });
|
||||
+ reactHost.start();
|
||||
+ } else {
|
||||
+ runTask(reactContext, taskConfig, finishBroadcast);
|
||||
+ }
|
||||
+ } catch (Throwable t) {
|
||||
+ // Fall back to the WorkManager path so the click is never lost.
|
||||
+ RNWidgetJsCommunication.startBackgroundTask(context, data);
|
||||
+ finishBroadcast.run();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static void runTask(ReactContext reactContext, HeadlessJsTaskConfig taskConfig, Runnable finishBroadcast) {
|
||||
+ UiThreadUtil.runOnUiThread(() -> {
|
||||
+ try {
|
||||
+ HeadlessJsTaskContext headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext);
|
||||
+ int[] taskIdHolder = new int[]{-1};
|
||||
+ HeadlessJsTaskEventListener listener = new HeadlessJsTaskEventListener() {
|
||||
+ @Override
|
||||
+ public void onHeadlessJsTaskStart(int taskId) {
|
||||
+ }
|
||||
+
|
||||
+ @Override
|
||||
+ public void onHeadlessJsTaskFinish(int taskId) {
|
||||
+ if (taskId == taskIdHolder[0]) {
|
||||
+ headlessJsTaskContext.removeTaskEventListener(this);
|
||||
+ finishBroadcast.run();
|
||||
+ }
|
||||
+ }
|
||||
+ };
|
||||
+ headlessJsTaskContext.addTaskEventListener(listener);
|
||||
+ taskIdHolder[0] = headlessJsTaskContext.startTask(taskConfig);
|
||||
+ } catch (Throwable t) {
|
||||
+ finishBroadcast.run();
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java
|
||||
index f818d97..07432e8 100644
|
||||
--- a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java
|
||||
+++ b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java
|
||||
@@ -32,6 +32,20 @@ public class RNWidgetCollectionService extends RemoteViewsService {
|
||||
}
|
||||
}
|
||||
|
||||
+ // PATCH (simpl-liste): when the widget has a single (light/dark-agnostic)
|
||||
+ // config, both modes show identical item images. Copying the already-encoded
|
||||
+ // files is much cheaper than compressing every item bitmap a second time.
|
||||
+ public static void copyCollection(ReactApplicationContext context, int widgetId, int collectionId, int itemCount, String fromMode, String toMode) {
|
||||
+ RNWidgetImageProvider.deleteCollectionImages(context, widgetId, collectionId, toMode);
|
||||
+ for (int i = 0; i < itemCount; i++) {
|
||||
+ RNWidgetImageProvider.copyImage(
|
||||
+ context,
|
||||
+ getImageName(widgetId, collectionId, i, fromMode),
|
||||
+ getImageName(widgetId, collectionId, i, toMode)
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
static String getImageName(int widgetId, int collectionId, int position, String mode) {
|
||||
return "widget_" + widgetId + "_mode_" + mode + "_collection_" + collectionId + "_" + position + ".png";
|
||||
}
|
||||
diff --git a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetImageProvider.java b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetImageProvider.java
|
||||
index 0f963f0..ae18c25 100644
|
||||
--- a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetImageProvider.java
|
||||
+++ b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetImageProvider.java
|
||||
@@ -6,11 +6,13 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
+import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
+import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -94,7 +96,15 @@ public class RNWidgetImageProvider extends ContentProvider {
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
fos = new FileOutputStream(file);
|
||||
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
+ // PATCH (simpl-liste): WEBP_LOSSLESS at minimum effort encodes several
|
||||
+ // times faster than PNG for widget-sized bitmaps, which cuts
|
||||
+ // tap-to-render latency. Consumers decode by magic bytes, so the .png
|
||||
+ // file name and the provider's image/png MIME type are not a problem.
|
||||
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
+ bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 0, fos);
|
||||
+ } else {
|
||||
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
+ }
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
@@ -108,6 +118,26 @@ public class RNWidgetImageProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
+ // PATCH (simpl-liste): copy an already-encoded image instead of re-compressing
|
||||
+ // the same bitmap a second time (used for the dark variant when the widget has
|
||||
+ // a single light/dark-agnostic config).
|
||||
+ static void copyImage(Context context, String fromName, String toName) {
|
||||
+ File folder = getFolderWithImages(context);
|
||||
+ File from = new File(folder, fromName);
|
||||
+ File to = new File(folder, toName);
|
||||
+
|
||||
+ try (FileInputStream in = new FileInputStream(from);
|
||||
+ FileOutputStream out = new FileOutputStream(to)) {
|
||||
+ byte[] buffer = new byte[8192];
|
||||
+ int read;
|
||||
+ while ((read = in.read(buffer)) != -1) {
|
||||
+ out.write(buffer, 0, read);
|
||||
+ }
|
||||
+ } catch (Exception e) {
|
||||
+ e.printStackTrace();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
static Uri getImageUri(Context context, String fileName) {
|
||||
String authority = context.getPackageName() + AUTHORITY_SUFFIX;
|
||||
|
||||
diff --git a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java
|
||||
index 647d8f4..043c56c 100644
|
||||
--- a/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java
|
||||
+++ b/node_modules/react-native-android-widget/android/src/main/java/com/reactnativeandroidwidget/RNWidgetProvider.java
|
||||
@@ -151,6 +151,9 @@ public class RNWidgetProvider extends AppWidgetProvider {
|
||||
}
|
||||
|
||||
Data data = RNWidgetJsCommunication.buildData(context, getClass().getSimpleName(), widgetId, "WIDGET_CLICK", additionalData);
|
||||
- RNWidgetJsCommunication.startBackgroundTask(context, data);
|
||||
+ // PATCH (simpl-liste): user-initiated clicks bypass WorkManager and run the
|
||||
+ // headless JS task directly for latency. goAsync() keeps the process alive
|
||||
+ // until the task reports completion (or the safety timeout hits).
|
||||
+ RNWidgetClickTask.start(context, goAsync(), data);
|
||||
}
|
||||
}
|
||||
78
src/services/widgetState.ts
Normal file
78
src/services/widgetState.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// Consolidated widget state stored under a single AsyncStorage key.
|
||||
//
|
||||
// This module is on the critical path of the headless widget click handler:
|
||||
// it must stay dependency-light (no DB, no drizzle, no date-fns) so that a
|
||||
// cold-started headless task executes as little module code as possible.
|
||||
|
||||
export const WIDGET_STATE_KEY = 'widget:state';
|
||||
export const WIDGET_NAMES = ['SimplListeSmall', 'SimplListeMedium', 'SimplListeLarge'] as const;
|
||||
|
||||
// Legacy keys — used for migration only
|
||||
const LEGACY_DATA_KEY = 'widget:tasks';
|
||||
const LEGACY_DARK_KEY = 'widget:isDark';
|
||||
const LEGACY_EXPANDED_KEY = 'widget:expandedTaskIds';
|
||||
|
||||
export interface WidgetSubtask {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetTask {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: number;
|
||||
dueDate: string | null;
|
||||
completed: boolean;
|
||||
listColor: string | null;
|
||||
subtaskCount: number;
|
||||
subtaskDoneCount: number;
|
||||
subtasks: WidgetSubtask[];
|
||||
}
|
||||
|
||||
export interface WidgetState {
|
||||
tasks: WidgetTask[];
|
||||
isDark: boolean;
|
||||
expandedTaskIds: string[];
|
||||
}
|
||||
|
||||
export async function getWidgetState(): Promise<WidgetState> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(WIDGET_STATE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
|
||||
isDark: parsed.isDark === true,
|
||||
expandedTaskIds: Array.isArray(parsed.expandedTaskIds) ? parsed.expandedTaskIds : [],
|
||||
};
|
||||
}
|
||||
|
||||
// Migration from legacy keys
|
||||
const [dataRaw, darkRaw, expandedRaw] = await Promise.all([
|
||||
AsyncStorage.getItem(LEGACY_DATA_KEY),
|
||||
AsyncStorage.getItem(LEGACY_DARK_KEY),
|
||||
AsyncStorage.getItem(LEGACY_EXPANDED_KEY),
|
||||
]);
|
||||
|
||||
const state: WidgetState = {
|
||||
tasks: dataRaw ? JSON.parse(dataRaw) : [],
|
||||
isDark: darkRaw ? JSON.parse(darkRaw) === true : false,
|
||||
expandedTaskIds: expandedRaw ? JSON.parse(expandedRaw) : [],
|
||||
};
|
||||
|
||||
// Write consolidated key and clean up legacy keys
|
||||
await AsyncStorage.setItem(WIDGET_STATE_KEY, JSON.stringify(state));
|
||||
await AsyncStorage.multiRemove([LEGACY_DATA_KEY, LEGACY_DARK_KEY, LEGACY_EXPANDED_KEY]);
|
||||
|
||||
return state;
|
||||
} catch {
|
||||
return { tasks: [], isDark: false, expandedTaskIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export async function setWidgetState(state: WidgetState): Promise<void> {
|
||||
await AsyncStorage.setItem(WIDGET_STATE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
|
@ -6,77 +6,17 @@ import { tasks, lists } from '../db/schema';
|
|||
import { eq, and, isNull, gte, lte, lt, asc, sql } from 'drizzle-orm';
|
||||
import { startOfDay, endOfDay, addWeeks } from 'date-fns';
|
||||
import { TaskListWidget } from '../widgets/TaskListWidget';
|
||||
import {
|
||||
getWidgetState,
|
||||
setWidgetState,
|
||||
WIDGET_NAMES,
|
||||
type WidgetState,
|
||||
type WidgetTask,
|
||||
} from './widgetState';
|
||||
|
||||
export const WIDGET_STATE_KEY = 'widget:state';
|
||||
export const WIDGET_NAMES = ['SimplListeSmall', 'SimplListeMedium', 'SimplListeLarge'] as const;
|
||||
|
||||
// Legacy keys — used for migration only
|
||||
const LEGACY_DATA_KEY = 'widget:tasks';
|
||||
const LEGACY_DARK_KEY = 'widget:isDark';
|
||||
const LEGACY_EXPANDED_KEY = 'widget:expandedTaskIds';
|
||||
|
||||
export interface WidgetSubtask {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetTask {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: number;
|
||||
dueDate: string | null;
|
||||
completed: boolean;
|
||||
listColor: string | null;
|
||||
subtaskCount: number;
|
||||
subtaskDoneCount: number;
|
||||
subtasks: WidgetSubtask[];
|
||||
}
|
||||
|
||||
export interface WidgetState {
|
||||
tasks: WidgetTask[];
|
||||
isDark: boolean;
|
||||
expandedTaskIds: string[];
|
||||
}
|
||||
|
||||
export async function getWidgetState(): Promise<WidgetState> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(WIDGET_STATE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
|
||||
isDark: parsed.isDark === true,
|
||||
expandedTaskIds: Array.isArray(parsed.expandedTaskIds) ? parsed.expandedTaskIds : [],
|
||||
};
|
||||
}
|
||||
|
||||
// Migration from legacy keys
|
||||
const [dataRaw, darkRaw, expandedRaw] = await Promise.all([
|
||||
AsyncStorage.getItem(LEGACY_DATA_KEY),
|
||||
AsyncStorage.getItem(LEGACY_DARK_KEY),
|
||||
AsyncStorage.getItem(LEGACY_EXPANDED_KEY),
|
||||
]);
|
||||
|
||||
const state: WidgetState = {
|
||||
tasks: dataRaw ? JSON.parse(dataRaw) : [],
|
||||
isDark: darkRaw ? JSON.parse(darkRaw) === true : false,
|
||||
expandedTaskIds: expandedRaw ? JSON.parse(expandedRaw) : [],
|
||||
};
|
||||
|
||||
// Write consolidated key and clean up legacy keys
|
||||
await AsyncStorage.setItem(WIDGET_STATE_KEY, JSON.stringify(state));
|
||||
await AsyncStorage.multiRemove([LEGACY_DATA_KEY, LEGACY_DARK_KEY, LEGACY_EXPANDED_KEY]);
|
||||
|
||||
return state;
|
||||
} catch {
|
||||
return { tasks: [], isDark: false, expandedTaskIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export async function setWidgetState(state: WidgetState): Promise<void> {
|
||||
await AsyncStorage.setItem(WIDGET_STATE_KEY, JSON.stringify(state));
|
||||
}
|
||||
// State accessors and types live in widgetState.ts (dependency-light module
|
||||
// used by the headless click handler); re-exported here for existing callers.
|
||||
export * from './widgetState';
|
||||
|
||||
export async function syncWidgetData(): Promise<void> {
|
||||
if (Platform.OS !== 'android') return;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { WidgetInfo } from 'react-native-android-widget';
|
|||
|
||||
type HexColor = `#${string}`;
|
||||
type ColorProp = HexColor;
|
||||
import type { WidgetTask, WidgetSubtask } from '../services/widgetSync';
|
||||
import type { WidgetTask, WidgetSubtask } from '../services/widgetState';
|
||||
import {
|
||||
isToday,
|
||||
isTomorrow,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { WidgetTaskHandlerProps } from 'react-native-android-widget';
|
||||
import { requestWidgetUpdate } from 'react-native-android-widget';
|
||||
import { TaskListWidget } from './TaskListWidget';
|
||||
import { getWidgetState, setWidgetState, WIDGET_NAMES, type WidgetTask } from '../services/widgetSync';
|
||||
import { getWidgetState, setWidgetState, WIDGET_NAMES, type WidgetTask } from '../services/widgetState';
|
||||
import { isValidUUID } from '../lib/validation';
|
||||
|
||||
const EXPAND_DEBOUNCE_MS = 600;
|
||||
|
|
|
|||
|
|
@ -76,6 +76,59 @@ check('uuid v5 with buffer arg fills buffer (vuln site)', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// --- Widget latency guards ---
|
||||
// The headless widget click handler must stay on a dependency-light import
|
||||
// path: a cold-started headless task executes every statically imported
|
||||
// module before the first render, so heavy imports directly translate into
|
||||
// tap-to-feedback latency on the home screen widget.
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
function staticImportsOf(file) {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
|
||||
const imports = [];
|
||||
const re = /^import\s[^;]*?from\s+['"]([^'"]+)['"]/gms;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) imports.push(m[1]);
|
||||
return imports;
|
||||
}
|
||||
|
||||
check('widgetState.ts import graph stays light (no db/drizzle/date-fns)', () => {
|
||||
const imports = staticImportsOf('src/services/widgetState.ts');
|
||||
const heavy = imports.filter(
|
||||
(i) => /drizzle|date-fns|\/db\/|expo-sqlite/.test(i)
|
||||
);
|
||||
assert.deepEqual(heavy, [], `heavy imports found: ${heavy.join(', ')}`);
|
||||
});
|
||||
|
||||
check('widgetTaskHandler.ts does not statically import widgetSync (DB path)', () => {
|
||||
const imports = staticImportsOf('src/widgets/widgetTaskHandler.ts');
|
||||
const offenders = imports.filter((i) => /widgetSync|\/db\/|drizzle/.test(i));
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`widgetTaskHandler must reach the DB via dynamic import only, found: ${offenders.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
// The native latency patches (WorkManager bypass on click, WEBP compression,
|
||||
// light/dark collection dedup) live in patches/ and are re-applied by the
|
||||
// postinstall hook. Without either piece, an EAS build silently ships the
|
||||
// unpatched (slow) widget pipeline.
|
||||
check('react-native-android-widget latency patch is wired up', () => {
|
||||
const pkg = require('../package.json');
|
||||
const version = pkg.dependencies['react-native-android-widget'].replace(/^[~^]/, '');
|
||||
const patchFile = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'patches',
|
||||
`react-native-android-widget+${version}.patch`
|
||||
);
|
||||
assert.ok(fs.existsSync(patchFile), `missing ${patchFile}`);
|
||||
assert.equal(pkg.scripts.postinstall, 'patch-package', 'postinstall must run patch-package');
|
||||
});
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\nsmoke OK');
|
||||
process.exit(0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue