diff --git a/.oxlint-plugins/metamask-core.ts b/.oxlint-plugins/metamask-core.ts new file mode 100644 index 00000000000..a026de6421b --- /dev/null +++ b/.oxlint-plugins/metamask-core.ts @@ -0,0 +1,24 @@ +import { eslintCompatPlugin } from '@oxlint/plugins'; + +import { noBarrelFiles } from './rules/no-barrel-files.ts'; +import { noWildcardExports } from './rules/no-wildcard-exports.ts'; + +/** + * Oxlint plugin containing custom rules for this monorepo. + * + * This is loaded via `jsPlugins` in `oxlint.config.ts`, and its rules are + * referenced there with the `metamask-core/` prefix. + */ +// We use `eslintCompatPlugin` as it's more performant (according to the Oxlint +// documentation, anyway). +const plugin = eslintCompatPlugin({ + meta: { + name: 'metamask-core', + }, + rules: { + 'no-wildcard-exports': noWildcardExports, + 'no-barrel-files': noBarrelFiles, + }, +}); + +export default plugin; diff --git a/.oxlint-plugins/rules/__fixtures__/packages/with-subpath-exports/package.json b/.oxlint-plugins/rules/__fixtures__/packages/with-subpath-exports/package.json new file mode 100644 index 00000000000..9b3214938f3 --- /dev/null +++ b/.oxlint-plugins/rules/__fixtures__/packages/with-subpath-exports/package.json @@ -0,0 +1,24 @@ +{ + "name": "@metamask/with-subpath-exports", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./v2": { + "import": { + "types": "./dist/v2/index.d.mts", + "default": "./dist/v2/index.mjs" + }, + "require": { + "types": "./dist/v2/index.d.cts", + "default": "./dist/v2/index.cjs" + } + }, + "./utils/*": { + "types": "./dist/utils/*.d.ts", + "default": "./dist/utils/*.js" + }, + "./package.json": "./package.json" + } +} diff --git a/.oxlint-plugins/rules/__fixtures__/packages/without-exports/package.json b/.oxlint-plugins/rules/__fixtures__/packages/without-exports/package.json new file mode 100644 index 00000000000..229d4d84142 --- /dev/null +++ b/.oxlint-plugins/rules/__fixtures__/packages/without-exports/package.json @@ -0,0 +1,4 @@ +{ + "name": "@metamask/without-exports", + "main": "./dist/index.js" +} diff --git a/.oxlint-plugins/rules/constants.ts b/.oxlint-plugins/rules/constants.ts new file mode 100644 index 00000000000..103ced9a536 --- /dev/null +++ b/.oxlint-plugins/rules/constants.ts @@ -0,0 +1,6 @@ +/** + * The location of the guidelines that explain how to resolve violations of + * the rules in this plugin. + */ +export const PACKAGE_GUIDELINES_PATH = + 'docs/code-guidelines/package-guidelines.md'; diff --git a/.oxlint-plugins/rules/no-barrel-files.test.ts b/.oxlint-plugins/rules/no-barrel-files.test.ts new file mode 100644 index 00000000000..070b3198c8c --- /dev/null +++ b/.oxlint-plugins/rules/no-barrel-files.test.ts @@ -0,0 +1,112 @@ +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { RuleTester } from 'oxlint/plugins-dev'; + +import { noBarrelFiles } from './no-barrel-files.ts'; + +const FIXTURES_DIRECTORY = fileURLToPath( + new URL('__fixtures__', import.meta.url), +); + +// This is a bit strange but is recommended. +// See: https://oxc.rs/docs/guide/usage/linter/writing-js-plugins.html#writing-tests-for-custom-rules +RuleTester.describe = describe; +RuleTester.it = it; + +const ruleTester = new RuleTester({ + cwd: FIXTURES_DIRECTORY, + languageOptions: { + sourceType: 'module', + }, +}); + +ruleTester.run('no-barrel-files', noBarrelFiles, { + valid: [ + { + name: 'the root index file of a package', + filename: getFixturePath('packages/with-subpath-exports/src/index.ts'), + code: "export { foo } from './foo';", + }, + { + name: 'an index file that is the entrypoint for a subpath export', + filename: getFixturePath('packages/with-subpath-exports/src/v2/index.ts'), + code: "export { foo } from './foo';", + }, + { + name: 'a non-index file in a subdirectory', + filename: getFixturePath( + 'packages/with-subpath-exports/src/module/foo.ts', + ), + code: 'export const foo = 1;', + }, + { + name: 'a file matching a wildcard subpath export', + filename: getFixturePath( + 'packages/with-subpath-exports/src/utils/index.ts', + ), + code: 'export const foo = 1;', + }, + { + name: 'an index file outside of a package source directory', + filename: getFixturePath('scripts/module/index.ts'), + code: 'export const foo = 1;', + }, + { + name: 'an index file in a package without an `exports` field at its root', + filename: getFixturePath('packages/without-exports/src/index.ts'), + code: 'export const foo = 1;', + }, + ], + + invalid: [ + { + name: 'an index file in a subdirectory of a package', + filename: getFixturePath( + 'packages/with-subpath-exports/src/module/index.ts', + ), + code: "export { foo } from './foo';", + errors: [{ messageId: 'noSubpathIndexFile' }], + }, + { + name: 'an index file nested several levels deep', + filename: getFixturePath( + 'packages/with-subpath-exports/src/v2/module/index.ts', + ), + code: "export { foo } from './foo';", + errors: [{ messageId: 'noSubpathIndexFile' }], + }, + { + name: 'an index file whose subdirectory is not exported', + filename: getFixturePath('packages/without-exports/src/module/index.ts'), + code: "export { foo } from './foo';", + errors: [{ messageId: 'noSubpathIndexFile' }], + }, + { + name: 'an index file with an extension other than `.ts`', + filename: getFixturePath( + 'packages/with-subpath-exports/src/module/index.mts', + ), + code: "export { foo } from './foo';", + errors: [{ messageId: 'noSubpathIndexFile' }], + }, + { + name: 'an index file with no contents', + filename: getFixturePath( + 'packages/with-subpath-exports/src/module/index.ts', + ), + code: '', + errors: [{ messageId: 'noSubpathIndexFile' }], + }, + ], +}); + +/** + * Builds an absolute path to a file within the fixtures directory. + * + * @param relativePath - The path to the file, relative to the fixtures + * directory. + * @returns The absolute path. + */ +function getFixturePath(relativePath: string): string { + return path.join(FIXTURES_DIRECTORY, relativePath); +} diff --git a/.oxlint-plugins/rules/no-barrel-files.ts b/.oxlint-plugins/rules/no-barrel-files.ts new file mode 100644 index 00000000000..69083a16117 --- /dev/null +++ b/.oxlint-plugins/rules/no-barrel-files.ts @@ -0,0 +1,195 @@ +import { defineRule } from '@oxlint/plugins'; +import type { VisitorWithHooks } from '@oxlint/plugins'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { PACKAGE_GUIDELINES_PATH } from './constants.ts'; + +/** + * Matches the name of an index file, such as `index.ts` or `index.mjs`. + */ +const INDEX_FILE_NAME_PATTERN = /^index\.[cm]?[jt]sx?$/u; + +/** + * Matches the extension of a built file that an `exports` target may point + * to, such as `.js` or `.d.mts`. + */ +const BUILT_FILE_EXTENSION_PATTERN = /(?:\.d)?\.[cm]?js$|\.d\.[cm]?ts$/u; + +/** + * The prefix that `exports` targets for built files begin with. + */ +const BUILT_FILE_PREFIX = './dist/'; + +/** + * Entrypoints for each package, keyed by the path to the package directory. + * + * Reading and parsing `package.json` for every linted file would be wasteful, + * so each package's entrypoints are computed once per lint run. + */ +const entrypointPatternsByPackageDirectory = new Map(); + +/** + * Rule that bans `index` files in subdirectories of a package's `src/` + * directory, unless they serve as an entrypoint for a subpath export. + */ +export const noBarrelFiles = defineRule({ + meta: { + type: 'problem', + docs: { + description: + 'Disallow "barrel" files in subdirectories of a package, except for subpath entrypoints.', + }, + schema: [], + messages: { + noSubpathIndexFile: `"Barrel" files are not allowed in subdirectories of a package unless they are entrypoints listed under \`exports\` in \`package.json\`. Files like these hide the public surface area of a package. Export each symbol by name from the package's root \`index.ts\` instead. Learn more: ${PACKAGE_GUIDELINES_PATH}.`, + }, + }, + + createOnce(context): VisitorWithHooks { + return { + Program(node) { + if (isDisallowedIndexFile(context.filename)) { + context.report({ node, messageId: 'noSubpathIndexFile' }); + } + }, + }; + }, +}); + +/** + * Determines whether the given file is an index file that is neither the + * root index file of a package nor an entrypoint for a subpath export. + * + * @param filePath - The absolute path to the file being linted. + * @returns True if the file should be reported, false otherwise. + */ +function isDisallowedIndexFile(filePath: string): boolean { + if (!INDEX_FILE_NAME_PATTERN.test(path.basename(filePath))) { + return false; + } + + const packageDirectory = findPackageDirectory(filePath); + if (packageDirectory === null) { + return false; + } + + const sourcePath = toPosixPath( + path.relative(path.join(packageDirectory, 'src'), filePath), + ); + if (!sourcePath.includes('/')) { + return false; + } + + const sourcePathWithoutExtension = sourcePath.replace(/\.[^./]+$/u, ''); + return !getEntrypointPatterns(packageDirectory).some((pattern) => + pattern.test(sourcePathWithoutExtension), + ); +} + +/** + * Finds the package that the given file belongs to, as long as the file lives + * within the package's `src/` directory. + * + * @param filePath - The absolute path to the file being linted. + * @returns The absolute path to the package directory, or null if the file is + * not within the `src/` directory of a package. + */ +function findPackageDirectory(filePath: string): string | null { + let directory = path.dirname(filePath); + + while (directory !== path.dirname(directory)) { + const parentDirectory = path.dirname(directory); + if ( + path.basename(directory) === 'src' && + fs.existsSync(path.join(parentDirectory, 'package.json')) + ) { + return parentDirectory; + } + directory = parentDirectory; + } + + return null; +} + +/** + * Gets patterns that match the source files (relative to `src/`, without + * extensions) which serve as entrypoints for the given package. + * + * @param packageDirectory - The absolute path to the package directory. + * @returns The entrypoint patterns. + */ +function getEntrypointPatterns(packageDirectory: string): RegExp[] { + const cachedPatterns = + entrypointPatternsByPackageDirectory.get(packageDirectory); + if (cachedPatterns) { + return cachedPatterns; + } + + const manifest: unknown = JSON.parse( + fs.readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'), + ); + const exportsField = + typeof manifest === 'object' && manifest !== null && 'exports' in manifest + ? manifest.exports + : undefined; + const patterns = collectExportTargets(exportsField) + .filter((target) => target.startsWith(BUILT_FILE_PREFIX)) + .map((target) => + convertToSourcePattern( + target + .slice(BUILT_FILE_PREFIX.length) + .replace(BUILT_FILE_EXTENSION_PATTERN, ''), + ), + ); + + entrypointPatternsByPackageDirectory.set(packageDirectory, patterns); + return patterns; +} + +/** + * Collects every file path that an `exports` field in `package.json` points + * to, however deeply nested its conditions are. + * + * @param exportsValue - The `exports` field, or a value nested within it. + * @returns The file paths. + */ +function collectExportTargets(exportsValue: unknown): string[] { + if (typeof exportsValue === 'string') { + return [exportsValue]; + } + + if (typeof exportsValue === 'object' && exportsValue !== null) { + return Object.values(exportsValue).flatMap(collectExportTargets); + } + + return []; +} + +/** + * Converts a path from an `exports` target, which may contain a `*` + * wildcard, into a pattern that matches source paths. + * + * @param targetPath - The path from the `exports` target, relative to `dist/` + * and without an extension. + * @returns The pattern. + */ +function convertToSourcePattern(targetPath: string): RegExp { + const escapedPath = targetPath + .split('*') + .map((segment) => segment.replace(/[.+?^${}()|[\]\\]/gu, '\\$&')) + .join('.+'); + return new RegExp(`^${escapedPath}$`, 'u'); +} + +/** + * Converts a path that uses the separator for the current platform into one + * that uses forward slashes, so that it can be compared with paths in + * `package.json`. + * + * @param filePath - The path to convert. + * @returns The converted path. + */ +function toPosixPath(filePath: string): string { + return filePath.split(path.sep).join('/'); +} diff --git a/.oxlint-plugins/rules/no-wildcard-exports.test.ts b/.oxlint-plugins/rules/no-wildcard-exports.test.ts new file mode 100644 index 00000000000..f80bfd603d3 --- /dev/null +++ b/.oxlint-plugins/rules/no-wildcard-exports.test.ts @@ -0,0 +1,62 @@ +import { RuleTester } from 'oxlint/plugins-dev'; + +import { noWildcardExports } from './no-wildcard-exports.ts'; + +// This is a bit strange but is recommended. +// See: https://oxc.rs/docs/guide/usage/linter/writing-js-plugins.html#writing-tests-for-custom-rules +RuleTester.describe = describe; +RuleTester.it = it; + +const ruleTester = new RuleTester({ + languageOptions: { + sourceType: 'module', + parserOptions: { lang: 'ts' }, + }, +}); + +ruleTester.run('no-wildcard-exports', noWildcardExports, { + valid: [ + { + name: 'a named re-export', + code: "export { Foo } from './foo';", + }, + { + name: 'a type-only named re-export', + code: "export type { Foo } from './foo';", + }, + { + name: 'a local named export', + code: 'export const foo = 1;', + }, + { + name: 'a manually built grouping object exported by name', + code: [ + "import { FOO, BAR } from './constants';", + 'export const Constants = { FOO, BAR };', + ].join('\n'), + }, + ], + + invalid: [ + { + name: 'a wildcard re-export', + code: "export * from './foo';", + errors: [{ messageId: 'noNamedWildcardExport' }], + }, + { + name: 'a type-only wildcard re-export', + code: "export type * from './foo';", + errors: [{ messageId: 'noNamedWildcardExport' }], + }, + { + name: 'a namespace re-export', + code: "export * as Foo from './foo';", + errors: [{ messageId: 'noNamespaceExport' }], + }, + { + name: 'a type-only namespace re-export', + code: "export type * as Foo from './foo';", + errors: [{ messageId: 'noNamespaceExport' }], + }, + ], +}); diff --git a/.oxlint-plugins/rules/no-wildcard-exports.ts b/.oxlint-plugins/rules/no-wildcard-exports.ts new file mode 100644 index 00000000000..4287c09cadf --- /dev/null +++ b/.oxlint-plugins/rules/no-wildcard-exports.ts @@ -0,0 +1,35 @@ +import { defineRule } from '@oxlint/plugins'; +import type { VisitorWithHooks } from '@oxlint/plugins'; + +import { PACKAGE_GUIDELINES_PATH } from './constants.ts'; + +/** + * Rule that bans wildcard exports (`export * from` and `export * as Ns from`, + * along with their type-only variants). + */ +export const noWildcardExports = defineRule({ + meta: { + type: 'problem', + docs: { + description: + 'Disallow wildcard exports (`export * from` and `export * as Ns from`).', + }, + schema: [], + messages: { + noNamedWildcardExport: `Named wildcard exports are not allowed. These kinds of exports hide the public surface area of a module and make it easy to expose symbols to consumers by accident. Name each export explicitly instead. Learn more: ${PACKAGE_GUIDELINES_PATH}.`, + noNamespaceExport: `Namespace re-exports are not allowed. These kinds of exports hide the public surface area of a module and make it easy to expose symbols to consumers by accident. Build the grouping object explicitly and export it by name, and export types individually by name. Learn more: ${PACKAGE_GUIDELINES_PATH}.`, + }, + }, + + createOnce(context): VisitorWithHooks { + return { + ExportAllDeclaration(node) { + const messageId = + node.exported === null + ? 'noNamedWildcardExport' + : 'noNamespaceExport'; + context.report({ node, messageId }); + }, + }; + }, +}); diff --git a/AGENTS.md b/AGENTS.md index 036e9b5ad9f..13369fdc494 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,7 @@ The monorepo uses a hierarchical configuration approach for different tools. For #### Oxlint - `oxlint.config.ts` configures Oxlint for the entire monorepo. +- `.oxlint-plugins/` defines custom Oxlint rules for the monorepo (e.g. `metamask-core/no-wildcard-exports`, `metamask-core/no-subpath-index-files`), which are loaded via `jsPlugins` in `oxlint.config.ts`. Tests for these rules run via `yarn test:scripts`. - `oxlint-suppressions.json` isn't a config file per se, but defines Oxlint errors that are being ignored (temporarily). #### Oxfmt @@ -173,7 +174,8 @@ Use `yarn create-package --name --description ` to add a new ### General package guidelines - Each package should have an `index.ts` file in `src/` that explicitly lists all exports. -- Avoid barrel exports (`export * from './file'`).ts`. Instead, explicitly name each export: +- Don't create `index.ts` files ("barrel" files) in subdirectories of `src/`. Export each symbol by name from `src/index.ts` instead. The only exception is an `index.ts` file that is an entrypoint for a subpath listed under `exports` in `package.json` (e.g. `src/v2/index.ts` for `"./v2"`). +- Avoid barrel exports (`export * from './file'`). Instead, explicitly name each export: ```typescript // Bad @@ -184,6 +186,21 @@ Use `yarn create-package --name --description ` to add a new export type { FooControllerMessenger } from './foo-controller'; ``` +- Avoid namespace re-exports (`export * as Ns from './file'`). To group values under one name, import them, build the object yourself, and export it by name. Export types by name individually: + + ```typescript + // Bad + export * as Constants from './constants'; + + // Good + import { FOO, BAR } from './constants'; + + export const Constants = { FOO, BAR }; + export type { FooType } from './constants'; + ``` + +- These guidelines are enforced by the `metamask-core/no-subpath-index-files` and `metamask-core/no-wildcard-exports` lint rules. See `docs/code-guidelines/package-guidelines.md` for more. + ### Controllers When adding or updating controllers in packages, follow these guidelines: diff --git a/docs/code-guidelines/package-guidelines.md b/docs/code-guidelines/package-guidelines.md index 75b26215e8c..12abad24d2c 100644 --- a/docs/code-guidelines/package-guidelines.md +++ b/docs/code-guidelines/package-guidelines.md @@ -1,32 +1,207 @@ # Guidelines for packages -## List exports explicitly +## Use clear filenames and paths for entrypoints -Every package in this monorepo should have an `index.ts` file in the `src/` directory. Any symbols that this file exports will be usable by consumers. +Library packages may have one or more entrypoints as defined in the `exports` field of `package.json`. -It is tempting to save time by re-exporting all symbols from one or many files by using the "wildcard" or ["barrel"](https://basarat.gitbook.io/typescript/main-1/barrel) export syntax: +The name of the root entrypoint — the file loaded when the package is imported by its name, without specifying any subpaths — should be called `src/index.ts`. + +The expected name of a non-root entrypoint depends on its nature: + +- If that entrypoint represents a distinct submodule of the package whose code lives in its own subdirectory of `src/`, then the file should be called `index.ts` and be located in that subdirectory. The subdirectory should be named after the subpath (e.g. `./one` → `src/one/index.ts`). +- Otherwise, the file should be named after the subpath and be located directly in `src/` (e.g. `./node` → `src/node.ts`). + +Subpaths may be nested, in which case the same rules apply to each segment (e.g. `./one/mocks` → `src/one/mocks/index.ts`). + +Do not use wildcards when defining patterns for entrypoints (e.g. `"./utils/*": "./dist/utils/*.js"`). These expose every file in a directory to consumers, which has [the same drawbacks as wildcard exports](#name-all-exports-explicitly-instead-of-grouping-them-together). + +### Examples + +Given two packages: + +- `@metamask/foo` has two entrypoints: the root and `./node`. All files that both entrypoints import are located directly in `src/`, but `./node` imports an extra file that the root doesn't. +- `@metamask/bar` has three entrypoints: the root, `./one`, and `./two`. The latter two represent distinct submodules within the package. + +Then this is the expected layout of both packages: + +``` +packages/ + foo/ + src/ + index.ts # Imports one.ts, two.ts + node.ts # Imports one.ts, two.ts, three.ts + one.ts + three.ts + two.ts + package.json + bar/ + src/ + one/ + index.ts # Imports other files within one/ + two/ + index.ts # Imports other files within two/ + index.ts # Imports individual files within one/ and two/ (not their index.ts files) + package.json +``` + +Here is what the `package.json` for `foo` should look like: + +```json +{ + "name": "@metamask/foo", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./node": { + "types": "./dist/node.d.ts", + "default": "./dist/node.js" + }, + "./package.json": "./package.json" + } +} +``` + +And the `package.json` for `bar` should look like this: + +```json +{ + "name": "@metamask/bar", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./one": { + "types": "./dist/one/index.d.ts", + "default": "./dist/one/index.js" + }, + "./two": { + "types": "./dist/two/index.d.ts", + "default": "./dist/two/index.js" + }, + "./package.json": "./package.json" + } +} +``` + +## Do not use "barrel" files for non-entrypoints + +No `index.ts` file should exist at any subdirectory within the package, except [those that act as entrypoints for subpath exports](#use-clear-filenames-and-paths-for-entrypoints) + +Instead, move all exports to an entrypoint and [name all exports explicitly](#name-all-exports-explicitly-instead-of-grouping-them-together). + +The examples below assume the following `package.json`: + +```json +{ + "name": "@metamask/foo", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + } +} +``` 🚫 ```typescript -export * from './foo-controller'; -export * from './foo-service'; -``` +// packages/foo/src/module-one/index.ts + +export { foo, bar, baz } from './first'; +export { qux } from './second'; + +// packages/foo/src/module-two/index.ts -However, using this syntax is not advised for the following reasons: +export { fizz, buzz } from './first'; -- Barrel exports make it difficult to understand the public surface area of a package at a glance. This is nice for general development, but is especially important for debugging when sorting through previously published versions of the package on a site such as `npmfs.com`. -- Any time a new export is added to one of these files, it will automatically become an export of the package. That may sound like a benefit, but this makes it very easy to increase the surface area of the package without knowing it. -- Sometimes it is useful to export a symbol from a file for testing purposes but not expose it publicly to consumers. With barrel exports, however, this is impossible. +// packages/foo/src/index.ts -Instead of using barrel exports, name every export explicitly: +export * as ModuleOne from './module-one'; +export * as ModuleTwo from './module-two'; +``` ✅ ```typescript +// packages/foo/src/index.ts + +// Module One +export { foo, bar, baz } from './module-one/first'; +export { qux } from './module-one/second'; + +// Module Two +export { fizz, buzz } from './module-two/first'; +``` + +
+Read more + +It is tempting to divide a large package into smaller modules and use an `index.ts` file (otherwise known as a "barrel" file) to re-export all symbols in one module to use in another. Usually this is combined with a namespace re-export in the root `index.ts` of the package. + +However, re-exporting symbols in this manner is not advised for the following reasons: + +- It makes it difficult to view the public surface area of a package at a glance, which is useful for security auditing purposes. +- It makes it difficult to notice new additions to the surface area. Any time a new export is added to one of these files, it will automatically become an export of the package, so it could be easily missed in a review (and fail to be added to the changelog). +- It makes it impossible to export a symbol from a file but not expose it publicly to consumers. This can be useful for testing purposes. +- It can slow down static analysis tools (TypeScript, linting tools, etc.) because it increases the number of paths it takes to reach a file. (You can read more about this here: https://www.atlassian.com/blog/how-we-build/faster-builds-when-removing-barrel-files) +
+ +## Name all exports explicitly instead of grouping them together + +All exports in an [entrypoint](#use-clear-filenames-and-paths-for-entrypoints) should be named explicitly; symbols should not be re-exported from other files using wildcards. + +🚫 **Re-exporting all named exports from a file** + +```typescript +// index.ts +export * from './foo-controller'; +export * from './foo-service'; +``` + +✅ **Re-exporting each named export individually** + +```typescript +// index.ts export { FooController } from './foo-controller'; export type { FooControllerMessenger } from './foo-controller'; export { FooService } from './foo-service'; -export { FooService } from './foo-service'; export type { AbstractFooService } from './foo-service'; ``` + +🚫 **Grouping exports under a namespace automatically** + +```typescript +// index.ts +export * as Constants from './constants'; +export type * as Types from './types'; +``` + +✅ **Grouping values under a namespace manually, and exporting types individually** + +(Note that types cannot be grouped together at all. Export each type by name instead.) + +```typescript +// index.ts + +import { FOO, BAR } from './constants'; + +export const Constants = { FOO, BAR }; +export type { FooType, BarType } from './types'; +``` + +
+Read more + +It is tempting to save time by re-exporting all symbols from a directory _en masse_ by using wildcard exports. + +However, re-exporting symbols in this manner is not advised for the following reasons: + +- It makes it difficult to view the public surface area of a package at a glance, which is useful for security auditing purposes. +- It makes it difficult to notice new additions to the surface area. Any time a new export is added to one of these files, it will automatically become an export of the package, so it could be easily missed in a review (and fail to be added to the changelog). +- It makes it impossible to export a symbol from a file but not expose it publicly to consumers. This can be useful for testing purposes. +
diff --git a/jest.config.scripts.cjs b/jest.config.scripts.cjs index 505c9194645..0f6f2c3acce 100644 --- a/jest.config.scripts.cjs +++ b/jest.config.scripts.cjs @@ -8,13 +8,21 @@ module.exports = { collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['/scripts/**/*.ts'], + collectCoverageFrom: [ + '/.oxlint-plugins/**/*.ts', + '/scripts/**/*.ts', + ], // The directory where Jest should output its coverage files coverageDirectory: '/scripts/coverage', // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['/package-template/'], + coveragePathIgnorePatterns: [ + // The plugin entrypoint only registers rules, so there is no logic to + // test; each rule is tested on its own. + '/.oxlint-plugins/metamask-core.ts', + '/package-template/', + ], // Indicates which provider should be used to instrument code for coverage coverageProvider: 'babel', @@ -25,6 +33,12 @@ module.exports = { // An object that configures minimum threshold enforcement for coverage results // does not work here. coverageThreshold: { + './.oxlint-plugins/**/*.ts': { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, './scripts/create-package/**/*.ts': { branches: 100, functions: 100, @@ -77,6 +91,7 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ + '/.oxlint-plugins/**/?(*.)+(spec|test).[tj]s?(x)', '/scripts/**/__tests__/**/*.[jt]s?(x)', '/scripts/**/?(*.)+(spec|test).[tj]s?(x)', ], diff --git a/knip.config.mts b/knip.config.mts index d274485f843..d5010aafcec 100644 --- a/knip.config.mts +++ b/knip.config.mts @@ -4,12 +4,18 @@ const config: KnipConfig = { workspaces: { '.': { entry: [ - 'scripts/**/*.{ts,js,sh}', - 'tests/**/*.ts', '*.config.{js,cjs,mjs,ts}', + '.oxlint-plugins/**/*.ts', '.prettierrc.cjs', + 'scripts/**/*.{ts,js,sh}', + 'tests/**/*.ts', + ], + project: [ + '*.{js,cjs,mjs,ts}', + '.oxlint-plugins/**/*.ts', + 'scripts/**/*.ts', + 'tests/**/*.ts', ], - project: ['scripts/**/*.ts', 'tests/**/*.ts', '*.{js,cjs,mjs,ts}'], ignore: ['scripts/create-package/package-template/**'], // The `preset: 'ts-jest'` shorthand expands to `ts-jest/jest-preset`, // which knip can't resolve to an actual file even though `ts-jest` is diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index efbd8787270..0acd46a560d 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -64,11 +64,27 @@ "count": 2 } }, + "packages/account-tree-controller/src/backup-and-sync/analytics/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/account-tree-controller/src/backup-and-sync/analytics/traces.test.ts": { "typescript/no-unsafe-assignment": { "count": 2 } }, + "packages/account-tree-controller/src/backup-and-sync/authentication/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/account-tree-controller/src/backup-and-sync/service/index.test.ts": { "no-useless-return": { "count": 1 @@ -90,6 +106,9 @@ } }, "packages/account-tree-controller/src/backup-and-sync/service/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, "typescript/explicit-function-return-type": { "count": 1 }, @@ -105,6 +124,14 @@ "count": 7 } }, + "packages/account-tree-controller/src/backup-and-sync/syncing/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 4 + } + }, "packages/account-tree-controller/src/backup-and-sync/syncing/legacy.test.ts": { "typescript/unbound-method": { "count": 7 @@ -141,6 +168,14 @@ "count": 3 } }, + "packages/account-tree-controller/src/backup-and-sync/user-storage/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 4 + } + }, "packages/account-tree-controller/src/backup-and-sync/user-storage/network-operations.test.ts": { "no-throw-literal": { "count": 4 @@ -183,6 +218,14 @@ "count": 3 } }, + "packages/account-tree-controller/src/backup-and-sync/utils/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/account-tree-controller/src/group.ts": { "typescript/no-unsafe-call": { "count": 1 @@ -379,6 +422,9 @@ } }, "packages/advanced-chart-core/src/features/indicators/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, "no-shadow": { "count": 6 }, @@ -408,6 +454,9 @@ } }, "packages/advanced-chart-core/src/features/volume/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, "unicorn/no-useless-undefined": { "count": 1 } @@ -438,6 +487,11 @@ "count": 1 } }, + "packages/advanced-chart-core/src/overlays/focusTime/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/advanced-chart-core/src/overlays/positionLines/__tests__/positionLines.test.ts": { "typescript/no-unsafe-assignment": { "count": 1 @@ -452,6 +506,9 @@ } }, "packages/advanced-chart-core/src/overlays/positionLines/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, "unicorn/no-useless-undefined": { "count": 3 } @@ -461,6 +518,11 @@ "count": 2 } }, + "packages/advanced-chart-core/src/overlays/socialLeaderboard/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/advanced-chart-core/src/overlays/tradeMarkers/__tests__/animation.test.ts": { "typescript/no-unnecessary-type-assertion": { "count": 1 @@ -478,6 +540,9 @@ } }, "packages/advanced-chart-core/src/overlays/tradeMarkers/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, "no-shadow": { "count": 1 } @@ -697,6 +762,11 @@ "count": 6 } }, + "packages/approval-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/assets-controller/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -762,6 +832,16 @@ "count": 4 } }, + "packages/assets-controller/src/__fixtures__/bsc-spam-token/api-responses/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/__fixtures__/scam-token-cleanup/api-responses/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/__fixtures__/scam-token-cleanup/captureTokenApiResponses.ts": { "typescript/no-unsafe-call": { "count": 2 @@ -772,6 +852,16 @@ "count": 1 } }, + "packages/assets-controller/src/__fixtures__/stale-balance/api-responses/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/__fixtures__/ws-price-updates/api-responses/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/data-sources/AccountActivityDataSource.test.ts": { "typescript/no-unsafe-assignment": { "count": 6 @@ -883,6 +973,16 @@ "count": 1 } }, + "packages/assets-controller/src/data-sources/evm-rpc-services/clients/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/evm-rpc-services/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/data-sources/evm-rpc-services/services/BalanceFetcher.test.ts": { "typescript/no-unsafe-assignment": { "count": 1 @@ -918,6 +1018,26 @@ "count": 13 } }, + "packages/assets-controller/src/data-sources/evm-rpc-services/services/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/evm-rpc-services/types/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/evm-rpc-services/utils/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controller/src/data-sources/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/middlewares/CustomAssetGraduationMiddleware.test.ts": { "typescript/no-unsafe-member-access": { "count": 1 @@ -931,6 +1051,11 @@ "count": 2 } }, + "packages/assets-controller/src/middlewares/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/migrations/healAssetsInfoMetadata.test.ts": { "typescript/no-unsafe-assignment": { "count": 2 @@ -946,6 +1071,11 @@ "count": 1 } }, + "packages/assets-controller/src/pipeline/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/selectors/balance.test.ts": { "no-useless-return": { "count": 1 @@ -962,6 +1092,11 @@ "count": 1 } }, + "packages/assets-controller/src/utils/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controller/src/utils/native-assets.test.ts": { "typescript/no-unsafe-return": { "count": 1 @@ -1116,6 +1251,11 @@ "count": 2 } }, + "packages/assets-controllers/src/MultichainAssetsController/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/MultichainAssetsRatesController/MultichainAssetsRatesController.test.ts": { "typescript/no-unsafe-assignment": { "count": 6 @@ -1129,6 +1269,11 @@ "count": 1 } }, + "packages/assets-controllers/src/MultichainAssetsRatesController/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts": { "typescript/explicit-function-return-type": { "count": 1 @@ -1142,6 +1287,11 @@ "count": 1 } }, + "packages/assets-controllers/src/MultichainBalancesController/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/NftController.test.ts": { "id-denylist": { "count": 1 @@ -1206,6 +1356,11 @@ "count": 1 } }, + "packages/assets-controllers/src/RatesController/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/Standards/ERC20Standard.test.ts": { "jest/no-commented-out-tests": { "count": 1 @@ -1392,6 +1547,14 @@ "count": 1 } }, + "packages/assets-controllers/src/TokenSearchDiscoveryDataController/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/assets-controllers/src/TokensController.test.ts": { "typescript/explicit-function-return-type": { "count": 6 @@ -1504,6 +1667,16 @@ "count": 4 } }, + "packages/assets-controllers/src/crypto-compare-service/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/assets-controllers/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/assets-controllers/src/multi-chain-accounts-service/api-balance-fetcher.test.ts": { "no-import-assign": { "count": 2 @@ -1532,6 +1705,11 @@ "count": 2 } }, + "packages/assets-controllers/src/multi-chain-accounts-service/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts": { "typescript/explicit-function-return-type": { "count": 3 @@ -1660,6 +1838,11 @@ "count": 4 } }, + "packages/assets-controllers/src/token-prices-service/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/assets-controllers/src/token-service.test.ts": { "typescript/no-unsafe-assignment": { "count": 3 @@ -2196,6 +2379,11 @@ "count": 1 } }, + "packages/bridge-status-controller/src/strategy/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/bridge-status-controller/src/strategy/non-evm-strategy.ts": { "no-useless-return": { "count": 2 @@ -2406,6 +2594,11 @@ "count": 1 } }, + "packages/client-utils/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/client-utils/src/mappers/api-transaction-mapper.test.ts": { "typescript/no-unsafe-assignment": { "count": 3 @@ -2483,6 +2676,11 @@ "count": 1 } }, + "packages/controller-utils/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/controller-utils/src/siwe.ts": { "no-undef": { "count": 1 @@ -2548,11 +2746,21 @@ "count": 11 } }, + "packages/core-backend/src/api/accounts/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/core-backend/src/api/base-client.test.ts": { "typescript/no-unsafe-member-access": { "count": 3 } }, + "packages/core-backend/src/api/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/core-backend/src/api/prices/client.test.ts": { "typescript/no-unsafe-assignment": { "count": 6 @@ -2561,6 +2769,11 @@ "count": 8 } }, + "packages/core-backend/src/api/prices/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/core-backend/src/api/test-utils.ts": { "no-undef": { "count": 9 @@ -2571,11 +2784,21 @@ "count": 2 } }, + "packages/core-backend/src/api/token/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/core-backend/src/api/tokens/client.test.ts": { "typescript/no-unsafe-assignment": { "count": 1 } }, + "packages/core-backend/src/api/tokens/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/core-backend/src/ws/AccountActivityService.test.ts": { "no-unused-vars": { "count": 1 @@ -2637,11 +2860,21 @@ "count": 4 } }, + "packages/core-backend/src/ws/ohlcv/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/cryptography/jest.config.cjs": { "import/unambiguous": { "count": 1 } }, + "packages/cryptography/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/delegation-controller/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -2823,6 +3056,11 @@ "count": 1 } }, + "packages/eth-block-tracker/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/eth-block-tracker/tests/setupAfterEnv.ts": { "id-length": { "count": 1 @@ -2843,6 +3081,11 @@ "count": 6 } }, + "packages/eth-json-rpc-middleware/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 8 + } + }, "packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.test.ts": { "no-unused-vars": { "count": 1 @@ -2896,6 +3139,9 @@ } }, "packages/eth-json-rpc-provider/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + }, "no-redeclare": { "count": 1 } @@ -3074,6 +3320,11 @@ "count": 19 } }, + "packages/gas-fee-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/gator-permissions-controller/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -3101,6 +3352,16 @@ "count": 2 } }, + "packages/gator-permissions-controller/src/decodePermission/decoders/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/gator-permissions-controller/src/decodePermission/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/gator-permissions-controller/src/permissionOnChainStatus.test.ts": { "typescript/no-unsafe-member-access": { "count": 13 @@ -3354,6 +3615,11 @@ "count": 7 } }, + "packages/keyring-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, "packages/keyring-controller/tests/mocks/mockEncryptor.ts": { "typescript/no-unsafe-argument": { "count": 5 @@ -3612,6 +3878,19 @@ "count": 2 } }, + "packages/logging-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, + "packages/logging-controller/src/logTypes/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, "packages/message-manager/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -3635,6 +3914,11 @@ "count": 1 } }, + "packages/message-manager/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 4 + } + }, "packages/message-manager/src/utils.test.ts": { "typescript/no-unsafe-argument": { "count": 8 @@ -3874,6 +4158,14 @@ "count": 2 } }, + "packages/multichain-account-service/src/analytics/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/multichain-account-service/src/analytics/perf.test.ts": { "typescript/no-unsafe-assignment": { "count": 2 @@ -3974,6 +4266,14 @@ "count": 2 } }, + "packages/multichain-account-service/src/providers/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 8 + } + }, "packages/multichain-account-service/src/providers/utils.ts": { "id-length": { "count": 2 @@ -3987,11 +4287,24 @@ "count": 1 } }, + "packages/multichain-account-service/src/tests/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 4 + } + }, "packages/multichain-api-middleware/jest.config.cjs": { "import/unambiguous": { "count": 1 } }, + "packages/multichain-api-middleware/src/handlers/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/multichain-api-middleware/src/handlers/wallet-createSession.test.ts": { "id-denylist": { "count": 1 @@ -4167,6 +4480,11 @@ "count": 16 } }, + "packages/name-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 6 + } + }, "packages/name-controller/src/providers/ens.test.ts": { "typescript/explicit-function-return-type": { "count": 2 @@ -4310,6 +4628,11 @@ "count": 2 } }, + "packages/network-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/network-controller/src/rpc-service/rpc-service-chain.test.ts": { "typescript/no-unsafe-assignment": { "count": 13 @@ -4445,6 +4768,11 @@ "count": 1 } }, + "packages/network-enablement-controller/src/services/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/notification-services-controller/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -4478,6 +4806,32 @@ "count": 3 } }, + "packages/notification-services-controller/src/NotificationServicesController/constants/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 9 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/mocks/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 4 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/processors/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, "packages/notification-services-controller/src/NotificationServicesController/processors/process-notifications.test.ts": { "typescript/no-unsafe-argument": { "count": 1 @@ -4499,11 +4853,59 @@ "count": 3 } }, + "packages/notification-services-controller/src/NotificationServicesController/types/feature-announcement/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/types/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 5 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/types/notification-api/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/notification-services-controller/src/NotificationServicesController/types/notification-api/notification-api.ts": { "id-length": { "count": 4 } }, + "packages/notification-services-controller/src/NotificationServicesController/types/notification/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/types/perps/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, + "packages/notification-services-controller/src/NotificationServicesController/types/snaps/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/notification-services-controller/src/NotificationServicesController/types/type-utils.ts": { "id-length": { "count": 1 @@ -4517,6 +4919,32 @@ "count": 8 } }, + "packages/notification-services-controller/src/NotificationServicesPushController/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 6 + } + }, + "packages/notification-services-controller/src/NotificationServicesPushController/mocks/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, + "packages/notification-services-controller/src/NotificationServicesPushController/types/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, + "packages/notification-services-controller/src/NotificationServicesPushController/utils/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.test.ts": { "no-import-assign": { "count": 3 @@ -4536,6 +4964,19 @@ "count": 1 } }, + "packages/notification-services-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, + "packages/notification-services-controller/src/shared/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, "packages/notification-services-controller/src/shared/to-raw-notification.ts": { "no-unused-vars": { "count": 1 @@ -4620,6 +5061,11 @@ "count": 1 } }, + "packages/permission-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 5 + } + }, "packages/permission-controller/src/permission-middleware.ts": { "no-useless-return": { "count": 1 @@ -4648,6 +5094,16 @@ "count": 2 } }, + "packages/perps-controller/src/aggregation/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, + "packages/perps-controller/src/constants/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 8 + } + }, "packages/perps-controller/src/providers/AggregatedPerpsProvider.ts": { "typescript/no-unsafe-assignment": { "count": 1 @@ -4672,6 +5128,11 @@ "count": 1 } }, + "packages/perps-controller/src/routing/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/perps-controller/src/services/AggregatedOrderBookConnection.ts": { "no-useless-return": { "count": 1 @@ -4698,6 +5159,11 @@ "count": 2 } }, + "packages/perps-controller/src/types/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/perps-controller/src/utils/hyperLiquidAdapter.ts": { "jsdoc/require-param": { "count": 1 @@ -4709,6 +5175,11 @@ "count": 1 } }, + "packages/perps-controller/src/utils/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 21 + } + }, "packages/perps-controller/src/utils/orderCalculations.ts": { "no-unneeded-ternary": { "count": 1 @@ -4878,6 +5349,11 @@ "count": 1 } }, + "packages/phishing-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/phishing-controller/src/tests/utils.ts": { "typescript/explicit-function-return-type": { "count": 2 @@ -4947,6 +5423,11 @@ "count": 1 } }, + "packages/preferences-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/profile-controller/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -5025,6 +5506,16 @@ "count": 1 } }, + "packages/profile-sync-controller/src/controllers/authentication/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, + "packages/profile-sync-controller/src/controllers/authentication/mocks/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.test.ts": { "typescript/no-unsafe-assignment": { "count": 1 @@ -5038,6 +5529,14 @@ "count": 1 } }, + "packages/profile-sync-controller/src/controllers/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts": { "typescript/explicit-function-return-type": { "count": 24 @@ -5154,6 +5653,16 @@ "count": 3 } }, + "packages/profile-sync-controller/src/controllers/user-storage/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 5 + } + }, + "packages/profile-sync-controller/src/controllers/user-storage/mocks/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/profile-sync-controller/src/controllers/user-storage/mocks/mockResponses.ts": { "typescript/explicit-function-return-type": { "count": 8 @@ -5169,6 +5678,11 @@ "count": 2 } }, + "packages/profile-sync-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 3 + } + }, "packages/profile-sync-controller/src/sdk/__fixtures__/test-utils.ts": { "no-undef": { "count": 2 @@ -5266,6 +5780,9 @@ "id-length": { "count": 3 }, + "metamask-core/no-wildcard-exports": { + "count": 1 + }, "typescript/explicit-function-return-type": { "count": 1 } @@ -5275,6 +5792,11 @@ "count": 2 } }, + "packages/profile-sync-controller/src/sdk/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 7 + } + }, "packages/profile-sync-controller/src/sdk/mocks/userstorage.ts": { "typescript/explicit-function-return-type": { "count": 2 @@ -5373,6 +5895,14 @@ "count": 2 } }, + "packages/profile-sync-controller/src/shared/encryption/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 1 + } + }, "packages/profile-sync-controller/src/shared/encryption/utils.ts": { "no-undef": { "count": 2 @@ -5480,6 +6010,11 @@ "count": 13 } }, + "packages/ramps-controller/src/order-syncing/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/ramps-controller/src/wallet-registration-service.test.ts": { "typescript/no-unsafe-assignment": { "count": 1 @@ -5708,6 +6243,11 @@ "count": 1 } }, + "packages/signature-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/signature-controller/src/utils/decoding-api.test.ts": { "typescript/explicit-function-return-type": { "count": 1 @@ -5839,6 +6379,11 @@ "count": 1 } }, + "packages/smart-transactions-controller/src/featureFlags/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/smart-transactions-controller/src/selectors.ts": { "typescript/explicit-function-return-type": { "count": 2 @@ -7127,6 +7672,11 @@ "count": 1 } }, + "packages/user-operation-controller/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/user-operation-controller/src/utils/gas-fees.test.ts": { "typescript/no-unsafe-return": { "count": 1 @@ -7179,6 +7729,14 @@ "count": 1 } }, + "packages/utils/src/__fixtures__/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + }, + "metamask-core/no-wildcard-exports": { + "count": 5 + } + }, "packages/utils/src/assert.test.ts": { "no-throw-literal": { "count": 1 @@ -7272,6 +7830,11 @@ "count": 1 } }, + "packages/utils/src/index.ts": { + "metamask-core/no-wildcard-exports": { + "count": 22 + } + }, "packages/utils/src/json.test.ts": { "no-throw-literal": { "count": 7 @@ -7337,6 +7900,11 @@ "count": 1 } }, + "packages/utils/src/node.ts": { + "metamask-core/no-wildcard-exports": { + "count": 2 + } + }, "packages/utils/src/unitsConversion.test.ts": { "typescript/no-explicit-any": { "count": 29 @@ -7575,6 +8143,11 @@ "count": 1 } }, + "packages/wallet/src/initialization/instances/index.ts": { + "metamask-core/no-barrel-files": { + "count": 1 + } + }, "packages/wallet/src/initialization/instances/keyring-controller/encryptor.test.ts": { "typescript/no-unsafe-assignment": { "count": 5 diff --git a/oxlint.config.ts b/oxlint.config.ts index 2163303abe1..a1c3e872a44 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -20,6 +20,14 @@ export default createConfig({ 'packages/wallet-framework-docs/site/build/**', ], + // Load custom plugins for this repo from `.oxlint-plugins` + jsPlugins: ['./.oxlint-plugins/metamask-core.ts'], + + rules: { + 'metamask-core/no-wildcard-exports': 'error', + 'metamask-core/no-barrel-files': 'error', + }, + options: { // TODO: Enable this once all unused disable directives are removed. // For the initial migration of ESLint to Oxlint, there are many unused @@ -59,6 +67,7 @@ export default createConfig({ 'packages/platform-api-docs/**', 'packages/wallet-cli/**', '**/scripts/**', + '.oxlint-plugins/**', ], extends: [nodejs], rules: { diff --git a/package.json b/package.json index de88334093b..da659e73fb5 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@metamask/oxlint-config-typescript": "^0.1.1", "@metamask/skills": "^0.3.1", "@metamask/utils": "^12.0.0", + "@oxlint/plugins": "^1.86.0", "@types/jest": "^30.0.0", "@types/lodash": "^4.17.25", "@types/node": "^22.13.14", diff --git a/tsconfig.json b/tsconfig.json index 35878a865d5..e632af32f60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -327,6 +327,7 @@ ], "files": [], "include": [ + "./.oxlint-plugins/**/*.ts", "./docs", "./types", "./scripts", diff --git a/yarn.lock b/yarn.lock index 3ce6e214976..4b417b8f344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6365,6 +6365,7 @@ __metadata: "@metamask/oxlint-config-typescript": "npm:^0.1.1" "@metamask/skills": "npm:^0.3.1" "@metamask/utils": "npm:^12.0.0" + "@oxlint/plugins": "npm:^1.86.0" "@types/jest": "npm:^30.0.0" "@types/lodash": "npm:^4.17.25" "@types/node": "npm:^22.13.14" @@ -10644,6 +10645,13 @@ __metadata: languageName: node linkType: hard +"@oxlint/plugins@npm:^1.86.0": + version: 1.86.0 + resolution: "@oxlint/plugins@npm:1.86.0" + checksum: 10/d38aa96f16f145ae47c5955f5b45fb4020094fbe2e04473666e71a19fa7878998f3e54f5bc16815c23598ea52a8d6be091914b8dab1c017557f09344754403f8 + languageName: node + linkType: hard + "@parcel/watcher-android-arm64@npm:2.6.0": version: 2.6.0 resolution: "@parcel/watcher-android-arm64@npm:2.6.0"