modpack-lock 0.5.1 → 0.6.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.
@@ -1,8 +1,9 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { getVersionsFromHashes, getProjects, getUsers } from './modrinth_interactions.js';
4
- import { getScanDirectories, scanDirectory } from './directory_scanning.js';
5
- import * as config from './config/index.js';
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import {getVersionsFromHashes} from "./modrinth_interactions.js";
4
+ import {getScanDirectories, scanDirectory} from "./directory_scanning.js";
5
+ import * as config from "./config/index.js";
6
+ import {logm, styleText} from "./logger.js";
6
7
 
7
8
  /**
8
9
  * @typedef {import('./config/types.js').Options} Options
@@ -29,6 +30,7 @@ function createEmptyLockfile() {
29
30
  function createLockfile(fileEntries, versionData) {
30
31
  const lockfile = createEmptyLockfile();
31
32
 
33
+ logm.newline();
32
34
  // Organize by category
33
35
  for (const fileInfo of fileEntries) {
34
36
  const version = versionData[fileInfo.hash];
@@ -41,12 +43,14 @@ function createLockfile(fileEntries, versionData) {
41
43
  };
42
44
 
43
45
  if (!version) {
44
- console.warn(`Warning: File ${fileInfo.path} not found on Modrinth`);
46
+ logm.warn(`File ${fileInfo.path} not found on Modrinth`);
45
47
  }
46
48
 
47
49
  lockfile.dependencies[fileInfo.category].push(entry);
48
50
  }
49
51
 
52
+ logm.header("Generating Lockfile");
53
+
50
54
  // Calculate counts for each category
51
55
  for (const [category, entries] of Object.entries(lockfile.dependencies)) {
52
56
  lockfile.counts[category] = entries.length;
@@ -62,331 +66,8 @@ function createLockfile(fileEntries, versionData) {
62
66
  */
63
67
  async function writeLockfile(lockfile, outputPath) {
64
68
  const content = JSON.stringify(lockfile, null, 2);
65
- await fs.writeFile(outputPath, content, 'utf-8');
66
- console.log(`Lockfile written to: ${outputPath}`);
67
- }
68
-
69
- /**
70
- * Generate README.md content for a category
71
- */
72
- function generateCategoryReadme(category, entries, projectsMap, usersMap) {
73
- const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
74
- const lines = [`# ${categoryTitle}`, '', '| Name | Author | Version | Dependencies | Dependants |', '|-|-|-|-|-|'];
75
-
76
- // Map category to Modrinth URL path segment
77
- const categoryPathMap = {};
78
- for (const category of config.DEPENDENCY_CATEGORIES) {
79
- categoryPathMap[category] = category === 'shaderpacks' ? 'shader' : category.toLowerCase().slice(0, -1);
80
- }
81
- const categoryPath = categoryPathMap[category] || 'project';
82
-
83
- // Build a set of project_ids present in this category for filtering dependencies
84
- const categoryProjectIds = new Set();
85
- for (const entry of entries) {
86
- if (entry.version && entry.version.project_id) {
87
- categoryProjectIds.add(entry.version.project_id);
88
- }
89
- }
90
-
91
- for (const entry of entries) {
92
- const version = entry.version;
93
- let nameCell = '';
94
- let authorCell = '';
95
- let versionCell = '';
96
- let dependenciesCell = '';
97
- let dependantsCell = '';
98
-
99
- if (version && version.project_id) {
100
- const project = projectsMap[version.project_id];
101
- const author = version.author_id ? usersMap[version.author_id] : null;
102
-
103
- // Name column with icon and link
104
- if (project) {
105
- const projectName = project.title || project.slug || 'Unknown';
106
- const projectSlug = project.slug || project.id;
107
- const projectUrl = `https://modrinth.com/${categoryPath}/${projectSlug}`;
108
-
109
- if (project.icon_url) {
110
- nameCell = `<img alt="Icon" src="${project.icon_url}" height="20px"> [${projectName}](${projectUrl})`;
111
- } else {
112
- nameCell = `[${projectName}](${projectUrl})`;
113
- }
114
- } else {
115
- // Project not found, use filename
116
- const fileName = path.basename(entry.path);
117
- nameCell = fileName;
118
- }
119
-
120
- // Author column with avatar and link
121
- if (author) {
122
- const authorName = author.username || 'Unknown';
123
- const authorUrl = `https://modrinth.com/user/${authorName}`;
124
-
125
- if (author.avatar_url) {
126
- authorCell = `<img alt="Avatar" src="${author.avatar_url}" height="20px"> [${authorName}](${authorUrl})`;
127
- } else {
128
- authorCell = `[${authorName}](${authorUrl})`;
129
- }
130
- } else {
131
- authorCell = 'Unknown';
132
- }
133
-
134
- // Version column
135
- versionCell = version.version_number || 'Unknown';
136
-
137
- // Dependencies column - only show dependencies that are present in this category
138
- if (version.dependencies && Array.isArray(version.dependencies) && version.dependencies.length > 0) {
139
- const dependencyLinks = [];
140
- for (const dep of version.dependencies) {
141
- if (dep.project_id && categoryProjectIds.has(dep.project_id)) {
142
- const depProject = projectsMap[dep.project_id];
143
- if (depProject) {
144
- const depProjectName = depProject.title || depProject.slug || 'Unknown';
145
- const depProjectSlug = depProject.slug || depProject.id;
146
- const depUrl = `https://modrinth.com/${categoryPath}/${depProjectSlug}`;
147
- if (depProject.icon_url) {
148
- dependencyLinks.push(`<a href="${depUrl}"><img alt="${depProjectName}" src="${depProject.icon_url}" height="20px"></a>`);
149
- } else {
150
- dependencyLinks.push(`[${depProjectName}](${depUrl})`);
151
- }
152
- }
153
- }
154
- }
155
- dependenciesCell = dependencyLinks.length > 0 ? dependencyLinks.join(' ') : '-';
156
- } else {
157
- dependenciesCell = '-';
158
- }
159
-
160
- // Dependants column - find all entries in the same category that depend on this project
161
- const dependants = [];
162
- for (const catEntry of entries) {
163
- // Skip if this is the same entry (same project_id)
164
- if (catEntry.version && catEntry.version.project_id === version.project_id) {
165
- continue;
166
- }
167
- if (catEntry.version && catEntry.version.dependencies && Array.isArray(catEntry.version.dependencies)) {
168
- const hasDependency = catEntry.version.dependencies.some(
169
- dep => dep.project_id === version.project_id
170
- );
171
- if (hasDependency) {
172
- const depProject = projectsMap[catEntry.version.project_id];
173
- if (depProject) {
174
- const depProjectName = depProject.title || depProject.slug || 'Unknown';
175
- const depProjectSlug = depProject.slug || depProject.id;
176
- const depUrl = `https://modrinth.com/${categoryPath}/${depProjectSlug}`;
177
- if (depProject.icon_url) {
178
- dependants.push(`<a href="${depUrl}"><img alt="${depProjectName}" src="${depProject.icon_url}" height="20px"></a>`);
179
- } else {
180
- dependants.push(`[${depProjectName}](${depUrl})`);
181
- }
182
- }
183
- }
184
- }
185
- }
186
- dependantsCell = dependants.length > 0 ? dependants.join(' ') : '-';
187
- } else {
188
- // File not found on Modrinth
189
- const fileName = path.basename(entry.path);
190
- nameCell = fileName;
191
- authorCell = 'Unknown';
192
- versionCell = '-';
193
- dependenciesCell = '-';
194
- dependantsCell = '-';
195
- }
196
-
197
- lines.push(`| ${nameCell} | ${authorCell} | ${versionCell} | ${dependenciesCell} | ${dependantsCell} |`);
198
- }
199
-
200
- return lines.join('\n') + '\n';
201
- }
202
-
203
- /**
204
- * Generate .gitignore rules for files not hosted on Modrinth and write them to .gitignore file
205
- * @param {Lockfile} lockfile - The lockfile object
206
- * @param {string} workingDir - The working directory
207
- * @param {Options | InitOptions} options - The options object
208
- */
209
- export async function generateGitignoreRules(lockfile, workingDir, options = {}) {
210
- const rules = [];
211
- const exceptions = [];
212
-
213
- // Base ignore patterns for each category
214
- for (const category of config.DEPENDENCY_CATEGORIES) {
215
- rules.push(`${category}/*.${category === "mods" ? "jar" : "zip"}`);
216
- }
217
- rules.push(`*/**/*.disabled`);
218
-
219
- // Find files not hosted on Modrinth
220
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
221
- for (const entry of entries) {
222
- if (entry.version === null) {
223
- exceptions.push(`!${entry.path}`);
224
- }
225
- }
226
- }
227
-
228
- // Add exceptions if any
229
- if (exceptions.length > 0) {
230
- rules.push('\n## Exceptions');
231
- rules.push(...exceptions);
232
- }
233
-
234
- const rulesContent = rules.join('\n');
235
- const gitignorePath = path.join(workingDir, config.GITIGNORE_NAME);
236
-
237
- // Read existing .gitignore file if it exists
238
- let existingContent = '';
239
- try {
240
- existingContent = await fs.readFile(gitignorePath, 'utf-8');
241
- } catch (error) {
242
- // File doesn't exist, that's okay - we'll create it
243
- if (error.code !== 'ENOENT') {
244
- console.warn(`Warning: Could not read .gitignore file: ${error.message}`);
245
- return;
246
- }
247
- }
248
-
249
- // Find markers in existing content
250
- const startMarkerIndex = existingContent.indexOf(config.GITIGNORE_START_MARKER);
251
- const endMarkerIndex = existingContent.indexOf(config.GITIGNORE_END_MARKER);
252
-
253
- let newContent = '';
254
-
255
- if (startMarkerIndex !== -1 && endMarkerIndex !== -1 && endMarkerIndex > startMarkerIndex) {
256
- // Both markers exist, replace content between them
257
- const beforeSection = existingContent.substring(0, startMarkerIndex);
258
- const afterSection = existingContent.substring(endMarkerIndex + config.GITIGNORE_END_MARKER.length);
259
-
260
- // Remove trailing newlines from before section and leading newlines from after section
261
- const beforeTrimmed = beforeSection.replace(/\n+$/, '');
262
- const afterTrimmed = afterSection.replace(/^\n+/, '');
263
-
264
- const parts = [beforeTrimmed];
265
- if (beforeTrimmed) parts.push(''); // Add separator if there's content before
266
- parts.push(
267
- config.GITIGNORE_START_MARKER,
268
- rulesContent,
269
- config.GITIGNORE_END_MARKER
270
- );
271
- if (afterTrimmed) {
272
- parts.push(''); // Add separator if there's content after
273
- parts.push(afterTrimmed);
274
- }
275
-
276
- newContent = parts.join('\n');
277
- } else if (startMarkerIndex !== -1 || endMarkerIndex !== -1) {
278
- // Only one marker exists, append to end
279
- const trimmed = existingContent.replace(/\n+$/, '');
280
- newContent = [
281
- trimmed,
282
- '',
283
- config.GITIGNORE_START_MARKER,
284
- rulesContent,
285
- config.GITIGNORE_END_MARKER
286
- ].join('\n');
287
- } else {
288
- // No markers exist, append to end
289
- if (existingContent.trim() === '') {
290
- // File is empty or only whitespace
291
- newContent = [
292
- config.GITIGNORE_START_MARKER,
293
- rulesContent,
294
- config.GITIGNORE_END_MARKER
295
- ].join('\n');
296
- } else {
297
- // File has content, append with newline
298
- const trimmed = existingContent.replace(/\n+$/, '');
299
- newContent = [
300
- trimmed,
301
- '',
302
- config.GITIGNORE_START_MARKER,
303
- rulesContent,
304
- config.GITIGNORE_END_MARKER
305
- ].join('\n');
306
- }
307
- }
308
-
309
- // Write the updated content
310
- if (options.dryRun) {
311
- console.log(config.dryRunText(config.GITIGNORE_NAME, gitignorePath));
312
- console.log();
313
- } else {
314
- try {
315
- await fs.writeFile(gitignorePath, newContent, 'utf-8');
316
- console.log(`Updated .gitignore: ${gitignorePath}`);
317
- } catch (error) {
318
- console.warn(`Warning: Could not write .gitignore file: ${error.message}`);
319
- }
320
- }
321
- }
322
-
323
- /**
324
- * Generate the README.md files for each category
325
- * @param {Lockfile} lockfile - The lockfile object
326
- * @param {string} workingDir - The working directory
327
- * @param {Options | InitOptions} options - The options object
328
- */
329
- export async function generateReadmeFiles(lockfile, workingDir, options = {}) {
330
- // Collect unique project IDs and author IDs from version data
331
- const projectIds = new Set();
332
- const authorIds = new Set();
333
-
334
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
335
- for (const entry of entries) {
336
- if (entry.version && entry.version.project_id) {
337
- projectIds.add(entry.version.project_id);
338
- }
339
- if (entry.version && entry.version.author_id) {
340
- authorIds.add(entry.version.author_id);
341
- }
342
- }
343
- }
344
-
345
- // Fetch projects and users in parallel
346
- console.log(`Fetching data for ${projectIds.size} project(s) and ${authorIds.size} user(s)...`);
347
-
348
- const [projects, users] = await Promise.all([
349
- getProjects(Array.from(projectIds)),
350
- getUsers(Array.from(authorIds)),
351
- ]);
352
-
353
- // Map projects and users to their IDs
354
- const projectsMap = {};
355
- for (const project of projects) {
356
- projectsMap[project.id] = project;
357
- }
358
-
359
- const usersMap = {};
360
- for (const user of users) {
361
- usersMap[user.id] = user;
362
- }
363
-
364
- // Generate README for each category
365
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
366
- if (entries.length === 0) {
367
- continue;
368
- }
369
-
370
- const readmeContent = generateCategoryReadme(category, entries, projectsMap, usersMap);
371
- const categoryDir = getScanDirectories(workingDir).find(d => d.name === category);
372
-
373
- if (categoryDir) {
374
- const readmePath = path.join(categoryDir.path, config.README_NAME);
375
-
376
- if (options.dryRun) {
377
- console.log(config.dryRunText(config.README_NAME, readmePath));
378
- } else {
379
- try {
380
- await fs.writeFile(readmePath, readmeContent, 'utf-8');
381
- console.log(`Generated README: ${readmePath}`);
382
- } catch (error) {
383
- console.warn(`Warning: Could not write README to ${readmePath}: ${error.message}`);
384
- }
385
- }
386
- }
387
- }
388
-
389
- console.log('README generation complete.');
69
+ await fs.writeFile(outputPath, content, "utf-8");
70
+ logm.generated(config.MODPACK_LOCKFILE_NAME, outputPath);
390
71
  }
391
72
 
392
73
  /**
@@ -396,46 +77,61 @@ export async function generateReadmeFiles(lockfile, workingDir, options = {}) {
396
77
  * @returns {Lockfile} The lockfile object
397
78
  */
398
79
  export async function generateLockfile(workingDir, options = {}) {
399
- console.log('Scanning directories for modpack files...');
80
+ logm.quietFromOptions(options);
81
+
82
+ logm.header("Scanning Directories");
400
83
 
401
84
  // Scan all directories
402
85
  const allFileEntries = [];
403
86
  for (const dirInfo of getScanDirectories(workingDir)) {
404
- console.log(`Scanning ${dirInfo.name}...`);
87
+ logm.info(styleText(["cyan"], `${dirInfo.name}/`));
405
88
  const fileEntries = await scanDirectory(dirInfo, workingDir);
406
- console.log(` Found ${fileEntries.length} file(s)`);
89
+ logm.info(
90
+ styleText(["dim"], ` └─ Found`),
91
+ styleText(["yellow"], `${fileEntries.length}`),
92
+ styleText(["dim"], `file${fileEntries.length !== 1 ? "s" : ""}`),
93
+ );
407
94
  allFileEntries.push(...fileEntries);
408
95
  }
409
96
 
410
97
  // Sort file entries
411
98
  allFileEntries.sort((a, b) => {
412
99
  if (a.category !== b.category) {
413
- return a.category.localeCompare(b.category, 'en', { sensitivity: 'base' });
100
+ return a.category.localeCompare(b.category, "en", {sensitivity: "base"});
414
101
  }
415
- return a.path.localeCompare(b.path, 'en', { numeric: true, sensitivity: 'base' });
102
+ return a.path.localeCompare(b.path, "en", {numeric: true, sensitivity: "base"});
416
103
  });
417
104
 
418
105
  if (allFileEntries.length === 0) {
419
- console.log('No files found. Creating empty lockfile.');
106
+ logm.header("GENERATING LOCKFILE");
107
+ logm.warn("No files found. Creating empty lockfile.");
108
+ const emptyLockfile = createEmptyLockfile();
420
109
  const outputPath = path.join(workingDir, config.MODPACK_LOCKFILE_NAME);
421
110
  if (options.dryRun) {
422
- console.log(config.dryRunText(config.MODPACK_LOCKFILE_NAME, outputPath));
111
+ logm.debug(config.dryRunText(config.MODPACK_LOCKFILE_NAME, outputPath));
423
112
  } else {
424
- await writeLockfile(createEmptyLockfile(), outputPath);
113
+ await writeLockfile(emptyLockfile, outputPath);
425
114
  }
426
- return;
115
+ return emptyLockfile;
427
116
  }
428
117
 
429
- console.log(`\nTotal files found: ${allFileEntries.length}`);
430
- console.log('\nQuerying Modrinth API...');
118
+ logm.info(styleText(["dim"], "Total:"), allFileEntries.length);
119
+ logm.header("Querying Modrinth API");
431
120
 
432
121
  // Extract all hashes
433
- const hashes = allFileEntries.map(info => info.hash);
122
+ const hashes = allFileEntries.map((info) => info.hash);
434
123
 
435
124
  // Query Modrinth API
436
125
  const versionData = await getVersionsFromHashes(hashes);
437
126
 
438
- console.log(`\nFound version information for ${Object.keys(versionData).length} out of ${hashes.length} files`);
127
+ logm.info(styleText(["dim"], "Found version information for:"));
128
+ logm.info(
129
+ styleText(["dim"], " └─"),
130
+ styleText(["green"], `${Object.keys(versionData).length}`),
131
+ styleText(["dim"], "out of"),
132
+ styleText(["yellow"], `${hashes.length}`),
133
+ styleText(["dim"], "files"),
134
+ );
439
135
 
440
136
  // Create lockfile
441
137
  const lockfile = createLockfile(allFileEntries, versionData);
@@ -443,29 +139,40 @@ export async function generateLockfile(workingDir, options = {}) {
443
139
  // Write lockfile
444
140
  const outputPath = path.join(workingDir, config.MODPACK_LOCKFILE_NAME);
445
141
  if (options.dryRun) {
446
- console.log(config.dryRunText(config.MODPACK_LOCKFILE_NAME, outputPath));
142
+ logm.debug(config.dryRunText(config.MODPACK_LOCKFILE_NAME, outputPath));
447
143
  } else {
448
144
  await writeLockfile(lockfile, outputPath);
449
145
  }
450
146
 
451
- // Summary
452
- console.log('\n=== Summary ===');
453
- for (const [category, entries] of Object.entries(lockfile.dependencies)) {
454
- const withVersion = entries.filter(e => e.version !== null).length;
455
- const withoutVersion = entries.length - withVersion;
456
- console.log(`${category}: ${entries.length} file(s) (${withVersion} found on Modrinth, ${withoutVersion} unknown)`);
457
- }
147
+ return lockfile;
148
+ }
458
149
 
459
- // Generate .gitignore rules
460
- if (options.gitignore) {
461
- await generateGitignoreRules(lockfile, workingDir, options);
462
- }
150
+ /**
151
+ * Print a summary of the lockfile contents
152
+ * @param {Lockfile} lockfile - The lockfile object
153
+ */
154
+ export function printLockfileSummary(lockfile) {
155
+ logm.header("Lockfile Summary");
463
156
 
464
- // Generate README files
465
- if (options.readme) {
466
- console.log('\nGenerating README files...');
467
- await generateReadmeFiles(lockfile, workingDir, options);
157
+ if (lockfile.total === 0) {
158
+ logm.info(styleText(["dim"], "No files found. Empty lockfile created."));
159
+ return;
468
160
  }
469
161
 
470
- return lockfile;
162
+ for (const [category, entries] of Object.entries(lockfile.dependencies)) {
163
+ const withVersion = entries.filter((e) => e.version !== null).length;
164
+ const withoutVersion = entries.length - withVersion;
165
+ logm.info(
166
+ styleText(["bold"], `${category}:`),
167
+ entries.length,
168
+ styleText(["dim"], `file${entries.length !== 1 ? "s" : ""}`),
169
+ );
170
+ logm.info(
171
+ styleText(["dim"], " └─"),
172
+ styleText(["green"], String(withVersion)),
173
+ styleText(["dim"], "found,"),
174
+ styleText(["yellow"], String(withoutVersion)),
175
+ styleText(["dim"], "unknown"),
176
+ );
177
+ }
471
178
  }