Compare commits
385 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0019a1079 | ||
|
|
21f819184d | ||
|
|
3ee1058f7f | ||
|
|
8107ab3ad8 | ||
|
|
92012b3546 | ||
|
|
79004c7727 | ||
|
|
4492e2f799 | ||
|
|
d553336adc | ||
|
|
86d105dea0 | ||
|
|
83617e3ff3 | ||
|
|
3dfc580a92 | ||
|
|
21bca063a7 | ||
|
|
93cbb121f1 | ||
|
|
5331862e44 | ||
|
|
d829a84af9 | ||
|
|
7c467ef85c | ||
|
|
c6e087b842 | ||
|
|
83dcbfb143 | ||
|
|
72afecc4f3 | ||
|
|
2edee6f807 | ||
|
|
dfec1ef0fb | ||
|
|
eab19262cb | ||
|
|
a36e7aad48 | ||
|
|
90ae26db67 | ||
|
|
4c4b2aa353 | ||
|
|
240a262285 | ||
|
|
c6996e6184 | ||
|
|
c80b7e2744 | ||
|
|
55c5c00e26 | ||
|
|
9be0fabd94 | ||
|
|
97721dff60 | ||
|
|
4aca3b9445 | ||
|
|
80fb55fed0 | ||
|
|
cb4fac7bb4 | ||
|
|
49d87a5f6f | ||
|
|
6c37a4005b | ||
|
|
86c2a8420d | ||
|
|
d0235ff8f1 | ||
|
|
cfe2193db0 | ||
|
|
7b1db7325c | ||
|
|
39a851c4c4 | ||
|
|
734f710345 | ||
|
|
12ba35bcd5 | ||
|
|
9ba4145f00 | ||
|
|
a23020d18a | ||
|
|
4c3ec21815 | ||
|
|
e74e4d1e02 | ||
|
|
128e6e1e5e | ||
|
|
8853dead2b | ||
|
|
fd1e60d46e | ||
|
|
04a18e7812 | ||
|
|
ab91e69cac | ||
|
|
8140261b5b | ||
|
|
01dde9aac3 | ||
|
|
1db0ad06b2 | ||
|
|
12c7df49f3 | ||
|
|
108241e477 | ||
|
|
f5020bde61 | ||
|
|
16239c00b8 | ||
|
|
a4888e69bf | ||
|
|
2a437ce2e2 | ||
|
|
8d32b9cf9d | ||
|
|
4e6edd921a | ||
|
|
cb9b9cacaf | ||
|
|
e11f4e8651 | ||
|
|
48f7e72cbd | ||
|
|
6d47e89e38 | ||
|
|
6a31fbc05c | ||
|
|
8c5073512f | ||
|
|
d7082bad17 | ||
|
|
bd9a23c5ef | ||
|
|
6914bf38f9 | ||
|
|
21e2b03a92 | ||
|
|
e1e09e3a8b | ||
|
|
ffd3b9ea35 | ||
|
|
2f772e96ab | ||
|
|
4eddde9922 | ||
|
|
add9b36540 | ||
|
|
34dd2c7f70 | ||
|
|
8e1728ff63 | ||
|
|
37b5b39a3e | ||
|
|
a0af83d748 | ||
|
|
9103655a00 | ||
|
|
2239a46163 | ||
|
|
3a9a8bf7a8 | ||
|
|
f39a576b67 | ||
|
|
866ca8e830 | ||
|
|
f00b79e3e2 | ||
|
|
1dbb748c9c | ||
|
|
9daca313d3 | ||
|
|
851d8bf39f | ||
|
|
b88af5db21 | ||
|
|
fa3634c6d9 | ||
|
|
706c0020cc | ||
|
|
2ddc8fc5fd | ||
|
|
99f704363d | ||
|
|
89550b31fe | ||
|
|
433437df27 | ||
|
|
b5bfaff82d | ||
|
|
7ec0d0e577 | ||
|
|
0e2c07324f | ||
|
|
a11ba29c48 | ||
|
|
a1dfc702d9 | ||
|
|
bd36b19a84 | ||
|
|
42097d7bd8 | ||
|
|
55f73e702c | ||
|
|
88f989cba5 | ||
|
|
ca0990c001 | ||
|
|
8aea456151 | ||
|
|
4ab8ef745d | ||
|
|
d4720fec1e | ||
|
|
5088d02e01 | ||
|
|
02bd907768 | ||
|
|
865d37de64 | ||
|
|
0353eaf61f | ||
|
|
2524f83079 | ||
|
|
2c3ddcd6b9 | ||
|
|
c83efc3b6e | ||
|
|
a7e78e9a7f | ||
|
|
0cc5bb1b50 | ||
|
|
a8f91c2c20 | ||
|
|
d7e25ff415 | ||
|
|
ff358a59d1 | ||
|
|
ab4d2190e8 | ||
|
|
e2628dc7f3 | ||
|
|
ca5100f602 | ||
|
|
410216e2ae | ||
|
|
5ee6a0c1b6 | ||
|
|
3f871dcbaa | ||
|
|
a949293617 | ||
|
|
7055569291 | ||
|
|
5806c75882 | ||
|
|
89f418b11f | ||
|
|
1ffe46f587 | ||
|
|
ef970f74c8 | ||
|
|
57aa58506d | ||
|
|
4bd35d7378 | ||
|
|
81130ec712 | ||
|
|
ac252557ac | ||
|
|
0c4b7fc537 | ||
|
|
65db74055d | ||
|
|
fd19b59561 | ||
|
|
a106a82488 | ||
|
|
da8bed8b23 | ||
|
|
e1a624bde5 | ||
|
|
1cfc6c572b | ||
|
|
8a6dd0b11a | ||
|
|
0304dbc5a6 | ||
|
|
8686ed8f27 | ||
|
|
20586294d4 | ||
|
|
d25e9b6bff | ||
|
|
6b6bd053be | ||
|
|
5fc45209f9 | ||
|
|
58be82c01a | ||
|
|
3951a058a8 | ||
|
|
bf4bfbdaad | ||
|
|
90194e39c2 | ||
|
|
346c483787 | ||
|
|
958f754c96 | ||
|
|
38304b2146 | ||
|
|
44332410d8 | ||
|
|
0ea3b1bbc7 | ||
|
|
00e5282361 | ||
|
|
0aa27d3d9b | ||
|
|
6482dff544 | ||
|
|
de4ce71aba | ||
|
|
6cf6d2ebee | ||
|
|
568925f0f7 | ||
|
|
03dd5f2b9f | ||
|
|
55b24e25af | ||
|
|
d47554abdd | ||
|
|
e15c3adddc | ||
|
|
e0ef26bb4f | ||
|
|
c8acf86e6f | ||
|
|
1bf2664b58 | ||
|
|
7c68acfb1e | ||
|
|
37341680a6 | ||
|
|
1b2a3e7b28 | ||
|
|
6f1a6ace98 | ||
|
|
6441519468 | ||
|
|
b73d17d69d | ||
|
|
eecb7bf966 | ||
|
|
b0ed5f4a9b | ||
|
|
659fb39d28 | ||
|
|
9ba7277c72 | ||
|
|
d5f8c1a71a | ||
|
|
54b5772536 | ||
|
|
be3c6f307d | ||
|
|
d33119891e | ||
|
|
2f2f2aab37 | ||
|
|
0c907cdc8a | ||
|
|
d2b13b8cbd | ||
|
|
432c2ae47a | ||
|
|
d7ee96cc04 | ||
|
|
2ed50fa045 | ||
|
|
47ea47a914 | ||
|
|
77d48f5565 | ||
|
|
97106e0d6e | ||
|
|
de6a50a972 | ||
|
|
8335c1263e | ||
|
|
592e1b4476 | ||
|
|
71fe87c611 | ||
|
|
d6aad9d6c8 | ||
|
|
836eb19aef | ||
|
|
52cf12170b | ||
|
|
328dc05de3 | ||
|
|
a8722716c1 | ||
|
|
71a3e5788b | ||
|
|
d707a220cb | ||
|
|
9b9dd9f867 | ||
|
|
a90dca4e81 | ||
|
|
321a2feed6 | ||
|
|
93e3cd1311 | ||
|
|
674471e633 | ||
|
|
f799036e39 | ||
|
|
43f6b17c66 | ||
|
|
120e359768 | ||
|
|
2dea157233 | ||
|
|
04d9a109b2 | ||
|
|
7eaad06b30 | ||
|
|
54c436c48b | ||
|
|
f71f793411 | ||
|
|
7745835143 | ||
|
|
3ab39b61b7 | ||
|
|
1902d88de9 | ||
|
|
1fa5a98e44 | ||
|
|
6b3bb33386 | ||
|
|
781a60cc65 | ||
|
|
4b79efa033 | ||
|
|
d791bb880c | ||
|
|
ec4a696d59 | ||
|
|
37e50ea8ec | ||
|
|
3409302ee3 | ||
|
|
a8b620b23b | ||
|
|
dcc64d35cb | ||
|
|
bd4bb8ddff | ||
|
|
4b6be6f27d | ||
|
|
9c24615058 | ||
|
|
35d8fe53d7 | ||
|
|
8606182c21 | ||
|
|
db01cc2a64 | ||
|
|
f0b7f604f1 | ||
|
|
aa19f771b5 | ||
|
|
6dc2420f81 | ||
|
|
754c3cca58 | ||
|
|
4fcfc2d754 | ||
|
|
6f1915c424 | ||
|
|
5a592f95c7 | ||
|
|
281de1b6ca | ||
|
|
593f73c51a | ||
|
|
d3892510a9 | ||
|
|
ccf899f70d | ||
|
|
5f6edbdaae | ||
|
|
4b2d27dcb0 | ||
|
|
f4cdf08bbc | ||
|
|
0122ae3c9a | ||
|
|
bb9bb142eb | ||
|
|
dd19ab4777 | ||
|
|
40a9c9387b | ||
|
|
92d6c38d07 | ||
|
|
3be6b65f1a | ||
|
|
2a3f51e250 | ||
|
|
88ec0a7b88 | ||
|
|
5fe6a8212f | ||
|
|
8433e9a33d | ||
|
|
d680b7c5e2 | ||
|
|
6050b239eb | ||
|
|
530973ad09 | ||
|
|
1b1f33d505 | ||
|
|
8a6b993825 | ||
|
|
b0d49f77d2 | ||
|
|
862162b8fa | ||
|
|
272465e1b2 | ||
|
|
e605a2ff14 | ||
|
|
e11519ec7e | ||
|
|
56bf08738f | ||
|
|
4a22305de2 | ||
|
|
817337129b | ||
|
|
bd81cf103a | ||
|
|
7bd835561a | ||
|
|
bb07abb8b1 | ||
|
|
09b7cebb6b | ||
|
|
f265acc99a | ||
|
|
26905a67ab | ||
|
|
e48c948baf | ||
|
|
1d53ccfee1 | ||
|
|
a28d941ee9 | ||
|
|
400fd7ded1 | ||
|
|
d281b60ee7 | ||
|
|
cb2b0708fb | ||
|
|
eedbe099b4 | ||
|
|
77455b5217 | ||
|
|
bba214676b | ||
|
|
f0cc24a2df | ||
|
|
2e3a80b556 | ||
|
|
2bdfc4979e | ||
|
|
60a1e12545 | ||
|
|
453d56986e | ||
|
|
2979366c2e | ||
|
|
b3c84eb21c | ||
|
|
d0bf922b6d | ||
|
|
91e0f526da | ||
|
|
662e10831d | ||
|
|
977712cbe4 | ||
|
|
7d7c1d0249 | ||
|
|
159712ee24 | ||
|
|
ed14685c58 | ||
|
|
2bea0fdf6d | ||
|
|
0605a1afb8 | ||
|
|
e68b9d81a6 | ||
|
|
dc56d2656d | ||
|
|
230af12965 | ||
|
|
8bfd6a7475 | ||
|
|
f644c06474 | ||
|
|
11d5256a1c | ||
|
|
367c745d4c | ||
|
|
e18638325b | ||
|
|
9fe65f3d3b | ||
|
|
229ac20549 | ||
|
|
0dcdbd9f23 | ||
|
|
38799c5db7 | ||
|
|
70608fbc41 | ||
|
|
0ab2412202 | ||
|
|
54fde1dc96 | ||
|
|
d454fe0fef | ||
|
|
6cbd3b9334 | ||
|
|
3d624fc6d0 | ||
|
|
af73cf5094 | ||
|
|
0dbadcbbbb | ||
|
|
f3595e0784 | ||
|
|
5e683b0c1a | ||
|
|
a45114f8ad | ||
|
|
83764b81b2 | ||
|
|
e88bf72c73 | ||
|
|
0cecca0e0b | ||
|
|
ab52c4cd27 | ||
|
|
0d696b15ac | ||
|
|
1a9f7064bf | ||
|
|
41bff2f5b9 | ||
|
|
de0d430b72 | ||
|
|
9ff4e75b22 | ||
|
|
f79e3ede2b | ||
|
|
774522cfc9 | ||
|
|
9c68f2a747 | ||
|
|
ceca70301d | ||
|
|
8bd2caccf9 | ||
|
|
c11d503240 | ||
|
|
9e5bd39b3f | ||
|
|
245c98b875 | ||
|
|
9d26bd5ed2 | ||
|
|
3229859157 | ||
|
|
83b7305258 | ||
|
|
64b2247f1d | ||
|
|
0f3f0cb0fb | ||
|
|
236470da22 | ||
|
|
31ae6a1e44 | ||
|
|
77367df528 | ||
|
|
5dbdfaf5c5 | ||
|
|
99da687995 | ||
|
|
3d04a8ba0b | ||
|
|
f665b35478 | ||
|
|
fc12ea015a | ||
|
|
d8ef296bc3 | ||
|
|
a10783f4e0 | ||
|
|
2dd05d0221 | ||
|
|
bb284b6829 | ||
|
|
bb111a3107 | ||
|
|
e158261fea | ||
|
|
b7ca8f278a | ||
|
|
caa10d02c3 | ||
|
|
224bb811e1 | ||
|
|
d8df1e2869 | ||
|
|
cb8886ebd9 | ||
|
|
f1f2055058 | ||
|
|
35328c39ed | ||
|
|
d4b1c1c9f7 | ||
|
|
346876171d | ||
|
|
a048f83dbc | ||
|
|
70f111e868 | ||
|
|
f975d864c7 | ||
|
|
6d072b4546 | ||
|
|
df99252d82 | ||
|
|
e7544a5565 | ||
|
|
61095cc0a1 | ||
|
|
e8efc3c660 |
@ -1,20 +1,24 @@
|
||||
// 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),
|
||||
process.env.SIGNAL_ENV === 'storybook' &&
|
||||
import.meta.resolve('@babel/plugin-transform-runtime'),
|
||||
].filter(plugin => {
|
||||
return typeof plugin === 'string';
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@ -1,42 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const rule = require('./enforce-tw');
|
||||
const RuleTester = require('eslint').RuleTester;
|
||||
|
||||
const message = 'Tailwind classes must be wrapped with tw()';
|
||||
|
||||
// avoid triggering mocha's global leak detection
|
||||
require('@typescript-eslint/parser');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('enforce-tw', rule, {
|
||||
valid: [
|
||||
{ code: `classNames("foo")` },
|
||||
{ code: `<div className="foo"/>` },
|
||||
{ code: `tw("flex")` },
|
||||
],
|
||||
invalid: [
|
||||
{ code: `classNames("flex")`, errors: [{ message }] },
|
||||
{ code: `<div className="flex"/>`, errors: [{ message }] },
|
||||
{ code: `<div className={"flex"}/>`, errors: [{ message }] },
|
||||
{ code: `classNames("foo", "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames(cond ? "foo" : "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames(cond ? "flex" : "foo")`, errors: [{ message }] },
|
||||
{ code: `classNames(cond && "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames(cond || "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames(cond ?? "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames("foo" + "flex")`, errors: [{ message }] },
|
||||
{ code: `classNames("flex" + "foo")`, errors: [{ message }] },
|
||||
],
|
||||
});
|
||||
@ -1,134 +0,0 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const rule = require('./file-suffix.js');
|
||||
const RuleTester = require('eslint').RuleTester;
|
||||
|
||||
// avoid triggering mocha's global leak detection
|
||||
require('@typescript-eslint/parser');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('file-suffix', rule, {
|
||||
valid: [
|
||||
// Allowed references
|
||||
...[
|
||||
['std', '', ['std']],
|
||||
['dom', 'window.addEventListener();', ['std', 'dom']],
|
||||
['node', 'require("node:fs");', ['std', 'node']],
|
||||
[
|
||||
'preload',
|
||||
'import { ipcRenderer } from "electron";',
|
||||
['std', 'node', 'preload'],
|
||||
],
|
||||
[
|
||||
'main',
|
||||
'import { autoUpdater } from "electron";',
|
||||
['std', 'node', 'main'],
|
||||
],
|
||||
]
|
||||
.map(([fileSuffix, requiredLine, depSuffixes]) => {
|
||||
return depSuffixes.map(depSuffix => {
|
||||
return {
|
||||
name: `importing ${depSuffix} from ${fileSuffix}`,
|
||||
filename: `a.${fileSuffix}.ts`,
|
||||
code: `
|
||||
import { x } from './b.${depSuffix}.js';
|
||||
${requiredLine}
|
||||
`,
|
||||
globals: {
|
||||
window: 'writable',
|
||||
require: 'readable',
|
||||
},
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat(),
|
||||
|
||||
{
|
||||
name: 'type import should have no effect',
|
||||
filename: 'a.std.ts',
|
||||
code: `import type { ReadonlyDeep } from './b.dom.js'`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Disallowed references
|
||||
...[
|
||||
['std', ['dom', 'node', 'preload', 'main']],
|
||||
['dom', ['node', 'preload', 'main']],
|
||||
['node', ['preload', 'main']],
|
||||
['preload', ['main']],
|
||||
['main', ['dom', 'preload']],
|
||||
]
|
||||
.map(([fileSuffix, depSuffixes]) => {
|
||||
return depSuffixes.map(depSuffix => {
|
||||
return {
|
||||
name: `importing ${depSuffix} from ${fileSuffix}`,
|
||||
filename: `a.${fileSuffix}.ts`,
|
||||
code: `import { x } from './b.${depSuffix}.js'`,
|
||||
errors: [
|
||||
{
|
||||
message: `Invalid suffix ${fileSuffix}, expected: ${depSuffix}`,
|
||||
type: 'Program',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
})
|
||||
.flat(),
|
||||
|
||||
...['dom', 'node', 'preload', 'main'].map(suffix => {
|
||||
return {
|
||||
name: `no ${suffix} imports`,
|
||||
filename: `a.${suffix}.ts`,
|
||||
code: '',
|
||||
errors: [
|
||||
{
|
||||
message: `Invalid suffix ${suffix}, expected: std`,
|
||||
type: 'Program',
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
|
||||
// Invalid imports
|
||||
{
|
||||
name: 'preload in main',
|
||||
filename: 'a.main.ts',
|
||||
code: `
|
||||
import { autoUpdater } from 'electron';
|
||||
import './b.preload.js';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: 'Invalid import/reference for suffix: main',
|
||||
type: 'ImportDeclaration',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'main in preload',
|
||||
filename: 'a.preload.ts',
|
||||
code: `
|
||||
import { ipcRenderer } from 'electron';
|
||||
import './b.main.js';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
message: 'Invalid suffix preload, expected: main',
|
||||
type: 'Program',
|
||||
},
|
||||
{
|
||||
message: 'Invalid import/reference for suffix: main',
|
||||
type: 'ImportSpecifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1,58 +0,0 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
function isReadOnlyDeep(node, scope) {
|
||||
if (node.type !== 'TSTypeReference') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let reference = scope.references.find(reference => {
|
||||
return reference.identifier === node.typeName;
|
||||
});
|
||||
|
||||
let variable = reference.resolved;
|
||||
if (variable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let defs = variable.defs;
|
||||
if (defs.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [def] = defs;
|
||||
|
||||
return (
|
||||
def.type === 'ImportBinding' &&
|
||||
def.parent.type === 'ImportDeclaration' &&
|
||||
def.parent.source.type === 'Literal' &&
|
||||
def.parent.source.value === 'type-fest'
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: false,
|
||||
fixable: false,
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
TSTypeAliasDeclaration(node) {
|
||||
let scope = context.getScope(node);
|
||||
|
||||
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.id,
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -1,79 +0,0 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const rule = require('./type-alias-readonlydeep');
|
||||
const RuleTester = require('eslint').RuleTester;
|
||||
|
||||
// avoid triggering mocha's global leak detection
|
||||
require('@typescript-eslint/parser');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('type-alias-readonlydeep', rule, {
|
||||
valid: [
|
||||
{
|
||||
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
|
||||
},
|
||||
{
|
||||
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `type Foo = {}`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
type: 'Identifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `type Foo = Bar<{}>`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
type: 'Identifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
type: 'Identifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
type: 'Identifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
type: 'Identifier',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1,37 +0,0 @@
|
||||
components/**
|
||||
coverage/**
|
||||
dist/**
|
||||
release/**
|
||||
|
||||
# Github workflows
|
||||
.github/**
|
||||
|
||||
# Generated files
|
||||
js/curve/*
|
||||
js/components.js
|
||||
js/util_worker.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
test/test.js
|
||||
ts/protobuf/compiled.std.d.ts
|
||||
storybook-static/**
|
||||
build/ICUMessageParams.d.ts
|
||||
|
||||
# Third-party files
|
||||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
js/calling-tools/**
|
||||
|
||||
# TypeScript generated files
|
||||
build/**/*.js
|
||||
app/**/*.js
|
||||
ts/**/*.js
|
||||
|
||||
.eslintrc.js
|
||||
webpack.config.ts
|
||||
preload.bundle.*
|
||||
preload.wrapper.*
|
||||
bundles/**
|
||||
|
||||
# Sticker Creator has its own eslint config
|
||||
sticker-creator/**
|
||||
485
.eslintrc.js
485
.eslintrc.js
@ -1,485 +0,0 @@
|
||||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
const rules = {
|
||||
'comma-dangle': [
|
||||
'error',
|
||||
{
|
||||
arrays: 'always-multiline',
|
||||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
functions: 'never',
|
||||
},
|
||||
],
|
||||
|
||||
// No omitting braces, keep on the same line
|
||||
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||
curly: ['error', 'all'],
|
||||
|
||||
// Immer support
|
||||
'no-param-reassign': [
|
||||
'error',
|
||||
{
|
||||
props: true,
|
||||
ignorePropertyModificationsForRegex: ['^draft'],
|
||||
ignorePropertyModificationsFor: ['acc', 'ctx', 'context'],
|
||||
},
|
||||
],
|
||||
|
||||
// Always use === and !== except when directly comparing to null
|
||||
// (which only will equal null or undefined)
|
||||
eqeqeq: ['error', 'always', { null: 'never' }],
|
||||
|
||||
// prevents us from accidentally checking in exclusive tests (`.only`):
|
||||
'mocha/no-exclusive-tests': 'error',
|
||||
|
||||
// encourage consistent use of `async` / `await` instead of `then`
|
||||
'more/no-then': 'error',
|
||||
|
||||
// it helps readability to put public API at top,
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
|
||||
// useful for unused or internal fields
|
||||
'no-underscore-dangle': 'off',
|
||||
|
||||
// Temp: We have because TypeScript's `allowUnreachableCode` option is on.
|
||||
'no-unreachable': 'error',
|
||||
|
||||
// though we have a logger, we still remap console to log to disk
|
||||
'no-console': 'error',
|
||||
|
||||
// consistently place operators at end of line except ternaries
|
||||
'operator-linebreak': [
|
||||
'error',
|
||||
'after',
|
||||
{ overrides: { '?': 'ignore', ':': 'ignore' } },
|
||||
],
|
||||
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
{ avoidEscape: true, allowTemplateLiterals: false },
|
||||
],
|
||||
|
||||
'no-continue': 'off',
|
||||
'lines-between-class-members': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
|
||||
// Prettier overrides:
|
||||
'arrow-parens': 'off',
|
||||
'function-paren-newline': 'off',
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
// Prettier generally limits line length to 80 but sometimes goes over.
|
||||
// The `max-len` plugin doesn’t let us omit `code` so we set it to a
|
||||
// high value as a buffer to let Prettier control the line length:
|
||||
code: 999,
|
||||
// We still want to limit comments as before:
|
||||
comments: 90,
|
||||
ignoreUrls: true,
|
||||
},
|
||||
],
|
||||
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
|
||||
// Updated to reflect future airbnb standard
|
||||
// Allows for declaring defaultProps inside a class
|
||||
'react/static-property-placement': ['error', 'static public field'],
|
||||
|
||||
// JIRA: DESKTOP-657
|
||||
'react/sort-comp': 'off',
|
||||
|
||||
// We don't have control over the media we're sharing, so can't require
|
||||
// captions.
|
||||
'jsx-a11y/media-has-caption': 'off',
|
||||
|
||||
// We prefer named exports
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/enforce-node-protocol-usage': ['error', 'always'],
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
checkTypeImports: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Prefer functional components with default params
|
||||
'react/require-default-props': 'off',
|
||||
|
||||
// Empty fragments are used in adapters between models and react views.
|
||||
'react/jsx-no-useless-fragment': [
|
||||
'error',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Our code base has tons of arrow functions passed directly to components.
|
||||
'react/jsx-no-bind': 'off',
|
||||
|
||||
// Does not support forwardRef
|
||||
'react/no-unused-prop-types': 'off',
|
||||
|
||||
// Not useful for us as we have lots of complicated types.
|
||||
'react/destructuring-assignment': 'off',
|
||||
|
||||
'react/function-component-definition': [
|
||||
'error',
|
||||
{
|
||||
namedComponents: 'function-declaration',
|
||||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
|
||||
'react/display-name': 'error',
|
||||
|
||||
'react/jsx-pascal-case': ['error', { allowNamespace: true }],
|
||||
|
||||
// Allow returning values from promise executors for brevity.
|
||||
'no-promise-executor-return': 'off',
|
||||
|
||||
// Redux ducks use this a lot
|
||||
'default-param-last': 'off',
|
||||
|
||||
'jsx-a11y/label-has-associated-control': ['error', { assert: 'either' }],
|
||||
|
||||
'jsx-a11y/no-static-element-interactions': 'error',
|
||||
|
||||
'@typescript-eslint/no-non-null-assertion': ['error'],
|
||||
'@typescript-eslint/no-empty-interface': ['error'],
|
||||
'no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'TSInterfaceDeclaration',
|
||||
message:
|
||||
'Prefer `type`. Interfaces are mutable and less powerful, so we prefer `type` for simplicity.',
|
||||
},
|
||||
// Defaults
|
||||
{
|
||||
selector: 'ForInStatement',
|
||||
message:
|
||||
'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
|
||||
},
|
||||
{
|
||||
selector: 'LabeledStatement',
|
||||
message:
|
||||
'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
|
||||
},
|
||||
{
|
||||
selector: 'WithStatement',
|
||||
message:
|
||||
'`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
|
||||
},
|
||||
],
|
||||
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'error',
|
||||
{
|
||||
additionalHooks: '^(useSpring|useSprings)$',
|
||||
},
|
||||
],
|
||||
'local-rules/license-comments': 'error',
|
||||
};
|
||||
|
||||
const typescriptRules = {
|
||||
...rules,
|
||||
|
||||
'local-rules/file-suffix': 'error',
|
||||
|
||||
// Override brace style to enable typescript-specific syntax
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': [
|
||||
'error',
|
||||
'1tbs',
|
||||
{ allowSingleLine: false },
|
||||
],
|
||||
|
||||
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
|
||||
|
||||
'no-restricted-imports': 'off',
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'chai',
|
||||
importNames: ['expect', 'should', 'Should'],
|
||||
message: 'Please use assert',
|
||||
allowTypeImports: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// Overrides recommended by typescript-eslint
|
||||
// https://github.com/typescript-eslint/typescript-eslint/releases/tag/v4.0.0
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
'error',
|
||||
{
|
||||
checksVoidReturn: false,
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
// We allow "void promise", but new call-sites should use `drop(promise)`.
|
||||
'no-void': ['error', { allowAsStatement: true }],
|
||||
|
||||
'no-shadow': 'off',
|
||||
'no-useless-constructor': 'off',
|
||||
|
||||
// useful for unused parameters
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
|
||||
// Upgrade from a warning
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
||||
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
|
||||
// Future: Maybe switch to never and always use `satisfies`
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{
|
||||
assertionStyle: 'as',
|
||||
// Future: Maybe switch to allow-as-parameter or never
|
||||
objectLiteralTypeAssertions: 'allow',
|
||||
},
|
||||
],
|
||||
|
||||
// Already enforced by TypeScript
|
||||
'consistent-return': 'off',
|
||||
|
||||
// TODO: DESKTOP-4655
|
||||
'import/no-cycle': 'off',
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
{
|
||||
zones: [
|
||||
{
|
||||
target: ['ts/util', 'ts/types'],
|
||||
from: ['ts/components/**', 'ts/axo/**/*.dom.*'],
|
||||
message: 'Importing components is forbidden from ts/{util,types}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'local-rules/enforce-array-buffer': 'error',
|
||||
};
|
||||
|
||||
const TAILWIND_REPLACEMENTS = [
|
||||
// inset
|
||||
{ pattern: 'left-*', fix: 'start-*' },
|
||||
{ pattern: 'right-*', fix: 'end-*' },
|
||||
// margin
|
||||
{ pattern: 'ml-*', fix: 'ms-*' },
|
||||
{ pattern: 'mr-*', fix: 'me-*' },
|
||||
// padding
|
||||
{ pattern: 'pl-*', fix: 'ps-*' },
|
||||
{ pattern: 'pr-*', fix: 'pe-*' },
|
||||
// border
|
||||
{ pattern: 'border-l-*', fix: 'border-s-*' },
|
||||
{ pattern: 'border-r-*', fix: 'border-e-*' },
|
||||
// border-radius
|
||||
{ pattern: 'rounded-l', fix: 'rounded-s' },
|
||||
{ pattern: 'rounded-r', fix: 'rounded-e' },
|
||||
{ pattern: 'rounded-tl', fix: 'rounded-ss' },
|
||||
{ pattern: 'rounded-tr', fix: 'rounded-se' },
|
||||
{ pattern: 'rounded-bl', fix: 'rounded-es' },
|
||||
{ pattern: 'rounded-br', fix: 'rounded-ee' },
|
||||
{ pattern: 'rounded-l-*', fix: 'rounded-s-*' },
|
||||
{ pattern: 'rounded-r-*', fix: 'rounded-e-*' },
|
||||
{ pattern: 'rounded-tl-*', fix: 'rounded-ss-*' },
|
||||
{ pattern: 'rounded-tr-*', fix: 'rounded-se-*' },
|
||||
{ pattern: 'rounded-bl-*', fix: 'rounded-es-*' },
|
||||
{ pattern: 'rounded-br-*', fix: 'rounded-ee-*' },
|
||||
// text-align
|
||||
{ pattern: 'text-left', fix: 'text-start' },
|
||||
{ pattern: 'text-right', fix: 'text-end' },
|
||||
// float
|
||||
{ pattern: 'float-left', fix: 'float-start' },
|
||||
{ pattern: 'float-right', fix: 'float-end' },
|
||||
// clear
|
||||
{ pattern: 'clear-left', fix: 'clear-start' },
|
||||
{ pattern: 'clear-right', fix: 'clear-end' },
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
'import/core-modules': ['electron'],
|
||||
},
|
||||
|
||||
extends: ['airbnb-base', 'prettier'],
|
||||
|
||||
plugins: ['mocha', 'more', 'local-rules'],
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'ts/**/*.ts',
|
||||
'ts/**/*.tsx',
|
||||
'app/**/*.ts',
|
||||
'app/**/*.tsx',
|
||||
'build/intl-linter/**/*.ts',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'airbnb-typescript-prettier',
|
||||
],
|
||||
rules: typescriptRules,
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.stories.tsx',
|
||||
'ts/build/**',
|
||||
'ts/test-*/**',
|
||||
'build/intl-linter/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
...typescriptRules,
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'react/no-array-index-key': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['ts/state/ducks/**/*.ts'],
|
||||
rules: {
|
||||
'local-rules/type-alias-readonlydeep': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['ts/**/*_test.*.{ts,tsx}'],
|
||||
rules: {
|
||||
'func-names': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['ts/**/*.tsx'],
|
||||
plugins: ['better-tailwindcss'],
|
||||
settings: {
|
||||
'better-tailwindcss': {
|
||||
entryPoint: './stylesheets/tailwind-config.css',
|
||||
callees: ['tw'],
|
||||
attributes: [],
|
||||
variables: [],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'local-rules/enforce-tw': 'error',
|
||||
|
||||
// stylistic: Enforce consistent line wrapping for tailwind classes. (recommended, autofix)
|
||||
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
|
||||
// stylistic: Enforce a consistent order for tailwind classes. (recommended, autofix)
|
||||
'better-tailwindcss/enforce-consistent-class-order': 'error',
|
||||
// stylistic: Enforce consistent variable syntax. (autofix)
|
||||
'better-tailwindcss/enforce-consistent-variable-syntax': 'error',
|
||||
// stylistic: Enforce consistent position of the important modifier. (autofix)
|
||||
'better-tailwindcss/enforce-consistent-important-position': 'error',
|
||||
// stylistic: Enforce shorthand class names. (autofix)
|
||||
'better-tailwindcss/enforce-shorthand-classes': 'error',
|
||||
// stylistic: Remove duplicate classes. (autofix)
|
||||
'better-tailwindcss/no-duplicate-classes': 'error',
|
||||
// stylistic: Remove deprecated classes. (autofix)
|
||||
'better-tailwindcss/no-deprecated-classes': 'off',
|
||||
// stylistic: Disallow unnecessary whitespace in tailwind classes. (autofix)
|
||||
'better-tailwindcss/no-unnecessary-whitespace': 'error',
|
||||
// correctness: Report classes not registered with tailwindcss. (recommended)
|
||||
'better-tailwindcss/no-unregistered-classes': 'error',
|
||||
// correctness: Report classes that produce conflicting styles.
|
||||
'better-tailwindcss/no-conflicting-classes': 'error',
|
||||
// correctness: Disallow restricted classes. (autofix)
|
||||
'better-tailwindcss/no-restricted-classes': [
|
||||
'error',
|
||||
{
|
||||
restrict: [
|
||||
{
|
||||
pattern: '\\[#[a-fA-F0-9]{3,8}?\\]', // ex: "text-[#fff]"
|
||||
message: 'No arbitrary hex values',
|
||||
},
|
||||
{
|
||||
pattern: '\\[rgba?\\(.*\\)\\]', // ex: "text-[rgb(255,255,255)]"
|
||||
message: 'No arbitrary rgb values',
|
||||
},
|
||||
{
|
||||
pattern: '\\[hsla?\\(.*\\)\\]', // ex: "text-[hsl(255,255,255)]"
|
||||
message: 'No arbitrary hsl values',
|
||||
},
|
||||
{
|
||||
pattern: '^.*!$', // ex: "p-4!"
|
||||
message: 'No !important modifiers',
|
||||
},
|
||||
{
|
||||
pattern: '^\\*+:.*', // ex: "*:mx-0",
|
||||
message: 'No child variants',
|
||||
},
|
||||
...TAILWIND_REPLACEMENTS.map(item => {
|
||||
const pattern = item.pattern.replace('*', '(.*)');
|
||||
const fix = item.fix.replace('*', '$2');
|
||||
return {
|
||||
message: `Use logical property ${item.fix} instead of ${item.pattern}`,
|
||||
pattern: `^(.*:)?${pattern}$`,
|
||||
fix: `$1${fix}`,
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['ts/axo/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
// Rule doesn't understand TypeScript namespaces
|
||||
'no-inner-declarations': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-redeclare': [
|
||||
'error',
|
||||
{
|
||||
ignoreDeclarationMerge: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': [
|
||||
'error',
|
||||
{
|
||||
allowHigherOrderFunctions: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
rules: {
|
||||
...rules,
|
||||
'import/no-unresolved': 'off',
|
||||
'import/extensions': 'off',
|
||||
},
|
||||
|
||||
reportUnusedDisableDirectives: true,
|
||||
};
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Backport-Action-Private
|
||||
|
||||
165
.github/workflows/benchmark.yml
vendored
165
.github/workflows/benchmark.yml
vendored
@ -1,165 +0,0 @@
|
||||
# Copyright 2020 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
name: Benchmark
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 */12 * * *'
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
strategy:
|
||||
matrix:
|
||||
metric:
|
||||
- startup
|
||||
- send
|
||||
- groupSend
|
||||
- largeGroupSendWithBlocks
|
||||
- largeGroupSend
|
||||
- convoOpen
|
||||
- callHistorySearch
|
||||
- backup
|
||||
include:
|
||||
- metric: startup
|
||||
script: ts/test-mock/benchmarks/startup_bench.node.js
|
||||
runCount: 10
|
||||
- metric: send
|
||||
script: ts/test-mock/benchmarks/send_bench.node.js
|
||||
runCount: 100
|
||||
- metric: groupSend
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 100
|
||||
conversationSize: 500
|
||||
- metric: largeGroupSendWithBlocks
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 50
|
||||
conversationSize: 500
|
||||
groupSize: 500
|
||||
contactCount: 500
|
||||
blockedCount: 10
|
||||
discardCount: 2
|
||||
- metric: largeGroupSend
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 20
|
||||
conversationSize: 50
|
||||
groupSize: 500
|
||||
contactCount: 500
|
||||
discardCount: 2
|
||||
- metric: convoOpen
|
||||
script: ts/test-mock/benchmarks/convo_open_bench.node.js
|
||||
runCount: 100
|
||||
- metric: callHistorySearch
|
||||
script: ts/test-mock/benchmarks/call_history_search_bench.node.js
|
||||
runCount: 100
|
||||
- metric: backup
|
||||
script: ts/test-mock/benchmarks/backup_bench.node.js
|
||||
|
||||
runs-on: ubuntu-22.04-8-cores
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && (!github.event.schedule || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Get system specs
|
||||
run: lsb_release -a
|
||||
- name: Get other system specs
|
||||
run: uname -a
|
||||
|
||||
- name: Clone Desktop repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# id: cache-sccache
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install xvfb and libpulse0
|
||||
run: sudo apt-get install xvfb libpulse0 || (sudo apt-get update && sudo apt-get install xvfb libpulse0)
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
# CC: sccache gcc
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- name: Build typescript
|
||||
run: pnpm run generate
|
||||
- name: Bundle
|
||||
run: pnpm run build:esbuild:prod
|
||||
- name: Create preload cache
|
||||
run: xvfb-run --auto-servernum pnpm run build:preload-cache
|
||||
|
||||
- name: Set MAX_CYCLES=2 on main
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
echo "MAX_CYCLES=2" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run ${{ matrix.metric }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
xvfb-run --auto-servernum node ${{ matrix.script }} | tee benchmark.log
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_ENV: production
|
||||
ELECTRON_ENABLE_STACK_DUMPING: on
|
||||
DEBUG: 'mock:benchmarks'
|
||||
ARTIFACTS_DIR: artifacts/${{ matrix.metric }}
|
||||
GROUP_SIZE: ${{ matrix.groupSize }}
|
||||
CONTACT_COUNT: ${{ matrix.contactCount }}
|
||||
BLOCKED_COUNT: ${{ matrix.blockedCount }}
|
||||
DISCARD_COUNT: ${{ matrix.discardCount }}
|
||||
RUN_COUNT: ${{ matrix.runCount }}
|
||||
CONVERSATION_SIZE: ${{ matrix.conversationSize }}
|
||||
|
||||
- name: Upload benchmark logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: logs
|
||||
path: artifacts
|
||||
|
||||
- name: Clone benchmark repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
|
||||
path: 'benchmark-results'
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
|
||||
- name: Build benchmark repo
|
||||
working-directory: benchmark-results
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
- name: Publish
|
||||
working-directory: benchmark-results
|
||||
run: |
|
||||
node ./bin/publish.js ../benchmark.log desktop.ci.performance.${{ matrix.metric }}
|
||||
env:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
445
.github/workflows/ci.yml
vendored
445
.github/workflows/ci.yml
vendored
@ -10,25 +10,45 @@ on:
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
name: Dependencies
|
||||
runs-on: ubuntu-22.04-8-cores
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: node --test .pnpmfile.mjs
|
||||
- run: pnpm audit --audit-level=high
|
||||
- run: pnpm audit signatures
|
||||
- run: pnpm dedupe --check
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04-8-cores
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- run: lsb_release -a
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@ -36,20 +56,11 @@ jobs:
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Restore cached .eslintcache and tsconfig.tsbuildinfo
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
id: cache-lint
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
key: lint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**', '.eslintrc.js', '.eslint/**', 'tsconfig.json') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
@ -57,13 +68,27 @@ jobs:
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- name: Install Sticker Creator node_modules
|
||||
run: pnpm install
|
||||
working-directory: sticker-creator
|
||||
|
||||
- name: Install libpulse0
|
||||
run: sudo apt-get install -y libpulse0 || (sudo apt-get update && sudo apt-get install -y libpulse0)
|
||||
- run: pnpm run generate
|
||||
- run: pnpm run lint
|
||||
- run: pnpm run build:db-schema --check
|
||||
- run: pnpm run lint-prettier
|
||||
- run: pnpm run lint-css
|
||||
- run: pnpm run check:types
|
||||
- run: pnpm run oxlint:ci
|
||||
- run: pnpm run lint-deps
|
||||
- run: pnpm run lint-license-comments
|
||||
- run: pnpm run lint-intl
|
||||
|
||||
- run: pnpm run lint-knip:all --reporter github-actions
|
||||
- run: pnpm run lint-knip:prod --reporter github-actions
|
||||
- name: Check acknowledgments file is up to date
|
||||
run: pnpm run build:acknowledgments
|
||||
env:
|
||||
@ -71,34 +96,26 @@ jobs:
|
||||
|
||||
- run: git diff --exit-code
|
||||
|
||||
- name: Update cached .eslintcache and tsconfig.tsbuildinfo
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
key: ${{ steps.cache-lint.outputs.cache-primary-key }}
|
||||
|
||||
macos:
|
||||
name: MacOS
|
||||
needs: lint
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-26-arm64
|
||||
if: github.ref == 'refs/heads/main'
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@ -106,7 +123,7 @@ jobs:
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
@ -118,9 +135,11 @@ jobs:
|
||||
# CXX: sccache clang++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: pnpm run generate
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
- run: pnpm run prepare-beta-build
|
||||
- run: pnpm run generate
|
||||
- run: pnpm run test-node
|
||||
- run: pnpm run test-electron
|
||||
env:
|
||||
@ -139,38 +158,45 @@ jobs:
|
||||
- name: Upload installer size
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
node ts/scripts/publish-installer-size.node.js macos-arm64
|
||||
node ts/scripts/publish-installer-size.node.js macos-x64
|
||||
node ts/scripts/publish-installer-size.node.js 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
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
linux:
|
||||
name: Linux
|
||||
needs: lint
|
||||
runs-on: ubuntu-22.04-8-cores
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04-8-cores
|
||||
arch: x64
|
||||
- os: ubuntu-22.04-arm64-4-cores
|
||||
arch: arm64
|
||||
steps:
|
||||
- run: lsb_release -a
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@ -181,7 +207,7 @@ jobs:
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
@ -193,17 +219,21 @@ jobs:
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- run: pnpm run generate
|
||||
- run: pnpm run prepare-beta-build
|
||||
- run: pnpm run generate
|
||||
|
||||
- name: Create bundle
|
||||
run: pnpm run build:esbuild:prod
|
||||
- name: Create preload cache
|
||||
run: xvfb-run --auto-servernum pnpm run build:preload-cache
|
||||
env:
|
||||
ARTIFACTS_DIR: artifacts/linux
|
||||
|
||||
- name: Set Linux build target architecture
|
||||
run: pnpm run prepare-linux-build deb ${{ matrix.arch }}
|
||||
|
||||
- name: Build with packaging .deb file
|
||||
run: pnpm run build:release --publish=never
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@ -223,15 +253,15 @@ jobs:
|
||||
|
||||
- name: Upload installer size
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
|
||||
run: node ts/scripts/publish-installer-size.node.js linux
|
||||
run: node scripts/publish-installer-size.mjs linux-${{ matrix.arch }}
|
||||
|
||||
- run: xvfb-run --auto-servernum pnpm run test-node
|
||||
|
||||
- name: Clone backup integration tests
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: 'signalapp/Signal-Message-Backup-Tests'
|
||||
ref: '33b3d0cd4367a898f5f8b4a2c57ee12ba7ec38ea'
|
||||
ref: 'a0f900243210efbedc72f0907c5d2f140385daa4'
|
||||
path: 'backup-integration-tests'
|
||||
|
||||
- run: xvfb-run --auto-servernum pnpm run test-electron
|
||||
@ -248,43 +278,46 @@ jobs:
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
windows:
|
||||
name: Windows
|
||||
needs: lint
|
||||
runs-on: windows-latest-8-cores
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- run: systeminfo
|
||||
- run: git config --global core.autocrlf false
|
||||
- run: git config --global core.eol lf
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- run: touch noop.js
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
NPM_CONFIG_NODE_GYP: ${{ github.workspace }}\noop.js
|
||||
|
||||
- run: pnpm run generate
|
||||
- run: pnpm run test-node
|
||||
- run: copy package.json temp.json
|
||||
- run: del package.json
|
||||
- run: type temp.json | findstr /v certificateSubjectName | findstr /v certificateSha1 > package.json
|
||||
- run: pnpm run prepare-beta-build
|
||||
- run: pnpm run generate
|
||||
- run: pnpm run test-node
|
||||
|
||||
- name: Create bundle
|
||||
run: pnpm run build:esbuild:prod
|
||||
- name: Create preload cache
|
||||
run: pnpm run build:preload-cache
|
||||
env:
|
||||
@ -303,7 +336,7 @@ jobs:
|
||||
|
||||
- name: Upload installer size
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' && github.ref == 'refs/heads/main' }}
|
||||
run: node ts/scripts/publish-installer-size.node.js windows
|
||||
run: node scripts/publish-installer-size.mjs windows
|
||||
|
||||
- run: pnpm run test-electron
|
||||
env:
|
||||
@ -316,7 +349,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
@ -330,11 +363,11 @@ jobs:
|
||||
working-directory: sticker-creator
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@ -353,14 +386,12 @@ jobs:
|
||||
- name: Check Sticker Creator linting
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test -- --run
|
||||
|
||||
mock-tests:
|
||||
name: Mock Tests
|
||||
needs: lint
|
||||
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
workerIndex: [0, 1, 2, 3]
|
||||
|
||||
@ -375,18 +406,18 @@ jobs:
|
||||
run: uname -a
|
||||
|
||||
- name: Clone Desktop repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@ -400,7 +431,7 @@ jobs:
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
@ -408,6 +439,7 @@ jobs:
|
||||
- name: Install Desktop node_modules
|
||||
run: |
|
||||
pnpm install
|
||||
./node_modules/.bin/install-electron
|
||||
sudo chown root node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 node_modules/.pnpm/electron@*/node_modules/electron/dist/chrome-sandbox
|
||||
env:
|
||||
@ -415,11 +447,12 @@ jobs:
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- name: Build typescript
|
||||
run: pnpm run generate
|
||||
- name: Bundle
|
||||
run: pnpm run build:esbuild:prod
|
||||
- name: Create preload cache
|
||||
run: xvfb-run --auto-servernum pnpm run build:preload-cache
|
||||
env:
|
||||
@ -453,45 +486,301 @@ jobs:
|
||||
|
||||
- name: Upload mock server test logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: logs-${{ matrix.workerIndex }}
|
||||
path: artifacts
|
||||
|
||||
check-min-os-version:
|
||||
name: Check Min OS Version
|
||||
needs: lint
|
||||
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04-8-cores, macos-latest, windows-latest-8-cores]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- run: uname -a
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: pnpm install
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- run: touch noop.js
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
- name: Install Desktop node_modules on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm install
|
||||
env:
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: ${{ github.workspace }}\noop.js
|
||||
|
||||
- run: pnpm generate:phase-0
|
||||
- name: Run OS version check
|
||||
run: |
|
||||
node ts/scripts/check-min-os-version.node.js
|
||||
node scripts/check-min-os-version.mjs
|
||||
|
||||
danger:
|
||||
name: Danger
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
package-manager-cache: false # Avoid cache key clashes
|
||||
- name: Install danger node_modules
|
||||
run: pnpm install
|
||||
working-directory: danger
|
||||
- name: Run DangerJS
|
||||
run: pnpm run danger:ci
|
||||
working-directory: danger
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
|
||||
storybook:
|
||||
name: Storybook
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# id: cache-sccache
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
# CC: sccache gcc
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- run: pnpm run build:storybook
|
||||
- run: ./node_modules/.bin/playwright install chromium
|
||||
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
|
||||
|
||||
benchmark:
|
||||
name: Benchmark
|
||||
strategy:
|
||||
matrix:
|
||||
metric:
|
||||
- startup
|
||||
- send
|
||||
- groupSend
|
||||
- largeGroupSendWithBlocks
|
||||
- largeGroupSend
|
||||
- convoOpen
|
||||
- callHistorySearch
|
||||
- backup
|
||||
include:
|
||||
- metric: startup
|
||||
script: ts/test-mock/benchmarks/startup_bench.node.js
|
||||
runCount: 10
|
||||
- metric: send
|
||||
script: ts/test-mock/benchmarks/send_bench.node.js
|
||||
runCount: 100
|
||||
- metric: groupSend
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 100
|
||||
conversationSize: 500
|
||||
- metric: largeGroupSendWithBlocks
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 50
|
||||
conversationSize: 500
|
||||
groupSize: 500
|
||||
contactCount: 500
|
||||
blockedCount: 10
|
||||
discardCount: 2
|
||||
- metric: largeGroupSend
|
||||
script: ts/test-mock/benchmarks/group_send_bench.node.js
|
||||
runCount: 20
|
||||
conversationSize: 50
|
||||
groupSize: 500
|
||||
contactCount: 500
|
||||
discardCount: 2
|
||||
- metric: convoOpen
|
||||
script: ts/test-mock/benchmarks/convo_open_bench.node.js
|
||||
runCount: 100
|
||||
- metric: callHistorySearch
|
||||
script: ts/test-mock/benchmarks/call_history_search_bench.node.js
|
||||
runCount: 100
|
||||
- metric: backup
|
||||
script: ts/test-mock/benchmarks/backup_bench.node.js
|
||||
|
||||
runs-on: ubuntu-22.04-8-cores
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Get system specs
|
||||
run: lsb_release -a
|
||||
- name: Get other system specs
|
||||
run: uname -a
|
||||
|
||||
- name: Clone Desktop repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# id: cache-sccache
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
- name: Install xvfb and libpulse0
|
||||
run: sudo apt-get install xvfb libpulse0 || (sudo apt-get update && sudo apt-get install xvfb libpulse0)
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
# CC: sccache gcc
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- name: Build typescript
|
||||
run: pnpm run generate
|
||||
- name: Create preload cache
|
||||
run: xvfb-run --auto-servernum pnpm run build:preload-cache
|
||||
|
||||
- name: Set MAX_CYCLES=2 on main
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
echo "MAX_CYCLES=2" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run ${{ matrix.metric }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
xvfb-run --auto-servernum ./node_modules/.bin/tsx \
|
||||
${{ matrix.script }} | tee benchmark.log
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_ENV: production
|
||||
ELECTRON_ENABLE_STACK_DUMPING: on
|
||||
DEBUG: 'mock:benchmarks'
|
||||
ARTIFACTS_DIR: artifacts/${{ matrix.metric }}
|
||||
GROUP_SIZE: ${{ matrix.groupSize }}
|
||||
CONTACT_COUNT: ${{ matrix.contactCount }}
|
||||
BLOCKED_COUNT: ${{ matrix.blockedCount }}
|
||||
DISCARD_COUNT: ${{ matrix.discardCount }}
|
||||
RUN_COUNT: ${{ matrix.runCount }}
|
||||
CONVERSATION_SIZE: ${{ matrix.conversationSize }}
|
||||
|
||||
- name: Upload benchmark logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: logs
|
||||
path: artifacts
|
||||
|
||||
- name: Clone benchmark repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
repository: 'signalapp/Signal-Desktop-Benchmarks-Private'
|
||||
path: 'benchmark-results'
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
|
||||
- name: Build benchmark repo
|
||||
working-directory: benchmark-results
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build
|
||||
|
||||
- name: Publish
|
||||
working-directory: benchmark-results
|
||||
run: |
|
||||
node ./bin/publish.js ../benchmark.log desktop.ci.performance.${{ matrix.metric }}
|
||||
env:
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
|
||||
auto-merge-ready:
|
||||
if: ${{ github.event_name == 'pull_request' && github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
name: Auto Merge Ready
|
||||
needs:
|
||||
- lint
|
||||
- linux
|
||||
- windows
|
||||
- sticker-creator
|
||||
- mock-tests
|
||||
- check-min-os-version
|
||||
- danger
|
||||
- storybook
|
||||
- benchmark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Ok
|
||||
run: echo ok
|
||||
|
||||
1
.github/workflows/commits.yml
vendored
1
.github/workflows/commits.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
jobs:
|
||||
linux:
|
||||
name: Commit Title Check
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
steps:
|
||||
|
||||
28
.github/workflows/danger.yml
vendored
28
.github/workflows/danger.yml
vendored
@ -1,28 +0,0 @@
|
||||
# Copyright 2020 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
name: Danger
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0 # fetch all history
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
package-manager-cache: false # Avoid cache key clashes
|
||||
- name: Install danger node_modules
|
||||
run: cd danger && pnpm install
|
||||
- name: Run DangerJS
|
||||
run: pnpm run danger:ci
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
19
.github/workflows/icu-book.yml
vendored
19
.github/workflows/icu-book.yml
vendored
@ -13,17 +13,17 @@ jobs:
|
||||
if: ${{ github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
# id: cache-sccache
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
@ -44,18 +44,21 @@ jobs:
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
# We rebuild in `electron:install-app-deps` that doesn't look at this
|
||||
# environment variable
|
||||
NPM_CONFIG_NODE_GYP: echo
|
||||
|
||||
- run: pnpm run build:storybook
|
||||
- run: ./node_modules/.bin/playwright install chromium
|
||||
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
|
||||
env:
|
||||
ARTIFACTS_DIR: stories
|
||||
- run: pnpm run build:esbuild
|
||||
- run: node ts/scripts/compile-stories-icu-lookup.node.js stories
|
||||
- run: pnpm run build:rolldown
|
||||
- run: node scripts/compile-stories-icu-lookup.mjs stories
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: desktop-test-icu
|
||||
path: stories
|
||||
@ -63,7 +66,7 @@ jobs:
|
||||
|
||||
- name: Upload release artifacts
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: desktop-${{ github.ref_name }}-icu
|
||||
path: stories
|
||||
|
||||
2
.github/workflows/notes.yml
vendored
2
.github/workflows/notes.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Notes-Action-Private
|
||||
|
||||
2
.github/workflows/release-notes.yml
vendored
2
.github/workflows/release-notes.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.AUTOMATED_GITHUB_PAT }}
|
||||
repository: signalapp/Signal-Release-Notes-Action-Private
|
||||
|
||||
@ -20,6 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Log info
|
||||
run: |
|
||||
@ -37,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Restore previous version file from cache
|
||||
id: restore-cache-version
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
key: ${{ matrix.package }}-version-git-ref-txt
|
||||
path: ~/version-git-ref.txt
|
||||
@ -66,6 +67,9 @@ jobs:
|
||||
echo "$VERSION_GIT_TAG" > ~/version-git-ref.txt
|
||||
echo "tag=$VERSION_GIT_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
BRANCH_PREFIX=$(echo "$VERSION_GIT_TAG" | grep -oE '[0-9]+\.[0-9]+')
|
||||
echo "git_branch=$BRANCH_PREFIX.x" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine if a build is needed
|
||||
id: should-run
|
||||
run: |
|
||||
@ -88,7 +92,18 @@ jobs:
|
||||
|
||||
- name: Cache latest version
|
||||
if: steps.should-run.outputs.result == 'true'
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
key: ${{ matrix.package }}-version-git-ref-txt
|
||||
path: ~/version-git-ref.txt
|
||||
|
||||
- name: Open issue on failure
|
||||
if: ${{ failure() && github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/issues \
|
||||
-d '{"title":"Reproducible build scheduler failed: ${{ steps.latest-version.outputs.tag }}","body":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"},"labels":["${{ steps.latest-version.outputs.git_branch }}"]}'
|
||||
|
||||
30
.github/workflows/reproducible-builds.yml
vendored
30
.github/workflows/reproducible-builds.yml
vendored
@ -21,6 +21,9 @@ jobs:
|
||||
linux:
|
||||
name: Linux deb
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Get system specs
|
||||
run: lsb_release -a
|
||||
@ -37,6 +40,9 @@ jobs:
|
||||
PARSED_VERSION=$(echo "${{ inputs.version_tag }}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+.*' | tr '-' '~')
|
||||
echo "PACKAGE_VERSION=$PARSED_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
BRANCH_PREFIX=$(echo "${{ inputs.version_tag }}" | grep -oE '[0-9]+\.[0-9]+')
|
||||
echo "git_branch=$BRANCH_PREFIX.x" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "# Reproducing ${{ inputs.package }} Linux deb" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Version: ${{ inputs.version_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@ -84,7 +90,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Clone Desktop git repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ steps.app_info.outputs.git_ref }}
|
||||
|
||||
@ -127,6 +133,7 @@ jobs:
|
||||
run: |
|
||||
cd release
|
||||
BUILT_FILE=$(ls | grep deb | tail -1)
|
||||
echo "built_file=$BUILT_FILE" >> $GITHUB_OUTPUT
|
||||
ACTUAL_SHA512=$(sha512sum $BUILT_FILE | cut -d' ' -f1)
|
||||
echo "actual_sha512=$ACTUAL_SHA512" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
@ -153,3 +160,24 @@ jobs:
|
||||
echo "❌ Build checksum verification failed!" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run diffoscope to find diffs
|
||||
if: failure()
|
||||
run: |
|
||||
apt-get download "$PACKAGE_NAME=$PACKAGE_VERSION"
|
||||
|
||||
docker run --rm -w $(pwd) -v $(pwd):$(pwd):ro \
|
||||
registry.salsa.debian.org/reproducible-builds/diffoscope@sha256:51fa7187f093c4cb75b386e7e4caa38ea5783b4a9204b517dc1c91dada209421 --no-progress --max-text-report-size 5000 --max-report-size 5000 --max-diff-block-lines 100 \
|
||||
release/${{ steps.build_checksum.outputs.built_file }} \
|
||||
${{ steps.download.outputs.deb_file }}
|
||||
|
||||
- name: Open issue on failure
|
||||
if: ${{ failure() && github.repository == 'signalapp/Signal-Desktop-Private' }}
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/${{ github.repository }}/issues \
|
||||
-d '{"title":"Reproducible build failed: ${{ inputs.version_tag }}","body":"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}","labels":["${{ steps.app_info.outputs.git_branch }}"]}'
|
||||
|
||||
50
.github/workflows/stories.yml
vendored
50
.github/workflows/stories.yml
vendored
@ -1,50 +0,0 @@
|
||||
# Copyright 2023 Signal Messenger, LLC
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
name: Stories
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
- name: Setup node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'pnpm-lock.yaml'
|
||||
- name: Cache .electron-gyp
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: ~/.electron-gyp
|
||||
key: electron-gyp-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
# - name: Setup sccache
|
||||
# uses: mozilla-actions/sccache-action@054db53350805f83040bf3e6e9b8cf5a139aa7c9 # v0.0.7
|
||||
# - name: Restore sccache
|
||||
# uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
# id: cache-sccache
|
||||
# with:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
env:
|
||||
# CC: sccache gcc
|
||||
# CXX: sccache g++
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
NPM_CONFIG_LOGLEVEL: verbose
|
||||
|
||||
- run: pnpm run build:storybook
|
||||
- run: ./node_modules/.bin/playwright install chromium
|
||||
- run: ./node_modules/.bin/run-p --race test:storybook:serve test:storybook:test
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,6 +5,7 @@ coverage/*
|
||||
build/curve25519_compiled.js
|
||||
build/compact-locales
|
||||
build/*.policy
|
||||
build/emoji-data.json
|
||||
stylesheets/*.css.map
|
||||
/dist
|
||||
.DS_Store
|
||||
@ -17,14 +18,11 @@ release/
|
||||
|
||||
/sql/
|
||||
/start.sh
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
tsconfig.tsbuildinfo
|
||||
.smartling-source.sh
|
||||
|
||||
# generated files
|
||||
js/components.js
|
||||
js/util_worker.js
|
||||
libtextsecure/components.js
|
||||
stylesheets/*.css
|
||||
!stylesheets/tailwind-config.css
|
||||
@ -39,6 +37,7 @@ build/ICUMessageParams.d.ts
|
||||
build/**/*.js
|
||||
app/*.js
|
||||
ts/**/*.js
|
||||
!ts/windows/main/tsx.js
|
||||
ts/protobuf/*.d.ts
|
||||
|
||||
# CSS Modules
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
{
|
||||
"checkLeaks": true
|
||||
"checkLeaks": true,
|
||||
"node-option": ["import=tsx"]
|
||||
}
|
||||
|
||||
9
.npmrc
9
.npmrc
@ -1,9 +0,0 @@
|
||||
legacy-peer-deps=true
|
||||
public-hoist-pattern[]=*eslint-*
|
||||
minimum-release-age=14400
|
||||
minimum-release-age-exclude[]=@signalapp/*
|
||||
minimum-release-age-exclude[]=@indutny/*
|
||||
minimum-release-age-exclude[]=@types/*
|
||||
minimum-release-age-exclude[]=electron
|
||||
minimum-release-age-exclude[]=react
|
||||
minimum-release-age-exclude[]=react-dom
|
||||
37
.oxlint/plugin.mjs
Normal file
37
.oxlint/plugin.mjs
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { enforceArrayBuffer } from './rules/enforceArrayBuffer.mjs';
|
||||
import { enforceFileSuffix } from './rules/enforceFileSuffix.mjs';
|
||||
import { enforceLicenseComments } from './rules/enforceLicenseComments.mjs';
|
||||
import { enforceTw } from './rules/enforceTw.mjs';
|
||||
import { enforceTypeAliasReadonlyDeep } from './rules/enforceTypeAliasReadonlyDeep.mjs';
|
||||
import { noDisabledTests } from './rules/noDisabledTests.mjs';
|
||||
import { noExtraneousDependencies } from './rules/noExtraneousDependencies.mjs';
|
||||
import { noFocusedTests } from './rules/noFocusedTests.mjs';
|
||||
import { noForIn } from './rules/noForIn.mjs';
|
||||
import { noRestrictedPaths } from './rules/noRestrictedPaths.mjs';
|
||||
import { noThen } from './rules/noThen.mjs';
|
||||
|
||||
/** @type {import("@typescript-eslint/utils").TSESLint.Linter.Plugin} */
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: 'signal-desktop',
|
||||
version: '0.0.0',
|
||||
},
|
||||
rules: {
|
||||
'enforce-array-buffer': enforceArrayBuffer,
|
||||
'enforce-file-suffix': enforceFileSuffix,
|
||||
'enforce-license-comments': enforceLicenseComments,
|
||||
'enforce-tw': enforceTw,
|
||||
'enforce-type-alias-readonlydeep': enforceTypeAliasReadonlyDeep,
|
||||
'no-disabled-tests': noDisabledTests,
|
||||
'no-extraneous-dependencies': noExtraneousDependencies,
|
||||
'no-focused-tests': noFocusedTests,
|
||||
'no-for-in': noForIn,
|
||||
'no-restricted-paths': noRestrictedPaths,
|
||||
'no-then': noThen,
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@ -1,12 +1,18 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
module.exports = {
|
||||
export const enforceArrayBuffer = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'enforce-array-buffer',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: true,
|
||||
fixable: true,
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
shouldUseArrayBuffer: `Should be {{replacement}}`,
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
@ -24,13 +30,14 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.typeParameters != null) {
|
||||
if (node.typeArguments != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: `Should be ${replacement}`,
|
||||
messageId: 'shouldUseArrayBuffer',
|
||||
data: { replacement },
|
||||
fix(fixer) {
|
||||
return [fixer.replaceTextRange(node.range, replacement)];
|
||||
},
|
||||
@ -38,4 +45,4 @@ module.exports = {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -1,31 +1,12 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { enforceArrayBuffer } from './enforceArrayBuffer.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const rule = require('./enforce-array-buffer');
|
||||
const RuleTester = require('eslint').RuleTester;
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
// avoid triggering mocha's global leak detection
|
||||
require('@typescript-eslint/parser');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
const EXPECTED_ARRAY_ERROR = {
|
||||
message: 'Should be Uint8Array<ArrayBuffer>',
|
||||
type: 'TSTypeReference',
|
||||
};
|
||||
|
||||
const EXPECTED_BUFFER_ERROR = {
|
||||
message: 'Should be Buffer<ArrayBuffer>',
|
||||
type: 'TSTypeReference',
|
||||
};
|
||||
|
||||
ruleTester.run('enforce-array-buffer', rule, {
|
||||
ruleTester.run('enforce-array-buffer', enforceArrayBuffer, {
|
||||
valid: [
|
||||
{ code: 'type T = number;' },
|
||||
{ code: 'type T = Uint16Array;' },
|
||||
@ -52,32 +33,32 @@ ruleTester.run('enforce-array-buffer', rule, {
|
||||
{
|
||||
code: `type T = Uint8Array`,
|
||||
output: `type T = Uint8Array<ArrayBuffer>`,
|
||||
errors: [EXPECTED_ARRAY_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
{
|
||||
code: `function f(): Uint8Array {}`,
|
||||
output: `function f(): Uint8Array<ArrayBuffer> {}`,
|
||||
errors: [EXPECTED_ARRAY_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
{
|
||||
code: `function f(p: Uint8Array) {}`,
|
||||
output: `function f(p: Uint8Array<ArrayBuffer>) {}`,
|
||||
errors: [EXPECTED_ARRAY_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
{
|
||||
code: `let v: Uint8Array;`,
|
||||
output: `let v: Uint8Array<ArrayBuffer>;`,
|
||||
errors: [EXPECTED_ARRAY_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
{
|
||||
code: `let v: { p: Uint8Array };`,
|
||||
output: `let v: { p: Uint8Array<ArrayBuffer> };`,
|
||||
errors: [EXPECTED_ARRAY_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
{
|
||||
code: `type T = Buffer`,
|
||||
output: `type T = Buffer<ArrayBuffer>`,
|
||||
errors: [EXPECTED_BUFFER_ERROR],
|
||||
errors: [{ messageId: 'shouldUseArrayBuffer' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1,5 +1,23 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { getReferenceType } from './utils/getReferenceType.mjs';
|
||||
import { isStringLiteral } from './utils/astUtils.mjs';
|
||||
import { assert } from './utils/assert.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportDeclaration} ImportDeclaration
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportAllDeclaration} ExportAllDeclaration
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportNamedDeclaration} ExportNamedDeclaration
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.ImportClause} ImportClause
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.ExportSpecifier} ExportSpecifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'std' | 'node' | 'dom' | 'preload' | 'main'} Suffix
|
||||
*/
|
||||
|
||||
const ELECTRON_MAIN_MODULES = new Set([
|
||||
'app',
|
||||
@ -73,9 +91,11 @@ const NODE_PACKAGES = new Set([
|
||||
'fs-xattr',
|
||||
'got',
|
||||
'growing-file',
|
||||
'http-proxy-agent',
|
||||
'https-proxy-agent',
|
||||
'node-fetch',
|
||||
'proxy-agent',
|
||||
'read-last-lines',
|
||||
'socks-proxy-agent',
|
||||
'split2',
|
||||
'write-file-atomic',
|
||||
|
||||
@ -97,13 +117,10 @@ const NODE_PACKAGES = new Set([
|
||||
'endanger',
|
||||
'enhanced-resolve',
|
||||
'enquirer',
|
||||
'esbuild',
|
||||
'execa',
|
||||
'html-webpack-plugin',
|
||||
'http-server',
|
||||
'json-to-ast',
|
||||
'log-symbols',
|
||||
'mini-css-extract-plugin',
|
||||
'node-gyp',
|
||||
'node-gyp-build',
|
||||
'npm-run-all',
|
||||
@ -111,13 +128,13 @@ const NODE_PACKAGES = new Set([
|
||||
'pe-library',
|
||||
'pixelmatch',
|
||||
'playwright',
|
||||
'postcss',
|
||||
'postcss-loader',
|
||||
'prettier',
|
||||
'prettier-plugin-tailwindcss',
|
||||
'react-devtools',
|
||||
'react-devtools-core',
|
||||
'resolve-url-loader',
|
||||
'rolldown',
|
||||
'sass',
|
||||
'sass-loader',
|
||||
'style-loader',
|
||||
@ -128,8 +145,7 @@ const NODE_PACKAGES = new Set([
|
||||
'svgo',
|
||||
'synckit',
|
||||
'tailwindcss',
|
||||
'terser-webpack-plugin',
|
||||
'ts-node',
|
||||
'tsx',
|
||||
'typescript',
|
||||
'wait-on',
|
||||
'webpack',
|
||||
@ -147,10 +163,8 @@ const DOM_PACKAGES = new Set([
|
||||
'@tanstack/react-virtual',
|
||||
'blob-util',
|
||||
'blueimp-load-image',
|
||||
'copy-text-to-clipboard',
|
||||
'dom-accessibility-api',
|
||||
'fabric',
|
||||
'focus-trap-react',
|
||||
'radix-ui',
|
||||
'react-aria',
|
||||
'react-aria-components',
|
||||
@ -170,8 +184,6 @@ const DOM_PACKAGES = new Set([
|
||||
'@storybook/addon-toolbars',
|
||||
'@storybook/addon-viewport',
|
||||
'@storybook/addon-webpack5-compiler-swc',
|
||||
'@storybook/csf',
|
||||
'@storybook/preview-api',
|
||||
'@storybook/react',
|
||||
'@storybook/react-webpack5',
|
||||
'@storybook/test',
|
||||
@ -183,9 +195,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',
|
||||
@ -198,7 +207,9 @@ const STD_PACKAGES = new Set([
|
||||
'@internationalized/date',
|
||||
'@react-types/shared',
|
||||
'@signalapp/minimask',
|
||||
'@signalapp/parchment-cjs',
|
||||
'@signalapp/quill-cjs',
|
||||
'@signalapp/lame',
|
||||
'@typescript-eslint/eslint-plugin',
|
||||
'@typescript-eslint/parser',
|
||||
'axe-core',
|
||||
@ -211,7 +222,6 @@ const STD_PACKAGES = new Set([
|
||||
'casual',
|
||||
'chai',
|
||||
'chai-as-promised',
|
||||
'chalk',
|
||||
'changedpi',
|
||||
'classnames',
|
||||
'country-codes-list',
|
||||
@ -221,18 +231,9 @@ const STD_PACKAGES = new Set([
|
||||
'danger',
|
||||
'debug',
|
||||
'direction',
|
||||
'emoji-datasource',
|
||||
'emoji-datasource-apple',
|
||||
'emoji-regex',
|
||||
'emoji-regex-xs',
|
||||
'eslint',
|
||||
'eslint-config-airbnb-typescript-prettier',
|
||||
'eslint-config-prettier',
|
||||
'eslint-plugin-better-tailwindcss',
|
||||
'eslint-plugin-import',
|
||||
'eslint-plugin-local-rules',
|
||||
'eslint-plugin-mocha',
|
||||
'eslint-plugin-more',
|
||||
'eslint-plugin-react',
|
||||
'filesize',
|
||||
'firstline',
|
||||
'form-data',
|
||||
@ -257,7 +258,6 @@ const STD_PACKAGES = new Set([
|
||||
'p-queue',
|
||||
'p-timeout',
|
||||
'parsecurrency',
|
||||
'pify',
|
||||
'pino',
|
||||
'pngjs',
|
||||
'qrcode-generator',
|
||||
@ -274,29 +274,60 @@ const STD_PACKAGES = new Set([
|
||||
'tinykeys',
|
||||
'type-fest',
|
||||
'url',
|
||||
'urlpattern-polyfill',
|
||||
'uuid',
|
||||
'zod',
|
||||
]);
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
module.exports = {
|
||||
export const enforceFileSuffix = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'enforce-file-suffix',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: false,
|
||||
fixable: false,
|
||||
messages: {
|
||||
missingFileSuffix: 'Missing file suffix in {{source}} import',
|
||||
unrecognizedFileSuffix:
|
||||
'Unrecognized file suffix in {{source}}, expected: node/preload/main/std, found: {{depSuffix}}',
|
||||
commonJsImportOfElectronNoAllowed:
|
||||
'CJS import of electron is not allowed',
|
||||
uncategorizedElectronApi:
|
||||
'Uncategorized electron API: "{{name}}". ' +
|
||||
'Please update .oxlint/rules/file-suffix.js and add it to ' +
|
||||
'ELECTRON_MAIN_MODULES/ELECTRON_RENDERER_MODULES/' +
|
||||
'ELECTRON_SHARED_MODULES',
|
||||
unsupportedNamespaceImportForElectron:
|
||||
'Unsupported namespace import specifier for electron',
|
||||
unsupportedImportSpecifierForElectron:
|
||||
'Unsupported import specifier for electron',
|
||||
uncategorizedDependency:
|
||||
'Uncategorized dependency "{{moduleName}}". ' +
|
||||
'Please update .oxlint/rules/file-suffix.js and add it to either ' +
|
||||
'of NODE_PACKAGES/DOM_PACKAGES/STD_PACKAGES',
|
||||
missingFileSuffixMustBeOneOf:
|
||||
'Missing file suffix. Has to be one of: node/preload/main/std',
|
||||
wrongFileSuffix:
|
||||
'Invalid suffix {{fileSuffix}}, expected: {{expectedSuffix}}',
|
||||
invalidImportForSuffix:
|
||||
'Invalid import/reference for suffix: {{expectedSuffix}}',
|
||||
invalidRequireCount: 'Invalid require() argument count',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
const { filename, sourceCode } = context;
|
||||
|
||||
/** @type {string} */
|
||||
let fileSuffix;
|
||||
|
||||
/** @type {Node[]} */
|
||||
const nodeUses = [];
|
||||
/** @type {Node[]} */
|
||||
const domUses = [];
|
||||
/** @type {Node[]} */
|
||||
const preloadUses = [];
|
||||
/** @type {Node[]} */
|
||||
const mainUses = [];
|
||||
|
||||
/** @type Record<Suffix, Node[][]> */
|
||||
const invalidUsesBySuffix = {
|
||||
std: [nodeUses, domUses, preloadUses, mainUses],
|
||||
node: [domUses, preloadUses, mainUses],
|
||||
@ -305,16 +336,21 @@ module.exports = {
|
||||
main: [domUses, preloadUses],
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @param {string} source
|
||||
*/
|
||||
function trackLocalDep(node, source) {
|
||||
if (!source.endsWith('.js')) {
|
||||
if (!/\.tsx?/.test(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = source.match(/\.([^.\/]+)(?:\.stories)?\.js$/);
|
||||
const match = source.match(/\.([^.\/]+)(?:\.stories)?\.tsx?$/);
|
||||
if (match == null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Missing file suffix in ${source} import`,
|
||||
messageId: 'missingFileSuffix',
|
||||
data: { source },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -333,13 +369,17 @@ module.exports = {
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
`Unrecognized file suffix in ${source}, ` +
|
||||
`expected: node/preload/main/std, found: ${depSuffix}`,
|
||||
messageId: 'unrecognizedFileSuffix',
|
||||
data: { source, depSuffix },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @param {string} source
|
||||
* @param {Array<ImportClause | ExportSpecifier> | null} specifiers
|
||||
*/
|
||||
function processUse(node, source, specifiers) {
|
||||
if (source.startsWith('.')) {
|
||||
trackLocalDep(node, source);
|
||||
@ -356,38 +396,43 @@ module.exports = {
|
||||
if (source === 'electron' && specifiers == null) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'CJS import of electron is not allowed',
|
||||
messageId: 'commonJsImportOfElectronNoAllowed',
|
||||
});
|
||||
return;
|
||||
} else if (source === 'electron') {
|
||||
for (const s of specifiers) {
|
||||
if (s.importKind === 'type') {
|
||||
continue;
|
||||
}
|
||||
for (const s of specifiers ?? []) {
|
||||
// We implicitly skip:
|
||||
// they are used in scripts
|
||||
if (s.type === 'ImportSpecifier') {
|
||||
if (ELECTRON_MAIN_MODULES.has(s.imported.name)) {
|
||||
if (s.importKind === 'type') {
|
||||
continue;
|
||||
}
|
||||
/** @type {string} */
|
||||
let importName;
|
||||
if (s.imported.type === 'Identifier') {
|
||||
importName = s.imported.name;
|
||||
} else {
|
||||
importName = s.imported.value;
|
||||
}
|
||||
|
||||
if (ELECTRON_MAIN_MODULES.has(importName)) {
|
||||
mainUses.push(s);
|
||||
} else if (ELECTRON_RENDERER_MODULES.has(s.imported.name)) {
|
||||
} else if (ELECTRON_RENDERER_MODULES.has(importName)) {
|
||||
preloadUses.push(s);
|
||||
} else if (ELECTRON_SHARED_MODULES.has(s.imported.name)) {
|
||||
} else if (ELECTRON_SHARED_MODULES.has(importName)) {
|
||||
// no-op
|
||||
} else {
|
||||
context.report({
|
||||
node: s,
|
||||
message:
|
||||
`Uncategorized electron API: "${s.imported.name}". ` +
|
||||
'Please update .eslint/rules/file-suffix.js and add it to ' +
|
||||
'ELECTRON_MAIN_MODULES/ELECTRON_RENDERER_MODULES/' +
|
||||
'ELECTRON_SHARED_MODULES',
|
||||
messageId: 'uncategorizedElectronApi',
|
||||
data: { name: importName },
|
||||
});
|
||||
}
|
||||
} else if (s.type === 'ImportNamespaceSpecifier') {
|
||||
// import * as electron from 'electron';
|
||||
context.report({
|
||||
node: s,
|
||||
message: 'Unsupported namespace import specifier for electron',
|
||||
messageId: 'unsupportedNamespaceImportForElectron',
|
||||
});
|
||||
nodeUses.push(s);
|
||||
} else if (s.type === 'ImportDefaultSpecifier') {
|
||||
@ -396,14 +441,20 @@ module.exports = {
|
||||
} else {
|
||||
context.report({
|
||||
node: s,
|
||||
message: 'Unsupported import specifier for electron',
|
||||
messageId: 'unsupportedImportSpecifierForElectron',
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [, moduleName] = source.match(/^([^@\/]+|@[^\/]+\/[^\/]+)/);
|
||||
const match = source.match(/^([^@/]+|@[^/]+\/[^/]+)/);
|
||||
if (match == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, moduleName] = match;
|
||||
assert(moduleName, 'Missing moduleName');
|
||||
if (NODE_PACKAGES.has(moduleName)) {
|
||||
nodeUses.push(node);
|
||||
} else if (source === 'react-dom/server') {
|
||||
@ -416,60 +467,81 @@ module.exports = {
|
||||
} else if (!STD_PACKAGES.has(moduleName)) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
`Uncategorized dependency "${moduleName}". ` +
|
||||
'Please update .eslint/rules/file-suffix.js and add it to either ' +
|
||||
'of NODE_PACKAGES/DOM_PACKAGES/STD_PACKAGES',
|
||||
messageId: 'uncategorizedDependency',
|
||||
data: { moduleName },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ImportDeclaration | ExportAllDeclaration | ExportNamedDeclaration} node
|
||||
*/
|
||||
function processESMReference(node) {
|
||||
if (
|
||||
node.importKind === 'type' ||
|
||||
(node.specifiers?.length &&
|
||||
node.specifiers.every(x => x.importKind === 'type'))
|
||||
) {
|
||||
return;
|
||||
/** @type {Array<ImportClause | ExportSpecifier> | null} */
|
||||
let specifiers;
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
if (node.importKind === 'type') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.specifiers.length > 0) {
|
||||
const allTypes = node.specifiers.every(specifier => {
|
||||
return (
|
||||
specifier.type === 'ImportSpecifier' &&
|
||||
specifier.importKind === 'type'
|
||||
);
|
||||
});
|
||||
|
||||
if (allTypes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
specifiers = node.specifiers;
|
||||
} else if (node.type === 'ExportNamedDeclaration') {
|
||||
specifiers = node.specifiers;
|
||||
} else {
|
||||
specifiers = null;
|
||||
}
|
||||
|
||||
if (!node.source) {
|
||||
return;
|
||||
}
|
||||
if (node.source.type !== 'Literal') {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
specifiers,
|
||||
source: { value: source },
|
||||
} = node;
|
||||
|
||||
const source = node.source.value;
|
||||
processUse(node, source, specifiers);
|
||||
}
|
||||
|
||||
return {
|
||||
Program: node => {
|
||||
if (filename.endsWith('.d.ts')) {
|
||||
if (/\.d\.m?ts$/.test(filename)) {
|
||||
// Skip types
|
||||
return;
|
||||
}
|
||||
|
||||
const match = filename.match(/\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx)$/);
|
||||
const match = filename.match(
|
||||
/\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx|js|mjs)$/
|
||||
);
|
||||
if (match == null) {
|
||||
context.report({
|
||||
node: node,
|
||||
message:
|
||||
'Missing file suffix. Has to be one of: node/preload/main/std',
|
||||
node,
|
||||
messageId: 'missingFileSuffixMustBeOneOf',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fileSuffix = match[1];
|
||||
const matchedSuffix = match[1];
|
||||
assert(matchedSuffix, 'Missing matchedSuffix');
|
||||
fileSuffix = matchedSuffix;
|
||||
},
|
||||
'Program:exit': node => {
|
||||
if (fileSuffix == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {Suffix} */
|
||||
let expectedSuffix;
|
||||
if (mainUses.length > 0) {
|
||||
expectedSuffix = 'main';
|
||||
@ -500,7 +572,8 @@ module.exports = {
|
||||
if (fileSuffix !== expectedSuffix) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid suffix ${fileSuffix}, expected: ${expectedSuffix}`,
|
||||
messageId: 'wrongFileSuffix',
|
||||
data: { fileSuffix, expectedSuffix },
|
||||
});
|
||||
}
|
||||
|
||||
@ -508,7 +581,8 @@ module.exports = {
|
||||
for (const use of invalid) {
|
||||
context.report({
|
||||
node: use,
|
||||
message: `Invalid import/reference for suffix: ${expectedSuffix}`,
|
||||
messageId: 'invalidImportForSuffix',
|
||||
data: { expectedSuffix },
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -529,27 +603,29 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = sourceCode.getScope(node);
|
||||
const ref = scope.references.find(r => r.identifier === node.callee);
|
||||
if (ref.resolved.scope.type !== 'global') {
|
||||
const refType = getReferenceType(sourceCode, node.callee);
|
||||
if (refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { arguments: args } = node;
|
||||
if (args.length !== 1) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Invalid require() argument count',
|
||||
messageId: 'invalidRequireCount',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const [arg] = args;
|
||||
assert(arg, 'Missing arg');
|
||||
|
||||
/** @type {string} */
|
||||
let source;
|
||||
if (arg.type === 'Literal') {
|
||||
if (isStringLiteral(arg)) {
|
||||
source = arg.value;
|
||||
} else if (
|
||||
arg.type === 'TSAsExpression' &&
|
||||
arg.expression.type === 'Literal'
|
||||
isStringLiteral(arg.expression)
|
||||
) {
|
||||
source = arg.expression.value;
|
||||
} else {
|
||||
@ -557,23 +633,22 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
processUse(node, source, undefined);
|
||||
processUse(node, source, null);
|
||||
},
|
||||
Identifier(node) {
|
||||
if (node.name !== 'window' && node.name !== 'document') {
|
||||
return;
|
||||
}
|
||||
const scope = sourceCode.getScope(node);
|
||||
const ref = scope.references.find(r => r.identifier === node);
|
||||
if (ref == null) {
|
||||
const refType = getReferenceType(sourceCode, node);
|
||||
if (refType == null) {
|
||||
// Not part of expression
|
||||
return;
|
||||
}
|
||||
if (ref.resolved.scope.type !== 'global') {
|
||||
if (refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
domUses.push(node);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
139
.oxlint/rules/enforceFileSuffix.test.mjs
Normal file
139
.oxlint/rules/enforceFileSuffix.test.mjs
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { enforceFileSuffix } from './enforceFileSuffix.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
const ALLOWED_REFERENCES = /* @type {const} */ [
|
||||
{
|
||||
fileSuffix: 'std',
|
||||
requiredLine: '',
|
||||
depSuffixes: ['std'],
|
||||
},
|
||||
{
|
||||
fileSuffix: 'dom',
|
||||
requiredLine: 'window.addEventListener();',
|
||||
depSuffixes: ['std', 'dom'],
|
||||
},
|
||||
{
|
||||
fileSuffix: 'node',
|
||||
requiredLine: 'require("node:fs");',
|
||||
depSuffixes: ['std', 'node'],
|
||||
},
|
||||
{
|
||||
fileSuffix: 'preload',
|
||||
requiredLine: 'import { ipcRenderer } from "electron";',
|
||||
depSuffixes: ['std', 'node', 'preload'],
|
||||
},
|
||||
{
|
||||
fileSuffix: 'main',
|
||||
requiredLine: 'import { autoUpdater } from "electron";',
|
||||
depSuffixes: ['std', 'node', 'main'],
|
||||
},
|
||||
];
|
||||
|
||||
const DISALLOWED_REFERENCES = /* @type {const} */ [
|
||||
{ fileSuffix: 'std', depSuffixes: ['dom', 'node', 'preload', 'main'] },
|
||||
{ fileSuffix: 'dom', depSuffixes: ['node', 'preload', 'main'] },
|
||||
{ fileSuffix: 'node', depSuffixes: ['preload', 'main'] },
|
||||
{ fileSuffix: 'preload', depSuffixes: ['main'] },
|
||||
{ fileSuffix: 'main', depSuffixes: ['dom', 'preload'] },
|
||||
];
|
||||
|
||||
ruleTester.run('file-suffix', enforceFileSuffix, {
|
||||
valid: [
|
||||
...ALLOWED_REFERENCES.map(({ fileSuffix, requiredLine, depSuffixes }) => {
|
||||
return depSuffixes.map(depSuffix => {
|
||||
/** @type {const} */
|
||||
return {
|
||||
name: `importing ${depSuffix} from ${fileSuffix}`,
|
||||
filename: `a.${fileSuffix}.ts`,
|
||||
code: `
|
||||
import { x } from './b.${depSuffix}.ts';
|
||||
${requiredLine}
|
||||
`,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
window: 'writable',
|
||||
require: 'readable',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}).flat(),
|
||||
{
|
||||
name: 'type import should have no effect',
|
||||
filename: 'a.std.ts',
|
||||
code: `import type { ReadonlyDeep } from './b.dom.ts'`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
...DISALLOWED_REFERENCES.map(({ fileSuffix, depSuffixes }) => {
|
||||
return depSuffixes.map(depSuffix => {
|
||||
/** @type {const} */
|
||||
return {
|
||||
name: `importing ${depSuffix} from ${fileSuffix}`,
|
||||
filename: `a.${fileSuffix}.ts`,
|
||||
code: `import { x } from './b.${depSuffix}.ts'`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrongFileSuffix',
|
||||
data: { fileSuffix, expectedSuffix: depSuffix },
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}).flat(),
|
||||
|
||||
...['dom', 'node', 'preload', 'main'].map(fileSuffix => {
|
||||
/** @type {const} */
|
||||
return {
|
||||
name: `no ${fileSuffix} imports`,
|
||||
filename: `a.${fileSuffix}.ts`,
|
||||
code: '',
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrongFileSuffix',
|
||||
data: { fileSuffix, expectedSuffix: 'std' },
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
|
||||
// Invalid imports
|
||||
{
|
||||
name: 'preload in main',
|
||||
filename: 'a.main.ts',
|
||||
code: `
|
||||
import { autoUpdater } from 'electron';
|
||||
import './b.preload.ts';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'invalidImportForSuffix',
|
||||
data: { expectedSuffix: 'main' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'main in preload',
|
||||
filename: 'a.preload.ts',
|
||||
code: `
|
||||
import { ipcRenderer } from 'electron';
|
||||
import './b.main.ts';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrongFileSuffix',
|
||||
data: { fileSuffix: 'preload', expectedSuffix: 'main' },
|
||||
},
|
||||
{
|
||||
messageId: 'invalidImportForSuffix',
|
||||
data: { expectedSuffix: 'main' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1,24 +1,29 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
const COMMENT_LINE_1_EXACT = /^ Copyright \d{4} Signal Messenger, LLC$/;
|
||||
const COMMENT_LINE_2_EXACT = /^ SPDX-License-Identifier: AGPL-3.0-only$/;
|
||||
|
||||
const COMMENT_LINE_1_LOOSE = /Copyright (\d{4}) Signal Messenger, LLC/;
|
||||
const COMMENT_LINE_2_LOOSE = /SPDX-License-Identifier: AGPL-3.0-only/;
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
module.exports = {
|
||||
export const enforceLicenseComments = ESLintUtils.RuleCreator.withoutDocs({
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: false,
|
||||
fixable: true,
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
missingLicenseComment: 'Missing license comment',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
Program(node) {
|
||||
let comment1 = node.comments.at(0);
|
||||
let comment2 = node.comments.at(1);
|
||||
const comment1 = node.comments?.at(0);
|
||||
const comment2 = node.comments?.at(1);
|
||||
|
||||
if (
|
||||
comment1?.type === 'Line' &&
|
||||
@ -31,15 +36,14 @@ module.exports = {
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message: 'Missing license comment',
|
||||
|
||||
messageId: 'missingLicenseComment',
|
||||
fix(fixer) {
|
||||
let year = null;
|
||||
let remove = [];
|
||||
const remove = [];
|
||||
|
||||
for (let comment of node.comments) {
|
||||
let match1 = comment.value.match(COMMENT_LINE_1_LOOSE);
|
||||
let match2 = comment.value.match(COMMENT_LINE_2_LOOSE);
|
||||
for (const comment of node.comments ?? []) {
|
||||
const match1 = comment.value.match(COMMENT_LINE_1_LOOSE);
|
||||
const match2 = comment.value.match(COMMENT_LINE_2_LOOSE);
|
||||
|
||||
if (match1 != null) {
|
||||
year = match1[1];
|
||||
@ -52,7 +56,7 @@ module.exports = {
|
||||
|
||||
year ??= new Date().getFullYear().toString();
|
||||
|
||||
let insert =
|
||||
const insert =
|
||||
`// Copyright ${year} Signal Messenger, LLC\n` +
|
||||
'// SPDX-License-Identifier: AGPL-3.0-only\n';
|
||||
|
||||
@ -70,4 +74,4 @@ module.exports = {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -1,18 +1,30 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
const { createSyncFn } = require('synckit');
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { createSyncFn } from 'synckit';
|
||||
|
||||
const worker = createSyncFn(require.resolve('./enforce-tw.worker.js'));
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
|
||||
*/
|
||||
|
||||
/** @type {import("eslint").Rule.RuleModule} */
|
||||
module.exports = {
|
||||
const worker = createSyncFn(import.meta.resolve('./enforceTw.worker.mjs'));
|
||||
|
||||
export const enforceTw = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'enforce-tw',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: true,
|
||||
fixable: true,
|
||||
messages: {
|
||||
needsTw: 'Tailwind classes must be wrapped with tw()',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Node} node
|
||||
*/
|
||||
function check(input, node) {
|
||||
if (typeof input !== 'string') {
|
||||
throw new Error(`Unexpected input ${input} for node type ${node.type}`);
|
||||
@ -35,11 +47,14 @@ module.exports = {
|
||||
column: node.loc.start.column + index + length,
|
||||
},
|
||||
},
|
||||
message: 'Tailwind classes must be wrapped with tw()',
|
||||
messageId: 'needsTw',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
*/
|
||||
function traverse(node) {
|
||||
if (node.type === 'Literal') {
|
||||
if (typeof node.value === 'string') {
|
||||
@ -47,14 +62,16 @@ module.exports = {
|
||||
}
|
||||
// ignore other literals
|
||||
} else if (node.type === 'TemplateLiteral') {
|
||||
for (let element of node.quasis) {
|
||||
for (const element of node.quasis) {
|
||||
traverse(element);
|
||||
}
|
||||
for (let expression of node.expressions) {
|
||||
for (const expression of node.expressions) {
|
||||
traverse(expression);
|
||||
}
|
||||
} else if (node.type === 'TemplateElement') {
|
||||
check(node.value.cooked, node);
|
||||
if (node.value.cooked != null) {
|
||||
check(node.value.cooked, node);
|
||||
}
|
||||
} else if (node.type === 'JSXExpressionContainer') {
|
||||
traverse(node.expression);
|
||||
} else if (node.type === 'ConditionalExpression') {
|
||||
@ -74,7 +91,7 @@ module.exports = {
|
||||
throw new Error(`Unexpected binary operator: ${node.operator}`);
|
||||
}
|
||||
} else if (node.type === 'ObjectExpression') {
|
||||
for (let prop of node.properties) {
|
||||
for (const prop of node.properties) {
|
||||
traverse(prop);
|
||||
}
|
||||
} else if (node.type === 'Property') {
|
||||
@ -93,8 +110,10 @@ module.exports = {
|
||||
throw new Error(`Unexpected property key type: ${node.key.type}`);
|
||||
}
|
||||
} else if (node.type === 'ArrayExpression') {
|
||||
for (let element of node.elements) {
|
||||
traverse(element);
|
||||
for (const element of node.elements) {
|
||||
if (element != null) {
|
||||
traverse(element);
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'Identifier') {
|
||||
// ignore
|
||||
@ -111,15 +130,17 @@ module.exports = {
|
||||
CallExpression(node) {
|
||||
if (node.callee.type !== 'Identifier') return;
|
||||
if (node.callee.name !== 'classNames') return;
|
||||
for (let arg of node.arguments) {
|
||||
for (const arg of node.arguments) {
|
||||
traverse(arg);
|
||||
}
|
||||
},
|
||||
JSXAttribute(node) {
|
||||
if (node.name.type !== 'JSXIdentifier') return;
|
||||
if (node.name.name !== 'className') return;
|
||||
traverse(node.value);
|
||||
if (node.value != null) {
|
||||
traverse(node.value);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
69
.oxlint/rules/enforceTw.test.mjs
Normal file
69
.oxlint/rules/enforceTw.test.mjs
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { enforceTw } from './enforceTw.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('enforce-tw', enforceTw, {
|
||||
valid: [
|
||||
{ code: `classNames("foo")` },
|
||||
{ code: `<div className="foo"/>` },
|
||||
{ code: `tw("flex")` },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `classNames("flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `<div className="flex"/>`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `<div className={"flex"}/>`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames("foo", "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames(cond ? "foo" : "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames(cond ? "flex" : "foo")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames(cond && "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames(cond || "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames(cond ?? "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames("foo" + "flex")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
{
|
||||
code: `classNames("flex" + "foo")`,
|
||||
errors: [{ messageId: 'needsTw' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1,12 +1,13 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
const { runAsWorker } = require('synckit');
|
||||
const enhancedResolve = require('enhanced-resolve');
|
||||
const tailwind = require('tailwindcss');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
// @ts-check
|
||||
import { runAsWorker } from 'synckit';
|
||||
import enhancedResolve from 'enhanced-resolve';
|
||||
import * as tailwind from 'tailwindcss';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
const rootDir = path.join(import.meta.dirname, '../..');
|
||||
const tailwindCssPath = path.join(rootDir, 'stylesheets/tailwind-config.css');
|
||||
|
||||
async function loadDesignSystem() {
|
||||
@ -21,12 +22,13 @@ async function loadDesignSystem() {
|
||||
tailwindCss,
|
||||
{
|
||||
base: path.dirname(tailwindCssPath),
|
||||
loadStylesheet(id, base) {
|
||||
async loadStylesheet(id, base) {
|
||||
const resolved = resolver(base, id);
|
||||
if (!resolved) {
|
||||
return { base: '', content: '' };
|
||||
return { path: '', base: '', content: '' };
|
||||
}
|
||||
return {
|
||||
path: resolved,
|
||||
base: path.dirname(resolved),
|
||||
content: fs.readFileSync(resolved, 'utf-8'),
|
||||
};
|
||||
@ -39,12 +41,17 @@ async function loadDesignSystem() {
|
||||
|
||||
let cachedDesignSystem = null;
|
||||
|
||||
runAsWorker(async classNames => {
|
||||
/**
|
||||
* @param {Array<string>} classNames
|
||||
*/
|
||||
async function worker(classNames) {
|
||||
cachedDesignSystem ??= await loadDesignSystem();
|
||||
const designSystem = cachedDesignSystem;
|
||||
const css = designSystem.candidatesToCss(classNames);
|
||||
const tailwindClassNames = classNames.filter((_, index) => {
|
||||
return css.at(index) !== null;
|
||||
return css.at(index) != null;
|
||||
});
|
||||
return tailwindClassNames;
|
||||
});
|
||||
}
|
||||
|
||||
runAsWorker(worker);
|
||||
75
.oxlint/rules/enforceTypeAliasReadonlyDeep.mjs
Normal file
75
.oxlint/rules/enforceTypeAliasReadonlyDeep.mjs
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { assert } from './utils/assert.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
|
||||
* @typedef {import("@typescript-eslint/utils").TSESLint.Scope.Scope} Scope
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @param {Scope} scope
|
||||
*/
|
||||
function isReadOnlyDeep(node, scope) {
|
||||
if (node.type !== 'TSTypeReference') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reference = scope.references.find(ref => {
|
||||
return ref.identifier === node.typeName;
|
||||
});
|
||||
|
||||
const variable = reference?.resolved;
|
||||
if (variable == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const defs = variable.defs;
|
||||
if (defs.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [def] = defs;
|
||||
assert(def, 'Missing def');
|
||||
|
||||
return (
|
||||
def.type === 'ImportBinding' &&
|
||||
def.parent.type === 'ImportDeclaration' &&
|
||||
def.parent.source.type === 'Literal' &&
|
||||
def.parent.source.value === 'type-fest'
|
||||
);
|
||||
}
|
||||
|
||||
export const enforceTypeAliasReadonlyDeep = ESLintUtils.RuleCreator.withoutDocs(
|
||||
{
|
||||
name: 'enforce-type-alias-readonlydeep',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
needsReadonlyDeep:
|
||||
'Type aliases must be wrapped with ReadonlyDeep from type-fest',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
TSTypeAliasDeclaration(node) {
|
||||
const scope = context.sourceCode.getScope(node);
|
||||
|
||||
if (isReadOnlyDeep(node.typeAnnotation, scope)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.id,
|
||||
messageId: 'needsReadonlyDeep',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
40
.oxlint/rules/enforceTypeAliasReadonlyDeep.test.mjs
Normal file
40
.oxlint/rules/enforceTypeAliasReadonlyDeep.test.mjs
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { enforceTypeAliasReadonlyDeep } from './enforceTypeAliasReadonlyDeep.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('type-alias-readonlydeep', enforceTypeAliasReadonlyDeep, {
|
||||
valid: [
|
||||
{
|
||||
code: `import type { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
|
||||
},
|
||||
{
|
||||
code: `import { ReadonlyDeep } from "type-fest"; type Foo = ReadonlyDeep<{}>`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `type Foo = {}`,
|
||||
errors: [{ messageId: 'needsReadonlyDeep' }],
|
||||
},
|
||||
{
|
||||
code: `type Foo = Bar<{}>`,
|
||||
errors: [{ messageId: 'needsReadonlyDeep' }],
|
||||
},
|
||||
{
|
||||
code: `type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [{ messageId: 'needsReadonlyDeep' }],
|
||||
},
|
||||
{
|
||||
code: `interface ReadonlyDeep<T> {}; type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [{ messageId: 'needsReadonlyDeep' }],
|
||||
},
|
||||
{
|
||||
code: `import type { ReadonlyDeep } from "foo"; type Foo = ReadonlyDeep<{}>`,
|
||||
errors: [{ messageId: 'needsReadonlyDeep' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"prod-dep": "0.0.0",
|
||||
"@scoped/prod-dep": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dev-dep": "0.0.0",
|
||||
"@scoped/dev-dep": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"peer-dep": "0.0.0",
|
||||
"@scoped/peer-dep": "0.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"optional-dep": "0.0.0",
|
||||
"@scoped/optional-dep": "0.0.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"bundled-dep",
|
||||
"@scoped/bundled-dep"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
export {};
|
||||
@ -0,0 +1,3 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
export {};
|
||||
4
.oxlint/rules/fixtures/noRestrictedPaths/tsconfig.json
Normal file
4
.oxlint/rules/fixtures/noRestrictedPaths/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"include": ["./client/**", "./server/**"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
64
.oxlint/rules/noDisabledTests.mjs
Normal file
64
.oxlint/rules/noDisabledTests.mjs
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { getReferenceType } from './utils/getReferenceType.mjs';
|
||||
import { isPropertyAccess } from './utils/astUtils.mjs';
|
||||
|
||||
export const noDisabledTests = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-disabled-tests',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: true,
|
||||
messages: {
|
||||
unexpectedDisabledTest: 'Unexpected disabled test',
|
||||
removeSkip: 'Remove .skip()',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (node.object.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
|
||||
let replacement;
|
||||
if (node.object.name === 'describe') {
|
||||
replacement = 'describe';
|
||||
} else if (node.object.name === 'it') {
|
||||
replacement = 'it';
|
||||
} else if (node.object.name === 'test') {
|
||||
replacement = 'test';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPropertyAccess(node, 'skip')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refType = getReferenceType(sourceCode, node.object);
|
||||
if (refType != null && refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unexpectedDisabledTest',
|
||||
suggest: [
|
||||
{
|
||||
messageId: 'removeSkip',
|
||||
fix(fixer) {
|
||||
return [fixer.replaceTextRange(node.range, replacement)];
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
56
.oxlint/rules/noDisabledTests.test.mjs
Normal file
56
.oxlint/rules/noDisabledTests.test.mjs
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { noDisabledTests } from './noDisabledTests.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-disabled-tests', noDisabledTests, {
|
||||
valid: [
|
||||
{ code: 'describe(() => {});' },
|
||||
{ code: 'it(() => {});' },
|
||||
{ code: 'test(() => {});' },
|
||||
{ code: 'describe.only(() => {});' },
|
||||
{ code: 'it.only(() => {});' },
|
||||
{ code: 'test.only(() => {});' },
|
||||
{ code: 'let describe; describe.skip(() => {});' },
|
||||
{ code: 'x.describe.skip(() => {});' },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `describe.skip(() => {});`,
|
||||
suggestion: `describe(() => {});`,
|
||||
},
|
||||
{
|
||||
code: `it.skip(() => {});`,
|
||||
suggestion: `it(() => {});`,
|
||||
},
|
||||
{
|
||||
code: `test.skip(() => {});`,
|
||||
suggestion: `test(() => {});`,
|
||||
},
|
||||
{
|
||||
code: `describe['skip'](() => {});`,
|
||||
suggestion: `describe(() => {});`,
|
||||
},
|
||||
{
|
||||
code: `it['skip'](() => {});`,
|
||||
suggestion: `it(() => {});`,
|
||||
},
|
||||
{
|
||||
code: `test['skip'](() => {});`,
|
||||
suggestion: `test(() => {});`,
|
||||
},
|
||||
].map(opts => {
|
||||
return {
|
||||
code: opts.code,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'unexpectedDisabledTest',
|
||||
suggestions: [{ messageId: 'removeSkip', output: opts.suggestion }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
});
|
||||
202
.oxlint/rules/noExtraneousDependencies.mjs
Normal file
202
.oxlint/rules/noExtraneousDependencies.mjs
Normal file
@ -0,0 +1,202 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { isBuiltin, findPackageJSON } from 'node:module';
|
||||
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
|
||||
|
||||
/**
|
||||
* @param value {unknown}
|
||||
* @returns {value is Record<string, unknown>}
|
||||
*/
|
||||
function isObject(value) {
|
||||
return typeof value === 'object' && value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param deps {unknown}
|
||||
*/
|
||||
function getDepsKeys(deps) {
|
||||
return new Set(isObject(deps) ? Object.keys(deps) : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param deps {unknown}
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
function getBundledDepsKeys(deps) {
|
||||
return Array.isArray(deps) ? new Set(deps) : getDepsKeys(deps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} PkgDeps
|
||||
* @property {Set<string>} dependencies
|
||||
* @property {Set<string>} devDependencies
|
||||
* @property {Set<string>} peerDependencies
|
||||
* @property {Set<string>} optionalDependencies
|
||||
* @property {Set<string>} bundledDependencies
|
||||
*/
|
||||
|
||||
/** @type {Map<string, PkgDeps>} */
|
||||
const PKG_DEPS_CACHE = new Map();
|
||||
|
||||
/** @param {string} currentFile */
|
||||
function getPkgDeps(currentFile) {
|
||||
const currentDir = dirname(currentFile);
|
||||
|
||||
const cached = PKG_DEPS_CACHE.get(currentDir);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const pkgPath = findPackageJSON('.', currentFile);
|
||||
if (pkgPath == null) {
|
||||
throw new Error(`Could not resolve package.json from ${currentFile}`);
|
||||
}
|
||||
const pkgText = readFileSync(pkgPath, 'utf8');
|
||||
const pkgJson = JSON.parse(pkgText);
|
||||
|
||||
/** @type {PkgDeps} */
|
||||
const pkgDeps = {
|
||||
dependencies: getDepsKeys(pkgJson.dependencies),
|
||||
devDependencies: getDepsKeys(pkgJson.devDependencies),
|
||||
peerDependencies: getDepsKeys(pkgJson.peerDependencies),
|
||||
optionalDependencies: getDepsKeys(pkgJson.optionalDependencies),
|
||||
bundledDependencies: getBundledDepsKeys(pkgJson.bundledDependencies),
|
||||
};
|
||||
|
||||
PKG_DEPS_CACHE.set(currentDir, pkgDeps);
|
||||
|
||||
return pkgDeps;
|
||||
}
|
||||
|
||||
/** @param {string} source */
|
||||
function getPackageNameFromSource(source) {
|
||||
if (source.startsWith('@')) {
|
||||
const [scope, name] = source.split('/', 2);
|
||||
return `${scope}/${name}`;
|
||||
}
|
||||
const [name] = source.split('/', 1);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Options
|
||||
* @property {boolean=} devDependencies
|
||||
* @property {boolean=} peerDependencies
|
||||
* @property {boolean=} optionalDependencies
|
||||
* @property {boolean=} bundledDependencies
|
||||
*/
|
||||
|
||||
/** @type {[Options]} */
|
||||
const defaultOptions = [
|
||||
{
|
||||
devDependencies: true,
|
||||
peerDependencies: true,
|
||||
optionalDependencies: true,
|
||||
bundledDependencies: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const noExtraneousDependencies = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-extraneous-dependencies',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
missingFromProjectDeps:
|
||||
"'{{pkgName}}' should be listed in the project's dependencies",
|
||||
wrongProjectDeps:
|
||||
"'{{pkgName}}' should be listed in the project's dependencies, found in {{found}}",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
devDependencies: { type: 'boolean' },
|
||||
peerDependencies: { type: 'boolean' },
|
||||
optionalDependencies: { type: 'boolean' },
|
||||
bundledDependencies: { type: 'boolean' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions,
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode, options } = context;
|
||||
|
||||
const opts = {
|
||||
devDependencies: options[0]?.devDependencies ?? true,
|
||||
peerDependencies: options[0]?.peerDependencies ?? true,
|
||||
optionalDependencies: options[0]?.optionalDependencies ?? true,
|
||||
bundledDependencies: options[0]?.bundledDependencies ?? true,
|
||||
};
|
||||
|
||||
const pkgDeps = getPkgDeps(context.physicalFilename);
|
||||
|
||||
return createImportSourceVisitor(sourceCode, node => {
|
||||
const source = node.value;
|
||||
|
||||
if (
|
||||
source.startsWith('.') ||
|
||||
source.startsWith('/') ||
|
||||
source.trim() === ''
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBuiltin(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pkgName = getPackageNameFromSource(source);
|
||||
|
||||
/** @type {Array<string>} */
|
||||
const found = [];
|
||||
|
||||
if (pkgDeps.dependencies.has(pkgName)) {
|
||||
return;
|
||||
}
|
||||
if (pkgDeps.devDependencies.has(pkgName)) {
|
||||
found.push('devDependencies');
|
||||
if (opts.devDependencies) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pkgDeps.peerDependencies.has(pkgName)) {
|
||||
found.push('peerDependencies');
|
||||
if (opts.peerDependencies) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pkgDeps.optionalDependencies.has(pkgName)) {
|
||||
found.push('optionalDependencies');
|
||||
if (opts.optionalDependencies) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pkgDeps.bundledDependencies.has(pkgName)) {
|
||||
found.push('bundledDependencies');
|
||||
if (opts.bundledDependencies) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (found.length > 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongProjectDeps',
|
||||
data: { pkgName, found: found.join(', ') },
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'missingFromProjectDeps',
|
||||
data: { pkgName },
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
112
.oxlint/rules/noExtraneousDependencies.test.mjs
Normal file
112
.oxlint/rules/noExtraneousDependencies.test.mjs
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import path from 'node:path';
|
||||
import { noExtraneousDependencies } from './noExtraneousDependencies.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
/**
|
||||
* @typedef {import("./noExtraneousDependencies.mjs").Options} Options
|
||||
*/
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
const filename = path.join(
|
||||
import.meta.dirname,
|
||||
'fixtures/noExtraneousDependencies/package/foo.js'
|
||||
);
|
||||
|
||||
/** @type {Options} */
|
||||
const NONE = {
|
||||
devDependencies: false,
|
||||
peerDependencies: false,
|
||||
optionalDependencies: false,
|
||||
bundledDependencies: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @satisfies {Record<string, [Options]>}
|
||||
*/
|
||||
const opts = {
|
||||
none: [NONE],
|
||||
dev: [{ ...NONE, devDependencies: true }],
|
||||
peer: [{ ...NONE, peerDependencies: true }],
|
||||
optional: [{ ...NONE, optionalDependencies: true }],
|
||||
bundled: [{ ...NONE, bundledDependencies: true }],
|
||||
};
|
||||
|
||||
ruleTester.run('no-extraneous-dependencies', noExtraneousDependencies, {
|
||||
valid: [
|
||||
{ filename, code: `import a from "./a";`, options: opts.none },
|
||||
{ filename, code: `import a from "../a";`, options: opts.none },
|
||||
{ filename, code: `import a from "path";`, options: opts.none },
|
||||
{ filename, code: `import a from "node:path";`, options: opts.none },
|
||||
{ filename, code: `import a from "";`, options: opts.none },
|
||||
{ filename, code: `import a from "prod-dep";`, options: opts.none },
|
||||
{ filename, code: `import a from "prod-dep/nested";`, options: opts.none },
|
||||
{ filename, code: `import a from "@scoped/prod-dep";`, options: opts.none },
|
||||
{
|
||||
filename,
|
||||
code: `import a from "@scoped/prod-dep/nested";`,
|
||||
options: opts.none,
|
||||
},
|
||||
{ filename, code: `import a from "dev-dep";`, options: opts.dev },
|
||||
{ filename, code: `import a from "peer-dep";`, options: opts.peer },
|
||||
{
|
||||
filename,
|
||||
code: `import a from "optional-dep";`,
|
||||
options: opts.optional,
|
||||
},
|
||||
{ filename, code: `import a from "bundled-dep";`, options: opts.bundled },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
filename,
|
||||
code: `import a from "dev-dep";`,
|
||||
options: opts.none,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "peer-dep";`,
|
||||
options: opts.none,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "optional-dep";`,
|
||||
options: opts.none,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "bundled-dep";`,
|
||||
options: opts.none,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "dev-dep";`,
|
||||
options: opts.peer,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "dev-dep";`,
|
||||
options: opts.optional,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "dev-dep";`,
|
||||
options: opts.bundled,
|
||||
errors: [{ messageId: 'wrongProjectDeps' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
code: `import a from "does-not-exist";`,
|
||||
options: opts.bundled,
|
||||
errors: [{ messageId: 'missingFromProjectDeps' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
60
.oxlint/rules/noFocusedTests.mjs
Normal file
60
.oxlint/rules/noFocusedTests.mjs
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { getReferenceType } from './utils/getReferenceType.mjs';
|
||||
import { isPropertyAccess } from './utils/astUtils.mjs';
|
||||
|
||||
export const noFocusedTests = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-focused-tests',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: true,
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
unexpectedFocusedTest: 'Unexpected focused test',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
const { sourceCode } = context;
|
||||
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (node.object.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
let replacement;
|
||||
if (node.object.name === 'describe') {
|
||||
replacement = 'describe';
|
||||
} else if (node.object.name === 'it') {
|
||||
replacement = 'it';
|
||||
} else if (node.object.name === 'test') {
|
||||
replacement = 'test';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPropertyAccess(node, 'only')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refType = getReferenceType(sourceCode, node.object);
|
||||
if (refType != null && refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'unexpectedFocusedTest',
|
||||
fix(fixer) {
|
||||
if (node.range == null) {
|
||||
return null;
|
||||
}
|
||||
return [fixer.replaceTextRange(node.range, replacement)];
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
52
.oxlint/rules/noFocusedTests.test.mjs
Normal file
52
.oxlint/rules/noFocusedTests.test.mjs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { noFocusedTests } from './noFocusedTests.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-focused-tests', noFocusedTests, {
|
||||
valid: [
|
||||
{ code: 'describe(() => {});' },
|
||||
{ code: 'it(() => {});' },
|
||||
{ code: 'test(() => {});' },
|
||||
{ code: 'describe.skip(() => {});' },
|
||||
{ code: 'it.skip(() => {});' },
|
||||
{ code: 'test.skip(() => {});' },
|
||||
{ code: 'let describe; describe.only(() => {});' },
|
||||
{ code: 'x.describe.only(() => {});' },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `describe.only(() => {});`,
|
||||
output: `describe(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
{
|
||||
code: `it.only(() => {});`,
|
||||
output: `it(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
{
|
||||
code: `test.only(() => {});`,
|
||||
output: `test(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
{
|
||||
code: `describe['only'](() => {});`,
|
||||
output: `describe(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
{
|
||||
code: `it['only'](() => {});`,
|
||||
output: `it(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
{
|
||||
code: `test['only'](() => {});`,
|
||||
output: `test(() => {});`,
|
||||
errors: [{ messageId: 'unexpectedFocusedTest' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
25
.oxlint/rules/noForIn.mjs
Normal file
25
.oxlint/rules/noForIn.mjs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
export const noForIn = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-for-in',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
preferForOf: 'Prefer for..of loops',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ForInStatement(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'preferForOf',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
25
.oxlint/rules/noForIn.test.mjs
Normal file
25
.oxlint/rules/noForIn.test.mjs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { noForIn } from './noForIn.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-for-in', noForIn, {
|
||||
valid: [
|
||||
{ code: 'for (let a of b) {}' },
|
||||
{ code: 'for (;;) {}' },
|
||||
{ code: 'if (a in b) {}' },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `for (let a in b) {}`,
|
||||
errors: [{ messageId: 'preferForOf' }],
|
||||
},
|
||||
{
|
||||
code: `for (a in b) {}`,
|
||||
errors: [{ messageId: 'preferForOf' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
264
.oxlint/rules/noRestrictedPaths.mjs
Normal file
264
.oxlint/rules/noRestrictedPaths.mjs
Normal file
@ -0,0 +1,264 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { createImportSourceVisitor } from './utils/createImportSourceVisitor.mjs';
|
||||
import micromatch from 'micromatch';
|
||||
import isGlob from 'is-glob';
|
||||
import * as path from 'node:path';
|
||||
import { assert } from './utils/assert.mjs';
|
||||
import enhancedResolve from 'enhanced-resolve';
|
||||
|
||||
const resolver = enhancedResolve.create.sync({
|
||||
extensionAlias: {
|
||||
'.js': ['.ts', '.tsx', '.js'],
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} fromDir
|
||||
* @param {string} moduleName
|
||||
*/
|
||||
function resolveFrom(fromDir, moduleName) {
|
||||
try {
|
||||
const result = resolver(fromDir, moduleName);
|
||||
if (result === false) {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function toArray(input) {
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath
|
||||
* @param {string} target
|
||||
*/
|
||||
function containsPath(filePath, target) {
|
||||
const relative = path.relative(target, filePath);
|
||||
return relative === '' || !relative.startsWith('..');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName
|
||||
* @param {RegExp | string} targetPath
|
||||
*/
|
||||
function isMatchingTargetPath(fileName, targetPath) {
|
||||
return typeof targetPath === 'string'
|
||||
? containsPath(fileName, targetPath)
|
||||
: targetPath.test(fileName);
|
||||
}
|
||||
|
||||
/** @type {Map<string, RegExp | string>} */
|
||||
const REGEX_CACHE = new Map();
|
||||
|
||||
/**
|
||||
* @typedef {object} Zone
|
||||
* @property {string | string[]=} target
|
||||
* @property {string | string[]=} from
|
||||
* @property {string[]=} except
|
||||
* @property {string=} message
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Matcher
|
||||
* @property {(RegExp | string)[]} targetPaths
|
||||
* @property {(RegExp | string)[]} fromPaths
|
||||
* @property {(RegExp | string)[] | null} exceptPaths
|
||||
* @property {string | null} message
|
||||
*/
|
||||
|
||||
/** @type {[Options]} */
|
||||
const defaultOptions = [{}];
|
||||
|
||||
/**
|
||||
* @typedef {object} Options
|
||||
* @property {Zone[]=} zones
|
||||
* @property {string=} basePath
|
||||
*/
|
||||
|
||||
export const noRestrictedPaths = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-restricted-paths',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
pathRestrictedNoMessage:
|
||||
'Unexpected path "{{moduleName}}" imported in restricted zone.',
|
||||
pathRestrictedWithMessage:
|
||||
'Unexpected path "{{moduleName}}" imported in restricted zone. {{message}}',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
zones: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
target: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
from: {
|
||||
anyOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
except: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
message: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
basePath: { type: 'string' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
defaultOptions,
|
||||
},
|
||||
create(context) {
|
||||
const { filename, sourceCode } = context;
|
||||
|
||||
const zones = context.options[0]?.zones ?? [];
|
||||
const basePath = context.options[0]?.basePath ?? context.cwd;
|
||||
|
||||
const matchers = zones.map(zone => {
|
||||
assert(zone.target != null, 'Zone missing `target`');
|
||||
assert(zone.from != null, 'Zone missing `from`');
|
||||
const zoneTarget = toArray(zone.target);
|
||||
const zoneFrom = toArray(zone.from);
|
||||
assert(zoneTarget.length > 0, 'Zone needs at least one `target`');
|
||||
assert(zoneFrom.length > 0, 'Zone needs at least one `from`');
|
||||
|
||||
let zoneExcept = zone.except != null ? toArray(zone.except) : null;
|
||||
if (zoneExcept?.length === 0) {
|
||||
zoneExcept = null;
|
||||
}
|
||||
|
||||
let hasGlobPatterns = false;
|
||||
let hasNonGlobPatterns = false;
|
||||
|
||||
/** @param {string} target */
|
||||
function compilePattern(target) {
|
||||
const targetPath = path.resolve(basePath, target);
|
||||
|
||||
const cached = REGEX_CACHE.get(targetPath);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** @type {RegExp | string} */
|
||||
let result;
|
||||
if (isGlob(targetPath)) {
|
||||
hasGlobPatterns = true;
|
||||
result = micromatch.makeRe(targetPath);
|
||||
} else {
|
||||
hasNonGlobPatterns = true;
|
||||
result = targetPath;
|
||||
}
|
||||
|
||||
if (hasGlobPatterns && hasNonGlobPatterns) {
|
||||
throw new Error(
|
||||
'Cannot have both glob and non-glob patterns in the same zone'
|
||||
);
|
||||
}
|
||||
|
||||
REGEX_CACHE.set(targetPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @type {Matcher} */
|
||||
const matcher = {
|
||||
targetPaths: zoneTarget.map(target => compilePattern(target)),
|
||||
fromPaths: zoneFrom.map(from => compilePattern(from)),
|
||||
exceptPaths: zoneExcept?.map(except => compilePattern(except)) ?? null,
|
||||
message: zone.message ?? null,
|
||||
};
|
||||
|
||||
return matcher;
|
||||
});
|
||||
|
||||
const targetMatchers = matchers.filter(matcher => {
|
||||
return matcher.targetPaths.some(targetPath => {
|
||||
return isMatchingTargetPath(filename, targetPath);
|
||||
});
|
||||
});
|
||||
|
||||
if (targetMatchers.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return createImportSourceVisitor(sourceCode, source => {
|
||||
const dirname = path.dirname(filename);
|
||||
const moduleName = source.value;
|
||||
|
||||
const resolvedPath = resolveFrom(dirname, moduleName);
|
||||
if (resolvedPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const matcher of targetMatchers) {
|
||||
const matchesFromPath = matcher.fromPaths.some(fromPath => {
|
||||
return isMatchingTargetPath(resolvedPath, fromPath);
|
||||
});
|
||||
|
||||
if (!matchesFromPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const matchesExceptPath = matcher.exceptPaths?.some(exceptPath => {
|
||||
return isMatchingTargetPath(resolvedPath, exceptPath);
|
||||
});
|
||||
|
||||
if (matchesExceptPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matcher.message != null) {
|
||||
context.report({
|
||||
node: source,
|
||||
messageId: 'pathRestrictedWithMessage',
|
||||
data: { moduleName, message: matcher.message },
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
node: source,
|
||||
messageId: 'pathRestrictedNoMessage',
|
||||
data: { moduleName },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
54
.oxlint/rules/noRestrictedPaths.test.mjs
Normal file
54
.oxlint/rules/noRestrictedPaths.test.mjs
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { noRestrictedPaths } from './noRestrictedPaths.mjs';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const basePath = path.join(import.meta.dirname, 'fixtures/noRestrictedPaths');
|
||||
const filename = path.join(basePath, 'client/entry.ts');
|
||||
|
||||
/**
|
||||
* @param {boolean=} withMessage
|
||||
* @returns {[import("./noRestrictedPaths.mjs").Options]}
|
||||
*/
|
||||
function opts(withMessage) {
|
||||
const message = withMessage ? 'just stop it' : undefined;
|
||||
return [
|
||||
{ basePath, zones: [{ target: './client', from: './server', message }] },
|
||||
];
|
||||
}
|
||||
|
||||
const ruleTester = new RuleTester();
|
||||
|
||||
ruleTester.run('no-restricted-paths', noRestrictedPaths, {
|
||||
valid: [
|
||||
{ filename, options: opts(), code: `import b from './client.ts';` },
|
||||
{ filename, options: opts(), code: `import b from './client.js';` },
|
||||
{ filename, options: opts(), code: `import b from '../client/client.ts';` },
|
||||
{ filename, options: opts(), code: `import b from './nonexistant';` },
|
||||
{ filename, options: opts(), code: `import b from 'node:path';` },
|
||||
{ filename, options: opts(), code: `import b from 'react';` },
|
||||
{ filename, options: opts(), code: `import b from 'fake-module';` },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
filename,
|
||||
options: opts(),
|
||||
code: `import b from '../server/server.ts';`,
|
||||
errors: [{ messageId: 'pathRestrictedNoMessage' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
options: opts(),
|
||||
code: `import b from '../server/server.js';`,
|
||||
errors: [{ messageId: 'pathRestrictedNoMessage' }],
|
||||
},
|
||||
{
|
||||
filename,
|
||||
options: opts(true),
|
||||
code: `import b from '../server/server.ts';`,
|
||||
errors: [{ messageId: 'pathRestrictedWithMessage' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
35
.oxlint/rules/noThen.mjs
Normal file
35
.oxlint/rules/noThen.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||
import { isPropertyAccess } from './utils/astUtils.mjs';
|
||||
|
||||
export const noThen = ESLintUtils.RuleCreator.withoutDocs({
|
||||
name: 'no-then',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
messages: {
|
||||
preferAwait: 'Prefer await instead of .then()',
|
||||
},
|
||||
schema: [],
|
||||
defaultOptions: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
if (!isPropertyAccess(node, 'then')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.parent.type !== 'CallExpression') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.property,
|
||||
messageId: 'preferAwait',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
14
.oxlint/rules/utils/assert.mjs
Normal file
14
.oxlint/rules/utils/assert.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param condition {unknown}
|
||||
* @param message {string}
|
||||
* @returns {asserts condition}
|
||||
*/
|
||||
export function assert(condition, message) {
|
||||
if (condition == null || condition === false) {
|
||||
throw new TypeError(message);
|
||||
}
|
||||
}
|
||||
36
.oxlint/rules/utils/astUtils.mjs
Normal file
36
.oxlint/rules/utils/astUtils.mjs
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Node} Node
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Literal} Literal
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.MemberExpression} MemberExpression
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Node=} node
|
||||
* @returns {node is StringLiteral}
|
||||
*/
|
||||
export function isStringLiteral(node) {
|
||||
return node?.type === 'Literal' && typeof node.value === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node | null | undefined} node
|
||||
* @param {string} property
|
||||
* @returns {node is MemberExpression}
|
||||
*/
|
||||
export function isPropertyAccess(node, property) {
|
||||
if (node?.type !== 'MemberExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.computed) {
|
||||
return node.property.type === 'Literal' && node.property.value === property;
|
||||
}
|
||||
|
||||
return node.property.type === 'Identifier' && node.property.name === property;
|
||||
}
|
||||
123
.oxlint/rules/utils/createImportSourceVisitor.mjs
Normal file
123
.oxlint/rules/utils/createImportSourceVisitor.mjs
Normal file
@ -0,0 +1,123 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
import { getReferenceType } from './getReferenceType.mjs';
|
||||
import { isStringLiteral } from './astUtils.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.StringLiteral} StringLiteral
|
||||
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
|
||||
* @typedef {import("@typescript-eslint/utils").TSESLint.RuleListener} RuleListener
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SourceCode} sourceCode
|
||||
* @param {(source: StringLiteral) => void} visitSource
|
||||
* @returns {RuleListener}
|
||||
*/
|
||||
export function createImportSourceVisitor(sourceCode, visitSource) {
|
||||
return {
|
||||
// import ... from '<source>'
|
||||
ImportDeclaration(node) {
|
||||
visitSource(node.source);
|
||||
},
|
||||
// import('<source>')
|
||||
ImportExpression(node) {
|
||||
if (!isStringLiteral(node.source)) {
|
||||
return;
|
||||
}
|
||||
visitSource(node.source);
|
||||
},
|
||||
CallExpression(node) {
|
||||
// require('<source>')
|
||||
if (node.callee.type === 'Identifier') {
|
||||
if (node.callee.name !== 'require') {
|
||||
return;
|
||||
}
|
||||
const refType = getReferenceType(sourceCode, node.callee);
|
||||
if (refType != null && refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
const arg = node.arguments.at(0);
|
||||
if (!isStringLiteral(arg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitSource(arg);
|
||||
return;
|
||||
}
|
||||
|
||||
// require.resolve('<source>')
|
||||
if (node.callee.type === 'MemberExpression') {
|
||||
const { object, property } = node.callee;
|
||||
|
||||
if (object.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
if (object.name !== 'require') {
|
||||
return;
|
||||
}
|
||||
const refType = getReferenceType(sourceCode, object);
|
||||
if (refType != null && refType !== 'global') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (property.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
if (property.name !== 'resolve') {
|
||||
return;
|
||||
}
|
||||
|
||||
const arg = node.arguments.at(0);
|
||||
if (!isStringLiteral(arg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitSource(arg);
|
||||
}
|
||||
},
|
||||
// import.meta.resolve('<source>')
|
||||
MetaProperty(node) {
|
||||
if (node.meta.name !== 'import') {
|
||||
return;
|
||||
}
|
||||
if (node.property.name !== 'meta') {
|
||||
return;
|
||||
}
|
||||
const memberExpression = node.parent;
|
||||
if (memberExpression.type !== 'MemberExpression') {
|
||||
return;
|
||||
}
|
||||
if (memberExpression.property.type !== 'Identifier') {
|
||||
return;
|
||||
}
|
||||
if (memberExpression.property.name !== 'resolve') {
|
||||
return;
|
||||
}
|
||||
const callExpression = memberExpression.parent;
|
||||
if (callExpression.type !== 'CallExpression') {
|
||||
return;
|
||||
}
|
||||
const arg = callExpression.arguments.at(0);
|
||||
if (!isStringLiteral(arg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitSource(arg);
|
||||
},
|
||||
// export {...} from '<source>'
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.source == null) {
|
||||
return;
|
||||
}
|
||||
visitSource(node.source);
|
||||
},
|
||||
// export * ... from '<source>'
|
||||
ExportAllDeclaration(node) {
|
||||
visitSource(node.source);
|
||||
},
|
||||
};
|
||||
}
|
||||
18
.oxlint/rules/utils/getReferenceType.mjs
Normal file
18
.oxlint/rules/utils/getReferenceType.mjs
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {import("@typescript-eslint/utils").TSESTree.Identifier} Identifier
|
||||
* @typedef {import("@typescript-eslint/utils").TSESLint.SourceCode} SourceCode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SourceCode} sourceCode
|
||||
* @param {Identifier} node
|
||||
*/
|
||||
export function getReferenceType(sourceCode, node) {
|
||||
const scope = sourceCode.getScope(node);
|
||||
const ref = scope.references.find(r => r.identifier === node);
|
||||
return ref?.resolved?.scope.type ?? null;
|
||||
}
|
||||
7
.oxlint/test-setup.mjs
Normal file
7
.oxlint/test-setup.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
import * as mocha from 'mocha'
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
RuleTester.afterAll = mocha.after;
|
||||
1902
.oxlintrc.json
Normal file
1902
.oxlintrc.json
Normal file
File diff suppressed because it is too large
Load Diff
269
.pnpmfile.mjs
Normal file
269
.pnpmfile.mjs
Normal file
@ -0,0 +1,269 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
//
|
||||
// WARNING: Do not import (or even `import()`) any packages, they won't always be installed.
|
||||
//
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { styleText } from 'node:util';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
/**
|
||||
* From @pnpm/pnpmfile/lib/Hooks.d.ts
|
||||
*
|
||||
* @typedef {{
|
||||
* deprecated?: boolean;
|
||||
* }} PackageSnapshot
|
||||
*
|
||||
* @typedef {Record<string, PackageSnapshot>} PackageSnapshots
|
||||
*
|
||||
* @typedef {{
|
||||
* packages?: PackageSnapshots
|
||||
* }} LockfileObject
|
||||
*
|
||||
* @typedef {{
|
||||
* log: (message: string) => void;
|
||||
* }} HookContext
|
||||
*
|
||||
* @typedef {{
|
||||
* verifyDepsBeforeRun?: unknown,
|
||||
* }} Config
|
||||
*
|
||||
* @typedef {{
|
||||
* afterAllResolved?: (
|
||||
* lockfile: LockfileObject,
|
||||
* context: HookContext,
|
||||
* ) => LockfileObject | Promise<LockfileObject>;
|
||||
* updateConfig?: (config: Config) => Config | Promise<Config>
|
||||
* }} Hooks
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {boolean} condition
|
||||
* @param {string} message
|
||||
* @returns {asserts condition}
|
||||
*/
|
||||
// function assert(condition, message) {
|
||||
// if (!condition) {
|
||||
// throw new TypeError(message);
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
*/
|
||||
function formatError(message) {
|
||||
return `${styleText(['bgRed', 'whiteBright'], '[ERROR]')} ${styleText('red', message)}`;
|
||||
}
|
||||
|
||||
/** @type {any} */
|
||||
let CACHED_WORKSPACE_CONFIG;
|
||||
async function getWorkspaceConfig() {
|
||||
if (CACHED_WORKSPACE_CONFIG == null) {
|
||||
const stdout = execSync('pnpm config list --json', {
|
||||
encoding: 'utf-8',
|
||||
env: { PATH: process.env.PATH },
|
||||
});
|
||||
const config = JSON.parse(stdout);
|
||||
CACHED_WORKSPACE_CONFIG = config;
|
||||
}
|
||||
return CACHED_WORKSPACE_CONFIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples:
|
||||
* - "jest-process-manager@0.4.0"
|
||||
* - "@jest/process-manager@0.4.0"
|
||||
* - "jest-process-manager@0.4.0(debug@4.4.3)"
|
||||
*
|
||||
* @param {string} packagePath
|
||||
*/
|
||||
function parsePackagePath(packagePath) {
|
||||
const truncateAt = packagePath.indexOf('(');
|
||||
const packageSpec =
|
||||
truncateAt === -1 ? packagePath : packagePath.slice(0, truncateAt);
|
||||
|
||||
const splitAt = packageSpec.lastIndexOf('@');
|
||||
const name = packageSpec.slice(0, splitAt);
|
||||
const version = packageSpec.slice(splitAt + 1);
|
||||
|
||||
return { name, version };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* path: string,
|
||||
* name: string,
|
||||
* version: string,
|
||||
* snapshot: PackageSnapshot,
|
||||
* }} PackageSnapshotEntry
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LockfileObject} lockfile
|
||||
* @returns {ReadonlyArray<PackageSnapshotEntry>}
|
||||
*/
|
||||
function getPackages(lockfile) {
|
||||
const { packages = {} } = lockfile;
|
||||
return Object.keys(packages).map(path => {
|
||||
const snapshot = packages[path];
|
||||
const { name, version } = parsePackagePath(path);
|
||||
return { path, name, version, snapshot };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal semver support, only supports exact versions and `||`
|
||||
* @param {string} version
|
||||
* @param {string} range
|
||||
*/
|
||||
function satisfies(version, range) {
|
||||
return range.split('||').some(choice => {
|
||||
return choice.trim() === version;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {(lockfile: LockfileObject, context: HookContext) => Promise<boolean>} CustomCheck
|
||||
*/
|
||||
|
||||
/** @type {CustomCheck} */
|
||||
async function noDeprecatedPackages(lockfile, context) {
|
||||
const config = await getWorkspaceConfig();
|
||||
const packages = getPackages(lockfile);
|
||||
|
||||
const deprecated = packages.filter(pkg => {
|
||||
if (!pkg.snapshot.deprecated) {
|
||||
return false;
|
||||
}
|
||||
const allowed = config.allowedDeprecatedVersions?.[pkg.name];
|
||||
if (allowed != null && satisfies(pkg.version, allowed)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const success = deprecated.length === 0;
|
||||
|
||||
if (!success) {
|
||||
context.log('');
|
||||
context.log(
|
||||
formatError(
|
||||
'Found deprecated packages, to ignore them add this to the pnpm-workspace.yaml file:'
|
||||
)
|
||||
);
|
||||
context.log('');
|
||||
context.log('allowedDeprecatedVersions:');
|
||||
for (const pkg of deprecated) {
|
||||
context.log(` '${pkg.name}': '${pkg.version}'`);
|
||||
}
|
||||
context.log('');
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/** @type {ReadonlyArray<RegExp>} */
|
||||
const RESTRICTED_DUPLICATE_DEPENDENCIES = [
|
||||
// /^@signalapp\//,
|
||||
// /^@indutny\//,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isRestrictedDuplicateDependency(name) {
|
||||
return RESTRICTED_DUPLICATE_DEPENDENCIES.some(regex => {
|
||||
return regex.test(name);
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {CustomCheck} */
|
||||
async function restrictDuplicateDependencies(lockfile, context) {
|
||||
const packages = getPackages(lockfile);
|
||||
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const seen = new Map();
|
||||
/** @type {Set<string>} */
|
||||
const duplicates = new Set();
|
||||
|
||||
for (const pkg of packages) {
|
||||
if (!isRestrictedDuplicateDependency(pkg.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let versions = seen.get(pkg.name);
|
||||
if (versions != null) {
|
||||
duplicates.add(pkg.name);
|
||||
} else {
|
||||
versions = new Set();
|
||||
seen.set(pkg.name, versions);
|
||||
}
|
||||
versions.add(pkg.version);
|
||||
}
|
||||
|
||||
const success = duplicates.size === 0;
|
||||
|
||||
if (!success) {
|
||||
context.log('');
|
||||
context.log(formatError('Found duplicate restricted packages:'));
|
||||
context.log('');
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const versions = seen.get(duplicate);
|
||||
assert(versions != null, `Missing package versions for ${duplicate}`);
|
||||
context.log(` ${duplicate}: ${Array.from(versions).join(', ')}`);
|
||||
}
|
||||
context.log('');
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/** @type {Hooks} */
|
||||
export const hooks = {
|
||||
async afterAllResolved(lockfile, context) {
|
||||
const results = await Promise.all([
|
||||
noDeprecatedPackages(lockfile, context),
|
||||
restrictDuplicateDependencies(lockfile, context),
|
||||
]);
|
||||
|
||||
const hasAnyFailures = results.includes(false);
|
||||
if (hasAnyFailures) {
|
||||
context.log(
|
||||
formatError(
|
||||
'pnpm install failed because of a custom check in .pnpmfile.mjs'
|
||||
)
|
||||
);
|
||||
context.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return lockfile;
|
||||
},
|
||||
|
||||
updateConfig(config) {
|
||||
return {
|
||||
...config,
|
||||
verifyDepsBeforeRun:
|
||||
process.env.CI || process.env.SKIP_VERIFY_DEPS_BEFORE_RUN
|
||||
? false
|
||||
: config.verifyDepsBeforeRun,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.NODE_TEST_CONTEXT) {
|
||||
await test('noDeprecatedPackages', async () => {
|
||||
const pkg = '@scope/pkg-name@1.0.0(@other-scope/other-pkg@2.0.0)';
|
||||
const success = await noDeprecatedPackages(
|
||||
{ packages: { [pkg]: { deprecated: true } } },
|
||||
{ log: () => undefined }
|
||||
);
|
||||
assert(!success);
|
||||
});
|
||||
}
|
||||
@ -3,18 +3,14 @@
|
||||
|
||||
# Generated files
|
||||
build/**/*.js
|
||||
build/**/*.json
|
||||
app/**/*.js
|
||||
config/local-*.json
|
||||
config/local.json
|
||||
dist/**
|
||||
js/components.js
|
||||
js/util_worker.js
|
||||
libtextsecure/components.js
|
||||
libtextsecure/test/test.js
|
||||
stylesheets/*.css
|
||||
test/test.js
|
||||
ts/**/*.js
|
||||
!ts/**/.eslintrc.js
|
||||
ts/protobuf/*.d.ts
|
||||
ts/protobuf/*.js
|
||||
stylesheets/manifest.css
|
||||
@ -30,13 +26,13 @@ pnpm-lock.yaml
|
||||
# Third-party files
|
||||
node_modules/**
|
||||
packages/*/node_modules/**
|
||||
packages/lame/wrapper.mjs
|
||||
packages/lame/lame-*/
|
||||
packages/windows-ucv/dist/**
|
||||
danger/node_modules/**
|
||||
sticker-creator/node_modules/**
|
||||
components/**
|
||||
js/curve/**
|
||||
js/Mp3LameEncoder.min.js
|
||||
js/WebAudioRecorderMp3.js
|
||||
js/calling-tools/**
|
||||
scripts/emoji-datasource/emoji-datasource.json
|
||||
|
||||
# Assets
|
||||
/images/
|
||||
|
||||
@ -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
|
||||
7
.storybook/StorybookThemeContext.std.d.ts
vendored
7
.storybook/StorybookThemeContext.std.d.ts
vendored
@ -1,7 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Context } from 'react';
|
||||
import type { ThemeType } from '../ts/types/Util.std.js';
|
||||
|
||||
export const StorybookThemeContext: Context<ThemeType>;
|
||||
@ -2,6 +2,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { ThemeType } from '../ts/types/Util.std.js';
|
||||
import { ThemeType } from '../ts/types/Util.std.ts';
|
||||
|
||||
export const StorybookThemeContext = createContext(ThemeType.light);
|
||||
@ -11,7 +11,7 @@ const EXTERNALS = new Set(builtinModules);
|
||||
EXTERNALS.delete('buffer');
|
||||
EXTERNALS.delete('url');
|
||||
|
||||
const config: StorybookConfig = {
|
||||
const storybookConfig: StorybookConfig = {
|
||||
typescript: {
|
||||
reactDocgen: false,
|
||||
},
|
||||
@ -43,63 +43,82 @@ const config: StorybookConfig = {
|
||||
{ from: '../fonts', to: 'fonts' },
|
||||
{ from: '../images', to: 'images' },
|
||||
{ from: '../fixtures', to: 'fixtures' },
|
||||
{
|
||||
from: '../node_modules/emoji-datasource-apple/img',
|
||||
to: 'node_modules/emoji-datasource-apple/img',
|
||||
},
|
||||
{
|
||||
from: '../node_modules/intl-tel-input/build/img',
|
||||
to: 'node_modules/intl-tel-input/build/img',
|
||||
},
|
||||
],
|
||||
|
||||
webpackFinal(config) {
|
||||
config.cache = {
|
||||
swc() {
|
||||
return {
|
||||
jsc: {
|
||||
transform: {
|
||||
react: { runtime: 'automatic' },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
webpackFinal(webpackConfig) {
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
webpackConfig.cache = {
|
||||
type: 'filesystem',
|
||||
};
|
||||
|
||||
config.resolve!.extensionAlias = {
|
||||
// oxlint-disable-next-line no-param-reassign, typescript/no-non-null-assertion
|
||||
webpackConfig.resolve!.extensionAlias = {
|
||||
'.js': ['.tsx', '.ts', '.js'],
|
||||
};
|
||||
|
||||
config.module!.rules!.unshift({
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
webpackConfig.module!.rules!.unshift({
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' },
|
||||
{ loader: 'css-loader', options: { modules: false, url: false } },
|
||||
{ loader: 'sass-loader' },
|
||||
{ loader: require.resolve('style-loader') },
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: { modules: false, url: false },
|
||||
},
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
additionalData: '$is-storybook: true;',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.module!.rules!.unshift({
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
webpackConfig.module!.rules!.unshift({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
// prevent storybook defaults from being applied
|
||||
],
|
||||
});
|
||||
|
||||
config.module!.rules!.push({
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
webpackConfig.module!.rules!.push({
|
||||
test: /tailwind-config\.css$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: false,
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
plugins: [require.resolve('@tailwindcss/postcss')],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.node = { global: true };
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
webpackConfig.node = { global: true };
|
||||
|
||||
config.externals = ({ request }, callback) => {
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
webpackConfig.externals = ({ request }, callback) => {
|
||||
if (
|
||||
(/^node:/.test(request) && request !== 'node:buffer') ||
|
||||
(request.startsWith('node:') && request !== 'node:buffer') ||
|
||||
EXTERNALS.has(request)
|
||||
) {
|
||||
// Keep Node.js imports unchanged
|
||||
@ -108,16 +127,17 @@ const config: StorybookConfig = {
|
||||
callback();
|
||||
};
|
||||
|
||||
config.plugins!.push(
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
webpackConfig.plugins!.push(
|
||||
new ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
})
|
||||
);
|
||||
|
||||
return config;
|
||||
return webpackConfig;
|
||||
},
|
||||
|
||||
docs: {},
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default storybookConfig;
|
||||
|
||||
@ -3,38 +3,38 @@
|
||||
|
||||
import '../ts/window.d.ts';
|
||||
|
||||
import React, { StrictMode } from 'react';
|
||||
|
||||
import '@signalapp/quill-cjs/dist/quill.core.css';
|
||||
import '../stylesheets/manifest.scss';
|
||||
import '../stylesheets/tailwind-config.css';
|
||||
import * as styles from './styles.scss';
|
||||
import messages from '../_locales/en/messages.json';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store, combineReducers, createStore } from 'redux';
|
||||
import type { Store } from 'redux';
|
||||
import { combineReducers, createStore } from 'redux';
|
||||
import { Globals } from '@react-spring/web';
|
||||
|
||||
import { StorybookThemeContext } from './StorybookThemeContext.std.js';
|
||||
import { SystemThemeType, ThemeType } from '../ts/types/Util.std.js';
|
||||
import { setupI18n } from '../ts/util/setupI18n.dom.js';
|
||||
import { HourCyclePreference } from '../ts/types/I18N.std.js';
|
||||
import { AxoProvider } from '../ts/axo/AxoProvider.dom.js';
|
||||
import type { StateType } from '../ts/state/reducer.preload.js';
|
||||
import { StorybookThemeContext } from './StorybookThemeContext.std.ts';
|
||||
import { SystemThemeType, ThemeType } from '../ts/types/Util.std.ts';
|
||||
import { setupI18n } from '../ts/util/setupI18n.dom.tsx';
|
||||
import { HourCyclePreference } from '../ts/types/I18N.std.ts';
|
||||
import { AppProvider } from '../ts/windows/AppProvider.dom.tsx';
|
||||
import type { StateType } from '../ts/state/reducer.preload.ts';
|
||||
import {
|
||||
ScrollerLockContext,
|
||||
createScrollerLock,
|
||||
} from '../ts/hooks/useScrollLock.dom.js';
|
||||
import { Environment, setEnvironment } from '../ts/environment.std.js';
|
||||
import { parseUnknown } from '../ts/util/schemas.std.js';
|
||||
import { LocaleEmojiListSchema } from '../ts/types/emoji.std.js';
|
||||
import { FunProvider } from '../ts/components/fun/FunProvider.dom.js';
|
||||
import { EmojiSkinTone } from '../ts/components/fun/data/emojis.std.js';
|
||||
import { MOCK_GIFS_PAGINATED_ONE_PAGE } from '../ts/components/fun/mocks.dom.js';
|
||||
import { NavTab } from '../ts/types/Nav.std.js';
|
||||
} from '../ts/hooks/useScrollLock.dom.tsx';
|
||||
import { Environment, setEnvironment } from '../ts/environment.std.ts';
|
||||
import { parseUnknown } from '../ts/util/schemas.std.ts';
|
||||
import { LocaleEmojiListSchema } from '../ts/types/emoji.std.ts';
|
||||
import { FunProvider } from '../ts/components/fun/FunProvider.dom.tsx';
|
||||
import { MOCK_GIFS_PAGINATED_ONE_PAGE } from '../ts/test-helpers/funPickerMocks.dom.tsx';
|
||||
import { NavTab } from '../ts/types/Nav.std.ts';
|
||||
|
||||
import type { FunEmojiSelection } from '../ts/components/fun/panels/FunPanelEmojis.dom.js';
|
||||
import type { FunGifSelection } from '../ts/components/fun/panels/FunPanelGifs.dom.js';
|
||||
import type { FunStickerSelection } from '../ts/components/fun/panels/FunPanelStickers.dom.js';
|
||||
import type { FunEmojiSelection } from '../ts/components/fun/panels/FunPanelEmojis.dom.tsx';
|
||||
import type { FunGifSelection } from '../ts/components/fun/panels/FunPanelGifs.dom.tsx';
|
||||
import type { FunStickerSelection } from '../ts/components/fun/panels/FunPanelStickers.dom.tsx';
|
||||
import { Emoji } from '../ts/axo/emoji.std.ts';
|
||||
|
||||
setEnvironment(Environment.Development, true);
|
||||
|
||||
@ -100,10 +100,10 @@ const mockStore: Store<StateType> = createStore(
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// oxlint-disable-next-line
|
||||
const noop = () => {};
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper ??= {};
|
||||
window.Whisper.events = {
|
||||
on: noop,
|
||||
off: noop,
|
||||
@ -138,6 +138,7 @@ window.SignalContext = {
|
||||
platform: '',
|
||||
release: '',
|
||||
},
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
config: {} as any,
|
||||
|
||||
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
||||
@ -166,7 +167,7 @@ window.SignalContext = {
|
||||
_stopTrackingICUStrings: () => i18n.stopTrackingUsage(),
|
||||
};
|
||||
|
||||
window.ConversationController = window.ConversationController || {};
|
||||
window.ConversationController ??= {};
|
||||
window.ConversationController.isSignalConversationId = () => false;
|
||||
window.ConversationController.onConvoMessageMount = noop;
|
||||
window.reduxStore = mockStore;
|
||||
@ -182,14 +183,6 @@ window.Signal = {
|
||||
},
|
||||
};
|
||||
|
||||
function withStrictMode(Story, context) {
|
||||
return (
|
||||
<StrictMode>
|
||||
<Story {...context} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
const withGlobalTypesProvider = (Story, context) => {
|
||||
const theme =
|
||||
context.globals.theme === 'light' ? ThemeType.light : ThemeType.dark;
|
||||
@ -241,7 +234,7 @@ function withMockStoreProvider(Story, context) {
|
||||
function withScrollLockProvider(Story, context) {
|
||||
return (
|
||||
<ScrollerLockContext.Provider
|
||||
value={createScrollerLock('MockStories', () => {})}
|
||||
value={createScrollerLock('MockStories', () => null)}
|
||||
>
|
||||
<Story {...context} />
|
||||
</ScrollerLockContext.Provider>
|
||||
@ -255,7 +248,7 @@ function withFunProvider(Story, context) {
|
||||
recentEmojis={[]}
|
||||
recentStickers={[]}
|
||||
recentGifs={[]}
|
||||
emojiSkinToneDefault={EmojiSkinTone.None}
|
||||
emojiSkinToneDefault={Emoji.SkinTone.None}
|
||||
onEmojiSkinToneDefaultChange={noop}
|
||||
installedStickerPacks={[]}
|
||||
showStickerPickerHint={false}
|
||||
@ -265,12 +258,15 @@ function withFunProvider(Story, context) {
|
||||
fetchGifsFeatured={() => Promise.resolve(MOCK_GIFS_PAGINATED_ONE_PAGE)}
|
||||
fetchGif={() => Promise.resolve(new Blob([new Uint8Array(1)]))}
|
||||
onSelectEmoji={function (emojiSelection: FunEmojiSelection): void {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('onSelectEmoji', emojiSelection);
|
||||
}}
|
||||
onSelectSticker={function (stickerSelection: FunStickerSelection): void {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('onSelectSticker', stickerSelection);
|
||||
}}
|
||||
onSelectGif={function (gifSelection: FunGifSelection): void {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('onSelectGif', gifSelection);
|
||||
}}
|
||||
>
|
||||
@ -279,19 +275,16 @@ function withFunProvider(Story, context) {
|
||||
);
|
||||
}
|
||||
|
||||
function withAxoProvider(Story, context) {
|
||||
const globalValue = context.globals.direction ?? 'ltr';
|
||||
const dir = globalValue === 'auto' ? 'ltr' : globalValue;
|
||||
function withAppProvider(Story, context) {
|
||||
return (
|
||||
<AxoProvider dir={dir}>
|
||||
<AppProvider>
|
||||
<Story {...context} />
|
||||
</AxoProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export const decorators = [
|
||||
withStrictMode,
|
||||
withAxoProvider,
|
||||
withAppProvider,
|
||||
withGlobalTypesProvider,
|
||||
withMockStoreProvider,
|
||||
withScrollLockProvider,
|
||||
|
||||
@ -54,16 +54,20 @@ const config: TestRunnerConfig = {
|
||||
.or(page.getByTitle(value))
|
||||
.or(page.getByLabel(value));
|
||||
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
if (await locator.count()) {
|
||||
const first = locator.first();
|
||||
|
||||
try {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await first.focus({ timeout: SECOND });
|
||||
} catch {
|
||||
// Opportunistic
|
||||
}
|
||||
try {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
if (await first.isVisible()) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await first.scrollIntoViewIfNeeded({ timeout: SECOND });
|
||||
}
|
||||
} catch {
|
||||
@ -71,6 +75,7 @@ const config: TestRunnerConfig = {
|
||||
}
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const image = await page.screenshot({
|
||||
animations: 'disabled',
|
||||
fullPage: true,
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
// @ts-expect-error
|
||||
import githubActionsFormatter from '@csstools/stylelint-formatter-github';
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('stylelint').Config} */
|
||||
const config = {
|
||||
formatter: process.env.CI ? githubActionsFormatter : undefined,
|
||||
extends: [
|
||||
'stylelint-config-recommended-scss',
|
||||
'stylelint-config-css-modules',
|
||||
@ -9,6 +14,7 @@ module.exports = {
|
||||
plugins: ['stylelint-use-logical-spec'],
|
||||
rules: {
|
||||
// Disabled from recommended set to get stylelint working initially
|
||||
'at-rule-empty-line-before': null,
|
||||
'block-no-empty': null,
|
||||
'declaration-block-no-duplicate-properties': null,
|
||||
'declaration-block-no-shorthand-property-overrides': null,
|
||||
@ -16,7 +22,6 @@ module.exports = {
|
||||
'no-duplicate-selectors': null,
|
||||
'no-descending-specificity': null,
|
||||
'selector-pseudo-element-no-unknown': null,
|
||||
'scss/at-import-partial-extension': null,
|
||||
'scss/comment-no-empty': null,
|
||||
'scss/no-global-function-names': null,
|
||||
'scss/operator-no-newline-after': null,
|
||||
@ -45,3 +50,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
6827
ACKNOWLEDGMENTS.md
6827
ACKNOWLEDGMENTS.md
File diff suppressed because it is too large
Load Diff
@ -5,17 +5,49 @@
|
||||
|
||||
## Advice for new contributors
|
||||
|
||||
Start small. The PRs most likely to be merged are the ones that make small,
|
||||
easily reviewed changes with clear and specific intentions. See below for more
|
||||
[guidelines on pull requests](#pull-requests).
|
||||
First, there are ways to contribute that don't involve the code at all. It helps to
|
||||
_start small_. Here's a list of things to consider:
|
||||
|
||||
It's a good idea to gauge interest in your intended work by finding the current issue
|
||||
for it or creating a new one yourself. You can use also that issue as a place to signal
|
||||
your intentions and get feedback from the users most likely to appreciate your changes.
|
||||
1. Talk about Signal with your friends and family - get them to join you in using it!
|
||||
1. Join the Beta and test out recently-released features before the general public gets access
|
||||
1. Find and comment on duplicate GitHub issues, so we can close them
|
||||
1. Determine and provide workarounds on existing GitHub issues
|
||||
1. Test Signal Desktop and find reliable, well-defined reproduction steps for existing GitHub issues
|
||||
1. For a given GitHub issue, test Signal iOS and/or Signal Android to see if their behavior matches Signal Desktop, and provide the details of your analysis.
|
||||
|
||||
Once you've spent a little bit of time planning your solution, you can go
|
||||
back to the issue and talk about your approach. We'd be happy to provide feedback. [An
|
||||
ounce of prevention, as they say!](https://www.goodreads.com/quotes/247269-an-ounce-of-prevention-is-worth-a-pound-of-cure)
|
||||
If you're ready to spend some time on a GitHub issue, please consider commenting to ask us
|
||||
if there's interest. That will help ensure we minimize wasted time.
|
||||
|
||||
### Getting into the code
|
||||
|
||||
Have you spent some good time with Signal Desktop and GitHub issues? You can go deeper
|
||||
and get into the code itself.
|
||||
|
||||
Again, it helps to start small. You don't need to create a PR to contribute!
|
||||
|
||||
1. Find the root cause of a GitHub issue, and provide your explanation in the issue, with links to specific places in the code
|
||||
1. For a particularly difficult investigation, provide your process. Talk about the changes you made, and what happened - this might be useful to others working to fix the issue.
|
||||
1. Test or review the code for others' pull requests
|
||||
1. When reporting any issue, also include the specific place in the code where it happens.
|
||||
|
||||
### Considering a Pull Request?
|
||||
|
||||
If you're getting more comfortable with the code, you can consider assembling a PR. We
|
||||
have very high standards for the code we put into Signal Desktop, so take special care
|
||||
in changing the code, adding tests, and crafting the PR summary.
|
||||
|
||||
Because this can take a lot of time, it's a good idea to gauge interest in your intended
|
||||
changes first. Find the current GitHub issue for it or create a new one yourself, then
|
||||
post about your plans. That way you'll get feedback from the users most likely to
|
||||
appreciate your changes. And we may tell you not to move forward with changes, because we
|
||||
have other plans for the issue itself or that area of the code.
|
||||
|
||||
Than, once you've spent some time planning your solution, please consider going back
|
||||
to the issue and talking about your approach. We'd be happy to provide feedback.
|
||||
|
||||
The PRs most likely to be merged are the ones that fix issues with real user impact,
|
||||
make small easily reviewed changes, and have clear and specific intentions. See below for
|
||||
more [guidelines on pull requests](#pull-requests).
|
||||
|
||||
## Developer Setup
|
||||
|
||||
@ -217,9 +249,19 @@ command line. You can run the client-side tests in an interactive session with
|
||||
|
||||
## Pull requests
|
||||
|
||||
So you wanna make a pull request? Please observe the following guidelines.
|
||||
So you wanna make a pull request?
|
||||
|
||||
- First, make sure that your `pnpm run ready` run passes - it's very similar to what our
|
||||
First, know that it's highly unlikely we'll accept visual changes, new strings, or really
|
||||
anything that changes the user experience. Please talk to us first if you're planning
|
||||
something like this! It's possible that we'll give you the okay, but very likely not.
|
||||
|
||||
The best changes fix bugs in our implementation of the existing user experience. For
|
||||
example, it's almost certain that we'll reject anything that adds a new option.
|
||||
|
||||
More guidelines:
|
||||
|
||||
- Don't forget to sign the [CLA](https://signal.org/cla/).
|
||||
- Be very sure that your `pnpm run ready` run passes - it's very similar to what our
|
||||
Continuous Integration servers do to test the app.
|
||||
- Please do not submit pull requests for translation fixes.
|
||||
- Never use plain strings right in the source code - pull them from `messages.json`!
|
||||
@ -230,7 +272,7 @@ So you wanna make a pull request? Please observe the following guidelines.
|
||||
changes on the latest `main` branch, resolving any conflicts.
|
||||
This ensures that your changes will merge cleanly when you open your PR.
|
||||
- Be sure to add and run tests!
|
||||
- Make sure the diff between the development branch and your branch contains only the
|
||||
- Make sure the diff between the main branch and your branch contains only the
|
||||
minimal set of changes needed to implement your feature or bugfix. This will
|
||||
make it easier for the person reviewing your code to approve the changes.
|
||||
Please do not submit a PR with commented out code or unfinished features.
|
||||
|
||||
3068
DATABASE_SCHEMA.md
Normal file
3068
DATABASE_SCHEMA.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,14 +20,13 @@ Please search for any [existing issues](https://github.com/signalapp/Signal-Desk
|
||||
|
||||
Please use our community forum: https://community.signalusers.org/
|
||||
|
||||
## Contributing Code
|
||||
## Contributing to the project
|
||||
|
||||
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md)
|
||||
for setup instructions and guidelines for new contributors. Don't forget to sign the [CLA](https://signal.org/cla/).
|
||||
Please see [CONTRIBUTING.md](https://github.com/signalapp/Signal-Desktop/blob/main/CONTRIBUTING.md). There are lots of ways to contribute - many that don't involve code!
|
||||
|
||||
## Contributing Funds
|
||||
## Donate to Signal
|
||||
|
||||
You can donate to Signal development through the [Signal Technology Foundation](https://signal.org/donate), an independent 501c3 nonprofit.
|
||||
You can donate to Signal from inside Signal apps (Desktop, Android, or iOS), or via the web here: [Signal Technology Foundation](https://signal.org/donate). Signal is an independent 501c3 nonprofit.
|
||||
|
||||
## Cryptography Notice
|
||||
|
||||
|
||||
21
_locales/af-ZA/mas-description.txt
Normal file
21
_locales/af-ZA/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Signal is 'n boodskaptoepassing wat privaatheid as kernwaarde het. Dis gratis en maklik om te gebruik, met kragtige end-tot-end-enkriptering wat jou kletse en oproepe heeltemal privaat hou. Signal kan nie jou boodskappe lees of na jou oproepe luister nie, en niemand anders kan ook nie.
|
||||
|
||||
• Signal in MacOS is gekoppel aan Signal op jou foon.
|
||||
|
||||
• Stuur end-tot-end-geënkripteerde teksboodskappe, stemboodskappe, foto’s, video’s, GIF’s en lêers gratis.
|
||||
|
||||
• Bly in verbinding met groepkletse vir tot 1000 deelnemers. Beheer wie boodskappe mag plaas en bestuur groeplede met administrateurtoestemming-instellings.
|
||||
|
||||
• Bel jou vriende met klokhelder end-tot-end-geënkripteerde stem- en video-oproepe. Groepoproepe ondersteun tot 75 deelnemers.
|
||||
|
||||
• Signal is gebou vir jou privaatheid. Ons weet glad nie wie jy is of met wie jy praat nie. Ons oopbron Signal-protokol beteken dat ons nie jou boodskappe kan lees of na jou oproepe kan luister nie. En niemand anders kan ook nie. Geen agterdeure nie, geen data-insameling nie, geen kompromisse nie.
|
||||
|
||||
• Deel foto-, teks- en video-Stories wat ná 24 uur verdwyn. Privaatheidinstellings hou jou in beheer van presies wie elke Storie kan sien.
|
||||
|
||||
• Signal is onafhanklik en niewinsgewend; dit is 'n ander soort tegnologie van 'n ander soort organisasie. As 'n 501c3-niewinsgewende organisasie word ons ondersteun deur jou skenkings, nie deur adverteerders of beleggers nie.
|
||||
|
||||
• Vir steundiens, vrae of meer inligting, besoek asb. https://support.signal.org/
|
||||
|
||||
Om na ons bronkode te kyk, besoek https://github.com/signalapp
|
||||
|
||||
Volg ons op X @signalapp en Instagram @signal_app
|
||||
1
_locales/af-ZA/mas-keywords.txt
Normal file
1
_locales/af-ZA/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,boodskap(per),oproep,stem,geënkripteer,privaat,veilig,privaatheid,groep,video,klets,stories
|
||||
1
_locales/af-ZA/mas-subtitle.txt
Normal file
1
_locales/af-ZA/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
Sê “hallo” vir privaatheid.
|
||||
1
_locales/af-ZA/mas-title.txt
Normal file
1
_locales/af-ZA/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
Signal Private Messenger
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/ar/mas-description.txt
Normal file
21
_locales/ar/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
سيجنال هو تطبيق للمراسلة قائم على مراعاة الخصوصية. هو تطبيق مجاني وسهل الاستخدام، ويُوفِّر تشفيرًا قويًا من طرف لِطرف للحفاظ على السرية التامة لمراسلاتك. لا يمكن لسيجنال قراءة رسائلك أو التنصت على مكالماتك، ولا يُمكن لأي أحد آخر القيام بذلك أيضًا.
|
||||
|
||||
• يتصل سيجنال على نظام macOS بسيجنال المُثبَّت على هاتفك.
|
||||
|
||||
• أرسِل رسائل نصيّة ورسائل صوتية وصور وفيديوهات وصورة متحركة وملفات مُشفَّرة من طرف لطرف بالمجان.
|
||||
|
||||
• ابقَ على اتصال مع دردشات جماعية تجمع أكثر من 1000 شخص. تحكَّم في من يُمكنه النشر والقيام بإدارة أعضاء المجموعة باستخدام إعدادات أذونات المُشرِف.
|
||||
|
||||
• أجرِ مُكالمات صوتية ومكالمات فيديو مُشفَّرة بجودة عالية مع أصدقائك. تدعم المكالمات الجماعية ما يصل إلى 75 شخصًا.
|
||||
|
||||
صُمِّمَ سيجنال للحفاظ على خصوصيتك. لا نعرف شيئًا عنك أو مع من تتحدث. بروتوكول سيجنال الخاص بنا ذو المصدر المفتوح يعني أنه لا يُمكننا قراءة رسائلك أو الاستماع إلى مكالماتك. ولا يُمكن لأي شخص آخر القيام بذلك. لا أبواب خلفية ولا عملية جمع بيانات ولا مساومات.
|
||||
|
||||
• شارِك الصور والرسائل وقِصص الفيديو التي تختفي بعد 24 ساعة. تُتيح لك إعدادات الخصوصية فرصة البقاء مسؤولًا عن من يُمكنه رؤية كل قصة.
|
||||
|
||||
• تطبيق سيجنال مُستقل ولا يهدف إلى الربح؛ نوع مختلف من التكنولوجيا من نوع مختلف من المنظمات. وبصفتنا مؤسسة غير ربحية، فإننا نستمد دعمنا من التبرُّعات وليس من الإعلانات ولا المُستثمرين.
|
||||
|
||||
• للدعم أو لطرح الأسئلة أو للمزيد من المعلومات، يُرجى زيارة https://support.signal.org/
|
||||
|
||||
لإلقاء نظرة على كود المصدر الخاص بنا، قُم بزيارة https://github.com/signalapp
|
||||
|
||||
تابعنا على X على العنوان @signalapp وعلى انستغرام على العنوان @signal_app
|
||||
1
_locales/ar/mas-keywords.txt
Normal file
1
_locales/ar/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
سيجنال،رسالة،تطبيق،دردشة،مكالمة،صوت،مشفر،خاص،آمن،خصوصية،مجموعة،فيديو،دردشة،قصص
|
||||
1
_locales/ar/mas-subtitle.txt
Normal file
1
_locales/ar/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
مرحبًا بكم في عالم الخصوصية.
|
||||
1
_locales/ar/mas-title.txt
Normal file
1
_locales/ar/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
سيجنال - تطبيق مراسلة يحترم الخصوصية
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/az-AZ/mas-description.txt
Normal file
21
_locales/az-AZ/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Signal məxfiliyə əsaslanan bir mesajlaşma tətbiqidir. Çatlar və zənglərinizin məxfiliyini tamamilə qoruyan güclü tam şifrələmə xüsusiyyəti ilə ondan istifadə pulsuz və asandır. Signal mesajlarınızı oxuya və ya zənglərinizi dinləyə bilmir, digərlərinə də bunu etməyə icazə vermir.
|
||||
|
||||
• MacOS ilə işləyən Signal telefonunuzdakı Signal tətbiqi ilə əlaqələndirilir.
|
||||
|
||||
• Tam şifrələnmiş mətnləri, səsli mesajları, fotoları, videoları, GIF və faylları pulsuz göndərin.
|
||||
|
||||
• 1000 nəfərə qədər iştirakçını dəstəkləyən qrup çatlarına qoşulun. Admin icazəsi parametrləri ilə kimin qrup üzvləri ilə yazı paylaşacağına və onları idarə edəcəyinə nəzarət edin.
|
||||
|
||||
• Dostlarınıza tam şifrələnmiş yüksək səs keyfiyyətinə malik audio və video zənglər edin. Qrup zəngləri 75 nəfərə qədər insanın qoşulmasını dəstəkləyir.
|
||||
|
||||
• Signal məxfiliyinizi qorumaq üçün yaradılıb. Siz və ya söhbət etdiyiniz şəxslər haqqında heç nə bilmirik. Açıq mənbəli Signal Protokolumuz mesajlarınızı oxuya və zənglərinizi dinləyə bilməyəcəyimizi nəzərdə tutur. Bunu nə biz, nə də başqası edə bilməz. Arxa qapılar, verilənlərin toplanması və kompromislər yoxdur.
|
||||
|
||||
• 24 saat sonra yox olacaq foto, mətn və video Hekayələri paylaşın. Məxfilik parametrləri sayəsində hər bir Hekayəni kimin görə biləcəyini dəqiqliklə seçirsiniz.
|
||||
|
||||
• Signal müstəqildir və mənfəət məqsədi güdmür, o, fərqli bir təşkilatın fərq yaradan bir texnologiyasıdır. Bir 501c3 qeyri-kommersiya təşkilatı kimi biz, reklamçı və investorların deyil, sizin ianələrinizlə dəstəklənirik.
|
||||
|
||||
• Dəstək, sual və əlavə məlumat üçün https://support.signal.org/ keçidinə daxil olun
|
||||
|
||||
Mənbə kodumuzu yoxlamaq üçün https://github.com/signalapp keçidinə daxil olun
|
||||
|
||||
Bizi X-də @signalapp, Instagram-da isə @signal_app istifadəçi profilləri ilə izləyin
|
||||
1
_locales/az-AZ/mas-keywords.txt
Normal file
1
_locales/az-AZ/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,mesaj,messenger,zəng,səsli,şifrələnmiş,şəxsi,təhlükəsiz,məxfilik,qrup,video,çat,hekayələr
|
||||
1
_locales/az-AZ/mas-subtitle.txt
Normal file
1
_locales/az-AZ/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
Məxfiliyə "Salam" verin.
|
||||
1
_locales/az-AZ/mas-title.txt
Normal file
1
_locales/az-AZ/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
Signal Gizli Mesajlaşma
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/bg-BG/mas-description.txt
Normal file
21
_locales/bg-BG/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Signal е приложение за съобщения, изградено около поверителността. То е безплатно и лесно за използване, със силно криптиране от край до край, което поддържа вашите чатове и обаждания напълно поверителни. Signal не може да чете вашите съобщения или да слуша вашите обаждания, а и никой друг не може.
|
||||
|
||||
• Signal на MacOS се свързва със Signal на телефона ви.
|
||||
|
||||
• Изпращайте криптирани от край до край текстови и гласови съобщения, снимки, видеоклипове, GIF-ове и файлове безплатно.
|
||||
|
||||
• Поддържайте връзка с групови чатове до 1000 души. Контролирайте кой може да публикува и управлява членовете на групата с настройките за администраторски разрешения.
|
||||
|
||||
• Обаждайте се на приятелите си с кристално чисти криптирани от край до край гласови и видео обаждания. Поддържат се групови разговори до 75 души.
|
||||
|
||||
• Signal е създаден за вашата поверителност. Не знаем нищо за вас или хората, с които говорите. Нашият Signal протокол с отворен код означава, че не можем да четем вашите съобщения или да слушаме вашите обаждания. И никой друг не може. Без задни вратички, без събиране на данни, без компромиси.
|
||||
|
||||
• Споделяйте снимки, текст и видео истории, които изчезват след 24 часа. Настройките за поверителност ви позволяват да контролирате точно кой може да вижда всяка история.
|
||||
|
||||
• Signal е независим и не е с цел печалба; различен вид технология от различен вид организация. Като организация с нестопанска цел, ние се издържаме от вашите дарения, а не от рекламодатели или инвеститори.
|
||||
|
||||
• За поддръжка, въпроси или повече информация, моля посетете https://support.signal.org/
|
||||
|
||||
За да разгледате нашия изходен код, посетете https://github.com/signalapp
|
||||
|
||||
Последвайте ни в X @signalapp и Instagram @signal_app
|
||||
1
_locales/bg-BG/mas-keywords.txt
Normal file
1
_locales/bg-BG/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,съобщения,обаждане,глас,криптиран,поверителен,сигурно,поверителност,група,видео,чат,истории
|
||||
1
_locales/bg-BG/mas-subtitle.txt
Normal file
1
_locales/bg-BG/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
Поверителността преди всичко.
|
||||
1
_locales/bg-BG/mas-title.txt
Normal file
1
_locales/bg-BG/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
Signal - Private Messenger
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/bn-BD/mas-description.txt
Normal file
21
_locales/bn-BD/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Signal একটি মেসেজিং অ্যাপ যার মূল ভিত্তি হলো গোপনীয়তা। এটি ফ্রি-তে ব্যবহার করা যায় এবং ব্যবহার করা সহজ, এতে রয়েছে শক্তিশালী এন্ড-টু-এন্ড এনক্রিপশন ব্যবস্থা যা আপনার চ্যাট ও কল সম্পূর্ণ গোপন রাখে। Signal আপনার মেসেজ পড়তে বা আপনার কল শুনতে পারে না, এবং অন্য কেউই তা পারে না।
|
||||
|
||||
• MacOS-এর Signal আপনার ফোনের Signal-এর সাথে সংযুক্ত হয়।
|
||||
|
||||
• ফ্রি-তে এন্ড-টু-এন্ড এনক্রিপ্ট করা টেক্সট, ভয়েস মেসেজ, ছবি, ভিডিও, GIF ও ফাইল পাঠান।
|
||||
|
||||
• 1,000 জন পর্যন্ত ব্যক্তি নিয়ে সংগঠিত গ্রুপ চ্যাটের মাধ্যমে সংযুক্ত থাকুন সবার সাথে। অ্যাডমিন অনুমতির সেটিংস সহ গ্রুপ সদস্যদের মধ্যে কে কে পোস্ট করতে পারবেন এবং কে কে নিয়ন্ত্রণ করতে পারবেন তা নিয়ন্ত্রণ করুন।
|
||||
|
||||
• আপনার বন্ধুদের সাথে এন্ড-টু-এন্ড এনক্রিপ্ট করা ভয়েস ও ভিডিও কলের মাধ্যমে কল করুন। গ্ৰুপ কলে একসাথে সর্বোচ্চ 75 জন যোগ দিতে পারে।
|
||||
|
||||
• Signal আপনার গোপনীয়তা রক্ষা করার জন্য তৈরি করা হয়েছে। আমরা আপনার সম্পর্কে এবং আপনি কার সাথে কথা বলছেন তার সম্পর্কে কিছুই জানি না। আমাদের ওপেন সোর্স Signal Protocol-এর অর্থ হলো আমরা আপনার ম্যাসেজ পড়তে বা আপনার কল থেকে কথা শুনতে পারি না। এটি অন্য আর কেউও পারে না। নেই কোনো অসৎ উদ্দেশ্য, নেই কোনো তথ্য সংগ্রহের চর্চা, নেই কোনো আপোষ।
|
||||
|
||||
• ছবি, টেক্সট ও ভিডিও স্টোরি শেয়ার করা যায়, যা 24 ঘন্টা পরে অদৃশ্য হয়ে যায়। গোপনীয়তা সেটিংস আপনাকে প্রত্যেকটি স্টোরি কে দেখতে পাবেন তা নিয়ন্ত্রণ করার সুযোগ দেয়।
|
||||
|
||||
• Signal একটি স্বাধীন এবং অলাভজনক উদ্যোগ; একটি ভিন্নধর্মী প্রতিষ্ঠানের প্রচেষ্টায় তৈরি একটি ভিন্নধর্মী প্রযুক্তি। একটি 501c3 অলাভজনক প্রতিষ্ঠান হিসাবে আমরা আপনার দেওয়া ডোনেশনের সমর্থনে পরিচালিত, কোনো বিজ্ঞাপনদাতা বা বিনিয়োগকারীর দ্বারা সমর্থিত নয়।
|
||||
|
||||
• এ সংক্রান্ত সহায়তা, প্রশ্ন বা আরো তথ্যের জন্য অনুগ্রহ করে ভিজিট করুন: https://support.signal.org/
|
||||
|
||||
আমাদের সোর্স কোড চেক করতে, ভিজিট করুন: https://github.com/signalapp
|
||||
|
||||
X-এ @signalapp পেজে এবং Instagram-এ @signal_app পেজে আমাদের ফলো করুন
|
||||
1
_locales/bn-BD/mas-keywords.txt
Normal file
1
_locales/bn-BD/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,মেসেজ,মেসেঞ্জার,কল,ভয়েস,এনক্রিপ্ট করা,ব্যক্তিগত,সুরক্ষিত,গোপনীয়তা,গ্ৰুপ,ভিডিও,চ্যাট,স্টোরি
|
||||
1
_locales/bn-BD/mas-subtitle.txt
Normal file
1
_locales/bn-BD/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
গোপনীয়তাকে “হ্যালো” বলুন।
|
||||
1
_locales/bn-BD/mas-title.txt
Normal file
1
_locales/bn-BD/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
Signal - প্রাইভেট মেসেঞ্জার
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/bs-BA/mas-description.txt
Normal file
21
_locales/bs-BA/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
Signal je aplikacija za razmjenu poruka čiji je ključ privatnost. Besplatna je i jednostavna za korištenje, sa snažnim sveobuhvatnim šifriranjem koje čuva vaše razgovore i pozive potpuno privatnima. Ne možemo čitati vaše poruke niti slušati vaše pozive, a ne može ni niko drugi.
|
||||
|
||||
• Signal na MacOS-u se povezuje sa Signalom na vašem telefonu.
|
||||
|
||||
• Besplatno šaljite tekstualne poruke, glasovne poruke, fotografije, videozapise, GIF-ove i datoteke, sve sveobuhvatno šifrirano.
|
||||
|
||||
• Ostanite povezani uz grupne razgovore i do 1000 ljudi. Kontrolirajte ko može objavljivati i upravljati članovima grupe pomoću postavki administratorskih dozvola.
|
||||
|
||||
• Pozovite svoje prijatelje kristalno čistim glasovnim i video pozivima, sve sveobhvatno šifrirano. Grupni pozivi podržavaju do 75 osoba.
|
||||
|
||||
• Signal je kreiran za vašu privatnost. Ne znamo ništa o vama niti s kim razgovarate. Naš Signal protokol otvorenog koda podrazumijeva da ne možemo čitati vaše poruke niti slušati vaše pozive. A to ne može ni niko drugi. Nema skrivenih motiva, prikupljanja podataka niti kompromisa.
|
||||
|
||||
• Dijelite fotografije, tekstualne i video priče koje nestaju nakon 24 sata. S postavkama privatnosti kontrolirate ko tačno može vidjeti svaku vašu priču.
|
||||
|
||||
• Signal je nezavisan i neprofitan; drugačija vrsta tehnologije od drugačije vrste organizacije. Kao neprofitna organizacija, podržavaju nas vaše donacije, a ne oglašivači ili investitori.
|
||||
|
||||
• Za podršku, pitanja ili više informacija posjetite https://support.signal.org/
|
||||
|
||||
Da provjerite naš izvorni kȏd, posjetite https://github.com/signalapp
|
||||
|
||||
Pratite nas na X @signalapp i Instagramu @signal_app
|
||||
1
_locales/bs-BA/mas-keywords.txt
Normal file
1
_locales/bs-BA/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,poruka,messenger,poziv,glas,šifrirano,privatno,sigurno,privatnost,grupa,video,chat,priče
|
||||
1
_locales/bs-BA/mas-subtitle.txt
Normal file
1
_locales/bs-BA/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
Uživajte u svojoj privatnosti.
|
||||
1
_locales/bs-BA/mas-title.txt
Normal file
1
_locales/bs-BA/mas-title.txt
Normal file
@ -0,0 +1 @@
|
||||
Signal - Privatna aplikacija za razmjenu poruka
|
||||
File diff suppressed because it is too large
Load Diff
21
_locales/ca/mas-description.txt
Normal file
21
_locales/ca/mas-description.txt
Normal file
@ -0,0 +1,21 @@
|
||||
• Signal és una aplicació de missatgeria centrada en la privacitat. És gratuïta i fàcil d'utilitzar, amb un fort xifratge d'extrem a extrem que manté els teus xats i trucades completament privats. Ni Signal ni ningú més pot llegir els teus missatges o escoltar les teves trucades.
|
||||
|
||||
• El Signal de macOS s'enllaça amb el Signal del teu telèfon.
|
||||
|
||||
• Envia de manera gratuïta missatges, notes de veu, fotos, vídeos, GIFs i arxius xifrats d'extrem a extrem.
|
||||
|
||||
• No perdis mai el contacte amb xats grupals de fins a 1.000 persones. Controla qui pot publicar i gestionar els membres del grup amb la configuració de permisos d'administrador.
|
||||
|
||||
• Comunica't amb els teus amics amb trucades de veu i videotrucades totalment xifrades i nítides. S'admeten trucades en grup per a un màxim de 75 persones.
|
||||
|
||||
• Signal està dissenyada per a la teva privacitat. No sabem res de tu ni amb qui estàs parlant. El nostre protocol de codi obert de Signal significa que no podem llegir els teus missatges ni escoltar les teves trucades. Ni tampoc pot fer-ho ningú més. Sense portes posteriors, sense recollida de dades, sense compromisos.
|
||||
|
||||
• Comparteix Històries d'imatge, text i vídeo que desapareixeran automàticament al cap de 24 hores. La configuració de privacitat et manté a càrrec de qui pot veure exactament cada història.
|
||||
|
||||
• Signal és independent i sense ànim de lucre; un tipus de tecnologia diferent d'un tipus d'organització diferent. Com a entitat sense ànim de lucre 501c3, comptem amb el suport de les teves donacions, no d'anunciants o inversors.
|
||||
|
||||
• Si necessites assistència, tens alguna pregunta o vols saber-ne més, visita https://support.signal.org/
|
||||
|
||||
Per consultar el nostre codi font, visita https://github.com/signalapp
|
||||
|
||||
Segueix-nos a X @signalapp i Instagram @signal_app
|
||||
1
_locales/ca/mas-keywords.txt
Normal file
1
_locales/ca/mas-keywords.txt
Normal file
@ -0,0 +1 @@
|
||||
signal,missatge,messenger,trucada,veu,xifrat,privat,segur,privacitat,grup,vídeo,xat,històries
|
||||
1
_locales/ca/mas-subtitle.txt
Normal file
1
_locales/ca/mas-subtitle.txt
Normal file
@ -0,0 +1 @@
|
||||
Digues "hola" a la privacitat.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user