mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
456 lines
11 KiB
JavaScript
456 lines
11 KiB
JavaScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
const MAIN_MODULES = new Set([
|
|
'app',
|
|
'autoUpdater',
|
|
'BaseWindow',
|
|
'BrowserView',
|
|
'BrowserWindow',
|
|
'contentTracing',
|
|
'desktopCapturer',
|
|
'dialog',
|
|
'globalShortcut',
|
|
'inAppPurchase',
|
|
'ipcMain',
|
|
'Menu',
|
|
'MenuItem',
|
|
'MessageChannelMain',
|
|
'MessagePortMain',
|
|
'nativeTheme',
|
|
'net',
|
|
'netLog',
|
|
'Notification',
|
|
'powerMonitor',
|
|
'powerSaveBlocker',
|
|
'process',
|
|
'protocol',
|
|
'pushNotifications',
|
|
'safeStorage',
|
|
'screen',
|
|
'session',
|
|
'ShareMenu',
|
|
'shell',
|
|
'systemPreferences',
|
|
'TouchBar',
|
|
'Tray',
|
|
'utilityProcess',
|
|
'webContents',
|
|
'WebContentsView',
|
|
'webFrameMain',
|
|
'View',
|
|
]);
|
|
const RENDERER_MODULES = new Set([
|
|
'contextBridge',
|
|
'ipcRenderer',
|
|
'webFrame',
|
|
'webUtils',
|
|
]);
|
|
const NODE_MODULES = new Set([
|
|
'@electron/asar',
|
|
'@indutny/dicer',
|
|
'@indutny/mac-screen-share',
|
|
'@indutny/range-finder',
|
|
'@indutny/simple-windows-notifications',
|
|
'@signalapp/libsignal-client',
|
|
'@signalapp/ringrtc',
|
|
'@signalapp/sqlcipher',
|
|
'@signalapp/windows-ucv',
|
|
'cirbuf',
|
|
'config',
|
|
'dashdash',
|
|
'encoding',
|
|
'fast-glob',
|
|
'fs-extra',
|
|
'got',
|
|
'growing-file',
|
|
'node-fetch',
|
|
'proxy-agent',
|
|
'read-last-lines',
|
|
'websocket',
|
|
'write-file-atomic',
|
|
|
|
// Dev dependencies
|
|
'@electron/fuses',
|
|
'@electron/notarize',
|
|
'@electron/symbolicate-mac',
|
|
'@indutny/parallel-prettier',
|
|
'@indutny/rezip-electron',
|
|
'@napi-rs/canvas',
|
|
'@signalapp/mock-server',
|
|
'@tailwindcss/cli',
|
|
'@tailwindcss/postcss',
|
|
'chokidar-cli',
|
|
'cross-env',
|
|
'electron-builder',
|
|
'electron-mocha',
|
|
'endanger',
|
|
'enhanced-resolve',
|
|
'enquirer',
|
|
'esbuild',
|
|
'execa',
|
|
'html-webpack-plugin',
|
|
'http-server',
|
|
'json-to-ast',
|
|
'log-symbols',
|
|
'mini-css-extract-plugin',
|
|
'node-gyp',
|
|
'node-gyp-build',
|
|
'npm-run-all',
|
|
'p-limit',
|
|
'pixelmatch',
|
|
'playwright',
|
|
'postcss',
|
|
'postcss-loader',
|
|
'prettier',
|
|
'prettier-plugin-tailwindcss',
|
|
'protobufjs-cli',
|
|
'react-devtools',
|
|
'react-devtools-core',
|
|
'resedit',
|
|
'resolve-url-loader',
|
|
'sass',
|
|
'sass-loader',
|
|
'style-loader',
|
|
'stylelint',
|
|
'stylelint-config-css-modules',
|
|
'stylelint-config-recommended-scss',
|
|
'stylelint-use-logical-spec',
|
|
'svgo',
|
|
'synckit',
|
|
'tailwindcss',
|
|
'terser-webpack-plugin',
|
|
'ts-node',
|
|
'typescript',
|
|
'wait-on',
|
|
'webpack',
|
|
'webpack-cli',
|
|
'webpack-dev-server',
|
|
]);
|
|
const DOM_MODULES = new Set([
|
|
'@popperjs/core',
|
|
'@radix-ui/react-tooltip',
|
|
'@react-aria/focus',
|
|
'@react-aria/interactions',
|
|
'@react-aria/utils',
|
|
'@react-spring/web',
|
|
'@tanstack/react-virtual',
|
|
'blob-util',
|
|
'blueimp-load-image',
|
|
'copy-text-to-clipboard',
|
|
'fabric',
|
|
'focus-trap-react',
|
|
'radix-ui',
|
|
'react-aria',
|
|
'react-aria-components',
|
|
'react-blurhash',
|
|
'react-contextmenu',
|
|
'react-popper',
|
|
'react-virtualized',
|
|
|
|
// Dev dependencies
|
|
'@storybook/addon-a11y',
|
|
'@storybook/addon-actions',
|
|
'@storybook/addon-controls',
|
|
'@storybook/addon-interactions',
|
|
'@storybook/addon-jest',
|
|
'@storybook/addon-measure',
|
|
'@storybook/addon-toolbars',
|
|
'@storybook/addon-viewport',
|
|
'@storybook/addon-webpack5-compiler-swc',
|
|
'@storybook/csf',
|
|
'@storybook/preview-api',
|
|
'@storybook/react',
|
|
'@storybook/react-webpack5',
|
|
'@storybook/test',
|
|
'@storybook/test-runner',
|
|
'@storybook/types',
|
|
'storybook',
|
|
]);
|
|
|
|
/** @type {import("eslint").Rule.RuleModule} */
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
hasSuggestions: false,
|
|
fixable: false,
|
|
schema: [],
|
|
},
|
|
create(context) {
|
|
const { filename, sourceCode } = context;
|
|
|
|
let fileSuffix;
|
|
|
|
const nodeUses = [];
|
|
const domUses = [];
|
|
const preloadUses = [];
|
|
const mainUses = [];
|
|
|
|
const invalidUsesBySuffix = {
|
|
std: [nodeUses, domUses, preloadUses, mainUses],
|
|
node: [domUses, preloadUses, mainUses],
|
|
dom: [nodeUses, preloadUses, mainUses],
|
|
preload: [mainUses],
|
|
main: [domUses, preloadUses],
|
|
};
|
|
|
|
function trackLocalDep(node, source) {
|
|
if (!source.endsWith('.js')) {
|
|
return;
|
|
}
|
|
|
|
const match = source.match(/\.([^.\/]+)(?:\.stories)?\.js$/);
|
|
if (match == null) {
|
|
context.report({
|
|
node,
|
|
message: `Missing file suffix in ${source} import`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const [, depSuffix] = match;
|
|
if (depSuffix === 'node') {
|
|
nodeUses.push(node);
|
|
} else if (depSuffix === 'dom') {
|
|
domUses.push(node);
|
|
} else if (depSuffix === 'preload') {
|
|
preloadUses.push(node);
|
|
} else if (depSuffix === 'main') {
|
|
mainUses.push(node);
|
|
} else if (depSuffix === 'std') {
|
|
// Ignore
|
|
} else {
|
|
context.report({
|
|
node,
|
|
message:
|
|
`Unrecognized file suffix in ${source}, ` +
|
|
`expected: node/preload/main/std, found: ${depSuffix}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
function transformESMReference(node) {
|
|
if (
|
|
node.importKind === 'type' ||
|
|
(node.specifiers?.length &&
|
|
node.specifiers.every(x => x.importKind === 'type'))
|
|
) {
|
|
return;
|
|
}
|
|
if (!node.source) {
|
|
return;
|
|
}
|
|
if (node.source.type !== 'Literal') {
|
|
return;
|
|
}
|
|
const {
|
|
specifiers,
|
|
source: { value: source },
|
|
} = node;
|
|
|
|
if (source.startsWith('.')) {
|
|
trackLocalDep(node, source);
|
|
return;
|
|
}
|
|
|
|
// Node APIs
|
|
if (source.startsWith('node:')) {
|
|
nodeUses.push(node);
|
|
return;
|
|
}
|
|
|
|
// Electron
|
|
if (source === 'electron') {
|
|
for (const s of specifiers) {
|
|
// We implicitly skip:
|
|
// they are used in scripts
|
|
if (s.type === 'ImportSpecifier') {
|
|
if (MAIN_MODULES.has(s.imported.name)) {
|
|
mainUses.push(s);
|
|
} else if (RENDERER_MODULES.has(s.imported.name)) {
|
|
preloadUses.push(s);
|
|
}
|
|
} else if (s.type === 'ImportNamespaceSpecifier') {
|
|
// import * as electron from 'electron';
|
|
context.report({
|
|
node: s,
|
|
message: 'Unsupported namespace import specifier for electron',
|
|
});
|
|
nodeUses.push(s);
|
|
} else if (s.type === 'ImportDefaultSpecifier') {
|
|
// import ELECTRON_CLI from 'electron';
|
|
nodeUses.push(s);
|
|
} else {
|
|
context.report({
|
|
node: s,
|
|
message: 'Unsupported import specifier for electron',
|
|
});
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const [, moduleName] = source.match(/^([^@\/]+|@[^\/]+\/[^\/]+)/);
|
|
if (NODE_MODULES.has(moduleName)) {
|
|
nodeUses.push(node);
|
|
} else if (DOM_MODULES.has(moduleName) || source === 'react-dom/client') {
|
|
domUses.push(node);
|
|
}
|
|
}
|
|
|
|
return {
|
|
Program: node => {
|
|
if (filename.endsWith('.d.ts')) {
|
|
// Skip types
|
|
return;
|
|
}
|
|
|
|
const match = filename.match(/\.([^.\/]+)(?:\.stories)?\.(?:ts|tsx)$/);
|
|
if (match == null) {
|
|
context.report({
|
|
node: node,
|
|
message:
|
|
'Missing file suffix. Has to be one of: node/preload/main/std',
|
|
});
|
|
return;
|
|
}
|
|
|
|
fileSuffix = match[1];
|
|
},
|
|
'Program:exit': node => {
|
|
if (fileSuffix == null) {
|
|
return;
|
|
}
|
|
|
|
let expectedSuffix;
|
|
if (mainUses.length > 0) {
|
|
expectedSuffix = 'main';
|
|
} else if (preloadUses.length > 0) {
|
|
expectedSuffix = 'preload';
|
|
} else if (nodeUses.length > 0) {
|
|
if (domUses.length > 0) {
|
|
expectedSuffix = 'preload';
|
|
} else {
|
|
expectedSuffix = 'node';
|
|
}
|
|
} else if (domUses.length > 0) {
|
|
expectedSuffix = 'dom';
|
|
} else {
|
|
expectedSuffix = 'std';
|
|
}
|
|
|
|
// All .std.tsx components should be .dom.tsx for now
|
|
if (expectedSuffix === 'std' && filename.endsWith('.tsx')) {
|
|
expectedSuffix = 'dom';
|
|
}
|
|
|
|
if (fileSuffix !== expectedSuffix) {
|
|
context.report({
|
|
node,
|
|
message: `Invalid suffix ${fileSuffix}, expected: ${expectedSuffix}`,
|
|
});
|
|
}
|
|
|
|
const invalid = invalidUsesBySuffix[expectedSuffix].flat();
|
|
for (const use of invalid) {
|
|
context.report({
|
|
node: use,
|
|
message: `Invalid import/reference for suffix: ${expectedSuffix}`,
|
|
});
|
|
}
|
|
},
|
|
ImportDeclaration(node) {
|
|
transformESMReference(node);
|
|
},
|
|
ExportAllDeclaration(node) {
|
|
transformESMReference(node);
|
|
},
|
|
ExportNamedDeclaration(node) {
|
|
transformESMReference(node);
|
|
},
|
|
CallExpression(node) {
|
|
if (
|
|
node.callee.type !== 'Identifier' ||
|
|
node.callee.name !== 'require'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const scope = sourceCode.getScope(node);
|
|
const ref = scope.references.find(r => r.identifier === node.callee);
|
|
if (ref.resolved.scope.type !== 'global') {
|
|
return;
|
|
}
|
|
const { arguments: args } = node;
|
|
if (args.length !== 1) {
|
|
context.report({
|
|
node,
|
|
message: 'Invalid require() argument count',
|
|
});
|
|
return;
|
|
}
|
|
const [arg] = args;
|
|
|
|
let source;
|
|
if (arg.type === 'Literal') {
|
|
source = arg.value;
|
|
} else if (
|
|
arg.type === 'TSAsExpression' &&
|
|
arg.expression.type === 'Literal'
|
|
) {
|
|
source = arg.expression.value;
|
|
} else {
|
|
// Ignore other expressions
|
|
return;
|
|
}
|
|
|
|
// Keep local imports
|
|
if (source.startsWith('.')) {
|
|
trackLocalDep(node, source);
|
|
return;
|
|
}
|
|
|
|
// Node APIs
|
|
if (source.startsWith('node:')) {
|
|
nodeUses.push(node);
|
|
return;
|
|
}
|
|
|
|
// Electron
|
|
if (source === 'electron') {
|
|
context.report({
|
|
node,
|
|
message: 'CJS import of electron is not allowed',
|
|
});
|
|
}
|
|
|
|
const [, moduleName] = source.match(/^([^@\/]+|@[^\/]+\/[^\/]+)/);
|
|
if (NODE_MODULES.has(moduleName)) {
|
|
nodeUses.push(node);
|
|
} else if (
|
|
DOM_MODULES.has(moduleName) ||
|
|
source === 'react-dom/client'
|
|
) {
|
|
domUses.push(moduleName);
|
|
}
|
|
},
|
|
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) {
|
|
// Not part of expression
|
|
return;
|
|
}
|
|
if (ref.resolved.scope.type !== 'global') {
|
|
return;
|
|
}
|
|
domUses.push(node);
|
|
},
|
|
};
|
|
},
|
|
};
|