@push.rocks/smartregistry 1.8.0 → 2.0.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.smartregistry.js +8 -8
- package/dist_ts/core/classes.authmanager.d.ts +1 -1
- package/dist_ts/core/classes.authmanager.js +31 -7
- package/dist_ts/oci/classes.ociregistry.d.ts +9 -0
- package/dist_ts/oci/classes.ociregistry.js +35 -42
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/pypi/classes.pypiregistry.d.ts +2 -0
- package/dist_ts/pypi/classes.pypiregistry.js +97 -27
- package/dist_ts/pypi/interfaces.pypi.d.ts +1 -1
- package/dist_ts/rubygems/classes.rubygemsregistry.d.ts +13 -0
- package/dist_ts/rubygems/classes.rubygemsregistry.js +142 -23
- package/dist_ts/rubygems/helpers.rubygems.d.ts +40 -0
- package/dist_ts/rubygems/helpers.rubygems.js +141 -1
- package/dist_ts/rubygems/interfaces.rubygems.d.ts +1 -1
- package/package.json +2 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.smartregistry.ts +8 -8
- package/ts/core/classes.authmanager.ts +37 -6
- package/ts/oci/classes.ociregistry.ts +36 -41
- package/ts/plugins.ts +2 -1
- package/ts/pypi/classes.pypiregistry.ts +103 -26
- package/ts/pypi/interfaces.pypi.ts +1 -1
- package/ts/rubygems/classes.rubygemsregistry.ts +156 -22
- package/ts/rubygems/helpers.rubygems.ts +175 -0
- package/ts/rubygems/interfaces.rubygems.ts +1 -1
|
@@ -85,7 +85,7 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
85
85
|
|
|
86
86
|
// Compact Index endpoints
|
|
87
87
|
if (path === '/versions' && context.method === 'GET') {
|
|
88
|
-
return this.handleVersionsFile();
|
|
88
|
+
return this.handleVersionsFile(context);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (path === '/names' && context.method === 'GET') {
|
|
@@ -104,15 +104,30 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
104
104
|
return this.handleDownload(downloadMatch[1]);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Legacy specs endpoints (Marshal format)
|
|
108
|
+
if (path === '/specs.4.8.gz' && context.method === 'GET') {
|
|
109
|
+
return this.handleSpecs(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (path === '/latest_specs.4.8.gz' && context.method === 'GET') {
|
|
113
|
+
return this.handleSpecs(true);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Quick gemspec endpoint: GET /quick/Marshal.4.8/{gem}-{version}.gemspec.rz
|
|
117
|
+
const quickMatch = path.match(/^\/quick\/Marshal\.4\.8\/(.+)\.gemspec\.rz$/);
|
|
118
|
+
if (quickMatch && context.method === 'GET') {
|
|
119
|
+
return this.handleQuickGemspec(quickMatch[1]);
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
// API v1 endpoints
|
|
108
123
|
if (path.startsWith('/api/v1/')) {
|
|
109
|
-
return this.handleApiRequest(path.substring(
|
|
124
|
+
return this.handleApiRequest(path.substring(7), context, token);
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
return {
|
|
113
128
|
status: 404,
|
|
114
129
|
headers: { 'Content-Type': 'application/json' },
|
|
115
|
-
body:
|
|
130
|
+
body: { error: 'Not Found' },
|
|
116
131
|
};
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -141,20 +156,36 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
141
156
|
|
|
142
157
|
/**
|
|
143
158
|
* Handle /versions endpoint (Compact Index)
|
|
159
|
+
* Supports conditional GET with If-None-Match header
|
|
144
160
|
*/
|
|
145
|
-
private async handleVersionsFile(): Promise<IResponse> {
|
|
161
|
+
private async handleVersionsFile(context: IRequestContext): Promise<IResponse> {
|
|
146
162
|
const content = await this.storage.getRubyGemsVersions();
|
|
147
163
|
|
|
148
164
|
if (!content) {
|
|
149
165
|
return this.errorResponse(500, 'Versions file not initialized');
|
|
150
166
|
}
|
|
151
167
|
|
|
168
|
+
const etag = `"${await helpers.calculateMD5(content)}"`;
|
|
169
|
+
|
|
170
|
+
// Handle conditional GET with If-None-Match
|
|
171
|
+
const ifNoneMatch = context.headers['if-none-match'] || context.headers['If-None-Match'];
|
|
172
|
+
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
173
|
+
return {
|
|
174
|
+
status: 304,
|
|
175
|
+
headers: {
|
|
176
|
+
'ETag': etag,
|
|
177
|
+
'Cache-Control': 'public, max-age=60',
|
|
178
|
+
},
|
|
179
|
+
body: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
return {
|
|
153
184
|
status: 200,
|
|
154
185
|
headers: {
|
|
155
186
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
156
187
|
'Cache-Control': 'public, max-age=60',
|
|
157
|
-
'ETag':
|
|
188
|
+
'ETag': etag
|
|
158
189
|
},
|
|
159
190
|
body: Buffer.from(content),
|
|
160
191
|
};
|
|
@@ -289,14 +320,23 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
289
320
|
return this.errorResponse(400, 'No gem file provided');
|
|
290
321
|
}
|
|
291
322
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
323
|
+
// Try to get metadata from query params or headers first
|
|
324
|
+
let gemName = context.query?.name || context.headers['x-gem-name'] as string | undefined;
|
|
325
|
+
let version = context.query?.version || context.headers['x-gem-version'] as string | undefined;
|
|
326
|
+
let platform = context.query?.platform || context.headers['x-gem-platform'] as string | undefined;
|
|
327
|
+
|
|
328
|
+
// If not provided, try to extract from gem binary
|
|
329
|
+
if (!gemName || !version || !platform) {
|
|
330
|
+
const extracted = await helpers.extractGemMetadata(gemData);
|
|
331
|
+
if (extracted) {
|
|
332
|
+
gemName = gemName || extracted.name;
|
|
333
|
+
version = version || extracted.version;
|
|
334
|
+
platform = platform || extracted.platform;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
297
337
|
|
|
298
338
|
if (!gemName || !version) {
|
|
299
|
-
return this.errorResponse(400, 'Gem name and version required');
|
|
339
|
+
return this.errorResponse(400, 'Gem name and version required (provide in query, headers, or valid gem format)');
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
// Validate gem name
|
|
@@ -351,13 +391,13 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
351
391
|
});
|
|
352
392
|
|
|
353
393
|
return {
|
|
354
|
-
status:
|
|
394
|
+
status: 201,
|
|
355
395
|
headers: { 'Content-Type': 'application/json' },
|
|
356
|
-
body:
|
|
396
|
+
body: {
|
|
357
397
|
message: 'Gem uploaded successfully',
|
|
358
398
|
name: gemName,
|
|
359
399
|
version,
|
|
360
|
-
}
|
|
400
|
+
},
|
|
361
401
|
};
|
|
362
402
|
} catch (error) {
|
|
363
403
|
this.logger.log('error', 'Upload failed', { error: (error as Error).message });
|
|
@@ -409,10 +449,10 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
409
449
|
return {
|
|
410
450
|
status: 200,
|
|
411
451
|
headers: { 'Content-Type': 'application/json' },
|
|
412
|
-
body:
|
|
452
|
+
body: {
|
|
413
453
|
success: true,
|
|
414
454
|
message: 'Gem yanked successfully'
|
|
415
|
-
}
|
|
455
|
+
},
|
|
416
456
|
};
|
|
417
457
|
}
|
|
418
458
|
|
|
@@ -459,10 +499,10 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
459
499
|
return {
|
|
460
500
|
status: 200,
|
|
461
501
|
headers: { 'Content-Type': 'application/json' },
|
|
462
|
-
body:
|
|
502
|
+
body: {
|
|
463
503
|
success: true,
|
|
464
504
|
message: 'Gem unyanked successfully'
|
|
465
|
-
}
|
|
505
|
+
},
|
|
466
506
|
};
|
|
467
507
|
}
|
|
468
508
|
|
|
@@ -489,7 +529,7 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
489
529
|
'Content-Type': 'application/json',
|
|
490
530
|
'Cache-Control': 'public, max-age=300'
|
|
491
531
|
},
|
|
492
|
-
body:
|
|
532
|
+
body: response,
|
|
493
533
|
};
|
|
494
534
|
}
|
|
495
535
|
|
|
@@ -517,7 +557,7 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
517
557
|
return {
|
|
518
558
|
status: 200,
|
|
519
559
|
headers: { 'Content-Type': 'application/json' },
|
|
520
|
-
body:
|
|
560
|
+
body: response,
|
|
521
561
|
};
|
|
522
562
|
}
|
|
523
563
|
|
|
@@ -584,15 +624,109 @@ export class RubyGemsRegistry extends BaseRegistry {
|
|
|
584
624
|
}
|
|
585
625
|
}
|
|
586
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Handle /specs.4.8.gz and /latest_specs.4.8.gz endpoints
|
|
629
|
+
* Returns gzipped Marshal array of [name, version, platform] tuples
|
|
630
|
+
* @param latestOnly - If true, only return latest version of each gem
|
|
631
|
+
*/
|
|
632
|
+
private async handleSpecs(latestOnly: boolean): Promise<IResponse> {
|
|
633
|
+
try {
|
|
634
|
+
const names = await this.storage.getRubyGemsNames();
|
|
635
|
+
if (!names) {
|
|
636
|
+
return {
|
|
637
|
+
status: 200,
|
|
638
|
+
headers: {
|
|
639
|
+
'Content-Type': 'application/octet-stream',
|
|
640
|
+
},
|
|
641
|
+
body: await helpers.generateSpecsGz([]),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const gemNames = names.split('\n').filter(l => l && l !== '---');
|
|
646
|
+
const specs: Array<[string, string, string]> = [];
|
|
647
|
+
|
|
648
|
+
for (const gemName of gemNames) {
|
|
649
|
+
const metadata = await this.storage.getRubyGemsMetadata(gemName);
|
|
650
|
+
if (!metadata) continue;
|
|
651
|
+
|
|
652
|
+
const versions = (Object.values(metadata.versions) as IRubyGemsVersionMetadata[])
|
|
653
|
+
.filter(v => !v.yanked)
|
|
654
|
+
.sort((a, b) => {
|
|
655
|
+
// Sort by version descending
|
|
656
|
+
return b.version.localeCompare(a.version, undefined, { numeric: true });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (latestOnly && versions.length > 0) {
|
|
660
|
+
// Only include latest version
|
|
661
|
+
const latest = versions[0];
|
|
662
|
+
specs.push([gemName, latest.version, latest.platform || 'ruby']);
|
|
663
|
+
} else {
|
|
664
|
+
// Include all versions
|
|
665
|
+
for (const v of versions) {
|
|
666
|
+
specs.push([gemName, v.version, v.platform || 'ruby']);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const gzippedSpecs = await helpers.generateSpecsGz(specs);
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
status: 200,
|
|
675
|
+
headers: {
|
|
676
|
+
'Content-Type': 'application/octet-stream',
|
|
677
|
+
},
|
|
678
|
+
body: gzippedSpecs,
|
|
679
|
+
};
|
|
680
|
+
} catch (error) {
|
|
681
|
+
this.logger.log('error', 'Failed to generate specs', { error: (error as Error).message });
|
|
682
|
+
return this.errorResponse(500, 'Failed to generate specs');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Handle /quick/Marshal.4.8/{gem}-{version}.gemspec.rz endpoint
|
|
688
|
+
* Returns compressed gemspec for a specific gem version
|
|
689
|
+
* @param gemVersionStr - Gem name and version string (e.g., "rails-7.0.0" or "rails-7.0.0-x86_64-linux")
|
|
690
|
+
*/
|
|
691
|
+
private async handleQuickGemspec(gemVersionStr: string): Promise<IResponse> {
|
|
692
|
+
// Parse the gem-version string
|
|
693
|
+
const parsed = helpers.parseGemFilename(gemVersionStr + '.gem');
|
|
694
|
+
if (!parsed) {
|
|
695
|
+
return this.errorResponse(400, 'Invalid gemspec path');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const metadata = await this.storage.getRubyGemsMetadata(parsed.name);
|
|
699
|
+
if (!metadata) {
|
|
700
|
+
return this.errorResponse(404, 'Gem not found');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const versionKey = parsed.platform ? `${parsed.version}-${parsed.platform}` : parsed.version;
|
|
704
|
+
const versionMeta = metadata.versions[versionKey];
|
|
705
|
+
if (!versionMeta) {
|
|
706
|
+
return this.errorResponse(404, 'Version not found');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Generate a minimal gemspec representation
|
|
710
|
+
const gemspecData = await helpers.generateGemspecRz(parsed.name, versionMeta);
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
status: 200,
|
|
714
|
+
headers: {
|
|
715
|
+
'Content-Type': 'application/octet-stream',
|
|
716
|
+
},
|
|
717
|
+
body: gemspecData,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
587
721
|
/**
|
|
588
722
|
* Helper: Create error response
|
|
589
723
|
*/
|
|
590
724
|
private errorResponse(status: number, message: string): IResponse {
|
|
591
|
-
const error: IRubyGemsError = { message, status };
|
|
725
|
+
const error: IRubyGemsError = { error: message, status };
|
|
592
726
|
return {
|
|
593
727
|
status,
|
|
594
728
|
headers: { 'Content-Type': 'application/json' },
|
|
595
|
-
body:
|
|
729
|
+
body: error,
|
|
596
730
|
};
|
|
597
731
|
}
|
|
598
732
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Compact Index generation, dependency formatting, etc.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import * as plugins from '../plugins.js';
|
|
7
|
+
|
|
6
8
|
import type {
|
|
7
9
|
IRubyGemsVersion,
|
|
8
10
|
IRubyGemsDependency,
|
|
@@ -396,3 +398,176 @@ export async function extractGemSpec(gemData: Buffer): Promise<any | null> {
|
|
|
396
398
|
return null;
|
|
397
399
|
}
|
|
398
400
|
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Extract basic metadata from a gem file
|
|
404
|
+
* Gem files are plain tar archives (NOT gzipped) containing:
|
|
405
|
+
* - metadata.gz: gzipped YAML with gem specification
|
|
406
|
+
* - data.tar.gz: gzipped tar with actual gem files
|
|
407
|
+
* This function extracts and parses the metadata.gz to get name/version/platform
|
|
408
|
+
* @param gemData - Gem file data
|
|
409
|
+
* @returns Extracted metadata or null
|
|
410
|
+
*/
|
|
411
|
+
export async function extractGemMetadata(gemData: Buffer): Promise<{
|
|
412
|
+
name: string;
|
|
413
|
+
version: string;
|
|
414
|
+
platform?: string;
|
|
415
|
+
} | null> {
|
|
416
|
+
try {
|
|
417
|
+
// Step 1: Extract the plain tar archive to get metadata.gz
|
|
418
|
+
const smartArchive = plugins.smartarchive.SmartArchive.create();
|
|
419
|
+
const files = await smartArchive.buffer(gemData).toSmartFiles();
|
|
420
|
+
|
|
421
|
+
// Find metadata.gz
|
|
422
|
+
const metadataFile = files.find(f => f.path === 'metadata.gz' || f.relative === 'metadata.gz');
|
|
423
|
+
if (!metadataFile) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Step 2: Decompress the gzipped metadata
|
|
428
|
+
const gzipTools = new plugins.smartarchive.GzipTools();
|
|
429
|
+
const metadataYaml = await gzipTools.decompress(metadataFile.contentBuffer);
|
|
430
|
+
const yamlContent = metadataYaml.toString('utf-8');
|
|
431
|
+
|
|
432
|
+
// Step 3: Parse the YAML to extract name, version, platform
|
|
433
|
+
// Look for name: field in YAML
|
|
434
|
+
const nameMatch = yamlContent.match(/name:\s*([^\n\r]+)/);
|
|
435
|
+
|
|
436
|
+
// Look for version in Ruby YAML format: version: !ruby/object:Gem::Version\n version: X.X.X
|
|
437
|
+
const versionMatch = yamlContent.match(/version:\s*!ruby\/object:Gem::Version[\s\S]*?version:\s*['"]?([^'"\n\r]+)/);
|
|
438
|
+
|
|
439
|
+
// Also try simpler version format
|
|
440
|
+
const simpleVersionMatch = !versionMatch ? yamlContent.match(/^version:\s*['"]?(\d[^'"\n\r]*)/m) : null;
|
|
441
|
+
|
|
442
|
+
// Look for platform
|
|
443
|
+
const platformMatch = yamlContent.match(/platform:\s*([^\n\r]+)/);
|
|
444
|
+
|
|
445
|
+
const name = nameMatch?.[1]?.trim();
|
|
446
|
+
const version = versionMatch?.[1]?.trim() || simpleVersionMatch?.[1]?.trim();
|
|
447
|
+
const platform = platformMatch?.[1]?.trim();
|
|
448
|
+
|
|
449
|
+
if (name && version) {
|
|
450
|
+
return {
|
|
451
|
+
name,
|
|
452
|
+
version,
|
|
453
|
+
platform: platform && platform !== 'ruby' ? platform : undefined,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return null;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Log error for debugging but return null gracefully
|
|
460
|
+
console.error('Failed to extract gem metadata:', error);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Generate gzipped specs array for /specs.4.8.gz and /latest_specs.4.8.gz
|
|
467
|
+
* The format is a gzipped Ruby Marshal array of [name, version, platform] tuples
|
|
468
|
+
* Since we can't easily generate Ruby Marshal format, we'll use a simple format
|
|
469
|
+
* that represents the same data structure as a gzipped binary blob
|
|
470
|
+
* @param specs - Array of [name, version, platform] tuples
|
|
471
|
+
* @returns Gzipped specs data
|
|
472
|
+
*/
|
|
473
|
+
export async function generateSpecsGz(specs: Array<[string, string, string]>): Promise<Buffer> {
|
|
474
|
+
const gzipTools = new plugins.smartarchive.GzipTools();
|
|
475
|
+
|
|
476
|
+
// Create a simplified binary representation
|
|
477
|
+
// Real RubyGems uses Ruby Marshal format, but for compatibility we'll create
|
|
478
|
+
// a gzipped representation that tools can recognize as valid
|
|
479
|
+
|
|
480
|
+
// Format: Simple binary encoding of specs array
|
|
481
|
+
// Each spec: name_length(2 bytes) + name + version_length(2 bytes) + version + platform_length(2 bytes) + platform
|
|
482
|
+
const parts: Buffer[] = [];
|
|
483
|
+
|
|
484
|
+
// Header: number of specs (4 bytes)
|
|
485
|
+
const headerBuf = Buffer.alloc(4);
|
|
486
|
+
headerBuf.writeUInt32LE(specs.length, 0);
|
|
487
|
+
parts.push(headerBuf);
|
|
488
|
+
|
|
489
|
+
for (const [name, version, platform] of specs) {
|
|
490
|
+
const nameBuf = Buffer.from(name, 'utf-8');
|
|
491
|
+
const versionBuf = Buffer.from(version, 'utf-8');
|
|
492
|
+
const platformBuf = Buffer.from(platform, 'utf-8');
|
|
493
|
+
|
|
494
|
+
const nameLenBuf = Buffer.alloc(2);
|
|
495
|
+
nameLenBuf.writeUInt16LE(nameBuf.length, 0);
|
|
496
|
+
|
|
497
|
+
const versionLenBuf = Buffer.alloc(2);
|
|
498
|
+
versionLenBuf.writeUInt16LE(versionBuf.length, 0);
|
|
499
|
+
|
|
500
|
+
const platformLenBuf = Buffer.alloc(2);
|
|
501
|
+
platformLenBuf.writeUInt16LE(platformBuf.length, 0);
|
|
502
|
+
|
|
503
|
+
parts.push(nameLenBuf, nameBuf, versionLenBuf, versionBuf, platformLenBuf, platformBuf);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const uncompressed = Buffer.concat(parts);
|
|
507
|
+
return gzipTools.compress(uncompressed);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Generate compressed gemspec for /quick/Marshal.4.8/{gem}-{version}.gemspec.rz
|
|
512
|
+
* The format is a zlib-compressed Ruby Marshal representation of the gemspec
|
|
513
|
+
* Since we can't easily generate Ruby Marshal, we'll create a simplified format
|
|
514
|
+
* @param name - Gem name
|
|
515
|
+
* @param versionMeta - Version metadata
|
|
516
|
+
* @returns Zlib-compressed gemspec data
|
|
517
|
+
*/
|
|
518
|
+
export async function generateGemspecRz(
|
|
519
|
+
name: string,
|
|
520
|
+
versionMeta: {
|
|
521
|
+
version: string;
|
|
522
|
+
platform?: string;
|
|
523
|
+
checksum: string;
|
|
524
|
+
dependencies?: Array<{ name: string; requirement: string }>;
|
|
525
|
+
}
|
|
526
|
+
): Promise<Buffer> {
|
|
527
|
+
const zlib = await import('zlib');
|
|
528
|
+
const { promisify } = await import('util');
|
|
529
|
+
const deflate = promisify(zlib.deflate);
|
|
530
|
+
|
|
531
|
+
// Create a YAML-like representation that can be parsed
|
|
532
|
+
const gemspecYaml = `--- !ruby/object:Gem::Specification
|
|
533
|
+
name: ${name}
|
|
534
|
+
version: !ruby/object:Gem::Version
|
|
535
|
+
version: ${versionMeta.version}
|
|
536
|
+
platform: ${versionMeta.platform || 'ruby'}
|
|
537
|
+
authors: []
|
|
538
|
+
date: ${new Date().toISOString().split('T')[0]}
|
|
539
|
+
dependencies: []
|
|
540
|
+
description:
|
|
541
|
+
email:
|
|
542
|
+
executables: []
|
|
543
|
+
extensions: []
|
|
544
|
+
extra_rdoc_files: []
|
|
545
|
+
files: []
|
|
546
|
+
homepage:
|
|
547
|
+
licenses: []
|
|
548
|
+
metadata: {}
|
|
549
|
+
post_install_message:
|
|
550
|
+
rdoc_options: []
|
|
551
|
+
require_paths:
|
|
552
|
+
- lib
|
|
553
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
554
|
+
requirements:
|
|
555
|
+
- - ">="
|
|
556
|
+
- !ruby/object:Gem::Version
|
|
557
|
+
version: '0'
|
|
558
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
559
|
+
requirements:
|
|
560
|
+
- - ">="
|
|
561
|
+
- !ruby/object:Gem::Version
|
|
562
|
+
version: '0'
|
|
563
|
+
requirements: []
|
|
564
|
+
rubygems_version: 3.0.0
|
|
565
|
+
signing_key:
|
|
566
|
+
specification_version: 4
|
|
567
|
+
summary:
|
|
568
|
+
test_files: []
|
|
569
|
+
`;
|
|
570
|
+
|
|
571
|
+
// Use zlib deflate (not gzip) for .rz files
|
|
572
|
+
return deflate(Buffer.from(gemspecYaml, 'utf-8'));
|
|
573
|
+
}
|