cocoapods-lynx-capacitor 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7ed75fbedf98ec911a750917b50637db0c6c9eb60b21f2bca8a8d894c7f39e0b
4
+ data.tar.gz: 7df28de26c637d5981fa419f820b5935ff41bea782a4231d439d3439bb2daee7
5
+ SHA512:
6
+ metadata.gz: 4876b9361ec1c6546dd097d90823cf4ada7e9041bdf45c52efc7d62ee457fe135acf5f35fd40ece738c2e14cace70e0a0ec94ef0d01faba8881f0e1706ca3c2a
7
+ data.tar.gz: 9f7641bf54fe442937cd8e2d7030b7ea5e707fecfab727eb1bc180a818c7de6b22418cb75a5dff77efd21082c98497ab096513f775db92e43858c449198d4dd2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xuan Huang and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # cocoapods-lynx-capacitor
2
+
3
+ A CocoaPods plugin that links Capacitor plugin pods out of `node_modules`, so a
4
+ Lynx app running [lynx-capacitor](../../README.md) never names them in its
5
+ Podfile.
6
+
7
+ ```ruby
8
+ plugin 'cocoapods-lynx-library'
9
+ plugin 'cocoapods-lynx-capacitor'
10
+
11
+ target 'App' do
12
+ use_lynx_library! # the Capacitor bridge, as a Lynx native library
13
+ use_capacitor_plugins! # the Capacitor plugins it dispatches to
14
+ end
15
+ ```
16
+
17
+ `npm install @capacitor/camera` + `pod install` is now the whole install
18
+ procedure for a new plugin.
19
+
20
+ ## Why this exists
21
+
22
+ Lynx Autolink links Lynx native libraries. lynx-capacitor is exactly one of
23
+ those, so `use_lynx_library!` covers the bridge. It does not cover the plugin
24
+ pods the bridge dispatches to, and two constraints keep them out of
25
+ `LynxCapacitorRuntime.podspec`:
26
+
27
+ - **A podspec cannot declare `:path` dependencies.** External sources are only
28
+ expressible in a Podfile. So plugin pods must be declared there.
29
+ - **CocoaPods trunk is not a usable substitute.** As of Capacitor 8,
30
+ `CapacitorBarcodeScanner`, `CapacitorFileTransfer`, `CapacitorFileViewer`,
31
+ `CapacitorInappbrowser`, `CapacitorLocalLlm` and `CapacitorPrivacyScreen` are
32
+ not on trunk at all; `CapacitorBackgroundRunner` is at 1.0.5 against npm
33
+ 3.0.0 and `CapacitorGoogleMaps` at 5.3.2 against npm 8.0.1; and none of the
34
+ `@capacitor-community/*` plugins are published. npm is the real distribution
35
+ channel, which is why the Capacitor CLI generates `:path` lines too.
36
+
37
+ So something has to walk `node_modules` and emit those lines. Capacitor's CLI
38
+ does it by rewriting the text between `def capacitor_pods` and `end` during
39
+ `npx cap sync`. Doing it as a CocoaPods plugin instead means the Podfile is
40
+ never rewritten, there is no marker block to preserve, and `pod install` stays
41
+ the only command.
42
+
43
+ ## Behaviour
44
+
45
+ **Detection** matches the Capacitor CLI: a package is a plugin when its
46
+ `package.json` has a `capacitor` key. Official and community plugins are
47
+ treated identically, and a plugin written in your own repo is picked up as soon
48
+ as it is a dependency.
49
+
50
+ **Podspec resolution** looks for `*.podspec` at the package root, where
51
+ Capacitor plugins put it, then falls back to the `capacitor.ios.src` directory.
52
+ The pod name is read from `s.name` inside the podspec, not from the filename.
53
+
54
+ **The runtime** — `Capacitor` and `CapacitorCordova` from `@capacitor/ios` — is
55
+ linked by default, since every plugin podspec carries `s.dependency 'Capacitor'`
56
+ and that can only be satisfied by a local path. Pass `:runtime => false` to
57
+ declare it yourself.
58
+
59
+ **Paths** are the `node_modules` paths, not symlink targets. Under pnpm the real
60
+ path embeds the version and a dependency hash
61
+ (`.pnpm/@capacitor+device@8.0.3_@capacitor+core@8.4.2/…`), which would rewrite
62
+ `Podfile.lock` on every bump. Symlinks are still resolved for identity, so one
63
+ package reachable under two names is declared once.
64
+
65
+ **Nothing is dropped silently.** A package with a `capacitor` key but no iOS
66
+ podspec (`@capacitor/motion` today), a missing `@capacitor/ios`, or two distinct
67
+ packages claiming one pod name each produce a warning naming the package and the
68
+ reason.
69
+
70
+ ## Options
71
+
72
+ | Option | Default | Meaning |
73
+ |--------|---------|---------|
74
+ | `:root` | Podfile directory | Where to start looking for `node_modules`. Walks up 6 levels, matching `cocoapods-lynx-library`. Pass this when the app's `package.json` is not on the Podfile's ancestor path. |
75
+ | `:include` | all | Allowlist of npm package names. Mirrors Capacitor's `ios.includePlugins`. |
76
+ | `:exclude` | none | Denylist of npm package names. |
77
+ | `:runtime` | `true` | Also link `Capacitor` and `CapacitorCordova`. |
78
+
79
+ ## Tests
80
+
81
+ ```bash
82
+ ruby spec/autolink_spec.rb
83
+ ```
84
+
85
+ No CocoaPods or macOS needed — the suite builds `node_modules` fixtures in a
86
+ temp dir and asserts against a fake Podfile.
@@ -0,0 +1,251 @@
1
+ # Resolves Capacitor plugin pods out of node_modules so a Podfile does not have
2
+ # to name them one by one.
3
+ #
4
+ # This is the Capacitor half of the lynx-capacitor autolink story.
5
+ # `cocoapods-lynx-library` links the Lynx native library that hosts the bridge;
6
+ # this plugin links the Capacitor plugin pods the bridge dispatches to. Neither
7
+ # can do the other's job: a podspec cannot declare `:path` dependencies, so the
8
+ # plugin pods have to be named in the Podfile, and most Capacitor 8 plugins are
9
+ # either missing from CocoaPods trunk or lag npm badly, so a version-based
10
+ # dependency is not a substitute.
11
+ #
12
+ # Plugin detection matches the Capacitor CLI: a package is a plugin when its
13
+ # package.json carries a `capacitor` key.
14
+
15
+ require 'json'
16
+
17
+ module Capacitor
18
+ module Lynx
19
+ # package_dir is the path as found under node_modules and is what lands in
20
+ # the Podfile. resolved_dir follows symlinks and is used only for identity:
21
+ # under pnpm the real path embeds the version and a dependency hash, so
22
+ # declaring it would rewrite Podfile.lock on every version bump.
23
+ PluginInfo = Struct.new(:npm_name, :package_dir, :resolved_dir, :pod_name, :podspec_path)
24
+ SkippedPackage = Struct.new(:npm_name, :reason)
25
+
26
+ # How far up from the starting directory to look for node_modules. Matches
27
+ # cocoapods-lynx-library so both plugins agree on what "the app" is.
28
+ NODE_MODULES_SEARCH_DEPTH = 6
29
+
30
+ # @capacitor/ios is the runtime, not a plugin: it has no `capacitor` key in
31
+ # package.json and ships two podspecs at its package root.
32
+ RUNTIME_PACKAGE = '@capacitor/ios'.freeze
33
+ RUNTIME_POD_NAMES = %w[Capacitor CapacitorCordova].freeze
34
+
35
+ class Autolink
36
+ class << self
37
+ # Declares every discovered Capacitor pod on the given Podfile.
38
+ #
39
+ # @param podfile [Pod::Podfile::TargetDefinition] receives `pod` calls
40
+ # @option options [String] :root where to start looking for node_modules
41
+ # @option options [Array<String>] :include only link these npm packages
42
+ # @option options [Array<String>] :exclude never link these npm packages
43
+ # @option options [Boolean] :runtime also link Capacitor/CapacitorCordova
44
+ # @return [Array<PluginInfo>] what was linked, in declaration order
45
+ def install!(podfile, options = {})
46
+ root = File.expand_path(options[:root] || Dir.pwd)
47
+ resolution = resolve(root, options)
48
+
49
+ resolution[:skipped].each do |skipped|
50
+ warn_once("[lynx-capacitor] skipping #{skipped.npm_name}: #{skipped.reason}")
51
+ end
52
+
53
+ resolution[:pods].each do |plugin|
54
+ podfile.pod plugin.pod_name, :path => plugin.package_dir
55
+ end
56
+ resolution[:pods]
57
+ end
58
+
59
+ # The pure half of install!, so callers (and tests) can inspect what
60
+ # would be declared without a Podfile.
61
+ #
62
+ # @return [Hash] :pods => Array<PluginInfo>, :skipped => Array<SkippedPackage>
63
+ def resolve(root, options = {})
64
+ root = File.expand_path(root)
65
+ packages = discover_packages(root)
66
+
67
+ pods = []
68
+ skipped = []
69
+
70
+ if options.fetch(:runtime, true)
71
+ runtime = packages[RUNTIME_PACKAGE]
72
+ if runtime
73
+ pods.concat(runtime_pods(runtime))
74
+ else
75
+ skipped << SkippedPackage.new(
76
+ RUNTIME_PACKAGE,
77
+ 'not installed; declare `pod \'Capacitor\'` yourself or pass :runtime => false'
78
+ )
79
+ end
80
+ end
81
+
82
+ plugin_packages(packages, options).each do |npm_name, package_dir|
83
+ podspec_path = find_podspec(package_dir)
84
+ if podspec_path.nil?
85
+ # @capacitor/motion is the standing example: published with a
86
+ # `capacitor` key but no iOS implementation yet.
87
+ skipped << SkippedPackage.new(npm_name, 'no iOS podspec in the package')
88
+ next
89
+ end
90
+ pods << PluginInfo.new(npm_name, package_dir, real_path(package_dir),
91
+ pod_name_from(podspec_path), podspec_path)
92
+ end
93
+
94
+ { :pods => dedupe(pods, skipped), :skipped => skipped }
95
+ end
96
+
97
+ private
98
+
99
+ def plugin_packages(packages, options)
100
+ included = normalize_list(options[:include])
101
+ excluded = normalize_list(options[:exclude])
102
+
103
+ packages.reject { |npm_name, _dir| npm_name == RUNTIME_PACKAGE }
104
+ .select { |npm_name, dir| capacitor_plugin?(dir) && npm_name != RUNTIME_PACKAGE }
105
+ .reject { |npm_name, _dir| excluded.include?(npm_name) }
106
+ .select { |npm_name, _dir| included.empty? || included.include?(npm_name) }
107
+ .sort_by { |npm_name, _dir| npm_name }
108
+ end
109
+
110
+ def normalize_list(value)
111
+ Array(value).map(&:to_s)
112
+ end
113
+
114
+ # A package is a Capacitor plugin when package.json has a `capacitor`
115
+ # key -- the same test the Capacitor CLI applies.
116
+ def capacitor_plugin?(package_dir)
117
+ manifest = read_package_json(package_dir)
118
+ return false if manifest.nil?
119
+
120
+ capacitor = manifest['capacitor']
121
+ return false unless capacitor.is_a?(Hash)
122
+
123
+ # An `ios` entry is not required to be present for the pod to exist,
124
+ # but a package that opts out of iOS entirely has nothing to link.
125
+ !capacitor.key?('ios') || capacitor['ios'].is_a?(Hash)
126
+ end
127
+
128
+ def runtime_pods(package_dir)
129
+ RUNTIME_POD_NAMES.filter_map do |pod_name|
130
+ podspec_path = File.join(package_dir, "#{pod_name}.podspec")
131
+ next unless File.file?(podspec_path)
132
+ PluginInfo.new(RUNTIME_PACKAGE, package_dir, real_path(package_dir),
133
+ pod_name, podspec_path)
134
+ end
135
+ end
136
+
137
+ # Capacitor plugins put `<PodName>.podspec` at the package root. The
138
+ # `capacitor.ios.src` directory holds sources, not the podspec, but it
139
+ # is checked as a fallback for plugins that lay themselves out that way.
140
+ def find_podspec(package_dir)
141
+ candidates = Dir[File.join(package_dir, '*.podspec')].sort
142
+ return candidates.first if candidates.any?
143
+
144
+ manifest = read_package_json(package_dir)
145
+ src = manifest && manifest.dig('capacitor', 'ios', 'src')
146
+ return nil if src.nil?
147
+
148
+ src_dir = File.expand_path(src, package_dir)
149
+ return nil unless File.directory?(src_dir)
150
+
151
+ Dir[File.join(src_dir, '*.podspec')].sort.first
152
+ end
153
+
154
+ def pod_name_from(podspec_path)
155
+ match = File.read(podspec_path).match(/\.name\s*=\s*['"]([^'"]+)['"]/)
156
+ match ? match[1] : File.basename(podspec_path, '.podspec')
157
+ end
158
+
159
+ # CocoaPods raises on a duplicate pod name, and the same plugin can be
160
+ # reachable through more than one node_modules. Nearest wins.
161
+ def dedupe(pods, skipped)
162
+ seen = {}
163
+ pods.each_with_object([]) do |plugin, kept|
164
+ existing = seen[plugin.pod_name]
165
+ if existing
166
+ if existing.resolved_dir != plugin.resolved_dir
167
+ skipped << SkippedPackage.new(
168
+ plugin.npm_name,
169
+ "#{plugin.pod_name} already linked from #{existing.package_dir}"
170
+ )
171
+ end
172
+ next
173
+ end
174
+ seen[plugin.pod_name] = plugin
175
+ kept << plugin
176
+ end
177
+ end
178
+
179
+ # Maps npm package name => realpath of its directory, nearest first.
180
+ def discover_packages(start_dir)
181
+ packages = {}
182
+ node_modules_dirs(start_dir).each do |node_modules|
183
+ each_package_dir(node_modules) do |npm_name, package_dir|
184
+ packages[npm_name] ||= package_dir
185
+ end
186
+ end
187
+ packages
188
+ end
189
+
190
+ def node_modules_dirs(start_dir)
191
+ dirs = []
192
+ current = File.expand_path(start_dir)
193
+ NODE_MODULES_SEARCH_DEPTH.times do
194
+ candidate = File.join(current, 'node_modules')
195
+ dirs << candidate if File.directory?(candidate)
196
+ parent = File.dirname(current)
197
+ break if parent == current
198
+ current = parent
199
+ end
200
+ dirs.uniq
201
+ end
202
+
203
+ def each_package_dir(node_modules)
204
+ Dir.children(node_modules).sort.each do |name|
205
+ next if name.start_with?('.')
206
+ path = File.join(node_modules, name)
207
+ next unless File.directory?(path)
208
+
209
+ if name.start_with?('@')
210
+ Dir.children(path).sort.each do |scoped_name|
211
+ scoped_path = File.join(path, scoped_name)
212
+ next unless File.directory?(scoped_path)
213
+ yield "#{name}/#{scoped_name}", scoped_path
214
+ end
215
+ else
216
+ yield name, path
217
+ end
218
+ end
219
+ end
220
+
221
+ # Identity only -- see PluginInfo. pnpm and npm workspaces symlink
222
+ # packages, so two node_modules entries can be the same package.
223
+ def real_path(path)
224
+ File.realpath(path)
225
+ rescue SystemCallError
226
+ File.expand_path(path)
227
+ end
228
+
229
+ def read_package_json(package_dir)
230
+ manifest_path = File.join(package_dir, 'package.json')
231
+ return nil unless File.file?(manifest_path)
232
+ JSON.parse(File.read(manifest_path))
233
+ rescue JSON::ParserError
234
+ nil
235
+ end
236
+
237
+ def warn_once(message)
238
+ @warned ||= {}
239
+ return if @warned[message]
240
+ @warned[message] = true
241
+
242
+ if defined?(Pod::UI)
243
+ Pod::UI.warn(message)
244
+ else
245
+ Kernel.warn(message)
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1 @@
1
+ require 'capacitor/lynx/autolink'
@@ -0,0 +1,35 @@
1
+ require 'cocoapods-lynx-capacitor'
2
+
3
+ module Pod
4
+ class Podfile
5
+ module CapacitorLynxDSL
6
+ # Links every Capacitor plugin found in node_modules.
7
+ #
8
+ # plugin 'cocoapods-lynx-capacitor'
9
+ #
10
+ # target 'App' do
11
+ # use_lynx_library! # the bridge
12
+ # use_capacitor_plugins! # the plugins it dispatches to
13
+ # end
14
+ #
15
+ # @option options [String] :root where to start looking for node_modules.
16
+ # Defaults to the Podfile's directory. Pass this when the app's
17
+ # package.json does not sit on the Podfile's ancestor path.
18
+ # @option options [Array<String>] :include only link these npm packages
19
+ # @option options [Array<String>] :exclude never link these npm packages
20
+ # @option options [Boolean] :runtime also link Capacitor and
21
+ # CapacitorCordova from @capacitor/ios. Defaults to true; every plugin
22
+ # podspec depends on Capacitor, and that dependency can only be
23
+ # satisfied by a local path.
24
+ def use_capacitor_plugins!(options = {})
25
+ ::Capacitor::Lynx::Autolink.install!(self, options)
26
+ end
27
+ end
28
+
29
+ include CapacitorLynxDSL
30
+
31
+ module DSL
32
+ include CapacitorLynxDSL
33
+ end
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cocoapods-lynx-capacitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Xuan Huang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cocoapods
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.16'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ description: |
34
+ A CocoaPods plugin that resolves Capacitor plugin pods out of node_modules,
35
+ so a Lynx app running lynx-capacitor does not have to name each plugin in
36
+ its Podfile. Companion to cocoapods-lynx-library, which links the Lynx
37
+ native library hosting the Capacitor bridge.
38
+ email:
39
+ - 5563315+Huxpro@users.noreply.github.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - LICENSE
45
+ - README.md
46
+ - lib/capacitor/lynx/autolink.rb
47
+ - lib/cocoapods-lynx-capacitor.rb
48
+ - lib/cocoapods_plugin.rb
49
+ homepage: https://github.com/huxpro/lynx-capacitor
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ source_code_uri: https://github.com/huxpro/lynx-capacitor
54
+ rubygems_mfa_required: 'true'
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '2.7'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.3.15
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Links Capacitor plugin pods from node_modules into a Lynx app.
74
+ test_files: []