Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ ignores:
- '@metamask/permissions-kernel-snap'
# perps poc
- '@metamask/perps-controller'
# import aliases (not real packages)
- '~'

# files depcheck should not parse
ignorePatterns:
Expand Down
2 changes: 2 additions & 0 deletions .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ module.exports = {
config.node = {
__filename: true,
};
config.resolve.alias['~/ui'] = path.resolve(__dirname, '../ui');
config.resolve.alias['~/shared'] = path.resolve(__dirname, '../shared');
config.resolve.alias['webextension-polyfill'] = require.resolve(
'../ui/__mocks__/webextension-polyfill.js',
);
Expand Down
7 changes: 3 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"gitlens.advanced.blame.customArguments": [
"--ignore-revs-file .git-blame-ignore-revs"
],
"javascript.preferences.importModuleSpecifier": "relative",
"js/ts.preferences.importModuleSpecifier": "non-relative",
"js/ts.tsdk.path": "node_modules/typescript/lib",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid VS Code settings replace valid ones silently

Medium Severity

js/ts.preferences.importModuleSpecifier and js/ts.tsdk.path are not valid VS Code setting keys. VS Code uses separate namespaces: javascript.preferences.importModuleSpecifier, typescript.preferences.importModuleSpecifier, and typescript.tsdk. The removed settings were correct; the replacements will be silently ignored, so the intended "non-relative" import preference (the core DX goal of this PR) won't take effect, and the TypeScript SDK path is lost.

Fix in Cursor Fix in Web

"json.schemas": [
{
"fileMatch": ["app/manifest/*/*.json"],
Expand All @@ -27,7 +28,5 @@
],
"tailwindCSS.classFunctions": ["classnames", "classNames"],
"tailwindCSS.lint.cssConflict": "error",
"tailwindCSS.validate": true,
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.tsdk": "node_modules/typescript/lib"
"tailwindCSS.validate": true
}
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = function (api) {
},
],
plugins: [
path.resolve(__dirname, 'development/build/transforms/import-alias.js'),
// `browserify` is old and busted, and doesn't support `??=` (and other
// logical assignment operators). This plugin lets us target es2020-level
// browsers (except we do still end up with transpiled logical assignment
Expand Down
97 changes: 97 additions & 0 deletions development/build/transforms/import-alias.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const path = require('path');

Check warning on line 1 in development/build/transforms/import-alias.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=metamask-extension&issues=AZzQZ8cdfFO7fMyoHD4G&open=AZzQZ8cdfFO7fMyoHD4G&pullRequest=40695

const ROOT = path.resolve(__dirname, '../../..');

/**
* Mapping of import alias prefixes to directories (relative to project root).
* Add new aliases here to make them available across the codebase.
*/
const ALIASES = {
'~/ui': 'ui',
'~/shared': 'shared',
};

/**
* If `importSource` starts with a known alias, return the equivalent
* relative path from `filename`'s directory. Otherwise return null.
*
* @param {string} importSource - e.g. '~/shared/constants/network'
* @param {string} filename - absolute path of the file being compiled
* @returns {string | null} rewritten relative path or null
*/
function rewriteAlias(importSource, filename) {
for (const [alias, directory] of Object.entries(ALIASES)) {
if (importSource !== alias && !importSource.startsWith(`${alias}/`)) {
continue;
}

const rest = importSource.slice(alias.length);
const absoluteTarget = path.join(ROOT, directory, rest);
let relativePath = path
.relative(path.dirname(filename), absoluteTarget)
.split(path.sep)
.join('/');

if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}

return relativePath;
}

return null;
}

/**
* Babel plugin that rewrites `~/ui/...` and `~/shared/...` import aliases
* to relative paths. This allows browserify (which has no native alias
* support) to resolve them using standard Node module resolution.
*
* @returns {import('@babel/core').PluginObj} Babel plugin object
*/
module.exports = function importAliasPlugin() {
return {
visitor: {
// import X from '~/shared/...'
// export { X } from '~/shared/...'
// export * from '~/shared/...'
'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration'(
nodePath,
state,
) {
const { source } = nodePath.node;
if (!source) {
return;
}

const rewritten = rewriteAlias(source.value, state.filename);
if (rewritten) {
source.value = rewritten;
}
},

// require('~/shared/...')
CallExpression(nodePath, state) {
const { callee } = nodePath.node;
const arg = nodePath.node.arguments[0];

if (
callee.type !== 'Identifier' ||
callee.name !== 'require' ||
!arg ||
arg.type !== 'StringLiteral'

Check warning on line 82 in development/build/transforms/import-alias.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=metamask-extension&issues=AZzQZ8cdfFO7fMyoHD4H&open=AZzQZ8cdfFO7fMyoHD4H&pullRequest=40695
) {
return;
}

const rewritten = rewriteAlias(arg.value, state.filename);
if (rewritten) {
arg.value = rewritten;
}
},
},
};
};

module.exports.rewriteAlias = rewriteAlias;
module.exports.ALIASES = ALIASES;
126 changes: 126 additions & 0 deletions development/build/transforms/import-alias.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const path = require('path');
const { transformSync } = require('@babel/core');
const { rewriteAlias } = require('./import-alias');

const ROOT = path.resolve(__dirname, '../../..');

describe('import-alias babel plugin', () => {
function transform(code, filePath) {
const result = transformSync(code, {
filename: path.join(ROOT, filePath),
plugins: [require.resolve('./import-alias')],
parserOpts: { plugins: ['typescript'] },
configFile: false,
babelrc: false,
});
return result.code;
}

describe('rewriteAlias', () => {
it('rewrites ~/shared/ to a relative path', () => {
const filename = path.join(ROOT, 'app/scripts/migrations/183.ts');
const result = rewriteAlias('~/shared/constants/network', filename);
expect(result).toBe('../../../shared/constants/network');
});

it('rewrites ~/ui/ to a relative path', () => {
const filename = path.join(
ROOT,
'ui/components/multichain/activity-v2/hooks.ts',
);
const result = rewriteAlias('~/ui/hooks/useI18nContext', filename);
expect(result).toBe('../../../hooks/useI18nContext');
});

it('returns null for non-alias imports', () => {
const filename = path.join(ROOT, 'ui/components/foo.ts');
expect(rewriteAlias('./helpers', filename)).toBeNull();
expect(rewriteAlias('react', filename)).toBeNull();
expect(rewriteAlias('@metamask/utils', filename)).toBeNull();
});

it('does not match partial prefix like ~/shared-extra', () => {
const filename = path.join(ROOT, 'ui/components/foo.ts');
expect(rewriteAlias('~/shared-extra/foo', filename)).toBeNull();
});

it('handles bare alias without subpath', () => {
const filename = path.join(ROOT, 'app/scripts/background.js');
const result = rewriteAlias('~/shared', filename);
expect(result).toBe('../../shared');
});
});

describe('babel transform: import declarations', () => {
it('rewrites named import from ~/shared/', () => {
const code = `import { CHAIN_IDS } from '~/shared/constants/network';`;
const output = transform(
code,
'ui/components/multichain/activity-v2/hooks.ts',
);
expect(output).toContain(`from "../../../../shared/constants/network"`);
expect(output).not.toContain('~/');
});

it('rewrites default import from ~/ui/', () => {
const code = `import AssetPage from '~/ui/pages/asset/components/asset-page';`;
const output = transform(code, 'ui/components/multichain/foo.ts');
expect(output).toContain(
`from "../../pages/asset/components/asset-page"`,
);
expect(output).not.toContain('~/');
});

it('rewrites type import from ~/shared/', () => {
const code = `import type { Token } from '~/shared/lib/multichain/types';`;
const output = transform(
code,
'ui/components/multichain/activity-v2/hooks.ts',
);
expect(output).toContain(`"../../../../shared/lib/multichain/types"`);
});

it('does not touch non-alias imports', () => {
const code = [
`import React from 'react';`,
`import { Box } from '@metamask/design-system-react';`,
`import { foo } from './helpers';`,
`import { bar } from '../utils';`,
].join('\n');
const output = transform(code, 'ui/components/foo.ts');
expect(output).toContain(`react`);
expect(output).toContain(`@metamask/design-system-react`);
expect(output).toContain(`./helpers`);
expect(output).toContain(`../utils`);
expect(output).not.toContain('~/');
});
});

describe('babel transform: export declarations', () => {
it('rewrites export { X } from ~/shared/', () => {
const code = `export { CHAIN_IDS } from '~/shared/constants/network';`;
const output = transform(code, 'app/scripts/lib/util.ts');
expect(output).toContain(`from "../../../shared/constants/network"`);
});

it('rewrites export * from ~/ui/', () => {
const code = `export * from '~/ui/selectors';`;
const output = transform(code, 'ui/components/foo.ts');
expect(output).toContain(`from "../selectors"`);
});
});

describe('babel transform: require calls', () => {
it('rewrites require(~/shared/)', () => {
const code = `const { foo } = require('~/shared/lib/sentry');`;
const output = transform(code, 'app/scripts/migrations/183.ts');
expect(output).toContain(`require("../../../shared/lib/sentry")`);
});

it('does not touch non-alias require calls', () => {
const code = `const path = require('path');`;
const output = transform(code, 'app/scripts/lib/util.ts');
expect(output).toContain(`require('path')`);
});
});
});
2 changes: 2 additions & 0 deletions development/webpack/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ const config = {
context,
'../ui/__mocks__/perps/perps-controller',
),
'~/ui': join(context, '../ui'),
'~/shared': join(context, '../shared'),
},
// use `fallback` to redirect module requests when normal resolving fails,
// good for polyfill-ing built-in node modules that aren't available in
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module.exports = {
// Map @metamask/perps-controller to local mock
'^@metamask/perps-controller$':
'<rootDir>/ui/__mocks__/perps/perps-controller/index.ts',
'^~/ui/(.*)$': '<rootDir>/ui/$1',
'^~/shared/(.*)$': '<rootDir>/shared/$1',
},
// The path to the Prettier executable used to format snapshots
// Jest doesn't support Prettier 3 yet, so we use Prettier 2
Expand Down
2 changes: 2 additions & 0 deletions jest.integration.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ module.exports = {
// Map @metamask/perps-controller to local mock
'^@metamask/perps-controller$':
'<rootDir>/ui/__mocks__/perps/perps-controller/index.ts',
'^~/ui/(.*)$': '<rootDir>/ui/$1',
'^~/shared/(.*)$': '<rootDir>/shared/$1',
},
// The path to the Prettier executable used to format snapshots
// Jest doesn't support Prettier 3 yet, so we use Prettier 2
Expand Down
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"baseUrl": ".",
// Path mappings for module resolution
"paths": {
"@metamask/perps-controller": ["ui/__mocks__/perps/perps-controller"]
"@metamask/perps-controller": ["ui/__mocks__/perps/perps-controller"],
"~/ui/*": ["ui/*"],
"~/shared/*": ["shared/*"]
}
},
"exclude": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import { useSelector } from 'react-redux';
import { TransactionType } from '@metamask/transaction-controller';
import { toHex } from '@metamask/controller-utils';
import { Hex } from 'viem';
import type {
TransactionViewModel,
TransactionGroup,
} from '../../../../shared/lib/multichain/types';
import { TransactionDetailsModal as LegacyTransactionDetailsModal } from '../../../pages/confirmations/components/activity';
import { PAY_TRANSACTION_TYPES } from '../../../pages/confirmations/constants/pay';
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData';
import { getStatusKey } from '../../../helpers/utils/transactions.util';
import { formatDateWithYearContext } from '../../../helpers/utils/util';
import LegacyTransactionListItemDetails from '../../app/transaction-list-item-details';
import TransactionStatusLabel from '../../app/transaction-status-label/transaction-status-label';
import { getSelectedAddress } from '../../../selectors/selectors';
import { formatUnits } from '../../../../shared/lib/unit';
import { useBridgeActivityData } from '../../../hooks/bridge/useBridgeActivityData';
import { useGetTitle } from './hooks';
import { resolveTransactionType } from './helpers';
import type {
TransactionViewModel,
TransactionGroup,
} from '~/shared/lib/multichain/types';
import { TransactionDetailsModal as LegacyTransactionDetailsModal } from '~/ui/pages/confirmations/components/activity';
import { PAY_TRANSACTION_TYPES } from '~/ui/pages/confirmations/constants/pay';
import { useTransactionDisplayData } from '~/ui/hooks/useTransactionDisplayData';
import { getStatusKey } from '~/ui/helpers/utils/transactions.util';
import { formatDateWithYearContext } from '~/ui/helpers/utils/util';
import { getSelectedAddress } from '~/ui/selectors/selectors';
import { formatUnits } from '~/shared/lib/unit';
import { useBridgeActivityData } from '~/ui/hooks/bridge/useBridgeActivityData';

// eslint-disable-next-line no-empty-function
const noop = () => {};
Expand Down
8 changes: 4 additions & 4 deletions ui/components/multichain/activity-v2/activity-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { useSelector } from 'react-redux';
import { Box, Text, TextVariant } from '@metamask/design-system-react';
import { TransactionStatus } from '@metamask/transaction-controller';
import TransactionStatusLabel from '../../app/transaction-status-label/transaction-status-label';
import { useFormatters } from '../../../hooks/useFormatters';
import type { TransactionViewModel } from '../../../../shared/lib/multichain/types';
import { getCurrentCurrency } from '../../../ducks/metamask/metamask';
import { useBridgeActivityData } from '../../../hooks/bridge/useBridgeActivityData';
import { ChainBadge } from '../../app/chain-badge/chain-badge';
import { getPrimaryAmount } from './helpers';
import { useGetTitle, useFiatAmount } from './hooks';
import { ActivityTxIcon } from './activity-tx-icon';
import { useFormatters } from '~/ui/hooks/useFormatters';
import type { TransactionViewModel } from '~/shared/lib/multichain/types';
import { getCurrentCurrency } from '~/ui/ducks/metamask/metamask';
import { useBridgeActivityData } from '~/ui/hooks/bridge/useBridgeActivityData';

type Props = {
transaction: TransactionViewModel;
Expand Down
20 changes: 10 additions & 10 deletions ui/components/multichain/activity-v2/activity-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import { Box, Text } from '@metamask/design-system-react';
import type { Transaction } from '@metamask/keyring-api';
import { toEvmCaipChainId } from '@metamask/multichain-network-controller';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useScrollContainer } from '../../../contexts/scroll-container';
import { TransactionActivityEmptyState } from '../../app/transaction-activity-empty-state';
import { PENDING_STATUS_HASH } from '../../../helpers/constants/transactions';
import { selectLocalTransactions } from '../../../selectors/activity';
import { selectEvmAddress } from '../../../selectors/accounts';
import { selectCurrentAccountNonEvmTransactions } from '../../../selectors/multichain-transactions';
import { selectEnabledNetworksAsCaipChainIds } from '../../../selectors/multichain/networks';
import { useEarliestNonceByChain } from '../../../hooks/useEarliestNonceByChain';
import type { TransactionViewModel } from '../../../../shared/lib/multichain/types';
import { formatDateWithYearContext } from '../../../helpers/utils/util';
import AssetListControlBar from '../../app/assets/asset-list/asset-list-control-bar';
import {
mergeAllTransactionsByTime,
Expand All @@ -32,6 +22,16 @@ import { LocalActivityListItem } from './local-activity-list-item';
import { NonEvmActivityListItem } from './non-evm-activity-list-item';
import { NonEvmDetailsModal } from './non-evm-details-modal';
import { useTransactionsQuery } from './hooks';
import { useI18nContext } from '~/ui/hooks/useI18nContext';
import { useScrollContainer } from '~/ui/contexts/scroll-container';
import { PENDING_STATUS_HASH } from '~/ui/helpers/constants/transactions';
import { selectLocalTransactions } from '~/ui/selectors/activity';
import { selectEvmAddress } from '~/ui/selectors/accounts';
import { selectCurrentAccountNonEvmTransactions } from '~/ui/selectors/multichain-transactions';
import { selectEnabledNetworksAsCaipChainIds } from '~/ui/selectors/multichain/networks';
import { useEarliestNonceByChain } from '~/ui/hooks/useEarliestNonceByChain';
import type { TransactionViewModel } from '~/shared/lib/multichain/types';
import { formatDateWithYearContext } from '~/ui/helpers/utils/util';

const ITEM_HEIGHT = 70;
const HEADER_HEIGHT = 36;
Expand Down
Loading
Loading