mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-13 16:43:53 +01:00
Migrate scripts to ESM and ts-check
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+22
-14
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+49
-51
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
+5
-2
@@ -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() {
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+9
-6
@@ -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')
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+32
-8
@@ -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<t.Program>} 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<t.CallExpression | t.OptionalCallExpression>} 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
+24
-10
@@ -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]));
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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<t.ImportDeclaration | t.ExportAllDeclaration | t.ExportNamedDeclaration>} 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
+44
-44
@@ -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
|
||||
},
|
||||
|
||||
Generated
+519
-374
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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<Array<string>>}
|
||||
*/
|
||||
export async function afterAllArtifactBuild(result) {
|
||||
await notarizeUniversalDMG(result);
|
||||
return [];
|
||||
}
|
||||
@@ -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<void>}
|
||||
*/
|
||||
export async function afterPack(context) {
|
||||
await pruneMacOSRelease(context);
|
||||
await fuseElectron(context);
|
||||
await copyPacks(context);
|
||||
await ensureLinuxFilePermissions(context);
|
||||
}
|
||||
@@ -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<void>}
|
||||
*/
|
||||
export async function afterSign(context) {
|
||||
// This must be the last step
|
||||
await notarize(context);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/// <reference path="./utils/better-blockmap.d.ts">
|
||||
|
||||
/** @import { ArtifactCreated } from 'electron-builder' */
|
||||
|
||||
/**
|
||||
* @param {ArtifactCreated} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function artifactBuildCompleted({
|
||||
target,
|
||||
file,
|
||||
packager,
|
||||
updateInfo,
|
||||
}: ArtifactCreated): Promise<void> {
|
||||
}) {
|
||||
if (packager.platform.name === 'linux' && file.endsWith('.AppImage')) {
|
||||
const blockMapPath = `${file}.blockmap`;
|
||||
console.log(`Generating blockmap ${blockMapPath}`);
|
||||
+49
-43
@@ -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<string>;
|
||||
type Records = ReadonlyArray<Row>;
|
||||
/** @typedef {ReadonlyArray<string>} Row */
|
||||
/** @typedef {ReadonlyArray<Row>} Records */
|
||||
|
||||
function parseCsv(input: string) {
|
||||
return new Promise<Records>((resolve, reject) => {
|
||||
parse(input, { trim: true }, (error, records: Records) => {
|
||||
/**
|
||||
* @param {string} input
|
||||
* @returns {Promise<Records>}
|
||||
*/
|
||||
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<string, Record<string, string>>;
|
||||
/** @typedef {Record<string, Record<string, string>>} LocaleDisplayNamesResult */
|
||||
|
||||
function convertData(
|
||||
input: z.infer<typeof LocaleDisplayNames>
|
||||
): LocaleDisplayNamesResult {
|
||||
/**
|
||||
* @param {z.infer<typeof LocaleDisplayNames>} 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');
|
||||
@@ -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<void> {
|
||||
/**
|
||||
* @param {string} file
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function windowsDllImportCheck(file) {
|
||||
console.log(`${file}: checking...`);
|
||||
|
||||
const fileData = await readFile(file);
|
||||
@@ -183,7 +193,12 @@ async function windowsDllImportCheck(file: string): Promise<void> {
|
||||
ignoreCert: true,
|
||||
});
|
||||
|
||||
function cstr(data: Buffer<ArrayBuffer>, offset: number): string {
|
||||
/**
|
||||
* @param {Buffer<ArrayBuffer>} 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<void> {
|
||||
throw new Error('Invalid cstring');
|
||||
}
|
||||
|
||||
const imports = new Set<string>();
|
||||
/** @type {Set<string>} */
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -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<any>}
|
||||
*/
|
||||
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<LocalDependency>} */
|
||||
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<FetchedDependency>} */
|
||||
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<string, Set<string>>} */
|
||||
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<string>; }} */
|
||||
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<string> }} */
|
||||
// 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', '.']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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`);
|
||||
@@ -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<string, Array<[string, string]>>} */
|
||||
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))
|
||||
);
|
||||
@@ -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<void> {
|
||||
let defaultLocale: string;
|
||||
/** @import { AfterPackContext } from 'electron-builder' */
|
||||
|
||||
/**
|
||||
* @param {AfterPackContext} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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<Promise<void>>();
|
||||
|
||||
/** @type {Promise<void>[]} */
|
||||
const promises = [];
|
||||
for (const locale of ourLocales) {
|
||||
if (electronLocales.has(locale)) {
|
||||
continue;
|
||||
+8
-6
@@ -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<void> {
|
||||
/**
|
||||
* @param {AfterPackContext} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function afterPack({ appOutDir, electronPlatformName }) {
|
||||
if (electronPlatformName !== 'linux') {
|
||||
return;
|
||||
}
|
||||
@@ -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<void> {
|
||||
/** @import { AfterPackContext, LinuxPackager } from 'electron-builder' */
|
||||
|
||||
/**
|
||||
* @param {AfterPackContext} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
@@ -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`);
|
||||
@@ -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<number>();
|
||||
/** @type {Set<number>} */
|
||||
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<string>();
|
||||
/** @type {Array<string>} */
|
||||
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<ArrayBuffer> | undefined;
|
||||
/** @type {Buffer<ArrayBuffer> | undefined} */
|
||||
let json;
|
||||
for (const f of fallbacks) {
|
||||
try {
|
||||
json = fs.readFileSync(path.join(LOCALES_DIR, f, 'messages.json'));
|
||||
@@ -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 = `<description>${englishDescription}</description>\n`;
|
||||
let allMessages = `<message>${englishMessage}</message>\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 xml:lang="${localeName}">${description}</description>\n`;
|
||||
|
||||
const message = data[template.message]?.messageformat ?? englishMessage;
|
||||
allMessages += ` <message xml:lang="${localeName}">${message}</message>\n`;
|
||||
}
|
||||
|
||||
const targetPath = path.join(ROOT_DIR, 'build', template.name);
|
||||
let targetContent = template.content;
|
||||
|
||||
targetContent = targetContent.replace(
|
||||
'<!-- <description>{description}</description> -->',
|
||||
allDescriptions
|
||||
);
|
||||
targetContent = targetContent.replace(
|
||||
'<!-- <message>{message}</message> -->',
|
||||
allMessages
|
||||
);
|
||||
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await fs.writeFile(targetPath, targetContent);
|
||||
}
|
||||
@@ -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 = [
|
||||
'<!-- Copyright 2020 Signal Messenger, LLC -->',
|
||||
'<!-- SPDX-License-Identifier: AGPL-3.0-only -->',
|
||||
'# Acknowledgments',
|
||||
'',
|
||||
'Signal Desktop makes use of the following open source projects.',
|
||||
'',
|
||||
markdownsForDependency.join('\n\n'),
|
||||
'',
|
||||
'## Kyber Patent License',
|
||||
'',
|
||||
'<https://csrc.nist.gov/csrc/media/Projects/post-quantum-cryptography/documents/selected-algos-2022/nist-pqc-license-summary-and-excerpts.pdf>',
|
||||
'',
|
||||
markdownsFromSignalLibs.join('\n\n'),
|
||||
].join('\n');
|
||||
const unformattedOutput = [
|
||||
'<!-- Copyright 2020 Signal Messenger, LLC -->',
|
||||
'<!-- SPDX-License-Identifier: AGPL-3.0-only -->',
|
||||
'# Acknowledgments',
|
||||
'',
|
||||
'Signal Desktop makes use of the following open source projects.',
|
||||
'',
|
||||
markdownsForDependency.join('\n\n'),
|
||||
'',
|
||||
'## Kyber Patent License',
|
||||
'',
|
||||
'<https://csrc.nist.gov/csrc/media/Projects/post-quantum-cryptography/documents/selected-algos-2022/nist-pqc-license-summary-and-excerpts.pdf>',
|
||||
'',
|
||||
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);
|
||||
@@ -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<string>} options.keys
|
||||
* @returns {Promise<ReadonlyArray<string>>}
|
||||
*/
|
||||
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<string | null>} */
|
||||
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,
|
||||
});
|
||||
@@ -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`);
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
@@ -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<keyof typeof globalMessages>} */ (
|
||||
Object.keys(globalMessages)
|
||||
).sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
}) as Array<keyof typeof globalMessages>;
|
||||
});
|
||||
|
||||
function filterDefaultParams(params: Map<string, ICUMessageParamType>) {
|
||||
const filteredParams = new Map<string, ICUMessageParamType>();
|
||||
/**
|
||||
* @param {Map<string, ICUMessageParamType>} params
|
||||
*/
|
||||
function filterDefaultParams(params) {
|
||||
/** @type {Map<string, ICUMessageParamType>} */
|
||||
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<ts.TypeElement>();
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {boolean} supportsComponents
|
||||
* @returns {ts.Statement}
|
||||
*/
|
||||
function generateType(name, supportsComponents) {
|
||||
/** @type {Array<ts.TypeElement>} */
|
||||
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<ts.TypeElement>();
|
||||
/** @type {Array<ts.TypeElement>} */
|
||||
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<ts.Statement>();
|
||||
/** @type {Array<ts.Statement>} */
|
||||
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);
|
||||
@@ -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<number | null>} */
|
||||
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 });
|
||||
}
|
||||
@@ -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, TrayIconVariant> = {
|
||||
[TrayIconSize.Size16]: {
|
||||
/** @type {Record<TrayIconSize, TrayIconVariant>} */
|
||||
const Variants = {
|
||||
[TrayIconSizes.Size16]: {
|
||||
size: 16,
|
||||
maxCount: 9,
|
||||
badgePadding: 2,
|
||||
@@ -65,7 +76,7 @@ const Variants: Record<TrayIconSize, TrayIconVariant> = {
|
||||
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<TrayIconSize, TrayIconVariant> = {
|
||||
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<TrayIconSize, TrayIconVariant> = {
|
||||
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<TrayIconSize, TrayIconVariant> = {
|
||||
},
|
||||
};
|
||||
|
||||
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<Buffer<ArrayBuffer>> {
|
||||
/**
|
||||
* @param {TrayIconImageRequest} request
|
||||
* @returns {Promise<Buffer<ArrayBuffer>>}
|
||||
*/
|
||||
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<number> {
|
||||
/**
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @returns {Array<number>}
|
||||
*/
|
||||
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<TrayIconImageRequest> = [];
|
||||
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<TrayIconImageRequest>} */
|
||||
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');
|
||||
@@ -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<unknown>}
|
||||
*/
|
||||
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<string, OptionalResourceType>} */
|
||||
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);
|
||||
@@ -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(
|
||||
@@ -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<unknown>}
|
||||
*/
|
||||
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<string, OptionalResourceType>} */
|
||||
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);
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
+11
-4
@@ -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<string> {
|
||||
/**
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function getIcuLikeStrings() {
|
||||
const { status, stdout } = spawnSync(
|
||||
'grep',
|
||||
[
|
||||
@@ -36,7 +40,10 @@ function getIcuLikeStrings(): Set<string> {
|
||||
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');
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
export async function afterAllArtifactBuild({
|
||||
platformToTargets,
|
||||
artifactPaths,
|
||||
}: BuildResult): Promise<void> {
|
||||
}) {
|
||||
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({
|
||||
@@ -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<void> {
|
||||
/**
|
||||
* @param {AfterPackContext} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function afterSign({ appOutDir, packager, electronPlatformName }) {
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
console.log('notarize: Skipping, not on macOS');
|
||||
return;
|
||||
@@ -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;
|
||||
@@ -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' }));
|
||||
|
||||
@@ -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) {
|
||||
@@ -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) {
|
||||
@@ -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) {
|
||||
@@ -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) {
|
||||
@@ -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');
|
||||
|
||||
// ------
|
||||
@@ -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) {
|
||||
@@ -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(
|
||||
@@ -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<Array<string> | null> {
|
||||
/** @import { AfterPackContext } from 'electron-builder' */
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @returns {Promise<Array<string> | null>}
|
||||
*/
|
||||
async function safeReaddir(dir) {
|
||||
try {
|
||||
return await readdir(dir);
|
||||
} catch (error) {
|
||||
@@ -16,11 +21,11 @@ async function safeReaddir(dir: string): Promise<Array<string> | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function afterPack({
|
||||
appOutDir,
|
||||
packager,
|
||||
electronPlatformName,
|
||||
}: AfterPackContext): Promise<void> {
|
||||
/**
|
||||
* @param {AfterPackContext} context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function afterPack({ appOutDir, packager, electronPlatformName }) {
|
||||
if (electronPlatformName !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
@@ -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}`);
|
||||
@@ -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()}`);
|
||||
}
|
||||
@@ -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<string>} */
|
||||
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')`
|
||||
// - `<I18n id="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`);
|
||||
@@ -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<MacConfiguration['sign'], Function>} CustomMacSign
|
||||
* @typedef {Parameters<CustomMacSign>[0]} CustomMacSignOptions
|
||||
*/
|
||||
|
||||
// oxlint-disable-next-line typescript/explicit-module-boundary-types, typescript/no-explicit-any
|
||||
export async function sign(configuration: any): Promise<void> {
|
||||
/**
|
||||
* @param {CustomMacSignOptions} configuration
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sign(configuration) {
|
||||
if (process.env.SKIP_SIGNING_SCRIPT === '1') {
|
||||
console.log('SKIP_SIGNING_SCRIPT=1, skipping custom macOS signing script');
|
||||
return;
|
||||
@@ -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<void> {
|
||||
/**
|
||||
* @param {CustomWindowsSignTaskConfiguration} configuration
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sign(configuration) {
|
||||
// In CI, we remove certificate information from package.json to disable signing
|
||||
if (
|
||||
!configuration.options.signtoolOptions ||
|
||||
@@ -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';
|
||||
|
||||
/// <reference path="./utils/symbolicate-mac.d.ts">
|
||||
|
||||
const gunzip = promisify(gunzipCb);
|
||||
|
||||
if (!process.argv[2]) {
|
||||
throw new Error('Usage: node symbolicate-crash-report.js <input>');
|
||||
}
|
||||
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<string>} */
|
||||
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'));
|
||||
@@ -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<typeof failureSchema>;
|
||||
/** @typedef {z.infer<typeof failureSchema>} Failure */
|
||||
|
||||
const eventSchema = z
|
||||
.object({
|
||||
@@ -61,10 +61,12 @@ const eventSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
async function launchElectron(
|
||||
worker: number,
|
||||
attempt: number
|
||||
): Promise<{ pass: number; failures: Array<Failure> }> {
|
||||
/**
|
||||
* @param {number} worker
|
||||
* @param {number} attempt
|
||||
* @returns {Promise<{ pass: number; failures: Array<Failure> }>}
|
||||
*/
|
||||
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<void>();
|
||||
/** @type {PromiseWithResolvers<void>} */
|
||||
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<Failure>();
|
||||
/** @type {Array<Failure>} */
|
||||
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<Failure>();
|
||||
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<string | undefined> {
|
||||
/**
|
||||
* @returns {Promise<string | undefined>}
|
||||
*/
|
||||
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<string | undefined> {
|
||||
|
||||
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<Failure>} */
|
||||
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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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*(?<path>[^\s]+)@(?<ref>[^\s#]+)(?:\s*#\s*(?<originalRef>[^\n]+))?/g;
|
||||
|
||||
type RegexGroups = {
|
||||
path: string;
|
||||
ref: string;
|
||||
originalRef?: string;
|
||||
};
|
||||
|
||||
const CACHE = new Map<string, string>();
|
||||
/** @type {Map<string, string>} */
|
||||
const CACHE = new Map();
|
||||
|
||||
const TagsSchema = z
|
||||
.object({
|
||||
@@ -33,12 +26,19 @@ const TagsSchema = z
|
||||
})
|
||||
.array();
|
||||
|
||||
async function updateAction(fullPath: string): Promise<void> {
|
||||
/**
|
||||
* @param {string} fullPath
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
await writeFile(fullPath, result);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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());
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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<string>;
|
||||
}
|
||||
>;
|
||||
/**
|
||||
* @typedef {Readonly<
|
||||
* | {
|
||||
* type: 'string' | 'date' | 'number' | 'jsx' | 'time';
|
||||
* }
|
||||
* | {
|
||||
* type: 'select';
|
||||
* validOptions: ReadonlyArray<string>;
|
||||
* }
|
||||
* >} ICUMessageParamType
|
||||
*/
|
||||
|
||||
export function getICUMessageParams(
|
||||
message: string,
|
||||
defaultRichTextElementNames: Array<string> = []
|
||||
): Map<string, ICUMessageParamType> {
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {string[]} defaultRichTextElementNames
|
||||
* @returns {Map<string, ICUMessageParamType>}
|
||||
*/
|
||||
export function getICUMessageParams(message, defaultRichTextElementNames = []) {
|
||||
const params = new Map();
|
||||
|
||||
function visitOptions(options: Record<string, PluralOrSelectOption>) {
|
||||
/**
|
||||
* @param {Record<string, PluralOrSelectOption>} options
|
||||
*/
|
||||
function visitOptions(options) {
|
||||
for (const option of Object.values(options)) {
|
||||
visit(option.value);
|
||||
}
|
||||
}
|
||||
|
||||
function visit(elements: ReadonlyArray<MessageFormatElement>) {
|
||||
/**
|
||||
* @param {ReadonlyArray<MessageFormatElement>} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<keyof typeof LCID>} */
|
||||
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<string, number> = {
|
||||
/** @satisfies {Record<string, number>} */
|
||||
export const LCID = {
|
||||
af_ZA: 1078,
|
||||
am_ET: 1118,
|
||||
ar_AE: 14337,
|
||||
@@ -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<typeof OptionalResourceSchema>} OptionalResourceType */
|
||||
|
||||
export const OptionalResourcesDictSchema = z.record(
|
||||
z.string(),
|
||||
OptionalResourceSchema
|
||||
);
|
||||
|
||||
/** @typedef {z.infer<typeof OptionalResourcesDictSchema>} OptionalResourcesDictType */
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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<Headers> {
|
||||
/**
|
||||
* @param {AuthenticateOptionsType} options
|
||||
* @returns {Promise<Headers>}
|
||||
*/
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -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<number>} */
|
||||
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);
|
||||
}
|
||||
+4
-2
@@ -4,7 +4,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>TextSecure test runner</title>
|
||||
<title>
|
||||
Signal Desktop (The Artist Formerly Known as TextSecure) test runner
|
||||
</title>
|
||||
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
|
||||
<link rel="stylesheet" href="../stylesheets/manifest.css" />
|
||||
<link rel="stylesheet" href="../stylesheets/tailwind.css" />
|
||||
@@ -16,6 +18,6 @@
|
||||
type="text/javascript"
|
||||
src="../node_modules/mocha/mocha.js"
|
||||
></script>
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
<script type="module" src="test.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2014 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
/// <reference path="../ts/window.d.ts">
|
||||
|
||||
/*
|
||||
* 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();
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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<Array<string>> {
|
||||
await notarizeUniversalDMG(result);
|
||||
return [];
|
||||
}
|
||||
@@ -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<void> {
|
||||
await pruneMacOSRelease(context);
|
||||
await fuseElectron(context);
|
||||
await copyPacks(context);
|
||||
await ensureLinuxFilePermissions(context);
|
||||
}
|
||||
@@ -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<void> {
|
||||
// This must be the last step
|
||||
await notarize(context);
|
||||
}
|
||||
@@ -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<any> {
|
||||
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<LocalDependency> = 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<FetchedDependency> = 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<string, Set<string>>();
|
||||
|
||||
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<string>;
|
||||
}>({
|
||||
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<string>;
|
||||
}>({
|
||||
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);
|
||||
});
|
||||
@@ -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<void> {
|
||||
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<string, Array<[string, string]>> = 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());
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 = `<description>${englishDescription}</description>\n`;
|
||||
let allMessages = `<message>${englishMessage}</message>\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 xml:lang="${localeName}">${description}</description>\n`;
|
||||
|
||||
const message = data[template.message]?.messageformat ?? englishMessage;
|
||||
allMessages += ` <message xml:lang="${localeName}">${message}</message>\n`;
|
||||
}
|
||||
|
||||
const targetPath = path.join(ROOT_DIR, 'build', template.name);
|
||||
let targetContent = template.content;
|
||||
|
||||
targetContent = targetContent.replace(
|
||||
'<!-- <description>{description}</description> -->',
|
||||
allDescriptions
|
||||
);
|
||||
targetContent = targetContent.replace(
|
||||
'<!-- <message>{message}</message> -->',
|
||||
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);
|
||||
});
|
||||
@@ -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<string>;
|
||||
}): Promise<ReadonlyArray<string>> {
|
||||
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<string | null>();
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user