mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-24 00:29:12 +01:00
Switch from eslint to oxlint
This commit is contained in:
@@ -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
@@ -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,
|
||||
};
|
||||
@@ -41,14 +41,12 @@ jobs:
|
||||
# path: ${{ env.SCCACHE_PATH }}
|
||||
# key: sccache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**') }}
|
||||
|
||||
- name: Restore cached .eslintcache and tsconfig.tsbuildinfo
|
||||
- name: Restore cached 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') }}
|
||||
path: tsconfig.tsbuildinfo
|
||||
key: lint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'patches/**', 'tsconfig.json') }}
|
||||
|
||||
- name: Install Desktop node_modules
|
||||
run: pnpm install
|
||||
@@ -71,13 +69,11 @@ jobs:
|
||||
|
||||
- run: git diff --exit-code
|
||||
|
||||
- name: Update cached .eslintcache and tsconfig.tsbuildinfo
|
||||
- name: Update cached tsconfig.tsbuildinfo
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
path: tsconfig.tsbuildinfo
|
||||
key: ${{ steps.cache-lint.outputs.cache-primary-key }}
|
||||
|
||||
macos:
|
||||
|
||||
@@ -17,7 +17,6 @@ release/
|
||||
|
||||
/sql/
|
||||
/start.sh
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
tsconfig.tsbuildinfo
|
||||
.smartling-source.sh
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
legacy-peer-deps=true
|
||||
public-hoist-pattern[]=*eslint-*
|
||||
minimum-release-age=14400
|
||||
minimum-release-age=14400 # 10 days
|
||||
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
|
||||
minimum-release-age-exclude[]=oxlint
|
||||
minimum-release-age-exclude[]=oxlint-tsgolint
|
||||
minimum-release-age-exclude[]=@oxlint/*
|
||||
minimum-release-age-exclude[]=@oxlint-tsgolint/*
|
||||
minimum-release-age-exclude[]=eslint
|
||||
minimum-release-age-exclude[]=@eslint/*
|
||||
minimum-release-age-exclude[]=@typescript-eslint/*
|
||||
|
||||
@@ -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 = {
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
+11
-30
@@ -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',
|
||||
@@ -225,14 +243,7 @@ const STD_PACKAGES = new Set([
|
||||
'emoji-datasource-apple',
|
||||
'emoji-regex',
|
||||
'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',
|
||||
@@ -279,24 +290,56 @@ const STD_PACKAGES = new Set([
|
||||
'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,6 +348,10 @@ module.exports = {
|
||||
main: [domUses, preloadUses],
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @param {string} source
|
||||
*/
|
||||
function trackLocalDep(node, source) {
|
||||
if (!source.endsWith('.js')) {
|
||||
return;
|
||||
@@ -314,7 +361,8 @@ module.exports = {
|
||||
if (match == null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Missing file suffix in ${source} import`,
|
||||
messageId: 'missingFileSuffix',
|
||||
data: { source },
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -333,13 +381,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 +408,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 +453,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,33 +479,50 @@ 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);
|
||||
}
|
||||
|
||||
@@ -457,19 +537,21 @@ module.exports = {
|
||||
if (match == null) {
|
||||
context.report({
|
||||
node: node,
|
||||
message:
|
||||
'Missing file suffix. Has to be one of: node/preload/main/std',
|
||||
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 +582,8 @@ module.exports = {
|
||||
if (fileSuffix !== expectedSuffix) {
|
||||
context.report({
|
||||
node,
|
||||
message: `Invalid suffix ${fileSuffix}, expected: ${expectedSuffix}`,
|
||||
messageId: 'wrongFileSuffix',
|
||||
data: { fileSuffix, expectedSuffix },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -508,7 +591,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 +613,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 +643,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);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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}.js';
|
||||
${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.js'`,
|
||||
},
|
||||
],
|
||||
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}.js'`,
|
||||
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.js';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'invalidImportForSuffix',
|
||||
data: { expectedSuffix: 'main' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'main in preload',
|
||||
filename: 'a.preload.ts',
|
||||
code: `
|
||||
import { ipcRenderer } from 'electron';
|
||||
import './b.main.js';
|
||||
`,
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -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 {};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"include": ["./client/**", "./server/**"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -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)];
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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 }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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)];
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import * as mocha from 'mocha';
|
||||
import { RuleTester } from '@typescript-eslint/rule-tester';
|
||||
|
||||
RuleTester.afterAll = mocha.after;
|
||||
+1816
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ libtextsecure/test/test.js
|
||||
stylesheets/*.css
|
||||
test/test.js
|
||||
ts/**/*.js
|
||||
!ts/**/.eslintrc.js
|
||||
ts/protobuf/*.d.ts
|
||||
ts/protobuf/*.js
|
||||
stylesheets/manifest.css
|
||||
|
||||
+20
-12
@@ -11,7 +11,7 @@ const EXTERNALS = new Set(builtinModules);
|
||||
EXTERNALS.delete('buffer');
|
||||
EXTERNALS.delete('url');
|
||||
|
||||
const config: StorybookConfig = {
|
||||
const storybookConfig: StorybookConfig = {
|
||||
typescript: {
|
||||
reactDocgen: false,
|
||||
},
|
||||
@@ -53,16 +53,19 @@ const config: StorybookConfig = {
|
||||
},
|
||||
],
|
||||
|
||||
webpackFinal(config) {
|
||||
config.cache = {
|
||||
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' },
|
||||
@@ -71,14 +74,16 @@ const config: StorybookConfig = {
|
||||
],
|
||||
});
|
||||
|
||||
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: [
|
||||
{
|
||||
@@ -95,9 +100,11 @@ const config: StorybookConfig = {
|
||||
],
|
||||
});
|
||||
|
||||
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') ||
|
||||
EXTERNALS.has(request)
|
||||
@@ -108,16 +115,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;
|
||||
|
||||
@@ -11,7 +11,8 @@ 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';
|
||||
@@ -100,7 +101,7 @@ const mockStore: Store<StateType> = createStore(
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line
|
||||
// oxlint-disable-next-line
|
||||
const noop = () => {};
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
@@ -138,6 +139,7 @@ window.SignalContext = {
|
||||
platform: '',
|
||||
release: '',
|
||||
},
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
config: {} as any,
|
||||
|
||||
getHourCyclePreference: () => HourCyclePreference.UnknownPreference,
|
||||
@@ -241,7 +243,7 @@ function withMockStoreProvider(Story, context) {
|
||||
function withScrollLockProvider(Story, context) {
|
||||
return (
|
||||
<ScrollerLockContext.Provider
|
||||
value={createScrollerLock('MockStories', () => {})}
|
||||
value={createScrollerLock('MockStories', () => null)}
|
||||
>
|
||||
<Story {...context} />
|
||||
</ScrollerLockContext.Provider>
|
||||
@@ -265,12 +267,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);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -418,7 +418,7 @@ function deleteOrphanedAttachments({
|
||||
let attachments: ReadonlyArray<string>;
|
||||
let downloads: ReadonlyArray<string>;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
({ attachments, downloads, cursor } = await sql.sqlRead(
|
||||
'getKnownMessageAttachments',
|
||||
cursor
|
||||
@@ -443,7 +443,7 @@ function deleteOrphanedAttachments({
|
||||
|
||||
// Let other SQL calls come through. There are hundreds of thousands of
|
||||
// messages in the database and it might take time to go through them all.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await sleep(INTERACTIVITY_DELAY);
|
||||
} while (cursor !== undefined && !cursor.done);
|
||||
} finally {
|
||||
|
||||
@@ -240,7 +240,7 @@ export const deleteAllBadges = async ({
|
||||
let filesDeleted = 0;
|
||||
for (const file of await getAllBadgeImageFiles(userDataPath)) {
|
||||
if (!pathsToKeep.has(file)) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await deleteFromDisk(file);
|
||||
filesDeleted += 1;
|
||||
}
|
||||
@@ -261,7 +261,7 @@ export const deleteAllMegaphones = async ({
|
||||
let filesDeleted = 0;
|
||||
for (const file of await getAllMegaphoneImageFiles(userDataPath)) {
|
||||
if (!pathsToKeep.has(file)) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await deleteFromDisk(file);
|
||||
filesDeleted += 1;
|
||||
}
|
||||
|
||||
+2
-1
@@ -53,7 +53,7 @@ process.env.NODE_CONFIG_DIR = join(getAppRootDir(), 'config');
|
||||
// Note: we use `require()` because esbuild moves the imports to the top of
|
||||
// the module regardless of their actual placement in the file.
|
||||
// See: https://github.com/evanw/esbuild/issues/2011
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
// oxlint-disable-next-line typescript/no-var-requires
|
||||
const config: IConfig = require('config');
|
||||
|
||||
if (getEnvironment() !== Environment.PackagedApp) {
|
||||
@@ -73,6 +73,7 @@ if (getEnvironment() !== Environment.PackagedApp) {
|
||||
'SUPPRESS_NO_CONFIG_WARNING',
|
||||
'SIGNAL_ENABLE_HTTP',
|
||||
].forEach(s => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(`${s} ${config.util.getEnv(s)}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ let copyErrorAndQuitText = 'Copy error and quit';
|
||||
|
||||
function handleError(prefix: string, error: Error): void {
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(`${prefix}:`, formattedError);
|
||||
log.error(`${prefix}:`, formattedError);
|
||||
|
||||
|
||||
+19
-16
@@ -384,7 +384,7 @@ async function getBackgroundColor(
|
||||
|
||||
async function getLocaleOverrideSetting(): Promise<string | null> {
|
||||
const value = ephemeralConfig.get('localeOverride');
|
||||
// eslint-disable-next-line eqeqeq -- Checking for null explicitly
|
||||
// oxlint-disable-next-line eqeqeq -- Checking for null explicitly
|
||||
if (typeof value === 'string' || value === null) {
|
||||
log.info('got fast localeOverride setting', value);
|
||||
return value;
|
||||
@@ -818,13 +818,13 @@ async function createWindow() {
|
||||
maximized: mainWindow.isMaximized(),
|
||||
autoHideMenuBar: mainWindow.autoHideMenuBar,
|
||||
fullscreen: mainWindow.isFullScreen(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
width: size[0]!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
height: size[1]!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
x: position[0]!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
y: position[1]!,
|
||||
};
|
||||
|
||||
@@ -1555,7 +1555,7 @@ async function showCallDiagnosticWindow() {
|
||||
|
||||
let permissionsPopupWindow: BrowserWindow | undefined;
|
||||
function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
// oxlint-disable-next-line no-async-promise-executor
|
||||
return new Promise<void>(async (resolveFn, reject) => {
|
||||
if (permissionsPopupWindow) {
|
||||
permissionsPopupWindow.show();
|
||||
@@ -1569,9 +1569,9 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) {
|
||||
|
||||
const size = mainWindow.getSize();
|
||||
const options = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
width: Math.min(400, size[0]!),
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
height: Math.min(150, size[1]!),
|
||||
resizable: false,
|
||||
title: getResolvedMessagesLocale().i18n('icu:allowAccess'),
|
||||
@@ -1683,6 +1683,7 @@ function getSQLKey(): string {
|
||||
typeof previousBackend === 'string' &&
|
||||
previousBackend !== safeStorageBackend
|
||||
) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(
|
||||
`Detected change in safeStorage backend, can't decrypt DB key (previous: ${previousBackend}, current: ${safeStorageBackend})`
|
||||
);
|
||||
@@ -2271,7 +2272,7 @@ app.on('ready', async () => {
|
||||
});
|
||||
|
||||
drop(
|
||||
// eslint-disable-next-line more/no-then
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then, signal-desktop/no-then
|
||||
Promise.race([sqlInitPromise, timeout]).then(async maybeTimeout => {
|
||||
if (maybeTimeout !== 'timeout') {
|
||||
return;
|
||||
@@ -2469,6 +2470,7 @@ async function maybeRequestCloseConfirmation(): Promise<boolean> {
|
||||
'maybeRequestCloseConfirmation: Checking to see if close confirmation is needed'
|
||||
);
|
||||
const request = new Promise<boolean>(resolveFn => {
|
||||
// oxlint-disable-next-line prefer-const
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
if (!mainWindow) {
|
||||
@@ -2520,6 +2522,7 @@ async function requestShutdown() {
|
||||
|
||||
log.info('requestShutdown: Requesting close of mainWindow...');
|
||||
const request = new Promise<void>(resolveFn => {
|
||||
// oxlint-disable-next-line prefer-const
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
if (!mainWindow) {
|
||||
@@ -2937,31 +2940,31 @@ ipc.on('get-config', async event => {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = parsed.data;
|
||||
});
|
||||
|
||||
// Ingested in preload.js via a sendSync call
|
||||
ipc.on('locale-data', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = getResolvedMessagesLocale().messages;
|
||||
});
|
||||
|
||||
// Ingested in preload.js via a sendSync call
|
||||
ipc.on('locale-display-names', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = getResolvedMessagesLocale().localeDisplayNames;
|
||||
});
|
||||
|
||||
// Ingested in preload.js via a sendSync call
|
||||
ipc.on('country-display-names', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = getResolvedMessagesLocale().countryDisplayNames;
|
||||
});
|
||||
|
||||
// TODO DESKTOP-5241
|
||||
ipc.on('OS.getClassName', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = OS.getClassName();
|
||||
});
|
||||
|
||||
@@ -2990,7 +2993,7 @@ ipc.handle('DebugLogs.upload', async (_event, content: string) => {
|
||||
});
|
||||
|
||||
ipc.on('get-user-data-path', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = app.getPath('userData');
|
||||
});
|
||||
|
||||
@@ -3442,7 +3445,7 @@ async function showStickerCreatorWindow() {
|
||||
|
||||
if (isTestEnvironment(getEnvironment())) {
|
||||
ipc.on('ci:test-electron:getArgv', event => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
event.returnValue = process.argv;
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ if (userData !== undefined) {
|
||||
try {
|
||||
mkdirSync(userData, { recursive: true });
|
||||
} catch (error) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error('Failed to create userData', Errors.toLogFormat(error));
|
||||
}
|
||||
|
||||
@@ -34,6 +35,7 @@ if (userData !== undefined) {
|
||||
}
|
||||
|
||||
// Use console.log because logger isn't fully initialized yet
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log(`userData: ${app.getPath('userData')}`);
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
|
||||
@@ -174,7 +174,7 @@ async function lintMessages() {
|
||||
const key = topProp.key.value;
|
||||
|
||||
if (process.argv.includes('--test')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const test = tests[key]!;
|
||||
const actualErrors = reports.map(report => report.id);
|
||||
deepEqual(actualErrors, test.expectErrors);
|
||||
@@ -197,13 +197,13 @@ async function lintMessages() {
|
||||
loc = `:${line}:${column + report.locationOffset}`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(
|
||||
chalk`{bold.cyan ${relativePath}${loc}} ${report.message} {magenta ({underline ${report.id}})}`
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(chalk` {dim in ${key} is "}{red ${icuMessage}}{dim "}`);
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error();
|
||||
|
||||
failed = true;
|
||||
@@ -215,8 +215,9 @@ async function lintMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
lintMessages().catch(error => {
|
||||
// eslint-disable-next-line no-console
|
||||
// oxlint-disable-next-line no-console
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export default rule('wrapEmoji', context => {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const child = element.children[0]!;
|
||||
if (!isLiteralElement(child)) {
|
||||
// non-literal
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"danger": "12.3.4",
|
||||
"endanger": "7.0.4",
|
||||
"semver": "7.7.4",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
+5
-1
@@ -28,4 +28,8 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { File, Rule } from 'endanger';
|
||||
import type { File } from 'endanger';
|
||||
import { Rule } from 'endanger';
|
||||
import semver from 'semver';
|
||||
|
||||
function isPinnedVersion(spec: string): boolean {
|
||||
@@ -14,12 +15,13 @@ function isPinnedVersion(spec: string): boolean {
|
||||
} else {
|
||||
version = spec;
|
||||
}
|
||||
return semver.valid(version) !== null;
|
||||
return semver.valid(version) != null;
|
||||
}
|
||||
|
||||
async function getLineContaining(file: File, text: string) {
|
||||
let lines = await file.lines();
|
||||
for (let line of lines) {
|
||||
const lines = await file.lines();
|
||||
for (const line of lines) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
if (await line.contains(text)) {
|
||||
return line;
|
||||
}
|
||||
@@ -27,14 +29,15 @@ async function getLineContaining(file: File, text: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dependencyTypes = [
|
||||
const dependencyTypes = [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
'peerDependencies',
|
||||
'optionalDependencies',
|
||||
];
|
||||
|
||||
export default function packageJsonVersionsShouldBePinned() {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export default function packageJsonVersionsShouldBePinned(): Rule<any, any> {
|
||||
return new Rule({
|
||||
match: {
|
||||
files: ['**/package.json', '!**/node_modules/**'],
|
||||
@@ -47,17 +50,19 @@ export default function packageJsonVersionsShouldBePinned() {
|
||||
`,
|
||||
},
|
||||
async run({ files, context }) {
|
||||
for (let file of files.modifiedOrCreated) {
|
||||
let pkg = await file.json();
|
||||
for (let dependencyType of dependencyTypes) {
|
||||
let deps = pkg[dependencyType];
|
||||
for (const file of files.modifiedOrCreated) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const pkg = await file.json();
|
||||
for (const dependencyType of dependencyTypes) {
|
||||
const deps = pkg[dependencyType];
|
||||
if (deps == null) {
|
||||
continue;
|
||||
}
|
||||
for (let depName of Object.keys(deps)) {
|
||||
let depVersion = deps[depName];
|
||||
for (const depName of Object.keys(deps)) {
|
||||
const depVersion = deps[depName];
|
||||
if (!isPinnedVersion(depVersion)) {
|
||||
let line = await getLineContaining(
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const line = await getLineContaining(
|
||||
file,
|
||||
`"${depName}": "${depVersion}"`
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Line, Rule } from 'endanger';
|
||||
import { Rule } from 'endanger';
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
@@ -10,9 +10,10 @@ function assert(condition: boolean, message: string): asserts condition {
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is object {
|
||||
return typeof value === 'object' && value !== null;
|
||||
return typeof value === 'object' && value != null;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
function has<T extends object, const K extends T[any]>(
|
||||
value: T,
|
||||
key: K
|
||||
@@ -20,7 +21,8 @@ function has<T extends object, const K extends T[any]>(
|
||||
return Object.hasOwn(value, key);
|
||||
}
|
||||
|
||||
export default function pnpmLockDepsShouldHaveIntegrity() {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
export default function pnpmLockDepsShouldHaveIntegrity(): Rule<any, any> {
|
||||
return new Rule({
|
||||
match: {
|
||||
files: ['pnpm-lock.yaml'],
|
||||
@@ -36,6 +38,7 @@ export default function pnpmLockDepsShouldHaveIntegrity() {
|
||||
},
|
||||
async run({ files, context }) {
|
||||
for (const file of files.modifiedOrCreated) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const contents: unknown = await file.yaml();
|
||||
|
||||
assert(
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
/* eslint-disable global-require */
|
||||
|
||||
module.exports = {
|
||||
'license-comments': require('./.eslint/rules/license-comments'),
|
||||
'type-alias-readonlydeep': require('./.eslint/rules/type-alias-readonlydeep'),
|
||||
'enforce-tw': require('./.eslint/rules/enforce-tw'),
|
||||
'enforce-array-buffer': require('./.eslint/rules/enforce-array-buffer'),
|
||||
'file-suffix': require('./.eslint/rules/file-suffix'),
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: false,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
};
|
||||
+16
-17
@@ -43,16 +43,16 @@
|
||||
"prepare-adhoc-version": "node scripts/prepare_tagged_version.js adhoc",
|
||||
"prepare-staging-build": "node scripts/prepare_staging_build.js",
|
||||
"prepare-linux-build": "node scripts/prepare_linux_build.js",
|
||||
"test": "run-s test-node test-electron test-lint-intl test-eslint",
|
||||
"test": "run-s test-node test-electron test-lint-intl test-oxlint",
|
||||
"test-electron": "node ts/scripts/test-electron.node.js",
|
||||
"test-release": "node ts/scripts/test-release.node.js",
|
||||
"test-node": "cross-env NODE_ENV=test LANG=en-us electron-mocha --timeout 10000 --file test/setup-test-node.js --recursive ts/test-node",
|
||||
"test-mock": "node ts/scripts/mocha-separator.node.js --require ts/test-mock/setup-ci.node.js -- ts/test-mock/**/*_test.node.js",
|
||||
"test-mock-docker": "mocha --require ts/test-mock/setup-ci.node.js ts/test-mock/**/*_test.docker.node.js",
|
||||
"test-eslint": "mocha .eslint/rules/**/*.test.js --ignore-leaks",
|
||||
"test-oxlint": "mocha --require .oxlint/test-setup.mjs .oxlint/rules/**/*.test.mjs",
|
||||
"test-lint-intl": "ts-node ./build/intl-linter/linter.node.ts --test",
|
||||
"eslint": "eslint --cache . --cache-strategy content --max-warnings 0",
|
||||
"lint": "run-p --aggregate-output --print-label lint-prettier lint-css check:types eslint",
|
||||
"oxlint": "oxlint",
|
||||
"lint": "run-p --aggregate-output --print-label lint-prettier lint-css check:types oxlint",
|
||||
"lint-deps": "node ts/util/lint/linter.node.js",
|
||||
"lint-license-comments": "ts-node ts/util/lint/license_comments.node.ts",
|
||||
"lint-prettier": "pprettier --check '**/*.{ts,tsx,d.ts,js,json,html,scss,md,yml,yaml}' '!node_modules/**'",
|
||||
@@ -264,11 +264,13 @@
|
||||
"@types/google-libphonenumber": "7.4.30",
|
||||
"@types/humanize-duration": "3.18.1",
|
||||
"@types/intl-tel-input": "18.1.4",
|
||||
"@types/is-glob": "4.0.4",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/json-to-ast": "2.1.4",
|
||||
"@types/linkify-it": "5.0.0",
|
||||
"@types/lodash": "4.14.106",
|
||||
"@types/memoizee": "0.4.11",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/mocha": "10.0.9",
|
||||
"@types/node": "24.12.0",
|
||||
"@types/node-fetch": "2.6.12",
|
||||
@@ -289,7 +291,9 @@
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@typescript-eslint/parser": "8.57.2",
|
||||
"@typescript-eslint/rule-tester": "8.57.2",
|
||||
"@typescript-eslint/utils": "8.57.2",
|
||||
"axe-core": "4.10.2",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-loader": "9.2.1",
|
||||
@@ -312,25 +316,22 @@
|
||||
"enhanced-resolve": "5.18.3",
|
||||
"enquirer": "2.4.1",
|
||||
"esbuild": "0.25.9",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-typescript-prettier": "5.0.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-better-tailwindcss": "3.7.2",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-local-rules": "1.3.2",
|
||||
"eslint-plugin-mocha": "10.1.0",
|
||||
"eslint-plugin-more": "1.0.5",
|
||||
"eslint-plugin-react": "7.31.10",
|
||||
"eslint": "10.1.0",
|
||||
"eslint-plugin-better-tailwindcss": "4.3.2",
|
||||
"execa": "5.1.1",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"http-server": "14.1.1",
|
||||
"is-glob": "4.0.3",
|
||||
"json-to-ast": "2.1.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"micromatch": "4.0.8",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"mocha": "10.8.2",
|
||||
"node-gyp": "11.0.0",
|
||||
"node-gyp-build": "4.8.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"oxlint": "1.57.0",
|
||||
"oxlint-tsgolint": "0.17.4",
|
||||
"p-limit": "3.1.0",
|
||||
"pe-library": "2.0.1",
|
||||
"pixelmatch": "5.3.0",
|
||||
@@ -354,7 +355,7 @@
|
||||
"stylelint-use-logical-spec": "5.0.1",
|
||||
"svgo": "3.3.2",
|
||||
"synckit": "0.11.11",
|
||||
"tailwindcss": "4.1.7",
|
||||
"tailwindcss": "4.2.1",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
@@ -366,7 +367,6 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@storybook/core>node-fetch": "2.6.7",
|
||||
"eslint-config-airbnb-typescript-prettier>eslint-plugin-prettier": "5.2.1",
|
||||
"canvas": "-",
|
||||
"jsdom": "-",
|
||||
"thenify-all>thenify": "3.3.1",
|
||||
@@ -647,7 +647,6 @@
|
||||
"!**/.{github,husky,grenrc,npmrc,nycrc,taprc,bithoundrc}",
|
||||
"!**/.testem.json",
|
||||
"!**/.babelrc*",
|
||||
"!**/.eslintrc*",
|
||||
"!**/.prettier*",
|
||||
"!**/.jscs*",
|
||||
"!**/*travis*.yml",
|
||||
|
||||
Generated
+742
-1095
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -2,14 +2,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
import { Script, constants } from 'node:vm';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
const srcPath = join(__dirname, 'preload.bundle.js');
|
||||
const cachePath = join(__dirname, 'preload.bundle.cache');
|
||||
|
||||
let cachedData: Buffer | undefined;
|
||||
let cachedData: Buffer<ArrayBuffer> | undefined;
|
||||
try {
|
||||
if (!process.env.GENERATE_PRELOAD_CACHE) {
|
||||
cachedData = readFileSync(cachePath);
|
||||
@@ -76,7 +76,9 @@ if (process.env.GENERATE_PRELOAD_CACHE) {
|
||||
}
|
||||
|
||||
if (cachedDataRejected) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('preload cache rejected');
|
||||
} else {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.log('preload cache hit');
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ const PATTERNS = [
|
||||
'tsconfig.tsbuildinfo',
|
||||
'preload.bundle.js',
|
||||
'preload.bundle.cache',
|
||||
'.eslintcache',
|
||||
];
|
||||
|
||||
async function main() {
|
||||
@@ -25,12 +24,17 @@ async function main() {
|
||||
});
|
||||
|
||||
const promises = [];
|
||||
let count = 0;
|
||||
for await (const entry of readable) {
|
||||
count += 1;
|
||||
promises.push(rm(entry, { recursive: true, force: true }));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`Deleted ${count} files`);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
|
||||
@@ -205,6 +205,7 @@ async function main() {
|
||||
]);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
main().catch(error => {
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
|
||||
@@ -167,6 +167,7 @@ async function main() {
|
||||
await fs.promises.writeFile(destinationPath, output);
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
browser: true,
|
||||
},
|
||||
|
||||
globals: {
|
||||
assert: true,
|
||||
getString: true,
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
|
||||
rules: {
|
||||
// We still get the value of this rule, it just allows for dev deps
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
|
||||
// We want to keep each test structured the same, even if its contents are tiny
|
||||
'arrow-body-style': 'off',
|
||||
|
||||
strict: 'off',
|
||||
'more/no-then': 'off',
|
||||
},
|
||||
};
|
||||
@@ -43,11 +43,11 @@ global.navigator = {};
|
||||
global.WebSocket = {};
|
||||
|
||||
// For GlobalAudioContext.tsx
|
||||
/* eslint max-classes-per-file: ["error", 2] */
|
||||
global.AudioContext = class {};
|
||||
// oxlint-disable-next-line max-classes-per-file
|
||||
global.Audio = class {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
// oxlint-disable-next-line typescript/no-empty-function
|
||||
pause() {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
// oxlint-disable-next-line typescript/no-empty-function
|
||||
addEventListener() {}
|
||||
};
|
||||
|
||||
@@ -26,7 +26,9 @@ before(async () => {
|
||||
window.testUtilities.prepareTests();
|
||||
delete window.testUtilities.prepareTests;
|
||||
|
||||
// oxlint-disable-next-line no-unused-expressions
|
||||
!(function () {
|
||||
// oxlint-disable-next-line no-undef
|
||||
class Reporter extends Mocha.reporters.HTML {
|
||||
constructor(runner, options) {
|
||||
super(runner, options);
|
||||
|
||||
@@ -122,7 +122,7 @@ export async function benchmarkConversationOpen({
|
||||
'CI not enabled; ensure this is a staging build'
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
conversationId =
|
||||
conversationId || getSelectedConversationId(window.reduxStore.getState());
|
||||
|
||||
@@ -150,12 +150,12 @@ export async function benchmarkConversationOpen({
|
||||
const durations: Array<number> = [];
|
||||
for (let i = 0; i < runCount; i += 1) {
|
||||
// Give some buffer between tests
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await sleep(BUFFER_DELAY_MS);
|
||||
|
||||
log.info(`${logId}: running open test run ${i + 1}/${runCount}`);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const duration = await timeConversationOpen(conversationId);
|
||||
|
||||
if (i >= runCountToSkip) {
|
||||
@@ -184,7 +184,7 @@ async function waitForSelector(
|
||||
return element;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await sleep(BUFFER_DELAY_MS);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
} from './types/SignalConversation.std.js';
|
||||
import { getTitleNoDefault } from './util/getTitle.preload.js';
|
||||
import * as StorageService from './services/storage.preload.js';
|
||||
import textsecureUtils from './textsecure/Helpers.std.js';
|
||||
import { cdsLookup } from './textsecure/WebAPI.preload.js';
|
||||
import type { ConversationPropsForUnreadStats } from './util/countUnreadStats.std.js';
|
||||
import { countAllConversationsUnreadStats } from './util/countUnreadStats.std.js';
|
||||
@@ -67,6 +66,7 @@ import type {
|
||||
} from './types/ServiceId.std.js';
|
||||
import { itemStorage } from './textsecure/Storage.preload.js';
|
||||
import { getSelectedConversationId } from './state/selectors/nav.std.js';
|
||||
import { unencodeNumber } from './util/unencodeNumber.std.js';
|
||||
|
||||
const { debounce, pick, uniq, without } = lodash;
|
||||
|
||||
@@ -278,7 +278,7 @@ export class ConversationController {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
conversation.cachedProps = undefined;
|
||||
|
||||
const hasAttributeChanged = (name: keyof ConversationAttributesType) => {
|
||||
@@ -634,7 +634,7 @@ export class ConversationController {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [id] = textsecureUtils.unencodeNumber(address);
|
||||
const [id] = unencodeNumber(address);
|
||||
const conv = this.get(id);
|
||||
|
||||
if (conv) {
|
||||
@@ -1130,7 +1130,7 @@ export class ConversationController {
|
||||
// Keep the newer one if it has an e164, otherwise keep existing
|
||||
if (conversation.get('e164')) {
|
||||
// Keep new one
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: conversation,
|
||||
obsolete: existing,
|
||||
@@ -1138,7 +1138,7 @@ export class ConversationController {
|
||||
byServiceId[serviceId] = conversation;
|
||||
} else {
|
||||
// Keep existing - note that this applies if neither had an e164
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: existing,
|
||||
obsolete: conversation,
|
||||
@@ -1164,7 +1164,7 @@ export class ConversationController {
|
||||
// Keep the newer one if it has additional data, otherwise keep existing
|
||||
if (conversation.get('e164') || conversation.getPni()) {
|
||||
// Keep new one
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: conversation,
|
||||
obsolete: existing,
|
||||
@@ -1172,7 +1172,7 @@ export class ConversationController {
|
||||
byServiceId[pni] = conversation;
|
||||
} else {
|
||||
// Keep existing - note that this applies if neither had an e164
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: existing,
|
||||
obsolete: conversation,
|
||||
@@ -1210,7 +1210,7 @@ export class ConversationController {
|
||||
// Keep the newer one if it has a service id, otherwise keep existing
|
||||
if (conversation.getServiceId()) {
|
||||
// Keep new one
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: conversation,
|
||||
obsolete: existing,
|
||||
@@ -1218,7 +1218,7 @@ export class ConversationController {
|
||||
byE164[e164] = conversation;
|
||||
} else {
|
||||
// Keep existing - note that this applies if neither had a service id
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: existing,
|
||||
obsolete: conversation,
|
||||
@@ -1256,14 +1256,14 @@ export class ConversationController {
|
||||
isGroupV2(conversation.attributes) &&
|
||||
!isGroupV2(existing.attributes)
|
||||
) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: conversation,
|
||||
obsolete: existing,
|
||||
});
|
||||
byGroupV2Id[groupV2Id] = conversation;
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await this.#doCombineConversations({
|
||||
current: existing,
|
||||
obsolete: conversation,
|
||||
@@ -1598,7 +1598,7 @@ export class ConversationController {
|
||||
}
|
||||
|
||||
if (count % 10 === 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await sleep(300);
|
||||
}
|
||||
}
|
||||
@@ -1719,7 +1719,7 @@ export class ConversationController {
|
||||
continue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await removeConversation(convo.id);
|
||||
this.#removeConversation(convo);
|
||||
}
|
||||
|
||||
+7
-7
@@ -370,7 +370,7 @@ export function verifyHmacSha256(
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < theirMac.byteLength; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line no-bitwise, typescript/no-non-null-assertion
|
||||
result |= ourMac[i]! ^ theirMac[i]!;
|
||||
}
|
||||
if (result !== 0) {
|
||||
@@ -465,7 +465,7 @@ export function getZeroes(n: number): Uint8Array<ArrayBuffer> {
|
||||
}
|
||||
|
||||
export function highBitsToInt(byte: number): number {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
// oxlint-disable-next-line no-bitwise
|
||||
return (byte & 0xff) >> 4;
|
||||
}
|
||||
|
||||
@@ -473,7 +473,7 @@ export function intsToByteHighAndLow(
|
||||
highValue: number,
|
||||
lowValue: number
|
||||
): number {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
// oxlint-disable-next-line no-bitwise
|
||||
return ((highValue << 4) | lowValue) & 0xff;
|
||||
}
|
||||
|
||||
@@ -503,7 +503,7 @@ function verifyDigest(
|
||||
const ourDigest = sha256(data);
|
||||
let result = 0;
|
||||
for (let i = 0; i < theirDigest.byteLength; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line no-bitwise, typescript/no-non-null-assertion
|
||||
result |= ourDigest[i]! ^ theirDigest[i]!;
|
||||
}
|
||||
if (result !== 0) {
|
||||
@@ -777,7 +777,7 @@ export function getIdentifierHash({
|
||||
}
|
||||
|
||||
const digest = hash(HashType.size256, identifier);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
return digest[0]!;
|
||||
}
|
||||
|
||||
@@ -795,10 +795,10 @@ export function generateAvatarColor({
|
||||
const hashValue = getIdentifierHash({ aci, e164, pni, groupId });
|
||||
|
||||
if (hashValue == null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
return sample(AvatarColors) || AvatarColors[0]!;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
return AvatarColors[hashValue % AVATAR_COLOR_COUNT]!;
|
||||
}
|
||||
|
||||
+4
-5
@@ -146,16 +146,15 @@ function validatePubKeyFormat(
|
||||
}
|
||||
|
||||
export function setPublicKeyTypeByte(publicKey: Uint8Array<ArrayBuffer>): void {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
publicKey[0] = 5;
|
||||
}
|
||||
|
||||
export function clampPrivateKey(privateKey: Uint8Array<ArrayBuffer>): void {
|
||||
/* eslint-disable no-bitwise, no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
|
||||
privateKey[0]! &= 248;
|
||||
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
|
||||
privateKey[31]! &= 127;
|
||||
// oxlint-disable-next-line no-bitwise, no-param-reassign, typescript/no-non-null-assertion
|
||||
privateKey[31]! |= 64;
|
||||
/* eslint-enable no-bitwise, no-param-reassign */
|
||||
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
||||
}
|
||||
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// https://github.com/microsoft/TypeScript/issues/29129
|
||||
declare namespace Intl {
|
||||
function getCanonicalLocales(locales: string | Array<string>): Array<string>;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import lodash from 'lodash';
|
||||
|
||||
import type {
|
||||
@@ -108,6 +106,7 @@ export type IdentityKeysOptions = Readonly<{
|
||||
zone?: Zone;
|
||||
}>;
|
||||
|
||||
// oxlint-disable-next-line max-classes-per-file
|
||||
export class IdentityKeys extends IdentityKeyStore {
|
||||
readonly #signalProtocolStore: SignalProtocolStore;
|
||||
readonly #ourServiceId: ServiceIdString;
|
||||
|
||||
@@ -186,7 +186,7 @@ async function _fillCaches<ID, T extends HasIdType<ID>, HydratedType>(
|
||||
}
|
||||
|
||||
log.info(`Finished caching ${field} data`);
|
||||
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line no-param-reassign, typescript/no-explicit-any
|
||||
object[field] = cache as any;
|
||||
}
|
||||
|
||||
@@ -2090,11 +2090,11 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
throw new Error('saveIdentity: encodedAddress was undefined/null');
|
||||
}
|
||||
if (!(publicKey instanceof Uint8Array)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
publicKey = Bytes.fromBinary(publicKey);
|
||||
}
|
||||
if (typeof nonblockingApproval !== 'boolean') {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
// oxlint-disable-next-line no-param-reassign
|
||||
nonblockingApproval = false;
|
||||
}
|
||||
|
||||
@@ -2636,14 +2636,14 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
// Update database
|
||||
await Promise.all<void>([
|
||||
itemStorage.put('identityKeyMap', {
|
||||
...(itemStorage.get('identityKeyMap') || {}),
|
||||
...itemStorage.get('identityKeyMap'),
|
||||
[pni]: {
|
||||
pubKey: pniPublicKey,
|
||||
privKey: pniPrivateKey,
|
||||
},
|
||||
}),
|
||||
itemStorage.put('registrationIdMap', {
|
||||
...(itemStorage.get('registrationIdMap') || {}),
|
||||
...itemStorage.get('registrationIdMap'),
|
||||
[pni]: registrationId,
|
||||
}),
|
||||
(async () => {
|
||||
@@ -2851,7 +2851,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
|
||||
public override on(
|
||||
eventName: string | symbol,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
listener: (...args: Array<any>) => void
|
||||
): this {
|
||||
return super.on(eventName, listener);
|
||||
@@ -2877,7 +2877,7 @@ export class SignalProtocolStore extends EventEmitter {
|
||||
|
||||
public override emit(
|
||||
eventName: string | symbol,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
...args: Array<any>
|
||||
): boolean {
|
||||
return super.emit(eventName, ...args);
|
||||
|
||||
@@ -79,7 +79,7 @@ export class WebAudioRecorder {
|
||||
this.#input.connect(this.processor);
|
||||
this.processor.connect(this.#context.destination);
|
||||
this.processor.onaudioprocess = event => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
// oxlint-disable-next-line no-plusplus
|
||||
for (let ch = 0; ch < numChannels; ++ch) {
|
||||
buffer[ch] = event.inputBuffer.getChannelData(ch);
|
||||
}
|
||||
@@ -130,6 +130,7 @@ export class WebAudioRecorder {
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line no-undef FIXME
|
||||
this.worker = new Worker('js/WebAudioRecorderMp3.js');
|
||||
this.worker.onmessage = event => {
|
||||
const { data } = event;
|
||||
|
||||
@@ -17,10 +17,10 @@ function Card(props: { children: ReactNode }) {
|
||||
<AriaClickable.Root
|
||||
className={tw(
|
||||
'group flex items-center gap-4 rounded-md border border-border-secondary p-4',
|
||||
'data-[hovered]:bg-background-secondary',
|
||||
'data-[pressed]:bg-fill-secondary-pressed',
|
||||
'data-hovered:bg-background-secondary',
|
||||
'data-pressed:bg-fill-secondary-pressed',
|
||||
'outline-0 outline-border-focused',
|
||||
'data-[focused]:outline-[2.5px]'
|
||||
'data-focused:outline-[2.5px]'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -48,7 +48,7 @@ function CardSeeMoreLink(props: { onClick: () => void; children: ReactNode }) {
|
||||
id={id}
|
||||
className={tw(
|
||||
'text-color-label-primary',
|
||||
'group-data-[hovered]:underline'
|
||||
'group-data-hovered:underline'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -85,7 +85,7 @@ export namespace AriaClickable {
|
||||
return (
|
||||
<TriggerStateUpdateContext.Provider value={handleTriggerStateUpdate}>
|
||||
<div
|
||||
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
className={tw('relative!', props.className)}
|
||||
// For styling based on the HiddenTrigger state.
|
||||
data-hovered={hovered ? true : null}
|
||||
@@ -136,7 +136,7 @@ export namespace AriaClickable {
|
||||
// [css-position-3: Painting Order and Stacking Contexts]: https://drafts.csswg.org/css-position-3/#stacking
|
||||
// [css-flexbox-1: Flex Item Z-Ordering]: https://drafts.csswg.org/css-flexbox-1/#painting
|
||||
//
|
||||
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
className={tw('contents *:relative *:z-20')}
|
||||
>
|
||||
{props.children}
|
||||
@@ -160,7 +160,7 @@ export namespace AriaClickable {
|
||||
*/
|
||||
export const DeadArea: FC<DeadAreaProps> = memo(props => {
|
||||
return (
|
||||
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
<div className={tw('relative! z-20!', props.className)}>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -185,7 +185,7 @@ export function Icons(): JSX.Element {
|
||||
{size => (
|
||||
<AxoAvatar.Root size={size}>
|
||||
<AxoAvatar.Content label={null}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{/* oxlint-disable-next-line typescript/no-non-null-assertion */}
|
||||
<AxoAvatar.Icon symbol={icons[size % icons.length]!} />
|
||||
</AxoAvatar.Content>
|
||||
</AxoAvatar.Root>
|
||||
|
||||
+12
-13
@@ -75,20 +75,20 @@ export namespace AxoAvatar {
|
||||
};
|
||||
|
||||
const RingSizes: Record<Size, TailwindStyles | null> = {
|
||||
20: tw('border-[1px] p-[1.5px]'),
|
||||
24: tw('border-[1px] p-[1.5px]'),
|
||||
20: tw('border p-[1.5px]'),
|
||||
24: tw('border p-[1.5px]'),
|
||||
28: tw('border-[1.5px] p-[2px]'),
|
||||
30: tw('border-[1.5px] p-[2px]'),
|
||||
32: tw('border-[1.5px] p-[2px]'),
|
||||
36: tw('border-[1.5px] p-[2px]'),
|
||||
40: tw('border-[1.5px] p-[2px]'),
|
||||
48: tw('border-[2px] p-[3px]'),
|
||||
52: tw('border-[2px] p-[3px]'),
|
||||
64: tw('border-[2px] p-[3px]'),
|
||||
48: tw('border-2 p-[3px]'),
|
||||
52: tw('border-2 p-[3px]'),
|
||||
64: tw('border-2 p-[3px]'),
|
||||
72: tw('border-[2.5px] p-[3.5px]'),
|
||||
80: tw('border-[2.5px] p-[3.5px]'),
|
||||
96: tw('border-[3px] p-[4px]'),
|
||||
216: tw('border-[4px] p-[6px]'),
|
||||
216: tw('border-4 p-[6px]'),
|
||||
};
|
||||
|
||||
export function _getAllSizes(): ReadonlyArray<Size> {
|
||||
@@ -241,14 +241,13 @@ export namespace AxoAvatar {
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
// oxlint-disable-next-line jsx-a11y/alt-text
|
||||
<img
|
||||
ref={ref}
|
||||
src={props.src}
|
||||
width={props.srcWidth}
|
||||
height={props.srcHeight}
|
||||
decoding="async"
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
fetchPriority="low"
|
||||
loading="lazy"
|
||||
draggable={false}
|
||||
@@ -288,7 +287,7 @@ export namespace AxoAvatar {
|
||||
}, [preset]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
// oxlint-disable-next-line jsx-a11y/alt-text
|
||||
<img
|
||||
src={src}
|
||||
width={1024}
|
||||
@@ -325,7 +324,7 @@ export namespace AxoAvatar {
|
||||
className={tw(
|
||||
'absolute inset-0 rounded-full',
|
||||
'flex flex-col items-center-safe justify-center-safe gap-2',
|
||||
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
'bg-[#000]/20 text-[#fff] hover:bg-[#000]/40',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]'
|
||||
)}
|
||||
@@ -475,13 +474,13 @@ export namespace AxoAvatar {
|
||||
|
||||
const children = (
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/alt-text */}
|
||||
<img
|
||||
{...baseImageProps}
|
||||
src={badge.light}
|
||||
className={tw('size-full dark:hidden')}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/alt-text */}
|
||||
<img
|
||||
{...baseImageProps}
|
||||
src={badge.dark}
|
||||
@@ -493,7 +492,7 @@ export namespace AxoAvatar {
|
||||
const baseClassName = tw(
|
||||
'absolute rounded-full',
|
||||
// Proportionately sized & positioned based on the size of the avatar
|
||||
'-end-[calc(2.75px-3%)] -bottom-[calc(6.25px-1%)] size-[calc(5px+37.5%)]'
|
||||
'-inset-e-[calc(2.75px-3%)] -bottom-[calc(6.25px-1%)] size-[calc(5px+37.5%)]'
|
||||
);
|
||||
|
||||
let result: ReactNode;
|
||||
|
||||
@@ -58,8 +58,8 @@ export namespace AxoCheckbox {
|
||||
'data-[state=checked]:bg-color-fill-primary',
|
||||
'data-[state=checked]:text-label-primary-on-color',
|
||||
'data-[state=checked]:pressed:bg-color-fill-primary-pressed',
|
||||
'data-[disabled]:border-border-secondary',
|
||||
'data-[state=checked]:data-[disabled]:text-label-disabled-on-color',
|
||||
'data-disabled:border-border-secondary',
|
||||
'data-[state=checked]:data-disabled:text-label-disabled-on-color',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
'overflow-hidden'
|
||||
)}
|
||||
|
||||
@@ -239,8 +239,7 @@ export namespace AxoContextMenu {
|
||||
alignOffset={-6}
|
||||
collisionPadding={6}
|
||||
onCloseAutoFocus={props.onCloseAutoFocus}
|
||||
// @ts-expect-error -- React/TS doesn't know about inert
|
||||
inert={open ? undefined : 'true'}
|
||||
inert={!open}
|
||||
>
|
||||
{props.children}
|
||||
</ContextMenu.Content>
|
||||
@@ -453,12 +452,10 @@ export namespace AxoContextMenu {
|
||||
* -------------------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the context menu.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
export const Separator: FC = memo(() => {
|
||||
return (
|
||||
<ContextMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
|
||||
);
|
||||
|
||||
@@ -350,7 +350,6 @@ export function ExampleLanguageDialog(): React.JSX.Element {
|
||||
<AxoDialog.ExperimentalSearch>
|
||||
<input
|
||||
type="search"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
placeholder="Search languages"
|
||||
className={tw(
|
||||
|
||||
@@ -334,7 +334,7 @@ export namespace AxoDialog {
|
||||
// fit 1-2 words per line, push it up into its own row:
|
||||
'min-w-[calc-size(fit-content,min(20ch,size))]',
|
||||
// Allow it to fill its own row
|
||||
'flex-grow',
|
||||
'grow',
|
||||
'type-body-large text-label-primary'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -203,8 +203,7 @@ export namespace AxoDropdownMenu {
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={descriptionId}
|
||||
onCloseAutoFocus={props.onCloseAutoFocus}
|
||||
// @ts-expect-error -- React/TS doesn't know about inert
|
||||
inert={open ? undefined : 'true'}
|
||||
inert={!open}
|
||||
>
|
||||
{props.children}
|
||||
</DropdownMenu.Content>
|
||||
@@ -502,12 +501,10 @@ export namespace AxoDropdownMenu {
|
||||
* --------------------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = AxoBaseMenu.MenuSeparatorProps;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the dropdown menu.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
export const Separator: FC = memo(() => {
|
||||
return (
|
||||
<DropdownMenu.Separator className={AxoBaseMenu.menuSeparatorStyles} />
|
||||
);
|
||||
@@ -519,7 +516,7 @@ export namespace AxoDropdownMenu {
|
||||
* Component: <AxoDropdownMenu.ContentSeparator>
|
||||
*/
|
||||
|
||||
export const ContentSeparator: FC<SeparatorProps> = memo(() => {
|
||||
export const ContentSeparator: FC = memo(() => {
|
||||
return (
|
||||
<DropdownMenu.Separator
|
||||
className={AxoBaseMenu.menuContentSeparatorStyles}
|
||||
|
||||
@@ -110,7 +110,7 @@ export namespace AxoMenuBuilder {
|
||||
|
||||
Label.displayName = `${Namespace}.Label`;
|
||||
|
||||
export const Separator: FC<AxoBaseMenu.MenuSeparatorProps> = memo(props => {
|
||||
export const Separator: FC = memo(props => {
|
||||
const renderer = useStrictContext(MenuBuilderContext);
|
||||
if (renderer === 'AxoDropdownMenu') {
|
||||
return <AxoDropdownMenu.Separator {...props} />;
|
||||
|
||||
@@ -94,11 +94,7 @@ export namespace AxoRadioGroup {
|
||||
* ------------------------------------
|
||||
*/
|
||||
|
||||
export type IndicatorProps = Readonly<{
|
||||
// ...
|
||||
}>;
|
||||
|
||||
export const Indicator: FC<IndicatorProps> = memo(() => {
|
||||
export const Indicator: FC = memo(() => {
|
||||
const context = useStrictContext(ItemContext);
|
||||
return (
|
||||
<RadioGroup.Item
|
||||
@@ -112,7 +108,7 @@ export namespace AxoRadioGroup {
|
||||
'data-[state=unchecked]:pressed:bg-fill-primary-pressed',
|
||||
'data-[state=checked]:bg-color-fill-primary',
|
||||
'data-[state=checked]:pressed:bg-color-fill-primary-pressed',
|
||||
'data-[disabled]:border-border-secondary',
|
||||
'data-disabled:border-border-secondary',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
'overflow-hidden',
|
||||
'forced-colors:data-[state=checked]:bg-[SelectedItem]'
|
||||
@@ -123,7 +119,7 @@ export namespace AxoRadioGroup {
|
||||
className={tw(
|
||||
'size-[9px] rounded-full',
|
||||
'data-[state=checked]:bg-label-primary-on-color',
|
||||
'data-[state=checked]:data-[disabled]:bg-label-disabled-on-color',
|
||||
'data-[state=checked]:data-disabled:bg-label-disabled-on-color',
|
||||
'forced-colors:data-[state=checked]:bg-[SelectedItemText]'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -341,28 +341,28 @@ export namespace AxoScrollArea {
|
||||
edgeYStyles,
|
||||
edgeStartStyles,
|
||||
'top-0',
|
||||
'bg-gradient-to-b'
|
||||
'bg-linear-to-b'
|
||||
),
|
||||
bottom: tw(
|
||||
edgeStyles,
|
||||
edgeYStyles,
|
||||
edgeEndStyles,
|
||||
'bottom-0',
|
||||
'bg-gradient-to-t'
|
||||
'bg-linear-to-t'
|
||||
),
|
||||
'inline-start': tw(
|
||||
edgeStyles,
|
||||
edgeXStyles,
|
||||
edgeStartStyles,
|
||||
'start-0',
|
||||
'bg-gradient-to-r rtl:bg-gradient-to-l'
|
||||
'inset-s-0',
|
||||
'bg-linear-to-r rtl:bg-linear-to-l'
|
||||
),
|
||||
'inline-end': tw(
|
||||
edgeStyles,
|
||||
edgeXStyles,
|
||||
edgeEndStyles,
|
||||
'end-0',
|
||||
'bg-gradient-to-l rtl:bg-gradient-to-r'
|
||||
'inset-e-0',
|
||||
'bg-linear-to-l rtl:bg-linear-to-r'
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export namespace AxoSelect {
|
||||
},
|
||||
'on-hover': {
|
||||
chevronStyles: tw(
|
||||
'absolute inset-y-0 end-0 w-9.5',
|
||||
'absolute inset-y-0 inset-e-0 w-9.5',
|
||||
'flex items-center justify-end pe-2',
|
||||
'opacity-0 group-focus:opacity-100 group-data-[state=open]:opacity-100 group-hovered:opacity-100',
|
||||
'transition-opacity duration-150'
|
||||
@@ -146,10 +146,10 @@ export namespace AxoSelect {
|
||||
'group-hovered:[--axo-select-trigger-mask-start:transparent]',
|
||||
'group-focus:[--axo-select-trigger-mask-start:transparent]',
|
||||
'group-data-[state=open]:[--axo-select-trigger-mask-start:transparent]',
|
||||
'[mask-image:linear-gradient(to_left,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
|
||||
'rtl:[mask-image:linear-gradient(to_right,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
|
||||
'[mask-repeat:no-repeat]',
|
||||
'[mask-position:right] rtl:[mask-position:left]',
|
||||
'mask-[linear-gradient(to_left,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
|
||||
'rtl:mask-[linear-gradient(to_right,var(--axo-select-trigger-mask-start)_19px,black_38px)]',
|
||||
'mask-no-repeat',
|
||||
'mask-right rtl:mask-left',
|
||||
'[transition-property:--axo-select-trigger-mask-start] duration-150'
|
||||
),
|
||||
},
|
||||
@@ -408,14 +408,10 @@ export namespace AxoSelect {
|
||||
* ---------------------------
|
||||
*/
|
||||
|
||||
export type SeparatorProps = Readonly<{
|
||||
// N/A
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Used to visually separate items in the select.
|
||||
*/
|
||||
export const Separator: FC<SeparatorProps> = memo(() => {
|
||||
export const Separator: FC = memo(() => {
|
||||
return <Select.Separator className={AxoBaseMenu.selectSeperatorStyles} />;
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export namespace AxoSwitch {
|
||||
'group relative z-0 flex h-[18px] w-8 items-center rounded-full',
|
||||
'border border-border-secondary inset-shadow-on-color',
|
||||
'bg-fill-secondary',
|
||||
'data-[disabled]:bg-fill-primary',
|
||||
'data-disabled:bg-fill-primary',
|
||||
'pressed:bg-fill-secondary-pressed',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
'overflow-hidden'
|
||||
@@ -34,21 +34,21 @@ export namespace AxoSwitch {
|
||||
>
|
||||
<span
|
||||
className={tw(
|
||||
'absolute top-0 bottom-0',
|
||||
'absolute inset-y-0',
|
||||
'w-5.5 rounded-s-full',
|
||||
'group-data-[disabled]:w-7.5 group-data-[disabled]:rounded-full',
|
||||
'group-data-disabled:w-7.5 group-data-disabled:rounded-full',
|
||||
'opacity-0 group-data-[state=checked]:opacity-100',
|
||||
'-translate-x-3.5 group-data-[state=checked]:translate-x-0 rtl:translate-x-3.5',
|
||||
'bg-color-fill-primary group-pressed:bg-color-fill-primary-pressed',
|
||||
'transition-all duration-200 ease-out-cubic',
|
||||
'forced-colors:bg-[AccentColor]',
|
||||
'forced-colors:group-data-[disabled]:bg-[GrayText]'
|
||||
'forced-colors:group-data-disabled:bg-[GrayText]'
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={tw(
|
||||
'invisible forced-colors:visible',
|
||||
'absolute start-0.5 z-0 text-[12px]',
|
||||
'absolute inset-s-0.5 z-0 text-[12px]',
|
||||
'forced-color-adjust-none',
|
||||
'forced-colors:text-[AccentColorText]'
|
||||
)}
|
||||
@@ -58,16 +58,16 @@ export namespace AxoSwitch {
|
||||
<Switch.Thumb
|
||||
className={tw(
|
||||
'z-10 block size-4 rounded-full',
|
||||
// eslint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
// oxlint-disable-next-line better-tailwindcss/no-restricted-classes
|
||||
'shadow-[#000]/12',
|
||||
'shadow-[0.5px_0_0.5px_0.5px,-0.5px_0_0.5px_0.5px]',
|
||||
'bg-label-primary-on-color',
|
||||
'data-[disabled]:bg-label-disabled-on-color',
|
||||
'data-disabled:bg-label-disabled-on-color',
|
||||
'transition-all duration-200 ease-out-cubic',
|
||||
'data-[state=checked]:translate-x-3.5',
|
||||
'rtl:data-[state=checked]:-translate-x-3.5',
|
||||
'forced-colors:border',
|
||||
'forced-colors:data-[disabled]:bg-[ButtonFace]'
|
||||
'forced-colors:data-disabled:bg-[ButtonFace]'
|
||||
)}
|
||||
/>
|
||||
</Switch.Root>
|
||||
|
||||
@@ -108,7 +108,7 @@ export function All(): React.JSX.Element {
|
||||
setInput(event.currentTarget.value);
|
||||
}}
|
||||
className={tw(
|
||||
'w-full rounded bg-elevated-background-secondary p-3 type-body-medium'
|
||||
'w-full rounded-sm bg-elevated-background-secondary p-3 type-body-medium'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ export namespace AxoTokens {
|
||||
Number.isInteger(hash) && hash >= 0,
|
||||
'Hash must be positive integer'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
return ALL_COLOR_NAMES[hash % ALL_COLOR_NAMES.length]!;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export namespace AxoTokens {
|
||||
Number.isInteger(hash) && hash >= 0,
|
||||
'Hash must be positive integer'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
return Gradients[hash % Gradients.length]!;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ export namespace AxoBaseMenu {
|
||||
const navigableItemStyles = tw(
|
||||
labeledItemStyles,
|
||||
'curved-md type-body-medium',
|
||||
'outline-0 data-[highlighted]:bg-fill-secondary-pressed',
|
||||
'data-[disabled]:text-label-disabled',
|
||||
'outline-0 data-highlighted:bg-fill-secondary-pressed',
|
||||
'data-disabled:text-label-disabled',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
'forced-colors:text-[CanvasText]',
|
||||
'forced-colors:data-[highlighted]:bg-[Highlight]',
|
||||
'forced-colors:data-[highlighted]:text-[HighlightText]',
|
||||
'forced-colors:data-[disabled]:text-[GrayText]',
|
||||
'forced-colors:data-highlighted:bg-[Highlight]',
|
||||
'forced-colors:data-highlighted:text-[HighlightText]',
|
||||
'forced-colors:data-disabled:text-[GrayText]',
|
||||
'forced-color-adjust-none'
|
||||
);
|
||||
|
||||
@@ -165,7 +165,7 @@ export namespace AxoBaseMenu {
|
||||
<span
|
||||
dir="auto"
|
||||
className={tw(
|
||||
'ms-auto px-1 type-body-medium text-label-secondary forced-colors:text-[inherit]'
|
||||
'ms-auto px-1 type-body-medium text-label-secondary forced-colors:text-inherit'
|
||||
)}
|
||||
>
|
||||
{props.keyboardShortcut}
|
||||
@@ -212,8 +212,8 @@ export namespace AxoBaseMenu {
|
||||
export const menuContentStyles = tw(
|
||||
baseContentStyles,
|
||||
baseContentGridStyles,
|
||||
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
|
||||
'overflow-auto [scrollbar-width:none]'
|
||||
'max-h-(--radix-popper-available-height) overflow-auto scrollbar-width-none',
|
||||
'overflow-auto scrollbar-width-none'
|
||||
);
|
||||
|
||||
export const selectContentStyles = tw(baseContentStyles);
|
||||
@@ -338,10 +338,6 @@ export namespace AxoBaseMenu {
|
||||
* ----------------------
|
||||
*/
|
||||
|
||||
export type MenuSeparatorProps = Readonly<{
|
||||
// N/A
|
||||
}>;
|
||||
|
||||
const baseSeparatorStyles = tw('my-1 border-t-[0.5px] border-border-primary');
|
||||
|
||||
export const menuSeparatorStyles = tw(
|
||||
@@ -375,9 +371,9 @@ export namespace AxoBaseMenu {
|
||||
|
||||
export const menuSubTriggerStyles = tw(
|
||||
navigableItemStyles,
|
||||
'data-[state=open]:not-data-[highlighted]:bg-fill-secondary',
|
||||
'forced-colors:data-[state=open]:not-data-[highlighted]:bg-[Highlight]',
|
||||
'forced-colors:data-[state=open]:not-data-[highlighted]:text-[HighlightText]'
|
||||
'data-[state=open]:not-data-highlighted:bg-fill-secondary',
|
||||
'forced-colors:data-[state=open]:not-data-highlighted:bg-[Highlight]',
|
||||
'forced-colors:data-[state=open]:not-data-highlighted:text-[HighlightText]'
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -391,7 +387,7 @@ export namespace AxoBaseMenu {
|
||||
|
||||
export const menuSubContentStyles = tw(
|
||||
baseContentStyles,
|
||||
'max-h-(--radix-popper-available-height) overflow-auto [scrollbar-width:none]',
|
||||
'max-h-(--radix-popper-available-height) overflow-auto scrollbar-width-none',
|
||||
baseContentGridStyles
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const FlexWrapDetector = memo(function FlexWrapDetector(
|
||||
<div className={tw('absolute -end-px size-px')} />
|
||||
</div>
|
||||
{/* 5. When not wrapped, this item should take priority when growing the items */}
|
||||
<div className={tw('grow-[9999]')}>{props.children}</div>
|
||||
<div className={tw('grow-9999')}>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export function assert(condition: boolean, message?: string): asserts condition;
|
||||
export function assert<T>(input: T, message?: string): NonNullable<T>;
|
||||
export function assert<T>(input: T, message?: string): NonNullable<T> {
|
||||
if (input === false || input == null) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
// oxlint-disable-next-line no-debugger
|
||||
debugger;
|
||||
throw new AssertionError(message ?? `input is ${input}`);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function assert<T>(input: T, message?: string): NonNullable<T> {
|
||||
}
|
||||
|
||||
export function unreachable(_value: never): never {
|
||||
// eslint-disable-next-line no-debugger
|
||||
// oxlint-disable-next-line no-debugger
|
||||
debugger;
|
||||
throw new AssertionError('unreachable');
|
||||
}
|
||||
|
||||
@@ -1199,6 +1199,7 @@ export async function startApp(): Promise<void> {
|
||||
} finally {
|
||||
setupAppState();
|
||||
drop(
|
||||
// oxlint-disable-next-line promise/prefer-await-to-then
|
||||
start().catch(error => {
|
||||
log.error('start: threw an unexpected error', error);
|
||||
})
|
||||
@@ -1665,7 +1666,7 @@ export async function startApp(): Promise<void> {
|
||||
|
||||
while (afterAuthSocketConnectPromise?.promise) {
|
||||
log.info(`${logId}: waiting for previous run to finish`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await afterAuthSocketConnectPromise.promise;
|
||||
}
|
||||
|
||||
@@ -2126,7 +2127,7 @@ export async function startApp(): Promise<void> {
|
||||
let lastRowId: number | null = 0;
|
||||
while (lastRowId != null) {
|
||||
const result =
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await DataWriter.dequeueOldestSyncTasks({
|
||||
previousRowId: lastRowId,
|
||||
incrementAttempts: false,
|
||||
@@ -2140,9 +2141,9 @@ export async function startApp(): Promise<void> {
|
||||
log.info(
|
||||
`onEmpty/syncTasks: Queueing ${syncTasks.length} sync tasks for reattempt`
|
||||
);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await queueSyncTasks(syncTasks, DataWriter.removeSyncTaskById);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await Promise.resolve(); // one tick
|
||||
}
|
||||
|
||||
@@ -2448,7 +2449,7 @@ export async function startApp(): Promise<void> {
|
||||
});
|
||||
|
||||
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
// oxlint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
return handleMessageReceivedProfileUpdate({
|
||||
@@ -2787,7 +2788,7 @@ export async function startApp(): Promise<void> {
|
||||
}) {
|
||||
// First set profileSharing = true for the conversation we sent to
|
||||
const { id } = messageDescriptor;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const conversation = window.ConversationController.get(id)!;
|
||||
|
||||
conversation.enableProfileSharing({
|
||||
@@ -2797,7 +2798,7 @@ export async function startApp(): Promise<void> {
|
||||
|
||||
// Then we update our own profileKey if it's different from what we have
|
||||
const ourId = window.ConversationController.getOurConversationId();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
const me = window.ConversationController.get(ourId)!;
|
||||
const { profileKey } = data.message;
|
||||
strictAssert(
|
||||
@@ -3029,7 +3030,7 @@ export async function startApp(): Promise<void> {
|
||||
});
|
||||
|
||||
const { PROFILE_KEY_UPDATE } = Proto.DataMessage.Flags;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
// oxlint-disable-next-line no-bitwise
|
||||
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
|
||||
if (isProfileUpdate) {
|
||||
return handleMessageSentProfileUpdate({
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
import { videoPixelFormatToEnum } from '@signalapp/ringrtc';
|
||||
import type { VideoFrameSender, VideoFrameSource } from '@signalapp/ringrtc';
|
||||
import type { RefObject } from 'react';
|
||||
@@ -23,6 +19,7 @@ export class GumVideoCaptureOptions {
|
||||
onEnded?: () => void;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/consistent-type-definitions
|
||||
interface GumTrackConstraints extends MediaTrackConstraints {
|
||||
mandatory?: GumTrackConstraintSet;
|
||||
}
|
||||
@@ -46,6 +43,7 @@ export type SetLocalPreviewType = {
|
||||
sizeCallback: SizeCallbackType | undefined;
|
||||
};
|
||||
|
||||
// oxlint-disable-next-line max-classes-per-file
|
||||
export class GumVideoCapturer {
|
||||
private localPreview?: HTMLVideoElement;
|
||||
private sizeCallback?: SizeCallbackType;
|
||||
@@ -329,10 +327,11 @@ export class GumVideoCapturer {
|
||||
}).readable.getReader();
|
||||
const buffer = new Uint8Array(MAX_VIDEO_CAPTURE_BUFFER_SIZE);
|
||||
this.spawnedSenderRunning = true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
// oxlint-disable-next-line typescript/no-floating-promises
|
||||
(async () => {
|
||||
try {
|
||||
while (mediaStream === this.mediaStream) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const { done, value: frame } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
@@ -352,6 +351,7 @@ export class GumVideoCapturer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await frame.copyTo(buffer);
|
||||
if (sender !== this.sender) {
|
||||
break;
|
||||
|
||||
@@ -24,7 +24,6 @@ export default {
|
||||
},
|
||||
} satisfies Meta<Props>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<Props> = args => {
|
||||
return (
|
||||
<AddUserToAnotherGroupModal
|
||||
|
||||
@@ -68,7 +68,7 @@ export function AnimatedEmojiGalore({
|
||||
<>
|
||||
{springs.map((styles, index) => (
|
||||
<animated.div
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
style={{
|
||||
left: `${random(0, 100)}%`,
|
||||
|
||||
@@ -84,7 +84,6 @@ const sizes = Object.values(AvatarSize).filter(
|
||||
x => typeof x === 'number'
|
||||
) as Array<AvatarSize>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<Props> = (args: Props) => {
|
||||
return (
|
||||
<>
|
||||
@@ -95,7 +94,6 @@ const Template: StoryFn<Props> = (args: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const TemplateSingle: StoryFn<Props> = (args: Props) => (
|
||||
<Avatar {...args} size={AvatarSize.EIGHTY} />
|
||||
);
|
||||
@@ -104,12 +102,12 @@ export const Default = Template.bind({});
|
||||
Default.args = createProps({
|
||||
avatarUrl: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
Default.play = async (context: any) => {
|
||||
const { args, canvasElement } = context;
|
||||
const canvas = within(canvasElement);
|
||||
const [avatar] = canvas.getAllByRole('button');
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion
|
||||
await userEvent.click(avatar!);
|
||||
await expect(args.onClick).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user