/[sudobot]/branches/8.x/scripts/extensions.js
ViewVC logotype

Contents of /branches/8.x/scripts/extensions.js

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: text/javascript
File size: 16283 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 #!/usr/bin/env node
2
3 /*
4 * This file is part of SudoBot.
5 *
6 * Copyright (C) 2021-2023 OSN Developers.
7 *
8 * SudoBot is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * SudoBot is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
20 */
21
22 require("module-alias/register");
23 require("dotenv/config");
24
25 const chalk = require("chalk");
26 const { spawnSync } = require("child_process");
27 const { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync, rmSync } = require("fs");
28 const { readFile } = require("fs/promises");
29 const path = require("path");
30 const { chdir, cwd } = require("process");
31 const { z } = require("zod");
32 const semver = require("semver");
33
34 const extensionsPath = process.env.EXTENSIONS_DIRECTORY ?? path.resolve(__dirname, "../extensions");
35
36 function error(...args) {
37 console.error(...args);
38 process.exit(-1);
39 }
40
41 if (!existsSync(extensionsPath)) {
42 error("You're not using any extension! To get started, create an `extensions` folder in the project root.");
43 }
44
45 function getRecuriveJavaScriptFiles(dir) {
46 if (!existsSync(dir)) {
47 return [];
48 }
49
50 const files = readdirSync(dir);
51 const flat = [];
52
53 for (const fileName of files) {
54 const file = path.join(dir, fileName);
55 const isDirectory = lstatSync(file).isDirectory();
56
57 if (isDirectory) {
58 flat.push(...getRecuriveJavaScriptFiles(file));
59 continue;
60 }
61
62 if (!file.endsWith(".js")) {
63 continue;
64 }
65
66 flat.push(file);
67 }
68
69 return flat;
70 }
71
72 const extensionMetadataSchema = z.object({
73 main: z.string().optional(),
74 commands: z.string().optional(),
75 services: z.string().optional(),
76 events: z.string().optional(),
77 language: z.enum(["typescript", "javascript"]).optional(),
78 main_directory: z.string().optional(),
79 build_command: z.string().optional(),
80 name: z.string().optional(),
81 description: z.string().optional(),
82 resources: z.string().optional(),
83 id: z.string(),
84 icon: z.string().optional(),
85 readmeFileName: z.string().default("README.md")
86 });
87
88 function readMeta(extensionName, extensionDirectory) {
89 const metadataFile = path.join(extensionDirectory, "extension.json");
90
91 if (!existsSync(metadataFile)) {
92 error(`Extension ${extensionName} does not have a "extension.json" file!`);
93 }
94
95 const metadata = JSON.parse(readFileSync(metadataFile, { encoding: "utf-8" }));
96
97 try {
98 return extensionMetadataSchema.parse(metadata);
99 } catch (e) {
100 error(`Extension ${extensionName} has an invalid "extension.json" file!`, e);
101 }
102 }
103
104 async function writeCacheIndex() {
105 const extensionsOutputArray = [];
106 const meta = [];
107 const extensions = readdirSync(extensionsPath);
108
109 for await (const extensionName of extensions) {
110 const extensionDirectory = path.resolve(extensionsPath, extensionName);
111 const isDirectory = lstatSync(extensionDirectory).isDirectory();
112
113 if (!isDirectory || extensionName === ".extbuilds") {
114 continue;
115 }
116
117 console.log("Found extension: ", extensionName);
118
119 const {
120 main_directory = "./build",
121 commands = `./${main_directory}/commands`,
122 events = `./${main_directory}/events`,
123 services = `./${main_directory}/services`,
124 main = `./${main_directory}/index.js`,
125 language = "typescript",
126 id
127 } = readMeta(extensionName, extensionDirectory);
128
129 const extensionPath = path.join(extensionDirectory, main);
130
131 const imported = await require(extensionPath);
132 const { default: ExtensionClass } = imported.__esModule ? imported : { default: imported };
133 const extension = new ExtensionClass(this.client);
134 let commandPaths = await extension.commands();
135 let eventPaths = await extension.events();
136 let servicePaths = await extension.services();
137
138 if (commandPaths === null) {
139 const directory = path.join(
140 ...(process.env.EXTENSIONS_DIRECTORY ? [process.env.EXTENSIONS_DIRECTORY] : [__dirname, "../extensions"]),
141 extensionName,
142 commands
143 );
144
145 if (existsSync(directory)) {
146 commandPaths = getRecuriveJavaScriptFiles(directory);
147 }
148 }
149
150 if (eventPaths === null) {
151 const directory = path.join(
152 ...(process.env.EXTENSIONS_DIRECTORY ? [process.env.EXTENSIONS_DIRECTORY] : [__dirname, "../extensions"]),
153 extensionName,
154 events
155 );
156
157 if (existsSync(directory)) {
158 eventPaths = getRecuriveJavaScriptFiles(directory);
159 }
160 }
161
162 if (servicePaths === null) {
163 const directory = path.join(
164 ...(process.env.EXTENSIONS_DIRECTORY ? [process.env.EXTENSIONS_DIRECTORY] : [__dirname, "../extensions"]),
165 extensionName,
166 services
167 );
168
169 if (existsSync(directory)) {
170 servicePaths = getRecuriveJavaScriptFiles(directory);
171 }
172 }
173
174 extensionsOutputArray.push({
175 name: extensionName,
176 entry: extensionPath,
177 commands: commandPaths ?? [],
178 events: eventPaths ?? [],
179 services: servicePaths ?? [],
180 id
181 });
182
183 meta.push({
184 language
185 });
186 }
187
188 console.log("Overview of the extensions: ");
189 console.table(
190 extensionsOutputArray.map((e, i) => ({
191 name: e.name,
192 entry: e.entry.replace(extensionsPath, "{ROOT}"),
193 commands: e.commands.length,
194 events: e.events.length,
195 services: e.services.length,
196 language: meta[i].language
197 }))
198 );
199
200 const indexFile = path.join(extensionsPath, "index.json");
201
202 writeFileSync(
203 indexFile,
204 JSON.stringify(
205 {
206 extensions: extensionsOutputArray
207 },
208 null,
209 4
210 )
211 );
212
213 console.log("Wrote cache index file: ", indexFile);
214 console.warn(
215 "Note: If you're getting import errors after caching extensions, please try first by removing or rebuilding them."
216 );
217 }
218
219 const MAX_CHARS = 7;
220
221 function actionLog(action, description) {
222 console.log(
223 chalk.green.bold(`${action}${action.length >= MAX_CHARS ? "" : " ".repeat(MAX_CHARS - action.length)} `),
224 description
225 );
226 }
227
228 function spawnSyncCatchExit(...args) {
229 actionLog("RUN", args[0]);
230 const { status } = spawnSync(...args);
231 if (status !== 0) process.exit(status);
232 }
233
234 async function buildExtensions() {
235 const startTime = Date.now();
236 const extensions = readdirSync(extensionsPath);
237 const workingDirectory = cwd();
238 let count = 0;
239
240 for await (const extensionName of extensions) {
241 const extensionDirectory = path.resolve(extensionsPath, extensionName);
242 const isDirectory = lstatSync(extensionDirectory).isDirectory();
243
244 if (!isDirectory || extensionName === ".extbuilds") {
245 continue;
246 }
247
248 chdir(path.join(extensionsPath, extensionName));
249
250 if (!process.argv.includes("--tsc")) {
251 actionLog("DEPS", extensionName);
252 spawnSyncCatchExit("npm install -D", {
253 encoding: "utf-8",
254 shell: true,
255 stdio: "inherit"
256 });
257
258 actionLog("RELINK", extensionName);
259 spawnSyncCatchExit(`npm install --save ${path.relative(cwd(), path.resolve(__dirname, ".."))}`, {
260 encoding: "utf-8",
261 shell: true,
262 stdio: "inherit"
263 });
264 }
265
266 actionLog("BUILD", extensionName);
267 const { build_command } = readMeta(extensionName, extensionDirectory);
268
269 if (!build_command) {
270 console.log(chalk.cyan.bold("INFO "), "This extension doesn't require building.");
271 continue;
272 }
273
274 spawnSyncCatchExit(build_command, {
275 encoding: "utf-8",
276 shell: true,
277 stdio: "inherit"
278 });
279
280 count++;
281 }
282
283 actionLog("SUCCESS", `in ${((Date.now() - startTime) / 1000).toFixed(2)}s, built ${count} extensions`);
284 chdir(workingDirectory);
285 }
286
287 async function writeExtensionIndex() {
288 const extensionsPath = path.resolve(__dirname, "../extensions");
289 const extensionsOutputRecord = {};
290 const extensions = readdirSync(extensionsPath);
291
292 for await (const extensionName of extensions) {
293 const extensionDirectory = path.resolve(extensionsPath, extensionName);
294 const isDirectory = lstatSync(extensionDirectory).isDirectory();
295
296 if (!isDirectory || extensionName === ".extbuilds") {
297 continue;
298 }
299
300 console.log("Found extension: ", extensionName);
301
302 const packageJsonPath = path.join(extensionDirectory, "package.json");
303 const packageJson = JSON.parse(await readFile(packageJsonPath, { encoding: "utf-8" }));
304
305 const {
306 main_directory = "./build",
307 commands = `./${main_directory}/commands`,
308 events = `./${main_directory}/events`,
309 build_command = null,
310 name = packageJson.name ?? extensionName,
311 description = packageJson.description,
312 language = "javascript",
313 main = `./${main_directory}/index.js`,
314 services = `./${main_directory}/services`,
315 id,
316 icon,
317 readmeFileName
318 } = readMeta(extensionName, extensionDirectory) ?? {};
319
320 const commandPaths = getRecuriveJavaScriptFiles(path.join(extensionDirectory, commands));
321 const eventPaths = getRecuriveJavaScriptFiles(path.join(extensionDirectory, events));
322 const servicePaths = getRecuriveJavaScriptFiles(path.join(extensionDirectory, services));
323
324 const tarballs = readdirSync(path.resolve(extensionsPath, ".extbuilds", extensionName));
325
326 tarballs.sort((a, b) => {
327 const vA = path
328 .basename(a)
329 .replace(`${extensionName}-`, "")
330 .replace(/\.tar\.gz$/gi, "");
331 const vB = path
332 .basename(b)
333 .replace(`${extensionName}-`, "")
334 .replace(/\.tar\.gz$/gi, "");
335 const splitA = vA.split("-");
336 const splitB = vB.split("-");
337 const dashVA = splitA[1];
338 const dashVB = splitB[1];
339 const revA = isNaN(dashVA) ? 0 : parseInt(dashVA);
340 const revB = isNaN(dashVB) ? 0 : parseInt(dashVB);
341 const result = semver.rcompare(vA, vB);
342
343 if (splitA[0] === splitB[0] && vA.includes("-") && !isNaN(dashVA) && (!vB.includes("-") || isNaN(dashVB))) {
344 return -1;
345 }
346
347 if (splitA[0] === splitB[0] && vB.includes("-") && !isNaN(dashVB) && (!vA.includes("-") || isNaN(dashVA))) {
348 return 1;
349 }
350
351 return result === 0 ? revB - revA : result;
352 });
353
354 const tarballList = tarballs.map(file => {
355 const basename = path.basename(file);
356 const filePath = path.join(extensionsPath, ".extbuilds", extensionName, basename);
357 const { stdout } = spawnSync(`sha512sum`, [filePath], {
358 encoding: "utf-8"
359 });
360 const { size, birthtime } = lstatSync(filePath);
361 const checksum = stdout.split(" ")[0];
362
363 return {
364 url:
365 "https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/extensions" +
366 path.join("/.extbuilds/", extensionName, basename),
367 basename,
368 version: basename.replace(`${extensionName}-`, "").replace(/\.tar\.gz$/gi, ""),
369 checksum,
370 size,
371 createdAt: birthtime
372 };
373 });
374
375 extensionsOutputRecord[id] = {
376 name,
377 id,
378 shortName: packageJson.name ?? extensionName,
379 description,
380 version: packageJson.version,
381 commands: commandPaths.length,
382 events: eventPaths.length,
383 buildCommand: build_command,
384 language,
385 services: servicePaths.length,
386 main,
387 iconURL: icon
388 ? "https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/extensions" + path.join("/", extensionName, icon)
389 : null,
390 author:
391 typeof packageJson.author === "string"
392 ? {
393 name: packageJson.author
394 }
395 : packageJson.author,
396 license: packageJson.license,
397 licenseURL: `https://spdx.org/licenses/${packageJson.license}.html`,
398 homepage: packageJson.homepage,
399 repository: typeof packageJson.repository === "string" ? packageJson.repository : packageJson.repository?.url,
400 issues: typeof packageJson.bugs === "string" ? packageJson.bugs : packageJson.bugs?.url,
401 lastUpdated: new Date(),
402 readmeFileName: readmeFileName,
403 readmeFileURL: `https://raw.githubusercontent.com/onesoft-sudo/sudobot/main/extensions/${extensionName}/${readmeFileName}`,
404 tarballs: tarballList
405 };
406 }
407
408 console.log("Overview of the extensions: ");
409 console.table(
410 Object.values(extensionsOutputRecord).map(e => ({
411 name: e.name,
412 commands: e.commands.length,
413 events: e.events.length
414 }))
415 );
416
417 const indexFile = path.join(extensionsPath, ".extbuilds/index.json");
418
419 writeFileSync(indexFile, JSON.stringify(extensionsOutputRecord, null, 4));
420
421 console.log("Wrote index file: ", indexFile);
422 }
423
424 if (process.argv.includes("--clear-cache") || process.argv.includes("--clean") || process.argv.includes("--delcache")) {
425 const indexFile = path.join(extensionsPath, "index.json");
426
427 if (!existsSync(indexFile)) {
428 error("No cached index file found!");
429 }
430
431 rmSync(indexFile);
432 console.log("Removed cached index file: ", indexFile);
433 } else if (process.argv.includes("--cache") || process.argv.includes("--index")) {
434 console.time("Finished in");
435 console.log("Creating cache index for all the installed extensions");
436 writeCacheIndex()
437 .then(() => console.timeEnd("Finished in"))
438 .catch(e => {
439 console.log(e);
440 console.timeEnd("Finished in");
441 });
442 } else if (process.argv.includes("--build")) {
443 console.log("Building installed extensions");
444 buildExtensions().catch(e => {
445 console.log(e);
446 process.exit(-1);
447 });
448 } else if (process.argv.includes("--mkindex")) {
449 console.log("Creating index for all the available extensions in the extensions/ directory");
450
451 writeExtensionIndex().catch(e => {
452 console.log(e);
453 process.exit(-1);
454 });
455 } else {
456 console.log("Usage:");
457 console.log(" node extensions.js <options>\n");
458 console.log("Options:");
459 console.log(" --build [--tsc] | Builds all the installed extensions, if needed.");
460 console.log(" | The `--tsc` flag will only run the typescript compiler.");
461 console.log(" --cache | Creates cache indexes for installed extensions, to improve the startup time.");
462 console.log(" --clean | Clears all installed extension cache.");
463 console.log(
464 " --mkindex | Creates indexes for all the available extensions, in the extensions/ top level directory."
465 );
466 process.exit(-1);
467 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26