@capgo/capacitor-pretty-toast 8.1.17 → 8.1.19
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/android/src/main/java/com/toast/ToastOverlay.kt +10 -1
- package/android/src/main/java/com/toast/anim/CutoutMorphAnimator.kt +18 -0
- package/ios/Sources/PrettyToastPlugin/PrettyToastView.swift +42 -41
- package/ios/Sources/PrettyToastPlugin/ToastManager.swift +13 -2
- package/package.json +2 -1
- package/scripts/check-cap9-deprecated.mjs +304 -0
|
@@ -66,7 +66,13 @@ class ToastOverlay(activity: Activity) {
|
|
|
66
66
|
var onPress: (() -> Unit)? = null
|
|
67
67
|
var onActionPress: (() -> Unit)? = null
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// A `var`, not a `val`: show() calls destroy() on this same instance (not
|
|
70
|
+
// a fresh one) whenever useDynamicIsland changes, which shuts this down.
|
|
71
|
+
// loadIcon() rebuilds it on demand so a later custom-icon toast on the
|
|
72
|
+
// same overlay doesn't hit a RejectedExecutionException on a dead pool.
|
|
73
|
+
private var imageLoader: ExecutorService = newImageLoader()
|
|
74
|
+
|
|
75
|
+
private fun newImageLoader(): ExecutorService = Executors.newSingleThreadExecutor { r ->
|
|
70
76
|
Thread(r, "ToastIconLoader").apply { isDaemon = true }
|
|
71
77
|
}
|
|
72
78
|
// Bitmaps keyed by URI so a repeat show of the same custom icon paints
|
|
@@ -379,6 +385,9 @@ class ToastOverlay(activity: Activity) {
|
|
|
379
385
|
private fun loadIcon(built: ToastViewFactory.Built, uri: String) {
|
|
380
386
|
val targetW = built.icon.layoutParams.width.takeIf { it > 0 } ?: DEFAULT_ICON_TARGET_PX
|
|
381
387
|
val targetH = built.icon.layoutParams.height.takeIf { it > 0 } ?: DEFAULT_ICON_TARGET_PX
|
|
388
|
+
if (imageLoader.isShutdown) {
|
|
389
|
+
imageLoader = newImageLoader()
|
|
390
|
+
}
|
|
382
391
|
imageLoader.execute {
|
|
383
392
|
val bitmap = try {
|
|
384
393
|
when {
|
|
@@ -28,6 +28,17 @@ class CutoutMorphAnimator(
|
|
|
28
28
|
private var cornerAnimator: ValueAnimator? = null
|
|
29
29
|
private val cornerRadiiBuf = FloatArray(8)
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Set for the duration of dismiss()'s own pill.animate() run. A tap or
|
|
33
|
+
* release arriving in that window would otherwise reach snapBack(),
|
|
34
|
+
* which cancels the very same ViewPropertyAnimator instance (Android
|
|
35
|
+
* caches one per view) — cancel() never runs withEndAction, so the
|
|
36
|
+
* dismiss's own state reset and onEnd() callback would be lost forever,
|
|
37
|
+
* leaving the toast overlay's isDismissing flag stuck and every later
|
|
38
|
+
* toast permanently queued behind it.
|
|
39
|
+
*/
|
|
40
|
+
private var isDismissingAnim = false
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Lower bound for the measured pill height — guards against racing
|
|
33
44
|
* the layout pass, where `pill.height` can briefly read as 0.
|
|
@@ -76,6 +87,7 @@ class CutoutMorphAnimator(
|
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
override fun dismiss(onEnd: () -> Unit) {
|
|
90
|
+
isDismissingAnim = true
|
|
79
91
|
onBeforeDismiss()
|
|
80
92
|
|
|
81
93
|
val expandedWidth = pill.width.toFloat().coerceAtLeast(1f)
|
|
@@ -110,6 +122,7 @@ class CutoutMorphAnimator(
|
|
|
110
122
|
.setDuration(MORPH_DURATION_MS)
|
|
111
123
|
.setInterpolator(MORPH_EASING)
|
|
112
124
|
.withEndAction {
|
|
125
|
+
isDismissingAnim = false
|
|
113
126
|
resetPillToExpandedResting()
|
|
114
127
|
onEnd()
|
|
115
128
|
}
|
|
@@ -123,6 +136,11 @@ class CutoutMorphAnimator(
|
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
override fun snapBack() {
|
|
139
|
+
// A dismiss already in flight owns this animator; cancelling it here
|
|
140
|
+
// would strand it (see isDismissingAnim above) instead of letting it
|
|
141
|
+
// finish and report back to ToastOverlay.
|
|
142
|
+
if (isDismissingAnim) return
|
|
143
|
+
|
|
126
144
|
// Release-without-dismiss during a morph drag: spring back to full
|
|
127
145
|
// size and restore content opacity.
|
|
128
146
|
pill.animate().cancel()
|
|
@@ -17,55 +17,56 @@ struct PrettyToastView: View {
|
|
|
17
17
|
let scaleX: CGFloat = isExpanded ? 1 : (layout.compactWidth / layout.expandedWidth)
|
|
18
18
|
let scaleY: CGFloat = isExpanded ? 1 : (layout.compactHeight / layout.expandedHeight)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// A separate `.onTapGesture` alongside a low-threshold
|
|
21
|
+
// `.gesture(DragGesture())` on the same view is a classic SwiftUI
|
|
22
|
+
// trap: `minimumDistance: 2` makes the drag start recognizing on
|
|
23
|
+
// almost any real touch, taps included, and once it does, it took
|
|
24
|
+
// exclusive priority over the sibling tap gesture — so a plain
|
|
25
|
+
// tap never reached `wasTapped` and the toast could never be
|
|
26
|
+
// dismissed by tapping it. One gesture, one decision on release,
|
|
27
|
+
// fixes that: a short release is always a tap regardless of
|
|
28
|
+
// whether swipe-dismiss is enabled.
|
|
29
|
+
let dismissGesture = DragGesture(minimumDistance: 0).onEnded { value in
|
|
30
|
+
if window.enableSwipeDismiss && (value.translation.height < -8 || value.predictedEndTranslation.height < -40) {
|
|
22
31
|
window.swipeDismissRequested = true
|
|
32
|
+
} else {
|
|
33
|
+
window.wasTapped = true
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
ZStack {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
toastBackground()
|
|
39
|
+
.overlay {
|
|
40
|
+
toastContent(layout)
|
|
41
|
+
.frame(width: layout.expandedWidth, height: layout.expandedHeight)
|
|
42
|
+
.scaleEffect(x: scaleX, y: scaleY)
|
|
43
|
+
}
|
|
44
|
+
.frame(
|
|
45
|
+
width: isExpanded ? layout.expandedWidth : layout.compactWidth,
|
|
46
|
+
height: isExpanded ? layout.expandedHeight : layout.compactHeight
|
|
47
|
+
)
|
|
48
|
+
.opacity(layout.hasDynamicIsland ? 1 : (isExpanded ? 1 : 0))
|
|
49
|
+
.modifier(CapsuleOpacityModifier(
|
|
50
|
+
haveDynamicIsland: layout.hasDynamicIsland,
|
|
51
|
+
isExpanded: isExpanded
|
|
52
|
+
))
|
|
53
|
+
.modifier(GeometryGroupModifier())
|
|
54
|
+
.contentShape(Rectangle())
|
|
55
|
+
.gesture(dismissGesture)
|
|
56
|
+
.background(
|
|
57
|
+
GeometryReader { geo in
|
|
58
|
+
Color.clear.preference(
|
|
59
|
+
key: ToastFrameKey.self,
|
|
60
|
+
value: geo.frame(in: .named("overlayWindow"))
|
|
61
|
+
)
|
|
47
62
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
value: geo.frame(in: .named("overlayWindow"))
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
.onPreferenceChange(ToastFrameKey.self) { frame in
|
|
57
|
-
if window.toastHitFrame != frame {
|
|
58
|
-
window.toastHitFrame = frame
|
|
59
|
-
}
|
|
63
|
+
)
|
|
64
|
+
.onPreferenceChange(ToastFrameKey.self) { frame in
|
|
65
|
+
if window.toastHitFrame != frame {
|
|
66
|
+
window.toastHitFrame = frame
|
|
60
67
|
}
|
|
61
|
-
|
|
62
|
-
if window.enableSwipeDismiss {
|
|
63
|
-
pill.gesture(swipeGesture)
|
|
64
|
-
} else {
|
|
65
|
-
pill
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
.offset(y: layout.hasDynamicIsland ? (isExpanded ? layout.expandedTopOffset : layout.topOffset) : 0)
|
|
69
|
+
.offset(y: layout.hasDynamicIsland ? (isExpanded ? layout.expandedTopOffset : layout.topOffset) : 0)
|
|
69
70
|
}
|
|
70
71
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
71
72
|
.padding(.top, layout.hasDynamicIsland ? 0 : (isExpanded ? max(safeArea.top, 10) : 0))
|
|
@@ -84,11 +84,18 @@ import UIKit
|
|
|
84
84
|
self.cancelTimer()
|
|
85
85
|
if autoDismiss && duration > 0 {
|
|
86
86
|
let interval = TimeInterval(duration) / 1000.0
|
|
87
|
-
|
|
87
|
+
// `.common` mode, like the backdrop sampler below: `.default`-mode
|
|
88
|
+
// timers are suspended for as long as the run loop is in `.tracking`
|
|
89
|
+
// mode, which UIKit enters for any active touch/scroll — so a toast
|
|
90
|
+
// triggered while (or dismissed while) the user is scrolling would
|
|
91
|
+
// never time out, jamming every later toast behind it in the queue.
|
|
92
|
+
let timer = Timer(timeInterval: interval, repeats: false) { [weak self] _ in
|
|
88
93
|
DispatchQueue.main.async {
|
|
89
94
|
self?.dismiss()
|
|
90
95
|
}
|
|
91
96
|
}
|
|
97
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
98
|
+
self.autoDismissTimer = timer
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
if !accessibilityAnnouncement.isEmpty {
|
|
@@ -148,11 +155,15 @@ import UIKit
|
|
|
148
155
|
cancelTimer()
|
|
149
156
|
if autoDismiss && duration > 0 {
|
|
150
157
|
let interval = TimeInterval(duration) / 1000.0
|
|
151
|
-
|
|
158
|
+
// See the matching comment in show(): `.common` mode so scrolling
|
|
159
|
+
// doesn't suspend the countdown.
|
|
160
|
+
let timer = Timer(timeInterval: interval, repeats: false) { [weak self] _ in
|
|
152
161
|
DispatchQueue.main.async {
|
|
153
162
|
self?.dismiss()
|
|
154
163
|
}
|
|
155
164
|
}
|
|
165
|
+
RunLoop.main.add(timer, forMode: .common)
|
|
166
|
+
autoDismissTimer = timer
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-pretty-toast",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.19",
|
|
4
4
|
"description": "Native-first pretty toast notifications for Capacitor and the web",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"watch": "tsc --watch",
|
|
55
55
|
"prepublishOnly": "bun run build",
|
|
56
56
|
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
|
|
57
|
+
"check:cap9-deprecated": "node scripts/check-cap9-deprecated.mjs",
|
|
57
58
|
"example:install": "cd example-app && bun install --frozen-lockfile",
|
|
58
59
|
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
|
|
59
60
|
"changelog:ai": "node scripts/generate-ai-changelog.mjs"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capacitor 9 deprecated native API guard.
|
|
4
|
+
*
|
|
5
|
+
* Fails when plugin native sources still use APIs removed in Capacitor 9.
|
|
6
|
+
* Does not flag Cordova SwiftPM product dependencies (still required on Cap 8).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/check-cap9-deprecated.mjs
|
|
10
|
+
* node scripts/check-cap9-deprecated.mjs --workspace
|
|
11
|
+
* node scripts/check-cap9-deprecated.mjs --dir path
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const SKIP_DIRS = new Set([
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
".build",
|
|
23
|
+
".gradle",
|
|
24
|
+
"Pods",
|
|
25
|
+
"DerivedData",
|
|
26
|
+
".swiftpm",
|
|
27
|
+
".git",
|
|
28
|
+
"example-app",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** @type {{ id: string, pattern: RegExp, exts: string[], ignoreLine?: RegExp }[]} */
|
|
32
|
+
const RULES = [
|
|
33
|
+
{
|
|
34
|
+
id: "hasOption",
|
|
35
|
+
pattern: /\bhasOption\s*\(/,
|
|
36
|
+
exts: [".java", ".kt", ".swift"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "getConfigValue",
|
|
40
|
+
pattern: /\bgetConfigValue\s*\(/,
|
|
41
|
+
exts: [".java", ".kt", ".swift"],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "@NativePlugin",
|
|
45
|
+
pattern: /@NativePlugin\b/,
|
|
46
|
+
exts: [".java", ".kt"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "saveCall",
|
|
50
|
+
pattern: /\bsaveCall\s*\(/,
|
|
51
|
+
exts: [".java", ".kt", ".swift"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "getSavedCall",
|
|
55
|
+
pattern: /\bgetSavedCall\s*\(/,
|
|
56
|
+
exts: [".java", ".kt", ".swift"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "freeSavedCall",
|
|
60
|
+
pattern: /\bfreeSavedCall\s*\(/,
|
|
61
|
+
exts: [".java", ".kt", ".swift"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "releaseCall",
|
|
65
|
+
pattern: /\breleaseCall\s*\(/,
|
|
66
|
+
exts: [".java", ".kt", ".swift"],
|
|
67
|
+
ignoreLine: /\.releaseCall\s*\(\s*withID:/,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "pluginRequestPermission",
|
|
71
|
+
pattern: /\bpluginRequestPermissions?\s*\(/,
|
|
72
|
+
exts: [".java", ".kt"],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "pluginRequestAllPermissions",
|
|
76
|
+
pattern: /\bpluginRequestAllPermissions\s*\(/,
|
|
77
|
+
exts: [".java", ".kt"],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "hasDefinedPermissions",
|
|
81
|
+
pattern: /\bhasDefinedPermissions\s*\(/,
|
|
82
|
+
exts: [".java", ".kt"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "CAPBridge",
|
|
86
|
+
pattern: /\bCAPBridge\./,
|
|
87
|
+
exts: [".swift"],
|
|
88
|
+
ignoreLine: /CAPBridgedPlugin/,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "CAPNotifications",
|
|
92
|
+
pattern: /\bCAPNotifications\b/,
|
|
93
|
+
exts: [".swift"],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const CORDova_SPM_LINE =
|
|
98
|
+
/\.product\s*\(\s*name\s*:\s*"Cordova"\s*,\s*package\s*:\s*"capacitor-swift-pm"\s*\)/;
|
|
99
|
+
|
|
100
|
+
function readText(p) {
|
|
101
|
+
try {
|
|
102
|
+
return fs.readFileSync(p, "utf8");
|
|
103
|
+
} catch {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exists(p) {
|
|
109
|
+
try {
|
|
110
|
+
fs.accessSync(p);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseArgs(argv) {
|
|
118
|
+
const out = { dir: process.cwd(), workspace: false };
|
|
119
|
+
for (let i = 2; i < argv.length; i++) {
|
|
120
|
+
const a = argv[i];
|
|
121
|
+
if (a === "--workspace") {
|
|
122
|
+
out.workspace = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (a === "--dir" || a === "--pluginDir") {
|
|
126
|
+
out.dir = path.resolve(argv[++i] || ".");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function walkFiles(rootDir, exts) {
|
|
134
|
+
const out = [];
|
|
135
|
+
const stack = [rootDir];
|
|
136
|
+
while (stack.length) {
|
|
137
|
+
const dir = stack.pop();
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (e.isDirectory()) {
|
|
146
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
147
|
+
stack.push(path.join(dir, e.name));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!e.isFile()) continue;
|
|
151
|
+
for (const ext of exts) {
|
|
152
|
+
if (e.name.endsWith(ext)) {
|
|
153
|
+
out.push(path.join(dir, e.name));
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
out.sort();
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectScanRoots(pluginDir, cap) {
|
|
164
|
+
const roots = [];
|
|
165
|
+
if (cap.android) {
|
|
166
|
+
const androidMain = path.join(pluginDir, "android", "src", "main");
|
|
167
|
+
if (exists(androidMain)) roots.push(androidMain);
|
|
168
|
+
}
|
|
169
|
+
if (cap.ios) {
|
|
170
|
+
const iosSources = path.join(pluginDir, "ios", "Sources");
|
|
171
|
+
if (exists(iosSources)) roots.push(iosSources);
|
|
172
|
+
else {
|
|
173
|
+
const iosDir = path.join(pluginDir, "ios");
|
|
174
|
+
if (exists(iosDir)) roots.push(iosDir);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const packageSwift = path.join(pluginDir, "Package.swift");
|
|
178
|
+
if (exists(packageSwift)) roots.push(packageSwift);
|
|
179
|
+
return roots;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function scanFile(filePath, rule) {
|
|
183
|
+
const ext = path.extname(filePath);
|
|
184
|
+
if (!rule.exts.includes(ext)) return [];
|
|
185
|
+
|
|
186
|
+
const txt = readText(filePath);
|
|
187
|
+
const lines = txt.split(/\r?\n/);
|
|
188
|
+
const hits = [];
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
if (filePath.endsWith("Package.swift") && CORDova_SPM_LINE.test(line)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (rule.ignoreLine?.test(line)) continue;
|
|
195
|
+
if (rule.pattern.test(line)) {
|
|
196
|
+
hits.push({ line: i + 1, text: line.trim() });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return hits;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function listWorkspacePluginDirs(repoRoot) {
|
|
203
|
+
const packagesRoot = path.join(repoRoot, "packages");
|
|
204
|
+
if (!exists(packagesRoot)) return [];
|
|
205
|
+
return fs
|
|
206
|
+
.readdirSync(packagesRoot, { withFileTypes: true })
|
|
207
|
+
.filter((e) => e.isDirectory())
|
|
208
|
+
.map((e) => path.join(packagesRoot, e.name))
|
|
209
|
+
.filter((dir) => {
|
|
210
|
+
const pkgPath = path.join(dir, "package.json");
|
|
211
|
+
if (!exists(pkgPath)) return false;
|
|
212
|
+
try {
|
|
213
|
+
const pkg = JSON.parse(readText(pkgPath));
|
|
214
|
+
const cap = typeof pkg.capacitor === "object" && pkg.capacitor ? pkg.capacitor : {};
|
|
215
|
+
return Boolean(cap.android || cap.ios);
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.sort();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @returns {boolean} true when the plugin passes
|
|
225
|
+
*/
|
|
226
|
+
function checkPluginDir(pluginDir) {
|
|
227
|
+
const pkgPath = path.join(pluginDir, "package.json");
|
|
228
|
+
|
|
229
|
+
if (!exists(pkgPath)) {
|
|
230
|
+
console.error(`[cap9-deprecated] ERROR: missing package.json in ${pluginDir}`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let pkg;
|
|
235
|
+
try {
|
|
236
|
+
pkg = JSON.parse(readText(pkgPath));
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error(`[cap9-deprecated] ERROR: invalid package.json (${pkgPath}): ${e?.message || e}`);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const cap = typeof pkg.capacitor === "object" && pkg.capacitor ? pkg.capacitor : {};
|
|
243
|
+
if (!cap.android && !cap.ios) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const scanRoots = collectScanRoots(pluginDir, cap);
|
|
248
|
+
const allExts = [...new Set(RULES.flatMap((r) => r.exts))];
|
|
249
|
+
const files = [];
|
|
250
|
+
for (const root of scanRoots) {
|
|
251
|
+
if (root.endsWith("Package.swift")) {
|
|
252
|
+
files.push(root);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
files.push(...walkFiles(root, allExts));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const violations = [];
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
for (const rule of RULES) {
|
|
261
|
+
const hits = scanFile(file, rule);
|
|
262
|
+
for (const hit of hits) {
|
|
263
|
+
violations.push({
|
|
264
|
+
rule: rule.id,
|
|
265
|
+
file: path.relative(pluginDir, file),
|
|
266
|
+
line: hit.line,
|
|
267
|
+
text: hit.text,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (violations.length) {
|
|
274
|
+
const relDir = path.relative(process.cwd(), pluginDir) || ".";
|
|
275
|
+
console.error(`[cap9-deprecated] FAIL in ${relDir}`);
|
|
276
|
+
for (const v of violations) {
|
|
277
|
+
console.error(`- ${v.rule}: ${v.file}:${v.line}: ${v.text}`);
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
286
|
+
const args = parseArgs(process.argv);
|
|
287
|
+
|
|
288
|
+
if (args.workspace) {
|
|
289
|
+
const pluginDirs = listWorkspacePluginDirs(repoRoot);
|
|
290
|
+
if (!pluginDirs.length) {
|
|
291
|
+
console.error("[cap9-deprecated] ERROR: no Capacitor plugin packages found under packages/*");
|
|
292
|
+
process.exit(2);
|
|
293
|
+
}
|
|
294
|
+
let failed = false;
|
|
295
|
+
for (const dir of pluginDirs) {
|
|
296
|
+
if (!checkPluginDir(dir)) failed = true;
|
|
297
|
+
}
|
|
298
|
+
if (failed) process.exit(1);
|
|
299
|
+
console.log(`[cap9-deprecated] OK (${pluginDirs.length} plugin package(s) scanned)`);
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const ok = checkPluginDir(args.dir);
|
|
304
|
+
process.exit(ok ? 0 : 1);
|