소스 검색

ref: overhaul post-build a lil bit

Sv443 1 주 전
부모
커밋
c92ce7892f
2개의 변경된 파일163개의 추가작업 그리고 126개의 파일을 삭제
  1. 1 1
      .vscode/settings.json
  2. 162 125
      src/tools/post-build.ts

+ 1 - 1
.vscode/settings.json

@@ -38,7 +38,7 @@
       "color": "black",
       "overviewRulerColor": "#ed0",
     },
-    "((//\\s*|/\\*\\s*)?#region ([^\\S\\r\\n]*[\\(\\)\\w,.\\-_&+#*'\"/:]+)*)": { //#region test: (abc):
+    "((//\\s*|/\\*\\s*)?#region ([^\\S\\r\\n]*[\\(\\)\\w,.\\-_&@+#*'\"/:]+)*)": { //#region foo: (@bar):
       "backgroundColor": "#35b5d0",
       "color": "#000",
       "overviewRulerColor": "#35b5d0",

+ 162 - 125
src/tools/post-build.ts

@@ -14,32 +14,42 @@ import pkg from "../../package.json" with { type: "json" };
 
 const { argv, env, exit, stdout } = process;
 
+//#region types
+
 /** Any type that is either a string or can be implicitly converted to one by having a .toString() method */
 type Stringifiable = string | { toString(): string; };
 
+/** Resolves a CLI argument defined in {@linkcode RollupArgs} in the file `rollup.config.mjs` */
+type CliArg<TName extends keyof Required<RollupArgs>> = Required<RollupArgs>[TName];
+
 /** An entry in the file `assets/require.json` */
 type RequireObj = RequireObjPkg | RequireObjUrl;
+
+/** Static URL-based package entry */
 type RequireObjUrl = {
   url: string;
 };
+
+/** npm-based package entry */
 type RequireObjPkg = {
   pkgName: keyof (typeof pkg)["dependencies"] | keyof (typeof pkg)["devDependencies"];
   baseUrl?: string;
   path?: string;
 };
 
+/** Build script stats, persisted in the file at {@linkcode buildStatsPath} */
 type BuildStats = {
   sizeKiB: number;
   mode: string;
   timestamp: number;
 };
 
+//#region vars
+
 const buildTs = Date.now();
 /** Used to force the browser and userscript extension to refresh resources */
 const buildUid = randomId(12, 36);
 
-type CliArg<TName extends keyof Required<RollupArgs>> = Required<RollupArgs>[TName];
-
 const mode = getCliArg<CliArg<"config-mode">>("mode", "development");
 const branch = getCliArg<CliArg<"config-branch">>("branch", (mode === "production" ? "main" : "develop"));
 const host = getCliArg<CliArg<"config-host">>("host", "github");
@@ -50,36 +60,36 @@ const genMeta = getCliArg<CliArg<"config-gen-meta">>("meta", "true") === "true";
 const envPort = Number(env.DEV_SERVER_PORT);
 /** HTTP port of the dev server */
 const devServerPort = isNaN(envPort) || envPort === 0 ? 8710 : envPort;
-const devServerUserscriptUrl = `http://localhost:${devServerPort}/${rollupCfgOutputFile}`;
+const devServerUserscriptUrl = `http://localhost:${devServerPort}/${rollupCfgOutputFile}` as const;
 
-const repo = "Sv443/BetterYTM";
-const userscriptDistFile = `BetterYTM${suffix}.user.js`;
-const userscriptMetaFile = `BetterYTM${suffix}.meta.js`;
-const distFolderPath = `./${rollupCfgOutputDir}/`;
-const assetFolderPath = "./assets/";
+const repo = "Sv443/BetterYTM" as const;
+const userscriptDistFile = `BetterYTM${suffix}.user.js` as const;
+const userscriptMetaFile = `BetterYTM${suffix}.meta.js` as const;
+const distFolderPath = `./${rollupCfgOutputDir}/` as const;
+const assetFolderPath = "./assets/" as const;
+const buildStatsPath = ".build.json" as const;
 
 const hostScriptUrl = (() => {
   switch(host) {
-  case "greasyfork":
-    return "https://update.greasyfork.org/scripts/475682/BetterYTM.user.js";
-  case "openuserjs":
-    return "https://openuserjs.org/src/scripts/Sv443/BetterYTM.user.js";
-  default:
-    return `https://github.com/${repo}/raw/refs/heads/main/dist/${userscriptDistFile}`;
+  case "greasyfork": return "https://update.greasyfork.org/scripts/475682/BetterYTM.user.js" as const;
+  case "openuserjs": return "https://openuserjs.org/src/scripts/Sv443/BetterYTM.user.js" as const;
+  default:           return `https://github.com/${repo}/raw/refs/heads/main/dist/${userscriptDistFile}` as const;
   }
 })();
-const hostMetaUrl = `https://github.com/${repo}/raw/refs/heads/main/dist/${userscriptMetaFile}`;
+const hostMetaUrl = `https://github.com/${repo}/raw/refs/heads/main/dist/${userscriptMetaFile}` as const;
 
 /** Whether to trigger the bell sound in some terminals when the code has finished compiling */
 const ringBell = Boolean(env.RING_BELL && (env.RING_BELL.length > 0 && env.RING_BELL.trim().toLowerCase() === "true"));
 
 /** Directives that are only added in dev mode */
-const devDirectives = mode === "development" ? `\
-// @grant             GM.registerMenuCommand
-// @grant             GM.listValues\
-` : undefined;
+const devDirectives = mode !== "development"
+  ? undefined
+  : `// @grant             GM.registerMenuCommand
+// @grant             GM.listValues` as const;
+
+//#region main
 
-(async () => {
+async function main() {
   const buildNbr = await getLastCommitSha();
 
   const resourcesDirectives = await getResourceDirectives(buildNbr);
@@ -132,12 +142,12 @@ ${devDirectives ? "\n" + devDirectives : ""}
 I welcome every contribution on GitHub!
   https://github.com/Sv443/BetterYTM
 */
-`;
+` as const;
 
   const footer = `
 /* Disclaimer: I am not affiliated with or endorsed by YouTube, Google, Alphabet, Genius or anyone else */
 /* C&D this 🖕 */
-`;
+` as const;
 
   try {
     const rootPath = join(dirname(fileURLToPath(import.meta.url)), "../../");
@@ -176,9 +186,9 @@ I welcome every contribution on GitHub!
     const sizeKiB = Number((Buffer.byteLength(finalUserscript, "utf8") / 1024).toFixed(2));
 
     let buildStats: Partial<BuildStats>[] = [];
-    if(await exists(".build.json")) {
+    if(await exists(buildStatsPath)) {
       try {
-        const buildJsonParsed = JSON.parse(String(await readFile(".build.json")));
+        const buildJsonParsed = JSON.parse(String(await readFile(buildStatsPath)));
         buildStats = (Array.isArray(buildJsonParsed) ? buildJsonParsed : []) as Partial<BuildStats>[];
       }
       catch {}
@@ -218,7 +228,7 @@ I welcome every contribution on GitHub!
       ...(buildStats.filter((v) => v.mode !== mode)),
     ];
 
-    await writeFile(".build.json", JSON.stringify(newBuildStats, undefined, 2));
+    await writeFile(buildStatsPath, JSON.stringify(newBuildStats, undefined, 2));
 
     schedExit(0);
   }
@@ -226,7 +236,27 @@ I welcome every contribution on GitHub!
     console.error(k.red("Error while adding userscript header:\n"), err);
     schedExit(1);
   }
-})();
+};
+
+//#region process
+
+/** Returns the value of a CLI argument (in the format `--arg=<value>`) or the value of `defaultVal` if it doesn't exist */
+function getCliArg<TReturn extends string = string>(name: string, defaultVal: TReturn | (string & {})): TReturn
+/** Returns the value of a CLI argument (in the format `--arg=<value>`) or undefined if it doesn't exist */
+function getCliArg<TReturn extends string = string>(name: string, defaultVal?: TReturn | (string & {})): TReturn | undefined
+/** Returns the value of a CLI argument (in the format `--arg=<value>`) or the value of `defaultVal` if it doesn't exist */
+function getCliArg<TReturn extends string = string>(name: string, defaultVal?: TReturn | (string & {})): TReturn | undefined {
+  const arg = argv.find((v) => v.trim().match(new RegExp(`^(--)?${name}=.+$`, "i")));
+  const val = arg?.split("=")?.[1];
+  return (val && val.length > 0 ? val : defaultVal)?.trim() as TReturn | undefined;
+}
+
+/** Schedules an exit after I/O events finish */
+function schedExit(code: number) {
+  setImmediate(() => exit(code));
+}
+
+//#region modify userscript
 
 /** Replaces tokens in the format `#{{key}}` or `/⋆#{{key}}⋆/` of the `replacements` param with their respective value */
 function insertValues(userscript: string, replacements: Record<string, Stringifiable>) {
@@ -236,11 +266,13 @@ function insertValues(userscript: string, replacements: Record<string, Stringifi
 }
 
 /** Removes sourcemapping comments */
-function removeSourcemapComments(input: string) {
-  return input
+function removeSourcemapComments(userscript: string) {
+  return userscript
     .replace(/\/\/\s?#\s?sourceMappingURL\s?=\s?.+$/gm, "");
 }
 
+//#region git
+
 /**
  * Used as a kind of "build number", though note it is always behind by at least one commit,
  * as the act of putting this number in the userscript and committing it changes the hash again, indefinitely
@@ -257,6 +289,9 @@ function getLastCommitSha() {
   });
 }
 
+//#region fs
+
+/** Checks if the given path exists and is readable and writable by the process */
 async function exists(path: string) {
   try {
     await access(path, fsconst.R_OK | fsconst.W_OK);
@@ -267,6 +302,36 @@ async function exists(path: string) {
   }
 }
 
+/**
+ * Calculates the SHA-256 hash of the file at the given path.  
+ * Uses {@linkcode resolveResourcePath()} to resolve the path, meaning paths prefixed with a slash are relative to the repository root, otherwise they are relative to the `assets` directory.
+ */
+function getFileHashSha256(path: string): Promise<string> {
+  path = resolveResourcePath(path);
+
+  return new Promise((res, rej) => {
+    const hash = createHash("sha256");
+    const stream = createReadStream(resolve(path));
+    stream.on("data", data => hash.update(data));
+    stream.on("end", () => res(hash.digest("base64")));
+    stream.on("error", rej);
+  });
+}
+
+/** Calculates the SHA-256 hash of a ReadableStream, like the `body` prop of a `fetch()` call */
+function getStreamHashSha256(rStream: ReadableStream): Promise<string> {
+  return new Promise((res, rej) => {
+    const hash = createHash("sha256");
+    rStream.pipeTo(new WritableStream({
+      write(chunk) {
+        hash.update(chunk);
+      },
+    })).then(() => res(hash.digest("base64"))).catch(rej);
+  });
+}
+
+//#region @resource
+
 /** Resolves the value of an entry in resources.json */
 function resolveResourceVal(value: string, buildNbr: string) {
   if(!(/\$[A-Z]+/.test(value)))
@@ -365,6 +430,39 @@ async function getResourceDirectives(ref: string) {
   }
 }
 
+//#region svg spritesheet
+
+/** Compiles all `icon-*` assets into a single SVG spritesheet file and writes it to `assets/spritesheet.svg` */
+async function createSvgSpritesheet() {
+  try {
+    const sprites: string[] = [];
+
+    for(const [name, val] of Object.entries(resourcesJson.resources)) {
+      if(!/^icon-/.test(name))
+        continue;
+
+      const iconPath = resolveResourcePath(typeof val === "string" ? val : val.path);
+      const iconSvg = String(await readFile(iconPath)).replace(/\n/g, "");
+
+      sprites.push(`<symbol id="bytm-svg-${name}">\n    ${iconSvg}\n  </symbol>`);
+    }
+
+    const spritesheet = `\
+<svg xmlns="http://www.w3.org/2000/svg" id="bytm-svg-spritesheet" style="display: none;" inert="true">
+  ${sprites.join("\n  ")}
+</svg>`;
+
+    await writeFile(resolveResourcePath("spritesheet.svg"), spritesheet);
+  }
+  catch(err) {
+    console.error(k.red("Error while creating SVG spritesheet:"), err);
+    return schedExit(1);
+  }
+}
+
+//#region @require
+
+/** Returns the `@require` directive block for each defined package in `assets/require.json`, using the version numbers from `package.json` if found */
 async function getRequireDirectives() {
   const directives: string[] = [];
   const requireFile = String(await readFile(join(assetFolderPath, "require.json")));
@@ -380,6 +478,7 @@ async function getRequireDirectives() {
   return directives.length > 0 ? directives.join("\n") : undefined;
 }
 
+/** Returns the `@require` directive for a given package entry */
 function getRequireEntry(entry: RequireObjPkg) {
   const baseUrl = entry.baseUrl ?? "https://cdn.jsdelivr.net/npm/";
 
@@ -397,6 +496,36 @@ function getRequireEntry(entry: RequireObjPkg) {
   return `// @require           ${baseUrl}${entry.pkgName}@${version}${entry.path ? `${entry.path.startsWith("/") ? "" : "/"}${entry.path}` : ""}`;
 }
 
+//#region locally linked packages
+
+/** Returns all packages set as locally linked (similar to [`npm link`](https://docs.npmjs.com/cli/v9/commands/npm-link)) in `assets/require.json` */
+async function getLinkedPkgs() {
+  const requireFile = String(await readFile(join(assetFolderPath, "require.json")));
+  const require = (JSON.parse(requireFile) as RequireObj[]);
+
+  let retStr = "";
+
+  for(const entry of require) {
+    if(!("link" in entry) || typeof entry.link !== "string" || !("pkgName" in entry))
+      continue;
+
+    try {
+      const scriptCont = String(await readFile(resolve(entry.link)));
+      const trimmedScript = scriptCont
+        .replace(/\n?\/\/\s*==.+==[\s\S]+\/\/\s*==\/.+==/gm, "");
+      retStr += `\n// <link ${entry.pkgName}>\n${trimmedScript}\n// </link ${entry.pkgName}>\n\n`;
+    }
+    catch(err) {
+      console.error(`Couldn't read linked package at '${entry.link}':`, err);
+      schedExit(1);
+    }
+  }
+
+  return retStr;
+}
+
+//#region @description:localized
+
 /** Returns the @description directive block for each defined locale in `assets/locales.json` */
 function getLocalizedDescriptions() {
   try {
@@ -423,6 +552,8 @@ function getLocalizedDescriptions() {
   }
 }
 
+//#region @resource
+
 /**
  * Returns the full URL for a given resource path, based on the current mode and branch
  * @param path If the path starts with a /, it is treated as an absolute path, starting at project root. Otherwise it will be relative to the assets folder.
@@ -453,46 +584,7 @@ function resolveResourcePath(path: string): string {
   return `assets/${path}`;
 }
 
-/** Returns the value of a CLI argument (in the format `--arg=<value>`) or the value of `defaultVal` if it doesn't exist */
-function getCliArg<TReturn extends string = string>(name: string, defaultVal: TReturn | (string & {})): TReturn
-/** Returns the value of a CLI argument (in the format `--arg=<value>`) or undefined if it doesn't exist */
-function getCliArg<TReturn extends string = string>(name: string, defaultVal?: TReturn | (string & {})): TReturn | undefined
-/** Returns the value of a CLI argument (in the format `--arg=<value>`) or the value of `defaultVal` if it doesn't exist */
-function getCliArg<TReturn extends string = string>(name: string, defaultVal?: TReturn | (string & {})): TReturn | undefined {
-  const arg = argv.find((v) => v.trim().match(new RegExp(`^(--)?${name}=.+$`, "i")));
-  const val = arg?.split("=")?.[1];
-  return (val && val.length > 0 ? val : defaultVal)?.trim() as TReturn | undefined;
-}
-
-async function getLinkedPkgs() {
-  const requireFile = String(await readFile(join(assetFolderPath, "require.json")));
-  const require = (JSON.parse(requireFile) as RequireObj[]);
-
-  let retStr = "";
-
-  for(const entry of require) {
-    if(!("link" in entry) || typeof entry.link !== "string" || !("pkgName" in entry))
-      continue;
-
-    try {
-      const scriptCont = String(await readFile(resolve(entry.link)));
-      const trimmedScript = scriptCont
-        .replace(/\n?\/\/\s*==.+==[\s\S]+\/\/\s*==\/.+==/gm, "");
-      retStr += `\n// <link ${entry.pkgName}>\n${trimmedScript}\n// </link ${entry.pkgName}>\n\n`;
-    }
-    catch(err) {
-      console.error(`Couldn't read linked package at '${entry.link}':`, err);
-      schedExit(1);
-    }
-  }
-
-  return retStr;
-}
-
-/** Schedules an exit after I/O events finish */
-function schedExit(code: number) {
-  setImmediate(() => exit(code));
-}
+//#region misc
 
 /** Generates a random ID of the given {@linkcode length} and {@linkcode radix} */
 function randomId(length = 16, radix = 16, randomCase = true) {
@@ -505,63 +597,6 @@ function randomId(length = 16, radix = 16, randomCase = true) {
   return arr.join("");
 }
 
-/**
- * Calculates the SHA-256 hash of the file at the given path.  
- * Uses {@linkcode resolveResourcePath()} to resolve the path, meaning paths prefixed with a slash are relative to the repository root, otherwise they are relative to the `assets` directory.
- */
-function getFileHashSha256(path: string): Promise<string> {
-  path = resolveResourcePath(path);
-
-  return new Promise((res, rej) => {
-    const hash = createHash("sha256");
-    const stream = createReadStream(resolve(path));
-    stream.on("data", data => hash.update(data));
-    stream.on("end", () => res(hash.digest("base64")));
-    stream.on("error", rej);
-  });
-}
-
-/** Calculates the SHA-256 hash of a ReadableStream, like the `body` prop of a `fetch()` call */
-function getStreamHashSha256(rStream: ReadableStream): Promise<string> {
-  return new Promise((res, rej) => {
-    const hash = createHash("sha256");
-    rStream.pipeTo(new WritableStream({
-      write(chunk) {
-        hash.update(chunk);
-      },
-    })).then(() => res(hash.digest("base64"))).catch(rej);
-  });
-}
-
-/** Compiles all `icon-*` assets into a single SVG spritesheet file and writes it to `assets/spritesheet.svg` */
-async function createSvgSpritesheet() {
-  try {
-    const sprites: string[] = [];
-
-    for(const [name, val] of Object.entries(resourcesJson.resources)) {
-      if(!/^icon-/.test(name))
-        continue;
-
-      const iconPath = resolveResourcePath(typeof val === "string" ? val : val.path);
-      const iconSvg = String(await readFile(iconPath)).replace(/\n/g, "");
-
-      sprites.push(`<symbol id="bytm-svg-${name}">\n    ${iconSvg}\n  </symbol>`);
-    }
-
-    await writeFile(
-      resolveResourcePath("spritesheet.svg"),
-      `\
-<svg xmlns="http://www.w3.org/2000/svg" id="bytm-svg-spritesheet" style="display: none;" inert="true">
-  ${sprites.join("\n  ")}
-</svg>`,
-    );
-  }
-  catch(err) {
-    console.error(k.red("Error while creating SVG spritesheet:"), err);
-    return schedExit(1);
-  }
-}
-
 /** Checks if the given string is a valid URL with a protocol that starts with `http` */
 function validUrl(url: string) {
   try {
@@ -571,3 +606,5 @@ function validUrl(url: string) {
     return false;
   }
 }
+
+main();