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): void { + const languages: Record = {}; 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): 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>((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==