cocoapods-lynx-extension 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: e469114efbb082e6d6f41b033c27cad07a9040c034f5cd32ec7361225eb8a293
4
+ data.tar.gz: daa9d4933feb84294aa4efb38c60005d8105eb3551f0608718caddb53561ff93
5
+ SHA512:
6
+ metadata.gz: 3f75e2b6cea85991218a8dea036ec1fcb5911162cfc7dbc85bd699063d1ecab663976534e7a9f8bc833a777468550b5b4d3907f95ef258a39fed8fa76ddacd0f
7
+ data.tar.gz: 5b263cc73806034713c757d1bd8fb5f2ad45335813ecbf78a210ff53c703ee6f8c0e8f8afe4c522d502109b888dd0df3973f95dd19cc71b3b83a25d07581ed8f
@@ -0,0 +1,5 @@
1
+ # Copyright 2026 The Lynx Authors. All rights reserved.
2
+ # Licensed under the Apache License Version 2.0 that can be found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ require 'lynx/extension/autolink'
@@ -0,0 +1,21 @@
1
+ # Copyright 2026 The Lynx Authors. All rights reserved.
2
+ # Licensed under the Apache License Version 2.0 that can be found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ require 'cocoapods-lynx-extension'
6
+
7
+ module Pod
8
+ class Podfile
9
+ module LynxExtensionDSL
10
+ def use_lynx_extension!(options = {})
11
+ Lynx::Extension::Autolink.install!(self, options)
12
+ end
13
+ end
14
+
15
+ include LynxExtensionDSL
16
+
17
+ module DSL
18
+ include LynxExtensionDSL
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,260 @@
1
+ # Copyright 2026 The Lynx Authors. All rights reserved.
2
+ # Licensed under the Apache License Version 2.0 that can be found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ require 'fileutils'
6
+ require 'json'
7
+
8
+ module Lynx
9
+ module Extension
10
+ ExtensionInfo = Struct.new(:npm_name, :package_dir, :manifest_file, :source_dir, :podspec_path)
11
+ ComponentInfo = Struct.new(:kind, :name, :class_name)
12
+
13
+ class Autolink
14
+ REGISTRY_CLASS_NAME = 'LynxGeneratedExtensionRegistry'
15
+
16
+ class << self
17
+ def install!(podfile, options = {})
18
+ start_dir = File.expand_path(options[:root] || Dir.pwd)
19
+ output_dir = File.expand_path(options[:output_dir] || 'generated/lynx-extension', start_dir)
20
+ extensions = scan(start_dir)
21
+ extensions.each do |extension|
22
+ pod_name = pod_name_from_podspec(extension.podspec_path)
23
+ podfile.pod pod_name, :path => File.dirname(extension.podspec_path)
24
+ end
25
+ generate_registry(output_dir, extensions)
26
+ podfile.pod 'LynxExtensionRegistry', :path => output_dir
27
+ extensions
28
+ end
29
+
30
+ def scan(start_dir)
31
+ node_modules_dirs(start_dir).flat_map do |node_modules|
32
+ manifest_files(node_modules).map { |manifest| parse_manifest(manifest) }.compact
33
+ end.sort_by(&:npm_name)
34
+ end
35
+
36
+ def generate_registry(output_dir, extensions)
37
+ FileUtils.mkdir_p(output_dir)
38
+ components = extensions.flat_map { |extension| scan_components(extension.source_dir) }
39
+ File.write(File.join(output_dir, "#{REGISTRY_CLASS_NAME}.h"), header_source)
40
+ File.write(File.join(output_dir, "#{REGISTRY_CLASS_NAME}.m"),
41
+ implementation_source(components))
42
+ File.write(File.join(output_dir, 'LynxExtensionRegistry.podspec'), podspec_source)
43
+ components
44
+ end
45
+
46
+ def scan_components(source_dir)
47
+ source_files(source_dir).flat_map { |file| scan_source_file(file) }
48
+ end
49
+
50
+ private
51
+
52
+ def node_modules_dirs(start_dir)
53
+ dirs = []
54
+ current = File.expand_path(start_dir)
55
+ 6.times do
56
+ candidate = File.join(current, 'node_modules')
57
+ dirs << candidate if File.directory?(candidate)
58
+ parent = File.dirname(current)
59
+ break if parent == current
60
+ current = parent
61
+ end
62
+ dirs.uniq
63
+ end
64
+
65
+ def manifest_files(node_modules)
66
+ manifests = []
67
+ Dir.children(node_modules).sort.each do |name|
68
+ next if name.start_with?('.')
69
+ path = File.join(node_modules, name)
70
+ next unless File.directory?(path)
71
+ if name.start_with?('@')
72
+ Dir.children(path).sort.each do |scoped_name|
73
+ add_manifest(manifests, File.join(path, scoped_name))
74
+ end
75
+ else
76
+ add_manifest(manifests, path)
77
+ end
78
+ end
79
+ manifests
80
+ end
81
+
82
+ def add_manifest(manifests, package_dir)
83
+ manifest = File.join(package_dir, 'lynx.ext.json')
84
+ manifests << manifest if File.file?(manifest)
85
+ end
86
+
87
+ def parse_manifest(manifest_file)
88
+ json = JSON.parse(File.read(manifest_file))
89
+ ios = json.dig('platforms', 'ios')
90
+ return nil if ios.nil?
91
+ raise "Invalid ios platform entry in #{manifest_file}" unless ios.is_a?(Hash)
92
+
93
+ package_dir = File.dirname(manifest_file)
94
+ package_realpath = File.realpath(package_dir)
95
+ source_dir_name = ios['sourceDir'] || 'ios'
96
+ source_dir = resolve_package_path(package_realpath, source_dir_name, manifest_file, 'sourceDir')
97
+ raise "iOS sourceDir '#{source_dir_name}' does not exist for #{manifest_file}" unless
98
+ File.directory?(source_dir)
99
+
100
+ podspec_path = if ios['podspecPath']
101
+ resolve_package_path(package_realpath, ios['podspecPath'], manifest_file, 'podspecPath')
102
+ else
103
+ path = podspec_files(source_dir, package_realpath).first
104
+ validate_package_path(package_realpath, path, manifest_file, 'podspecPath') if path
105
+ end
106
+ raise "No iOS podspec found for #{manifest_file}" unless
107
+ podspec_path && File.file?(podspec_path)
108
+
109
+ npm_name = File.basename(package_dir)
110
+ parent_name = File.basename(File.dirname(package_dir))
111
+ npm_name = "#{parent_name}/#{npm_name}" if parent_name.start_with?('@')
112
+ ExtensionInfo.new(npm_name, package_dir, manifest_file, source_dir, podspec_path)
113
+ rescue JSON::ParserError => e
114
+ raise "Failed to parse #{manifest_file}: #{e.message}"
115
+ end
116
+
117
+ def pod_name_from_podspec(podspec_path)
118
+ content = File.read(podspec_path)
119
+ match = content.match(/\.name\s*=\s*['"]([^'"]+)['"]/)
120
+ raise "Unable to read pod name from #{podspec_path}" unless match
121
+ match[1]
122
+ end
123
+
124
+ def resolve_package_path(package_realpath, configured_path, manifest_file, field_name)
125
+ path = File.expand_path(configured_path, package_realpath)
126
+ validate_package_path(package_realpath, path, manifest_file, field_name, configured_path)
127
+ end
128
+
129
+ def validate_package_path(package_realpath, path, manifest_file, field_name, configured_path = path)
130
+ path = File.realpath(path) if File.exist?(path)
131
+ return path if package_path?(package_realpath, path)
132
+
133
+ raise "iOS #{field_name} '#{configured_path}' must stay within package directory for #{manifest_file}"
134
+ end
135
+
136
+ def package_path?(package_realpath, path)
137
+ path == package_realpath || path.start_with?("#{package_realpath}#{File::SEPARATOR}")
138
+ end
139
+
140
+ def podspec_files(source_dir, package_realpath)
141
+ files = []
142
+ dirs = [source_dir]
143
+ until dirs.empty?
144
+ dir = dirs.pop
145
+ Dir.children(dir).each do |name|
146
+ path = File.join(dir, name)
147
+ if File.symlink?(path)
148
+ files << path if name.end_with?('.podspec')
149
+ elsif File.directory?(path)
150
+ dirs << path if package_path?(package_realpath, File.realpath(path))
151
+ elsif File.file?(path) && name.end_with?('.podspec')
152
+ files << path
153
+ end
154
+ end
155
+ end
156
+ files.sort
157
+ end
158
+
159
+ def source_files(source_dir)
160
+ Dir[File.join(source_dir, '**/*.{h,m,mm,swift}')].sort
161
+ end
162
+
163
+ def scan_source_file(file)
164
+ content = File.read(file)
165
+ components = []
166
+ content.scan(/@implementation\s+([A-Za-z_][A-Za-z0-9_]*)(.*?)(?=@implementation|\z)/m) do
167
+ |class_name, body|
168
+ body.scan(/LYNX_LAZY_REGISTER_UI\(\s*@?"([^"]+)"\s*\)/) do |name|
169
+ components << ComponentInfo.new(:ui, name.first, class_name)
170
+ end
171
+ body.scan(/LYNX_LAZY_REGISTER_SHADOW_NODE\(\s*@?"([^"]+)"\s*\)/) do |name|
172
+ components << ComponentInfo.new(:shadow_node, name.first, class_name)
173
+ end
174
+ body.scan(/LYNX_LAZY_REGISTER_RENDERER_HOST\(\s*@?"([^"]+)"\s*\)/) do |name|
175
+ components << ComponentInfo.new(:renderer_host, name.first, class_name)
176
+ end
177
+ end
178
+ content.scan(/@?LynxAutolinkUI\(\s*@?"([^"]+)"\s*\)\s*@implementation\s+([A-Za-z_][A-Za-z0-9_]*)/) do
179
+ |name, class_name|
180
+ components << ComponentInfo.new(:ui, name, class_name)
181
+ end
182
+ content.scan(/@?LynxAutolinkNativeModule\(\s*@?"([^"]+)"\s*\)\s*@(implementation|interface)\s+([A-Za-z_][A-Za-z0-9_]*)/) do
183
+ |name, _declaration, class_name|
184
+ components << ComponentInfo.new(:native_module, name, class_name)
185
+ end
186
+ content.scan(/@?LynxAutolinkService\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/) do
187
+ |class_name, protocol_name|
188
+ components << ComponentInfo.new(:service, protocol_name, class_name)
189
+ end
190
+ content.scan(/@?LynxServiceRegister\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/) do
191
+ |class_name, protocol_name|
192
+ components << ComponentInfo.new(:service, protocol_name, class_name)
193
+ end
194
+ components.uniq { |component| [component.kind, component.name, component.class_name] }
195
+ end
196
+
197
+ def header_source
198
+ <<~HEADER
199
+ // Generated by cocoapods-lynx-extension. Do not edit.
200
+ #import <Foundation/Foundation.h>
201
+
202
+ @class LynxConfig;
203
+
204
+ @interface #{REGISTRY_CLASS_NAME} : NSObject
205
+ - (void)setup:(LynxConfig *)config;
206
+ @end
207
+ HEADER
208
+ end
209
+
210
+ def implementation_source(components)
211
+ lines = components.map do |component|
212
+ class_expr = "NSClassFromString(@\"#{component.class_name}\")"
213
+ case component.kind
214
+ when :ui
215
+ " if (#{class_expr}) { [config registerUI:#{class_expr} withName:@\"#{component.name}\"]; }"
216
+ when :shadow_node
217
+ " if (#{class_expr}) { [config registerShadowNode:#{class_expr} withName:@\"#{component.name}\"]; }"
218
+ when :renderer_host
219
+ " if (#{class_expr}) { [config.componentRegistry registerRendererHost:#{class_expr} withName:@\"#{component.name}\"]; }"
220
+ when :native_module
221
+ " if (#{class_expr}) { [config registerModule:#{class_expr} withName:@\"#{component.name}\"]; }"
222
+ end
223
+ end.compact.join("\n")
224
+
225
+ <<~IMPL
226
+ // Generated by cocoapods-lynx-extension. Do not edit.
227
+ #import "#{REGISTRY_CLASS_NAME}.h"
228
+ #import <Lynx/LynxConfig.h>
229
+
230
+ @implementation #{REGISTRY_CLASS_NAME}
231
+ - (void)setup:(LynxConfig *)config {
232
+ if (config == nil) {
233
+ return;
234
+ }
235
+ #{lines}
236
+ }
237
+ @end
238
+ IMPL
239
+ end
240
+
241
+ def podspec_source
242
+ <<~PODSPEC
243
+ Pod::Spec.new do |s|
244
+ s.name = 'LynxExtensionRegistry'
245
+ s.version = '0.1.0'
246
+ s.summary = 'Generated Lynx extension registry.'
247
+ s.homepage = 'https://github.com/lynx-family/lynx'
248
+ s.license = 'Apache-2.0'
249
+ s.author = 'Lynx'
250
+ s.source = { :path => '.' }
251
+ s.source_files = '#{REGISTRY_CLASS_NAME}.{h,m}'
252
+ s.dependency 'Lynx'
253
+ s.requires_arc = true
254
+ end
255
+ PODSPEC
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cocoapods-lynx-extension
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Lynx Authors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-18 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.11.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.11.0
27
+ description:
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/cocoapods-lynx-extension.rb
34
+ - lib/cocoapods_plugin.rb
35
+ - lib/lynx/extension/autolink.rb
36
+ homepage:
37
+ licenses:
38
+ - Apache-2.0
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.3.27
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: CocoaPods autolink plugin for Lynx native extensions.
59
+ test_files: []