modpack-lock 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/index.js +218 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,6 +47,7 @@ Flags:
47
47
  - `--quiet` or `-q`: Print only errors and warnings
48
48
  - `--silent` or `-s`: Print nothing
49
49
  - `--gitignore` or `-g`: Print the rules to add to your `.gitignore` file
50
+ - `--readme` or `-r`: Generate README.md files with lists of all content for each category inside their directories
50
51
 
51
52
  The script will:
52
53
 
package/index.js CHANGED
@@ -8,6 +8,9 @@ const LOCKFILE_VERSION = '1.0.1';
8
8
  const MODPACK_LOCKFILE_NAME = 'modpack.lock';
9
9
  const MODRINTH_API_BASE = 'https://api.modrinth.com/v2';
10
10
  const MODRINTH_VERSION_FILES_ENDPOINT = `${MODRINTH_API_BASE}/version_files`;
11
+ const MODRINTH_PROJECTS_ENDPOINT = `${MODRINTH_API_BASE}/projects`;
12
+ const MODRINTH_USERS_ENDPOINT = `${MODRINTH_API_BASE}/users`;
13
+ const BATCH_SIZE = 100; // Safe limit for URL length
11
14
 
12
15
  // Get the workspace root from the current working directory
13
16
  const WORKSPACE_ROOT = process.cwd();
@@ -22,6 +25,7 @@ function parseArgs() {
22
25
  quiet: args.includes('--quiet') || args.includes('-q'),
23
26
  silent: args.includes('--silent') || args.includes('-s'),
24
27
  gitignore: args.includes('--gitignore') || args.includes('-g'),
28
+ readme: args.includes('--readme') || args.includes('-r'),
25
29
  };
26
30
  }
27
31
 
@@ -143,6 +147,81 @@ async function getVersionsFromHashes(hashes) {
143
147
  }
144
148
  }
145
149
 
150
+ /**
151
+ * Split an array into chunks of specified size
152
+ */
153
+ function chunkArray(array, size) {
154
+ const chunks = [];
155
+ for (let i = 0; i < array.length; i += size) {
156
+ chunks.push(array.slice(i, i + size));
157
+ }
158
+ return chunks;
159
+ }
160
+
161
+ /**
162
+ * Fetch multiple projects by their IDs (batched to avoid URL length limits)
163
+ */
164
+ async function getProjects(projectIds) {
165
+ if (projectIds.length === 0) {
166
+ return [];
167
+ }
168
+
169
+ const chunks = chunkArray(projectIds, BATCH_SIZE);
170
+ const results = [];
171
+
172
+ for (const chunk of chunks) {
173
+ try {
174
+ const url = `${MODRINTH_PROJECTS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
175
+ const response = await fetch(url);
176
+
177
+ if (!response.ok) {
178
+ const errorText = await response.text();
179
+ throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
180
+ }
181
+
182
+ const data = await response.json();
183
+ results.push(...data);
184
+ } catch (error) {
185
+ console.error(`Error fetching projects: ${error.message}`);
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ return results;
191
+ }
192
+
193
+ /**
194
+ * Fetch multiple users by their IDs (batched to avoid URL length limits)
195
+ */
196
+ async function getUsers(userIds) {
197
+ if (userIds.length === 0) {
198
+ return [];
199
+ }
200
+
201
+ const chunks = chunkArray(userIds, BATCH_SIZE);
202
+ const results = [];
203
+
204
+ for (const chunk of chunks) {
205
+ try {
206
+ const url = `${MODRINTH_USERS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
207
+ const response = await fetch(url);
208
+
209
+ if (!response.ok) {
210
+ const errorText = await response.text();
211
+ throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
212
+ }
213
+
214
+ const data = await response.json();
215
+ results.push(...data);
216
+ } catch (error) {
217
+ console.error(`Error fetching users: ${error.message}`);
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ return results;
223
+ }
224
+
146
225
 
147
226
  /**
148
227
  * Create empty lockfile structure
@@ -201,6 +280,79 @@ async function writeLockfile(lockfile, outputPath, log) {
201
280
  log(`Lockfile written to: ${outputPath}`);
202
281
  }
203
282
 
283
+ /**
284
+ * Generate README.md content for a category
285
+ */
286
+ function generateCategoryReadme(category, entries, projectsMap, usersMap) {
287
+ const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
288
+ const lines = [`# ${categoryTitle}`, '', '| Name | Author | Version |', '|-|-|-|'];
289
+
290
+ // Map category to Modrinth URL path segment
291
+ const categoryPathMap = {
292
+ mods: 'mod',
293
+ resourcepacks: 'resourcepack',
294
+ shaderpacks: 'shader',
295
+ datapacks: 'datapack',
296
+ };
297
+ const categoryPath = categoryPathMap[category] || 'project';
298
+
299
+ for (const entry of entries) {
300
+ const version = entry.version;
301
+ let nameCell = '';
302
+ let authorCell = '';
303
+ let versionCell = '';
304
+
305
+ if (version && version.project_id) {
306
+ const project = projectsMap[version.project_id];
307
+ const author = version.author_id ? usersMap[version.author_id] : null;
308
+
309
+ // Name column with icon and link
310
+ if (project) {
311
+ const projectName = project.title || project.slug || 'Unknown';
312
+ const projectSlug = project.slug || project.id;
313
+ const projectUrl = `https://modrinth.com/${categoryPath}/${projectSlug}`;
314
+
315
+ if (project.icon_url) {
316
+ nameCell = `<img alt="Icon" src="${project.icon_url}" height="20px"> [${projectName}](${projectUrl})`;
317
+ } else {
318
+ nameCell = `[${projectName}](${projectUrl})`;
319
+ }
320
+ } else {
321
+ // Project not found, use filename
322
+ const fileName = path.basename(entry.path);
323
+ nameCell = fileName;
324
+ }
325
+
326
+ // Author column with avatar and link
327
+ if (author) {
328
+ const authorName = author.username || 'Unknown';
329
+ const authorUrl = `https://modrinth.com/user/${authorName}`;
330
+
331
+ if (author.avatar_url) {
332
+ authorCell = `<img alt="Avatar" src="${author.avatar_url}" height="20px"> [${authorName}](${authorUrl})`;
333
+ } else {
334
+ authorCell = `[${authorName}](${authorUrl})`;
335
+ }
336
+ } else {
337
+ authorCell = 'Unknown';
338
+ }
339
+
340
+ // Version column
341
+ versionCell = version.version_number || 'Unknown';
342
+ } else {
343
+ // File not found on Modrinth
344
+ const fileName = path.basename(entry.path);
345
+ nameCell = fileName;
346
+ authorCell = 'Unknown';
347
+ versionCell = '-';
348
+ }
349
+
350
+ lines.push(`| ${nameCell} | ${authorCell} | ${versionCell} |`);
351
+ }
352
+
353
+ return lines.join('\n') + '\n';
354
+ }
355
+
204
356
  /**
205
357
  * Generate .gitignore rules for files not hosted on Modrinth
206
358
  */
@@ -307,6 +459,72 @@ async function main() {
307
459
  log('\n=== .gitignore Rules ===');
308
460
  log(generateGitignoreRules(lockfile));
309
461
  }
462
+
463
+ // Generate README files if requested
464
+ if (config.readme) {
465
+ log('\nGenerating README files...');
466
+
467
+ // Collect unique project IDs and author IDs from version data
468
+ const projectIds = new Set();
469
+ const authorIds = new Set();
470
+
471
+ for (const [category, entries] of Object.entries(lockfile.dependencies)) {
472
+ for (const entry of entries) {
473
+ if (entry.version && entry.version.project_id) {
474
+ projectIds.add(entry.version.project_id);
475
+ }
476
+ if (entry.version && entry.version.author_id) {
477
+ authorIds.add(entry.version.author_id);
478
+ }
479
+ }
480
+ }
481
+
482
+ // Fetch projects and users in parallel
483
+ log(`Fetching data for ${projectIds.size} project(s) and ${authorIds.size} user(s)...`);
484
+
485
+ const [projects, users] = await Promise.all([
486
+ getProjects(Array.from(projectIds)),
487
+ getUsers(Array.from(authorIds)),
488
+ ]);
489
+
490
+ // Create maps for easy lookup
491
+ const projectsMap = {};
492
+ for (const project of projects) {
493
+ projectsMap[project.id] = project;
494
+ }
495
+
496
+ const usersMap = {};
497
+ for (const user of users) {
498
+ usersMap[user.id] = user;
499
+ }
500
+
501
+ // Generate README for each category
502
+ for (const [category, entries] of Object.entries(lockfile.dependencies)) {
503
+ if (entries.length === 0) {
504
+ continue; // Skip empty categories
505
+ }
506
+
507
+ const readmeContent = generateCategoryReadme(category, entries, projectsMap, usersMap);
508
+ const categoryDir = DIRECTORIES_TO_SCAN.find(d => d.name === category);
509
+
510
+ if (categoryDir) {
511
+ const readmePath = path.join(categoryDir.path, 'README.md');
512
+
513
+ if (config.dryRun) {
514
+ log(`[DRY RUN] Would write README to: ${readmePath}`);
515
+ } else {
516
+ try {
517
+ await fs.writeFile(readmePath, readmeContent, 'utf-8');
518
+ log(`Generated README: ${readmePath}`);
519
+ } catch (error) {
520
+ console.warn(`Warning: Could not write README to ${readmePath}: ${error.message}`);
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ log('README generation complete.');
527
+ }
310
528
  }
311
529
 
312
530
  main().catch(error => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modpack-lock",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)",
5
5
  "bugs": {
6
6
  "url": "https://github.com/nickesc/modpack-lock/issues"