diff --git a/development/builder.js b/development/builder.js index 23a6d9714b..f2dfb46648 100644 --- a/development/builder.js +++ b/development/builder.js @@ -3,6 +3,7 @@ const AdmZip = require("adm-zip"); const pathUtil = require("path"); const compatibilityAliases = require("./compatibility-aliases"); const parseMetadata = require("./parse-extension-metadata"); +const { mkdirp, recursiveReadDirectory } = require("./fs-utils"); /** * @typedef {'development'|'production'|'desktop'} Mode @@ -14,52 +15,6 @@ const parseMetadata = require("./parse-extension-metadata"); * @property {string} developer_comment Helper text to help translators */ -/** - * Recursively read a directory. - * @param {string} directory - * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. - * The return result includes files in subdirectories, but not the subdirectories themselves. - */ -const recursiveReadDirectory = (directory) => { - const result = []; - for (const name of fs.readdirSync(directory)) { - if (name.startsWith(".")) { - // Ignore .eslintrc.js, .DS_Store, etc. - continue; - } - const absolutePath = pathUtil.join(directory, name); - const stat = fs.statSync(absolutePath); - if (stat.isDirectory()) { - for (const [ - relativeToChildName, - childAbsolutePath, - ] of recursiveReadDirectory(absolutePath)) { - // This always needs to use / on all systems - result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); - } - } else { - result.push([name, absolutePath]); - } - } - return result; -}; - -/** - * Synchronous create a directory and any parents. Does nothing if the folder already exists. - * @param {string} directory - */ -const mkdirp = (directory) => { - try { - fs.mkdirSync(directory, { - recursive: true, - }); - } catch (e) { - if (e.code !== "ENOENT") { - throw e; - } - } -}; - /** * @param {Record>} allTranslations * @param {string} idPrefix diff --git a/development/fs-utils.js b/development/fs-utils.js new file mode 100644 index 0000000000..020a1e9d98 --- /dev/null +++ b/development/fs-utils.js @@ -0,0 +1,53 @@ +const fs = require("fs"); +const pathUtil = require("path"); + +/** + * Recursively read a directory. + * @param {string} directory + * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. + * The return result includes files in subdirectories, but not the subdirectories themselves. + */ +const recursiveReadDirectory = (directory) => { + const result = []; + for (const name of fs.readdirSync(directory)) { + if (name.startsWith(".")) { + // Ignore .eslintrc.js, .DS_Store, etc. + continue; + } + const absolutePath = pathUtil.join(directory, name); + const stat = fs.statSync(absolutePath); + if (stat.isDirectory()) { + for (const [ + relativeToChildName, + childAbsolutePath, + ] of recursiveReadDirectory(absolutePath)) { + // This always needs to use / on all systems + result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); + } + } else { + result.push([name, absolutePath]); + } + } + return result; +}; + +/** + * Synchronous create a directory and any parents. Does nothing if the folder already exists. + * @param {string} directory + */ +const mkdirp = (directory) => { + try { + fs.mkdirSync(directory, { + recursive: true, + }); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } +}; + +module.exports = { + recursiveReadDirectory, + mkdirp, +}; diff --git a/development/get-credits-for-gui.js b/development/get-credits-for-gui.js new file mode 100644 index 0000000000..22465e525c --- /dev/null +++ b/development/get-credits-for-gui.js @@ -0,0 +1,118 @@ +const fs = require("fs"); +const path = require("path"); +const https = require("https"); +const fsUtils = require("./fs-utils"); +const parseMetadata = require("./parse-extension-metadata"); + +class AggregatePersonInfo { + /** @param {Person} person */ + constructor(person) { + this.name = person.name; + + /** @type {Set} */ + this.links = new Set(); + } + + /** @param {string} link */ + addLink(link) { + this.links.add(link); + } +} + +/** + * @param {string} username + * @returns {Promise} + */ +const getUserID = (username) => + new Promise((resolve, reject) => { + process.stdout.write(`Getting user ID for ${username}... `); + const request = https.get(`https://api.scratch.mit.edu/users/${username}`); + + request.on("response", (response) => { + const data = []; + response.on("data", (newData) => { + data.push(newData); + }); + + response.on("end", () => { + const allData = Buffer.concat(data); + const json = JSON.parse(allData.toString("utf-8")); + const userID = String(json.id); + process.stdout.write(`${userID}\n`); + resolve(userID); + }); + + response.on("error", (error) => { + process.stdout.write("error\n"); + reject(error); + }); + }); + + request.on("error", (error) => { + process.stdout.write("error\n"); + reject(error); + }); + + request.end(); + }); + +const run = async () => { + /** + * @type {Map} + */ + const aggregate = new Map(); + + const extensionRoot = path.resolve(__dirname, "../extensions/"); + for (const [name, absolutePath] of fsUtils.recursiveReadDirectory( + extensionRoot + )) { + if (!name.endsWith(".js")) { + continue; + } + + const code = fs.readFileSync(absolutePath, "utf-8"); + const metadata = parseMetadata(code); + + for (const person of [...metadata.by, ...metadata.original]) { + const personID = person.name.toLowerCase(); + if (!aggregate.has(personID)) { + aggregate.set(personID, new AggregatePersonInfo(person)); + } + + if (person.link) { + aggregate.get(personID).addLink(person.link); + } + } + } + + const result = []; + + for (const id of [...aggregate.keys()].sort()) { + const info = aggregate.get(id); + + if (info.links.size > 0) { + const link = [...info.links.values()].sort()[0]; + const username = link.match(/users\/(.+?)\/?$/)[1]; + const userID = await getUserID(username); + result.push({ + userID, + username, + }); + } else { + result.push({ + username: info.name, + }); + } + } + + return result; +}; + +run() + .then((result) => { + console.log(JSON.stringify(result, null, 4)); + }) + .catch((error) => { + console.error(error); + process.exit(1); + });