paquette 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 +7 -0
- data/LICENSE.md +10 -0
- data/README.md +90 -0
- data/lib/paquette/gem_server/directory_gem_repository.rb +154 -0
- data/lib/paquette/gem_server/gem_repacker.rb +170 -0
- data/lib/paquette/gem_server/gem_repository.rb +53 -0
- data/lib/paquette/gem_server/personalizer.rb +66 -0
- data/lib/paquette/gem_server/read_gated_repository.rb +74 -0
- data/lib/paquette/gem_server.rb +393 -0
- data/lib/paquette/npm_repacker.rb +153 -0
- data/lib/paquette/npm_server/directory_npm_repository.rb +127 -0
- data/lib/paquette/npm_server/gated_npm_repository.rb +66 -0
- data/lib/paquette/npm_server/npm_repository.rb +53 -0
- data/lib/paquette/npm_server.rb +98 -0
- data/lib/paquette/routes.rb +78 -0
- data/lib/paquette/subdomain_router.rb +59 -0
- data/lib/paquette/version.rb +3 -0
- data/lib/paquette.rb +12 -0
- data/test/fixtures/gems/minuscule_test/minuscule_test-0.1.0.gem +0 -0
- data/test/fixtures/gems/zip_kit/zip_kit-6.2.0.gem +0 -0
- data/test/fixtures/gems/zip_kit/zip_kit-6.2.1.gem +0 -0
- data/test/fixtures/npm/react-dropzone/react-dropzone-14.3.8.tgz +0 -0
- data/test/gem_server/bundler_install_test.rb +86 -0
- data/test/gem_server/directory_gem_repository_test.rb +208 -0
- data/test/gem_server/gem_repacker_test.rb +289 -0
- data/test/gem_server/personalizer_metadata_test.rb +49 -0
- data/test/gem_server/personalizer_test.rb +66 -0
- data/test/gem_server/read_gated_repository_test.rb +254 -0
- data/test/gem_server/repository_stack_test.rb +71 -0
- data/test/gem_server_test.rb +339 -0
- data/test/npm_repacker_sourcemap_test.rb +108 -0
- data/test/npm_repacker_test.rb +117 -0
- data/test/npm_server_test.rb +140 -0
- data/test/routes_test.rb +142 -0
- data/test/subdomain_router_integration_test.rb +126 -0
- data/test/subdomain_router_test.rb +125 -0
- data/test/test_helper.rb +12 -0
- metadata +117 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "rubygems"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "digest"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
require_relative "routes"
|
|
10
|
+
require_relative "gem_server/directory_gem_repository"
|
|
11
|
+
require_relative "gem_server/read_gated_repository"
|
|
12
|
+
require_relative "gem_server/personalizer"
|
|
13
|
+
|
|
14
|
+
module Paquette
|
|
15
|
+
class GemServer
|
|
16
|
+
@@routes = Routes.draw do |r|
|
|
17
|
+
# Root endpoint
|
|
18
|
+
r.get "/" do
|
|
19
|
+
text_ok("Paquette RubyGems Repository")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# API endpoints
|
|
23
|
+
r.post "/api/v1/gems" do
|
|
24
|
+
handle_push
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
r.delete "/api/v1/gems/yank" do
|
|
28
|
+
handle_yank
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
r.get "/api/v1/versions" do
|
|
32
|
+
handle_versions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
r.get "/api/v1/names" do
|
|
36
|
+
handle_names
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
r.get "/api/v1/search.json" do |query: nil|
|
|
40
|
+
handle_search(query)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Specs endpoints
|
|
44
|
+
r.get "/specs.4.8" do
|
|
45
|
+
handle_specs("4.8")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
r.get "/specs.4.8.gz" do
|
|
49
|
+
handle_specs("4.8.gz")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
r.get "/latest_specs.4.8" do
|
|
53
|
+
handle_latest_specs("4.8")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
r.get "/latest_specs.4.8.gz" do
|
|
57
|
+
handle_latest_specs("4.8.gz")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compact index endpoints
|
|
61
|
+
r.get "/names" do
|
|
62
|
+
handle_compact_names
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
r.get "/versions" do
|
|
66
|
+
handle_compact_versions
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Dynamic endpoints with parameters
|
|
70
|
+
r.get "/info/:gem_name" do |gem_name:|
|
|
71
|
+
info_lines = @repository.compact_info(gem_name)
|
|
72
|
+
|
|
73
|
+
if info_lines.empty?
|
|
74
|
+
not_found("Not Found")
|
|
75
|
+
else
|
|
76
|
+
text_ok(info_lines.join("\n"))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
r.get "/quick/Marshal.4.8/:gem_spec_name.gemspec.rz" do |gem_spec_name:|
|
|
81
|
+
# Parse gem name and version from the spec name (e.g., "zip_kit-6.3.2")
|
|
82
|
+
if (match = gem_spec_name.match(/^(.+?)-(\d+\.\d+\.\d+.*)$/))
|
|
83
|
+
gem_name, version = match[1], match[2]
|
|
84
|
+
|
|
85
|
+
if @repository.gem_exists?(gem_name, version)
|
|
86
|
+
spec = @repository.gem_spec(gem_name, version)
|
|
87
|
+
if spec
|
|
88
|
+
# Marshal the spec and compress it with raw deflate (not gzip)
|
|
89
|
+
marshaled_spec = Marshal.dump(spec)
|
|
90
|
+
compressed_spec = Zlib::Deflate.deflate(marshaled_spec)
|
|
91
|
+
[200, {"Content-Type" => "application/octet-stream"}, [compressed_spec]]
|
|
92
|
+
else
|
|
93
|
+
not_found("Spec not found")
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
not_found("Gem not found: #{gem_name}-#{version}")
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
not_found("Invalid gem spec name: #{gem_spec_name}")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
r.get "/gems/:gem_filename" do |gem_filename:|
|
|
104
|
+
# Extract gem name and version from filename
|
|
105
|
+
if (match = gem_filename.match(/^(.+)-(\d+\.\d+\.\d+.*)\.gem$/))
|
|
106
|
+
gem_name, version = match[1], match[2]
|
|
107
|
+
return not_found("Gem not found or it is not within your license") unless @repository.gem_exists?(gem_name, version)
|
|
108
|
+
|
|
109
|
+
# gem_file_path now automatically returns personalized gem
|
|
110
|
+
gem_path = @repository.gem_file_path(gem_name, version)
|
|
111
|
+
clen = File.size(gem_path).to_s
|
|
112
|
+
hh = {"Content-Type" => "application/octet-stream", "Content-Length" => clen}
|
|
113
|
+
|
|
114
|
+
[200, hh, File.open(gem_path, "rb")]
|
|
115
|
+
else
|
|
116
|
+
not_found("Invalid gem filename")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a gem server backed by `repository`. Reads, writes, and yanks
|
|
122
|
+
# all flow through this one object — whether writes are accepted depends
|
|
123
|
+
# on the wrapper chain the caller assembled. A ReadGatedRepository, for
|
|
124
|
+
# example, refuses add_gem and yank_gem; a bare DirectoryGemRepository
|
|
125
|
+
# accepts both. Callers typically construct the stack per-request so
|
|
126
|
+
# user-specific state (entitlements, license keys) is captured in
|
|
127
|
+
# plain closures rather than stored on the server.
|
|
128
|
+
def initialize(repository)
|
|
129
|
+
@repository = repository
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def call(env)
|
|
133
|
+
request = Rack::Request.new(env)
|
|
134
|
+
route = @@routes.match(request)
|
|
135
|
+
return not_found("Not found") unless route
|
|
136
|
+
|
|
137
|
+
@request = request
|
|
138
|
+
@@routes.perform_action(route, self, request)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def handle_dependencies(request)
|
|
144
|
+
gems = request.params["gems"]
|
|
145
|
+
|
|
146
|
+
# Handle different parameter formats
|
|
147
|
+
if gems.is_a?(String)
|
|
148
|
+
# Split comma-separated gem names
|
|
149
|
+
gems = gems.split(",").map(&:strip)
|
|
150
|
+
elsif gems.nil?
|
|
151
|
+
gems = []
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# If no gems specified, return empty array
|
|
155
|
+
if gems.empty?
|
|
156
|
+
return json_ok([])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
dependencies = []
|
|
160
|
+
gems.each do |gem_name|
|
|
161
|
+
gem_versions = @repository.versions_for_gem(gem_name)
|
|
162
|
+
gem_versions.each do |version|
|
|
163
|
+
gem_dependencies = @repository.gem_dependencies(gem_name, version)
|
|
164
|
+
dependencies << {
|
|
165
|
+
name: gem_name,
|
|
166
|
+
number: version,
|
|
167
|
+
platform: "ruby",
|
|
168
|
+
dependencies: gem_dependencies
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
json_ok(dependencies)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_dependencies_json(request)
|
|
177
|
+
handle_dependencies(request)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def handle_versions
|
|
181
|
+
versions = []
|
|
182
|
+
@repository.gem_versions.each do |name, version|
|
|
183
|
+
spec = @repository.gem_spec(name, version)
|
|
184
|
+
versions << {
|
|
185
|
+
name: name,
|
|
186
|
+
number: version,
|
|
187
|
+
platform: spec.platform.to_s,
|
|
188
|
+
authors: spec.authors,
|
|
189
|
+
info: spec.description || "",
|
|
190
|
+
homepage: spec.homepage || "",
|
|
191
|
+
description: spec.description || "",
|
|
192
|
+
summary: spec.summary || "",
|
|
193
|
+
metadata: spec.metadata || {}
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
json_ok(versions)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def handle_names
|
|
201
|
+
names = @repository.gem_names
|
|
202
|
+
json_ok(names)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def handle_push
|
|
206
|
+
gem_data = @request.body.read
|
|
207
|
+
|
|
208
|
+
spec = @repository.add_gem(gem_data)
|
|
209
|
+
text_ok("Successfully registered gem: #{spec.name}-#{spec.version}")
|
|
210
|
+
rescue ReadGatedRepository::WriteNotAllowed => e
|
|
211
|
+
[403, {"Content-Type" => "text/plain"}, [e.message]]
|
|
212
|
+
rescue DirectoryGemRepository::GemYanked => e
|
|
213
|
+
[403, {"Content-Type" => "text/plain"}, [e.message]]
|
|
214
|
+
rescue DirectoryGemRepository::GemAlreadyExists => e
|
|
215
|
+
[409, {"Content-Type" => "text/plain"}, [e.message]]
|
|
216
|
+
rescue DirectoryGemRepository::InvalidGem => e
|
|
217
|
+
bad_request(e.message)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def handle_yank
|
|
221
|
+
gem_name = @request.params["gem_name"]
|
|
222
|
+
version = @request.params["version"]
|
|
223
|
+
|
|
224
|
+
return bad_request("Missing gem_name") if gem_name.nil? || gem_name.empty?
|
|
225
|
+
return bad_request("Missing version") if version.nil? || version.empty?
|
|
226
|
+
|
|
227
|
+
@repository.yank_gem(gem_name, version)
|
|
228
|
+
text_ok("Successfully yanked gem: #{gem_name}-#{version}")
|
|
229
|
+
rescue ReadGatedRepository::WriteNotAllowed => e
|
|
230
|
+
[403, {"Content-Type" => "text/plain"}, [e.message]]
|
|
231
|
+
rescue DirectoryGemRepository::GemNotFound => e
|
|
232
|
+
not_found(e.message)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def handle_search(query)
|
|
236
|
+
query ||= ""
|
|
237
|
+
results = []
|
|
238
|
+
|
|
239
|
+
@repository.gem_versions.each do |name, version|
|
|
240
|
+
if name.include?(query)
|
|
241
|
+
results << {
|
|
242
|
+
name: name,
|
|
243
|
+
version: version,
|
|
244
|
+
platform: "ruby",
|
|
245
|
+
authors: ["Unknown"],
|
|
246
|
+
info: "Uploaded to Paquette"
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
json_ok(results)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def handle_specs(version)
|
|
255
|
+
# Generate specs in the format expected by Bundler
|
|
256
|
+
specs = generate_specs_array
|
|
257
|
+
|
|
258
|
+
# Use Marshal 4.8 format for compatibility with Bundler
|
|
259
|
+
specs_data = marshal_dump_4_8(specs)
|
|
260
|
+
|
|
261
|
+
# For .gz requests, compress the data
|
|
262
|
+
if version.include?(".gz")
|
|
263
|
+
specs_data = gzip_compress(specs_data)
|
|
264
|
+
[200, {"Content-Type" => "application/x-gzip"}, [specs_data]]
|
|
265
|
+
else
|
|
266
|
+
[200, {"Content-Type" => "application/octet-stream"}, [specs_data]]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def handle_compact_names
|
|
271
|
+
names = @repository.gem_names
|
|
272
|
+
text_ok(names.join("\n"))
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def handle_compact_versions
|
|
276
|
+
# Group versions by gem name
|
|
277
|
+
gem_versions = {}
|
|
278
|
+
@repository.gem_versions.each do |name, version|
|
|
279
|
+
gem_versions[name] ||= []
|
|
280
|
+
gem_versions[name] << version
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Sort versions for each gem
|
|
284
|
+
gem_versions.each { |name, versions| versions.sort! }
|
|
285
|
+
|
|
286
|
+
# Generate the content in the official format
|
|
287
|
+
lines = []
|
|
288
|
+
lines << "created_at: #{Time.now.utc.iso8601}"
|
|
289
|
+
lines << "---"
|
|
290
|
+
|
|
291
|
+
# Add each gem with its versions and checksum
|
|
292
|
+
gem_versions.sort.each do |name, versions|
|
|
293
|
+
versions_str = versions.join(",")
|
|
294
|
+
# Create a checksum for this gem's versions
|
|
295
|
+
version_data = "#{name} #{versions.join(" ")}"
|
|
296
|
+
checksum = Digest::MD5.hexdigest(version_data)
|
|
297
|
+
lines << "#{name} #{versions_str} #{checksum}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
content = lines.join("\n")
|
|
301
|
+
|
|
302
|
+
# Calculate overall checksum for the entire content
|
|
303
|
+
overall_checksum = Digest::MD5.hexdigest(content)
|
|
304
|
+
|
|
305
|
+
[200, {"Content-Type" => "text/plain", "X-Checksum-Sha256" => overall_checksum}, [content]]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def generate_specs_array
|
|
309
|
+
# Generate specs array in the format expected by RubyGems/Bundler
|
|
310
|
+
# Each spec is [gem_name, version, platform]
|
|
311
|
+
# Use only basic Ruby types to ensure Marshal 4.8 compatibility
|
|
312
|
+
specs = []
|
|
313
|
+
@repository.gem_versions.each do |name, version|
|
|
314
|
+
specs << [name.to_s, version.to_s, "ruby"]
|
|
315
|
+
end
|
|
316
|
+
specs
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def handle_latest_specs(version)
|
|
320
|
+
# Generate latest specs (only the latest version of each gem)
|
|
321
|
+
latest_specs = generate_latest_specs_array
|
|
322
|
+
|
|
323
|
+
# Use Marshal 4.8 format for compatibility with Bundler
|
|
324
|
+
specs_data = marshal_dump_4_8(latest_specs)
|
|
325
|
+
|
|
326
|
+
# For .gz requests, compress the data
|
|
327
|
+
if version.include?(".gz")
|
|
328
|
+
specs_data = gzip_compress(specs_data)
|
|
329
|
+
[200, {"Content-Type" => "application/x-gzip"}, [specs_data]]
|
|
330
|
+
else
|
|
331
|
+
[200, {"Content-Type" => "application/octet-stream"}, [specs_data]]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def generate_latest_specs_array
|
|
336
|
+
# Generate latest specs array - only the latest version of each gem
|
|
337
|
+
latest_versions = {}
|
|
338
|
+
@repository.gem_versions.each do |name, version|
|
|
339
|
+
if !latest_versions[name] || Gem::Version.new(version) > Gem::Version.new(latest_versions[name])
|
|
340
|
+
latest_versions[name] = version
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
specs = []
|
|
345
|
+
latest_versions.each do |name, version|
|
|
346
|
+
specs << [name.to_s, version.to_s, "ruby"]
|
|
347
|
+
end
|
|
348
|
+
specs
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def marshal_dump_4_8(obj)
|
|
352
|
+
# Create Marshal data in format 4.8 for compatibility with Bundler
|
|
353
|
+
# Use only basic Ruby types to ensure compatibility
|
|
354
|
+
specs_array = obj.is_a?(Array) ? obj : []
|
|
355
|
+
|
|
356
|
+
# Simple Marshal.dump should work with basic types
|
|
357
|
+
Marshal.dump(specs_array)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def gzip_compress(data)
|
|
361
|
+
# Create proper gzip format with headers, checksums, etc.
|
|
362
|
+
StringIO.open do |io|
|
|
363
|
+
Zlib::GzipWriter.wrap(io) do |gz|
|
|
364
|
+
gz.write(data)
|
|
365
|
+
end
|
|
366
|
+
io.string
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Helper methods for common response patterns
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
def json_ok(data)
|
|
374
|
+
[200, {"Content-Type" => "application/json"}, [JSON.pretty_generate(data)]]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def text_ok(data)
|
|
378
|
+
[200, {"Content-Type" => "text/plain"}, [data]]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def not_found(message = "Not Found")
|
|
382
|
+
[404, {"Content-Type" => "text/plain"}, [message]]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def bad_request(message = "Bad Request")
|
|
386
|
+
[400, {"Content-Type" => "text/plain"}, [message]]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def server_error(message = "Internal Server Error")
|
|
390
|
+
[500, {"Content-Type" => "text/plain"}, [message]]
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "digest"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require "sourcemap"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module Paquette
|
|
9
|
+
class NpmRepacker
|
|
10
|
+
def self.repack(npm_path, &block)
|
|
11
|
+
new(npm_path).repack(&block)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(npm_path)
|
|
15
|
+
@npm_path = npm_path
|
|
16
|
+
@temp_dir = nil
|
|
17
|
+
@unpacked_package_dir = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def repack(&block)
|
|
21
|
+
raise ArgumentError, "Block required" unless block_given?
|
|
22
|
+
raise ArgumentError, "NPM package not found: #{@npm_path}" unless File.exist?(@npm_path)
|
|
23
|
+
|
|
24
|
+
unpack_package
|
|
25
|
+
process_js_files(&block)
|
|
26
|
+
new_package_path = repackage_package
|
|
27
|
+
cleanup
|
|
28
|
+
new_package_path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def unpack_package
|
|
34
|
+
@temp_dir = Dir.mktmpdir("npm_repacker")
|
|
35
|
+
@unpacked_package_dir = File.join(@temp_dir, "unpacked_package")
|
|
36
|
+
FileUtils.mkdir_p(@unpacked_package_dir)
|
|
37
|
+
|
|
38
|
+
# Extract the NPM package (tar.gz file)
|
|
39
|
+
result = system("tar -xzf #{@npm_path} -C #{@unpacked_package_dir}")
|
|
40
|
+
raise "Failed to unpack NPM package: #{@npm_path}" unless result
|
|
41
|
+
|
|
42
|
+
# Find the unpacked package directory - it should be the only directory in unpacked_package_dir
|
|
43
|
+
@package_dir = Dir.glob(File.join(@unpacked_package_dir, "*")).find { |path| File.directory?(path) }
|
|
44
|
+
raise "Could not find unpacked package directory" unless @package_dir
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def process_js_files(&block)
|
|
48
|
+
# Process .ts, .js, .mjs, .jsx, .tsx files
|
|
49
|
+
extensions = %w[.ts .js .mjs .jsx .tsx]
|
|
50
|
+
extensions.each do |ext|
|
|
51
|
+
Dir.glob(File.join(@package_dir, "**", "*#{ext}")).each do |js_file|
|
|
52
|
+
process_js_file(js_file, &block)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def process_js_file(file_path, &block)
|
|
58
|
+
# Get the relative path within the package directory
|
|
59
|
+
relative_path = file_path.sub("#{@package_dir}/", "")
|
|
60
|
+
|
|
61
|
+
# Create a temporary file for output
|
|
62
|
+
temp_output = Tempfile.new("npm_repacker_output")
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
# Open input file in binary mode for reading
|
|
66
|
+
File.open(file_path, "rb") do |input_file|
|
|
67
|
+
# Open output file in binary mode for writing
|
|
68
|
+
File.open(temp_output.path, "wb") do |output_file|
|
|
69
|
+
# Call the block with input file, output file, and relative path
|
|
70
|
+
yield(input_file, output_file, relative_path)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Handle sourcemap updates if this is a JS file
|
|
75
|
+
if File.extname(file_path) == ".js"
|
|
76
|
+
update_sourcemap_offsets(file_path, temp_output.path)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Replace the original file with the processed content
|
|
80
|
+
FileUtils.mv(temp_output.path, file_path)
|
|
81
|
+
ensure
|
|
82
|
+
temp_output.close
|
|
83
|
+
temp_output.unlink if File.exist?(temp_output.path)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_sourcemap_offsets(js_file_path, new_js_content_path)
|
|
88
|
+
# Look for sourcemap reference in the JS file
|
|
89
|
+
sourcemap_path = find_sourcemap_file(js_file_path)
|
|
90
|
+
return unless sourcemap_path
|
|
91
|
+
|
|
92
|
+
# Calculate the offset difference
|
|
93
|
+
original_content = File.read(js_file_path)
|
|
94
|
+
new_content = File.read(new_js_content_path)
|
|
95
|
+
offset_diff = new_content.length - original_content.length
|
|
96
|
+
|
|
97
|
+
return if offset_diff == 0 # No changes, no need to update sourcemap
|
|
98
|
+
|
|
99
|
+
# Update the sourcemap
|
|
100
|
+
update_sourcemap_file(sourcemap_path, offset_diff)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def find_sourcemap_file(js_file_path)
|
|
104
|
+
# Look for sourceMappingURL comment in the JS file
|
|
105
|
+
content = File.read(js_file_path)
|
|
106
|
+
if content =~ /\/\/# sourceMappingURL=(.+)/
|
|
107
|
+
sourcemap_filename = $1.strip
|
|
108
|
+
sourcemap_path = File.join(File.dirname(js_file_path), sourcemap_filename)
|
|
109
|
+
return sourcemap_path if File.exist?(sourcemap_path)
|
|
110
|
+
end
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def update_sourcemap_file(sourcemap_path, offset_diff)
|
|
115
|
+
# Read and parse the sourcemap
|
|
116
|
+
sourcemap_content = File.read(sourcemap_path)
|
|
117
|
+
JSON.parse(sourcemap_content)
|
|
118
|
+
|
|
119
|
+
# Sourcemap found and needs updating (debug output removed)
|
|
120
|
+
|
|
121
|
+
# For now, we'll just log the information. In a production system,
|
|
122
|
+
# you would need to implement proper sourcemap offset adjustment
|
|
123
|
+
# which requires understanding the specific sourcemap format and
|
|
124
|
+
# updating the mappings array accordingly.
|
|
125
|
+
|
|
126
|
+
# TODO: Implement proper sourcemap offset adjustment
|
|
127
|
+
# This would involve:
|
|
128
|
+
# 1. Parsing the mappings string
|
|
129
|
+
# 2. Adjusting the generated line/column positions
|
|
130
|
+
# 3. Re-encoding the mappings string
|
|
131
|
+
# 4. Updating the sourcemap JSON
|
|
132
|
+
rescue
|
|
133
|
+
# If sourcemap parsing fails, silently continue (debug output removed)
|
|
134
|
+
# The sourcemap update is not critical for the repacking process
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def repackage_package
|
|
138
|
+
# Create a new NPM package
|
|
139
|
+
package_name = File.basename(@npm_path, ".tgz")
|
|
140
|
+
new_package_path = File.join(Dir.tmpdir, "#{package_name}-repacked.tgz")
|
|
141
|
+
|
|
142
|
+
# Create the new tar.gz file
|
|
143
|
+
result = system("cd #{@unpacked_package_dir} && tar -czf #{new_package_path} package/")
|
|
144
|
+
raise "Failed to repackage NPM package" unless result
|
|
145
|
+
|
|
146
|
+
new_package_path
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cleanup
|
|
150
|
+
FileUtils.rm_rf(@temp_dir) if @temp_dir && Dir.exist?(@temp_dir)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require_relative "npm_repository"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Paquette
|
|
5
|
+
class NpmServer
|
|
6
|
+
# Repository implementation that reads NPM packages from a directory
|
|
7
|
+
class DirectoryNpmRepository < NpmRepository
|
|
8
|
+
def initialize(packages_dir)
|
|
9
|
+
@packages_dir = packages_dir
|
|
10
|
+
FileUtils.mkdir_p(@packages_dir)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def package_names
|
|
14
|
+
Dir.glob(File.join(@packages_dir, "*")).select { |path| File.directory?(path) }.map do |package_path|
|
|
15
|
+
File.basename(package_path)
|
|
16
|
+
end.sort
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def package_versions
|
|
20
|
+
versions = []
|
|
21
|
+
package_names.each do |package_name|
|
|
22
|
+
versions_for_package(package_name).each do |version|
|
|
23
|
+
versions << [package_name, version]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
versions.sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def versions_for_package(package_name)
|
|
30
|
+
package_dir = File.join(@packages_dir, package_name)
|
|
31
|
+
return [] unless Dir.exist?(package_dir)
|
|
32
|
+
|
|
33
|
+
Dir.glob(File.join(package_dir, "*.tgz")).map do |tgz_path|
|
|
34
|
+
filename = File.basename(tgz_path, ".tgz")
|
|
35
|
+
if (match = filename.match(/^#{Regexp.escape(package_name)}-(\d+\.\d+\.\d+.*)$/))
|
|
36
|
+
match[1]
|
|
37
|
+
end
|
|
38
|
+
end.compact.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def package_file_path(package_name, version)
|
|
42
|
+
File.join(@packages_dir, package_name, "#{package_name}-#{version}.tgz")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def package_exists?(package_name, version)
|
|
46
|
+
File.exist?(package_file_path(package_name, version))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def package_info(package_name, version)
|
|
50
|
+
package_file = package_file_path(package_name, version)
|
|
51
|
+
return nil unless File.exist?(package_file)
|
|
52
|
+
|
|
53
|
+
# For now, return basic package info
|
|
54
|
+
# In a real implementation, you'd extract the package.json from the .tgz file
|
|
55
|
+
{
|
|
56
|
+
name: package_name,
|
|
57
|
+
version: version,
|
|
58
|
+
description: "Package #{package_name} version #{version}",
|
|
59
|
+
main: "index.js",
|
|
60
|
+
scripts: {},
|
|
61
|
+
dependencies: {},
|
|
62
|
+
devDependencies: {}
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def package_dependencies(package_name, version)
|
|
67
|
+
info = package_info(package_name, version)
|
|
68
|
+
return {} unless info
|
|
69
|
+
|
|
70
|
+
(info[:dependencies] || {}).merge(info[:devDependencies] || {})
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def package_metadata(package_name)
|
|
74
|
+
versions = versions_for_package(package_name)
|
|
75
|
+
return nil if versions.empty?
|
|
76
|
+
|
|
77
|
+
# Get the latest version
|
|
78
|
+
latest_version = versions.max_by { |v| Gem::Version.new(v) }
|
|
79
|
+
|
|
80
|
+
metadata = {
|
|
81
|
+
name: package_name,
|
|
82
|
+
versions: {},
|
|
83
|
+
"dist-tags": {
|
|
84
|
+
latest: latest_version
|
|
85
|
+
},
|
|
86
|
+
time: {},
|
|
87
|
+
maintainers: [],
|
|
88
|
+
description: "Package #{package_name}",
|
|
89
|
+
keywords: [],
|
|
90
|
+
license: "MIT",
|
|
91
|
+
repository: {},
|
|
92
|
+
bugs: {},
|
|
93
|
+
homepage: "",
|
|
94
|
+
readme: "",
|
|
95
|
+
readmeFilename: "README.md"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Add version-specific metadata
|
|
99
|
+
versions.each do |version|
|
|
100
|
+
info = package_info(package_name, version)
|
|
101
|
+
next unless info
|
|
102
|
+
|
|
103
|
+
metadata[:versions][version] = {
|
|
104
|
+
name: package_name,
|
|
105
|
+
version: version,
|
|
106
|
+
description: info[:description],
|
|
107
|
+
main: info[:main],
|
|
108
|
+
scripts: info[:scripts] || {},
|
|
109
|
+
dependencies: info[:dependencies] || {},
|
|
110
|
+
devDependencies: info[:devDependencies] || {},
|
|
111
|
+
dist: {
|
|
112
|
+
shasum: "dummy-sha", # In real implementation, calculate actual shasum
|
|
113
|
+
tarball: "#{package_name}/#{package_name}-#{version}.tgz"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
metadata[:time][version] = Time.now.iso8601
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
metadata[:time][:created] = Time.now.iso8601
|
|
121
|
+
metadata[:time][:modified] = Time.now.iso8601
|
|
122
|
+
|
|
123
|
+
metadata
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|