PresenceCompiler.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import { execSync } from "node:child_process";
  2. import { existsSync, rmSync, writeFileSync } from "node:fs";
  3. import { createRequire } from "node:module";
  4. import { basename, dirname, resolve } from "node:path";
  5. import { fileURLToPath } from "node:url";
  6. import actions from "@actions/core";
  7. import chalk from "chalk";
  8. import { config } from "dotenv";
  9. import webpack from "webpack";
  10. import { ErrorInfo } from "ts-loader/dist/interfaces";
  11. import { getFolderLetter } from "../util.js";
  12. const require = createRequire(import.meta.url),
  13. rootPath = resolve(fileURLToPath(new URL(".", import.meta.url)), "../..");
  14. if (!process.env.GITHUB_ACTIONS) config({ path: resolve(rootPath, ".env") });
  15. export const tsconfig = JSON.stringify({
  16. extends: "../../../tsconfig.json",
  17. });
  18. export default class PresenceCompiler {
  19. cwd: string;
  20. constructor(
  21. public options?: {
  22. cwd?: string;
  23. webpack?: webpack.Configuration;
  24. }
  25. ) {
  26. this.cwd = options?.cwd ?? rootPath;
  27. }
  28. getPresenceFolder(presence: string) {
  29. return resolve(this.cwd, "websites", getFolderLetter(presence), presence);
  30. }
  31. async compilePresence(
  32. presences: string | string[],
  33. options: {
  34. output?: string;
  35. transpileOnly?: boolean;
  36. emit?: boolean;
  37. } = {}
  38. ) {
  39. options.emit ??= true;
  40. options.transpileOnly ??= false;
  41. const webpackConfig: webpack.Configuration = {
  42. mode: "production",
  43. devtool: "inline-source-map",
  44. resolve: {
  45. extensions: [".ts"],
  46. },
  47. output: options.emit
  48. ? {
  49. iife: false,
  50. path: options.output,
  51. filename: "[name].js",
  52. }
  53. : undefined,
  54. plugins: [
  55. {
  56. apply(compiler) {
  57. compiler.hooks.emit.tap("PresenceCompiler", compilation => {
  58. //* Add empty line after file content to prevent errors from PreMiD
  59. for (const file in compilation.assets) {
  60. //@ts-expect-error - This is defined. (ConcatSource class)
  61. compilation.assets[file].add("\n");
  62. }
  63. });
  64. },
  65. },
  66. ],
  67. module: {
  68. rules: [
  69. {
  70. test: /\.ts$/,
  71. loader: "ts-loader",
  72. exclude: /node_modules/,
  73. options: {
  74. transpileOnly: options.transpileOnly,
  75. errorFormatter: (error: ErrorInfo) => {
  76. actions.error(error.content, {
  77. file: error.file,
  78. title: `TS ${error.code}`,
  79. startLine: error.line,
  80. });
  81. return chalk.cyan(
  82. basename(
  83. dirname(error.file) +
  84. "/" +
  85. basename(error.file) +
  86. ":" +
  87. chalk.yellowBright(error.line) +
  88. ":" +
  89. chalk.yellowBright(error.character) +
  90. " - " +
  91. chalk.redBright("Error ") +
  92. chalk.gray("TS" + error.code + ": ") +
  93. error.content
  94. )
  95. );
  96. },
  97. },
  98. },
  99. ],
  100. },
  101. ...this.options?.webpack,
  102. };
  103. if (Array.isArray(presences)) {
  104. let errors: webpack.WebpackError[] = [];
  105. actions.info(chalk.yellow(`Compiling ${presences.length} Presence(s)`));
  106. for (const presence of presences) {
  107. const presencePath = this.getPresenceFolder(presence);
  108. writeFileSync(resolve(presencePath, "tsconfig.json"), tsconfig);
  109. await this.installPresenceDependencies(presence);
  110. const job = await new Promise<{
  111. error: Error | undefined;
  112. stats: webpack.Stats | undefined;
  113. }>(r =>
  114. webpack.webpack(
  115. {
  116. ...webpackConfig,
  117. context: presencePath,
  118. output: options.emit
  119. ? {
  120. iife: false,
  121. path: presencePath,
  122. filename: "[name].js",
  123. }
  124. : undefined,
  125. entry: {
  126. presence: "./presence.ts",
  127. ...(existsSync(resolve(presencePath, "iframe.ts")) && {
  128. iframe: "./iframe.ts",
  129. }),
  130. },
  131. },
  132. (error, stats) => r({ error, stats })
  133. )
  134. );
  135. if (job.error) throw job.error;
  136. const { service } = require(resolve(presencePath, "metadata.json"));
  137. if (job.stats?.compilation.errors.length) {
  138. actions.info(chalk.red(`Failed to compile ${service || presence}`));
  139. errors.push(...(job.stats?.compilation?.errors || []));
  140. } else {
  141. actions.info(
  142. chalk.green(`Successfully compiled ${service || presence}`)
  143. );
  144. }
  145. }
  146. for (const presence of presences) {
  147. if (
  148. existsSync(resolve(this.getPresenceFolder(presence), "tsconfig.json"))
  149. )
  150. rmSync(resolve(this.getPresenceFolder(presence), "tsconfig.json"));
  151. }
  152. errors = errors.filter(e => e.name !== "ModuleBuildError");
  153. if (!errors.length) {
  154. if (!options.transpileOnly) {
  155. actions.info(
  156. chalk.green(`Successfully compiled ${presences.length} Presence(s)`)
  157. );
  158. } else {
  159. actions.info(
  160. chalk.green(
  161. `Successfully transpiled ${presences.length} Presence(s)`
  162. )
  163. );
  164. }
  165. }
  166. return errors;
  167. }
  168. const presencePath = this.getPresenceFolder(presences);
  169. if (!options.output) options.output = presencePath;
  170. writeFileSync(resolve(presencePath, "tsconfig.json"), tsconfig);
  171. await this.installPresenceDependencies(presences);
  172. actions.info(chalk.yellow(`Compiling ${presences}...`));
  173. const job = await new Promise<{
  174. err: Error | undefined;
  175. stats: webpack.Stats | undefined;
  176. }>(r => {
  177. return webpack.webpack(
  178. {
  179. ...webpackConfig,
  180. context: presencePath,
  181. entry: {
  182. presence: "./presence.ts",
  183. ...(existsSync(resolve(presencePath, "iframe.ts"))
  184. ? { iframe: "./iframe.ts" }
  185. : {}),
  186. },
  187. },
  188. (err, stats) => r({ err, stats })
  189. );
  190. });
  191. if (existsSync(resolve(presencePath, "tsconfig.json")))
  192. rmSync(resolve(presencePath, "tsconfig.json"));
  193. if (job.err) throw job.err;
  194. if (!job.stats?.compilation.errors.length) {
  195. if (!options.transpileOnly)
  196. actions.info(chalk.green(`Successfully compiled ${presences}`));
  197. else actions.info(chalk.green(`Successfully transpiled ${presences}`));
  198. }
  199. let errors = job.stats?.compilation.errors;
  200. errors = errors?.filter(e => e.name !== "ModuleBuildError");
  201. return errors || [];
  202. }
  203. async installPresenceDependencies(presence: string) {
  204. const folder = this.getPresenceFolder(presence);
  205. if (!existsSync(resolve(folder, "package.json"))) return;
  206. actions.info(chalk.blue(`Installing dependencies for ${basename(folder)}`));
  207. execSync("npm install --quiet --loglevel=error", {
  208. cwd: folder,
  209. });
  210. }
  211. }
  212. export interface Metadata {
  213. $schema: `https://schemas.premid.app/metadata/${number}.${number}`;
  214. author: Contributor;
  215. contributors?: Contributor[];
  216. service: string;
  217. altnames?: string[];
  218. description: { [lang: string]: string };
  219. url: `${string}.${string}` | `${string}.${string}`[];
  220. regExp?: string;
  221. version: `${number}.${number}.${number}`;
  222. apiVersion: number;
  223. logo: `https://i.imgur.com/${string}.${ImageTypes}`;
  224. thumbnail: `https://i.imgur.com/${string}.${ImageTypes}`;
  225. color: `#${string}`;
  226. tags: string | string[];
  227. category: string;
  228. iframe?: boolean;
  229. iFrameRegExp?: string;
  230. readLogs?: boolean;
  231. settings?:
  232. | Setting
  233. | MultiLanguageSetting
  234. | StringSetting
  235. | ValueSetting
  236. | ValuesSetting;
  237. }
  238. interface Contributor {
  239. name: string;
  240. id: `${bigint}`;
  241. }
  242. type ImageTypes = "png" | "jpeg" | "jpg" | "gif";
  243. interface BaseSetting {
  244. id: string;
  245. }
  246. interface MultiLanguageSetting extends BaseSetting {
  247. multiLanguage: true;
  248. }
  249. interface Setting extends BaseSetting {
  250. title: string;
  251. icon: string;
  252. if?: {
  253. [key: string]: Value;
  254. };
  255. }
  256. interface StringSetting extends Setting {
  257. value: string;
  258. placeholder: string;
  259. }
  260. interface ValueSetting extends Setting {
  261. value: boolean;
  262. }
  263. interface ValuesSetting extends Setting {
  264. value: number;
  265. values: Value[];
  266. }
  267. type Value = string | number | boolean;