modpack-lock 0.2.0 → 0.3.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.
@@ -0,0 +1,108 @@
1
+ import * as config from './config/index.js';
2
+
3
+ /**
4
+ * Split an array into chunks of specified size
5
+ */
6
+ function chunkArray(array, size) {
7
+ const chunks = [];
8
+ for (let i = 0; i < array.length; i += size) {
9
+ chunks.push(array.slice(i, i + size));
10
+ }
11
+ return chunks;
12
+ }
13
+
14
+ /**
15
+ * Query Modrinth API for version information from hashes
16
+ */
17
+ export async function getVersionsFromHashes(hashes) {
18
+ if (hashes.length === 0) {
19
+ return {};
20
+ }
21
+
22
+ try {
23
+ const response = await fetch(config.MODRINTH_VERSION_FILES_ENDPOINT, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ body: JSON.stringify({
29
+ hashes: hashes,
30
+ algorithm: 'sha1',
31
+ }),
32
+ });
33
+
34
+ if (!response.ok) {
35
+ const errorText = await response.text();
36
+ throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
37
+ }
38
+
39
+ return await response.json();
40
+ } catch (error) {
41
+ console.error(`Error querying Modrinth API: ${error.message}`);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Fetch multiple projects by their IDs in batches
48
+ */
49
+ export async function getProjects(projectIds) {
50
+ if (projectIds.length === 0) {
51
+ return [];
52
+ }
53
+
54
+ const chunks = chunkArray(projectIds, config.BATCH_SIZE);
55
+ const results = [];
56
+
57
+ for (const chunk of chunks) {
58
+ try {
59
+ const url = `${config.MODRINTH_PROJECTS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
60
+ const response = await fetch(url);
61
+
62
+ if (!response.ok) {
63
+ const errorText = await response.text();
64
+ throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
65
+ }
66
+
67
+ const data = await response.json();
68
+ results.push(...data);
69
+ } catch (error) {
70
+ console.error(`Error fetching projects: ${error.message}`);
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ return results;
76
+ }
77
+
78
+ /**
79
+ * Fetch multiple users by their IDs in batches
80
+ */
81
+ export async function getUsers(userIds) {
82
+ if (userIds.length === 0) {
83
+ return [];
84
+ }
85
+
86
+ const chunks = chunkArray(userIds, config.BATCH_SIZE);
87
+ const results = [];
88
+
89
+ for (const chunk of chunks) {
90
+ try {
91
+ const url = `${config.MODRINTH_USERS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
92
+ const response = await fetch(url);
93
+
94
+ if (!response.ok) {
95
+ const errorText = await response.text();
96
+ throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
97
+ }
98
+
99
+ const data = await response.json();
100
+ results.push(...data);
101
+ } catch (error) {
102
+ console.error(`Error fetching users: ${error.message}`);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ return results;
108
+ }
@@ -1,517 +0,0 @@
1
- import fs from 'fs/promises';
2
- import crypto from 'crypto';
3
- import path from 'path';
4
-
5
- const LOCKFILE_VERSION = '1.0.1';
6
- const MODPACK_LOCKFILE_NAME = 'modpack.lock';
7
- const MODRINTH_API_BASE = 'https://api.modrinth.com/v2';
8
- const MODRINTH_VERSION_FILES_ENDPOINT = `${MODRINTH_API_BASE}/version_files`;
9
- const MODRINTH_PROJECTS_ENDPOINT = `${MODRINTH_API_BASE}/projects`;
10
- const MODRINTH_USERS_ENDPOINT = `${MODRINTH_API_BASE}/users`;
11
- const BATCH_SIZE = 100;
12
-
13
- // Get the workspace root from the current working directory
14
- //const WORKSPACE_ROOT = process.cwd();
15
-
16
- /**
17
- * Create a logger function that respects quiet mode
18
- */
19
- function createLogger(quiet) {
20
- if (quiet) {
21
- return () => {};
22
- }
23
- return (...args) => console.log(...args);
24
- }
25
-
26
- /**
27
- * Silence all console.log output
28
- */
29
- function silenceConsole() {
30
- console.log = () => {};
31
- console.warn = () => {};
32
- console.error = () => {};
33
- console.info = () => {};
34
- }
35
-
36
- /**
37
- * Get the directories to scan for modpack files
38
- */
39
- function getScanDirectories(directoryPath) {
40
- return [
41
- { name: 'mods', path: path.join(directoryPath, 'mods') },
42
- { name: 'resourcepacks', path: path.join(directoryPath, 'resourcepacks') },
43
- { name: 'datapacks', path: path.join(directoryPath, 'datapacks') },
44
- { name: 'shaderpacks', path: path.join(directoryPath, 'shaderpacks') },
45
- ];
46
- }
47
-
48
- /**
49
- * Calculate SHA1 hash of a file
50
- */
51
- async function calculateSHA1(filePath) {
52
- const fileBuffer = await fs.readFile(filePath);
53
- return crypto.createHash('sha1').update(fileBuffer).digest('hex');
54
- }
55
-
56
- /**
57
- * Find all files in a directory
58
- */
59
- async function findFiles(dirPath) {
60
- const files = [];
61
-
62
- try {
63
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
64
-
65
- for (const entry of entries) {
66
- if (entry.isFile() && (entry.name.endsWith('.jar') || entry.name.endsWith('.zip'))) {
67
- const fullPath = path.join(dirPath, entry.name);
68
- files.push(fullPath);
69
- }
70
- }
71
- } catch (error) {
72
- if (error.code !== 'ENOENT') {
73
- console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`);
74
- }
75
- }
76
-
77
- return files;
78
- }
79
-
80
- /**
81
- * Scan a directory and return file info with hashes
82
- */
83
- async function scanDirectory(dirInfo, workspaceRoot) {
84
- const files = await findFiles(dirInfo.path);
85
- const fileEntries = [];
86
-
87
- for (const filePath of files) {
88
- try {
89
- const hash = await calculateSHA1(filePath);
90
- const relativePath = path.relative(workspaceRoot, filePath);
91
-
92
- fileEntries.push({
93
- path: relativePath,
94
- fullPath: filePath,
95
- hash: hash,
96
- category: dirInfo.name,
97
- });
98
- } catch (error) {
99
- console.warn(`Warning: Could not hash file ${filePath}: ${error.message}`);
100
- }
101
- }
102
-
103
- return fileEntries;
104
- }
105
-
106
- /**
107
- * Query Modrinth API for version information from hashes
108
- */
109
- async function getVersionsFromHashes(hashes) {
110
- if (hashes.length === 0) {
111
- return {};
112
- }
113
-
114
- try {
115
- const response = await fetch(MODRINTH_VERSION_FILES_ENDPOINT, {
116
- method: 'POST',
117
- headers: {
118
- 'Content-Type': 'application/json',
119
- },
120
- body: JSON.stringify({
121
- hashes: hashes,
122
- algorithm: 'sha1',
123
- }),
124
- });
125
-
126
- if (!response.ok) {
127
- const errorText = await response.text();
128
- throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
129
- }
130
-
131
- return await response.json();
132
- } catch (error) {
133
- console.error(`Error querying Modrinth API: ${error.message}`);
134
- throw error;
135
- }
136
- }
137
-
138
- /**
139
- * Split an array into chunks of specified size
140
- */
141
- function chunkArray(array, size) {
142
- const chunks = [];
143
- for (let i = 0; i < array.length; i += size) {
144
- chunks.push(array.slice(i, i + size));
145
- }
146
- return chunks;
147
- }
148
-
149
- /**
150
- * Fetch multiple projects by their IDs in batches
151
- */
152
- async function getProjects(projectIds) {
153
- if (projectIds.length === 0) {
154
- return [];
155
- }
156
-
157
- const chunks = chunkArray(projectIds, BATCH_SIZE);
158
- const results = [];
159
-
160
- for (const chunk of chunks) {
161
- try {
162
- const url = `${MODRINTH_PROJECTS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
163
- const response = await fetch(url);
164
-
165
- if (!response.ok) {
166
- const errorText = await response.text();
167
- throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
168
- }
169
-
170
- const data = await response.json();
171
- results.push(...data);
172
- } catch (error) {
173
- console.error(`Error fetching projects: ${error.message}`);
174
- throw error;
175
- }
176
- }
177
-
178
- return results;
179
- }
180
-
181
- /**
182
- * Fetch multiple users by their IDs in batches
183
- */
184
- async function getUsers(userIds) {
185
- if (userIds.length === 0) {
186
- return [];
187
- }
188
-
189
- const chunks = chunkArray(userIds, BATCH_SIZE);
190
- const results = [];
191
-
192
- for (const chunk of chunks) {
193
- try {
194
- const url = `${MODRINTH_USERS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
195
- const response = await fetch(url);
196
-
197
- if (!response.ok) {
198
- const errorText = await response.text();
199
- throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
200
- }
201
-
202
- const data = await response.json();
203
- results.push(...data);
204
- } catch (error) {
205
- console.error(`Error fetching users: ${error.message}`);
206
- throw error;
207
- }
208
- }
209
-
210
- return results;
211
- }
212
-
213
-
214
- /**
215
- * Create empty lockfile structure
216
- */
217
- function createEmptyLockfile() {
218
- return {
219
- version: LOCKFILE_VERSION,
220
- generated: new Date().toISOString(),
221
- total: 0,
222
- counts: {},
223
- dependencies: {},
224
- };
225
- }
226
-
227
- /**
228
- * Create lockfile structure from file info and version data
229
- */
230
- function createLockfile(fileEntries, versionData) {
231
- const lockfile = createEmptyLockfile();
232
-
233
- // Organize by category
234
- for (const fileInfo of fileEntries) {
235
- const version = versionData[fileInfo.hash];
236
-
237
- lockfile.dependencies[fileInfo.category] ||= [];
238
-
239
- const entry = {
240
- path: fileInfo.path,
241
- version: version || null,
242
- };
243
-
244
- if (!version) {
245
- console.warn(`Warning: File ${fileInfo.path} not found on Modrinth`);
246
- }
247
-
248
- lockfile.dependencies[fileInfo.category].push(entry);
249
- }
250
-
251
- // Calculate counts for each category
252
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
253
- lockfile.counts[category] = entries.length;
254
- }
255
-
256
- lockfile.total = fileEntries.length;
257
-
258
- return lockfile;
259
- }
260
-
261
- /**
262
- * Write lockfile to disk
263
- */
264
- async function writeLockfile(lockfile, outputPath, log) {
265
- const content = JSON.stringify(lockfile, null, 2);
266
- await fs.writeFile(outputPath, content, 'utf-8');
267
- log(`Lockfile written to: ${outputPath}`);
268
- }
269
-
270
- /**
271
- * Generate README.md content for a category
272
- */
273
- function generateCategoryReadme(category, entries, projectsMap, usersMap) {
274
- const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
275
- const lines = [`# ${categoryTitle}`, '', '| Name | Author | Version |', '|-|-|-|'];
276
-
277
- // Map category to Modrinth URL path segment
278
- const categoryPathMap = {
279
- mods: 'mod',
280
- resourcepacks: 'resourcepack',
281
- shaderpacks: 'shader',
282
- datapacks: 'datapack',
283
- };
284
- const categoryPath = categoryPathMap[category] || 'project';
285
-
286
- for (const entry of entries) {
287
- const version = entry.version;
288
- let nameCell = '';
289
- let authorCell = '';
290
- let versionCell = '';
291
-
292
- if (version && version.project_id) {
293
- const project = projectsMap[version.project_id];
294
- const author = version.author_id ? usersMap[version.author_id] : null;
295
-
296
- // Name column with icon and link
297
- if (project) {
298
- const projectName = project.title || project.slug || 'Unknown';
299
- const projectSlug = project.slug || project.id;
300
- const projectUrl = `https://modrinth.com/${categoryPath}/${projectSlug}`;
301
-
302
- if (project.icon_url) {
303
- nameCell = `<img alt="Icon" src="${project.icon_url}" height="20px"> [${projectName}](${projectUrl})`;
304
- } else {
305
- nameCell = `[${projectName}](${projectUrl})`;
306
- }
307
- } else {
308
- // Project not found, use filename
309
- const fileName = path.basename(entry.path);
310
- nameCell = fileName;
311
- }
312
-
313
- // Author column with avatar and link
314
- if (author) {
315
- const authorName = author.username || 'Unknown';
316
- const authorUrl = `https://modrinth.com/user/${authorName}`;
317
-
318
- if (author.avatar_url) {
319
- authorCell = `<img alt="Avatar" src="${author.avatar_url}" height="20px"> [${authorName}](${authorUrl})`;
320
- } else {
321
- authorCell = `[${authorName}](${authorUrl})`;
322
- }
323
- } else {
324
- authorCell = 'Unknown';
325
- }
326
-
327
- // Version column
328
- versionCell = version.version_number || 'Unknown';
329
- } else {
330
- // File not found on Modrinth
331
- const fileName = path.basename(entry.path);
332
- nameCell = fileName;
333
- authorCell = 'Unknown';
334
- versionCell = '-';
335
- }
336
-
337
- lines.push(`| ${nameCell} | ${authorCell} | ${versionCell} |`);
338
- }
339
-
340
- return lines.join('\n') + '\n';
341
- }
342
-
343
- /**
344
- * Generate .gitignore rules for files not hosted on Modrinth
345
- */
346
- function generateGitignoreRules(lockfile) {
347
- const rules = [];
348
- const exceptions = [];
349
-
350
- // Base ignore patterns for each category
351
- rules.push('mods/*.jar');
352
- rules.push('resourcepacks/*.zip');
353
- rules.push('datapacks/*.zip');
354
- rules.push('shaderpacks/*.zip');
355
- rules.push('');
356
- rules.push('## Exceptions');
357
-
358
- // Find files not hosted on Modrinth
359
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
360
- for (const entry of entries) {
361
- if (entry.version === null) {
362
- exceptions.push(`!${entry.path}`);
363
- }
364
- }
365
- }
366
-
367
- // Add exceptions if any
368
- if (exceptions.length > 0) {
369
- rules.push(...exceptions);
370
- } else {
371
- rules.push('# No exceptions needed - all files are hosted on Modrinth');
372
- }
373
-
374
- return rules.join('\n');
375
- }
376
-
377
- /**
378
- * Main execution function
379
- */
380
- async function generateLockfile(config) {
381
- const log = createLogger(config.quiet);
382
-
383
- if (config.silent) {
384
- silenceConsole();
385
- }
386
-
387
- if (config.dryRun) {
388
- log('[DRY RUN] Preview mode - no files will be written');
389
- }
390
-
391
- log('Scanning directories for modpack files...');
392
-
393
- // Scan all directories
394
- const allFileEntries = [];
395
- for (const dirInfo of getScanDirectories(config.path)) {
396
- log(`Scanning ${dirInfo.name}...`);
397
- const fileEntries = await scanDirectory(dirInfo, config.path);
398
- log(` Found ${fileEntries.length} file(s)`);
399
- allFileEntries.push(...fileEntries);
400
- }
401
-
402
- if (allFileEntries.length === 0) {
403
- log('No files found. Creating empty lockfile.');
404
- const outputPath = path.join(config.path, MODPACK_LOCKFILE_NAME);
405
- if (config.dryRun) {
406
- log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
407
- } else {
408
- await writeLockfile(createEmptyLockfile(), outputPath, log);
409
- }
410
- return;
411
- }
412
-
413
- log(`\nTotal files found: ${allFileEntries.length}`);
414
- log('\nQuerying Modrinth API...');
415
-
416
- // Extract all hashes
417
- const hashes = allFileEntries.map(info => info.hash);
418
-
419
- // Query Modrinth API
420
- const versionData = await getVersionsFromHashes(hashes);
421
-
422
- log(`\nFound version information for ${Object.keys(versionData).length} out of ${hashes.length} files`);
423
-
424
- // Create lockfile
425
- const lockfile = createLockfile(allFileEntries, versionData);
426
-
427
- // Write lockfile
428
- const outputPath = path.join(config.path, MODPACK_LOCKFILE_NAME);
429
- if (config.dryRun) {
430
- log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
431
- } else {
432
- await writeLockfile(lockfile, outputPath, log);
433
- }
434
-
435
- // Summary
436
- log('\n=== Summary ===');
437
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
438
- const withVersion = entries.filter(e => e.version !== null).length;
439
- const withoutVersion = entries.length - withVersion;
440
- log(`${category}: ${entries.length} file(s) (${withVersion} found on Modrinth, ${withoutVersion} unknown)`);
441
- }
442
-
443
- // Generate .gitignore rules
444
- if (config.gitignore) {
445
- log('\n=== .gitignore Rules ===');
446
- log(generateGitignoreRules(lockfile));
447
- }
448
-
449
- // Generate README files
450
- if (config.readme) {
451
- log('\nGenerating README files...');
452
-
453
- // Collect unique project IDs and author IDs from version data
454
- const projectIds = new Set();
455
- const authorIds = new Set();
456
-
457
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
458
- for (const entry of entries) {
459
- if (entry.version && entry.version.project_id) {
460
- projectIds.add(entry.version.project_id);
461
- }
462
- if (entry.version && entry.version.author_id) {
463
- authorIds.add(entry.version.author_id);
464
- }
465
- }
466
- }
467
-
468
- // Fetch projects and users in parallel
469
- log(`Fetching data for ${projectIds.size} project(s) and ${authorIds.size} user(s)...`);
470
-
471
- const [projects, users] = await Promise.all([
472
- getProjects(Array.from(projectIds)),
473
- getUsers(Array.from(authorIds)),
474
- ]);
475
-
476
- // Map projects and users to their IDs
477
- const projectsMap = {};
478
- for (const project of projects) {
479
- projectsMap[project.id] = project;
480
- }
481
-
482
- const usersMap = {};
483
- for (const user of users) {
484
- usersMap[user.id] = user;
485
- }
486
-
487
- // Generate README for each category
488
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
489
- if (entries.length === 0) {
490
- continue;
491
- }
492
-
493
- const readmeContent = generateCategoryReadme(category, entries, projectsMap, usersMap);
494
- const categoryDir = getScanDirectories(config.path).find(d => d.name === category);
495
-
496
- if (categoryDir) {
497
- const readmePath = path.join(categoryDir.path, 'README.md');
498
-
499
- if (config.dryRun) {
500
- log(`[DRY RUN] Would write README to: ${readmePath}`);
501
- } else {
502
- try {
503
- await fs.writeFile(readmePath, readmeContent, 'utf-8');
504
- log(`Generated README: ${readmePath}`);
505
- } catch (error) {
506
- console.warn(`Warning: Could not write README to ${readmePath}: ${error.message}`);
507
- }
508
- }
509
- }
510
- }
511
-
512
- log('README generation complete.');
513
- }
514
- return true;
515
- }
516
-
517
- export default generateLockfile;