diff --git a/.babelrc.js b/.babelrc.mjs similarity index 76% rename from .babelrc.js rename to .babelrc.mjs index 412338095f..9c75d7006b 100644 --- a/.babelrc.js +++ b/.babelrc.mjs @@ -1,20 +1,23 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check -module.exports = { +/** @type {import("@babel/core").TransformOptions} */ +const config = { presets: ['@babel/preset-react', '@babel/preset-typescript'], // Detects the type of file being babel'd (either esmodule or commonjs) sourceType: 'unambiguous', plugins: [ 'lodash', '@babel/plugin-transform-typescript', - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-optional-chaining', - '@babel/plugin-proposal-nullish-coalescing-operator', // This plugin converts commonjs to esmodules which is required for // importing commonjs modules from esmodules in storybook. As a part of // converting to TypeScript we should use esmodules and can eventually // remove this plugin process.env.SIGNAL_ENV === 'storybook' && '@babel/transform-runtime', - ].filter(Boolean), + ].filter(plugin => { + return typeof plugin === 'string'; + }), }; + +export default config; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a3fdf2df..4284a549f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,9 +134,9 @@ jobs: - name: Upload installer size if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }} run: | - ./node_modules/.bin/tsx ts/scripts/publish-installer-size.node.ts macos-arm64 - ./node_modules/.bin/tsx ts/scripts/publish-installer-size.node.ts macos-x64 - ./node_modules/.bin/tsx ts/scripts/publish-installer-size.node.ts macos-universal + node scripts/publish-installer-size.mjs macos-arm64 + node scripts/publish-installer-size.mjs macos-x64 + node scripts/publish-installer-size.mjs macos-universal - run: pnpm run test-release env: NODE_ENV: production @@ -216,7 +216,7 @@ jobs: - name: Upload installer size if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }} - run: ./node_modules/.bin/tsx ts/scripts/publish-installer-size.node.ts linux + run: node scripts/publish-installer-size.mjs linux - run: xvfb-run --auto-servernum pnpm run test-node @@ -294,7 +294,7 @@ jobs: - name: Upload installer size if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }} - run: ./node_modules/.bin/tsx ts/scripts/publish-installer-size.node.ts windows + run: node scripts/publish-installer-size.mjs windows - run: pnpm run test-electron env: @@ -483,4 +483,4 @@ jobs: - run: pnpm generate:phase-0 - name: Run OS version check run: | - ./node_modules/.bin/tsx ts/scripts/check-min-os-version.node.ts + node scripts/check-min-os-version.mjs diff --git a/.github/workflows/icu-book.yml b/.github/workflows/icu-book.yml index b227bb919f..7cbd74505a 100644 --- a/.github/workflows/icu-book.yml +++ b/.github/workflows/icu-book.yml @@ -51,7 +51,7 @@ jobs: env: ARTIFACTS_DIR: stories - run: pnpm run build:rolldown - - run: ./node_modules/.bin/tsx ts/scripts/compile-stories-icu-lookup.node.ts stories + - run: node scripts/compile-stories-icu-lookup.mjs stories - name: Upload test artifacts if: github.event_name == 'workflow_dispatch' diff --git a/.oxlint/rules/enforceFileSuffix.mjs b/.oxlint/rules/enforceFileSuffix.mjs index 33c0fb7d98..12db7f78b9 100644 --- a/.oxlint/rules/enforceFileSuffix.mjs +++ b/.oxlint/rules/enforceFileSuffix.mjs @@ -150,7 +150,6 @@ const NODE_PACKAGES = new Set([ 'tailwindcss', 'terser-webpack-plugin', 'tsx', - 'ts-node', 'typescript', 'wait-on', 'webpack', @@ -204,9 +203,6 @@ const DOM_PACKAGES = new Set([ // Packages that can run in both browser/node const STD_PACKAGES = new Set([ '@babel/core', - '@babel/plugin-proposal-class-properties', - '@babel/plugin-proposal-nullish-coalescing-operator', - '@babel/plugin-proposal-optional-chaining', '@babel/plugin-transform-runtime', '@babel/plugin-transform-typescript', '@babel/preset-react', @@ -536,7 +532,9 @@ export const enforceFileSuffix = ESLintUtils.RuleCreator.withoutDocs({ return; } - const match = filename.match(/\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx)$/); + const match = filename.match( + /\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx|js|mjs)$/ + ); if (match == null) { context.report({ node: node, diff --git a/.oxlint/test-setup.mjs b/.oxlint/test-setup.mjs index 351e313291..4ad4eaf77d 100644 --- a/.oxlint/test-setup.mjs +++ b/.oxlint/test-setup.mjs @@ -1,6 +1,7 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import * as mocha from 'mocha'; +// @ts-check +import * as mocha from 'mocha' import { RuleTester } from '@typescript-eslint/rule-tester'; RuleTester.afterAll = mocha.after; diff --git a/.oxlintrc.json b/.oxlintrc.json index fa48a150dd..8d08ff7216 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -62,6 +62,16 @@ "target": ["ts/util/**", "ts/types/**"], "from": ["ts/components/**", "ts/axo/**/*.dom.*"], "message": "Importing components is forbidden from ts/{util,types}" + }, + { + "target": ["scripts/**", "codemods/**"], + "from": ["ts/**"], + "message": "Scripts should not import from ts/**" + }, + { + "target": ["ts/**"], + "from": ["scripts/**", "codemods/**"], + "message": "App should not import from scripts" } ] } @@ -1599,8 +1609,6 @@ "sticker-creator/**", // Third-party files "components/mp3lameencoder/lib/Mp3LameEncoder.js", - "components/recorderjs/recorder.js", - "components/recorderjs/recorderWorker.js", "components/webaudiorecorder/lib/WebAudioRecorderMp3.js", "js/WebAudioRecorderMp3.js" ], @@ -1626,7 +1634,7 @@ // misc ".storybook/preview.tsx", "preload.wrapper.ts", - "test/test.js" + "test/test.mjs" ], "env": { "browser": true } }, @@ -1642,14 +1650,14 @@ "codemods/**", "danger/**", "scripts/**", - ".babelrc.js", - ".prettierrc.js", + "test/test.m", + ".babelrc.mjs", + ".prettierrc.mjs", ".storybook/test-runner.ts", ".stylelintrc.js", "ci.js", "dangerfile.js", "eslint-local-rules.js", - "test/setup-test-node.js", "preload.wrapper.ts", "rolldown.config.ts" ], @@ -1660,14 +1668,15 @@ "ts/test-node/**", "ts/test-electron/**", "ts/test-mock/**", - "test/test.js" + "test/test.mjs", + "scripts/**/*_test.mjs" ], "env": { "mocha": true } }, // disables { - "files": ["ts/scripts/**", "scripts/**", "danger/**", "ts/util/lint/**"], + "files": ["scripts/**", "danger/**", "ts/util/lint/**"], "rules": { "no-console": "off" } @@ -1692,8 +1701,10 @@ "danger/**", "packages/**", "scripts/**", - "test/**", - ".*rc.js", + "test/test.mjs", + ".babelrc.mjs", + ".prettierrc.mjs", + ".stylelintrc.mjs", "ci.js", "dangerfile.mjs", "preload.wrapper.ts", @@ -1714,16 +1725,13 @@ ".oxlint/**", ".storybook/**", "build/intl-linter/**", - "codemods/**", "danger/**", - "scripts/**", - "test/**", - "ts/scripts/**", "ts/test-electron/**", "ts/test-mock/**", "ts/test-node/**", "ts/util/lint/**", "ts/windows/main/*_test.*", + "test/test.mjs", "dangerfile.js", "preload.wrapper.ts", "rolldown.config.ts" diff --git a/.prettierignore b/.prettierignore index 8cc0ceae96..2920ef57af 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,9 +10,7 @@ dist/** js/components.js js/util_worker.js libtextsecure/components.js -libtextsecure/test/test.js stylesheets/*.css -test/test.js ts/**/*.js ts/protobuf/*.d.ts ts/protobuf/*.js diff --git a/.prettierrc.js b/.prettierrc.mjs similarity index 86% rename from .prettierrc.js rename to .prettierrc.mjs index ebff2fb615..2cee7a81a4 100644 --- a/.prettierrc.js +++ b/.prettierrc.mjs @@ -1,8 +1,9 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check /** @type {import("prettier").Config} */ -module.exports = { +const config = { plugins: ['prettier-plugin-tailwindcss'], singleQuote: true, arrowParens: 'avoid', @@ -11,3 +12,5 @@ module.exports = { tailwindFunctions: ['tw'], tailwindAttributes: [], }; + +export default config diff --git a/.stylelintrc.js b/.stylelintrc.mjs similarity index 93% rename from .stylelintrc.js rename to .stylelintrc.mjs index 8c836090a0..7b6b631e78 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.mjs @@ -1,7 +1,9 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check -module.exports = { +/** @type {import('stylelint').Config} */ +const config = { extends: [ 'stylelint-config-recommended-scss', 'stylelint-config-css-modules', @@ -45,3 +47,5 @@ module.exports = { }, }, }; + +export default config diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index ff959eda27..3d21ba288b 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -30,57 +30,7 @@ Signal Desktop makes use of the following open source projects. OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## @babel/plugin-proposal-class-properties - - MIT License - - Copyright (c) 2014-present Sebastian McKenzie and other contributors - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## @babel/plugin-proposal-nullish-coalescing-operator - - MIT License - - Copyright (c) 2014-present Sebastian McKenzie and other contributors - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -## @babel/plugin-proposal-optional-chaining +## @babel/helper-plugin-utils MIT License @@ -4464,6 +4414,54 @@ Signal Desktop makes use of the following open source projects. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## @types/babel\_\_core + + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +## @types/babel\_\_helper-plugin-utils + + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + ## @types/blueimp-load-image MIT License diff --git a/app/.eslintrc.js b/app/.eslintrc.js deleted file mode 100644 index ea1600cb85..0000000000 --- a/app/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2018 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -// For reference: https://github.com/airbnb/javascript - -const rules = { - 'no-console': 'off', -}; - -module.exports = { - rules, -}; diff --git a/codemods/protopiler-migration/01-remove-finish.js b/codemods/protopiler-migration/01-remove-finish.mjs similarity index 87% rename from codemods/protopiler-migration/01-remove-finish.js rename to codemods/protopiler-migration/01-remove-finish.mjs index 8bf887330c..6c0583f2bf 100644 --- a/codemods/protopiler-migration/01-remove-finish.js +++ b/codemods/protopiler-migration/01-remove-finish.mjs @@ -1,6 +1,9 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export default function transform() { +// @ts-check +import { declare } from '@babel/helper-plugin-utils'; + +export default declare(function transform() { return { visitor: { CallExpression(path) { @@ -32,4 +35,4 @@ export default function transform() { }, }, }; -} +}); diff --git a/codemods/protopiler-migration/02-remove-i.js b/codemods/protopiler-migration/02-remove-i.mjs similarity index 68% rename from codemods/protopiler-migration/02-remove-i.js rename to codemods/protopiler-migration/02-remove-i.mjs index 63fcea00c8..c5a316af69 100644 --- a/codemods/protopiler-migration/02-remove-i.js +++ b/codemods/protopiler-migration/02-remove-i.mjs @@ -1,6 +1,9 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export default function transform(babel) { +// @ts-check +import { declare } from '@babel/helper-plugin-utils'; + +export default declare(function transform(babel) { const { types: t } = babel; return { @@ -20,15 +23,15 @@ export default function transform(babel) { } path.replaceWith( - t.TSQualifiedName( - t.TSQualifiedName( + t.tsQualifiedName( + t.tsQualifiedName( node.left, - t.Identifier(node.right.name.slice(1)) + t.identifier(node.right.name.slice(1)) ), - t.Identifier('Params') + t.identifier('Params') ) ); }, }, }; -} +}); diff --git a/codemods/protopiler-migration/03-to-number.js b/codemods/protopiler-migration/03-to-number.mjs similarity index 62% rename from codemods/protopiler-migration/03-to-number.js rename to codemods/protopiler-migration/03-to-number.mjs index 493c092e91..43e308bcad 100644 --- a/codemods/protopiler-migration/03-to-number.js +++ b/codemods/protopiler-migration/03-to-number.mjs @@ -1,18 +1,35 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check +import { declare } from '@babel/helper-plugin-utils'; import { relative, dirname, join } from 'node:path'; -const importPath = join(__dirname, '..', 'ts', 'util', 'toNumber.std.js'); +/** @import { NodePath, types as t, PluginPass } from '@babel/core' */ -export default function transform(babel) { +const importPath = join( + import.meta.dirname, + '..', + 'ts', + 'util', + 'toNumber.std.js' +); + +export default declare(function transform(babel) { const { types: t } = babel; + /** @type {t.Program | undefined} */ let program; + + /** + * @param {NodePath} path + */ function Program({ node }) { program = node; } + /** + * @param {string} filename + */ function addImport(filename) { if (program === undefined) { return; @@ -33,14 +50,18 @@ export default function transform(babel) { program.body.splice( index, 0, - t.ImportDeclaration( - [t.ImportSpecifier(t.Identifier('toNumber'), t.Identifier('toNumber'))], - t.StringLiteral(relativePath) + t.importDeclaration( + [t.importSpecifier(t.identifier('toNumber'), t.identifier('toNumber'))], + t.stringLiteral(relativePath) ) ); program = undefined; } + /** + * @param {NodePath} path + * @param {PluginPass} plugin + */ function CallExpression(path, { filename }) { const { callee, arguments: args } = path.node; if (args.length !== 0) { @@ -59,10 +80,13 @@ export default function transform(babel) { return; } - const replacement = t.CallExpression(t.Identifier('toNumber'), [ + const replacement = t.callExpression(t.identifier('toNumber'), [ callee.object, ]); path.replaceWith(replacement); + if (filename == null) { + throw path.buildCodeFrameError('Missing filename'); + } addImport(filename); } @@ -73,4 +97,4 @@ export default function transform(babel) { OptionalCallExpression: CallExpression, }, }; -} +}); diff --git a/codemods/protopiler-migration/04-long-from-number.js b/codemods/protopiler-migration/04-long-from-number.mjs similarity index 53% rename from codemods/protopiler-migration/04-long-from-number.js rename to codemods/protopiler-migration/04-long-from-number.mjs index 9dc57b411a..0dba532410 100644 --- a/codemods/protopiler-migration/04-long-from-number.js +++ b/codemods/protopiler-migration/04-long-from-number.mjs @@ -1,6 +1,9 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -export default function transform(babel) { +// @ts-check +import { declare } from '@babel/helper-plugin-utils'; + +export default declare(function transform(babel) { const { types: t } = babel; return { @@ -22,11 +25,17 @@ export default function transform(babel) { } if (property.name === 'isLong') { + const [arg] = node.arguments; + if (!t.isExpression(arg)) { + throw path.buildCodeFrameError( + 'First argument to isLong must be an expression' + ); + } path.replaceWith( - t.BinaryExpression( + t.binaryExpression( '===', - t.UnaryExpression('typeof', node.arguments[0]), - t.StringLiteral('bigint') + t.unaryExpression('typeof', arg), + t.stringLiteral('bigint') ) ); return; @@ -40,14 +49,19 @@ export default function transform(babel) { return; } - if (node.arguments[0].type === 'NumericLiteral') { - path.replaceWith(t.BigIntLiteral(node.arguments[0].value.toString())); + const [arg] = node.arguments; + if (arg == null) { + throw path.buildCodeFrameError( + `Missing first argument to ${property.name}` + ); + } + + if (arg.type === 'NumericLiteral') { + path.replaceWith(t.bigIntLiteral(arg.value.toString())); return; } - path.replaceWith( - t.CallExpression(t.Identifier('BigInt'), [node.arguments[0]]) - ); + path.replaceWith(t.callExpression(t.identifier('BigInt'), [arg])); }, }, }; -} +}); diff --git a/codemods/rolldown/01-rename-imports.js b/codemods/rolldown/01-rename-imports.mjs similarity index 70% rename from codemods/rolldown/01-rename-imports.js rename to codemods/rolldown/01-rename-imports.mjs index 2c9d7a42a7..95d1698efb 100644 --- a/codemods/rolldown/01-rename-imports.js +++ b/codemods/rolldown/01-rename-imports.mjs @@ -1,10 +1,23 @@ // Copyright 2026 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { declare } from '@babel/helper-plugin-utils'; import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; -export default function transform() { - function ImportOrExport({ node }, { filename }) { +/** @import { NodePath, types as t, PluginPass } from '@babel/core' */ + +export default declare(function transform() { + /** + * @param {NodePath} path + * @param {PluginPass} plugin + */ + function ImportOrExport(path, { filename }) { + const { node } = path; + if (filename == null) { + throw path.buildCodeFrameError('Missing filename'); + } + if ( !node.source || !node.source.value.startsWith('.') || @@ -23,13 +36,10 @@ export default function transform() { const dir = dirname(filename); if (existsSync(join(dir, ts))) { - // oxlint-disable-next-line no-param-reassign node.source.value = ts; } else if (existsSync(join(dir, tsx))) { - // oxlint-disable-next-line no-param-reassign node.source.value = tsx; } else if (existsSync(join(dir, dts))) { - // oxlint-disable-next-line no-param-reassign node.source.value = dts; } else { throw new Error(`File not found: ${join(dir, node.source.value)}`); @@ -43,4 +53,4 @@ export default function transform() { ExportNamedDeclaration: ImportOrExport, }, }; -} +}); diff --git a/components/recorderjs/recorder.js b/components/recorderjs/recorder.js deleted file mode 100644 index 84ec0d4e5e..0000000000 --- a/components/recorderjs/recorder.js +++ /dev/null @@ -1,97 +0,0 @@ -(function(window){ - - var WORKER_PATH = 'recorderWorker.js'; - - var Recorder = function(source, cfg){ - var config = cfg || {}; - var bufferLen = config.bufferLen || 4096; - this.context = source.context; - this.node = (this.context.createScriptProcessor || - this.context.createJavaScriptNode).call(this.context, - bufferLen, 2, 2); - var worker = new Worker(config.workerPath || WORKER_PATH); - worker.postMessage({ - command: 'init', - config: { - sampleRate: this.context.sampleRate - } - }); - var recording = false, - currCallback; - - var self = this; - this.node.onaudioprocess = function(e){ - if (!recording) return; - self.ondata && self.ondata(e.inputBuffer.getChannelData(0)); - worker.postMessage({ - command: 'record', - buffer: [ - e.inputBuffer.getChannelData(0), - e.inputBuffer.getChannelData(1) - ] - }); - } - - this.configure = function(cfg){ - for (var prop in cfg){ - if (cfg.hasOwnProperty(prop)){ - config[prop] = cfg[prop]; - } - } - } - - this.record = function(){ - recording = true; - } - - this.stop = function(){ - recording = false; - } - - this.clear = function(){ - worker.postMessage({ command: 'clear' }); - } - - this.getBuffer = function(cb) { - currCallback = cb || config.callback; - worker.postMessage({ command: 'getBuffer' }) - } - - this.exportWAV = function(cb, type){ - currCallback = cb || config.callback; - type = type || config.type || 'audio/wav'; - if (!currCallback) throw new Error('Callback not set'); - worker.postMessage({ - command: 'exportWAV', - type: type - }); - } - - this.shutdown = function(){ - worker.terminate(); - source.disconnect(); - this.node.disconnect(); - }; - - worker.onmessage = function(e){ - var blob = e.data; - currCallback(blob); - } - - source.connect(this.node); - this.node.connect(this.context.destination); //this should not be necessary - }; - - Recorder.forceDownload = function(blob, filename){ - var url = (window.URL || window.webkitURL).createObjectURL(blob); - var link = window.document.createElement('a'); - link.href = url; - link.download = filename || 'output.wav'; - var click = document.createEvent("Event"); - click.initEvent("click", true, true); - link.dispatchEvent(click); - } - - window.Recorder = Recorder; - -})(window); diff --git a/components/recorderjs/recorderWorker.js b/components/recorderjs/recorderWorker.js deleted file mode 100644 index 08ad444c76..0000000000 --- a/components/recorderjs/recorderWorker.js +++ /dev/null @@ -1,131 +0,0 @@ -var recLength = 0, - recBuffersL = [], - recBuffersR = [], - sampleRate; - -this.onmessage = function(e){ - switch(e.data.command){ - case 'init': - init(e.data.config); - break; - case 'record': - record(e.data.buffer); - break; - case 'exportWAV': - exportWAV(e.data.type); - break; - case 'getBuffer': - getBuffer(); - break; - case 'clear': - clear(); - break; - } -}; - -function init(config){ - sampleRate = config.sampleRate; -} - -function record(inputBuffer){ - recBuffersL.push(inputBuffer[0]); - recBuffersR.push(inputBuffer[1]); - recLength += inputBuffer[0].length; -} - -function exportWAV(type){ - var bufferL = mergeBuffers(recBuffersL, recLength); - var bufferR = mergeBuffers(recBuffersR, recLength); - var interleaved = interleave(bufferL, bufferR); - var dataview = encodeWAV(interleaved); - var audioBlob = new Blob([dataview], { type: type }); - - this.postMessage(audioBlob); -} - -function getBuffer() { - var buffers = []; - buffers.push( mergeBuffers(recBuffersL, recLength) ); - buffers.push( mergeBuffers(recBuffersR, recLength) ); - this.postMessage(buffers); -} - -function clear(){ - recLength = 0; - recBuffersL = []; - recBuffersR = []; -} - -function mergeBuffers(recBuffers, recLength){ - var result = new Float32Array(recLength); - var offset = 0; - for (var i = 0; i < recBuffers.length; i++){ - result.set(recBuffers[i], offset); - offset += recBuffers[i].length; - } - return result; -} - -function interleave(inputL, inputR){ - var length = inputL.length + inputR.length; - var result = new Float32Array(length); - - var index = 0, - inputIndex = 0; - - while (index < length){ - result[index++] = inputL[inputIndex]; - result[index++] = inputR[inputIndex]; - inputIndex++; - } - return result; -} - -function floatTo16BitPCM(output, offset, input){ - for (var i = 0; i < input.length; i++, offset+=2){ - var s = Math.max(-1, Math.min(1, input[i])); - output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); - } -} - -function writeString(view, offset, string){ - for (var i = 0; i < string.length; i++){ - view.setUint8(offset + i, string.charCodeAt(i)); - } -} - -function encodeWAV(samples){ - var buffer = new ArrayBuffer(44 + samples.length * 2); - var view = new DataView(buffer); - - /* RIFF identifier */ - writeString(view, 0, 'RIFF'); - /* RIFF chunk length */ - view.setUint32(4, 36 + samples.length * 2, true); - /* RIFF type */ - writeString(view, 8, 'WAVE'); - /* format chunk identifier */ - writeString(view, 12, 'fmt '); - /* format chunk length */ - view.setUint32(16, 16, true); - /* sample format (raw) */ - view.setUint16(20, 1, true); - /* channel count */ - view.setUint16(22, 2, true); - /* sample rate */ - view.setUint32(24, sampleRate, true); - /* byte rate (sample rate * block align) */ - view.setUint32(28, sampleRate * 4, true); - /* block align (channel count * bytes per sample) */ - view.setUint16(32, 4, true); - /* bits per sample */ - view.setUint16(34, 16, true); - /* data chunk identifier */ - writeString(view, 36, 'data'); - /* data chunk length */ - view.setUint32(40, samples.length * 2, true); - - floatTo16BitPCM(view, 44, samples); - - return view; -} diff --git a/package.json b/package.json index 49acd93de3..7f6dca05ef 100644 --- a/package.json +++ b/package.json @@ -23,31 +23,31 @@ "generate:phase-1": "run-p --aggregate-output --print-label build:icu-types build:compact-locales build:styles:prod get-expire-time build:webaudio-recorder build:policy-files", "build-release": "pnpm run build", "notarize": "echo 'No longer necessary'", - "get-strings": "tsx ts/scripts/get-strings.node.ts && tsx ts/scripts/gen-nsis-script.node.ts && tsx ts/scripts/gen-locales-config.node.ts && run-p get-strings:locales get-strings:countries get-strings:emoji mark-unusued-strings-deleted && run-p build:compact-locales", - "get-strings:locales": "tsx ./ts/scripts/build-localized-display-names.node.ts locales ts/scripts/locale-data/locale-display-names.csv build/locale-display-names.json", - "get-strings:countries": "tsx ./ts/scripts/build-localized-display-names.node.ts countries ts/scripts/locale-data/country-display-names.csv build/country-display-names.json", - "get-strings:emoji": "tsx ./ts/scripts/get-emoji-locales.node.ts", - "push-strings": "tsx ts/scripts/remove-strings.node.ts && tsx ts/scripts/push-strings.node.ts", - "mark-unusued-strings-deleted": "tsx ./ts/scripts/mark-unused-strings-deleted.node.ts", - "get-expire-time": "tsx ts/scripts/get-expire-time.node.ts", + "get-strings": "node scripts/get-strings.mjs && node scripts/gen-nsis-script.mjs && node scripts/gen-locales-config.mjs && run-p get-strings:locales get-strings:countries get-strings:emoji mark-unusued-strings-deleted && run-p build:compact-locales", + "get-strings:locales": "node scripts/build-localized-display-names.mjs locales scripts/locale-data/locale-display-names.csv build/locale-display-names.json", + "get-strings:countries": "node scripts/build-localized-display-names.mjs countries scripts/locale-data/country-display-names.csv build/country-display-names.json", + "get-strings:emoji": "node scripts/get-emoji-locales.mjs", + "push-strings": "node scripts/remove-strings.mjs && node scripts/push-strings.mjs", + "mark-unusued-strings-deleted": "node scripts/mark-unused-strings-deleted.mjs", + "get-expire-time": "node scripts/get-expire-time.mjs", "build:webaudio-recorder": "terser --compress ecma=2025 --toplevel components/mp3lameencoder/lib/Mp3LameEncoder.js components/webaudiorecorder/lib/WebAudioRecorderMp3.js -o js/WebAudioRecorderMp3.js", "build:protobuf": "protopiler --module cjs --output ts/protobuf/compiled.std.js --typedefs ts/protobuf/compiled.std.d.ts protos", "clean:protobuf": "rm -f ts/protobuf/compiled.std.d.ts ts/protobuf/compiled.std.js", - "prepare-beta-build": "tsx scripts/prepare_beta_build.js", - "prepare-alpha-build": "tsx scripts/prepare_alpha_build.js", - "prepare-alpha-version": "tsx scripts/prepare_tagged_version.js alpha", - "prepare-axolotl-build": "tsx scripts/prepare_axolotl_build.js", - "prepare-axolotl-version": "tsx scripts/prepare_tagged_version.js axolotl", - "prepare-adhoc-build": "tsx scripts/prepare_adhoc_build.js", - "prepare-adhoc-version": "tsx scripts/prepare_tagged_version.js adhoc", - "prepare-staging-build": "tsx scripts/prepare_staging_build.js", - "prepare-no-delay-release": "tsx ts/scripts/prepare-no-delay-release.node.ts", - "prepare-linux-build": "tsx scripts/prepare_linux_build.js", + "prepare-beta-build": "node scripts/prepare_beta_build.mjs", + "prepare-alpha-build": "node scripts/prepare_alpha_build.mjs", + "prepare-alpha-version": "node scripts/prepare_tagged_version.mjs alpha", + "prepare-axolotl-build": "node scripts/prepare_axolotl_build.mjs", + "prepare-axolotl-version": "node scripts/prepare_tagged_version.mjs axolotl", + "prepare-adhoc-build": "node scripts/prepare_adhoc_build.mjs", + "prepare-adhoc-version": "node scripts/prepare_tagged_version.mjs adhoc", + "prepare-staging-build": "node scripts/prepare_staging_build.mjs", + "prepare-no-delay-release": "node scripts/prepare-no-delay-release.mjs", + "prepare-linux-build": "node scripts/prepare_linux_build.mjs", "test": "run-s test-node test-electron test-lint-intl test-oxlint", - "test-electron": "tsx ts/scripts/test-electron.node.ts", - "test-release": "tsx ts/scripts/test-release.node.ts", - "test-node": "cross-env NODE_ENV=test NODE_OPTIONS='--import=tsx' LANG=en-us electron-mocha --timeout 10000 --file ts/test-node/setup.preload.ts --recursive ts/test-node/**/*.ts ts/test-node/**/*.tsx", - "test-mock": "tsx ts/scripts/mocha-separator.node.ts --require ts/test-mock/setup-ci.node.ts -- ts/test-mock/**/*_test.node.ts", + "test-electron": "node scripts/test-electron.mjs", + "test-release": "node scripts/test-release.mjs", + "test-node": "cross-env NODE_ENV=test NODE_OPTIONS='--import=tsx' LANG=en-us electron-mocha --timeout 10000 --file ts/test-node/setup.preload.ts --recursive ts/test-node/**/*.{ts,tsx} scripts/**/*_test.mjs", + "test-mock": "node scripts/mocha-separator.mjs --require ts/test-mock/setup-ci.node.ts -- ts/test-mock/**/*_test.node.ts", "test-mock-docker": "mocha --require ts/test-mock/setup-ci.node.ts ts/test-mock/**/*_test.docker.node.ts", "test-oxlint": "mocha --require .oxlint/test-setup.mjs .oxlint/rules/**/*.test.mjs", "test-lint-intl": "tsx ./build/intl-linter/linter.node.ts --test", @@ -64,7 +64,7 @@ "svgo": "svgo --multipass images/**/*.svg images/*.svg", "transpile": "run-p check:types build:rolldown", "check:types": "tsgo --noEmit", - "clean-transpile": "node ./scripts/clean-transpile.js", + "clean-transpile": "node scripts/clean-transpile.mjs", "ready": "npm-run-all --print-label clean-transpile generate --parallel lint lint-deps lint-intl test-node test-electron", "dev": "pnpm run build:protobuf && cross-env SIGNAL_ENV=storybook storybook dev --port 6006", "dev:transpile": "run-p \"check:types --watch\" dev:rolldown dev:icu-types dev:protobuf", @@ -81,16 +81,16 @@ "build": "run-s --print-label generate build:rolldown:prod build:release", "build-win32-all": "run-s --print-label generate build:rolldown:prod build:release-win32-all", "build-linux": "run-s build:policy-files generate build:rolldown:prod && pnpm run build:release --publish=never", - "build:acknowledgments": "node scripts/generate-acknowledgments.js", - "build:dns-fallback": "tsx ts/scripts/generate-dns-fallback.node.ts", - "build:icu-types": "tsx ts/scripts/generate-icu-types.node.ts", - "build:compact-locales": "tsx ts/scripts/generate-compact-locales.node.ts", - "build:tray-icons": "tsx ts/scripts/generate-tray-icons.node.ts", + "build:acknowledgments": "node scripts/generate-acknowledgments.mjs", + "build:dns-fallback": "node scripts/generate-dns-fallback.mjs", + "build:icu-types": "node scripts/generate-icu-types.mjs", + "build:compact-locales": "node scripts/generate-compact-locales.mjs", + "build:tray-icons": "node scripts/generate-tray-icons.mjs", "build:dev": "run-s --print-label generate build:rolldown:prod", "build:rolldown": "rolldown -c", "build:rolldown:prod": "pnpm build:rolldown --minify", "build:esbuild:prod": "pnpm build:rolldown:prod", - "build:policy-files": "tsx ts/scripts/gen-policy-files.node.ts", + "build:policy-files": "node scripts/gen-policy-files.mjs", "build:styles": "pnpm run \"/^build:styles:(sass|tailwind)$/\"", "build:styles:prod": "pnpm run \"/^build:styles:(sass|tailwind):prod$/\"", "build:styles:sass": "sass stylesheets/manifest.scss:stylesheets/manifest.css stylesheets/manifest_bridge.scss:stylesheets/manifest_bridge.css node_modules/@signalapp/quill-cjs/dist/quill.core.css:stylesheets/quill.css --fatal-deprecation=1.80.7", @@ -101,12 +101,12 @@ "build:release": "cross-env SIGNAL_ENV=production pnpm run build:electron --config.directories.output=release", "build:mas-dev": "bash ./scripts/build-mas-dev.sh", "build:release-win32-all": "pnpm run build:release --arm64 --x64", - "build:preload-cache": "tsx ts/scripts/generate-preload-cache.node.ts", + "build:preload-cache": "node scripts/generate-preload-cache.mjs", "build:emoji": "run-p build:emoji:32 build:emoji:64", "build:emoji:32": "cwebp -progress -mt -preset icon -alpha_filter best -alpha_q 20 -pass 10 -q 75 ./node_modules/emoji-datasource-apple/img/apple/sheets/32.png -o ./images/emoji-sheet-32.webp", "build:emoji:64": "cwebp -progress -mt -preset icon -alpha_filter best -alpha_q 20 -pass 10 -q 75 ./node_modules/emoji-datasource-apple/img/apple/sheets/64.png -o ./images/emoji-sheet-64.webp", "electron:install-app-deps": "electron-builder install-app-deps", - "check-upgradeable-deps": "tsx ts/scripts/check-upgradeable-deps.node.ts", + "check-upgradeable-deps": "node scripts/check-upgradeable-deps.mjs", "react-devtools": "react-devtools", "run-with-devtools": "cross-env REACT_DEVTOOLS=1 run-p --print-label react-devtools start" }, @@ -126,14 +126,12 @@ "google-libphonenumber": "3.2.39" }, "devDependencies": { - "@babel/core": "7.26.0", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", - "@babel/plugin-proposal-optional-chaining": "7.21.0", - "@babel/plugin-transform-runtime": "7.25.9", - "@babel/plugin-transform-typescript": "7.25.9", - "@babel/preset-react": "7.25.9", - "@babel/preset-typescript": "7.26.0", + "@babel/core": "7.29.0", + "@babel/helper-plugin-utils": "7.28.6", + "@babel/plugin-transform-runtime": "7.29.0", + "@babel/plugin-transform-typescript": "7.28.6", + "@babel/preset-react": "7.28.5", + "@babel/preset-typescript": "7.28.5", "@electron/asar": "3.4.1", "@electron/fuses": "1.5.0", "@electron/notarize": "2.1.0", @@ -178,6 +176,8 @@ "@tailwindcss/cli": "4.2.2", "@tailwindcss/postcss": "4.2.2", "@tanstack/react-virtual": "3.11.2", + "@types/babel__core": "7.20.5", + "@types/babel__helper-plugin-utils": "7.10.3", "@types/blueimp-load-image": "5.16.6", "@types/chai": "4.3.16", "@types/chai-as-promised": "7.1.4", @@ -437,7 +437,7 @@ } ], "mergeASARs": true, - "sign": "./ts/scripts/sign-macos.node.ts", + "sign": "scripts/sign-macos.mjs", "releaseInfo": { "vendor": { "minOSVersion": "21.0.1" @@ -505,7 +505,7 @@ "certificateSubjectName": "Signal Messenger, LLC", "certificateSha1": "8D5E3CD800736C5E1FE459A1F5AA48287D4F6EC6", "publisherName": "Signal Messenger, LLC", - "sign": "./ts/scripts/sign-windows.node.ts", + "sign": "scripts/sign-windows.mjs", "signingHashAlgorithms": [ "sha256" ] @@ -603,10 +603,10 @@ "signalcaptcha" ] }, - "artifactBuildCompleted": "ts/scripts/artifact-build-completed.node.ts", - "afterSign": "ts/scripts/after-sign.node.ts", - "afterPack": "ts/scripts/after-pack.node.ts", - "afterAllArtifactBuild": "ts/scripts/after-all-artifact-build.node.ts", + "artifactBuildCompleted": "scripts/artifact-build-completed.mjs", + "afterSign": "scripts/after-sign.mjs", + "afterPack": "scripts/after-pack.mjs", + "afterAllArtifactBuild": "scripts/after-all-artifact-build.mjs", "asar": { "smartUnpack": false }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d65af6663f..e84e4ae980 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,29 +105,23 @@ importers: version: 3.2.39 devDependencies: '@babel/core': - specifier: 7.26.0 - version: 7.26.0 - '@babel/plugin-proposal-class-properties': - specifier: 7.18.6 - version: 7.18.6(@babel/core@7.26.0) - '@babel/plugin-proposal-nullish-coalescing-operator': - specifier: 7.18.6 - version: 7.18.6(@babel/core@7.26.0) - '@babel/plugin-proposal-optional-chaining': - specifier: 7.21.0 - version: 7.21.0(@babel/core@7.26.0) + specifier: 7.29.0 + version: 7.29.0 + '@babel/helper-plugin-utils': + specifier: 7.28.6 + version: 7.28.6 '@babel/plugin-transform-runtime': - specifier: 7.25.9 - version: 7.25.9(@babel/core@7.26.0) + specifier: 7.29.0 + version: 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-typescript': - specifier: 7.25.9 - version: 7.25.9(@babel/core@7.26.0) + specifier: 7.28.6 + version: 7.28.6(@babel/core@7.29.0) '@babel/preset-react': - specifier: 7.25.9 - version: 7.25.9(@babel/core@7.26.0) + specifier: 7.28.5 + version: 7.28.5(@babel/core@7.29.0) '@babel/preset-typescript': - specifier: 7.26.0 - version: 7.26.0(@babel/core@7.26.0) + specifier: 7.28.5 + version: 7.28.5(@babel/core@7.29.0) '@electron/asar': specifier: 3.4.1 version: 3.4.1 @@ -260,6 +254,12 @@ importers: '@tanstack/react-virtual': specifier: 3.11.2 version: 3.11.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@types/babel__core': + specifier: 7.20.5 + version: 7.20.5 + '@types/babel__helper-plugin-utils': + specifier: 7.10.3 + version: 7.10.3 '@types/blueimp-load-image': specifier: 5.16.6 version: 5.16.6 @@ -397,10 +397,10 @@ importers: version: 4.10.2 babel-core: specifier: 7.0.0-bridge.0 - version: 7.0.0-bridge.0(@babel/core@7.26.0) + version: 7.0.0-bridge.0(@babel/core@7.29.0) babel-loader: specifier: 9.2.1 - version: 9.2.1(@babel/core@7.26.0)(webpack@5.96.1) + version: 9.2.1(@babel/core@7.29.0)(webpack@5.96.1) babel-plugin-lodash: specifier: 3.3.4 version: 3.3.4 @@ -854,91 +854,111 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.8': - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/core@7.26.0': - resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} '@babel/generator@7.26.8': resolution: {integrity: sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.25.9': - resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.26.5': - resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.25.9': - resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-define-polyfill-provider@0.6.3': - resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - '@babel/helper-member-expression-to-functions@7.25.9': - resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.25.9': resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.25.9': - resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} - '@babel/helper-replace-supers@7.26.5': - resolution: {integrity: sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==} + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-skip-transparent-expression-wrappers@7.25.9': - resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.7': - resolution: {integrity: sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} '@babel/parser@7.26.8': @@ -946,26 +966,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.21.0': - resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} @@ -1010,6 +1014,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -1058,56 +1068,62 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.26.3': - resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-display-name@7.25.9': - resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-development@7.25.9': - resolution: {integrity: sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==} + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.25.9': - resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-pure-annotations@7.25.9': - resolution: {integrity: sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==} + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-runtime@7.25.9': - resolution: {integrity: sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==} + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.25.9': - resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==} + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-react@7.25.9': - resolution: {integrity: sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw==} + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.26.0': - resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1120,14 +1136,26 @@ packages: resolution: {integrity: sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.26.8': resolution: {integrity: sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.26.8': resolution: {integrity: sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} @@ -1832,6 +1860,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1859,6 +1890,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -4202,6 +4236,9 @@ packages: '@types/babel__generator@7.6.8': resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__helper-plugin-utils@7.10.3': + resolution: {integrity: sha512-FcLBBPXInqKfULB2nvOBskQPcnSMZ0s1Y2q76u9H1NPPWaLcTeq38xBeKfF/RBUECK333qeaqRdYoPSwW7rTNQ==} + '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} @@ -5017,18 +5054,18 @@ packages: babel-plugin-lodash@3.3.4: resolution: {integrity: sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==} - babel-plugin-polyfill-corejs2@0.4.12: - resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs3@0.10.6: - resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.3: - resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -5056,6 +5093,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.10.10: + resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==} + engines: {node: '>=6.0.0'} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -5139,6 +5181,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -5246,6 +5293,9 @@ packages: caniuse-lite@1.0.30001699: resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} + caniuse-lite@1.0.30001780: + resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} + capture-stack-trace@1.0.2: resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} engines: {node: '>=0.10.0'} @@ -5550,8 +5600,8 @@ packages: resolution: {integrity: sha512-rxjlVPoTzuKQXem9rdIHSc6xo8TcvqmVZoItxvhMaI1/9MOSNEaee86CpMgv+QVul2Q5v/DkXfOOVwDJxF7KsA==} engines: {node: '>=6'} - core-js-compat@3.40.0: - resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} @@ -6044,6 +6094,9 @@ packages: electron-publish@26.0.13: resolution: {integrity: sha512-O5hfHSwli5cegQ4JS3Dp0dZcheex6UCRE/qYyRQvhB6DhSwojiwTnAGEuQCJXc8K8Zxz2lku5Du3VwYHf8d5Lw==} + electron-to-chromium@1.5.321: + resolution: {integrity: sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==} + electron-to-chromium@1.5.99: resolution: {integrity: sha512-77c/+fCyL2U+aOyqfIFi89wYLBeSTCs55xCZL0oFH0KjqsvSvyh6AdQ+UIl1vgpnQQE6g+/KK8hOIupH6VwPtg==} @@ -8375,6 +8428,9 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + nop@1.0.0: resolution: {integrity: sha512-XdkOuXGx0DTwlqb0DWTcDqelgU/F3YyZ+PTRaecpDVpkYskcnh3OeUYKfvjcRQ2D1diTIGxi/a3eHVjW5yPupQ==} @@ -9401,6 +9457,11 @@ packages: engines: {node: '>= 0.4'} hasBin: true + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -10346,6 +10407,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + update-notifier@2.5.0: resolution: {integrity: sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==} engines: {node: '>=4'} @@ -10805,31 +10872,32 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.26.8': {} - - '@babel/core@7.26.0': + '@babel/code-frame@7.29.0': dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.26.8 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helpers': 7.26.7 - '@babel/parser': 7.26.8 - '@babel/template': 7.26.8 - '@babel/traverse': 7.26.8 - '@babel/types': 7.26.8 + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -10846,46 +10914,56 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 - '@babel/helper-annotate-as-pure@7.25.9': + '@babel/generator@7.29.1': dependencies: - '@babel/types': 7.26.8 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.26.5': + '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 browserslist: 4.24.4 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-member-expression-to-functions': 7.25.9 - '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/helper-replace-supers': 7.26.5(@babel/core@7.26.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/traverse': 7.26.8 + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.26.5 - '@babel/helper-plugin-utils': 7.26.5 - debug: 4.3.7(supports-color@8.1.1) + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 lodash.debounce: 4.0.8 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color - '@babel/helper-member-expression-to-functions@7.25.9': + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.26.8 - '@babel/types': 7.26.8 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -10896,240 +10974,242 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.26.8 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.25.9': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/types': 7.26.8 - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-replace-supers@7.26.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-member-expression-to-functions': 7.25.9 - '@babel/helper-optimise-call-expression': 7.25.9 - '@babel/traverse': 7.26.8 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/traverse': 7.26.8 - '@babel/types': 7.26.8 + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} - '@babel/helper-validator-option@7.25.9': {} + '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helpers@7.26.7': + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': dependencies: - '@babel/template': 7.26.8 - '@babel/types': 7.26.8 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/parser@7.26.8': dependencies: '@babel/types': 7.26.8 - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0)': + '@babel/parser@7.29.2': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.26.0)': + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.26.0)': + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-development@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/types': 7.26.8 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-react-pure-annotations@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-runtime@7.25.9(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-plugin-utils': 7.26.5 - babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) - babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) - babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.25.9 - '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/preset-react@7.25.9(@babel/core@7.26.0)': + '@babel/preset-react@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx-development': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-react-pure-annotations': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-option': 7.25.9 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) - '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -11143,6 +11223,12 @@ snapshots: '@babel/parser': 7.26.8 '@babel/types': 7.26.8 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@babel/traverse@7.26.8': dependencies: '@babel/code-frame': 7.26.2 @@ -11155,11 +11241,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + '@babel/types@7.26.8': dependencies: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@0.2.3': {} '@cspotcode/source-map-support@0.8.1': @@ -11903,7 +12006,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 babel-plugin-istanbul: 6.1.1 @@ -11930,10 +12033,15 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/remapping@2.3.5': @@ -11959,6 +12067,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -14519,7 +14632,7 @@ snapshots: '@storybook/test-runner@0.22.0(@swc/helpers@0.5.15)(@types/node@24.12.0)(debug@4.3.7)(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.7.4)(utf-8-validate@5.0.10))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@24.12.0)(typescript@5.9.3))': dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@babel/generator': 7.26.8 '@babel/template': 7.26.8 '@babel/types': 7.26.8 @@ -14807,6 +14920,10 @@ snapshots: dependencies: '@babel/types': 7.26.8 + '@types/babel__helper-plugin-utils@7.10.3': + dependencies: + '@types/babel__core': 7.20.5 + '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.26.8 @@ -15703,33 +15820,33 @@ snapshots: transitivePeerDependencies: - debug - babel-core@7.0.0-bridge.0(@babel/core@7.26.0): + babel-core@7.0.0-bridge.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 - babel-jest@29.7.0(@babel/core@7.26.0): + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.26.0) + babel-preset-jest: 29.6.3(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.96.1): + babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.96.1): dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 find-cache-dir: 4.0.0 schema-utils: 4.3.0 webpack: 5.96.1(@swc/core@1.10.16(@swc/helpers@0.5.15))(esbuild@0.24.0)(webpack-cli@5.1.4) babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.26.5 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -15754,54 +15871,54 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): dependencies: - '@babel/compat-data': 7.26.8 - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) - core-js-compat: 3.40.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 - '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) transitivePeerDependencies: - supports-color - babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + babel-preset-current-node-syntax@1.1.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) - '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - babel-preset-jest@29.6.3(@babel/core@7.26.0): + babel-preset-jest@29.6.3(@babel/core@7.29.0): dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0) balanced-match@1.0.2: {} @@ -15811,6 +15928,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.10.10: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -15912,6 +16031,14 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.10 + caniuse-lite: 1.0.30001780 + electron-to-chromium: 1.5.321 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -16077,6 +16204,8 @@ snapshots: caniuse-lite@1.0.30001699: {} + caniuse-lite@1.0.30001780: {} + capture-stack-trace@1.0.2: {} card-validator@10.0.3: @@ -16358,9 +16487,9 @@ snapshots: copy-text-to-clipboard@2.1.0: {} - core-js-compat@3.40.0: + core-js-compat@3.49.0: dependencies: - browserslist: 4.24.4 + browserslist: 4.28.1 core-js@3.40.0: {} @@ -16918,6 +17047,8 @@ snapshots: transitivePeerDependencies: - supports-color + electron-to-chromium@1.5.321: {} + electron-to-chromium@1.5.99: {} electron-window@0.8.1: @@ -18507,7 +18638,7 @@ snapshots: istanbul-lib-instrument@4.0.3: dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -18516,7 +18647,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@babel/parser': 7.26.8 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -18526,7 +18657,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@babel/parser': 7.26.8 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -18628,10 +18759,10 @@ snapshots: jest-config@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@24.12.0)(typescript@5.9.3)): dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) + babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -18856,15 +18987,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@babel/generator': 7.26.8 - '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.29.0) '@babel/types': 7.26.8 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.29.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -19663,6 +19794,8 @@ snapshots: node-releases@2.0.19: {} + node-releases@2.0.36: {} + nop@1.0.0: {} nopt@6.0.0: @@ -20501,7 +20634,7 @@ snapshots: react-docgen@7.1.1: dependencies: - '@babel/core': 7.26.0 + '@babel/core': 7.29.0 '@babel/traverse': 7.26.8 '@babel/types': 7.26.8 '@types/babel__core': 7.20.5 @@ -20823,6 +20956,12 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 @@ -21889,6 +22028,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + update-notifier@2.5.0: dependencies: boxen: 1.3.0 diff --git a/preload.wrapper.ts b/preload.wrapper.ts index 3631d6cfa6..d05df1d67a 100644 --- a/preload.wrapper.ts +++ b/preload.wrapper.ts @@ -61,7 +61,7 @@ const fn = script.runInThisContext({ importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, }); -// See `ts/scripts/generate-preload-cache.node.ts` +// See `scripts/generate-preload-cache.mjs` if (process.env.GENERATE_PRELOAD_CACHE) { // Use hottest cache possible in CI if (process.env.CI) { diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js deleted file mode 100644 index 5e8c6b7d77..0000000000 --- a/scripts/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -module.exports = { - rules: { - 'no-console': 'off', - - // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - }, - ], - }, -}; diff --git a/scripts/after-all-artifact-build.mjs b/scripts/after-all-artifact-build.mjs new file mode 100644 index 0000000000..561e9f9751 --- /dev/null +++ b/scripts/after-all-artifact-build.mjs @@ -0,0 +1,15 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { afterAllArtifactBuild as notarizeUniversalDMG } from './notarize-universal-dmg.mjs'; + +/** @import { BuildResult } from 'electron-builder' */ + +/** + * @param {BuildResult} result + * @returns {Promise>} + */ +export async function afterAllArtifactBuild(result) { + await notarizeUniversalDMG(result); + return []; +} diff --git a/scripts/after-pack.mjs b/scripts/after-pack.mjs new file mode 100644 index 0000000000..a787833f45 --- /dev/null +++ b/scripts/after-pack.mjs @@ -0,0 +1,20 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { afterPack as fuseElectron } from './fuse-electron.mjs'; +import { afterPack as copyPacks } from './copy-language-packs.mjs'; +import { afterPack as pruneMacOSRelease } from './prune-macos-release.mjs'; +import { afterPack as ensureLinuxFilePermissions } from './ensure-linux-file-permissions.mjs'; + +/** @import { AfterPackContext } from 'electron-builder' */ + +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterPack(context) { + await pruneMacOSRelease(context); + await fuseElectron(context); + await copyPacks(context); + await ensureLinuxFilePermissions(context); +} diff --git a/scripts/after-sign.mjs b/scripts/after-sign.mjs new file mode 100644 index 0000000000..5d9fd24c7e --- /dev/null +++ b/scripts/after-sign.mjs @@ -0,0 +1,17 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { afterSign as notarize } from './notarize.mjs'; + +/** @import { AfterPackContext } from 'electron-builder' */ + +/** + * NOTE: It is AfterPackContext here even though it is afterSign. + * See: https://www.electron.build/configuration/configuration.html#aftersign + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterSign(context) { + // This must be the last step + await notarize(context); +} diff --git a/ts/scripts/artifact-build-completed.node.ts b/scripts/artifact-build-completed.mjs similarity index 90% rename from ts/scripts/artifact-build-completed.node.ts rename to scripts/artifact-build-completed.mjs index 4f3ee9f771..6f643eec96 100644 --- a/ts/scripts/artifact-build-completed.node.ts +++ b/scripts/artifact-build-completed.mjs @@ -1,22 +1,28 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { tmpdir } from 'node:os'; - import { mkdtemp, rm, rename, stat, writeFile } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; import { pipeline } from 'node:stream/promises'; import { createHash } from 'node:crypto'; import path from 'node:path'; -import type { ArtifactCreated } from 'electron-builder'; import { BlockMap } from 'better-blockmap'; +/// + +/** @import { ArtifactCreated } from 'electron-builder' */ + +/** + * @param {ArtifactCreated} context + * @returns {Promise} + */ export async function artifactBuildCompleted({ target, file, packager, updateInfo, -}: ArtifactCreated): Promise { +}) { if (packager.platform.name === 'linux' && file.endsWith('.AppImage')) { const blockMapPath = `${file}.blockmap`; console.log(`Generating blockmap ${blockMapPath}`); diff --git a/ts/scripts/build-localized-display-names.node.ts b/scripts/build-localized-display-names.mjs similarity index 55% rename from ts/scripts/build-localized-display-names.node.ts rename to scripts/build-localized-display-names.mjs index 8274fe7e91..5fde11afd8 100644 --- a/ts/scripts/build-localized-display-names.node.ts +++ b/scripts/build-localized-display-names.mjs @@ -1,12 +1,11 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { parse } from 'csv-parse'; import fs from 'node:fs/promises'; -import { join } from 'node:path'; import { z } from 'zod'; -import { _getAvailableLocales } from '../../app/locale.node.ts'; -import { parseUnknown } from '../util/schemas.std.ts'; +import availableLocales from '../build/available-locales.json' with { type: 'json' }; +import { assert } from './utils/assert.mjs'; const type = process.argv[2]; if (type !== 'countries' && type !== 'locales') { @@ -22,9 +21,6 @@ if (!process.argv[4]) { throw new Error('Missing third argument: output json file'); } const localeDisplayNamesBuildPath = process.argv[4]; -const rootDir = join(__dirname, '..', '..'); - -const availableLocales = _getAvailableLocales(rootDir); const LocaleString = z.string().refine(arg => { try { @@ -42,12 +38,16 @@ const LocaleDisplayNames = z ]) .rest(z.tuple([LocaleString]).rest(z.string())); -type Row = ReadonlyArray; -type Records = ReadonlyArray; +/** @typedef {ReadonlyArray} Row */ +/** @typedef {ReadonlyArray} Records */ -function parseCsv(input: string) { - return new Promise((resolve, reject) => { - parse(input, { trim: true }, (error, records: Records) => { +/** + * @param {string} input + * @returns {Promise} + */ +function parseCsv(input) { + return new Promise((resolve, reject) => { + parse(input, { trim: true }, (error, records) => { if (error) { reject(error); } else { @@ -57,13 +57,17 @@ function parseCsv(input: string) { }); } -type LocaleDisplayNamesResult = Record>; +/** @typedef {Record>} LocaleDisplayNamesResult */ -function convertData( - input: z.infer -): LocaleDisplayNamesResult { +/** + * @param {z.infer} input + * @returns {LocaleDisplayNamesResult} + */ +function convertData(input) { const [[, ...keys], ...rows] = input; - const result: LocaleDisplayNamesResult = {}; + + /** @type {LocaleDisplayNamesResult} */ + const result = {}; if (type === 'locales') { for (const row of rows) { @@ -71,8 +75,9 @@ function convertData( result[subKey] = {}; for (const [index, message] of messages.entries()) { - // oxlint-disable-next-line typescript/no-non-null-assertion - result[subKey][keys[index]!] = message; + const key = keys[index]; + assert(key != null, 'Missing key'); + result[subKey][key] = message; } } } else { @@ -85,15 +90,21 @@ function convertData( const [subKey, ...messages] = row; for (const [index, message] of messages.entries()) { - // oxlint-disable-next-line typescript/no-non-null-assertion - result[keys[index]!]![subKey] = message; + const key = keys[index]; + assert(key != null, 'Missing key'); + const values = result[key]; + assert(values != null, 'Missing values'); + values[subKey] = message; } } } return result; } -function assertValuesForAllLocales(result: LocaleDisplayNamesResult) { +/** + * @param {LocaleDisplayNamesResult} result + */ +function assertValuesForAllLocales(result) { for (const locale of availableLocales) { const values = result[locale]; if (values == null) { @@ -107,9 +118,12 @@ function assertValuesForAllLocales(result: LocaleDisplayNamesResult) { } } -function assertValuesForAllCountries(result: LocaleDisplayNamesResult) { - // oxlint-disable-next-line typescript/no-non-null-assertion - const availableCountries = Object.keys(result.en!); +/** + * @param {LocaleDisplayNamesResult} result + */ +function assertValuesForAllCountries(result) { + assert(result.en != null, 'Missing result.en'); + const availableCountries = Object.keys(result.en); for (const locale of availableLocales) { const values = result[locale]; if (values == null) { @@ -123,22 +137,14 @@ function assertValuesForAllCountries(result: LocaleDisplayNamesResult) { } } -async function main() { - const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8'); - const records = await parseCsv(contents); - const data = parseUnknown(LocaleDisplayNames, records as unknown); - const result = convertData(data); - if (type === 'locales') { - assertValuesForAllLocales(result); - } else if (type === 'countries') { - assertValuesForAllCountries(result); - } - const json = JSON.stringify(result, null, 2); - await fs.writeFile(localeDisplayNamesBuildPath, json, 'utf-8'); +const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8'); +const records = await parseCsv(contents); +const data = LocaleDisplayNames.parse(records); +const result = convertData(data); +if (type === 'locales') { + assertValuesForAllLocales(result); +} else if (type === 'countries') { + assertValuesForAllCountries(result); } - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); +const json = JSON.stringify(result, null, 2); +await fs.writeFile(localeDisplayNamesBuildPath, json, 'utf-8'); diff --git a/ts/scripts/check-min-os-version.node.ts b/scripts/check-min-os-version.mjs similarity index 79% rename from ts/scripts/check-min-os-version.node.ts rename to scripts/check-min-os-version.mjs index 2890f4ae24..a75c771a3b 100644 --- a/ts/scripts/check-min-os-version.node.ts +++ b/scripts/check-min-os-version.mjs @@ -1,6 +1,6 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { execFile as execFileCb } from 'node:child_process'; import { promisify } from 'node:util'; import { existsSync } from 'node:fs'; @@ -9,12 +9,13 @@ import { join, basename } from 'node:path'; import fastGlob from 'fast-glob'; import { gte } from 'semver'; import { Format, NtExecutable } from 'pe-library'; +import packageJson from '../package.json' with { type: 'json' }; // Note: because we don't run under electron - this is a path to binary -import ELECTRON_BINARY from 'electron'; - -import { drop } from '../util/drop.std.ts'; -import { packageJson } from '../util/packageJson.node.ts'; +import electronImport from 'electron'; +const ELECTRON_BINARY = /** @type {string} */ ( + /** @type {unknown} */ (electronImport) +); const { ImageDosHeader, ImageNtHeaders, ImageDirectoryEntry } = Format; @@ -61,18 +62,23 @@ const MACOS_TO_DARWIN_VERSIONS = new Map([ ['26.0', '25.0.0'], ]); -async function macosVersionCheck(file: string) { +/** + * @param {string} file + */ +async function macosVersionCheck(file) { console.log(`${file}: checking...`); const { stdout } = await execFile('otool', ['-l', file]); - type MacosMatch = RegExpMatchArray & { 1: string }; const match = stdout.match(/minos\s+([\d.]+)/); if (match == null) { throw new Error(`Failed to detect min OS version of ${file}`); } - const [, macosVersion] = match as MacosMatch; + const [, macosVersion] = match; + if (macosVersion == null) { + throw new Error('Missing macosVersion'); + } const darwinVersion = MACOS_TO_DARWIN_VERSIONS.get(macosVersion); if (darwinVersion == null) { throw new Error(`No matching darwin version for macOS ${macosVersion}`); @@ -172,7 +178,11 @@ const ALLOWED_DLLS = new Set([ 'wtsapi32.dll', ]); -async function windowsDllImportCheck(file: string): Promise { +/** + * @param {string} file + * @returns {Promise} + */ +async function windowsDllImportCheck(file) { console.log(`${file}: checking...`); const fileData = await readFile(file); @@ -183,7 +193,12 @@ async function windowsDllImportCheck(file: string): Promise { ignoreCert: true, }); - function cstr(data: Buffer, offset: number): string { + /** + * @param {Buffer} data + * @param {number} offset + * @returns {string} + */ + function cstr(data, offset) { for (let end = offset; end < data.length; end += 1) { if (data[end] === 0) { return data.subarray(offset, end).toString(); @@ -192,7 +207,8 @@ async function windowsDllImportCheck(file: string): Promise { throw new Error('Invalid cstring'); } - const imports = new Set(); + /** @type {Set} */ + const imports = new Set(); for (const [entryType, { empty, nameOffset }] of DLL_TABLES) { const section = ntExecutable.getSectionByEntry(entryType); const imageDirectoryEntry = @@ -248,7 +264,10 @@ async function windowsDllImportCheck(file: string): Promise { } } -function padGlibcVersion(version: string) { +/** + * @param {string} version + */ +function padGlibcVersion(version) { if (/^\d+\.\d+$/.test(version)) { return `${version}.0`; } @@ -258,7 +277,10 @@ function padGlibcVersion(version: string) { throw new Error(`Unsupported glibc version: ${version}`); } -async function linuxVersionCheck(file: string) { +/** + * @param {string} file + */ +async function linuxVersionCheck(file) { if (!existsSync(file)) { console.log(`${file}: skipping`); return; @@ -270,10 +292,13 @@ async function linuxVersionCheck(file: string) { maxBuffer: 100 * 1024 * 1024, }); - let minGlibcVersion: string | undefined; - type GlibcMatch = RegExpExecArray & { 1: string }; + /** @type {string | undefined} */ + let minGlibcVersion; for (const match of stdout.matchAll(/GLIBC_([\d.]+)/g)) { - const [, unpaddedVersion] = match as GlibcMatch; + const [, unpaddedVersion] = match; + if (unpaddedVersion == null) { + throw new Error('Missing unpaddedVersion'); + } const glibcVersion = padGlibcVersion(unpaddedVersion); if (minGlibcVersion == null || gte(glibcVersion, minGlibcVersion)) { minGlibcVersion = glibcVersion; @@ -291,7 +316,6 @@ async function linuxVersionCheck(file: string) { throw new Error('Missing libc6 dependency in package.json'); } - type Libc6Match = RegExpMatchArray & { 1: string }; const match = libc6Dependency.match(/^libc6 \(>= ([\d.]+)\)$/); if (match == null) { throw new Error( @@ -299,7 +323,10 @@ async function linuxVersionCheck(file: string) { ); } - const [, unpaddedVersion] = match as Libc6Match; + const [, unpaddedVersion] = match; + if (unpaddedVersion == null) { + throw new Error('Missing unpaddedVersion'); + } const minSupported = padGlibcVersion(unpaddedVersion); if (gte(minSupported, minGlibcVersion)) { console.log(`${file}: required version ${minGlibcVersion}`); @@ -312,34 +339,31 @@ async function linuxVersionCheck(file: string) { ); } -async function main() { - const BINARY_FILES = [ - ELECTRON_BINARY as unknown as string, - ...(await fastGlob( - packageJson.build.files - .filter((p: unknown): p is string => typeof p === 'string') - .filter(p => p.endsWith('.node')) - .map(p => p.replace(/\${platform}/, process.platform)) - .map(p => p.replace(/\${arch}/, process.arch)), - { - absolute: true, - onlyFiles: true, - cwd: join(__dirname, '..', '..'), - } - )), - ]; - for (const file of BINARY_FILES) { - if (process.platform === 'darwin') { - // oxlint-disable-next-line no-await-in-loop - await macosVersionCheck(file); - } else if (process.platform === 'win32') { - // oxlint-disable-next-line no-await-in-loop - await windowsDllImportCheck(file); - } else if (process.platform === 'linux') { - // oxlint-disable-next-line no-await-in-loop - await linuxVersionCheck(file); +/** @type {string[]} */ +const BINARY_FILES = [ + ELECTRON_BINARY, + ...(await fastGlob( + packageJson.build.files + .filter(p => typeof p === 'string') + .filter(p => p.endsWith('.node')) + .map(p => p.replace(/\${platform}/, process.platform)) + .map(p => p.replace(/\${arch}/, process.arch)), + { + absolute: true, + onlyFiles: true, + cwd: join(import.meta.dirname, '..'), } + )), +]; +for (const file of BINARY_FILES) { + if (process.platform === 'darwin') { + // oxlint-disable-next-line no-await-in-loop + await macosVersionCheck(file); + } else if (process.platform === 'win32') { + // oxlint-disable-next-line no-await-in-loop + await windowsDllImportCheck(file); + } else if (process.platform === 'linux') { + // oxlint-disable-next-line no-await-in-loop + await linuxVersionCheck(file); } } - -drop(main()); diff --git a/scripts/check-upgradeable-deps.mjs b/scripts/check-upgradeable-deps.mjs new file mode 100644 index 0000000000..a43322872c --- /dev/null +++ b/scripts/check-upgradeable-deps.mjs @@ -0,0 +1,343 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { join } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import chalk from 'chalk'; +import semver from 'semver'; +import { got, HTTPError } from 'got'; +import enquirer from 'enquirer'; +import execa from 'execa'; +import { assert } from './utils/assert.mjs'; + +const rootDir = join(import.meta.dirname, '..'); + +/** + * @param {string} path + * @returns {Promise} + */ +async function readJsonFile(path) { + return JSON.parse(await readFile(path, 'utf-8')); +} + +/** + * @param {string | number | null | undefined} value + * @returns {number | null} + */ +function parseNumberField(value) { + if (value == null) { + return null; + } + if (typeof value === 'number') { + return value; + } + const trimmed = value.trim(); + if (trimmed === '') { + return null; + } + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return null; + } + return parsed; +} + +const npm = got.extend({ + prefixUrl: 'https://registry.npmjs.org/', + responseType: 'json', + retry: { + calculateDelay: retry => { + if ( + retry.error instanceof HTTPError && + retry.error.response.statusCode === 429 + ) { + const retryAfter = parseNumberField( + retry.error.response.headers['retry-after'] + ); + if (retryAfter != null) { + console.log( + chalk.gray(`Rate limited, retrying after ${retryAfter} seconds`) + ); + return retryAfter * 1000; + } + } + + return retry.computedValue; + }, + }, +}); + +const DependencyTypes = /** @type {const} */ ([ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', +]); + +/** + * @typedef {object} LocalDependency + * @prop {string} name + * @prop {(typeof DependencyTypes)[number]} depType + * @prop {string} requestedVersion + * @prop {string} resolvedVersion + */ + +/** + * @typedef {object} FetchedDependencyProps + * @prop {string} latestVersion + * @prop {'commonjs' | 'esm'} moduleType + * @prop {semver.ReleaseType | null} diff + * + * @typedef {LocalDependency & FetchedDependencyProps} FetchedDependency + */ + +const packageJson = await readJsonFile(join(rootDir, 'package.json')); +const packageLock = await readJsonFile(join(rootDir, 'package-lock.json')); + +/** @type {ReadonlyArray} */ +const localDeps = DependencyTypes.flatMap(depType => { + return Object.keys(packageJson[depType] ?? {}).map(name => { + const requestedVersion = packageJson[depType][name]; + const resolvedVersion = + packageLock.packages[`node_modules/${name}`]?.version; + assert(resolvedVersion, `Could not find resolved version for ${name}`); + + return { name, depType, requestedVersion, resolvedVersion }; + }); +}); + +console.log(chalk`Found {cyan ${localDeps.length}} local dependencies`); + +/** @type {ReadonlyArray} */ +const fetchedDeps = await Promise.all( + localDeps.map(async dep => { + /** @type {any} */ + const info = await npm(`${dep.name}/latest`).json(); + const latestVersion = info.version; + const moduleType = info.type ?? 'commonjs'; + assert( + moduleType === 'commonjs' || moduleType === 'module', + `Unexpected module type for ${dep.name}: ${moduleType}` + ); + const diff = semver.lt(dep.resolvedVersion, latestVersion) + ? semver.diff(dep.resolvedVersion, latestVersion) + : null; + return { ...dep, latestVersion, moduleType, diff }; + }) +); + +const outdatedDeps = fetchedDeps.filter(dep => dep.diff != null); +console.log(chalk`Found {cyan ${outdatedDeps.length}} outdated dependencies`); + +const upgradeableDeps = outdatedDeps.filter(dep => { + return dep.moduleType === 'commonjs'; +}); + +console.log( + chalk`Found {cyan ${upgradeableDeps.length}} upgradeable dependencies` +); + +/** @type {Map>} */ +const upgradeableDepsByDiff = new Map(); + +for (const dep of upgradeableDeps) { + assert(dep.diff != null, 'Expected diff to be non-null'); + + let group = upgradeableDepsByDiff.get(dep.diff); + if (group == null) { + group = new Set(); + upgradeableDepsByDiff.set(dep.diff, group); + } + + group.add(dep.name); +} + +for (const [diff, deps] of upgradeableDepsByDiff) { + console.log(chalk` - ${diff}: {cyan ${deps.size}}`); +} + +let longestNameLength = 0; +for (const dep of upgradeableDeps) { + longestNameLength = Math.max(longestNameLength, dep.name.length); +} + +/** @type {{ approvedDeps: ReadonlyArray; }} */ +const { approvedDeps } = await enquirer.prompt({ + type: 'multiselect', + name: 'approvedDeps', + message: 'Select which dependencies to upgrade', + choices: upgradeableDeps.map(deps => { + let color = chalk.red; + if (deps.diff === 'patch') { + color = chalk.green; + } else if (deps.diff === 'minor') { + color = chalk.yellow; + } + + return { + name: deps.name, + message: `${deps.name.padEnd(longestNameLength)}`, + hint: `(${color(deps.diff)}: ${deps.resolvedVersion} -> ${color(deps.latestVersion)})`, + }; + }), +}); + +console.log( + chalk`Starting upgrade of {cyan ${approvedDeps.length}} dependencies` +); + +for (const dep of upgradeableDeps) { + try { + if (!approvedDeps.includes(dep.name)) { + console.log(chalk`Skipping ${dep.name}`); + continue; + } + + // oxlint-disable-next-line no-await-in-loop + const gitStatusBefore = await execa('git', ['status', '--porcelain']); + if (gitStatusBefore.stdout.trim() !== '') { + console.error(chalk`{red Found uncommitted changes, exiting}`); + console.error(chalk.red(gitStatusBefore.stdout)); + process.exit(1); + } + + console.log( + chalk`Upgrading {cyan ${dep.name}} from {yellow ${dep.resolvedVersion}} to {magenta ${dep.latestVersion}}` + ); + // oxlint-disable-next-line no-await-in-loop + await execa( + 'npm', + ['install', '--save-exact', `${dep.name}@${dep.latestVersion}`], + { stdio: 'inherit' } + ); + + // oxlint-disable-next-line no-constant-condition + while (true) { + try { + // oxlint-disable-next-line no-await-in-loop + await execa( + 'npx', + ['patch-package', '--error-on-fail', '--error-on-warn'], + { stdio: 'inherit' } + ); + break; + } catch { + /** @type {{ retry: boolean }} */ + // oxlint-disable-next-line no-await-in-loop + const { retry } = await enquirer.prompt({ + type: 'confirm', + name: 'retry', + message: 'Retry patch-package?', + initial: true, + }); + + if (!retry) { + throw new Error('Failed to apply patch-package'); + } + } + } + + /** @type {{ npmScriptsToRun: Array }} */ + // oxlint-disable-next-line no-await-in-loop + const { npmScriptsToRun } = await enquirer.prompt({ + type: 'multiselect', + name: 'npmScriptsToRun', + message: 'Select which scripts to run', + choices: [ + // Fast and common + { name: 'oxlint' }, + { name: 'test-node' }, + { name: 'test-electron' }, + // Long + { name: 'test-mock' }, + // Uncommon + { name: 'test-oxlint' }, + { name: 'test-lint-intl' }, + ], + }); + + const allNpmScriptToRun = [ + // Mandatory + 'generate', + 'check:types', + 'lint-deps', + // Optional + ...npmScriptsToRun, + ]; + + for (const script of allNpmScriptToRun) { + console.log(chalk`Running {cyan npm run ${script}}`); + + // oxlint-disable-next-line no-constant-condition + while (true) { + try { + // oxlint-disable-next-line no-await-in-loop + await execa('npm', ['run', script], { stdio: 'inherit' }); + break; + } catch (error) { + console.log( + chalk.red( + `Failed to run ${script}, you could go make changes and try again` + ) + ); + + /** @type {{ retry: boolean }} */ + // oxlint-disable-next-line no-await-in-loop + const { retry } = await enquirer.prompt({ + type: 'confirm', + name: 'retry', + message: 'Retry running script?', + initial: true, + }); + + if (!retry) { + throw error; + } else { + console.log(chalk`Retrying {cyan npm run ${script}}`); + continue; + } + } + } + } + + console.log('Changes after upgrade:'); + // oxlint-disable-next-line no-await-in-loop + await execa('git', ['status', '--porcelain'], { stdio: 'inherit' }); + + /** @type {{ commitChanges: boolean }} */ + // oxlint-disable-next-line no-await-in-loop + const { commitChanges } = await enquirer.prompt({ + type: 'select', + name: 'commitChanges', + message: 'Commit these changes?', + choices: [ + { name: 'commit', message: 'Commit and continue', value: true }, + { name: 'revert', message: 'Revert and skip', value: false }, + ], + }); + + if (!commitChanges) { + console.log('Reverting changes, and skipping'); + // oxlint-disable-next-line no-await-in-loop + await execa('git', ['checkout', '.']); + continue; + } + + console.log('Committing changes'); + // oxlint-disable-next-line no-await-in-loop + await execa('git', ['add', '.']); + // oxlint-disable-next-line no-await-in-loop + await execa('git', [ + 'commit', + '-m', + `Upgrade ${dep.name} ${dep.depType} from ${dep.requestedVersion} to ${dep.latestVersion}`, + ]); + } catch (error) { + console.error(chalk.red(error)); + console.log( + chalk.red(`Failed to upgrade ${dep.name}, reverting and skipping`) + ); + // oxlint-disable-next-line no-await-in-loop + await execa('git', ['checkout', '.']); + } +} diff --git a/scripts/clean-transpile.js b/scripts/clean-transpile.js deleted file mode 100644 index cff9762fc9..0000000000 --- a/scripts/clean-transpile.js +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -const fastGlob = require('fast-glob'); -const { rm } = require('node:fs/promises'); -const { join } = require('node:path'); - -const repoRoot = join(__dirname, '..'); - -const PATTERNS = [ - 'sticker-creator/dist', - 'build/**/*.js', - 'app/**/*.js', - 'app/*.js', - 'ts/**/*.js', - 'bundles', - 'tsconfig.tsbuildinfo', - 'preload.bundle.js', - 'preload.bundle.cache', -]; - -const EXCEPTIONS = new Set(['ts/windows/main/tsx.preload.js']); - -async function main() { - const readable = fastGlob.stream(PATTERNS, { - cwd: repoRoot, - }); - - const promises = []; - let count = 0; - for await (const entry of readable) { - if (EXCEPTIONS.has(entry)) { - continue; - } - count += 1; - promises.push(rm(entry, { recursive: true, force: true })); - } - await Promise.all(promises); - - console.log(`Deleted ${count} files`); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/scripts/clean-transpile.mjs b/scripts/clean-transpile.mjs new file mode 100644 index 0000000000..49a30f3584 --- /dev/null +++ b/scripts/clean-transpile.mjs @@ -0,0 +1,42 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fastGlob from 'fast-glob'; +import { rm } from 'node:fs/promises'; +import { join } from 'node:path'; + +const repoRoot = join(import.meta.dirname, '..'); + +const PATTERNS = [ + 'sticker-creator/dist', + 'build/**/*.js', + 'app/**/*.js', + 'app/*.js', + 'ts/**/*.js', + 'bundles', + 'tsconfig.tsbuildinfo', + 'preload.bundle.js', + 'preload.bundle.cache', +]; + +const EXCEPTIONS = new Set(['ts/windows/main/tsx.preload.js']); + +const readable = fastGlob.stream(PATTERNS, { + cwd: repoRoot, +}); + +const promises = []; +let count = 0; +for await (const entry of readable) { + if (typeof entry !== 'string') { + throw new Error('Expected readable entry to be string'); + } + if (EXCEPTIONS.has(entry)) { + continue; + } + count += 1; + promises.push(rm(entry, { recursive: true, force: true })); +} +await Promise.all(promises); + +console.log(`Deleted ${count} files`); diff --git a/scripts/compile-stories-icu-lookup.mjs b/scripts/compile-stories-icu-lookup.mjs new file mode 100644 index 0000000000..e0c5abbe13 --- /dev/null +++ b/scripts/compile-stories-icu-lookup.mjs @@ -0,0 +1,63 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { readFile, writeFile, readdir, readlink } from 'node:fs/promises'; +import { join, basename } from 'node:path'; +import pMap from 'p-map'; + +const source = process.argv[2]; +if (!source) { + throw new Error('Missing required source directory argument'); +} + +const dirEntries = await readdir(join(source, 'components'), { + withFileTypes: true, + recursive: true, +}); + +const enMessages = JSON.parse( + await readFile( + join(import.meta.dirname, '..', '_locales', 'en', 'messages.json'), + 'utf8' + ) +); + +/** @type {Record>} */ +const icuToStory = Object.create(null); + +await pMap( + dirEntries, + async entry => { + if (!entry.isSymbolicLink()) { + return; + } + + const fullPath = join(entry.parentPath, entry.name); + const image = basename(await readlink(fullPath)); + + const storyId = basename(entry.parentPath); + const linkFile = entry.name; + + const icuId = `icu:${basename(linkFile, '.jpg')}`; + + let list = icuToStory[icuId]; + if (list == null) { + list = []; + icuToStory[icuId] = list; + } + list.push([storyId, image]); + }, + { concurrency: 20 } +); + +const index = Object.entries(icuToStory).map(([key, icuIds]) => { + return [key, enMessages[key]?.messageformat, icuIds]; +}); +const html = await readFile( + join(import.meta.dirname, '..', '.storybook', 'icu-lookup.html'), + 'utf8' +); +await writeFile( + join(source, 'index.html'), + html.replace('%INDEX%', JSON.stringify(index)) +); diff --git a/ts/scripts/copy-language-packs.node.ts b/scripts/copy-language-packs.mjs similarity index 78% rename from ts/scripts/copy-language-packs.node.ts rename to scripts/copy-language-packs.mjs index 32d1b7c48b..1596e88e13 100644 --- a/ts/scripts/copy-language-packs.node.ts +++ b/scripts/copy-language-packs.mjs @@ -1,21 +1,24 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import fse from 'fs-extra'; import path from 'node:path'; -import type { AfterPackContext } from 'electron-builder'; -export async function afterPack({ - appOutDir, - packager, - electronPlatformName, -}: AfterPackContext): Promise { - let defaultLocale: string; +/** @import { AfterPackContext } from 'electron-builder' */ + +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterPack({ appOutDir, packager, electronPlatformName }) { + /** @type {string} */ + let defaultLocale; let ourLocales = await fse.readdir( - path.join(__dirname, '..', '..', '_locales') + path.join(import.meta.dirname, '..', '_locales') ); - let localesPath: string; + /** @type {string} */ + let localesPath; if (electronPlatformName === 'darwin' || electronPlatformName === 'mas') { const { productFilename } = packager.appInfo; @@ -55,7 +58,9 @@ export async function afterPack({ } const electronLocales = new Set(await fse.readdir(localesPath)); - const promises = new Array>(); + + /** @type {Promise[]} */ + const promises = []; for (const locale of ourLocales) { if (electronLocales.has(locale)) { continue; diff --git a/ts/scripts/ensure-linux-file-permissions.node.ts b/scripts/ensure-linux-file-permissions.mjs similarity index 73% rename from ts/scripts/ensure-linux-file-permissions.node.ts rename to scripts/ensure-linux-file-permissions.mjs index 20ff2a1242..cf85840259 100644 --- a/ts/scripts/ensure-linux-file-permissions.node.ts +++ b/scripts/ensure-linux-file-permissions.mjs @@ -1,19 +1,21 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import path from 'node:path'; import { execSync } from 'node:child_process'; -import type { AfterPackContext } from 'electron-builder'; + +/** @import { AfterPackContext } from 'electron-builder' */ const FILES = [ 'resources/org.signalapp.enable-backups.policy', 'resources/org.signalapp.view-aep.policy', ]; -export async function afterPack({ - appOutDir, - electronPlatformName, -}: AfterPackContext): Promise { +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterPack({ appOutDir, electronPlatformName }) { if (electronPlatformName !== 'linux') { return; } diff --git a/ts/scripts/fuse-electron.node.ts b/scripts/fuse-electron.mjs similarity index 82% rename from ts/scripts/fuse-electron.node.ts rename to scripts/fuse-electron.mjs index 5288da36af..235c458d66 100644 --- a/ts/scripts/fuse-electron.node.ts +++ b/scripts/fuse-electron.mjs @@ -1,15 +1,16 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import path from 'node:path'; import { flipFuses, FuseVersion, FuseV1Options } from '@electron/fuses'; -import type { AfterPackContext } from 'electron-builder'; -export async function afterPack({ - appOutDir, - packager, - electronPlatformName, -}: AfterPackContext): Promise { +/** @import { AfterPackContext, LinuxPackager } from 'electron-builder' */ + +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterPack({ appOutDir, packager, electronPlatformName }) { const { productFilename } = packager.appInfo; let target; @@ -18,9 +19,7 @@ export async function afterPack({ } else if (electronPlatformName === 'win32') { target = `${productFilename}.exe`; } else if (electronPlatformName === 'linux') { - // Sadly, `LinuxPackager` type is not exported by electron-builder so we - // have to improvise - target = (packager as unknown as { executableName: string }).executableName; + target = /** @type {LinuxPackager} */ (packager).executableName; } else { throw new Error(`Unsupported platform: ${electronPlatformName}`); } diff --git a/scripts/gen-locales-config.mjs b/scripts/gen-locales-config.mjs new file mode 100644 index 0000000000..2c3e802839 --- /dev/null +++ b/scripts/gen-locales-config.mjs @@ -0,0 +1,59 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fs from 'node:fs/promises'; +import path from 'node:path'; +import fastGlob from 'fast-glob'; +import * as LocaleMatcher from '@formatjs/intl-localematcher'; + +const ROOT_DIR = path.join(import.meta.dirname, '..'); + +/** + * @param {string} input + * @param {string} expected + */ +function matches(input, expected) { + const match = LocaleMatcher.match([input], [expected], 'en', { + algorithm: 'best fit', + }); + return match === expected; +} + +const dirEntries = await fastGlob('_locales/*', { + cwd: ROOT_DIR, + onlyDirectories: true, +}); + +const localeDirNames = []; + +for (const dirEntry of dirEntries) { + const dirName = path.basename(dirEntry); + const locale = new Intl.Locale(dirName); + + // Smartling doesn't always use the correct language tag, so this check and + // reverse check are to make sure we don't accidentally add a locale that + // doesn't match its directory name (using LocaleMatcher). + // + // If this check ever fails, we may need to update our get-strings script to + // manually rename language tags before writing them to disk. + // + // Such is the case for Smartling's "zh-YU" locale, which we renamed to + // "yue" to match the language tag used by... everyone else. + + if (!matches(dirName, locale.baseName)) { + throw new Error( + `Matched locale "${dirName}" does not match its resolved name "${locale.baseName}"` + ); + } + if (!matches(locale.baseName, dirName)) { + throw new Error( + `Matched locale "${dirName}" does not match its dir name "${dirName}"` + ); + } + + localeDirNames.push(dirName); +} + +const jsonPath = path.join(ROOT_DIR, 'build', 'available-locales.json'); +console.log(`Writing to "${jsonPath}"...`); +await fs.writeFile(jsonPath, `${JSON.stringify(localeDirNames, null, 2)}\n`); diff --git a/ts/scripts/gen-nsis-script.node.ts b/scripts/gen-nsis-script.mjs similarity index 88% rename from ts/scripts/gen-nsis-script.node.ts rename to scripts/gen-nsis-script.mjs index 9ddbfdc115..a24633c509 100644 --- a/ts/scripts/gen-nsis-script.node.ts +++ b/scripts/gen-nsis-script.mjs @@ -1,6 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { createIntl } from '@formatjs/intl'; import path from 'node:path'; import fs from 'node:fs'; @@ -11,7 +11,7 @@ import fs from 'node:fs'; // // Make sure to sync up the values in `util/nsis` with upstream // `app-builder-lib`. -import { REQUIRED_LANGUAGES, LCID } from '../util/nsis.std.ts'; +import { REQUIRED_LANGUAGES, LCID } from './utils/nsis.mjs'; const STRING_VARS = new Map([ [ @@ -35,19 +35,20 @@ const STRING_VARS = new Map([ console.log('Generating updates NSIS script'); console.log(); -const USED = new Set(); +/** @type {Set} */ +const USED = new Set(); -const ROOT_DIR = path.join(__dirname, '..', '..'); +const ROOT_DIR = path.join(import.meta.dirname, '..'); const LOCALES_DIR = path.join(ROOT_DIR, '_locales'); const fallbackMessages = JSON.parse( fs.readFileSync(path.join(LOCALES_DIR, 'en', 'messages.json')).toString() ); -const nsisStrings = new Array(); +/** @type {Array} */ +const nsisStrings = []; for (const lang of REQUIRED_LANGUAGES) { - // oxlint-disable-next-line typescript/no-non-null-assertion - const langId = LCID[lang] ?? LCID.en_US!; + const langId = LCID[lang] ?? LCID.en_US; if (USED.has(langId)) { continue; } @@ -59,7 +60,8 @@ for (const lang of REQUIRED_LANGUAGES) { if (lang === 'zh_TW') { fallbacks.unshift('zh-Hant'); } - let json: Buffer | undefined; + /** @type {Buffer | undefined} */ + let json; for (const f of fallbacks) { try { json = fs.readFileSync(path.join(LOCALES_DIR, f, 'messages.json')); diff --git a/scripts/gen-policy-files.mjs b/scripts/gen-policy-files.mjs new file mode 100644 index 0000000000..3efea2c8a3 --- /dev/null +++ b/scripts/gen-policy-files.mjs @@ -0,0 +1,101 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fs from 'node:fs/promises'; +import path from 'node:path'; +import fastGlob from 'fast-glob'; +import { assert } from './utils/assert.mjs'; + +const ROOT_DIR = path.join(import.meta.dirname, '..'); + +const dirEntries = await fastGlob('_locales/*', { + cwd: ROOT_DIR, + onlyDirectories: true, +}); + +const english = JSON.parse( + await fs.readFile(path.join(ROOT_DIR, '_locales/en/messages.json'), 'utf8') +); +const templateDir = path.join(ROOT_DIR, 'build/policy-templates'); +const templates = [ + { + name: 'org.signalapp.enable-backups.policy', + content: await fs.readFile( + path.join(templateDir, 'org.signalapp.enable-backups.policy'), + 'utf8' + ), + description: + 'icu:Preferences__local-backups--enable--os-prompt-description--linux', + message: 'icu:Preferences__local-backups--enable--os-prompt-message--linux', + }, + { + name: 'org.signalapp.plaintext-export.policy', + content: await fs.readFile( + path.join(templateDir, 'org.signalapp.plaintext-export.policy'), + 'utf8' + ), + description: 'icu:PlaintextExport--OSPrompt--description--Linux', + message: 'icu:PlaintextExport--OSPrompt--message--Linux', + }, + { + name: 'org.signalapp.view-aep.policy', + content: await fs.readFile( + path.join(templateDir, 'org.signalapp.view-aep.policy'), + 'utf8' + ), + description: + 'icu:Preferences--local-backups--view-recovery-key--os-prompt-description--linux', + message: + 'icu:Preferences--local-backups--view-recovery-key--os-prompt-message--linux', + }, +]; + +for (const template of templates) { + const englishDescription = english[template.description]?.messageformat; + assert( + typeof englishDescription === 'string' && englishDescription.length !== 0, + `Must have english string for key ${template.description}` + ); + const englishMessage = english[template.message]?.messageformat; + assert( + typeof englishMessage === 'string', + `Must have english string for key ${template.message}` + ); + + let allDescriptions = `${englishDescription}\n`; + let allMessages = `${englishMessage}\n`; + + for (const dirEntry of dirEntries) { + const locale = path.basename(dirEntry); + const data = JSON.parse( + // oxlint-disable-next-line no-await-in-loop + await fs.readFile( + path.join(ROOT_DIR, '_locales', locale, 'messages.json'), + 'utf8' + ) + ); + + const localeName = locale.replace('-', '_'); + const description = + data[template.description]?.messageformat ?? englishDescription; + allDescriptions += ` ${description}\n`; + + const message = data[template.message]?.messageformat ?? englishMessage; + allMessages += ` ${message}\n`; + } + + const targetPath = path.join(ROOT_DIR, 'build', template.name); + let targetContent = template.content; + + targetContent = targetContent.replace( + '', + allDescriptions + ); + targetContent = targetContent.replace( + '', + allMessages + ); + + // oxlint-disable-next-line no-await-in-loop + await fs.writeFile(targetPath, targetContent); +} diff --git a/scripts/generate-acknowledgments.js b/scripts/generate-acknowledgments.mjs similarity index 51% rename from scripts/generate-acknowledgments.js rename to scripts/generate-acknowledgments.mjs index f141f4f10c..76c0dd5013 100644 --- a/scripts/generate-acknowledgments.js +++ b/scripts/generate-acknowledgments.mjs @@ -1,13 +1,12 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const assert = require('node:assert'); -const fs = require('node:fs'); -const { join } = require('node:path'); -const { default: pMap } = require('p-map'); -const prettier = require('prettier'); - -const { default: packageJson } = require('./packageJson.js'); +// @ts-check +import assert from 'node:assert'; +import fs from 'node:fs'; +import { join } from 'node:path'; +import pMap from 'p-map'; +import prettier from 'prettier'; +import packageJson from '../package.json' with { type: 'json' }; // During development, you might use local versions of dependencies which are missing // acknowledgment files. In this case we'll skip rebuilding the acknowledgment files. @@ -28,14 +27,20 @@ const SIGNAL_LIBS = [ const SKIPPED_DEPENDENCIES = new Set(SIGNAL_LIBS); -const rootDir = join(__dirname, '..'); +const rootDir = join(import.meta.dirname, '..'); const nodeModulesPath = join(rootDir, 'node_modules'); const destinationPath = join(rootDir, 'ACKNOWLEDGMENTS.md'); +/** + * @param {string} fileName + */ function isLicenseFileName(fileName) { return /^licen[s|c]e/i.test(fileName); } +/** + * @param {string} dependencyName + */ async function getMarkdownForDependency(dependencyName) { let licenseBody; @@ -59,7 +64,7 @@ async function getMarkdownForDependency(dependencyName) { } else { const packageJsonPath = join(dependencyRootPath, 'package.json'); const { license } = JSON.parse( - await fs.promises.readFile(packageJsonPath) + await fs.promises.readFile(packageJsonPath, 'utf8') ); if (!license) { throw new Error(`Could not find license for ${dependencyName}`); @@ -81,6 +86,9 @@ async function getMarkdownForDependency(dependencyName) { ].join('\n'); } +/** + * @param {string} dependencyName + */ async function getMarkdownForSignalLib(dependencyName) { const dependencyRootPath = join(nodeModulesPath, dependencyName); const licenseFilePath = join( @@ -89,20 +97,19 @@ async function getMarkdownForSignalLib(dependencyName) { 'acknowledgments.md' ); + /** @type {string} */ let licenseBody; try { licenseBody = await fs.promises.readFile(licenseFilePath, 'utf8'); } catch (err) { - if (err) { - if (err.code === 'ENOENT' && !REQUIRE_SIGNAL_LIB_FILES) { - console.warn( - `Missing acknowledgments file for ${dependencyName}. Skipping generation of acknowledgments.` - ); - process.exit(0); - } - - throw err; + if (err.code === 'ENOENT' && !REQUIRE_SIGNAL_LIB_FILES) { + console.warn( + `Missing acknowledgments file for ${dependencyName}. Skipping generation of acknowledgments.` + ); + process.exit(0); } + + throw err; } return [ @@ -112,68 +119,60 @@ async function getMarkdownForSignalLib(dependencyName) { ].join('\n'); } -async function main() { - assert.deepStrictEqual( - Object.keys(optionalDependencies), - ['fs-xattr'], - 'Unexpected optionalDependencies when generating acknowledgments file. To ensure that this file is generated deterministically, make sure to special-case it the acknowledgments generation script.' - ); +assert.deepStrictEqual( + Object.keys(optionalDependencies), + ['fs-xattr'], + 'Unexpected optionalDependencies when generating acknowledgments file. To ensure that this file is generated deterministically, make sure to special-case it the acknowledgments generation script.' +); - const dependencyNames = [ - ...Object.keys(dependencies), - ...Object.keys(devDependencies), - ...Object.keys(optionalDependencies), - ] - .filter(name => !SKIPPED_DEPENDENCIES.has(name)) - .sort(); +const dependencyNames = [ + ...Object.keys(dependencies), + ...Object.keys(devDependencies), + ...Object.keys(optionalDependencies), +] + .filter(name => !SKIPPED_DEPENDENCIES.has(name)) + .sort(); - const markdownsForDependency = await pMap( - dependencyNames, - getMarkdownForDependency, - // Without this, we may run into "too many open files" errors. - { - concurrency: 100, - timeout: 1000 * 60 * 2, - } - ); +const markdownsForDependency = await pMap( + dependencyNames, + getMarkdownForDependency, + // Without this, we may run into "too many open files" errors. + { + concurrency: 100, + signal: AbortSignal.timeout(1000 * 60 * 2), + } +); - // For our libraries copy the respective acknowledgement lists - const markdownsFromSignalLibs = await pMap( - SIGNAL_LIBS, - getMarkdownForSignalLib, - { - concurrency: 100, - timeout: 1000 * 60 * 2, - } - ); +// For our libraries copy the respective acknowledgement lists +const markdownsFromSignalLibs = await pMap( + SIGNAL_LIBS, + getMarkdownForSignalLib, + { + concurrency: 100, + signal: AbortSignal.timeout(1000 * 60 * 2), + } +); - const unformattedOutput = [ - '', - '', - '# Acknowledgments', - '', - 'Signal Desktop makes use of the following open source projects.', - '', - markdownsForDependency.join('\n\n'), - '', - '## Kyber Patent License', - '', - '', - '', - markdownsFromSignalLibs.join('\n\n'), - ].join('\n'); +const unformattedOutput = [ + '', + '', + '# Acknowledgments', + '', + 'Signal Desktop makes use of the following open source projects.', + '', + markdownsForDependency.join('\n\n'), + '', + '## Kyber Patent License', + '', + '', + '', + markdownsFromSignalLibs.join('\n\n'), +].join('\n'); - const prettierConfig = await prettier.resolveConfig(destinationPath); - const output = await prettier.format(unformattedOutput, { - ...prettierConfig, - filepath: destinationPath, - }); - - await fs.promises.writeFile(destinationPath, output); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); +const prettierConfig = await prettier.resolveConfig(destinationPath); +const output = await prettier.format(unformattedOutput, { + ...prettierConfig, + filepath: destinationPath, }); + +await fs.promises.writeFile(destinationPath, output); diff --git a/scripts/generate-compact-locales.mjs b/scripts/generate-compact-locales.mjs new file mode 100644 index 0000000000..a499776639 --- /dev/null +++ b/scripts/generate-compact-locales.mjs @@ -0,0 +1,85 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { readdir, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import pMap from 'p-map'; + +/** + * @param {unknown} value + * @returns {value is { messageformat: string }} + */ +function isLocaleMessageType(value) { + return ( + typeof value === 'object' && + value != null && + Object.hasOwn(value, 'messageformat') + ); +} + +/** + * @param {object} options + * @param {string} options.sourceDir + * @param {string} options.targetDir + * @param {string} options.locale + * @param {ReadonlyArray} options.keys + * @returns {Promise>} + */ +async function compact({ sourceDir, targetDir, locale, keys }) { + const sourcePath = join(sourceDir, locale, 'messages.json'); + const targetPath = join(targetDir, locale, 'values.json'); + + await mkdir(dirname(targetPath), { recursive: true }); + + const json = JSON.parse(await readFile(sourcePath, 'utf8')); + + /** @type {Array} */ + const result = []; + for (const key of keys) { + if (json[key] == null) { + // Pull English translation, or leave blank (string was deleted) + result.push(null); + continue; + } + + const value = json[key]; + if (!isLocaleMessageType(value)) { + continue; + } + if (value.messageformat == null) { + continue; + } + result.push(value.messageformat); + } + + await writeFile(targetPath, JSON.stringify(result)); + + return keys; +} + +const rootDir = join(import.meta.dirname, '..'); +const sourceDir = join(rootDir, '_locales'); +const targetDir = join(rootDir, 'build', 'compact-locales'); + +const locales = await readdir(sourceDir); + +const allKeys = await pMap( + locales, + async locale => { + const sourcePath = join(sourceDir, locale, 'messages.json'); + const json = JSON.parse(await readFile(sourcePath, 'utf8')); + return Object.entries(json) + .filter(([, value]) => isLocaleMessageType(value)) + .map(([key]) => key); + }, + { concurrency: 10 } +); + +// Sort keys alphabetically for better incremental updates. +const keys = Array.from(new Set(allKeys.flat())).sort(); +await mkdir(targetDir, { recursive: true }); +await writeFile(join(targetDir, 'keys.json'), JSON.stringify(keys)); + +await pMap(locales, locale => compact({ sourceDir, targetDir, locale, keys }), { + concurrency: 10, +}); diff --git a/scripts/generate-dns-fallback.mjs b/scripts/generate-dns-fallback.mjs new file mode 100644 index 0000000000..1ffa7ca5c2 --- /dev/null +++ b/scripts/generate-dns-fallback.mjs @@ -0,0 +1,62 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { resolve4 as resolve4Cb, resolve6 as resolve6Cb } from 'node:dns'; +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +const resolve4 = promisify(resolve4Cb); +const resolve6 = promisify(resolve6Cb); + +const FALLBACK_DOMAINS = [ + 'chat.signal.org', + 'storage.signal.org', + 'cdsi.signal.org', + 'cdn.signal.org', + 'cdn2.signal.org', + 'cdn3.signal.org', + 'updates2.signal.org', + 'sfu.voip.signal.org', +]; + +const config = await Promise.all( + FALLBACK_DOMAINS.sort().map(async domain => { + const ipv4endpoints = (await resolve4(domain)).map(a => ({ + family: 'ipv4', + address: a, + })); + + const ipv6endpoints = (await resolve6(domain)).map(a => ({ + family: 'ipv6', + address: a, + })); + + const endpoints = [...ipv4endpoints, ...ipv6endpoints] + .filter(value => { + return value != null; + }) + .sort((a, b) => { + if (a.family < b.family) { + return -1; + } + if (a.family > b.family) { + return 1; + } + + if (a.address < b.address) { + return -1; + } + if (a.address > b.address) { + return 1; + } + return 0; + }); + + return { domain, endpoints }; + }) +); + +const outPath = join(import.meta.dirname, '..', 'build', 'dns-fallback.json'); + +await writeFile(outPath, `${JSON.stringify(config, null, 2)}\n`); diff --git a/scripts/generate-fixtures.mjs b/scripts/generate-fixtures.mjs new file mode 100644 index 0000000000..4fae6bfe0a --- /dev/null +++ b/scripts/generate-fixtures.mjs @@ -0,0 +1,47 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fs from 'node:fs/promises'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { execFileSync } from 'node:child_process'; + +const FIXTURES = path.join(import.meta.dirname, '..', 'fixtures'); +const SIZE = 256 * 1024; + +const original = crypto.randomBytes(SIZE); + +const originalPath = path.join(FIXTURES, 'diff-original.bin'); +await fs.writeFile(originalPath, original); + +// Add a few broken bytes to help create useful blockmaps +for (let i = 0; i < 3; i += 1) { + original[Math.floor(Math.random() * original.length)] = 0; +} + +const modifiedPath = path.join(FIXTURES, 'diff-modified.bin'); +await fs.writeFile(modifiedPath, original); + +const appBuilder = path.join( + import.meta.dirname, + '..', + 'node_modules', + 'app-builder-bin', + 'mac', + 'app-builder_amd64' +); + +for (const filePath of [originalPath, modifiedPath]) { + console.log('Adding blockmap to', filePath); + + // Put blockmap into a separate file + console.log( + execFileSync(appBuilder, [ + 'blockmap', + '--input', + filePath, + '--output', + `${filePath}.blockmap`, + ]).toString() + ); +} diff --git a/ts/scripts/generate-icu-types.node.ts b/scripts/generate-icu-types.mjs similarity index 74% rename from ts/scripts/generate-icu-types.node.ts rename to scripts/generate-icu-types.mjs index a05ba3c7af..ce77f183f3 100644 --- a/ts/scripts/generate-icu-types.node.ts +++ b/scripts/generate-icu-types.mjs @@ -1,24 +1,25 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import fs from 'node:fs/promises'; import { createHash } from 'node:crypto'; import path from 'node:path'; import ts from 'typescript'; import prettier from 'prettier'; +import { getICUMessageParams } from './utils/getICUMessageParams.mjs'; +import globalMessages from '../_locales/en/messages.json' with { type: 'json' }; +import { unreachable } from './utils/assert.mjs'; +import { DELETED_REGEXP } from './utils/intlMessages.mjs'; -import { getICUMessageParams } from '../util/getICUMessageParams.std.ts'; -import type { ICUMessageParamType } from '../util/getICUMessageParams.std.ts'; -import { missingCaseError } from '../util/missingCaseError.std.ts'; -import globalMessages from '../../_locales/en/messages.json'; +/** @import { ICUMessageParamType } from './utils/getICUMessageParams.mjs' */ -import { DELETED_REGEXP } from './constants.std.ts'; - -function translateParamType( - param: ICUMessageParamType, - stringType: ts.TypeNode, - componentType: ts.TypeNode -): ts.TypeNode { +/** + * @param {ICUMessageParamType} param + * @param {ts.TypeNode} stringType + * @param {ts.TypeNode} componentType + * @returns {ts.TypeNode} + */ +function translateParamType(param, stringType, componentType) { switch (param.type) { case 'string': return stringType; @@ -42,16 +43,22 @@ function translateParamType( }) ); default: - throw missingCaseError(param); + unreachable(param); } } -const messageKeys = Object.keys(globalMessages).sort((a, b) => { +const messageKeys = /** @type {Array} */ ( + Object.keys(globalMessages) +).sort((a, b) => { return a.localeCompare(b); -}) as Array; +}); -function filterDefaultParams(params: Map) { - const filteredParams = new Map(); +/** + * @param {Map} params + */ +function filterDefaultParams(params) { + /** @type {Map} */ + const filteredParams = new Map(); for (const [key, value] of params) { if (key === 'emojify') { @@ -70,8 +77,14 @@ const ComponentNode = ts.factory.createTypeReferenceNode('Component'); const StringToken = ts.factory.createToken(ts.SyntaxKind.StringKeyword); const NeverToken = ts.factory.createToken(ts.SyntaxKind.NeverKeyword); -function generateType(name: string, supportsComponents: boolean): ts.Statement { - const props = new Array(); +/** + * @param {string} name + * @param {boolean} supportsComponents + * @returns {ts.Statement} + */ +function generateType(name, supportsComponents) { + /** @type {Array} */ + const props = []; for (const key of messageKeys) { if (key === 'smartling') { continue; @@ -102,11 +115,13 @@ function generateType(name: string, supportsComponents: boolean): ts.Statement { const stringType = supportsComponents ? ComponentOrStringNode : StringToken; const componentType = supportsComponents ? ComponentNode : NeverToken; - let paramType: ts.TypeNode; + /** @type {ts.TypeNode} */ + let paramType; if (params.size === 0) { paramType = ts.factory.createToken(ts.SyntaxKind.UndefinedKeyword); } else { - const subTypes = new Array(); + /** @type {Array} */ + const subTypes = []; for (const [paramName, value] of params) { subTypes.push( @@ -140,7 +155,8 @@ function generateType(name: string, supportsComponents: boolean): ts.Statement { ); } -const statements = new Array(); +/** @type {Array} */ +const statements = []; let top = ts.factory.createImportDeclaration( undefined, @@ -252,40 +268,32 @@ const unformattedOutput = printer.printNode( resultFile ); -async function main() { - const destinationPath = path.join( - __dirname, - '..', - '..', - 'build', - 'ICUMessageParams.d.ts' - ); +const destinationPath = path.join( + import.meta.dirname, + '..', + 'build', + 'ICUMessageParams.d.ts' +); - let oldHash: string | undefined; - try { - oldHash = createHash('sha512') - .update(await fs.readFile(destinationPath)) - .digest('hex'); - } catch { - // Ignore errors - } - - const prettierConfig = await prettier.resolveConfig(destinationPath); - const output = await prettier.format(unformattedOutput, { - ...prettierConfig, - filepath: destinationPath, - }); - - const newHash = createHash('sha512').update(output).digest('hex'); - if (oldHash === newHash) { - console.log('ICUMessageParams.d.ts is unchanged'); - } - - await fs.writeFile(destinationPath, output); +/** @type {string | undefined} */ +let oldHash; +try { + oldHash = createHash('sha512') + .update(await fs.readFile(destinationPath)) + .digest('hex'); +} catch { + // Ignore errors } -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); +const prettierConfig = await prettier.resolveConfig(destinationPath); +const output = await prettier.format(unformattedOutput, { + ...prettierConfig, + filepath: destinationPath, }); + +const newHash = createHash('sha512').update(output).digest('hex'); +if (oldHash === newHash) { + console.log('ICUMessageParams.d.ts is unchanged'); +} + +await fs.writeFile(destinationPath, output); diff --git a/scripts/generate-preload-cache.mjs b/scripts/generate-preload-cache.mjs new file mode 100644 index 0000000000..f67214bcaa --- /dev/null +++ b/scripts/generate-preload-cache.mjs @@ -0,0 +1,80 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { spawn } from 'node:child_process'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { mkdir, mkdtemp, rm, rename } from 'node:fs/promises'; +import pTimeout from 'p-timeout'; +import { MINUTE } from './utils/durations.mjs'; +// Note: because we don't run under electron - this is a path to binary +import electronImport from 'electron'; +const ELECTRON_BINARY = /** @type {string} */ ( + /** @type {unknown} */ (electronImport) +); + +const ROOT_DIR = join(import.meta.dirname, '..'); + +const V8_ARGS = ['--predictable']; + +const storagePath = await mkdtemp(join(tmpdir(), 'signal-preload-cache-')); + +const argv = [`--js-flags=${V8_ARGS.join(' ')}`]; +if (process.platform === 'linux') { + argv.push('--no-sandbox'); +} +argv.push('ci.js'); + +const proc = spawn(ELECTRON_BINARY, argv, { + stdio: [null, 'inherit', 'inherit'], + cwd: ROOT_DIR, + env: { + // Linux X11 support + DISPLAY: process.env.DISPLAY, + XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR, + WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY, + XAUTHORITY: process.env.XAUTHORITY, + + CI: process.env.CI ? 'on' : undefined, + GENERATE_PRELOAD_CACHE: 'on', + SIGNAL_CI_CONFIG: JSON.stringify({ + storagePath, + openDevTools: false, + }), + }, +}); + +try { + /** @type {PromiseWithResolvers} */ + const { promise, resolve, reject } = Promise.withResolvers(); + proc.on('exit', status => resolve(status)); + proc.on('error', error => reject(error)); + + const status = await pTimeout(promise, 5 * MINUTE); + + if (status !== 0) { + throw new Error(`Exit code: ${status}`); + } +} catch (error) { + const { ARTIFACTS_DIR } = process.env; + if (!ARTIFACTS_DIR) { + console.error( + 'Not saving artifacts. Please set ARTIFACTS_DIR env variable' + ); + } else { + console.error(`Saving logs to ${ARTIFACTS_DIR}`); + await mkdir(ARTIFACTS_DIR, { recursive: true }); + + const logsDir = join(storagePath, 'logs'); + await rename(logsDir, join(ARTIFACTS_DIR, 'logs')); + } + + throw error; +} finally { + try { + proc.kill(); + } catch { + // Ignore + } + await rm(storagePath, { recursive: true }); +} diff --git a/ts/scripts/generate-tray-icons.node.ts b/scripts/generate-tray-icons.mjs similarity index 68% rename from ts/scripts/generate-tray-icons.node.ts rename to scripts/generate-tray-icons.mjs index d7906c652f..f5f552a667 100644 --- a/ts/scripts/generate-tray-icons.node.ts +++ b/scripts/generate-tray-icons.mjs @@ -1,43 +1,53 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { createCanvas, GlobalFonts, loadImage } from '@napi-rs/canvas'; import { join } from 'node:path'; import { mkdir, rm, writeFile } from 'node:fs/promises'; -import { strictAssert } from '../util/assert.std.ts'; +import { assert } from './utils/assert.mjs'; -const cwd = __dirname; -const fontsDir = join(cwd, '..', '..', 'fonts'); -const imagesDir = join(cwd, '..', '..', 'images'); +const baseDir = join(import.meta.dirname, '..'); +const fontsDir = join(baseDir, 'fonts'); +const imagesDir = join(baseDir, 'images'); const trayIconsDir = join(imagesDir, 'tray-icons'); const trayIconsBaseDir = join(trayIconsDir, 'base'); const trayIconsAlertsDir = join(trayIconsDir, 'alert'); -enum TrayIconSize { - Size16 = '16', - Size32 = '32', - Size48 = '48', - Size256 = '256', -} +const TrayIconSizes = /** @type {const} */ ({ + Size16: '16', + Size32: '32', + Size48: '48', + Size256: '256', +}); -type TrayIconValue = number | string | null; +/** + * @typedef {typeof TrayIconSizes[keyof typeof TrayIconSizes]} TrayIconSize + */ -type TrayIconImageRequest = Readonly<{ - size: TrayIconSize; - value: TrayIconValue; -}>; +/** + * @typedef {number | string | null} TrayIconValue + */ -type TrayIconVariant = { - size: number; - maxCount: number; - badgePadding: number; - fontSize: number; - fontWeight: string; - fontOffsetY: number; - badgeShadowBlur: number; - badgeShadowOffsetY: number; - image: string; -}; +/** + * @typedef {Readonly<{ + * size: TrayIconSize; + * value: TrayIconValue; + * }>} TrayIconImageRequest + */ + +/** + * @typedef {{ + * size: number; + * maxCount: number; + * badgePadding: number; + * fontSize: number; + * fontWeight: string; + * fontOffsetY: number; + * badgeShadowBlur: number; + * badgeShadowOffsetY: number; + * image: string; + * }} TrayIconVariant + */ GlobalFonts.loadFontsFromDir(fontsDir); @@ -45,7 +55,7 @@ const Inter = GlobalFonts.families.find(family => { return family.family === 'Inter'; }); -strictAssert(Inter != null, `Failed to load fonts from ${fontsDir}`); +assert(Inter != null, `Failed to load fonts from ${fontsDir}`); const Constants = { fontFamily: 'Inter', @@ -53,8 +63,9 @@ const Constants = { badgeShadowColor: 'rgba(0, 0, 0, 0.25)', }; -const Variants: Record = { - [TrayIconSize.Size16]: { +/** @type {Record} */ +const Variants = { + [TrayIconSizes.Size16]: { size: 16, maxCount: 9, badgePadding: 2, @@ -65,7 +76,7 @@ const Variants: Record = { badgeShadowOffsetY: 0, image: join(trayIconsBaseDir, 'signal-tray-icon-16x16-base.png'), }, - [TrayIconSize.Size32]: { + [TrayIconSizes.Size32]: { size: 32, maxCount: 9, badgePadding: 4, @@ -76,7 +87,7 @@ const Variants: Record = { badgeShadowOffsetY: 1, image: join(trayIconsBaseDir, 'signal-tray-icon-32x32-base.png'), }, - [TrayIconSize.Size48]: { + [TrayIconSizes.Size48]: { size: 48, maxCount: 9, badgePadding: 6, @@ -87,7 +98,7 @@ const Variants: Record = { badgeShadowOffsetY: 1, image: join(trayIconsBaseDir, 'signal-tray-icon-48x48-base.png'), }, - [TrayIconSize.Size256]: { + [TrayIconSizes.Size256]: { size: 256, maxCount: 9, fontSize: 72, @@ -100,10 +111,12 @@ const Variants: Record = { }, }; -function trayIconValueToText( - value: TrayIconValue, - variant: TrayIconVariant -): string { +/** + * @param {TrayIconValue} value + * @param {TrayIconVariant} variant + * @returns {string} + */ +function trayIconValueToText(value, variant) { if (value == null) { return ''; } @@ -130,9 +143,11 @@ function trayIconValueToText( throw new TypeError(`Invalid value ${value}`); } -async function generateTrayIconImage( - request: TrayIconImageRequest -): Promise> { +/** + * @param {TrayIconImageRequest} request + * @returns {Promise>} + */ +async function generateTrayIconImage(request) { const variant = Variants[request.size]; if (variant == null) { throw new TypeError(`Invalid variant size (${request.size})`); @@ -225,52 +240,50 @@ async function generateTrayIconImage( return canvas.toBuffer('image/png'); } -function range(start: number, end: number): Array { +/** + * @param {number} start + * @param {number} end + * @returns {Array} + */ +function range(start, end) { const length = end - start + 1; return Array.from({ length }, (_, index) => start + index); } -async function main() { - try { - await rm(trayIconsAlertsDir, { recursive: true }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } +try { + await rm(trayIconsAlertsDir, { recursive: true }); +} catch (error) { + if (error.code !== 'ENOENT') { + throw error; } - - const requests: Array = []; - for (const size of Object.values(TrayIconSize)) { - const variant = Variants[size]; - const { maxCount } = variant; - const values = range(1, maxCount + 1); - for (const value of values) { - requests.push({ size, value }); - } - } - - await Promise.all( - requests.map(async ({ size, value }) => { - const variant = Variants[size]; - const text = trayIconValueToText(value, variant); - - const fileDir = join(trayIconsAlertsDir); - const fileName = `signal-tray-icon-${size}x${size}-alert-${text}.png`; - const filePath = join(fileDir, fileName); - - const fileContents = await generateTrayIconImage({ size, value }); - - console.log(`Writing "${fileName}"`); - await mkdir(fileDir, { recursive: true }); - await writeFile(filePath, fileContents); - }) - ); - - console.log('Done'); } -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); +/** @type {Array} */ +const requests = []; +for (const size of Object.values(TrayIconSizes)) { + const variant = Variants[size]; + const { maxCount } = variant; + const values = range(1, maxCount + 1); + for (const value of values) { + requests.push({ size, value }); + } +} + +await Promise.all( + requests.map(async ({ size, value }) => { + const variant = Variants[size]; + const text = trayIconValueToText(value, variant); + + const fileDir = join(trayIconsAlertsDir); + const fileName = `signal-tray-icon-${size}x${size}-alert-${text}.png`; + const filePath = join(fileDir, fileName); + + const fileContents = await generateTrayIconImage({ size, value }); + + console.log(`Writing "${fileName}"`); + await mkdir(fileDir, { recursive: true }); + await writeFile(filePath, fileContents); + }) +); + +console.log('Done'); diff --git a/scripts/get-emoji-locales.mjs b/scripts/get-emoji-locales.mjs new file mode 100644 index 0000000000..cd35007681 --- /dev/null +++ b/scripts/get-emoji-locales.mjs @@ -0,0 +1,95 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { writeFile, readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import z from 'zod'; +import prettier from 'prettier'; +import { OptionalResourcesDictSchema } from './utils/optionalResources.mjs'; + +/** @import { OptionalResourceType } from './utils/optionalResources.mjs' */ + +const MANIFEST_URL = + 'https://updates.signal.org/dynamic/android/emoji/search/manifest.json'; + +const ManifestSchema = z.object({ + version: z.number(), + languages: z.string().array(), + languageToSmartlingLocale: z.record(z.string(), z.string()), +}); + +/** + * @param {string} url + * @returns {Promise} + */ +async function fetchJSON(url) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}`); + } + + return res.json(); +} + +const manifest = ManifestSchema.parse(await fetchJSON(MANIFEST_URL)); + +manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant'; +manifest.languageToSmartlingLocale['sr'] = 'sr'; + +/** @type {Map} */ +const extraResources = new Map(); + +await Promise.all( + manifest.languages.map(async language => { + const langUrl = + 'https://updates.signal.org/static/android/' + + `emoji/search/${manifest.version}/${language}.json`; + + const res = await fetch(langUrl); + if (!res.ok) { + throw new Error(`Failed to fetch ${langUrl}`); + } + + const data = Buffer.from(await res.arrayBuffer()); + + const digest = createHash('sha512').update(data).digest('base64'); + + let locale = manifest.languageToSmartlingLocale[language] ?? language; + locale = locale.replace(/_/g, '-'); + + const pinnedUrl = + 'https://updates2.signal.org/static/android/' + + `emoji/search/${manifest.version}/${language}.json`; + + extraResources.set(locale, { + url: pinnedUrl, + size: data.length, + digest, + }); + }) +); + +const resourcesPath = join( + import.meta.dirname, + '..', + 'build', + 'optional-resources.json' +); +const resources = OptionalResourcesDictSchema.parse( + JSON.parse(await readFile(resourcesPath, 'utf8')) +); + +for (const [locale, resource] of extraResources) { + resources[`emoji-index-${locale}.json`] = resource; +} + +const prettierConfig = await prettier.resolveConfig( + join(import.meta.dirname, '..', 'build') +); + +const output = await prettier.format(JSON.stringify(resources, null, 2), { + ...prettierConfig, + filepath: resourcesPath, +}); +await writeFile(resourcesPath, output); diff --git a/ts/scripts/get-expire-time.node.ts b/scripts/get-expire-time.mjs similarity index 63% rename from ts/scripts/get-expire-time.node.ts rename to scripts/get-expire-time.mjs index deebfcee63..8c520ba5b4 100644 --- a/ts/scripts/get-expire-time.node.ts +++ b/scripts/get-expire-time.mjs @@ -1,13 +1,12 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { join } from 'node:path'; import { execSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; - -import { DAY } from '../util/durations/index.std.ts'; -import { packageJson } from '../util/packageJson.node.ts'; -import { isNotUpdatable } from '../util/version.std.ts'; +import { DAY } from './utils/durations.mjs'; +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; const unixTimestamp = parseInt( process.env.SOURCE_DATE_EPOCH || @@ -16,20 +15,22 @@ const unixTimestamp = parseInt( ); const buildCreation = unixTimestamp * 1000; +const isNotUpdatable = !parseVersion(packageJson.version).isUpdatable; + // NB: Build expirations are also determined via users' auto-update settings; see // getExpirationTimestamp -const validDuration = isNotUpdatable(packageJson.version) ? DAY * 30 : DAY * 90; +const validDuration = isNotUpdatable ? DAY * 30 : DAY * 90; const buildExpiration = buildCreation + validDuration; const localProductionPath = join( - __dirname, - '../../config/local-production.json' + import.meta.dirname, + '../config/local-production.json' ); const localProductionConfig = { buildCreation, buildExpiration, - ...(isNotUpdatable(packageJson.version) ? { updatesEnabled: false } : {}), + ...(isNotUpdatable ? { updatesEnabled: false } : {}), }; writeFileSync( diff --git a/scripts/get-jumbomoji.mjs b/scripts/get-jumbomoji.mjs new file mode 100644 index 0000000000..55194502b0 --- /dev/null +++ b/scripts/get-jumbomoji.mjs @@ -0,0 +1,100 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { writeFile, readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { Buffer } from 'node:buffer'; +import z from 'zod'; +import prettier from 'prettier'; +import { OptionalResourcesDictSchema } from './utils/optionalResources.mjs'; +import { utf16ToEmoji } from './utils/utf16ToEmoji.mjs'; + +/** @import { OptionalResourceType } from './utils/optionalResources.mjs'; */ + +const VERSION = 12; + +const STATIC_URL = 'https://updates.signal.org/static/android/emoji'; +const STATIC_PINNED_URL = 'https://updates2.signal.org/static/android/emoji'; +const MANIFEST_URL = `${STATIC_URL}/${VERSION}/emoji_data.json`; + +const ManifestSchema = z.object({ + jumbomoji: z.record(z.string(), z.string().transform(utf16ToEmoji).array()), +}); + +/** + * @param {string} url + * @returns {Promise} + */ +async function fetchJSON(url) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}`); + } + + return res.json(); +} + +const { jumbomoji } = ManifestSchema.parse(await fetchJSON(MANIFEST_URL)); + +/** @type {Map} */ +const extraResources = new Map(); + +await Promise.all( + Array.from(Object.keys(jumbomoji)).map(async sheet => { + const publicUrl = `${STATIC_URL}/${VERSION}/xhdpi/jumbo/${sheet}.proto`; + + const res = await fetch(publicUrl); + if (!res.ok) { + throw new Error(`Failed to fetch ${publicUrl}`); + } + + const data = Buffer.from(await res.arrayBuffer()); + + const digest = createHash('sha512').update(data).digest('base64'); + + const pinnedUrl = `${STATIC_PINNED_URL}/${VERSION}/xhdpi/jumbo/${sheet}.proto`; + + extraResources.set(sheet, { + url: pinnedUrl, + size: data.length, + digest, + }); + }) +); + +const manifestPath = join(import.meta.dirname, '..', 'build', 'jumbomoji.json'); + +const resourcesPath = join( + import.meta.dirname, + '..', + 'build', + 'optional-resources.json' +); +const resources = OptionalResourcesDictSchema.parse( + JSON.parse(await readFile(resourcesPath, 'utf8')) +); + +for (const [sheet, resource] of extraResources) { + resources[`emoji-sheet-${sheet}.proto`] = resource; +} + +const prettierConfig = await prettier.resolveConfig( + join(import.meta.dirname, '..', 'build') +); + +{ + const output = await prettier.format(JSON.stringify(jumbomoji, null, 2), { + ...prettierConfig, + filepath: manifestPath, + }); + await writeFile(manifestPath, output); +} + +{ + const output = await prettier.format(JSON.stringify(resources, null, 2), { + ...prettierConfig, + filepath: resourcesPath, + }); + await writeFile(resourcesPath, output); +} diff --git a/scripts/get-strings.mjs b/scripts/get-strings.mjs new file mode 100644 index 0000000000..a89286504d --- /dev/null +++ b/scripts/get-strings.mjs @@ -0,0 +1,144 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { rm, mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import fastGlob from 'fast-glob'; +import prettier from 'prettier'; +import pMap from 'p-map'; +import z from 'zod'; +import { authenticate, API_BASE, PROJECT_ID } from './utils/smartling.mjs'; + +const { SMARTLING_USER, SMARTLING_SECRET } = process.env; + +const RENAMES = new Map([ + // Smartling uses "zh-YU" for Cantonese (or Yue Chinese). + // This is wrong. + // The language tag for Yue Chinese is "yue" + // "zh-YU" actually implies "Chinese as spoken in Yugoslavia (canonicalized to Serbia)" + ['zh-YU', 'yue'], + + // For most of the Chinese-speaking world, where we don't have a region specific + // locale available (e.g. zh-HK), zh-TW is a suitable choice for "Traditional Chinese". + // + // However, Intl.LocaleMatcher won't match "zh-Hant-XX" to "zh-TW", + // we need to rename it to "zh-Hant" explicitly to make it work. + ['zh-TW', 'zh-Hant'], + + // "YR" is not a valid region subtag. Smartling made it up. + ['sr-YR', 'sr'], +]); + +const StatusSchema = z.object({ + response: z.object({ + code: z.literal('SUCCESS'), + data: z.object({ + items: z + .object({ + localeId: z.string(), + }) + .array(), + }), + }), +}); + +if (!SMARTLING_USER) { + console.error('Need to set SMARTLING_USER environment variable!'); + process.exit(1); +} +if (!SMARTLING_SECRET) { + console.error('Need to set SMARTLING_SECRET environment variable!'); + process.exit(1); +} + +console.log('Authenticating with Smartling'); +const headers = await authenticate({ + userIdentifier: SMARTLING_USER, + userSecret: SMARTLING_SECRET, +}); + +const statusURL = new URL( + `./files-api/v2/projects/${PROJECT_ID}/file/status`, + API_BASE +); +statusURL.searchParams.set('fileUri', '_locales/en/messages.json'); + +console.log('Getting list of locales...'); +const statusRes = await fetch(statusURL, { + headers, +}); + +if (!statusRes.ok) { + throw new Error('Failed to fetch the status'); +} +if (!statusRes.body) { + throw new Error('Missing body'); +} +const { + response: { + data: { items: locales }, + }, +} = StatusSchema.parse(await statusRes.json()); + +console.log('Cleaning _locales directory...'); +const dirEntries = await fastGlob(['_locales/*', '!_locales/en'], { + onlyDirectories: true, + absolute: true, +}); + +await Promise.all( + dirEntries.map(dirEntry => rm(dirEntry, { recursive: true })) +); + +console.log('Getting latest strings'); + +const prettierConfig = await prettier.resolveConfig('_locales'); + +await pMap( + locales, + async ({ localeId }) => { + const fileURL = new URL( + `./files-api/v2/projects/${PROJECT_ID}/` + + `locales/${encodeURIComponent(localeId)}/file`, + API_BASE + ); + fileURL.searchParams.set('fileUri', '_locales/en/messages.json'); + fileURL.searchParams.set('retrievalType', 'published'); + fileURL.searchParams.set('includeOriginalStrings', 'true'); + + const fileRes = await fetch(fileURL, { + headers, + }); + + if (!fileRes.ok) { + throw new Error('Failed to fetch the file'); + } + if (!fileRes.body) { + throw new Error('Missing body'); + } + + const targetLocale = RENAMES.get(localeId) ?? localeId; + const targetDir = path.join('_locales', targetLocale); + + try { + await mkdir(targetDir); + } catch (error) { + console.error(error); + } + + const targetFile = path.join(targetDir, 'messages.json'); + console.log('Writing', targetLocale); + const json = await fileRes.json(); + for (const value of Object.values(json)) { + const typedValue = /** @type {{ description?: string }} */ (value); + delete typedValue.description; + } + delete json.smartling; + const output = await prettier.format(JSON.stringify(json, null, 2), { + ...prettierConfig, + filepath: targetFile, + }); + await writeFile(targetFile, output); + }, + { concurrency: 20 } +); diff --git a/ts/scripts/locale-data/country-display-names.csv b/scripts/locale-data/country-display-names.csv similarity index 100% rename from ts/scripts/locale-data/country-display-names.csv rename to scripts/locale-data/country-display-names.csv diff --git a/ts/scripts/locale-data/locale-display-names.csv b/scripts/locale-data/locale-display-names.csv similarity index 100% rename from ts/scripts/locale-data/locale-display-names.csv rename to scripts/locale-data/locale-display-names.csv diff --git a/ts/scripts/mark-unused-strings-deleted.node.ts b/scripts/mark-unused-strings-deleted.mjs similarity index 88% rename from ts/scripts/mark-unused-strings-deleted.node.ts rename to scripts/mark-unused-strings-deleted.mjs index 0a0fd681fe..e46eb5fc8b 100644 --- a/ts/scripts/mark-unused-strings-deleted.node.ts +++ b/scripts/mark-unused-strings-deleted.mjs @@ -1,14 +1,18 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check import { spawnSync } from 'node:child_process'; import path from 'node:path'; import { readFileSync, writeFileSync } from 'node:fs'; -import { DELETED_REGEXP } from './constants.std.ts'; +import { DELETED_REGEXP } from './utils/intlMessages.mjs'; -const rootDir = path.resolve(__dirname, '..', '..'); +const rootDir = path.resolve(import.meta.dirname, '..'); const messagesPath = path.join(rootDir, '_locales/en/messages.json'); -function getIcuLikeStrings(): Set { +/** + * @returns {Set} + */ +function getIcuLikeStrings() { const { status, stdout } = spawnSync( 'grep', [ @@ -36,7 +40,10 @@ function getIcuLikeStrings(): Set { return new Set(stdout.trim().split('\n')); } -function getDateStr(date: Date) { +/** + * @param {Date} date + */ +function getDateStr(date) { const year = String(date.getFullYear()); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); diff --git a/ts/scripts/mocha-separator.node.ts b/scripts/mocha-separator.mjs similarity index 90% rename from ts/scripts/mocha-separator.node.ts rename to scripts/mocha-separator.mjs index ee0bfd7911..f616a5a9c7 100644 --- a/ts/scripts/mocha-separator.node.ts +++ b/scripts/mocha-separator.mjs @@ -1,10 +1,10 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; -const MOCHA = join(__dirname, '..', '..', 'node_modules', '.bin', 'mocha'); +const MOCHA = join(import.meta.dirname, '..', 'node_modules', '.bin', 'mocha'); const WORKER_COUNT = parseInt(process.env.WORKER_COUNT || '1', 10); const WORKER_INDEX = parseInt(process.env.WORKER_INDEX || '0', 10); diff --git a/ts/scripts/notarize-universal-dmg.node.ts b/scripts/notarize-universal-dmg.mjs similarity index 83% rename from ts/scripts/notarize-universal-dmg.node.ts rename to scripts/notarize-universal-dmg.mjs index 843a8b37a9..c4955fb916 100644 --- a/ts/scripts/notarize-universal-dmg.node.ts +++ b/scripts/notarize-universal-dmg.mjs @@ -1,16 +1,20 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -import type { BuildResult } from 'electron-builder'; - +// @ts-check import { notarize } from '@electron/notarize'; +import { assert } from './utils/assert.mjs'; +import packageJson from '../package.json' with { type: 'json' }; -import { packageJson } from '../util/packageJson.node.ts'; +/** @import { BuildResult } from 'electron-builder' */ +/** + * @param {BuildResult} result + * @returns {Promise} + */ export async function afterAllArtifactBuild({ platformToTargets, artifactPaths, -}: BuildResult): Promise { +}) { const platforms = Array.from(platformToTargets.keys()).map( platform => platform.name ); @@ -63,8 +67,8 @@ export async function afterAllArtifactBuild({ return; } - // oxlint-disable-next-line typescript/no-non-null-assertion - const dmgPath = artifactsToStaple[0]!; + const dmgPath = artifactsToStaple[0]; + assert(dmgPath != null, 'Missing dmgPath'); console.log(`Notarizing dmg: ${dmgPath}`); await notarize({ diff --git a/ts/scripts/notarize.node.ts b/scripts/notarize.mjs similarity index 82% rename from ts/scripts/notarize.node.ts rename to scripts/notarize.mjs index 3bb9b31b72..d01f6f3cd0 100644 --- a/ts/scripts/notarize.node.ts +++ b/scripts/notarize.mjs @@ -1,18 +1,17 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import path from 'node:path'; -import type { AfterPackContext } from 'electron-builder'; - import { notarize } from '@electron/notarize'; +import packageJson from '../package.json' with { type: 'json' }; -import { packageJson } from '../util/packageJson.node.ts'; +/** @import { AfterPackContext } from 'electron-builder' */ -export async function afterSign({ - appOutDir, - packager, - electronPlatformName, -}: AfterPackContext): Promise { +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterSign({ appOutDir, packager, electronPlatformName }) { if (electronPlatformName !== 'darwin') { console.log('notarize: Skipping, not on macOS'); return; diff --git a/scripts/packageJson.js b/scripts/packageJson.js deleted file mode 100644 index 47e12066f2..0000000000 --- a/scripts/packageJson.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -const { readFileSync } = require('node:fs'); -const { join } = require('node:path'); - -const PACKAGE_JSON_PATH = join(__dirname, '..', 'package.json'); - -const json = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8')); - -exports.default = json; -exports.name = json.name; -exports.version = json.version; -exports.productName = json.productName; -exports.build = json.build; diff --git a/ts/scripts/prepare-no-delay-release.node.ts b/scripts/prepare-no-delay-release.mjs similarity index 72% rename from ts/scripts/prepare-no-delay-release.node.ts rename to scripts/prepare-no-delay-release.mjs index 54486e9e2f..51ff6b3384 100644 --- a/ts/scripts/prepare-no-delay-release.node.ts +++ b/scripts/prepare-no-delay-release.mjs @@ -1,13 +1,10 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import fs from 'node:fs'; import { join } from 'node:path'; -const PACKAGE_FILE = join(__dirname, '..', '..', 'package.json'); +const PACKAGE_FILE = join(import.meta.dirname, '..', 'package.json'); const json = JSON.parse(fs.readFileSync(PACKAGE_FILE, { encoding: 'utf8' })); diff --git a/scripts/prepare_adhoc_build.js b/scripts/prepare_adhoc_build.mjs similarity index 88% rename from scripts/prepare_adhoc_build.js rename to scripts/prepare_adhoc_build.mjs index b863a313ac..9ee4d67ec9 100644 --- a/scripts/prepare_adhoc_build.js +++ b/scripts/prepare_adhoc_build.mjs @@ -1,11 +1,11 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const fs = require('node:fs'); -const _ = require('lodash'); -const { execSync } = require('node:child_process'); -const { isAdhoc } = require('../ts/util/version.std.ts'); -const { default: packageJson, version } = require('./packageJson.js'); +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; +import { execSync } from 'node:child_process'; +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // You might be wondering why this file is necessary. It comes down to our desire to allow // side-by-side installation of production and adhoc builds. Electron-Builder uses @@ -13,8 +13,8 @@ const { default: packageJson, version } = require('./packageJson.js'); // debian package name, the install directory under /opt on linux, etc. We tried // adding the ${channel} macro to these values, but Electron-Builder didn't like that. -if (!isAdhoc(version)) { - console.error(`Version '${version}' is not an adhoc version!`); +if (parseVersion(packageJson.version).channel !== 'adhoc') { + console.error(`Version '${packageJson.version}' is not an adhoc version!`); process.exit(1); } @@ -73,6 +73,11 @@ const ADHOC_EXECUTABLE_NAME = `signal-desktop-adhoc-${formattedDate}-${shortSha} // ------- +/** + * @param {object} object + * @param {string} objectPath + * @param {string} expected + */ function checkValue(object, objectPath, expected) { const actual = _.get(object, objectPath); if (actual !== expected) { diff --git a/scripts/prepare_alpha_build.js b/scripts/prepare_alpha_build.mjs similarity index 86% rename from scripts/prepare_alpha_build.js rename to scripts/prepare_alpha_build.mjs index fd238b275d..571e1dbba1 100644 --- a/scripts/prepare_alpha_build.js +++ b/scripts/prepare_alpha_build.mjs @@ -1,11 +1,10 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const fs = require('node:fs'); -const _ = require('lodash'); - -const { isAlpha } = require('../ts/util/version.std.ts'); -const { default: packageJson, version } = require('./packageJson.js'); +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // You might be wondering why this file is necessary. It comes down to our desire to allow // side-by-side installation of production and alpha builds. Electron-Builder uses @@ -13,8 +12,8 @@ const { default: packageJson, version } = require('./packageJson.js'); // debian package name, the install directory under /opt on linux, etc. We tried // adding the ${channel} macro to these values, but Electron-Builder didn't like that. -if (!isAlpha(version)) { - console.error(`Version '${version}' is not an alpha version!`); +if (parseVersion(packageJson.version).channel !== 'alpha') { + console.error(`Version '${packageJson.version}' is not an alpha version!`); process.exit(1); } @@ -51,6 +50,11 @@ const ALPHA_EXECUTABLE_NAME = 'signal-desktop-alpha'; // ------- +/** + * @param {object} object + * @param {string} objectPath + * @param {string} expected + */ function checkValue(object, objectPath, expected) { const actual = _.get(object, objectPath); if (actual !== expected) { diff --git a/scripts/prepare_axolotl_build.js b/scripts/prepare_axolotl_build.mjs similarity index 84% rename from scripts/prepare_axolotl_build.js rename to scripts/prepare_axolotl_build.mjs index 6958464ffd..a4af087707 100644 --- a/scripts/prepare_axolotl_build.js +++ b/scripts/prepare_axolotl_build.mjs @@ -1,11 +1,11 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; -const fs = require('node:fs'); -const _ = require('lodash'); - -const { isAxolotl } = require('../ts/util/version.std.ts'); -const { default: packageJson, version } = require('./packageJson.js'); +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // You might be wondering why this file is necessary. It comes down to our desire to allow // side-by-side installation of production and alpha builds. Electron-Builder uses @@ -13,12 +13,12 @@ const { default: packageJson, version } = require('./packageJson.js'); // debian package name, the install directory under /opt on linux, etc. We tried // adding the ${channel} macro to these values, but Electron-Builder didn't like that. -if (!isAxolotl(version)) { - console.error(`Version '${version}' is not an backup version!`); +if (parseVersion(packageJson.version).channel !== 'axolotl') { + console.error(`Version '${packageJson.version}' is not an axolotl version!`); process.exit(1); } -console.log('prepare_backup_build: updating package.json'); +console.log('prepare_axolotl_build: updating package.json'); // ------- @@ -51,6 +51,11 @@ const AXOLOTL_EXECUTABLE_NAME = 'signal-desktop-axolotl'; // ------- +/** + * @param {object} object + * @param {string} objectPath + * @param {string} expected + */ function checkValue(object, objectPath, expected) { const actual = _.get(object, objectPath); if (actual !== expected) { diff --git a/scripts/prepare_beta_build.js b/scripts/prepare_beta_build.mjs similarity index 88% rename from scripts/prepare_beta_build.js rename to scripts/prepare_beta_build.mjs index 3cdbbbe836..ef66284428 100644 --- a/scripts/prepare_beta_build.js +++ b/scripts/prepare_beta_build.mjs @@ -1,11 +1,11 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; -const fs = require('node:fs'); -const _ = require('lodash'); - -const { isBeta } = require('../ts/util/version.std.ts'); -const { default: packageJson, version } = require('./packageJson.js'); +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // You might be wondering why this file is necessary. It comes down to our desire to allow // side-by-side installation of production and beta builds. Electron-Builder uses @@ -13,7 +13,8 @@ const { default: packageJson, version } = require('./packageJson.js'); // debian package name, the install directory under /opt on linux, etc. We tried // adding the ${channel} macro to these values, but Electron-Builder didn't like that. -if (!isBeta(version)) { +if (parseVersion(packageJson.version).channel !== 'beta') { + // ignore process.exit(); } @@ -50,6 +51,11 @@ const BETA_EXECUTABLE_NAME = 'signal-desktop-beta'; // ------- +/** + * @param {object} object + * @param {string} objectPath + * @param {string} expected + */ function checkValue(object, objectPath, expected) { const actual = _.get(object, objectPath); if (actual !== expected) { diff --git a/scripts/prepare_linux_build.js b/scripts/prepare_linux_build.mjs similarity index 82% rename from scripts/prepare_linux_build.js rename to scripts/prepare_linux_build.mjs index c41bdde6a5..17a10cc91f 100644 --- a/scripts/prepare_linux_build.js +++ b/scripts/prepare_linux_build.mjs @@ -1,8 +1,9 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const fs = require('node:fs'); -const _ = require('lodash'); +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; +import packageJson from '../package.json' with { type: 'json' }; const TARGETS = new Set(['appimage', 'deb']); @@ -17,8 +18,6 @@ if ( process.exit(1); } -const { default: packageJson } = require('./packageJson.js'); - console.log('prepare_linux_build: updating package.json'); // ------ diff --git a/scripts/prepare_staging_build.js b/scripts/prepare_staging_build.mjs similarity index 85% rename from scripts/prepare_staging_build.js rename to scripts/prepare_staging_build.mjs index 97eac37e0f..e0f09e577c 100644 --- a/scripts/prepare_staging_build.js +++ b/scripts/prepare_staging_build.mjs @@ -1,11 +1,10 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const fs = require('node:fs'); -const _ = require('lodash'); - -const { isAlpha } = require('../ts/util/version.std.ts'); -const { default: packageJson, version } = require('./packageJson.js'); +// @ts-check +import fs from 'node:fs'; +import _ from 'lodash'; +import { parseVersion } from './utils/parseVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; // You might be wondering why this file is necessary. It comes down to our desire to allow // side-by-side installation of production and staging builds. Electron-Builder uses @@ -13,8 +12,8 @@ const { default: packageJson, version } = require('./packageJson.js'); // debian package name, the install directory under /opt on linux, etc. We tried // adding the ${channel} macro to these values, but Electron-Builder didn't like that. -if (!isAlpha(version)) { - console.error(`Version '${version}' is not an alpha version!`); +if (parseVersion(packageJson.version).channel !== 'alpha') { + console.error(`Version '${packageJson.version}' is not an alpha version!`); process.exit(1); } @@ -23,7 +22,7 @@ console.log('prepare_staging_build: updating package.json'); // ------- const VERSION_PATH = 'version'; -const STAGING_VERSION = version.replace('alpha', 'staging'); +const STAGING_VERSION = packageJson.version.replace('alpha', 'staging'); const NAME_PATH = 'name'; const PRODUCTION_NAME = 'signal-desktop'; @@ -54,6 +53,11 @@ const STAGING_EXECUTABLE_NAME = 'signal-desktop-staging'; // ------- +/** + * @param {object} object + * @param {string} objectPath + * @param {string} expected + */ function checkValue(object, objectPath, expected) { const actual = _.get(object, objectPath); if (actual !== expected) { diff --git a/scripts/prepare_tagged_version.js b/scripts/prepare_tagged_version.mjs similarity index 71% rename from scripts/prepare_tagged_version.js rename to scripts/prepare_tagged_version.mjs index 0f869fa789..1203a0da8c 100644 --- a/scripts/prepare_tagged_version.js +++ b/scripts/prepare_tagged_version.mjs @@ -1,14 +1,11 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -const fs = require('node:fs'); -const { execSync } = require('node:child_process'); - -const _ = require('lodash'); -const { - default: packageJson, - version: currentVersion, -} = require('./packageJson.js'); +// @ts-check +import fs from 'node:fs'; +import { execSync } from 'node:child_process'; +import _ from 'lodash'; +import { generateTaggedVersion } from './utils/generateTaggedVersion.mjs'; +import packageJson from '../package.json' with { type: 'json' }; const release = process.argv[2]; if (release !== 'alpha' && release !== 'axolotl' && release !== 'adhoc') { @@ -16,12 +13,11 @@ if (release !== 'alpha' && release !== 'axolotl' && release !== 'adhoc') { process.exit(1); } -const { generateTaggedVersion } = require('../ts/util/version.std.ts'); - const shortSha = execSync('git rev-parse --short=9 HEAD') .toString('utf8') .replace(/[\n\r]/g, ''); +const currentVersion = packageJson.version; const newVersion = generateTaggedVersion({ release, currentVersion, shortSha }); console.log( diff --git a/ts/scripts/prune-macos-release.node.ts b/scripts/prune-macos-release.mjs similarity index 81% rename from ts/scripts/prune-macos-release.node.ts rename to scripts/prune-macos-release.mjs index 525fffb22d..a8a8a5642e 100644 --- a/ts/scripts/prune-macos-release.node.ts +++ b/scripts/prune-macos-release.mjs @@ -1,11 +1,16 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - -import type { AfterPackContext } from 'electron-builder'; +// @ts-check import fs, { readdir, rm } from 'node:fs/promises'; import path from 'node:path'; -async function safeReaddir(dir: string): Promise | null> { +/** @import { AfterPackContext } from 'electron-builder' */ + +/** + * @param {string} dir + * @returns {Promise | null>} + */ +async function safeReaddir(dir) { try { return await readdir(dir); } catch (error) { @@ -16,11 +21,11 @@ async function safeReaddir(dir: string): Promise | null> { } } -export async function afterPack({ - appOutDir, - packager, - electronPlatformName, -}: AfterPackContext): Promise { +/** + * @param {AfterPackContext} context + * @returns {Promise} + */ +export async function afterPack({ appOutDir, packager, electronPlatformName }) { if (electronPlatformName !== 'darwin') { return; } diff --git a/scripts/publish-installer-size.mjs b/scripts/publish-installer-size.mjs new file mode 100644 index 0000000000..a8634ac623 --- /dev/null +++ b/scripts/publish-installer-size.mjs @@ -0,0 +1,62 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { assert } from './utils/assert.mjs'; +import packageJson from '../package.json' with { type: 'json' }; + +const NAME = packageJson.name; +const VERSION = packageJson.version; + +const SUPPORT_CONFIG = new Set([ + 'linux', + 'windows', + 'macos-arm64', + 'macos-x64', + 'macos-universal', +]); + +const RELEASE_DIR = join(import.meta.dirname, '..', 'release'); + +// TODO: DESKTOP-9836 +const config = process.argv[2]; +assert(config != null, 'Missing config arg'); +if (!SUPPORT_CONFIG.has(config)) { + throw new Error(`Invalid argument: ${config}`); +} + +/** @type {string} */ +let fileName; +/** @type {string} */ +let platform; +/** @type {string} */ +let arch; +if (config === 'linux') { + fileName = `${NAME}_${VERSION}_amd64.deb`; + platform = 'linux'; + arch = 'x64'; +} else if (config === 'windows') { + fileName = `${NAME}-win-x64-${VERSION}.exe`; + platform = 'windows'; + arch = 'x64'; +} else if (config === 'macos-arm64') { + fileName = `${NAME}-mac-arm64-${VERSION}.zip`; + platform = 'macos'; + arch = 'arm64'; +} else if (config === 'macos-x64') { + fileName = `${NAME}-mac-x64-${VERSION}.zip`; + platform = 'macos'; + arch = 'x64'; +} else if (config === 'macos-universal') { + fileName = `${NAME}-mac-universal-${VERSION}.dmg`; + platform = 'macos'; + arch = 'universal'; +} else { + throw new Error(`Unsupported config: ${config}`); +} + +const filePath = join(RELEASE_DIR, fileName); +const { size } = await stat(filePath); + +console.log(`${platform} ${arch} release size: ${size}`); diff --git a/scripts/push-strings.mjs b/scripts/push-strings.mjs new file mode 100644 index 0000000000..47fe7edb81 --- /dev/null +++ b/scripts/push-strings.mjs @@ -0,0 +1,60 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { randomBytes } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import { API_BASE, PROJECT_ID, authenticate } from './utils/smartling.mjs'; + +const { SMARTLING_USER, SMARTLING_SECRET } = process.env; + +if (!SMARTLING_USER) { + console.error('Need to set SMARTLING_USER environment variable!'); + process.exit(1); +} +if (!SMARTLING_SECRET) { + console.error('Need to set SMARTLING_SECRET environment variable!'); + process.exit(1); +} + +console.log('Authenticating with Smartling'); +const headers = await authenticate({ + userIdentifier: SMARTLING_USER, + userSecret: SMARTLING_SECRET, +}); + +const boundaryString = randomBytes(32).toString('hex'); + +headers.set('content-type', `multipart/form-data; boundary=${boundaryString}`); + +const url = new URL(`./files-api/v2/projects/${PROJECT_ID}/file`, API_BASE); +const body = [ + `--${boundaryString}`, + 'Content-Disposition: form-data; name="fileUri"', + 'Content-Type: text/plain', + '', + '_locales/en/messages.json', + + `--${boundaryString}`, + 'Content-Disposition: form-data; name="fileType"', + 'Content-Type: text/plain', + '', + 'json', + + `--${boundaryString}`, + 'Content-Disposition: form-data; name="file"; filename="_locales/en/messages.json"', + 'Content-Type: text/plain', + '', + await readFile('_locales/en/messages.json', 'utf8'), + `--${boundaryString}--`, + '', +]; + +console.log('Pushing strings'); +const res = await fetch(url, { + method: 'POST', + headers, + body: body.join('\r\n'), +}); +if (!res.ok) { + throw new Error(`Failed to push strings: ${await res.text()}`); +} diff --git a/scripts/remove-strings.mjs b/scripts/remove-strings.mjs new file mode 100644 index 0000000000..b94d926961 --- /dev/null +++ b/scripts/remove-strings.mjs @@ -0,0 +1,103 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import chalk from 'chalk'; +import execa from 'execa'; +import fs from 'node:fs/promises'; +import pLimit from 'p-limit'; +import path from 'node:path'; +import { DAY } from './utils/durations.mjs'; +import { DELETED_REGEXP } from './utils/intlMessages.mjs'; +import { assert } from './utils/assert.mjs'; + +const ROOT_DIR = path.join(import.meta.dirname, '..'); +const MESSAGES_FILE = path.join(ROOT_DIR, '_locales', 'en', 'messages.json'); + +const MAX_AGE = 30 * DAY; + +const limitter = pLimit(10); + +const removeBefore = Date.now() - MAX_AGE; + +const messages = JSON.parse(await fs.readFile(MESSAGES_FILE, 'utf-8')); + +/** @type {Set} */ +const stillUsed = new Set(); + +await Promise.all( + Object.keys(messages).map(key => + limitter(async () => { + /** @type {{ description?: string }} */ + const value = messages[key]; + + const match = value.description?.match(DELETED_REGEXP); + if (!match) { + return; + } + + const deletedAtStr = match[1]; + assert(deletedAtStr != null, 'Missing deletedAtStr'); + const deletedAt = new Date(deletedAtStr).getTime(); + if (deletedAt <= removeBefore) { + return; + } + + // Find uses in either: + // - `i18n('key')` + // - `` + + try { + const result = await execa( + 'git', + // prettier-ignore + [ + 'grep', + '--extended-regexp', + `'${key}'|id="${key}"`, + '--', + '**', + ':!\\_locales/**', + ':!\\sticker-creator/**', + ], + { + cwd: ROOT_DIR, + stdin: 'ignore', + stdout: 'pipe', + stderr: 'inherit', + } + ); + + // Match found + console.error( + chalk.red( + `ERROR: String is still used: "${key}", deleted on ${match[1]}` + ) + ); + console.error(result.stdout.trim()); + console.error(''); + stillUsed.add(key); + } catch (error) { + if (error.exitCode === 1) { + console.log( + chalk.dim(`Removing string: "${key}", deleted on ${match[1]}`) + ); + delete messages[key]; + } else { + throw error; + } + } + }) + ) +); + +if (stillUsed.size !== 0) { + console.error( + `ERROR: Didn't remove ${stillUsed.size} strings because of errors above`, + Array.from(stillUsed) + .map(str => `- ${str}`) + .join('\n') + ); + console.error('ERROR: Not saving changes'); + process.exit(1); +} +await fs.writeFile(MESSAGES_FILE, `${JSON.stringify(messages, null, 2)}\n`); diff --git a/ts/scripts/sign-macos.node.ts b/scripts/sign-macos.mjs similarity index 64% rename from ts/scripts/sign-macos.node.ts rename to scripts/sign-macos.mjs index 4bb153f9a5..3e510f6840 100644 --- a/ts/scripts/sign-macos.node.ts +++ b/scripts/sign-macos.mjs @@ -1,14 +1,22 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { execSync } from 'node:child_process'; +import { realpath } from 'node:fs/promises'; -import fsExtra from 'fs-extra'; +/** @import { MacConfiguration } from 'electron-builder' */ -const { realpath } = fsExtra; +/** + * Types not exported by electron-builder + * @typedef {Extract} CustomMacSign + * @typedef {Parameters[0]} CustomMacSignOptions + */ -// oxlint-disable-next-line typescript/explicit-module-boundary-types, typescript/no-explicit-any -export async function sign(configuration: any): Promise { +/** + * @param {CustomMacSignOptions} configuration + * @returns {Promise} + */ +export async function sign(configuration) { if (process.env.SKIP_SIGNING_SCRIPT === '1') { console.log('SKIP_SIGNING_SCRIPT=1, skipping custom macOS signing script'); return; diff --git a/ts/scripts/sign-windows.node.ts b/scripts/sign-windows.mjs similarity index 76% rename from ts/scripts/sign-windows.node.ts rename to scripts/sign-windows.mjs index e14b3f7288..b2cd1c3865 100644 --- a/ts/scripts/sign-windows.node.ts +++ b/scripts/sign-windows.mjs @@ -1,17 +1,16 @@ // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { execSync } from 'node:child_process'; +import { realpath } from 'node:fs/promises'; -import fsExtra from 'fs-extra'; +/** @import { CustomWindowsSignTaskConfiguration } from 'electron-builder' */ -import type { CustomWindowsSignTaskConfiguration } from 'electron-builder'; - -const { realpath } = fsExtra; - -export async function sign( - configuration: CustomWindowsSignTaskConfiguration -): Promise { +/** + * @param {CustomWindowsSignTaskConfiguration} configuration + * @returns {Promise} + */ +export async function sign(configuration) { // In CI, we remove certificate information from package.json to disable signing if ( !configuration.options.signtoolOptions || diff --git a/scripts/symbolicate-crash-report.mjs b/scripts/symbolicate-crash-report.mjs new file mode 100644 index 0000000000..376644497b --- /dev/null +++ b/scripts/symbolicate-crash-report.mjs @@ -0,0 +1,111 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { tmpdir } from 'node:os'; +import { readFile, writeFile, mkdtemp } from 'node:fs/promises'; +import { gunzip as gunzipCb } from 'node:zlib'; +import { join, basename } from 'node:path'; +import { promisify } from 'node:util'; +import { symbolicate } from '@electron/symbolicate-mac'; +import pMap from 'p-map'; +import { assert } from './utils/assert.mjs'; + +/// + +const gunzip = promisify(gunzipCb); + +if (!process.argv[2]) { + throw new Error('Usage: node symbolicate-crash-report.js '); +} +const INPUT_FILE = process.argv[2]; + +let file = await readFile(INPUT_FILE); +try { + file = await gunzip(file); +} catch (error) { + console.error('Failed to decompress, perhaps file is a plaintext?'); +} + +const matches = file + .toString() + .matchAll( + /WARN[^\n]*crashReports:\s+dump=\[REDACTED\]([0-9a-z]+).dmp\s+mtime="([\d\-T:.Z]+)"\s+({(\n|.)*?\n})/g + ); + +/** + * @param {string | undefined} filename + * @returns {string} + */ +function moduleName(filename) { + if (!filename) { + return ''; + } + + if (filename.startsWith('signal-desktop-')) { + return 'electron'; + } + + if (filename.startsWith('Signal') && filename.endsWith('.exe')) { + return 'electron.exe.pdb'; + } + return filename; +} + +/** @type {Array} */ +const dumps = []; +for (const match of matches) { + const [, dump, mtime, json] = match; + assert(dump != null, 'Missing dump'); + assert(mtime != null, 'Missing mtime'); + assert(json != null, 'Missing json'); + const out = []; + + let info; + try { + info = JSON.parse(json); + } catch (error) { + console.error('Failed to parse JSON, ignoring', dump, mtime, error); + continue; + } + + out.push(`## dump=${dump} mtime=${mtime}`); + out.push(''); + out.push('```'); + + for (const [index, frame] of info.crashing_thread.frames.entries()) { + out.push(`${index} ${moduleName(frame.module)} ${frame.offset} () + 0`); + } + + out.push(''); + + for (const m of info.modules) { + const filename = moduleName(m.filename); + + out.push( + `${m.base_addr} - ${m.end_addr} ` + + `${filename.replace(/\s+/g, '-')} (${m.version}) ` + + `<${m.debug_id.slice(0, -1)}> ${filename}` + ); + } + out.push('```'); + + dumps.push(out.join('\n')); +} + +const tmpFolder = await mkdtemp(join(tmpdir(), 'parse-crash-reports')); + +const result = await pMap( + dumps, + async (text, i) => { + const tmpFile = join(tmpFolder, `${i}.txt`); + await writeFile(tmpFile, text); + + console.error(`Symbolicating: ${tmpFile}`); + return symbolicate({ file: tmpFile }); + }, + { concurrency: 1 } +); + +console.log(`# Crash Report ${basename(INPUT_FILE)}`); +console.log(''); +console.log(result.join('\n')); diff --git a/ts/scripts/test-electron.node.ts b/scripts/test-electron.mjs similarity index 73% rename from ts/scripts/test-electron.node.ts rename to scripts/test-electron.mjs index 667759b4e7..511e4982ba 100644 --- a/ts/scripts/test-electron.node.ts +++ b/scripts/test-electron.mjs @@ -1,6 +1,6 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { spawn } from 'node:child_process'; import path, { join } from 'node:path'; import { pipeline } from 'node:stream/promises'; @@ -10,15 +10,15 @@ import crypto from 'node:crypto'; import z from 'zod'; import split2 from 'split2'; import logSymbols from 'log-symbols'; +import { unreachable } from './utils/assert.mjs'; +import { SECOND } from './utils/durations.mjs'; -import { explodePromise } from '../util/explodePromise.std.ts'; -import { missingCaseError } from '../util/missingCaseError.std.ts'; -import { SECOND } from '../util/durations/index.std.ts'; -import { parseUnknown } from '../util/schemas.std.ts'; +const ROOT_DIR = join(import.meta.dirname, '..'); -const ROOT_DIR = join(__dirname, '..', '..'); - -function getWorkerCount(): number { +/** + * @returns {number} + */ +function getWorkerCount() { if (process.env.WORKER_COUNT) { return parseInt(process.env.WORKER_COUNT, 10); } @@ -46,7 +46,7 @@ const failureSchema = z.object({ error: z.string(), }); -type Failure = z.infer; +/** @typedef {z.infer} Failure */ const eventSchema = z .object({ @@ -61,10 +61,12 @@ const eventSchema = z }) ); -async function launchElectron( - worker: number, - attempt: number -): Promise<{ pass: number; failures: Array }> { +/** + * @param {number} worker + * @param {number} attempt + * @returns {Promise<{ pass: number; failures: Array }>} + */ +async function launchElectron(worker, attempt) { if (attempt > MAX_RETRIES) { console.error(`Failed after ${MAX_RETRIES} retries, exiting.`); process.exit(1); @@ -108,9 +110,11 @@ async function launchElectron( } ); - const { resolve, reject, promise: exitPromise } = explodePromise(); + /** @type {PromiseWithResolvers} */ + const { resolve, reject, promise: exitPromise } = Promise.withResolvers(); - let exitSignal: string | undefined; + /** @type {string | undefined} */ + let exitSignal; proc.on('exit', (code, signal) => { if (code === 0) { resolve(); @@ -121,7 +125,8 @@ async function launchElectron( }); let pass = 0; - const failures = new Array(); + /** @type {Array} */ + const failures = []; let done = false; try { @@ -149,10 +154,7 @@ async function launchElectron( return; } - const event = parseUnknown( - eventSchema, - JSON.parse(match[1]) as unknown - ); + const event = eventSchema.parse(JSON.parse(match[1])); if (event.type === 'pass') { pass += 1; @@ -174,7 +176,7 @@ async function launchElectron( } else if (event.type === 'end') { done = true; } else { - throw missingCaseError(event); + unreachable(event); } }) ), @@ -211,49 +213,10 @@ async function launchElectron( return { pass, failures }; } -async function main() { - const promises = []; - for (let i = 0; i < WORKER_COUNT; i += 1) { - promises.push(launchElectron(i, 1)); - } - const results = await Promise.all(promises); - - let pass = 0; - let failures = new Array(); - for (const result of results) { - pass += result.pass; - failures = failures.concat(result.failures); - } - - if (failures.length) { - console.error(''); - console.error('Failing tests:'); - console.error(''); - for (const { title, error } of failures) { - console.log(` ${logSymbols.error} ${title.join(' ')}`); - console.log(error); - console.log(''); - } - } - - console.log(''); - console.log( - `Passed ${pass} | Failed ${failures.length} | ` + - `Total ${pass + failures.length}` - ); - - if (failures.length !== 0) { - process.exit(1); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); - -async function makeArtifactsDir(): Promise { +/** + * @returns {Promise} + */ +async function makeArtifactsDir() { const { ARTIFACTS_DIR } = process.env; if (!ARTIFACTS_DIR) { console.log('\nTo save artifacts, please set ARTIFACTS_DIR env variable\n'); @@ -267,3 +230,38 @@ async function makeArtifactsDir(): Promise { return outDir; } + +const promises = []; +for (let i = 0; i < WORKER_COUNT; i += 1) { + promises.push(launchElectron(i, 1)); +} +const results = await Promise.all(promises); + +let pass = 0; +/** @type {Array} */ +let failures = []; +for (const result of results) { + pass += result.pass; + failures = failures.concat(result.failures); +} + +if (failures.length) { + console.error(''); + console.error('Failing tests:'); + console.error(''); + for (const { title, error } of failures) { + console.log(` ${logSymbols.error} ${title.join(' ')}`); + console.log(error); + console.log(''); + } +} + +console.log(''); +console.log( + `Passed ${pass} | Failed ${failures.length} | ` + + `Total ${pass + failures.length}` +); + +if (failures.length !== 0) { + process.exit(1); +} diff --git a/ts/scripts/test-release.node.ts b/scripts/test-release.mjs similarity index 52% rename from ts/scripts/test-release.node.ts rename to scripts/test-release.mjs index 9796684383..03678e7970 100644 --- a/ts/scripts/test-release.node.ts +++ b/scripts/test-release.mjs @@ -1,23 +1,22 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import asar from '@electron/asar'; import assert from 'node:assert'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -import { mkdtemp, cp } from 'node:fs/promises'; +import { mkdtemp, cp, rm } from 'node:fs/promises'; import { constants as fsConstants } from 'node:fs'; import { _electron as electron } from 'playwright'; - -import { packageJson } from '../util/packageJson.node.ts'; -import { gracefulRmRecursive } from '../util/gracefulFs.node.ts'; -import { consoleLogger } from '../util/consoleLogger.std.ts'; +import packageJson from '../package.json' with { type: 'json' }; const ENVIRONMENT = 'production'; -const RELEASE_DIR = join(__dirname, '..', '..', 'release'); +const RELEASE_DIR = join(import.meta.dirname, '..', 'release'); -let archive: string; -let exe: string; +/** @type {string} */ +let archive; +/** @type {string} */ +let exe; if (process.platform === 'darwin') { archive = join( 'mac-arm64', @@ -60,43 +59,40 @@ for (const fileName of files) { } // A simple test to verify a visible window is opened with a title -const main = async () => { - const tmpFolder = await mkdtemp(join(tmpdir(), 'test-release')); - const tmpApp = join(tmpFolder, 'Signal'); - try { - await cp(RELEASE_DIR, tmpApp, { - recursive: true, - mode: fsConstants.COPYFILE_FICLONE, - }); +const tmpFolder = await mkdtemp(join(tmpdir(), 'test-release')); +const tmpApp = join(tmpFolder, 'Signal'); - const executablePath = join(tmpApp, exe); - console.log('Starting path', executablePath); - const app = await electron.launch({ - executablePath, - locale: 'en', - cwd: tmpApp, - }); +try { + await cp(RELEASE_DIR, tmpApp, { + recursive: true, + mode: fsConstants.COPYFILE_FICLONE, + }); - console.log('Waiting for a first window'); - const window = await app.firstWindow(); + const executablePath = join(tmpApp, exe); + console.log('Starting path', executablePath); + const app = await electron.launch({ + executablePath, + locale: 'en', + cwd: tmpApp, + }); - console.log('Waiting for app to fully load'); - await window.waitForSelector( - '.App, .app-loading-screen:has-text("Optimizing")' - ); + console.log('Waiting for a first window'); + const window = await app.firstWindow(); - console.log('Checking window title'); - assert.strictEqual(await window.title(), packageJson.productName); + console.log('Waiting for app to fully load'); + await window.waitForSelector( + '.App, .app-loading-screen:has-text("Optimizing")' + ); - await app.close(); - } finally { - await gracefulRmRecursive(consoleLogger, tmpFolder); - } -}; + console.log('Checking window title'); + assert.strictEqual(await window.title(), packageJson.productName); -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); + await app.close(); +} finally { + await rm(tmpFolder, { + recursive: true, + force: true, + maxRetries: 100, + }); +} diff --git a/ts/scripts/update-gha.node.ts b/scripts/update-gha.mjs similarity index 76% rename from ts/scripts/update-gha.node.ts rename to scripts/update-gha.mjs index bd99e74298..4fcfb6dfd3 100644 --- a/ts/scripts/update-gha.node.ts +++ b/scripts/update-gha.mjs @@ -1,28 +1,21 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - +// @ts-check import { join } from 'node:path'; import { readFile, readdir, writeFile } from 'node:fs/promises'; import z from 'zod'; import semver from 'semver'; - -import { drop } from '../util/drop.std.ts'; -import { strictAssert } from '../util/assert.std.ts'; +import { assert } from './utils/assert.mjs'; const { GITHUB_TOKEN } = process.env; -const WORKFLOWS_DIR = join(__dirname, '..', '..', '.github', 'workflows'); +const WORKFLOWS_DIR = join(import.meta.dirname, '..', '.github', 'workflows'); const REGEXP = /uses:\s*(?[^\s]+)@(?[^\s#]+)(?:\s*#\s*(?[^\n]+))?/g; -type RegexGroups = { - path: string; - ref: string; - originalRef?: string; -}; - -const CACHE = new Map(); +/** @type {Map} */ +const CACHE = new Map(); const TagsSchema = z .object({ @@ -33,12 +26,19 @@ const TagsSchema = z }) .array(); -async function updateAction(fullPath: string): Promise { +/** + * @param {string} fullPath + * @returns {Promise} + */ +async function updateAction(fullPath) { const source = await readFile(fullPath, 'utf8'); for (const { groups } of source.matchAll(REGEXP)) { - strictAssert(groups != null, 'Expected regexp to fully match'); - const { path, ref, originalRef } = groups as RegexGroups; + assert(groups != null, 'Expected regexp to fully match'); + const { path, ref, originalRef } = groups; + assert(path != null, 'Missing path'); + assert(ref != null, 'Missing ref'); + assert(originalRef != null, 'Missing originalRef'); // Skip local actions if (path.startsWith('.')) { @@ -56,7 +56,7 @@ async function updateAction(fullPath: string): Promise { } const [org, repo] = path.split('/', 2); - strictAssert(repo, 'Missing repo'); + assert(repo != null, 'Missing repo'); const url = `https://api.github.com/repos/${encodeURIComponent(org)}/` + @@ -113,12 +113,9 @@ async function updateAction(fullPath: string): Promise { await writeFile(fullPath, result); } -async function main(): Promise { - const actions = await readdir(WORKFLOWS_DIR); +const actions = await readdir(WORKFLOWS_DIR); - for (const name of actions) { - // oxlint-disable-next-line no-await-in-loop - await updateAction(join(WORKFLOWS_DIR, name)); - } +for (const name of actions) { + // oxlint-disable-next-line no-await-in-loop + await updateAction(join(WORKFLOWS_DIR, name)); } -drop(main()); diff --git a/scripts/utils/assert.mjs b/scripts/utils/assert.mjs new file mode 100644 index 0000000000..f06086a6c1 --- /dev/null +++ b/scripts/utils/assert.mjs @@ -0,0 +1,22 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check + +/** + * @param {boolean} condition + * @param {string} message + * @returns {asserts condition} + */ +export function assert(condition, message) { + if (!condition) { + throw new TypeError(message); + } +} + +/** + * @param {never} value + * @returns {never} + */ +export function unreachable(value) { + throw new TypeError(`Expected case to be unreachable, found ${value}`); +} diff --git a/ts/scripts/better-blockmap.d.ts b/scripts/utils/better-blockmap.d.ts similarity index 100% rename from ts/scripts/better-blockmap.d.ts rename to scripts/utils/better-blockmap.d.ts diff --git a/scripts/utils/durations.mjs b/scripts/utils/durations.mjs new file mode 100644 index 0000000000..c877f98f71 --- /dev/null +++ b/scripts/utils/durations.mjs @@ -0,0 +1,8 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check + +export const SECOND = 1000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; diff --git a/scripts/utils/generateTaggedVersion.mjs b/scripts/utils/generateTaggedVersion.mjs new file mode 100644 index 0000000000..9f1a180097 --- /dev/null +++ b/scripts/utils/generateTaggedVersion.mjs @@ -0,0 +1,45 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import * as semver from 'semver'; + +/** + * @typedef {{ + * release: string; + * currentVersion: string; + * shortSha: string; + * }} Options + */ + +/** + * @param {Options} options + * @returns {string} + */ +export function generateTaggedVersion(options) { + const { release, currentVersion, shortSha } = options; + + const parsed = semver.parse(currentVersion); + if (!parsed) { + throw new Error(`generateTaggedVersion: Invalid version ${currentVersion}`); + } + + const dateTimeParts = new Intl.DateTimeFormat('en', { + day: '2-digit', + hour: '2-digit', + hourCycle: 'h23', + month: '2-digit', + timeZone: 'GMT', + year: 'numeric', + }).formatToParts(new Date()); + const dateTimeMap = new Map(); + dateTimeParts.forEach(({ type, value }) => { + dateTimeMap.set(type, value); + }); + const formattedDate = `${dateTimeMap.get('year')}${dateTimeMap.get( + 'month' + )}${dateTimeMap.get('day')}.${dateTimeMap.get('hour')}`; + + const formattedVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`; + + return `${formattedVersion}-${release}.${formattedDate}-${shortSha}`; +} diff --git a/scripts/utils/generateTaggedVersion_test.mjs b/scripts/utils/generateTaggedVersion_test.mjs new file mode 100644 index 0000000000..4994837a42 --- /dev/null +++ b/scripts/utils/generateTaggedVersion_test.mjs @@ -0,0 +1,108 @@ +// Copyright 2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { assert } from 'chai'; +import { useFakeTimers } from 'sinon'; +import * as semver from 'semver'; +import { generateTaggedVersion } from './generateTaggedVersion.mjs'; + +/** @import { SinonFakeTimers } from 'sinon' */ + +describe('generateTaggedVersion', () => { + /** @type {SinonFakeTimers} */ + let clock; + + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('uses the current date and provided shortSha', () => { + clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + const expected = '5.12.0-alpha.20210723.01-07f0efc45'; + const actual = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + assert.strictEqual(expected, actual); + }); + + it('same production version is semver.gt', () => { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actual = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + assert.isTrue(semver.gt('5.12.0', actual)); + }); + + it('same beta version is semver.gt', () => { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actual = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + assert.isTrue(semver.gt(currentVersion, actual)); + }); + + it('build earlier same day is semver.lt', () => { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + clock.setSystemTime(new Date('2021-07-23T00:22:55.692Z').getTime()); + const actualEarlier = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actualLater = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + assert.isTrue(semver.lt(actualEarlier, actualLater)); + }); + + it('build previous day is semver.lt', () => { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + clock.setSystemTime(new Date('2021-07-22T01:22:55.692Z').getTime()); + const actualEarlier = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actualLater = generateTaggedVersion({ + release: 'alpha', + currentVersion, + shortSha, + }); + + assert.isTrue(semver.lt(actualEarlier, actualLater)); + }); +}); diff --git a/ts/util/getICUMessageParams.std.ts b/scripts/utils/getICUMessageParams.mjs similarity index 67% rename from ts/util/getICUMessageParams.std.ts rename to scripts/utils/getICUMessageParams.mjs index c7bf5f69b9..1feccbb733 100644 --- a/ts/util/getICUMessageParams.std.ts +++ b/scripts/utils/getICUMessageParams.mjs @@ -1,36 +1,44 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import { TYPE, parse } from '@formatjs/icu-messageformat-parser' +import { unreachable } from './assert.mjs'; -import { TYPE, parse } from '@formatjs/icu-messageformat-parser'; -import type { - MessageFormatElement, - PluralOrSelectOption, -} from '@formatjs/icu-messageformat-parser'; -import { missingCaseError } from './missingCaseError.std.ts'; +/** @import { MessageFormatElement, PluralOrSelectOption } from '@formatjs/icu-messageformat-parser' */ -export type ICUMessageParamType = Readonly< - | { - type: 'string' | 'date' | 'number' | 'jsx' | 'time'; - } - | { - type: 'select'; - validOptions: ReadonlyArray; - } ->; +/** + * @typedef {Readonly< + * | { + * type: 'string' | 'date' | 'number' | 'jsx' | 'time'; + * } + * | { + * type: 'select'; + * validOptions: ReadonlyArray; + * } + * >} ICUMessageParamType + */ -export function getICUMessageParams( - message: string, - defaultRichTextElementNames: Array = [] -): Map { +/** + * @param {string} message + * @param {string[]} defaultRichTextElementNames + * @returns {Map} + */ +export function getICUMessageParams(message, defaultRichTextElementNames = []) { const params = new Map(); - function visitOptions(options: Record) { + /** + * @param {Record} options + */ + function visitOptions(options) { for (const option of Object.values(options)) { visit(option.value); } } - function visit(elements: ReadonlyArray) { + /** + * @param {ReadonlyArray} elements + */ + function visit(elements) { for (const element of elements) { switch (element.type) { case TYPE.argument: @@ -68,7 +76,7 @@ export function getICUMessageParams( params.set(element.value, { type: 'time' }); break; default: - throw missingCaseError(element); + unreachable(element); } } } diff --git a/ts/scripts/constants.std.ts b/scripts/utils/intlMessages.mjs similarity index 72% rename from ts/scripts/constants.std.ts rename to scripts/utils/intlMessages.mjs index 7abd6ded08..61127ce305 100644 --- a/ts/scripts/constants.std.ts +++ b/scripts/utils/intlMessages.mjs @@ -1,5 +1,5 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check -export type DeletedMatch = RegExpMatchArray & { 1: string }; export const DELETED_REGEXP = /\(\s*deleted\s+(\d{2,4}\/\d{2}\/\d{2,4})\s*\)/i; diff --git a/ts/util/nsis.std.ts b/scripts/utils/nsis.mjs similarity index 96% rename from ts/util/nsis.std.ts rename to scripts/utils/nsis.mjs index 9fd1122e54..cc8f6af04e 100644 --- a/ts/util/nsis.std.ts +++ b/scripts/utils/nsis.mjs @@ -1,7 +1,9 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check // https://github.com/electron-userland/electron-builder/blob/c1448c6584bb5106aaa2766379fd0917508ce3bf/packages/app-builder-lib/src/util/langs.ts#L1 +/** @type {Array} */ export const REQUIRED_LANGUAGES = [ 'en_US', 'de_DE', @@ -32,7 +34,8 @@ export const REQUIRED_LANGUAGES = [ ]; // https://github.com/electron-userland/electron-builder/blob/c1448c6584bb5106aaa2766379fd0917508ce3bf/packages/app-builder-lib/src/util/langs.ts#L48 -export const LCID: Record = { +/** @satisfies {Record} */ +export const LCID = { af_ZA: 1078, am_ET: 1118, ar_AE: 14337, diff --git a/scripts/utils/optionalResources.mjs b/scripts/utils/optionalResources.mjs new file mode 100644 index 0000000000..ad958ec564 --- /dev/null +++ b/scripts/utils/optionalResources.mjs @@ -0,0 +1,19 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import z from 'zod'; + +export const OptionalResourceSchema = z.object({ + digest: z.string(), + url: z.string(), + size: z.number(), +}); + +/** @typedef {z.infer} OptionalResourceType */ + +export const OptionalResourcesDictSchema = z.record( + z.string(), + OptionalResourceSchema +); + +/** @typedef {z.infer} OptionalResourcesDictType */ diff --git a/scripts/utils/parseVersion.mjs b/scripts/utils/parseVersion.mjs new file mode 100644 index 0000000000..029803d779 --- /dev/null +++ b/scripts/utils/parseVersion.mjs @@ -0,0 +1,76 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +import * as semver from 'semver'; +import { assert } from './assert.mjs'; + +/** + * @typedef {'prod' | 'beta' | 'alpha' | 'staging' | 'axolotl' | 'adhoc'} VersionChannel + */ + +/** + * @typedef {object} VersionInfo + * @prop {VersionChannel} channel + * @prop {number} major + * @prop {number} minor + * @prop {number} patch + * @prop {number | null} prepatch + * @prop {string[]} build + * @prop {boolean} isUpdatable + * @prop {boolean} isNightly + */ + +/** + * @param {string} version + * @returns {VersionInfo} + */ +export function parseVersion(version) { + const parsed = semver.parse(version); + assert(parsed != null, `Invalid version: ${version}`); + + const [pre1, pre2] = parsed.prerelease; + + /** @type {VersionChannel} */ + let channel; + /** @type {number | null} */ + let prepatch; + if (pre1 == null) { + channel = 'prod'; + prepatch = null; + assert( + parsed.build.length === 0, + `Unexpected build info in "prod" version: "${version}"` + ); + } else if ( + pre1 === 'beta' || + pre1 === 'alpha' || + pre1 === 'staging' || + pre1 === 'axolotl' || + pre1 === 'adhoc' + ) { + channel = pre1; + assert( + typeof pre2 === 'number', + `Expected channel "${channel}" to have prepatch number in version: "${version}"` + ); + prepatch = pre2; + } else { + throw new TypeError( + `Unexpected channel "${pre1}" in version: "${version}"` + ); + } + + const isUpdatable = channel !== 'adhoc'; + const isNightly = channel === 'alpha' || channel === 'axolotl'; + + return { + channel, + major: parsed.major, + minor: parsed.minor, + patch: parsed.patch, + prepatch, + build: [...parsed.build], + isUpdatable, + isNightly, + }; +} diff --git a/ts/util/smartling.node.ts b/scripts/utils/smartling.mjs similarity index 64% rename from ts/util/smartling.node.ts rename to scripts/utils/smartling.mjs index f6fabb4db1..d630554923 100644 --- a/ts/util/smartling.node.ts +++ b/scripts/utils/smartling.mjs @@ -1,21 +1,22 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// @ts-check -import { packageJson } from './packageJson.node.ts'; -import { getUserAgent } from './getUserAgent.node.ts'; - -type AuthenticateOptionsType = Readonly<{ - userIdentifier: string; - userSecret: string; -}>; +/** + * @typedef {Readonly<{ + * userIdentifier: string; + * userSecret: string; + * }>} AuthenticateOptionsType + */ export const API_BASE = new URL('https://api.smartling.com/'); export const PROJECT_ID = 'ef62d1ebb'; -export async function authenticate({ - userIdentifier, - userSecret, -}: AuthenticateOptionsType): Promise { +/** + * @param {AuthenticateOptionsType} options + * @returns {Promise} + */ +export async function authenticate({ userIdentifier, userSecret }) { const res = await fetch(new URL('./auth-api/v2/authenticate', API_BASE), { method: 'POST', headers: { @@ -36,6 +37,5 @@ export async function authenticate({ return new Headers({ authorization: `${auth.tokenType} ${auth.accessToken}`, - 'user-agent': getUserAgent(packageJson.version), }); } diff --git a/ts/scripts/symbolicate-mac.d.ts b/scripts/utils/symbolicate-mac.d.ts similarity index 100% rename from ts/scripts/symbolicate-mac.d.ts rename to scripts/utils/symbolicate-mac.d.ts diff --git a/scripts/utils/utf16ToEmoji.mjs b/scripts/utils/utf16ToEmoji.mjs new file mode 100644 index 0000000000..a4bd0fa12b --- /dev/null +++ b/scripts/utils/utf16ToEmoji.mjs @@ -0,0 +1,17 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check + +/** + * @param {string} utf16 + * @returns {string} + */ +export function utf16ToEmoji(utf16) { + /** @type {Array} */ + const codePoints = []; + const buf = Buffer.from(utf16, 'hex'); + for (let i = 0; i < buf.length; i += 2) { + codePoints.push(buf.readUint16BE(i)); + } + return String.fromCodePoint(...codePoints); +} diff --git a/test/index.html b/test/index.html index 65b1c07e74..de473c920b 100644 --- a/test/index.html +++ b/test/index.html @@ -4,7 +4,9 @@ - TextSecure test runner + + Signal Desktop (The Artist Formerly Known as TextSecure) test runner + @@ -16,6 +18,6 @@ type="text/javascript" src="../node_modules/mocha/mocha.js" > - + diff --git a/test/test.js b/test/test.js deleted file mode 100644 index eea4157231..0000000000 --- a/test/test.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2014 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* - * global helpers for tests - */ - -mocha.setup('bdd'); -mocha.setup({ timeout: 10000 }); - -let themeSetting = 'light'; - -window.Events = { - getThemeSetting: () => themeSetting, - setThemeSetting: (newSetting) => { - themeSetting = newSetting; - }, - addDarkOverlay: () => undefined, -}; - -/* Delete the database before running any tests */ -before(async () => { - await window.testUtilities.initialize(); -}); - -window.testUtilities.prepareTests(); -delete window.testUtilities.prepareTests; - -// oxlint-disable-next-line no-unused-expressions -!(function () { - // oxlint-disable-next-line no-undef - class Reporter extends Mocha.reporters.HTML { - constructor(runner, options) { - super(runner, options); - - runner.on('pass', test => window.testUtilities.onTestEvent({ - type: 'pass', - title: test.titlePath(), - duration: test.duration, - })); - runner.on('fail', (test, error) => window.testUtilities.onTestEvent({ - type: 'fail', - title: test.titlePath(), - error: error?.stack || String(error), - })); - - runner.on('end', () => window.testUtilities.onTestEvent({ type: 'end' })); - } - } - - mocha.reporter(Reporter); - - mocha.setup(window.testUtilities.setup); - - mocha.run(); -})(); diff --git a/test/test.mjs b/test/test.mjs new file mode 100644 index 0000000000..56701dcecc --- /dev/null +++ b/test/test.mjs @@ -0,0 +1,56 @@ +// Copyright 2014 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// @ts-check +/// + +/* + * global helpers for tests + */ + +mocha.setup('bdd'); +mocha.setup({ timeout: 10000 }); + +let themeSetting = 'light'; + +window.Events = { + getThemeSetting: () => themeSetting, + setThemeSetting: (newSetting) => { + themeSetting = newSetting; + }, + addDarkOverlay: () => undefined, +}; + +/* Delete the database before running any tests */ +before(async () => { + await window.testUtilities.initialize(); +}); + +window.testUtilities.prepareTests(); +delete window.testUtilities.prepareTests; + + +// oxlint-disable-next-line no-undef +class Reporter extends Mocha.reporters.HTML { + constructor(runner, options) { + super(runner, options); + + runner.on('pass', test => window.testUtilities.onTestEvent({ + type: 'pass', + title: test.titlePath(), + duration: test.duration, + })); + runner.on('fail', (test, error) => window.testUtilities.onTestEvent({ + type: 'fail', + title: test.titlePath(), + error: error?.stack || String(error), + })); + + runner.on('end', () => window.testUtilities.onTestEvent({ type: 'end' })); + } +} + +mocha.reporter(Reporter); + +mocha.setup(window.testUtilities.setup); + +mocha.run(); diff --git a/ts/scripts/.eslintrc.js b/ts/scripts/.eslintrc.js deleted file mode 100644 index 5e8c6b7d77..0000000000 --- a/ts/scripts/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -module.exports = { - rules: { - 'no-console': 'off', - - // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - }, - ], - }, -}; diff --git a/ts/scripts/after-all-artifact-build.node.ts b/ts/scripts/after-all-artifact-build.node.ts deleted file mode 100644 index 2e382253d8..0000000000 --- a/ts/scripts/after-all-artifact-build.node.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { BuildResult } from 'electron-builder'; -import { afterAllArtifactBuild as notarizeUniversalDMG } from './notarize-universal-dmg.node.ts'; - -export async function afterAllArtifactBuild( - result: BuildResult -): Promise> { - await notarizeUniversalDMG(result); - return []; -} diff --git a/ts/scripts/after-pack.node.ts b/ts/scripts/after-pack.node.ts deleted file mode 100644 index cd6f47b944..0000000000 --- a/ts/scripts/after-pack.node.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { AfterPackContext } from 'electron-builder'; -import { afterPack as fuseElectron } from './fuse-electron.node.ts'; -import { afterPack as copyPacks } from './copy-language-packs.node.ts'; -import { afterPack as pruneMacOSRelease } from './prune-macos-release.node.ts'; -import { afterPack as ensureLinuxFilePermissions } from './ensure-linux-file-permissions.node.ts'; - -export async function afterPack(context: AfterPackContext): Promise { - await pruneMacOSRelease(context); - await fuseElectron(context); - await copyPacks(context); - await ensureLinuxFilePermissions(context); -} diff --git a/ts/scripts/after-sign.node.ts b/ts/scripts/after-sign.node.ts deleted file mode 100644 index bc96864263..0000000000 --- a/ts/scripts/after-sign.node.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { AfterPackContext } from 'electron-builder'; -import { afterSign as notarize } from './notarize.node.ts'; - -// NOTE: It is AfterPackContext here even though it is afterSign. -// See: https://www.electron.build/configuration/configuration.html#aftersign -export async function afterSign(context: AfterPackContext): Promise { - // This must be the last step - await notarize(context); -} diff --git a/ts/scripts/check-upgradeable-deps.node.ts b/ts/scripts/check-upgradeable-deps.node.ts deleted file mode 100644 index f2862225f6..0000000000 --- a/ts/scripts/check-upgradeable-deps.node.ts +++ /dev/null @@ -1,348 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { join } from 'node:path'; -import { readFile } from 'node:fs/promises'; -import chalk from 'chalk'; -import semver from 'semver'; -import { got, HTTPError } from 'got'; -import enquirer from 'enquirer'; -import execa from 'execa'; - -const rootDir = join(__dirname, '..', '..'); - -function assert(condition: unknown, message: string): asserts condition { - if (!condition) { - throw new Error(message); - } -} - -// oxlint-disable-next-line typescript/no-explicit-any -async function readJsonFile(path: string): Promise { - return JSON.parse(await readFile(path, 'utf-8')); -} - -function parseNumberField(value: string | number | null | void): number | null { - if (value == null) { - return null; - } - if (typeof value === 'number') { - return value; - } - const trimmed = value.trim(); - if (trimmed === '') { - return null; - } - const parsed = Number(value); - if (!Number.isFinite(parsed)) { - return null; - } - return parsed; -} - -const npm = got.extend({ - prefixUrl: 'https://registry.npmjs.org/', - responseType: 'json', - retry: { - calculateDelay: retry => { - if ( - retry.error instanceof HTTPError && - retry.error.response.statusCode === 429 - ) { - const retryAfter = parseNumberField( - retry.error.response.headers['retry-after'] - ); - if (retryAfter != null) { - console.log( - chalk.gray(`Rate limited, retrying after ${retryAfter} seconds`) - ); - return retryAfter * 1000; - } - } - - return retry.computedValue; - }, - }, -}); - -const DependencyTypes = [ - 'dependencies', - 'devDependencies', - 'optionalDependencies', - 'peerDependencies', -] as const; - -type LocalDependency = Readonly<{ - name: string; - depType: (typeof DependencyTypes)[number]; - requestedVersion: string; - resolvedVersion: string; -}>; - -type FetchedDependency = LocalDependency & - Readonly<{ - latestVersion: string; - moduleType: 'commonjs' | 'esm'; - diff: semver.ReleaseType | null; - }>; - -async function main() { - const packageJson = await readJsonFile(join(rootDir, 'package.json')); - const packageLock = await readJsonFile(join(rootDir, 'package-lock.json')); - - const localDeps: ReadonlyArray = DependencyTypes.flatMap( - depType => { - return Object.keys(packageJson[depType] ?? {}).map(name => { - const requestedVersion = packageJson[depType][name]; - const resolvedVersion = - packageLock.packages[`node_modules/${name}`]?.version; - assert(resolvedVersion, `Could not find resolved version for ${name}`); - - return { name, depType, requestedVersion, resolvedVersion }; - }); - } - ); - - console.log(chalk`Found {cyan ${localDeps.length}} local dependencies`); - - const fetchedDeps: ReadonlyArray = await Promise.all( - localDeps.map(async dep => { - // oxlint-disable-next-line typescript/no-explicit-any - const info: any = await npm(`${dep.name}/latest`).json(); - const latestVersion = info.version; - const moduleType = info.type ?? 'commonjs'; - assert( - moduleType === 'commonjs' || moduleType === 'module', - `Unexpected module type for ${dep.name}: ${moduleType}` - ); - const diff = semver.lt(dep.resolvedVersion, latestVersion) - ? semver.diff(dep.resolvedVersion, latestVersion) - : null; - return { ...dep, latestVersion, moduleType, diff }; - }) - ); - - const outdatedDeps = fetchedDeps.filter(dep => dep.diff != null); - console.log(chalk`Found {cyan ${outdatedDeps.length}} outdated dependencies`); - - const upgradeableDeps = outdatedDeps.filter(dep => { - return dep.moduleType === 'commonjs'; - }); - - console.log( - chalk`Found {cyan ${upgradeableDeps.length}} upgradeable dependencies` - ); - - const upgradeableDepsByDiff = new Map>(); - - for (const dep of upgradeableDeps) { - assert(dep.diff != null, 'Expected diff to be non-null'); - - let group = upgradeableDepsByDiff.get(dep.diff); - if (group == null) { - group = new Set(); - upgradeableDepsByDiff.set(dep.diff, group); - } - - group.add(dep.name); - } - - for (const [diff, deps] of upgradeableDepsByDiff) { - console.log(chalk` - ${diff}: {cyan ${deps.size}}`); - } - - let longestNameLength = 0; - for (const dep of upgradeableDeps) { - longestNameLength = Math.max(longestNameLength, dep.name.length); - } - - const { approvedDeps } = await enquirer.prompt<{ - approvedDeps: ReadonlyArray; - }>({ - type: 'multiselect', - name: 'approvedDeps', - message: 'Select which dependencies to upgrade', - choices: upgradeableDeps.map(deps => { - let color = chalk.red; - if (deps.diff === 'patch') { - color = chalk.green; - } else if (deps.diff === 'minor') { - color = chalk.yellow; - } - - return { - name: deps.name, - message: `${deps.name.padEnd(longestNameLength)}`, - hint: `(${color(deps.diff)}: ${deps.resolvedVersion} -> ${color(deps.latestVersion)})`, - }; - }), - }); - - console.log( - chalk`Starting upgrade of {cyan ${approvedDeps.length}} dependencies` - ); - - for (const dep of upgradeableDeps) { - try { - if (!approvedDeps.includes(dep.name)) { - console.log(chalk`Skipping ${dep.name}`); - continue; - } - - // oxlint-disable-next-line no-await-in-loop - const gitStatusBefore = await execa('git', ['status', '--porcelain']); - if (gitStatusBefore.stdout.trim() !== '') { - console.error(chalk`{red Found uncommitted changes, exiting}`); - console.error(chalk.red(gitStatusBefore.stdout)); - process.exit(1); - } - - console.log( - chalk`Upgrading {cyan ${dep.name}} from {yellow ${dep.resolvedVersion}} to {magenta ${dep.latestVersion}}` - ); - // oxlint-disable-next-line no-await-in-loop - await execa( - 'npm', - ['install', '--save-exact', `${dep.name}@${dep.latestVersion}`], - { stdio: 'inherit' } - ); - - // oxlint-disable-next-line no-constant-condition - while (true) { - try { - // oxlint-disable-next-line no-await-in-loop - await execa( - 'npx', - ['patch-package', '--error-on-fail', '--error-on-warn'], - { stdio: 'inherit' } - ); - break; - } catch { - // oxlint-disable-next-line no-await-in-loop - const { retry } = await enquirer.prompt<{ retry: boolean }>({ - type: 'confirm', - name: 'retry', - message: 'Retry patch-package?', - initial: true, - }); - - if (!retry) { - throw new Error('Failed to apply patch-package'); - } - } - } - - // oxlint-disable-next-line no-await-in-loop - const { npmScriptsToRun } = await enquirer.prompt<{ - npmScriptsToRun: Array; - }>({ - type: 'multiselect', - name: 'npmScriptsToRun', - message: 'Select which scripts to run', - choices: [ - // Fast and common - { name: 'oxlint' }, - { name: 'test-node' }, - { name: 'test-electron' }, - // Long - { name: 'test-mock' }, - // Uncommon - { name: 'test-oxlint' }, - { name: 'test-lint-intl' }, - ], - }); - - const allNpmScriptToRun = [ - // Mandatory - 'generate', - 'check:types', - 'lint-deps', - // Optional - ...npmScriptsToRun, - ]; - - for (const script of allNpmScriptToRun) { - console.log(chalk`Running {cyan npm run ${script}}`); - - // oxlint-disable-next-line no-constant-condition - while (true) { - try { - // oxlint-disable-next-line no-await-in-loop - await execa('npm', ['run', script], { stdio: 'inherit' }); - break; - } catch (error) { - console.log( - chalk.red( - `Failed to run ${script}, you could go make changes and try again` - ) - ); - - // oxlint-disable-next-line no-await-in-loop - const { retry } = await enquirer.prompt<{ - retry: boolean; - }>({ - type: 'confirm', - name: 'retry', - message: 'Retry running script?', - initial: true, - }); - - if (!retry) { - throw error; - } else { - console.log(chalk`Retrying {cyan npm run ${script}}`); - continue; - } - } - } - } - - console.log('Changes after upgrade:'); - // oxlint-disable-next-line no-await-in-loop - await execa('git', ['status', '--porcelain'], { stdio: 'inherit' }); - - // oxlint-disable-next-line no-await-in-loop - const { commitChanges } = await enquirer.prompt<{ - commitChanges: boolean; - }>({ - type: 'select', - name: 'commitChanges', - message: 'Commit these changes?', - choices: [ - { name: 'commit', message: 'Commit and continue', value: true }, - { name: 'revert', message: 'Revert and skip', value: false }, - ], - }); - - if (!commitChanges) { - console.log('Reverting changes, and skipping'); - // oxlint-disable-next-line no-await-in-loop - await execa('git', ['checkout', '.']); - continue; - } - - console.log('Committing changes'); - // oxlint-disable-next-line no-await-in-loop - await execa('git', ['add', '.']); - // oxlint-disable-next-line no-await-in-loop - await execa('git', [ - 'commit', - '-m', - `Upgrade ${dep.name} ${dep.depType} from ${dep.requestedVersion} to ${dep.latestVersion}`, - ]); - } catch (error) { - console.error(chalk.red(error)); - console.log( - chalk.red(`Failed to upgrade ${dep.name}, reverting and skipping`) - ); - // oxlint-disable-next-line no-await-in-loop - await execa('git', ['checkout', '.']); - } - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/scripts/compile-stories-icu-lookup.node.ts b/ts/scripts/compile-stories-icu-lookup.node.ts deleted file mode 100644 index b4f1cdca71..0000000000 --- a/ts/scripts/compile-stories-icu-lookup.node.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { readFile, writeFile, readdir, readlink } from 'node:fs/promises'; -import { join, basename } from 'node:path'; -import pMap from 'p-map'; - -import { drop } from '../util/drop.std.ts'; - -async function main(): Promise { - const source = process.argv[2]; - if (!source) { - throw new Error('Missing required source directory argument'); - } - - const dirEntries = await readdir(join(source, 'components'), { - withFileTypes: true, - recursive: true, - }); - - const enMessages = JSON.parse( - await readFile( - join(__dirname, '..', '..', '_locales', 'en', 'messages.json'), - 'utf8' - ) - ); - - const icuToStory: Record> = Object.create( - null - ); - - await pMap( - dirEntries, - async entry => { - if (!entry.isSymbolicLink()) { - return; - } - - const fullPath = join(entry.parentPath, entry.name); - const image = basename(await readlink(fullPath)); - - const storyId = basename(entry.parentPath); - const linkFile = entry.name; - - const icuId = `icu:${basename(linkFile, '.jpg')}`; - - let list = icuToStory[icuId]; - if (list == null) { - list = []; - icuToStory[icuId] = list; - } - list.push([storyId, image]); - }, - { concurrency: 20 } - ); - - const index = Object.entries(icuToStory).map(([key, icuIds]) => { - return [key, enMessages[key]?.messageformat, icuIds]; - }); - const html = await readFile( - join(__dirname, '..', '..', '.storybook', 'icu-lookup.html'), - 'utf8' - ); - await writeFile( - join(source, 'index.html'), - html.replace('%INDEX%', JSON.stringify(index)) - ); -} - -drop(main()); diff --git a/ts/scripts/gen-locales-config.node.ts b/ts/scripts/gen-locales-config.node.ts deleted file mode 100644 index 55238dd91f..0000000000 --- a/ts/scripts/gen-locales-config.node.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import fs from 'node:fs/promises'; -import path from 'node:path'; -import fastGlob from 'fast-glob'; -import * as LocaleMatcher from '@formatjs/intl-localematcher'; - -const ROOT_DIR = path.join(__dirname, '..', '..'); - -function matches(input: string, expected: string) { - const match = LocaleMatcher.match([input], [expected], 'en', { - algorithm: 'best fit', - }); - return match === expected; -} - -async function main() { - const dirEntries = await fastGlob('_locales/*', { - cwd: ROOT_DIR, - onlyDirectories: true, - }); - - const localeDirNames = []; - - for (const dirEntry of dirEntries) { - const dirName = path.basename(dirEntry); - const locale = new Intl.Locale(dirName); - - // Smartling doesn't always use the correct language tag, so this check and - // reverse check are to make sure we don't accidentally add a locale that - // doesn't match its directory name (using LocaleMatcher). - // - // If this check ever fails, we may need to update our get-strings script to - // manually rename language tags before writing them to disk. - // - // Such is the case for Smartling's "zh-YU" locale, which we renamed to - // "yue" to match the language tag used by... everyone else. - - if (!matches(dirName, locale.baseName)) { - throw new Error( - `Matched locale "${dirName}" does not match its resolved name "${locale.baseName}"` - ); - } - if (!matches(locale.baseName, dirName)) { - throw new Error( - `Matched locale "${dirName}" does not match its dir name "${dirName}"` - ); - } - - localeDirNames.push(dirName); - } - - const jsonPath = path.join(ROOT_DIR, 'build', 'available-locales.json'); - console.log(`Writing to "${jsonPath}"...`); - await fs.writeFile(jsonPath, `${JSON.stringify(localeDirNames, null, 2)}\n`); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/scripts/gen-policy-files.node.ts b/ts/scripts/gen-policy-files.node.ts deleted file mode 100644 index 9a83b6c237..0000000000 --- a/ts/scripts/gen-policy-files.node.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import fs from 'node:fs/promises'; -import path from 'node:path'; -import fastGlob from 'fast-glob'; - -import { strictAssert } from '../util/assert.std.ts'; - -const ROOT_DIR = path.join(__dirname, '..', '..'); - -async function main() { - const dirEntries = await fastGlob('_locales/*', { - cwd: ROOT_DIR, - onlyDirectories: true, - }); - - const english = JSON.parse( - await fs.readFile(path.join(ROOT_DIR, '_locales/en/messages.json'), 'utf8') - ); - const templateDir = path.join(ROOT_DIR, 'build/policy-templates'); - const templates = [ - { - name: 'org.signalapp.enable-backups.policy', - content: await fs.readFile( - path.join(templateDir, 'org.signalapp.enable-backups.policy'), - 'utf8' - ), - description: - 'icu:Preferences__local-backups--enable--os-prompt-description--linux', - message: - 'icu:Preferences__local-backups--enable--os-prompt-message--linux', - }, - { - name: 'org.signalapp.plaintext-export.policy', - content: await fs.readFile( - path.join(templateDir, 'org.signalapp.plaintext-export.policy'), - 'utf8' - ), - description: 'icu:PlaintextExport--OSPrompt--description--Linux', - message: 'icu:PlaintextExport--OSPrompt--message--Linux', - }, - { - name: 'org.signalapp.view-aep.policy', - content: await fs.readFile( - path.join(templateDir, 'org.signalapp.view-aep.policy'), - 'utf8' - ), - description: - 'icu:Preferences--local-backups--view-recovery-key--os-prompt-description--linux', - message: - 'icu:Preferences--local-backups--view-recovery-key--os-prompt-message--linux', - }, - ]; - - for (const template of templates) { - const englishDescription = english[template.description]?.messageformat; - strictAssert( - englishDescription, - `Must have english string for key ${template.description}` - ); - const englishMessage = english[template.message]?.messageformat; - strictAssert( - englishMessage, - `Must have english string for key ${template.message}` - ); - - let allDescriptions = `${englishDescription}\n`; - let allMessages = `${englishMessage}\n`; - - for (const dirEntry of dirEntries) { - const locale = path.basename(dirEntry); - const data = JSON.parse( - // oxlint-disable-next-line no-await-in-loop - await fs.readFile( - path.join(ROOT_DIR, '_locales', locale, 'messages.json'), - 'utf8' - ) - ); - - const localeName = locale.replace('-', '_'); - const description = - data[template.description]?.messageformat ?? englishDescription; - allDescriptions += ` ${description}\n`; - - const message = data[template.message]?.messageformat ?? englishMessage; - allMessages += ` ${message}\n`; - } - - const targetPath = path.join(ROOT_DIR, 'build', template.name); - let targetContent = template.content; - - targetContent = targetContent.replace( - '', - allDescriptions - ); - targetContent = targetContent.replace( - '', - allMessages - ); - - // oxlint-disable-next-line no-await-in-loop - await fs.writeFile(targetPath, targetContent); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/scripts/generate-compact-locales.node.ts b/ts/scripts/generate-compact-locales.node.ts deleted file mode 100644 index f10e10cdc2..0000000000 --- a/ts/scripts/generate-compact-locales.node.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { readdir, mkdir, readFile, writeFile } from 'node:fs/promises'; -import { join, dirname } from 'node:path'; -import pMap from 'p-map'; -import { isLocaleMessageType } from '../util/setupI18nMain.std.ts'; - -async function compact({ - sourceDir, - targetDir, - locale, - keys, -}: { - sourceDir: string; - targetDir: string; - locale: string; - keys: ReadonlyArray; -}): Promise> { - const sourcePath = join(sourceDir, locale, 'messages.json'); - const targetPath = join(targetDir, locale, 'values.json'); - - await mkdir(dirname(targetPath), { recursive: true }); - - const json = JSON.parse(await readFile(sourcePath, 'utf8')); - - const result = new Array(); - for (const key of keys) { - if (json[key] == null) { - // Pull English translation, or leave blank (string was deleted) - result.push(null); - continue; - } - - const value = json[key]; - if (!isLocaleMessageType(value)) { - continue; - } - if (value.messageformat == null) { - continue; - } - result.push(value.messageformat); - } - - await writeFile(targetPath, JSON.stringify(result)); - - return keys; -} - -async function main(): Promise { - const rootDir = join(__dirname, '..', '..'); - const sourceDir = join(rootDir, '_locales'); - const targetDir = join(rootDir, 'build', 'compact-locales'); - - const locales = await readdir(sourceDir); - - const allKeys = await pMap( - locales, - async locale => { - const sourcePath = join(sourceDir, locale, 'messages.json'); - const json = JSON.parse(await readFile(sourcePath, 'utf8')); - return Object.entries(json) - .filter(([, value]) => isLocaleMessageType(value)) - .map(([key]) => key); - }, - { concurrency: 10 } - ); - - // Sort keys alphabetically for better incremental updates. - const keys = Array.from(new Set(allKeys.flat())).sort(); - await mkdir(targetDir, { recursive: true }); - await writeFile(join(targetDir, 'keys.json'), JSON.stringify(keys)); - - await pMap( - locales, - locale => compact({ sourceDir, targetDir, locale, keys }), - { concurrency: 10 } - ); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/generate-dns-fallback.node.ts b/ts/scripts/generate-dns-fallback.node.ts deleted file mode 100644 index 4df35e62be..0000000000 --- a/ts/scripts/generate-dns-fallback.node.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { resolve4 as resolve4Cb, resolve6 as resolve6Cb } from 'node:dns'; -import { writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { promisify } from 'node:util'; - -import { isNotNil } from '../util/isNotNil.std.ts'; - -const resolve4 = promisify(resolve4Cb); -const resolve6 = promisify(resolve6Cb); - -const FALLBACK_DOMAINS = [ - 'chat.signal.org', - 'storage.signal.org', - 'cdsi.signal.org', - 'cdn.signal.org', - 'cdn2.signal.org', - 'cdn3.signal.org', - 'updates2.signal.org', - 'sfu.voip.signal.org', -]; - -async function main() { - const config = await Promise.all( - FALLBACK_DOMAINS.sort().map(async domain => { - const ipv4endpoints = (await resolve4(domain)).map(a => ({ - family: 'ipv4', - address: a, - })); - - const ipv6endpoints = (await resolve6(domain)).map(a => ({ - family: 'ipv6', - address: a, - })); - - const endpoints = [...ipv4endpoints, ...ipv6endpoints] - .filter(isNotNil) - .sort((a, b) => { - if (a.family < b.family) { - return -1; - } - if (a.family > b.family) { - return 1; - } - - if (a.address < b.address) { - return -1; - } - if (a.address > b.address) { - return 1; - } - return 0; - }); - - return { domain, endpoints }; - }) - ); - - const outPath = join(__dirname, '../../build/dns-fallback.json'); - - await writeFile(outPath, `${JSON.stringify(config, null, 2)}\n`); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/scripts/generate-fixtures.node.ts b/ts/scripts/generate-fixtures.node.ts deleted file mode 100644 index 0a89cbac72..0000000000 --- a/ts/scripts/generate-fixtures.node.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import fs from 'node:fs/promises'; -import path from 'node:path'; -import crypto from 'node:crypto'; -import { execFileSync } from 'node:child_process'; - -const FIXTURES = path.join(__dirname, '..', '..', 'fixtures'); -const SIZE = 256 * 1024; - -async function main() { - const original = crypto.randomBytes(SIZE); - - const originalPath = path.join(FIXTURES, 'diff-original.bin'); - await fs.writeFile(originalPath, original); - - // Add a few broken bytes to help create useful blockmaps - for (let i = 0; i < 3; i += 1) { - original[Math.floor(Math.random() * original.length)] = 0; - } - - const modifiedPath = path.join(FIXTURES, 'diff-modified.bin'); - await fs.writeFile(modifiedPath, original); - - const appBuilder = path.join( - __dirname, - '..', - '..', - 'node_modules', - 'app-builder-bin', - 'mac', - 'app-builder_amd64' - ); - - for (const filePath of [originalPath, modifiedPath]) { - console.log('Adding blockmap to', filePath); - - // Put blockmap into a separate file - console.log( - execFileSync(appBuilder, [ - 'blockmap', - '--input', - filePath, - '--output', - `${filePath}.blockmap`, - ]).toString() - ); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/generate-preload-cache.node.ts b/ts/scripts/generate-preload-cache.node.ts deleted file mode 100644 index 1220d809b1..0000000000 --- a/ts/scripts/generate-preload-cache.node.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { spawn } from 'node:child_process'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { mkdir, mkdtemp, rm, rename } from 'node:fs/promises'; -import pTimeout from 'p-timeout'; -import ELECTRON_BIN from 'electron'; - -import { MINUTE } from '../util/durations/index.std.ts'; -import { explodePromise } from '../util/explodePromise.std.ts'; - -const ROOT_DIR = join(__dirname, '..', '..'); - -const V8_ARGS = ['--predictable']; - -async function main(): Promise { - const storagePath = await mkdtemp(join(tmpdir(), 'signal-preload-cache-')); - - const argv = [`--js-flags=${V8_ARGS.join(' ')}`]; - if (process.platform === 'linux') { - argv.push('--no-sandbox'); - } - argv.push('ci.js'); - - const proc = spawn( - // When imported from Node.js - the default export of 'electron' is a path - // to the Electron binary. - ELECTRON_BIN as unknown as string, - argv, - { - stdio: [null, 'inherit', 'inherit'], - cwd: ROOT_DIR, - env: { - // Linux X11 support - DISPLAY: process.env.DISPLAY, - XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR, - WAYLAND_DISPLAY: process.env.WAYLAND_DISPLAY, - XAUTHORITY: process.env.XAUTHORITY, - - CI: process.env.CI ? 'on' : undefined, - GENERATE_PRELOAD_CACHE: 'on', - SIGNAL_CI_CONFIG: JSON.stringify({ - storagePath, - openDevTools: false, - }), - }, - } - ); - - try { - const { promise, resolve, reject } = explodePromise(); - proc.on('exit', status => resolve(status)); - proc.on('error', error => reject(error)); - - const status = await pTimeout(promise, 5 * MINUTE); - - if (status !== 0) { - throw new Error(`Exit code: ${status}`); - } - } catch (error) { - const { ARTIFACTS_DIR } = process.env; - if (!ARTIFACTS_DIR) { - console.error( - 'Not saving artifacts. Please set ARTIFACTS_DIR env variable' - ); - } else { - console.error(`Saving logs to ${ARTIFACTS_DIR}`); - await mkdir(ARTIFACTS_DIR, { recursive: true }); - - const logsDir = join(storagePath, 'logs'); - await rename(logsDir, join(ARTIFACTS_DIR, 'logs')); - } - - throw error; - } finally { - try { - proc.kill(); - } catch { - // Ignore - } - await rm(storagePath, { recursive: true }); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/scripts/get-emoji-locales.node.ts b/ts/scripts/get-emoji-locales.node.ts deleted file mode 100644 index 3e933e4662..0000000000 --- a/ts/scripts/get-emoji-locales.node.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { writeFile, readFile } from 'node:fs/promises'; -import { createHash } from 'node:crypto'; -import { join } from 'node:path'; -import z from 'zod'; -import prettier from 'prettier'; - -import type { OptionalResourceType } from '../types/OptionalResource.std.ts'; -import { OptionalResourcesDictSchema } from '../types/OptionalResource.std.ts'; -import { parseUnknown } from '../util/schemas.std.ts'; - -const MANIFEST_URL = - 'https://updates.signal.org/dynamic/android/emoji/search/manifest.json'; - -const ManifestSchema = z.object({ - version: z.number(), - languages: z.string().array(), - languageToSmartlingLocale: z.record(z.string(), z.string()), -}); - -async function fetchJSON(url: string): Promise { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`Failed to fetch ${url}`); - } - - return res.json(); -} - -async function main(): Promise { - const manifest = parseUnknown(ManifestSchema, await fetchJSON(MANIFEST_URL)); - - manifest.languageToSmartlingLocale['zh_TW'] = 'zh-Hant'; - manifest.languageToSmartlingLocale['sr'] = 'sr'; - - const extraResources = new Map(); - - await Promise.all( - manifest.languages.map(async language => { - const langUrl = - 'https://updates.signal.org/static/android/' + - `emoji/search/${manifest.version}/${language}.json`; - - const res = await fetch(langUrl); - if (!res.ok) { - throw new Error(`Failed to fetch ${langUrl}`); - } - - const data = Buffer.from(await res.arrayBuffer()); - - const digest = createHash('sha512').update(data).digest('base64'); - - let locale = manifest.languageToSmartlingLocale[language] ?? language; - locale = locale.replace(/_/g, '-'); - - const pinnedUrl = - 'https://updates2.signal.org/static/android/' + - `emoji/search/${manifest.version}/${language}.json`; - - extraResources.set(locale, { - url: pinnedUrl, - size: data.length, - digest, - }); - }) - ); - - const resourcesPath = join( - __dirname, - '..', - '..', - 'build', - 'optional-resources.json' - ); - const resources = parseUnknown( - OptionalResourcesDictSchema, - JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown - ); - - for (const [locale, resource] of extraResources) { - resources[`emoji-index-${locale}.json`] = resource; - } - - const prettierConfig = await prettier.resolveConfig( - join(__dirname, '..', '..', 'build') - ); - - const output = await prettier.format(JSON.stringify(resources, null, 2), { - ...prettierConfig, - filepath: resourcesPath, - }); - await writeFile(resourcesPath, output); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/get-jumbomoji.node.ts b/ts/scripts/get-jumbomoji.node.ts deleted file mode 100644 index a6ab29a4d4..0000000000 --- a/ts/scripts/get-jumbomoji.node.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { writeFile, readFile } from 'node:fs/promises'; -import { createHash } from 'node:crypto'; -import { join } from 'node:path'; -import { Buffer } from 'node:buffer'; -import z from 'zod'; -import prettier from 'prettier'; - -import type { OptionalResourceType } from '../types/OptionalResource.std.ts'; -import { OptionalResourcesDictSchema } from '../types/OptionalResource.std.ts'; -import { parseUnknown } from '../util/schemas.std.ts'; -import { utf16ToEmoji } from '../util/utf16ToEmoji.node.ts'; - -const VERSION = 12; - -const STATIC_URL = 'https://updates.signal.org/static/android/emoji'; -const STATIC_PINNED_URL = 'https://updates2.signal.org/static/android/emoji'; -const MANIFEST_URL = `${STATIC_URL}/${VERSION}/emoji_data.json`; - -const ManifestSchema = z.object({ - jumbomoji: z.record(z.string(), z.string().transform(utf16ToEmoji).array()), -}); - -async function fetchJSON(url: string): Promise { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`Failed to fetch ${url}`); - } - - return res.json(); -} - -async function main(): Promise { - const { jumbomoji } = parseUnknown( - ManifestSchema, - await fetchJSON(MANIFEST_URL) - ); - - const extraResources = new Map(); - - await Promise.all( - Array.from(Object.keys(jumbomoji)).map(async sheet => { - const publicUrl = `${STATIC_URL}/${VERSION}/xhdpi/jumbo/${sheet}.proto`; - - const res = await fetch(publicUrl); - if (!res.ok) { - throw new Error(`Failed to fetch ${publicUrl}`); - } - - const data = Buffer.from(await res.arrayBuffer()); - - const digest = createHash('sha512').update(data).digest('base64'); - - const pinnedUrl = `${STATIC_PINNED_URL}/${VERSION}/xhdpi/jumbo/${sheet}.proto`; - - extraResources.set(sheet, { - url: pinnedUrl, - size: data.length, - digest, - }); - }) - ); - - const manifestPath = join(__dirname, '..', '..', 'build', 'jumbomoji.json'); - - const resourcesPath = join( - __dirname, - '..', - '..', - 'build', - 'optional-resources.json' - ); - const resources = parseUnknown( - OptionalResourcesDictSchema, - JSON.parse(await readFile(resourcesPath, 'utf8')) as unknown - ); - - for (const [sheet, resource] of extraResources) { - resources[`emoji-sheet-${sheet}.proto`] = resource; - } - - const prettierConfig = await prettier.resolveConfig( - join(__dirname, '..', '..', 'build') - ); - - { - const output = await prettier.format(JSON.stringify(jumbomoji, null, 2), { - ...prettierConfig, - filepath: manifestPath, - }); - await writeFile(manifestPath, output); - } - - { - const output = await prettier.format(JSON.stringify(resources, null, 2), { - ...prettierConfig, - filepath: resourcesPath, - }); - await writeFile(resourcesPath, output); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/get-strings.node.ts b/ts/scripts/get-strings.node.ts deleted file mode 100644 index 15a29be986..0000000000 --- a/ts/scripts/get-strings.node.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { rm, mkdir, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import fastGlob from 'fast-glob'; -import prettier from 'prettier'; -import pMap from 'p-map'; -import z from 'zod'; - -import { authenticate, API_BASE, PROJECT_ID } from '../util/smartling.node.ts'; - -const { SMARTLING_USER, SMARTLING_SECRET } = process.env; - -const RENAMES = new Map([ - // Smartling uses "zh-YU" for Cantonese (or Yue Chinese). - // This is wrong. - // The language tag for Yue Chinese is "yue" - // "zh-YU" actually implies "Chinese as spoken in Yugoslavia (canonicalized to Serbia)" - ['zh-YU', 'yue'], - - // For most of the Chinese-speaking world, where we don't have a region specific - // locale available (e.g. zh-HK), zh-TW is a suitable choice for "Traditional Chinese". - // - // However, Intl.LocaleMatcher won't match "zh-Hant-XX" to "zh-TW", - // we need to rename it to "zh-Hant" explicitly to make it work. - ['zh-TW', 'zh-Hant'], - - // "YR" is not a valid region subtag. Smartling made it up. - ['sr-YR', 'sr'], -]); - -const StatusSchema = z.object({ - response: z.object({ - code: z.literal('SUCCESS'), - data: z.object({ - items: z - .object({ - localeId: z.string(), - }) - .array(), - }), - }), -}); - -async function main() { - if (!SMARTLING_USER) { - console.error('Need to set SMARTLING_USER environment variable!'); - process.exit(1); - } - if (!SMARTLING_SECRET) { - console.error('Need to set SMARTLING_SECRET environment variable!'); - process.exit(1); - } - - console.log('Authenticating with Smartling'); - const headers = await authenticate({ - userIdentifier: SMARTLING_USER, - userSecret: SMARTLING_SECRET, - }); - - const statusURL = new URL( - `./files-api/v2/projects/${PROJECT_ID}/file/status`, - API_BASE - ); - statusURL.searchParams.set('fileUri', '_locales/en/messages.json'); - - console.log('Getting list of locales...'); - const statusRes = await fetch(statusURL, { - headers, - }); - - if (!statusRes.ok) { - throw new Error('Failed to fetch the status'); - } - if (!statusRes.body) { - throw new Error('Missing body'); - } - const { - response: { - data: { items: locales }, - }, - } = StatusSchema.parse(await statusRes.json()); - - console.log('Cleaning _locales directory...'); - const dirEntries = await fastGlob(['_locales/*', '!_locales/en'], { - onlyDirectories: true, - absolute: true, - }); - - await Promise.all( - dirEntries.map(dirEntry => rm(dirEntry, { recursive: true })) - ); - - console.log('Getting latest strings'); - - const prettierConfig = await prettier.resolveConfig('_locales'); - - await pMap( - locales, - async ({ localeId }) => { - const fileURL = new URL( - `./files-api/v2/projects/${PROJECT_ID}/` + - `locales/${encodeURIComponent(localeId)}/file`, - API_BASE - ); - fileURL.searchParams.set('fileUri', '_locales/en/messages.json'); - fileURL.searchParams.set('retrievalType', 'published'); - fileURL.searchParams.set('includeOriginalStrings', 'true'); - - const fileRes = await fetch(fileURL, { - headers, - }); - - if (!fileRes.ok) { - throw new Error('Failed to fetch the file'); - } - if (!fileRes.body) { - throw new Error('Missing body'); - } - - const targetLocale = RENAMES.get(localeId) ?? localeId; - const targetDir = path.join('_locales', targetLocale); - - try { - await mkdir(targetDir); - } catch (error) { - console.error(error); - } - - const targetFile = path.join(targetDir, 'messages.json'); - console.log('Writing', targetLocale); - const json = await fileRes.json(); - for (const value of Object.values(json)) { - const typedValue = value as { description?: string }; - delete typedValue.description; - } - delete json.smartling; - const output = await prettier.format(JSON.stringify(json, null, 2), { - ...prettierConfig, - filepath: targetFile, - }); - await writeFile(targetFile, output); - }, - { concurrency: 20 } - ); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/publish-installer-size.node.ts b/ts/scripts/publish-installer-size.node.ts deleted file mode 100644 index a740506fb0..0000000000 --- a/ts/scripts/publish-installer-size.node.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { stat } from 'node:fs/promises'; -import { join } from 'node:path'; - -import { packageJson } from '../util/packageJson.node.ts'; - -const NAME = packageJson.name; -const VERSION = packageJson.version; - -const SUPPORT_CONFIG = new Set([ - 'linux', - 'windows', - 'macos-arm64', - 'macos-x64', - 'macos-universal', -]); - -const RELEASE_DIR = join(__dirname, '..', '..', 'release'); - -// TODO: DESKTOP-9836 -async function main(): Promise { - // oxlint-disable-next-line typescript/no-non-null-assertion - const config = process.argv[2]!; - if (!SUPPORT_CONFIG.has(config)) { - throw new Error(`Invalid argument: ${config}`); - } - - let fileName: string; - let platform: string; - let arch: string; - if (config === 'linux') { - fileName = `${NAME}_${VERSION}_amd64.deb`; - platform = 'linux'; - arch = 'x64'; - } else if (config === 'windows') { - fileName = `${NAME}-win-x64-${VERSION}.exe`; - platform = 'windows'; - arch = 'x64'; - } else if (config === 'macos-arm64') { - fileName = `${NAME}-mac-arm64-${VERSION}.zip`; - platform = 'macos'; - arch = 'arm64'; - } else if (config === 'macos-x64') { - fileName = `${NAME}-mac-x64-${VERSION}.zip`; - platform = 'macos'; - arch = 'x64'; - } else if (config === 'macos-universal') { - fileName = `${NAME}-mac-universal-${VERSION}.dmg`; - platform = 'macos'; - arch = 'universal'; - } else { - throw new Error(`Unsupported config: ${config}`); - } - - const filePath = join(RELEASE_DIR, fileName); - const { size } = await stat(filePath); - - console.log(`${platform} ${arch} release size: ${size}`); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error('Failed', err); - process.exit(1); -}); diff --git a/ts/scripts/push-strings.node.ts b/ts/scripts/push-strings.node.ts deleted file mode 100644 index 5b177196ac..0000000000 --- a/ts/scripts/push-strings.node.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { randomBytes } from 'node:crypto'; -import { readFile } from 'node:fs/promises'; - -import { API_BASE, PROJECT_ID, authenticate } from '../util/smartling.node.ts'; - -const { SMARTLING_USER, SMARTLING_SECRET } = process.env; - -async function main() { - if (!SMARTLING_USER) { - console.error('Need to set SMARTLING_USER environment variable!'); - process.exit(1); - } - if (!SMARTLING_SECRET) { - console.error('Need to set SMARTLING_SECRET environment variable!'); - process.exit(1); - } - - console.log('Authenticating with Smartling'); - const headers = await authenticate({ - userIdentifier: SMARTLING_USER, - userSecret: SMARTLING_SECRET, - }); - - const boundaryString = randomBytes(32).toString('hex'); - - headers.set( - 'content-type', - `multipart/form-data; boundary=${boundaryString}` - ); - - const url = new URL(`./files-api/v2/projects/${PROJECT_ID}/file`, API_BASE); - const body = [ - `--${boundaryString}`, - 'Content-Disposition: form-data; name="fileUri"', - 'Content-Type: text/plain', - '', - '_locales/en/messages.json', - - `--${boundaryString}`, - 'Content-Disposition: form-data; name="fileType"', - 'Content-Type: text/plain', - '', - 'json', - - `--${boundaryString}`, - 'Content-Disposition: form-data; name="file"; filename="_locales/en/messages.json"', - 'Content-Type: text/plain', - '', - await readFile('_locales/en/messages.json', 'utf8'), - `--${boundaryString}--`, - '', - ]; - - console.log('Pushing strings'); - const res = await fetch(url, { - method: 'POST', - headers, - body: body.join('\r\n'), - }); - if (!res.ok) { - throw new Error(`Failed to push strings: ${await res.text()}`); - } -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/remove-strings.node.ts b/ts/scripts/remove-strings.node.ts deleted file mode 100644 index 7eae60823b..0000000000 --- a/ts/scripts/remove-strings.node.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import chalk from 'chalk'; -import execa from 'execa'; -import fs from 'node:fs/promises'; -import pLimit from 'p-limit'; -import path from 'node:path'; - -import { MONTH } from '../util/durations/index.std.ts'; -import { isOlderThan } from '../util/timestamp.std.ts'; -import { DELETED_REGEXP, type DeletedMatch } from './constants.std.ts'; - -const ROOT_DIR = path.join(__dirname, '..', '..'); -const MESSAGES_FILE = path.join(ROOT_DIR, '_locales', 'en', 'messages.json'); - -const limitter = pLimit(10); - -async function main() { - const messages = JSON.parse(await fs.readFile(MESSAGES_FILE, 'utf-8')); - - const stillUsed = new Set(); - - await Promise.all( - Object.keys(messages).map(key => - limitter(async () => { - const value = messages[key]; - - const match = (value as Record).description?.match( - DELETED_REGEXP - ); - if (!match) { - return; - } - - const [deletedAtStr] = match as DeletedMatch; - const deletedAt = new Date(deletedAtStr).getTime(); - if (!isOlderThan(deletedAt, MONTH)) { - return; - } - - // Find uses in either: - // - `i18n('key')` - // - `` - - try { - const result = await execa( - 'git', - // prettier-ignore - [ - 'grep', - '--extended-regexp', - `'${key}'|id="${key}"`, - '--', - '**', - ':!\\_locales/**', - ':!\\sticker-creator/**', - ], - { - cwd: ROOT_DIR, - stdin: 'ignore', - stdout: 'pipe', - stderr: 'inherit', - } - ); - - // Match found - console.error( - chalk.red( - `ERROR: String is still used: "${key}", deleted on ${match[1]}` - ) - ); - console.error(result.stdout.trim()); - console.error(''); - stillUsed.add(key); - } catch (error) { - if (error.exitCode === 1) { - console.log( - chalk.dim(`Removing string: "${key}", deleted on ${match[1]}`) - ); - delete messages[key]; - } else { - throw error; - } - } - }) - ) - ); - - if (stillUsed.size !== 0) { - console.error( - `ERROR: Didn't remove ${stillUsed.size} strings because of errors above`, - Array.from(stillUsed) - .map(str => `- ${str}`) - .join('\n') - ); - console.error('ERROR: Not saving changes'); - process.exit(1); - } - await fs.writeFile(MESSAGES_FILE, `${JSON.stringify(messages, null, 2)}\n`); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/ts/scripts/symbolicate-crash-report.node.ts b/ts/scripts/symbolicate-crash-report.node.ts deleted file mode 100644 index 5db7beeecc..0000000000 --- a/ts/scripts/symbolicate-crash-report.node.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { tmpdir } from 'node:os'; -import { readFile, writeFile, mkdtemp } from 'node:fs/promises'; -import { gunzip as gunzipCb } from 'node:zlib'; -import { join, basename } from 'node:path'; -import { promisify } from 'node:util'; -import { symbolicate } from '@electron/symbolicate-mac'; -import pMap from 'p-map'; - -const gunzip = promisify(gunzipCb); - -if (!process.argv[2]) { - throw new Error('Usage: node symbolicate-crash-report.js '); -} -const INPUT_FILE = process.argv[2]; - -async function main(): Promise { - let file = await readFile(INPUT_FILE); - try { - file = await gunzip(file); - } catch (error) { - console.error('Failed to decompress, perhaps file is a plaintext?'); - } - - const matches = file - .toString() - .matchAll( - /WARN[^\n]*crashReports:\s+dump=\[REDACTED\]([0-9a-z]+).dmp\s+mtime="([\d\-T:.Z]+)"\s+({(\n|.)*?\n})/g - ); - type CrashDumpMatch = RegExpExecArray & { 1: string; 2: string; 3: string }; - - function moduleName(filename: string | undefined): string { - if (!filename) { - return ''; - } - - if (filename.startsWith('signal-desktop-')) { - return 'electron'; - } - - if (filename.startsWith('Signal') && filename.endsWith('.exe')) { - return 'electron.exe.pdb'; - } - return filename; - } - - const dumps = new Array(); - for (const match of matches) { - const [, dump, mtime, json] = match as CrashDumpMatch; - const out = []; - - let info; - try { - info = JSON.parse(json); - } catch (error) { - console.error('Failed to parse JSON, ignoring', dump, mtime, error); - continue; - } - - out.push(`## dump=${dump} mtime=${mtime}`); - out.push(''); - out.push('```'); - - for (const [index, frame] of info.crashing_thread.frames.entries()) { - out.push(`${index} ${moduleName(frame.module)} ${frame.offset} () + 0`); - } - - out.push(''); - - for (const m of info.modules) { - const filename = moduleName(m.filename); - - out.push( - `${m.base_addr} - ${m.end_addr} ` + - `${filename.replace(/\s+/g, '-')} (${m.version}) ` + - `<${m.debug_id.slice(0, -1)}> ${filename}` - ); - } - out.push('```'); - - dumps.push(out.join('\n')); - } - - const tmpFolder = await mkdtemp(join(tmpdir(), 'parse-crash-reports')); - - const result = await pMap( - dumps, - async (text, i) => { - const tmpFile = join(tmpFolder, `${i}.txt`); - await writeFile(tmpFile, text); - - console.error(`Symbolicating: ${tmpFile}`); - return symbolicate({ file: tmpFile }); - }, - { concurrency: 1 } - ); - - console.log(`# Crash Report ${basename(INPUT_FILE)}`); - console.log(''); - console.log(result.join('\n')); -} - -// oxlint-disable-next-line promise/prefer-await-to-then -main().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/ts/test-electron/.eslintrc.js b/ts/test-electron/.eslintrc.js deleted file mode 100644 index e273dfa7f9..0000000000 --- a/ts/test-electron/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -module.exports = { - rules: { - // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - }, - ], - }, -}; diff --git a/ts/test-node/util/version_test.std.ts b/ts/test-node/util/version_test.std.ts index 703de07239..0ddc525758 100644 --- a/ts/test-node/util/version_test.std.ts +++ b/ts/test-node/util/version_test.std.ts @@ -1,12 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only - import { assert } from 'chai'; -import { useFakeTimers } from 'sinon'; -import * as semver from 'semver'; - import { - generateTaggedVersion, isAlpha, isAxolotl, isNightly, @@ -102,101 +97,4 @@ describe('version utilities', () => { assert.isTrue(isStaging('1.2.3-staging.1232.23-adsfs')); }); }); - - describe('generateTaggedVersion', () => { - beforeEach(function (this: Mocha.Context) { - // This isn't a hook. - this.clock = useFakeTimers(); - }); - - afterEach(function (this: Mocha.Context) { - this.clock.restore(); - }); - - it('uses the current date and provided shortSha', function (this: Mocha.Context) { - this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); - - const currentVersion = '5.12.0-beta.1'; - const shortSha = '07f0efc45'; - - const expected = '5.12.0-alpha.20210723.01-07f0efc45'; - const actual = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - assert.strictEqual(expected, actual); - }); - - it('same production version is semver.gt', function (this: Mocha.Context) { - const currentVersion = '5.12.0-beta.1'; - const shortSha = '07f0efc45'; - - this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); - const actual = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - assert.isTrue(semver.gt('5.12.0', actual)); - }); - - it('same beta version is semver.gt', function (this: Mocha.Context) { - const currentVersion = '5.12.0-beta.1'; - const shortSha = '07f0efc45'; - - this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); - const actual = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - assert.isTrue(semver.gt(currentVersion, actual)); - }); - - it('build earlier same day is semver.lt', function (this: Mocha.Context) { - const currentVersion = '5.12.0-beta.1'; - const shortSha = '07f0efc45'; - - this.clock.setSystemTime(new Date('2021-07-23T00:22:55.692Z').getTime()); - const actualEarlier = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); - const actualLater = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - assert.isTrue(semver.lt(actualEarlier, actualLater)); - }); - - it('build previous day is semver.lt', function (this: Mocha.Context) { - const currentVersion = '5.12.0-beta.1'; - const shortSha = '07f0efc45'; - - this.clock.setSystemTime(new Date('2021-07-22T01:22:55.692Z').getTime()); - const actualEarlier = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); - const actualLater = generateTaggedVersion({ - release: 'alpha', - currentVersion, - shortSha, - }); - - assert.isTrue(semver.lt(actualEarlier, actualLater)); - }); - }); }); diff --git a/ts/util/lint/license_comments.node.ts b/ts/util/lint/license_comments.node.ts index dab9a7af30..1bd18515d7 100644 --- a/ts/util/lint/license_comments.node.ts +++ b/ts/util/lint/license_comments.node.ts @@ -40,8 +40,6 @@ const FILES_TO_IGNORE = new Set( '.smartling-source.sh', 'packages/mute-state-change/dist/acknowledgments.md', 'components/mp3lameencoder/lib/Mp3LameEncoder.js', - 'components/recorderjs/recorder.js', - 'components/recorderjs/recorderWorker.js', 'components/webaudiorecorder/lib/WebAudioRecorder.js', 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js', 'js/Mp3LameEncoder.min.js', diff --git a/ts/util/lint/linter.node.ts b/ts/util/lint/linter.node.ts index d5ff342ae9..5e4cad3f48 100644 --- a/ts/util/lint/linter.node.ts +++ b/ts/util/lint/linter.node.ts @@ -75,8 +75,6 @@ const excludedFilesRegexp = RegExp( '^js/components.js', '^js/curve/', '^js/util_worker.js', - '^libtextsecure/test/test.js', - '^test/test.js', '^ts/workers/heicConverter.bundle.js', '^ts/sql/mainWorker.bundle.js', @@ -274,7 +272,6 @@ const excludedFilesRegexp = RegExp( '^node_modules/to-ast/.+', '^node_modules/trough/.+', '^node_modules/ts-loader/.+', - '^node_modules/ts-node/.+', '^node_modules/tweetnacl/.+', '^node_modules/typed-scss-modules/.+', '^node_modules/typescript/.+', diff --git a/ts/util/setupI18n.dom.tsx b/ts/util/setupI18n.dom.tsx index 4173cda5e6..d447d4bdb8 100644 --- a/ts/util/setupI18n.dom.tsx +++ b/ts/util/setupI18n.dom.tsx @@ -14,8 +14,6 @@ import { import type { SetupI18nOptionsType } from './setupI18nMain.std.ts'; import { strictAssert } from './assert.std.ts'; -export { isLocaleMessageType } from './setupI18nMain.std.ts'; - export function renderEmojify( parts: ReadonlyArray ): React.JSX.Element { diff --git a/ts/util/setupI18nMain.std.ts b/ts/util/setupI18nMain.std.ts index 0d5ab74f8b..267500acd1 100644 --- a/ts/util/setupI18nMain.std.ts +++ b/ts/util/setupI18nMain.std.ts @@ -21,9 +21,7 @@ import { bidiIsolate, bidiStrip } from './unicodeBidi.std.ts'; const log = createLogger('setupI18nMain'); -export function isLocaleMessageType( - value: unknown -): value is LocaleMessageType { +function isLocaleMessageType(value: unknown): value is LocaleMessageType { return ( typeof value === 'object' && value != null && diff --git a/ts/util/version.std.ts b/ts/util/version.std.ts index 1e1f233013..4508962dde 100644 --- a/ts/util/version.std.ts +++ b/ts/util/version.std.ts @@ -32,36 +32,3 @@ export const isNotUpdatable = (version: string): boolean => isAdhoc(version); export const isStaging = (version: string): boolean => semver.parse(version)?.prerelease[0] === 'staging'; - -export const generateTaggedVersion = (options: { - release: string; - currentVersion: string; - shortSha: string; -}): string => { - const { release, currentVersion, shortSha } = options; - - const parsed = semver.parse(currentVersion); - if (!parsed) { - throw new Error(`generateTaggedVersion: Invalid version ${currentVersion}`); - } - - const dateTimeParts = new Intl.DateTimeFormat('en', { - day: '2-digit', - hour: '2-digit', - hourCycle: 'h23', - month: '2-digit', - timeZone: 'GMT', - year: 'numeric', - }).formatToParts(new Date()); - const dateTimeMap = new Map(); - dateTimeParts.forEach(({ type, value }) => { - dateTimeMap.set(type, value); - }); - const formattedDate = `${dateTimeMap.get('year')}${dateTimeMap.get( - 'month' - )}${dateTimeMap.get('day')}.${dateTimeMap.get('hour')}`; - - const formattedVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`; - - return `${formattedVersion}-${release}.${formattedDate}-${shortSha}`; -}; diff --git a/tsconfig.json b/tsconfig.json index b491a4b872..9a2385c14a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,13 @@ "app/**/*", "package.json", "build/intl-linter/**/*.ts", - ".oxlint/**/*" + ".oxlint/**/*", + "scripts/**/*", + "codemods/**/*", + "test/test.mjs", + ".babelrc.mjs", + ".prettierrc.mjs", + ".stylelintrc.mjs" ], "compilerOptions": { /* @@ -66,11 +72,11 @@ */ /* Specify what module code is generated. */ - "module": "ES2022", + "module": "ESNext", /* Specify the root folder within your source files. */ "rootDir": "./", /* Specify how TypeScript looks up a file from a given module specifier. */ - "moduleResolution": "Node", + "moduleResolution": "Bundler", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -232,17 +238,5 @@ // "skipDefaultLibCheck": true, /* Skip type checking all .d.ts files. */ "skipLibCheck": true - }, - - /** - * ts-node - */ - "ts-node": { - "transpileOnly": true, - // Override TypeScript options for `ts-node` only - "compilerOptions": { - // Necessary for `ts-node` to work with CommonJS modules until we migrate to package.json#type: "module" - "module": "CommonJS" - } } }