capacitor-video-transcoder 0.0.0-stage → 0.0.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/CapacitorVideoTranscoder.podspec +18 -0
- package/Package.swift +39 -0
- package/README.md +238 -2
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/TranscodeOptions.kt +122 -0
- package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/VideoTranscoder.kt +265 -0
- package/android/src/main/kotlin/io/github/maximbelov/videotranscoder/VideoTranscoderPlugin.kt +51 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +221 -0
- package/dist/esm/definitions.d.ts +120 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +6 -0
- package/dist/esm/web.js +10 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +24 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +27 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Package.swift +22 -0
- package/ios/Sources/VideoTranscoderCore/Geometry.swift +61 -0
- package/ios/Sources/VideoTranscoderCore/TranscodeOptions.swift +107 -0
- package/ios/Sources/VideoTranscoderCore/Transcoder.swift +388 -0
- package/ios/Sources/VideoTranscoderPlugin/VideoTranscoderPlugin.swift +73 -0
- package/ios/Tests/VideoTranscoderCoreTests/OptionsAndGeometryTests.swift +146 -0
- package/ios/Tests/VideoTranscoderCoreTests/TranscoderTests.swift +234 -0
- package/ios/Tests/VideoTranscoderPluginTests/VideoTranscoderPluginTests.swift +32 -0
- package/package.json +101 -4
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import VideoTranscoderCore
|
|
4
|
+
|
|
5
|
+
/// End to end: the fixtures in test/fixtures transcoded with AVFoundation, and the output read back. On a Mac with `swift test`
|
|
6
|
+
/// this is the same AVAssetReader/AVAssetWriter path the phone takes, with the Mac's encoder instead of the phone's.
|
|
7
|
+
final class TranscoderTests: XCTestCase {
|
|
8
|
+
static let fixtures = URL(fileURLWithPath: #filePath)
|
|
9
|
+
.deletingLastPathComponent() // VideoTranscoderCoreTests
|
|
10
|
+
.deletingLastPathComponent() // Tests
|
|
11
|
+
.deletingLastPathComponent() // ios
|
|
12
|
+
.deletingLastPathComponent() // the repository
|
|
13
|
+
.appendingPathComponent("test/fixtures")
|
|
14
|
+
|
|
15
|
+
var outputs: URL!
|
|
16
|
+
|
|
17
|
+
override func setUpWithError() throws {
|
|
18
|
+
outputs = FileManager.default.temporaryDirectory.appendingPathComponent("video-transcoder-tests-\(UUID().uuidString)")
|
|
19
|
+
try FileManager.default.createDirectory(at: outputs, withIntermediateDirectories: true)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override func tearDownWithError() throws {
|
|
23
|
+
try? FileManager.default.removeItem(at: outputs)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func fixture(_ name: String) -> URL { TranscoderTests.fixtures.appendingPathComponent(name) }
|
|
27
|
+
|
|
28
|
+
func testRotatedVideoWithPCMAudio() async throws {
|
|
29
|
+
let out = outputs.appendingPathComponent("rotated.mp4")
|
|
30
|
+
let progress = Recorder()
|
|
31
|
+
let outcome = try await Transcoder().transcode(
|
|
32
|
+
TranscodeOptions(source: fixture("rotated-pcm.mov"), destination: out),
|
|
33
|
+
progress: { progress.add($0) }
|
|
34
|
+
)
|
|
35
|
+
XCTAssertEqual(outcome.url, out)
|
|
36
|
+
// stored 320x240, shown upright as 240x320
|
|
37
|
+
XCTAssertEqual(outcome.width, 240)
|
|
38
|
+
XCTAssertEqual(outcome.height, 320)
|
|
39
|
+
XCTAssertEqual(Double(outcome.durationMs), 2000, accuracy: 100)
|
|
40
|
+
XCTAssertGreaterThan(outcome.size, 0)
|
|
41
|
+
|
|
42
|
+
let facts = try await Facts.read(out)
|
|
43
|
+
XCTAssertEqual(facts.videoCodec, kCMVideoCodecType_H264)
|
|
44
|
+
XCTAssertEqual(facts.avcProfile, 100, "H.264 High")
|
|
45
|
+
XCTAssertEqual(facts.storedSize, CGSize(width: 320, height: 240))
|
|
46
|
+
// the rotation is carried over as the track transform, unchanged
|
|
47
|
+
let source = try await Facts.read(fixture("rotated-pcm.mov"))
|
|
48
|
+
XCTAssertNotEqual(Geometry.rotationDegrees(of: source.transform), 0)
|
|
49
|
+
XCTAssertEqual(Geometry.rotationDegrees(of: facts.transform), Geometry.rotationDegrees(of: source.transform))
|
|
50
|
+
XCTAssertEqual(facts.primaries, kCMFormatDescriptionColorPrimaries_ITU_R_709_2 as String)
|
|
51
|
+
XCTAssertEqual(facts.transfer, kCMFormatDescriptionTransferFunction_ITU_R_709_2 as String)
|
|
52
|
+
XCTAssertEqual(facts.matrix, kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2 as String)
|
|
53
|
+
XCTAssertEqual(facts.audioFormat, kAudioFormatMPEG4AAC)
|
|
54
|
+
XCTAssertEqual(facts.audioChannels, 2)
|
|
55
|
+
XCTAssertEqual(facts.audioSampleRate, 16_000)
|
|
56
|
+
XCTAssertEqual(facts.topLevelAtoms.filter { $0 == "moov" || $0 == "mdat" }, ["moov", "mdat"], "faststart")
|
|
57
|
+
|
|
58
|
+
let values = progress.values
|
|
59
|
+
XCTAssertEqual(values.last, 1)
|
|
60
|
+
XCTAssertEqual(values, values.sorted(), "progress only grows")
|
|
61
|
+
XCTAssertGreaterThan(values.count, 2)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func testHDRIsToneMappedAndAACIsCopied() async throws {
|
|
65
|
+
let out = outputs.appendingPathComponent("hdr.mp4")
|
|
66
|
+
let outcome = try await Transcoder().transcode(TranscodeOptions(source: fixture("hdr-hlg.mov"), destination: out))
|
|
67
|
+
XCTAssertEqual(outcome.width, 256)
|
|
68
|
+
XCTAssertEqual(outcome.height, 144)
|
|
69
|
+
|
|
70
|
+
let facts = try await Facts.read(out)
|
|
71
|
+
XCTAssertEqual(facts.videoCodec, kCMVideoCodecType_H264)
|
|
72
|
+
XCTAssertEqual(facts.primaries, kCMFormatDescriptionColorPrimaries_ITU_R_709_2 as String)
|
|
73
|
+
XCTAssertEqual(facts.transfer, kCMFormatDescriptionTransferFunction_ITU_R_709_2 as String)
|
|
74
|
+
XCTAssertEqual(facts.audioFormat, kAudioFormatMPEG4AAC)
|
|
75
|
+
XCTAssertEqual(facts.audioChannels, 1)
|
|
76
|
+
XCTAssertEqual(facts.audioSampleRate, 44_100)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func testScalesDownCutsAndAddsNoAudio() async throws {
|
|
80
|
+
let out = outputs.appendingPathComponent("nested/dir/silent.mp4")
|
|
81
|
+
let outcome = try await Transcoder().transcode(TranscodeOptions(
|
|
82
|
+
source: fixture("long-silent.mp4"), destination: out, maxSize: 320, maxDurationMs: 1500, videoBitrate: 200_000
|
|
83
|
+
))
|
|
84
|
+
XCTAssertEqual(outcome.width, 320)
|
|
85
|
+
XCTAssertEqual(outcome.height, 180)
|
|
86
|
+
XCTAssertEqual(Double(outcome.durationMs), 1500, accuracy: 70)
|
|
87
|
+
|
|
88
|
+
let facts = try await Facts.read(out)
|
|
89
|
+
XCTAssertEqual(facts.storedSize, CGSize(width: 320, height: 180))
|
|
90
|
+
XCTAssertNil(facts.audioFormat)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func testReplacesAFileAtTheDestinationAndDefaultsToTheTemporaryDirectory() async throws {
|
|
94
|
+
let out = outputs.appendingPathComponent("taken.mp4")
|
|
95
|
+
try Data("not a video".utf8).write(to: out)
|
|
96
|
+
_ = try await Transcoder().transcode(TranscodeOptions(source: fixture("hdr-hlg.mov"), destination: out))
|
|
97
|
+
let replaced = try await Facts.read(out)
|
|
98
|
+
XCTAssertEqual(replaced.videoCodec, kCMVideoCodecType_H264)
|
|
99
|
+
|
|
100
|
+
let outcome = try await Transcoder().transcode(TranscodeOptions(source: fixture("hdr-hlg.mov")))
|
|
101
|
+
defer { try? FileManager.default.removeItem(at: outcome.url) }
|
|
102
|
+
XCTAssertTrue(outcome.url.path.hasPrefix(FileManager.default.temporaryDirectory.resolvingSymlinksInPath().path)
|
|
103
|
+
|| outcome.url.path.hasPrefix(FileManager.default.temporaryDirectory.path))
|
|
104
|
+
XCTAssertEqual(outcome.url.pathExtension, "mp4")
|
|
105
|
+
XCTAssertTrue(FileManager.default.fileExists(atPath: outcome.url.path))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func testNotFoundAndNoVideo() async throws {
|
|
109
|
+
await assertFails(.notFound, TranscodeOptions(source: outputs.appendingPathComponent("missing.mov")))
|
|
110
|
+
await assertFails(.notFound, TranscodeOptions(source: outputs))
|
|
111
|
+
let text = outputs.appendingPathComponent("text.mov")
|
|
112
|
+
try Data("not a video".utf8).write(to: text)
|
|
113
|
+
let out = outputs.appendingPathComponent("never.mp4")
|
|
114
|
+
await assertFails(.noVideo, TranscodeOptions(source: text, destination: out))
|
|
115
|
+
XCTAssertFalse(FileManager.default.fileExists(atPath: out.path))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func testOneAtATimeAndCancel() async throws {
|
|
119
|
+
let transcoder = Transcoder()
|
|
120
|
+
let out = outputs.appendingPathComponent("cancelled.mp4")
|
|
121
|
+
let started = expectation(description: "progress")
|
|
122
|
+
started.assertForOverFulfill = false
|
|
123
|
+
let first = Task {
|
|
124
|
+
try await transcoder.transcode(TranscodeOptions(source: fixture("long-silent.mp4"), destination: out)) { _ in
|
|
125
|
+
started.fulfill()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await fulfillment(of: [started], timeout: 30)
|
|
129
|
+
await assertFails(.busy, TranscodeOptions(source: fixture("hdr-hlg.mov")), with: transcoder)
|
|
130
|
+
transcoder.cancel()
|
|
131
|
+
do {
|
|
132
|
+
_ = try await first.value
|
|
133
|
+
// a fast machine can finish the clip before the cancel lands; then there is nothing to check
|
|
134
|
+
} catch {
|
|
135
|
+
XCTAssertEqual((error as? TranscodeError)?.code, .cancelled)
|
|
136
|
+
XCTAssertFalse(FileManager.default.fileExists(atPath: out.path), "a cancelled transcode leaves no file")
|
|
137
|
+
}
|
|
138
|
+
// and the transcoder is free again
|
|
139
|
+
_ = try await transcoder.transcode(TranscodeOptions(source: fixture("hdr-hlg.mov"), destination: out))
|
|
140
|
+
transcoder.cancel() // with nothing running, nothing happens
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func assertFails(
|
|
144
|
+
_ code: TranscodeErrorCode,
|
|
145
|
+
_ options: TranscodeOptions,
|
|
146
|
+
with transcoder: Transcoder = Transcoder(),
|
|
147
|
+
line: UInt = #line
|
|
148
|
+
) async {
|
|
149
|
+
do {
|
|
150
|
+
_ = try await transcoder.transcode(options)
|
|
151
|
+
XCTFail("expected \(code.rawValue)", line: line)
|
|
152
|
+
} catch {
|
|
153
|
+
XCTAssertEqual((error as? TranscodeError)?.code, code, "\(error)", line: line)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
final class Recorder: @unchecked Sendable {
|
|
159
|
+
private let lock = NSLock()
|
|
160
|
+
private var list: [Double] = []
|
|
161
|
+
func add(_ value: Double) {
|
|
162
|
+
lock.lock()
|
|
163
|
+
list.append(value)
|
|
164
|
+
lock.unlock()
|
|
165
|
+
}
|
|
166
|
+
var values: [Double] {
|
|
167
|
+
lock.lock()
|
|
168
|
+
defer { lock.unlock() }
|
|
169
|
+
return list
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// What a written MP4 says about itself, read with AVFoundation and from its top-level boxes.
|
|
174
|
+
struct Facts {
|
|
175
|
+
var videoCodec: FourCharCode?
|
|
176
|
+
var avcProfile: UInt8?
|
|
177
|
+
var storedSize = CGSize.zero
|
|
178
|
+
var transform = CGAffineTransform.identity
|
|
179
|
+
var primaries: String?
|
|
180
|
+
var transfer: String?
|
|
181
|
+
var matrix: String?
|
|
182
|
+
var audioFormat: AudioFormatID?
|
|
183
|
+
var audioChannels: UInt32?
|
|
184
|
+
var audioSampleRate: Float64?
|
|
185
|
+
var topLevelAtoms: [String] = []
|
|
186
|
+
|
|
187
|
+
static func read(_ url: URL) async throws -> Facts {
|
|
188
|
+
var facts = Facts()
|
|
189
|
+
let asset = AVURLAsset(url: url)
|
|
190
|
+
if let video = try await asset.loadTracks(withMediaType: .video).first {
|
|
191
|
+
facts.storedSize = try await video.load(.naturalSize)
|
|
192
|
+
facts.transform = try await video.load(.preferredTransform)
|
|
193
|
+
if let description = try await video.load(.formatDescriptions).first {
|
|
194
|
+
facts.videoCodec = CMFormatDescriptionGetMediaSubType(description)
|
|
195
|
+
let extensions = CMFormatDescriptionGetExtensions(description) as? [String: Any] ?? [:]
|
|
196
|
+
facts.primaries = extensions[kCMFormatDescriptionExtension_ColorPrimaries as String] as? String
|
|
197
|
+
facts.transfer = extensions[kCMFormatDescriptionExtension_TransferFunction as String] as? String
|
|
198
|
+
facts.matrix = extensions[kCMFormatDescriptionExtension_YCbCrMatrix as String] as? String
|
|
199
|
+
let atoms = extensions[kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms as String] as? [String: Any]
|
|
200
|
+
if let avcC = atoms?["avcC"] as? Data, avcC.count > 1 {
|
|
201
|
+
facts.avcProfile = avcC[avcC.startIndex + 1]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if let audio = try await asset.loadTracks(withMediaType: .audio).first,
|
|
206
|
+
let description = try await audio.load(.formatDescriptions).first,
|
|
207
|
+
let basic = CMAudioFormatDescriptionGetStreamBasicDescription(description)?.pointee {
|
|
208
|
+
facts.audioFormat = basic.mFormatID
|
|
209
|
+
facts.audioChannels = basic.mChannelsPerFrame
|
|
210
|
+
facts.audioSampleRate = basic.mSampleRate
|
|
211
|
+
}
|
|
212
|
+
facts.topLevelAtoms = try topLevelAtoms(url)
|
|
213
|
+
return facts
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static func topLevelAtoms(_ url: URL) throws -> [String] {
|
|
217
|
+
let data = try Data(contentsOf: url)
|
|
218
|
+
var atoms: [String] = []
|
|
219
|
+
var offset = 0
|
|
220
|
+
while offset + 8 <= data.count {
|
|
221
|
+
var size = data[offset..<offset + 4].reduce(0) { $0 << 8 | UInt64($1) }
|
|
222
|
+
let type = String(decoding: data[offset + 4..<offset + 8], as: UTF8.self)
|
|
223
|
+
if size == 1, offset + 16 <= data.count {
|
|
224
|
+
size = data[offset + 8..<offset + 16].reduce(0) { $0 << 8 | UInt64($1) }
|
|
225
|
+
} else if size == 0 {
|
|
226
|
+
size = UInt64(data.count - offset)
|
|
227
|
+
}
|
|
228
|
+
atoms.append(type)
|
|
229
|
+
guard size >= 8 else { break }
|
|
230
|
+
offset += Int(size)
|
|
231
|
+
}
|
|
232
|
+
return atoms
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import VideoTranscoderPlugin
|
|
3
|
+
import VideoTranscoderCore
|
|
4
|
+
|
|
5
|
+
/// The bridge side of the plugin: its name and methods, and how a call's options and a finished transcode cross the bridge. The
|
|
6
|
+
/// transcoding itself is tested in VideoTranscoderCoreTests, which also run on a Mac without a simulator.
|
|
7
|
+
final class VideoTranscoderPluginTests: XCTestCase {
|
|
8
|
+
func testRegistersTheMethodsOfTheTypeScriptAPI() {
|
|
9
|
+
let plugin = VideoTranscoderPlugin()
|
|
10
|
+
XCTAssertEqual(plugin.jsName, "VideoTranscoder")
|
|
11
|
+
XCTAssertEqual(plugin.pluginMethods.map(\.name), ["transcode", "cancel"])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func testKeepsTheStringKeysOfTheCallOptions() {
|
|
15
|
+
let raw = VideoTranscoderPlugin.raw(["source": "/a.mov", "maxSize": 640, 1: "dropped"])
|
|
16
|
+
XCTAssertEqual(raw.count, 2)
|
|
17
|
+
XCTAssertEqual(try TranscodeOptions.parse(raw).maxSize, 640)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testResultCarriesEveryFieldOfTheTypeScriptResult() {
|
|
21
|
+
let url = URL(fileURLWithPath: "/tmp/out put.mp4")
|
|
22
|
+
let result = VideoTranscoderPlugin.result(
|
|
23
|
+
TranscodeOutcome(url: url, width: 720, height: 1280, durationMs: 3000, size: 12345)
|
|
24
|
+
)
|
|
25
|
+
XCTAssertEqual(result["path"] as? String, "/tmp/out put.mp4")
|
|
26
|
+
XCTAssertEqual(result["uri"] as? String, "file:///tmp/out%20put.mp4")
|
|
27
|
+
XCTAssertEqual(result["width"] as? Int, 720)
|
|
28
|
+
XCTAssertEqual(result["height"] as? Int, 1280)
|
|
29
|
+
XCTAssertEqual(result["durationMs"] as? Int, 3000)
|
|
30
|
+
XCTAssertEqual(result["size"] as? Int, 12345)
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,103 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-video-transcoder",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Transcode a video into an MP4 that web browsers play, on iOS and Android, from a Capacitor app",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapacitorVideoTranscoder.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Maxim Belov",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/MaximBelov/capacitor-video-transcoder.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/MaximBelov/capacitor-video-transcoder/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native",
|
|
31
|
+
"video",
|
|
32
|
+
"transcode",
|
|
33
|
+
"mp4",
|
|
34
|
+
"h264",
|
|
35
|
+
"compress",
|
|
36
|
+
"ffmpeg alternative",
|
|
37
|
+
"media3",
|
|
38
|
+
"avfoundation"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
42
|
+
"verify:ios": "xcodebuild build -scheme CapacitorVideoTranscoder -destination generic/platform=iOS -derivedDataPath build/ios-device",
|
|
43
|
+
"verify:android": "cd android && ./gradlew clean build test",
|
|
44
|
+
"verify:web": "npm run build",
|
|
45
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
46
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
47
|
+
"eslint": "eslint . --ext ts",
|
|
48
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
49
|
+
"swiftlint": "node-swiftlint",
|
|
50
|
+
"docgen": "docgen --api VideoTranscoderPlugin --output-readme README.md --output-json dist/docs.json",
|
|
51
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
|
|
52
|
+
"clean": "rimraf ./dist",
|
|
53
|
+
"watch": "tsc --watch",
|
|
54
|
+
"prepublishOnly": "npm run build",
|
|
55
|
+
"test": "npm run test:unit",
|
|
56
|
+
"test:unit": "vitest run",
|
|
57
|
+
"test:unit:coverage": "vitest run --coverage",
|
|
58
|
+
"test:android": "cd android && ./gradlew test",
|
|
59
|
+
"test:core": "swift test --package-path ios",
|
|
60
|
+
"test:ios": "./scripts/test-ios.sh",
|
|
61
|
+
"fixtures": "./test/fixtures/make-fixtures.sh",
|
|
62
|
+
"example:install": "cd example-app && npm install --no-audit --no-fund",
|
|
63
|
+
"example:android:build": "npm run example:install && cd example-app && npm run build && npx cap sync android && cd android && ./gradlew assembleDebug",
|
|
64
|
+
"example:ios:build": "npm run example:install && cd example-app && npm run build && npx cap sync ios && xcodebuild build -project ios/App/App.xcodeproj -scheme App -destination generic/platform=iOS\\ Simulator -derivedDataPath ../build/example-ios CODE_SIGNING_ALLOWED=NO"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@capacitor/android": "^8.0.0",
|
|
68
|
+
"@capacitor/core": "^8.0.0",
|
|
69
|
+
"@capacitor/docgen": "^0.3.1",
|
|
70
|
+
"@capacitor/ios": "^8.0.0",
|
|
71
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
72
|
+
"@ionic/prettier-config": "^4.0.0",
|
|
73
|
+
"@ionic/swiftlint-config": "^2.0.0",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
75
|
+
"eslint": "^8.57.1",
|
|
76
|
+
"prettier": "^3.6.2",
|
|
77
|
+
"prettier-plugin-java": "^2.7.7",
|
|
78
|
+
"rimraf": "^6.1.0",
|
|
79
|
+
"rollup": "^4.53.2",
|
|
80
|
+
"swiftlint": "^2.0.0",
|
|
81
|
+
"typescript": "^5.9.3",
|
|
82
|
+
"vitest": "^4.1.12"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"@capacitor/core": ">=8.0.0"
|
|
86
|
+
},
|
|
87
|
+
"prettier": "@ionic/prettier-config",
|
|
88
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
89
|
+
"eslintConfig": {
|
|
90
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
91
|
+
},
|
|
92
|
+
"capacitor": {
|
|
93
|
+
"ios": {
|
|
94
|
+
"src": "ios"
|
|
95
|
+
},
|
|
96
|
+
"android": {
|
|
97
|
+
"src": "android"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"engines": {
|
|
101
|
+
"node": ">=20"
|
|
102
|
+
}
|
|
103
|
+
}
|