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 |
const chalk = require("chalk"); |
24 |
const { spawnSync } = require("child_process"); |
25 |
const { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync, rmSync } = require("fs"); |
26 |
const path = require("path"); |
27 |
const { chdir, cwd } = require("process"); |
28 |
|
29 |
const extensionsPath = path.resolve(__dirname, "../extensions"); |
30 |
|
31 |
function error(...args) { |
32 |
console.error(...args); |
33 |
process.exit(-1); |
34 |
} |
35 |
|
36 |
if (!existsSync(extensionsPath)) { |
37 |
error("You're not using any extension! To get started, create an `extensions` folder in the project root."); |
38 |
} |
39 |
|
40 |
function getRecuriveJavaScriptFiles(dir) { |
41 |
const files = readdirSync(dir); |
42 |
const flat = []; |
43 |
|
44 |
for (const fileName of files) { |
45 |
const file = path.join(dir, fileName); |
46 |
const isDirectory = lstatSync(file).isDirectory(); |
47 |
|
48 |
if (isDirectory) { |
49 |
flat.push(...getRecuriveJavaScriptFiles(file)); |
50 |
continue; |
51 |
} |
52 |
|
53 |
if (!file.endsWith(".js")) { |
54 |
continue; |
55 |
} |
56 |
|
57 |
flat.push(file); |
58 |
} |
59 |
|
60 |
return flat; |
61 |
} |
62 |
|
63 |
function readMeta(extensionName, extensionDirectory) { |
64 |
const metadataFile = path.join(extensionDirectory, "extension.json"); |
65 |
|
66 |
if (!existsSync(metadataFile)) { |
67 |
error(`Extension ${extensionName} does not have a "extension.json" file!`); |
68 |
} |
69 |
|
70 |
const metadata = JSON.parse(readFileSync(metadataFile, { encoding: "utf-8" })); |
71 |
return metadata; |
72 |
} |
73 |
|
74 |
async function writeCacheIndex() { |
75 |
const extensionsOutputArray = []; |
76 |
const meta = []; |
77 |
const extensions = readdirSync(extensionsPath); |
78 |
|
79 |
for await (const extensionName of extensions) { |
80 |
const extensionDirectory = path.resolve(extensionsPath, extensionName); |
81 |
const isDirectory = lstatSync(extensionDirectory).isDirectory(); |
82 |
|
83 |
if (!isDirectory) { |
84 |
continue; |
85 |
} |
86 |
|
87 |
console.log("Found extension: ", extensionName); |
88 |
|
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 |
// const { |
97 |
// main_directory = "./build", |
98 |
// commands = `./${main_directory}/commands`, |
99 |
// events = `./${main_directory}/events`, |
100 |
// main = `./${main_directory}/index.js`, |
101 |
// language = "typescript" |
102 |
// } = metadata; |
103 |
|
104 |
const { |
105 |
main_directory = "./build", |
106 |
commands = `./${main_directory}/commands`, |
107 |
events = `./${main_directory}/events`, |
108 |
main = `./${main_directory}/index.js`, |
109 |
language = "typescript" |
110 |
} = readMeta(extensionName, extensionDirectory); |
111 |
|
112 |
const extensionPath = path.join(extensionDirectory, main); |
113 |
|
114 |
const imported = await require(extensionPath); |
115 |
const { default: ExtensionClass } = imported.__esModule ? imported : { default: imported }; |
116 |
const extension = new ExtensionClass(this.client); |
117 |
let commandPaths = await extension.commands(); |
118 |
let eventPaths = await extension.events(); |
119 |
|
120 |
if (commandPaths === null) { |
121 |
const directory = path.join(__dirname, "../extensions", extensionName, commands); |
122 |
|
123 |
if (existsSync(directory)) { |
124 |
commandPaths = getRecuriveJavaScriptFiles(directory); |
125 |
} |
126 |
} |
127 |
|
128 |
if (eventPaths === null) { |
129 |
const directory = path.join(__dirname, "../extensions", extensionName, events); |
130 |
|
131 |
if (existsSync(directory)) { |
132 |
eventPaths = getRecuriveJavaScriptFiles(directory); |
133 |
} |
134 |
} |
135 |
|
136 |
extensionsOutputArray.push({ |
137 |
name: extensionName, |
138 |
entry: extensionPath, |
139 |
commands: commandPaths ?? [], |
140 |
events: eventPaths ?? [] |
141 |
}); |
142 |
|
143 |
meta.push({ |
144 |
language |
145 |
}); |
146 |
} |
147 |
|
148 |
console.log("Overview of the extensions: "); |
149 |
console.table( |
150 |
extensionsOutputArray.map((e, i) => ({ |
151 |
name: e.name, |
152 |
entry: e.entry.replace(path.resolve(__dirname, "../extensions"), "{ROOT}"), |
153 |
commands: e.commands.length, |
154 |
events: e.events.length, |
155 |
language: meta[i].language |
156 |
})) |
157 |
); |
158 |
|
159 |
const indexFile = path.join(__dirname, "../extensions/index.json"); |
160 |
|
161 |
writeFileSync( |
162 |
indexFile, |
163 |
JSON.stringify( |
164 |
{ |
165 |
extensions: extensionsOutputArray |
166 |
}, |
167 |
null, |
168 |
4 |
169 |
) |
170 |
); |
171 |
|
172 |
console.log("Wrote cache index file: ", indexFile); |
173 |
console.warn( |
174 |
"Note: If you're getting import errors after caching extensions, please try first by removing or rebuilding them." |
175 |
); |
176 |
} |
177 |
|
178 |
const MAX_CHARS = 7; |
179 |
|
180 |
function actionLog(action, description) { |
181 |
console.log( |
182 |
chalk.green.bold(`${action}${action.length >= MAX_CHARS ? "" : " ".repeat(MAX_CHARS - action.length)} `), |
183 |
description |
184 |
); |
185 |
} |
186 |
|
187 |
function spawnSyncCatchExit(...args) { |
188 |
actionLog("RUN", args[0]); |
189 |
const { status } = spawnSync(...args); |
190 |
if (status !== 0) process.exit(status); |
191 |
} |
192 |
|
193 |
async function buildExtensions() { |
194 |
const startTime = Date.now(); |
195 |
const extensions = readdirSync(extensionsPath); |
196 |
const workingDirectory = cwd(); |
197 |
|
198 |
for await (const extensionName of extensions) { |
199 |
const extensionDirectory = path.resolve(extensionsPath, extensionName); |
200 |
const isDirectory = lstatSync(extensionDirectory).isDirectory(); |
201 |
|
202 |
if (!isDirectory) { |
203 |
continue; |
204 |
} |
205 |
|
206 |
chdir(path.join(extensionsPath, extensionName)); |
207 |
|
208 |
actionLog("DEPS", extensionName); |
209 |
spawnSyncCatchExit("npm install -D", { |
210 |
encoding: "utf-8", |
211 |
shell: true, |
212 |
stdio: "inherit" |
213 |
}); |
214 |
|
215 |
actionLog("RELINK", extensionName); |
216 |
spawnSyncCatchExit(`npm link --save ${path.relative(cwd(), path.resolve(__dirname, ".."))}`, { |
217 |
encoding: "utf-8", |
218 |
shell: true, |
219 |
stdio: "inherit" |
220 |
}); |
221 |
|
222 |
actionLog("BUILD", extensionName); |
223 |
const { build_command } = readMeta(extensionName, extensionDirectory); |
224 |
|
225 |
if (!build_command) { |
226 |
console.log(chalk.cyan.bold("INFO "), "This extension doesn't require building."); |
227 |
continue; |
228 |
} |
229 |
|
230 |
spawnSyncCatchExit(build_command, { |
231 |
encoding: "utf-8", |
232 |
shell: true, |
233 |
stdio: "inherit" |
234 |
}); |
235 |
} |
236 |
|
237 |
actionLog("SUCCESS", `in ${((Date.now() - startTime) / 1000).toFixed(2)}s, built ${extensions.length} extensions`); |
238 |
chdir(workingDirectory); |
239 |
} |
240 |
|
241 |
if (process.argv.includes("--clear-cache") || process.argv.includes("--clean") || process.argv.includes("--delcache")) { |
242 |
const indexFile = path.join(__dirname, "../extensions/index.json"); |
243 |
|
244 |
if (!existsSync(indexFile)) { |
245 |
error("No cached index file found!"); |
246 |
} |
247 |
|
248 |
rmSync(indexFile); |
249 |
console.log("Removed cached index file: ", indexFile); |
250 |
} else if (process.argv.includes("--cache") || process.argv.includes("--index")) { |
251 |
console.time("Finished in"); |
252 |
console.log("Creating cache index for all the installed extensions"); |
253 |
writeCacheIndex() |
254 |
.then(() => console.timeEnd("Finished in")) |
255 |
.catch(e => { |
256 |
console.log(e); |
257 |
console.timeEnd("Finished in"); |
258 |
}); |
259 |
} else if (process.argv.includes("--build")) { |
260 |
console.log("Building installed extensions"); |
261 |
buildExtensions().catch(e => { |
262 |
console.log(e); |
263 |
process.exit(-1); |
264 |
}); |
265 |
} else { |
266 |
console.log("Usage:"); |
267 |
console.log(" node extensions.js <options>\n"); |
268 |
console.log("Options:"); |
269 |
console.log(" --build | Builds all the installed extensions, if needed."); |
270 |
console.log(" --cache | Creates cache indexes for installed extensions, to improve the startup time."); |
271 |
console.log(" --clean | Clears all installed extension cache."); |
272 |
process.exit(-1); |
273 |
} |