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.
@@ -0,0 +1,224 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import {getProjects, getUsers} from "./modrinth_interactions.js";
4
+ import {getScanDirectories} from "./directory_scanning.js";
5
+ import * as config from "./config/index.js";
6
+ import {logm, styleText} from "./logger.js";
7
+
8
+ /**
9
+ * @typedef {import('./config/types.js').Options} Options
10
+ * @typedef {import('./config/types.js').InitOptions} InitOptions
11
+ * @typedef {import('./config/types.js').Lockfile} Lockfile
12
+ */
13
+
14
+ /**
15
+ * Generate README.md content for a category
16
+ */
17
+ function generateCategoryReadme(category, entries, projectsMap, usersMap) {
18
+ const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
19
+ const lines = [`# ${categoryTitle}`, "", "| Name | Author | Version | Dependencies | Dependants |", "|-|-|-|-|-|"];
20
+
21
+ // Map category to Modrinth URL path segment
22
+ const categoryPathMap = {};
23
+ for (const cat of config.DEPENDENCY_CATEGORIES) {
24
+ categoryPathMap[cat] = cat === "shaderpacks" ? "shader" : cat.toLowerCase().slice(0, -1);
25
+ }
26
+ const categoryPath = categoryPathMap[category] || "project";
27
+
28
+ // Build a set of project_ids present in this category for filtering dependencies
29
+ const categoryProjectIds = new Set();
30
+ for (const entry of entries) {
31
+ if (entry.version && entry.version.project_id) {
32
+ categoryProjectIds.add(entry.version.project_id);
33
+ }
34
+ }
35
+
36
+ for (const entry of entries) {
37
+ const version = entry.version;
38
+ let nameCell = "";
39
+ let authorCell = "";
40
+ let versionCell = "";
41
+ let dependenciesCell = "";
42
+ let dependantsCell = "";
43
+
44
+ if (version && version.project_id) {
45
+ const project = projectsMap[version.project_id];
46
+ const author = version.author_id ? usersMap[version.author_id] : null;
47
+
48
+ // Name column with icon and link
49
+ if (project) {
50
+ const projectName = project.title || project.slug || "Unknown";
51
+ const projectSlug = project.slug || project.id;
52
+ const projectUrl = `https://modrinth.com/${categoryPath}/${projectSlug}`;
53
+
54
+ if (project.icon_url) {
55
+ nameCell = `<img alt="Icon" src="${project.icon_url}" height="20px"> [${projectName}](${projectUrl})`;
56
+ } else {
57
+ nameCell = `[${projectName}](${projectUrl})`;
58
+ }
59
+ } else {
60
+ // Project not found, use filename
61
+ const fileName = path.basename(entry.path);
62
+ nameCell = fileName;
63
+ }
64
+
65
+ // Author column with avatar and link
66
+ if (author) {
67
+ const authorName = author.username || "Unknown";
68
+ const authorUrl = `https://modrinth.com/user/${authorName}`;
69
+
70
+ if (author.avatar_url) {
71
+ authorCell = `<img alt="Avatar" src="${author.avatar_url}" height="20px"> [${authorName}](${authorUrl})`;
72
+ } else {
73
+ authorCell = `[${authorName}](${authorUrl})`;
74
+ }
75
+ } else {
76
+ authorCell = "Unknown";
77
+ }
78
+
79
+ // Version column
80
+ versionCell = version.version_number || "Unknown";
81
+
82
+ // Dependencies column - only show dependencies that are present in this category
83
+ if (version.dependencies && Array.isArray(version.dependencies) && version.dependencies.length > 0) {
84
+ const dependencyLinks = [];
85
+ for (const dep of version.dependencies) {
86
+ if (dep.project_id && categoryProjectIds.has(dep.project_id)) {
87
+ const depProject = projectsMap[dep.project_id];
88
+ if (depProject) {
89
+ const depProjectName = depProject.title || depProject.slug || "Unknown";
90
+ const depProjectSlug = depProject.slug || depProject.id;
91
+ const depUrl = `https://modrinth.com/${categoryPath}/${depProjectSlug}`;
92
+ if (depProject.icon_url) {
93
+ dependencyLinks.push(
94
+ `<a href="${depUrl}"><img alt="${depProjectName}" src="${depProject.icon_url}" height="20px"></a>`,
95
+ );
96
+ } else {
97
+ dependencyLinks.push(`[${depProjectName}](${depUrl})`);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ dependenciesCell = dependencyLinks.length > 0 ? dependencyLinks.join(" ") : "-";
103
+ } else {
104
+ dependenciesCell = "-";
105
+ }
106
+
107
+ // Dependants column - find all entries in the same category that depend on this project
108
+ const dependants = [];
109
+ for (const catEntry of entries) {
110
+ // Skip if this is the same entry (same project_id)
111
+ if (catEntry.version && catEntry.version.project_id === version.project_id) {
112
+ continue;
113
+ }
114
+ if (catEntry.version && catEntry.version.dependencies && Array.isArray(catEntry.version.dependencies)) {
115
+ const hasDependency = catEntry.version.dependencies.some(
116
+ (dep) => dep.project_id === version.project_id,
117
+ );
118
+ if (hasDependency) {
119
+ const depProject = projectsMap[catEntry.version.project_id];
120
+ if (depProject) {
121
+ const depProjectName = depProject.title || depProject.slug || "Unknown";
122
+ const depProjectSlug = depProject.slug || depProject.id;
123
+ const depUrl = `https://modrinth.com/${categoryPath}/${depProjectSlug}`;
124
+ if (depProject.icon_url) {
125
+ dependants.push(
126
+ `<a href="${depUrl}"><img alt="${depProjectName}" src="${depProject.icon_url}" height="20px"></a>`,
127
+ );
128
+ } else {
129
+ dependants.push(`[${depProjectName}](${depUrl})`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ dependantsCell = dependants.length > 0 ? dependants.join(" ") : "-";
136
+ } else {
137
+ // File not found on Modrinth
138
+ const fileName = path.basename(entry.path);
139
+ nameCell = fileName;
140
+ authorCell = "Unknown";
141
+ versionCell = "-";
142
+ dependenciesCell = "-";
143
+ dependantsCell = "-";
144
+ }
145
+
146
+ lines.push(`| ${nameCell} | ${authorCell} | ${versionCell} | ${dependenciesCell} | ${dependantsCell} |`);
147
+ }
148
+
149
+ return lines.join("\n") + "\n";
150
+ }
151
+
152
+ /**
153
+ * Generate the README.md files for each category
154
+ * @param {Lockfile} lockfile - The lockfile object
155
+ * @param {string} workingDir - The working directory
156
+ * @param {Options | InitOptions} options - The options object
157
+ */
158
+ export async function generateReadmeFiles(lockfile, workingDir, options = {}) {
159
+ logm.quietFromOptions(options);
160
+
161
+ // Collect unique project IDs and author IDs from version data
162
+ const projectIds = new Set();
163
+ const authorIds = new Set();
164
+
165
+ for (const [category, entries] of Object.entries(lockfile.dependencies)) {
166
+ for (const entry of entries) {
167
+ if (entry.version && entry.version.project_id) {
168
+ projectIds.add(entry.version.project_id);
169
+ }
170
+ if (entry.version && entry.version.author_id) {
171
+ authorIds.add(entry.version.author_id);
172
+ }
173
+ }
174
+ }
175
+
176
+ // Fetch projects and users in parallel
177
+ logm.newline();
178
+ logm.info(styleText(["dim"], "Fetching metadata for:"));
179
+ logm.info(
180
+ styleText(["dim"], " └─"),
181
+ styleText(["yellow"], `${projectIds.size} project(s)`),
182
+ styleText(["dim"], "and"),
183
+ styleText(["yellow"], `${authorIds.size} user(s)`),
184
+ );
185
+ logm.newline();
186
+
187
+ const [projects, users] = await Promise.all([getProjects(Array.from(projectIds)), getUsers(Array.from(authorIds))]);
188
+
189
+ // Map projects and users to their IDs
190
+ const projectsMap = {};
191
+ for (const project of projects) {
192
+ projectsMap[project.id] = project;
193
+ }
194
+
195
+ const usersMap = {};
196
+ for (const user of users) {
197
+ usersMap[user.id] = user;
198
+ }
199
+
200
+ // Generate README for each category
201
+ for (const [category, entries] of Object.entries(lockfile.dependencies)) {
202
+ if (entries.length === 0) {
203
+ continue;
204
+ }
205
+
206
+ const readmeContent = generateCategoryReadme(category, entries, projectsMap, usersMap);
207
+ const categoryDir = getScanDirectories(workingDir).find((d) => d.name === category);
208
+
209
+ if (categoryDir) {
210
+ const readmePath = path.join(categoryDir.path, config.README_NAME);
211
+
212
+ if (options.dryRun) {
213
+ logm.debug(config.dryRunText(config.README_NAME, readmePath));
214
+ } else {
215
+ try {
216
+ await fs.writeFile(readmePath, readmeContent, "utf-8");
217
+ logm.generated(config.README_NAME, readmePath);
218
+ } catch (error) {
219
+ logm.warn(`Could not write ${config.README_NAME} file to ${readmePath}: ${error.message}`);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
@@ -1,5 +1,5 @@
1
- import * as config from './config/index.js';
2
-
1
+ import * as config from "./config/index.js";
2
+ import {logm} from "./logger.js";
3
3
 
4
4
  /**
5
5
  * Fetch a list of the most popular licenses from GitHub
@@ -9,22 +9,19 @@ import * as config from './config/index.js';
9
9
  export async function getLicenseList(featured = false) {
10
10
  try {
11
11
  const url = featured ? config.GITHUB_FEATURED_LICENSES_ENDPOINT : config.GITHUB_LICENSES_ENDPOINT;
12
- const response = await fetch(url,
13
- {
14
- headers: {
15
- 'Accept': config.GITHUB_ACCEPT_HEADER,
16
- 'User-Agent': config.PACKAGE_USER_AGENT
17
- }
18
- }
19
- );
12
+ const response = await fetch(url, {
13
+ headers: {
14
+ Accept: config.GITHUB_ACCEPT_HEADER,
15
+ "User-Agent": config.PACKAGE_USER_AGENT,
16
+ },
17
+ });
20
18
  if (!response.ok) {
21
19
  const errorText = await response.text();
22
20
  throw new Error(`GitHub API error (${response.status}): ${errorText}`);
23
21
  }
24
22
  let licenseList = await response.json();
25
23
 
26
- let licenseSpdxIds = licenseList.map(license => ({ title: license.spdx_id, value: license.key }));
27
-
24
+ let licenseSpdxIds = licenseList.map((license) => ({title: license.spdx_id, value: license.key}));
28
25
 
29
26
  if (!featured) {
30
27
  // get featured licenses and place them at the beginning of the list, removing them from the original list
@@ -32,15 +29,15 @@ export async function getLicenseList(featured = false) {
32
29
  licenseSpdxIds.push(config.OTHER_OPTION);
33
30
  const featuredLicenseList = await getLicenseList(true);
34
31
  for (const license of featuredLicenseList) {
35
- licenseSpdxIds= licenseSpdxIds.filter( id => id !== license );
32
+ licenseSpdxIds = licenseSpdxIds.filter((id) => id.value !== license.value);
36
33
  licenseSpdxIds.unshift(license);
37
- };
34
+ }
38
35
  }
39
36
 
40
37
  return licenseSpdxIds;
41
38
  } catch (error) {
42
- console.warn(`Warning: could not fetch license list. Using fallbacks.`);
43
- const licenses = config.FALLBACK_LICENSES.push(config.ALL_RIGHTS_RESERVED_LICENSE)
39
+ logm.warn(`Could not fetch license list. Using fallbacks.`);
40
+ const licenses = config.FALLBACK_LICENSES.push(config.ALL_RIGHTS_RESERVED_LICENSE);
44
41
  licenses.push(config.OTHER_OPTION);
45
42
  return licenses;
46
43
  }
@@ -52,15 +49,15 @@ export async function getLicenseList(featured = false) {
52
49
  * @returns {Promise<string> | null} The license text
53
50
  */
54
51
  export async function getLicenseText(spdxId) {
55
- if (spdxId === 'all-rights-reserved') {
52
+ if (spdxId === "all-rights-reserved") {
56
53
  return config.ARR_LICENSE_TEXT;
57
54
  }
58
55
  try {
59
56
  const url = config.GITHUB_LICENSE_ENDPOINT(spdxId.toLowerCase());
60
57
  const response = await fetch(url, {
61
58
  headers: {
62
- 'Accept': config.GITHUB_ACCEPT_HEADER,
63
- 'User-Agent': config.PACKAGE_USER_AGENT
59
+ Accept: config.GITHUB_ACCEPT_HEADER,
60
+ "User-Agent": config.PACKAGE_USER_AGENT,
64
61
  },
65
62
  });
66
63
  if (!response.ok) {
@@ -76,10 +73,7 @@ export async function getLicenseText(spdxId) {
76
73
  }
77
74
  return null;
78
75
  } catch (error) {
79
- console.warn(`Warning: could not find license text for: ${spdxId}`);
76
+ logm.warn(`Could not find license text for: ${spdxId}`);
80
77
  return null;
81
78
  }
82
79
  }
83
-
84
-
85
-
package/src/logger.js ADDED
@@ -0,0 +1,178 @@
1
+ import {styleText} from "node:util";
2
+
3
+ class Logger {
4
+ /**
5
+ * The styles to apply to the console output.
6
+ * @type {Object}
7
+ */
8
+ styles = {
9
+ log: null,
10
+ info: null,
11
+ debug: ["magenta"],
12
+ warn: ["yellow"],
13
+ error: ["red"],
14
+ generated: ["green"],
15
+ label: ["inverse", "bold"],
16
+ labelDebug: ["magenta", "inverse", "bold"],
17
+ labelWarn: ["yellow", "inverse", "bold"],
18
+ labelError: ["red", "inverse", "bold"],
19
+ labelGenerated: ["green", "inverse", "bold"],
20
+ };
21
+
22
+ quietConsole = false;
23
+ silentConsole = false;
24
+ lastLogWasNewline = false;
25
+
26
+ quiet(silent = false) {
27
+ this.quietConsole = true;
28
+ this.silentConsole = silent;
29
+ }
30
+
31
+ quietFromOptions(options) {
32
+ if (options.silent) {
33
+ this.quietConsole = true;
34
+ this.silentConsole = true;
35
+ } else if (options.quiet) {
36
+ this.quietConsole = true;
37
+ }
38
+ }
39
+
40
+ styleArgs(style, args) {
41
+ if (!style) {
42
+ return args;
43
+ }
44
+ if (args.length === 0) {
45
+ return "";
46
+ }
47
+ return args.map((arg) => (typeof arg === "string" ? styleText(style, arg) : arg));
48
+ }
49
+
50
+ /**
51
+ * Style the text as a label.
52
+ * @param {string} text - The text to style.
53
+ * @param {string[]} style - The style to apply to the text.
54
+ * @returns {string} The styled text.
55
+ */
56
+ label(text, style = this.styles.label) {
57
+ return styleText(style, String(text).toUpperCase());
58
+ }
59
+
60
+ /**
61
+ * Log a header.
62
+ * @param {string} text - The text to log.
63
+ */
64
+ header(text) {
65
+ if (this.quietConsole) {
66
+ return;
67
+ }
68
+ if (!this.lastLogWasNewline) {
69
+ console.info();
70
+ }
71
+ console.info(this.label(text));
72
+ console.info();
73
+ this.lastLogWasNewline = true;
74
+ }
75
+
76
+ generated(desc, outputPath) {
77
+ if (this.quietConsole) {
78
+ return;
79
+ }
80
+ console.log(
81
+ this.label("Generated", this.styles.labelGenerated),
82
+ styleText(this.styles.generated, "Wrote"),
83
+ styleText(this.styles.generated, desc),
84
+ styleText(this.styles.generated, "to:"),
85
+ styleText(["dim"], `${outputPath}`),
86
+ );
87
+ this.lastLogWasNewline = false;
88
+ }
89
+
90
+ newline() {
91
+ if (this.quietConsole) {
92
+ return;
93
+ }
94
+ if (this.lastLogWasNewline) {
95
+ return;
96
+ }
97
+ console.info();
98
+ this.lastLogWasNewline = true;
99
+ }
100
+
101
+ /**
102
+ * Log a message.
103
+ * @param {string} message - The message to log.
104
+ * @param {...any} otherMessages - The other messages to log.
105
+ */
106
+ log(message, ...otherMessages) {
107
+ if (this.quietConsole) {
108
+ return;
109
+ }
110
+ console.log(...this.styleArgs(this.styles.log, [message, ...otherMessages]));
111
+ this.lastLogWasNewline = false;
112
+ }
113
+
114
+ /**
115
+ * Log an info message.
116
+ * @param {string} message - The message to log.
117
+ * @param {...any} otherMessages - The other messages to log.
118
+ */
119
+ info(message, ...otherMessages) {
120
+ if (this.quietConsole) {
121
+ return;
122
+ }
123
+ console.info(...this.styleArgs(this.styles.info, [message, ...otherMessages]));
124
+ this.lastLogWasNewline = false;
125
+ }
126
+
127
+ /**
128
+ * Log a debug message.
129
+ * @param {string} message - The message to log.
130
+ * @param {...any} otherMessages - The other messages to log.
131
+ */
132
+ debug(message, ...otherMessages) {
133
+ if (this.silentConsole) {
134
+ return;
135
+ }
136
+ console.debug(
137
+ this.label("//", this.styles.labelDebug),
138
+ ...this.styleArgs(this.styles.debug, [message, ...otherMessages]),
139
+ );
140
+ this.lastLogWasNewline = false;
141
+ }
142
+
143
+ /**
144
+ * Log a warning message.
145
+ * @param {string} message - The message to log.
146
+ * @param {...any} otherMessages - The other messages to log.
147
+ */
148
+ warn(message, ...otherMessages) {
149
+ if (this.silentConsole) {
150
+ return;
151
+ }
152
+ console.warn(
153
+ this.label("WARNING", this.styles.labelWarn),
154
+ ...this.styleArgs(this.styles.warn, [message, ...otherMessages]),
155
+ );
156
+ this.lastLogWasNewline = false;
157
+ }
158
+
159
+ /**
160
+ * Log an error message.
161
+ * @param {string} message - The message to log.
162
+ * @param {...any} otherMessages - The other messages to log.
163
+ */
164
+ error(message, ...otherMessages) {
165
+ if (this.silentConsole) {
166
+ return;
167
+ }
168
+ console.error(
169
+ this.label("ERROR", this.styles.labelError),
170
+ ...this.styleArgs(this.styles.error, [message, ...otherMessages]),
171
+ );
172
+ this.lastLogWasNewline = false;
173
+ }
174
+ }
175
+
176
+ const logm = new Logger();
177
+
178
+ export {logm, styleText};
@@ -1,8 +1,11 @@
1
- import { generateLockfile, generateReadmeFiles, generateGitignoreRules } from './generate_lockfile.js';
2
- import generateJson from './generate_json.js';
3
- import generateLicense from './generate_license.js';
4
- import { promptUserForInfo } from './modpack_info.js';
5
- import { getModpackInfo, getLockfile } from './directory_scanning.js';
1
+ import {generateLockfile} from "./generate_lockfile.js";
2
+ import {generateReadmeFiles} from "./generate_readme.js";
3
+ import {generateGitignoreRules} from "./generate_gitignore.js";
4
+ import generateJson from "./generate_json.js";
5
+ import generateLicense from "./generate_license.js";
6
+ import {logm} from "./logger.js";
7
+ import {promptUserForInfo} from "./modpack_info.js";
8
+ import {getModpackInfo, getLockfile} from "./directory_scanning.js";
6
9
 
7
10
  /**
8
11
  * @typedef {import('./config/types.js').ModpackInfo} ModpackInfo
@@ -18,16 +21,49 @@ import { getModpackInfo, getLockfile } from './directory_scanning.js';
18
21
  */
19
22
 
20
23
  /**
21
- * Generate the modpack files (lockfile and JSON)
24
+ * Generate the modpack files (lockfile, JSON, and optionally license, gitignore, and readme)
22
25
  * @param {ModpackInfo} modpackInfo - The modpack information
23
- * @param {string} directory - The directory to generate the files in
26
+ * @param {string} workingDir - The directory to generate the files in
24
27
  * @param {Options | InitOptions } options - The options object
25
28
  * @returns {Promise<Lockfile>} The lockfile object
26
29
  */
27
- async function generateModpackFiles(modpackInfo, directory, options = {}) {
28
- const lockfile = await generateLockfile(directory, options);
29
- await generateJson(modpackInfo, lockfile, directory, options);
30
+ async function generateModpackFiles(modpackInfo, workingDir, options = {}) {
31
+ logm.quietFromOptions(options);
32
+
33
+ const lockfile = await generateLockfile(workingDir, options);
34
+
35
+ await generateJson(modpackInfo, lockfile, workingDir, options);
36
+
37
+ if (options.licenseFile || options.gitignore || options.readme) {
38
+ logm.header("Generating Optional Files");
39
+ }
40
+
41
+ // Generate license if requested
42
+ if (options.licenseFile) {
43
+ await generateLicense(modpackInfo, workingDir, options);
44
+ }
45
+
46
+ // Generate gitignore if requested
47
+ if (options.gitignore) {
48
+ await generateGitignoreRules(lockfile, workingDir, options);
49
+ }
50
+
51
+ // Generate README files if requested
52
+ if (options.readme) {
53
+ await generateReadmeFiles(lockfile, workingDir, options);
54
+ }
55
+
30
56
  return lockfile;
31
57
  }
32
58
 
33
- export { generateModpackFiles, generateJson, generateLockfile, generateGitignoreRules, generateReadmeFiles, generateLicense, getModpackInfo, getLockfile, promptUserForInfo };
59
+ export {
60
+ generateModpackFiles,
61
+ generateJson,
62
+ generateLockfile,
63
+ generateGitignoreRules,
64
+ generateReadmeFiles,
65
+ generateLicense,
66
+ getModpackInfo,
67
+ getLockfile,
68
+ promptUserForInfo,
69
+ };