From c944a273d095dbd12b24281a77651aaa95089432 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 8 Sep 2023 10:33:57 +0100
Subject: [PATCH] Convert copy-res to typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 .eslintrc.js                         |  2 +-
 package.json                         |  8 ++--
 scripts/{copy-res.js => copy-res.ts} | 60 ++++++++++++++--------------
 src/@types/cpx.d.ts                  | 43 ++++++++++++++++++++
 src/@types/loader-utils.d.ts         | 28 +++++++++++++
 tsconfig.json                        |  3 +-
 webpack.config.js                    |  2 +-
 yarn.lock                            | 19 ++++++++-
 8 files changed, 127 insertions(+), 38 deletions(-)
 rename scripts/{copy-res.js => copy-res.ts} (81%)
 create mode 100644 src/@types/cpx.d.ts
 create mode 100644 src/@types/loader-utils.d.ts

diff --git a/.eslintrc.js b/.eslintrc.js
index 72cadf8138..14702724f9 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -19,7 +19,7 @@ module.exports = {
     },
     overrides: [
         {
-            files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
+            files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "scripts/*.ts"],
             extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
             // NOTE: These rules are frozen and new rules should not be added here.
             // New changes belong in https://github.com/matrix-org/eslint-plugin-matrix-org/
diff --git a/package.json b/package.json
index 7180aa2f33..195f663d99 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
         "build": "yarn clean && yarn build:genfiles && yarn build:bundle",
         "build-stats": "yarn clean && yarn build:genfiles && yarn build:bundle-stats",
         "build:jitsi": "ts-node scripts/build-jitsi.ts",
-        "build:res": "node scripts/copy-res.js",
+        "build:res": "ts-node scripts/copy-res.ts",
         "build:genfiles": "yarn build:res && yarn build:jitsi && yarn build:module_system",
         "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
         "build:bundle": "webpack --progress --bail --mode production",
@@ -47,7 +47,7 @@
         "dist": "scripts/package.sh",
         "start": "yarn build:module_system && concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js\"",
         "start:https": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n res,element-js \"yarn start:res\" \"yarn start:js --https\"",
-        "start:res": "yarn build:jitsi && node scripts/copy-res.js -w",
+        "start:res": "yarn build:jitsi && ts-node scripts/copy-res.ts -w",
         "start:js": "webpack-dev-server --output-filename=bundles/_dev_/[name].js --output-chunk-filename=bundles/_dev_/[name].js -w --mode development --disable-host-check --hot",
         "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
         "lint:js": "yarn lint:js:src && yarn lint:js:module_system",
@@ -106,9 +106,11 @@
         "@sentry/webpack-plugin": "^2.0.0",
         "@svgr/webpack": "^5.5.0",
         "@testing-library/react": "^12.1.5",
+        "@types/cpx": "1.5.0",
         "@types/jest": "^29.0.0",
         "@types/jitsi-meet": "^2.0.2",
         "@types/jsrsasign": "^10.5.4",
+        "@types/loader-utils": "^2.0.4",
         "@types/lodash": "^4.14.197",
         "@types/modernizr": "^3.5.3",
         "@types/node": "^16",
@@ -123,7 +125,7 @@
         "babel-loader": "^8.2.2",
         "chokidar": "^3.5.1",
         "concurrently": "^8.0.0",
-        "cpx": "^1.5.0",
+        "cpx": "1.5.0",
         "css-loader": "^4",
         "dotenv": "^16.0.2",
         "eslint": "8.48.0",
diff --git a/scripts/copy-res.js b/scripts/copy-res.ts
similarity index 81%
rename from scripts/copy-res.js
rename to scripts/copy-res.ts
index 91c7dd41c2..5954bc05ae 100755
--- a/scripts/copy-res.js
+++ b/scripts/copy-res.ts
@@ -1,16 +1,27 @@
 #!/usr/bin/env node
 
-const loaderUtils = require("loader-utils");
-
 // copies the resources into the webapp directory.
 
+import parseArgs from "minimist";
+import * as chokidar from "chokidar";
+import * as fs from "node:fs";
+import * as _ from "lodash";
+import * as Cpx from "cpx";
+import * as loaderUtils from "loader-utils";
+
 const I18N_BASE_PATH = "src/i18n/strings/";
 const INCLUDE_LANGS = fs.readdirSync(I18N_BASE_PATH).filter((fn) => fn.endsWith(".json"));
 
 // cpx includes globbed parts of the filename in the destination, but excludes
 // common parents. Hence, "res/{a,b}/**": the output will be "dest/a/..." and
 // "dest/b/...".
-const COPY_LIST = [
+const COPY_LIST: [
+    sourceGlob: string,
+    outputPath: string,
+    opts?: {
+        directwatch?: 1;
+    },
+][] = [
     ["res/apple-app-site-association", "webapp"],
     ["res/manifest.json", "webapp"],
     ["res/sw.js", "webapp"],
@@ -24,19 +35,12 @@ const COPY_LIST = [
     ["./config.json", "webapp", { directwatch: 1 }],
     ["contribute.json", "webapp"],
 ];
-
-const parseArgs = require("minimist");
-const Cpx = require("cpx");
-const chokidar = require("chokidar");
-const fs = require("fs");
-const _ = require("lodash");
-
 const argv = parseArgs(process.argv.slice(2), {});
 
 const watch = argv.w;
 const verbose = argv.v;
 
-function errCheck(err) {
+function errCheck(err?: Error): void {
     if (err) {
         console.error(err.message);
         process.exit(1);
@@ -52,7 +56,7 @@ if (!fs.existsSync("webapp/i18n/")) {
     fs.mkdirSync("webapp/i18n/");
 }
 
-function next(i, err) {
+function next(i: number, err?: Error): void {
     errCheck(err);
 
     if (i >= COPY_LIST.length) {
@@ -63,13 +67,9 @@ function next(i, err) {
     const source = ent[0];
     const dest = ent[1];
     const opts = ent[2] || {};
-    let cpx = undefined;
+    const cpx = new Cpx.Cpx(source, dest);
 
-    if (!opts.lang) {
-        cpx = new Cpx.Cpx(source, dest);
-    }
-
-    if (verbose && cpx) {
+    if (verbose) {
         cpx.on("copy", (event) => {
             console.log(`Copied: ${event.srcPath} --> ${event.dstPath}`);
         });
@@ -78,7 +78,7 @@ function next(i, err) {
         });
     }
 
-    const cb = (err) => {
+    const cb = (err?: Error): void => {
         next(i + 1, err);
     };
 
@@ -88,7 +88,7 @@ function next(i, err) {
             // which in the case of config.json is '.', which inevitably takes
             // ages to crawl. So we create our own watcher on the files
             // instead.
-            const copy = () => {
+            const copy = (): void => {
                 cpx.copy(errCheck);
             };
             chokidar.watch(source).on("add", copy).on("change", copy).on("ready", cb).on("error", errCheck);
@@ -102,7 +102,7 @@ function next(i, err) {
     }
 }
 
-function genLangFile(lang, dest) {
+function genLangFile(lang: string, dest: string): string {
     const reactSdkFile = "node_modules/matrix-react-sdk/src/i18n/strings/" + lang + ".json";
     const riotWebFile = I18N_BASE_PATH + lang + ".json";
 
@@ -120,7 +120,7 @@ function genLangFile(lang, dest) {
 
     const json = JSON.stringify(translations, null, 4);
     const jsonBuffer = Buffer.from(json);
-    const digest = loaderUtils.getHashDigest(jsonBuffer, null, null, 7);
+    const digest = loaderUtils.getHashDigest(jsonBuffer, null, "hex", 7);
     const filename = `${lang}.${digest}.json`;
 
     fs.writeFileSync(dest + filename, json);
@@ -131,8 +131,8 @@ function genLangFile(lang, dest) {
     return filename;
 }
 
-function genLangList(langFileMap) {
-    const languages = {};
+function genLangList(langFileMap: Record<string, string>): void {
+    const languages: Record<string, string> = {};
     INCLUDE_LANGS.forEach(function (lang) {
         const normalizedLanguage = lang.toLowerCase().replace("_", "-");
         const languageParts = normalizedLanguage.split("-");
@@ -144,7 +144,7 @@ function genLangList(langFileMap) {
     });
     fs.writeFile("webapp/i18n/languages.json", JSON.stringify(languages, null, 4), function (err) {
         if (err) {
-            console.error("Copy Error occured: " + err);
+            console.error("Copy Error occured: " + err.message);
             throw new Error("Failed to generate languages.json");
         }
     });
@@ -158,15 +158,15 @@ function genLangList(langFileMap) {
  * regenerate the file, adding its content-hashed filename to langFileMap
  * and regenerating languages.json with the new filename
  */
-function watchLanguage(lang, dest, langFileMap) {
+function watchLanguage(lang: string, dest: string, langFileMap: Record<string, string>): void {
     const reactSdkFile = "node_modules/matrix-react-sdk/src/i18n/strings/" + lang + ".json";
     const riotWebFile = I18N_BASE_PATH + lang + ".json";
 
     // XXX: Use a debounce because for some reason if we read the language
     // file immediately after the FS event is received, the file contents
     // appears empty. Possibly https://github.com/nodejs/node/issues/6112
-    let makeLangDebouncer;
-    const makeLang = () => {
+    let makeLangDebouncer: number;
+    const makeLang = (): void => {
         if (makeLangDebouncer) {
             clearTimeout(makeLangDebouncer);
         }
@@ -184,7 +184,7 @@ function watchLanguage(lang, dest, langFileMap) {
 
 // language resources
 const I18N_DEST = "webapp/i18n/";
-const I18N_FILENAME_MAP = INCLUDE_LANGS.reduce((m, l) => {
+const I18N_FILENAME_MAP = INCLUDE_LANGS.reduce<Record<string, string>>((m, l) => {
     const filename = genLangFile(l, I18N_DEST);
     m[l] = filename;
     return m;
@@ -192,7 +192,7 @@ const I18N_FILENAME_MAP = INCLUDE_LANGS.reduce((m, l) => {
 genLangList(I18N_FILENAME_MAP);
 
 if (watch) {
-    INCLUDE_LANGS.forEach((l) => watchLanguage(l.value, I18N_DEST, I18N_FILENAME_MAP));
+    INCLUDE_LANGS.forEach((l) => watchLanguage(l, I18N_DEST, I18N_FILENAME_MAP));
 }
 
 // non-language resources
diff --git a/src/@types/cpx.d.ts b/src/@types/cpx.d.ts
new file mode 100644
index 0000000000..f1980d932a
--- /dev/null
+++ b/src/@types/cpx.d.ts
@@ -0,0 +1,43 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import "cpx";
+import type EventEmitter from "events";
+
+declare module "cpx" {
+    export class Cpx extends EventEmitter {
+        public constructor(source: string, outDir: string, options?: object);
+
+        /**
+         * Copy all files that matches `this.source` pattern to `this.outDir`.
+         *
+         * @param {function} [cb = null] - A callback function.
+         * @returns {void}
+         */
+        public copy(cb: Function | null): void;
+
+        /**
+         * Copy all files that matches `this.source` pattern to `this.outDir`.
+         * And watch changes in `this.base`, and copy only the file every time.
+         *
+         * @returns {void}
+         * @throws {Error} This had been watching already.
+         */
+        public watch(): void;
+    }
+}
+
+export as namespace Cpx;
diff --git a/src/@types/loader-utils.d.ts b/src/@types/loader-utils.d.ts
new file mode 100644
index 0000000000..c9ceb6574a
--- /dev/null
+++ b/src/@types/loader-utils.d.ts
@@ -0,0 +1,28 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as LoaderUtils from "loader-utils";
+
+declare module "loader-utils" {
+    export function getHashDigest(
+        buffer: Buffer,
+        hashType: null,
+        digestType: LoaderUtils.DigestType,
+        maxLength: number,
+    ): string;
+}
+
+export as namespace Cpx;
diff --git a/tsconfig.json b/tsconfig.json
index ac8ea2d73f..5762df78d4 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,6 +24,7 @@
         "./src/**/*.ts",
         "./src/**/*.tsx",
         "./test/**/*.ts",
-        "./test/**/*.tsx"
+        "./test/**/*.tsx",
+        "./scripts/*.ts"
     ]
 }
diff --git a/webpack.config.js b/webpack.config.js
index f38a5caed0..7ec3fb89ab 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -507,7 +507,7 @@ module.exports = (env, argv) => {
                 },
                 {
                     // cache-bust languages.json file placed in
-                    // element-web/webapp/i18n during build by copy-res.js
+                    // element-web/webapp/i18n during build by copy-res.ts
                     test: /\.*languages.json$/,
                     type: "javascript/auto",
                     loader: "file-loader",
diff --git a/yarn.lock b/yarn.lock
index 5512016831..7dcf19233d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2398,6 +2398,13 @@
   dependencies:
     "@babel/types" "^7.20.7"
 
+"@types/cpx@1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@types/cpx/-/cpx-1.5.0.tgz#d2a44b0ab5ec32bfdd743f64aae84847858bce0c"
+  integrity sha512-kuGK3lZqEvHTSDbJcaA6tcPoEXV4/e88YrltZMcQUewZhzYQwNSTMGIiPBqeeFd4LCBo1CX5U6CV6LaHG3wXSg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/events@^3.0.0":
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -2492,6 +2499,14 @@
   resolved "https://registry.yarnpkg.com/@types/jsrsasign/-/jsrsasign-10.5.8.tgz#0d6c638505454b5e95c684d6f604d57641417336"
   integrity sha512-1oZ3TbarAhKtKUpyrCIqXpbx3ZAfoSulleJs6/UzzyYty0ut+kjRX7zHLAaHwVIuw8CBjIymwW4J2LK944HoHQ==
 
+"@types/loader-utils@^2.0.4":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-2.0.4.tgz#f1c9dd27392f163ee92394454563286dfc6e4778"
+  integrity sha512-I71X8yySVQW6DuXr78/McC+enpUYQ68JxAYlgVyuMvl5mb7jFUZpFAu1qURZcwvbITXwxPnrA7hbV0W3HHsbbg==
+  dependencies:
+    "@types/node" "*"
+    "@types/webpack" "^4"
+
 "@types/lodash@^4.14.197":
   version "4.14.198"
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.198.tgz#4d27465257011aedc741a809f1269941fa2c5d4c"
@@ -2651,7 +2666,7 @@
     "@types/source-list-map" "*"
     source-map "^0.7.3"
 
-"@types/webpack@^4.41.8":
+"@types/webpack@^4", "@types/webpack@^4.41.8":
   version "4.41.33"
   resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc"
   integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==
@@ -4449,7 +4464,7 @@ counterpart@^0.18.6:
     pluralizers "^0.1.7"
     sprintf-js "^1.0.3"
 
-cpx@^1.5.0:
+cpx@1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f"
   integrity sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==