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
1 change: 1 addition & 0 deletions .github/workflows/lint-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
- lint:misc:check
- lint:teams
- lint:tsconfigs:all
- lint:tsc
- messenger-action-types:check
- readme-content:check
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ scripts/coverage
!.yarn/versions

# typescript
**/.tsc-lint-cache/
packages/*/*.tsbuildinfo

# AI
Expand Down

@mcmire mcmire Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In typechecking this file tsc told me that it interpreted knip as an ESM-only module but it interpreted this file as CommonJS. Renaming this file cleared the type error (although it did mean I had to update how we call knip in package.json, because knip won't look for knip.config.mts by default).

File renamed without changes.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@
"docs:platform-api:build": "yarn workspace @metamask/platform-api-docs cli ../.. --build --project-label Core",
"docs:platform-api:dev": "yarn workspace @metamask/platform-api-docs cli ../.. --dev --project-label Core",
"docs:platform-api:serve": "yarn workspace @metamask/platform-api-docs cli ../.. --serve --project-label Core",
"lint": "yarn lint:eslint && echo && yarn lint:misc --check && yarn constraints && yarn lint:dependencies && yarn lint:teams && yarn messenger-action-types:check && yarn readme-content:check && yarn lint:tsconfigs:all && yarn codeowners:check",
"lint:dependencies": "knip --dependencies && yarn dedupe --check",
"lint:dependencies:fix": "knip --dependencies && yarn dedupe",
"lint": "yarn lint:tsc && yarn lint:eslint && echo && yarn lint:misc --check && yarn constraints && yarn lint:dependencies && yarn lint:teams && yarn messenger-action-types:check && yarn readme-content:check && yarn lint:tsconfigs:all && yarn codeowners:check",
"lint:dependencies": "knip --config knip.config.mts --dependencies && yarn dedupe --check",
"lint:dependencies:fix": "knip --config knip.config.mts --dependencies && yarn dedupe",
"lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=8192' yarn eslint",
"lint:fix": "yarn lint:eslint --fix --prune-suppressions && echo && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies:fix && yarn messenger-action-types:generate && yarn readme-content:update && yarn codeowners:generate",
"lint:misc": "oxfmt --ignore-path .gitignore",
"lint:misc:check": "yarn lint:misc --check",
"lint:teams": "tsx scripts/lint-teams-json.ts",
"lint:tsc": "tsc --build tsconfig.lint.json",
"lint:tsconfigs": "tsx scripts/lint-tsconfigs/lint-tsconfigs.mts",
"lint:tsconfigs:all": "yarn workspaces foreach --all --parallel --interlaced --verbose run lint:tsconfigs",
"lint:tsconfigs:fix": "tsx scripts/lint-tsconfigs/lint-tsconfigs.mts --fix",
Expand Down
12 changes: 12 additions & 0 deletions packages/base-controller/tsconfig.lint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": ["./tsconfig.json", "../../tsconfig.packages.lint.json"],

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

These per-package config files work a bit differently than the per-package tsconfig.build.json files, because they extend two files rather than one. I chose to do this because these files are very similar to tsconfig.json in concept but they just need to be tweaked a little bit (whereas tsconfig.build.json is almost entirely different from tsconfig.json, so it only makes sense to extend tsconfig.packages.build.json).

"compilerOptions": {
"outDir": "./.tsc-lint-cache",
"tsBuildInfoFile": "./.tsc-lint-cache/tsconfig.tsbuildinfo"
},
"references": [
{
"path": "../messenger/tsconfig.lint.json"
}
]
}
8 changes: 8 additions & 0 deletions packages/messenger-cli/tsconfig.lint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["./tsconfig.json", "../../tsconfig.packages.lint.json"],
"compilerOptions": {
"outDir": "./.tsc-lint-cache",
"tsBuildInfoFile": "./.tsc-lint-cache/tsconfig.tsbuildinfo"
},
"references": []
}
7 changes: 7 additions & 0 deletions packages/messenger/tsconfig.lint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["./tsconfig.json", "../../tsconfig.packages.lint.json"],
"compilerOptions": {
"outDir": "./.tsc-lint-cache",
"tsBuildInfoFile": "./.tsc-lint-cache/tsconfig.tsbuildinfo"
}
}
8 changes: 8 additions & 0 deletions packages/platform-api-docs/tsconfig.lint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["./tsconfig.json", "../../tsconfig.packages.lint.json"],
"compilerOptions": {
"outDir": "./.tsc-lint-cache",
"tsBuildInfoFile": "./.tsc-lint-cache/tsconfig.tsbuildinfo"
},
"references": []
}
2 changes: 1 addition & 1 deletion scripts/create-package/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('create-package/cli', () => {
beforeEach(() => {
// yargs calls process.exit() with 1 on failure and sometimes 0 on success.
// We have to intercept it.
jest.spyOn(process, 'exit').mockImplementation((code?: number) => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixing existing type error in this file, which only showed up in my editor.

jest.spyOn(process, 'exit').mockImplementation((code) => {
if (code === 1) {
throw new Error('exit: 1');
} else {
Expand Down
208 changes: 176 additions & 32 deletions scripts/lint-tsconfigs/lint-package-tsconfigs.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as path from 'path';

import type {
PackageManifest,
TsconfigLintMetaReport,
Workspace,
Workspaces,
} from './utils.mjs';
import {
ensureTsconfigsUpdated,
filterWorkspacesWithTsconfig,
getAllNonRootWorkspaces,
lintTsconfigs,
printReport,
Expand All @@ -13,8 +17,9 @@ import {
} from './utils.mjs';

/**
* Lints a package's tsconfig.json and tsconfig.build.json files to ensure they
* reference all workspace dependencies. Optionally fixes any issues found.
* Lints a package's development, build, and lint TypeScript configs to ensure
* they reference the expected workspace dependencies. Optionally fixes any
* issues found.
*
* @param options - The options object.
* @param options.packageRoot - The root directory of the package.
Expand All @@ -33,45 +38,158 @@ export async function lintPackageTsconfigs({
}): Promise<boolean> {
const manifest = await readPackageManifest(packageRoot);
const workspaces = await getAllNonRootWorkspaces(repoRoot);
const tsconfigs = await Promise.all([
const expectedPackageNames = getExpectedWorkspaceDependencies({
manifest,
workspaces,
});

const devAndBuildReport = await lintPackageDevAndBuildOnlyTsconfigs({
packageRoot,
repoRoot,
workspaces,
expectedPackageNames,
shouldFix,
});
const lintReport = await lintPackageLintOnlyTsconfigs({
packageRoot,
repoRoot,
manifest,
workspaces,
expectedPackageNames,
shouldFix,
});

return devAndBuildReport.didPass && lintReport.didPass;
}

/**
* Lints a package's development- and build-only TypeScript configs to ensure
* they reference the expected workspace dependencies. Optionally fixes any
* issues found.
*
* @param options - The options object.
* @param options.packageRoot - The root directory of the package.
* @param options.repoRoot - The root directory of the repository.
* @param options.workspaces - The workspaces in the repository.
* @param options.expectedPackageNames - Workspace dependencies to reference.
* @param options.shouldFix - Whether to automatically fix issues.
* @returns The report resulting from the lint.
*/
export async function lintPackageDevAndBuildOnlyTsconfigs({
packageRoot,
repoRoot,
workspaces,
expectedPackageNames,
shouldFix,
}: {
packageRoot: string;
repoRoot: string;
workspaces: Workspaces;
expectedPackageNames: Set<string>;
shouldFix: boolean;
}): Promise<TsconfigLintMetaReport> {
const devAndBuildTsconfigs = await Promise.all([
readTsconfig(packageRoot, 'tsconfig.json'),
readTsconfig(packageRoot, 'tsconfig.build.json'),
]);
const expectedPackageNames = getExpectedWorkspaceDependencies({
manifest,
const expectedWorkspaces = getSortedWorkspaces({
packageNames: expectedPackageNames,
workspaces,
});
const sortedExpectedWorkspaces = [...expectedPackageNames]
.sort((a, b) => a.localeCompare(b))
.map((name) => {
const workspace = workspaces.byName.get(name);
if (!workspace) {
throw new Error(`Expected workspace not found for package: ${name}`);
}
return workspace;
});

let report: TsconfigLintMetaReport;
if (shouldFix) {
report = await ensureTsconfigsUpdated({
workspaces: sortedExpectedWorkspaces,
tsconfigs,
repoRoot,
currentWorkspaceRoot: packageRoot,
});
} else {
report = await lintTsconfigs({
tsconfigs,
expectedPackageNames,
workspaces,
repoRoot,
currentWorkspaceRoot: packageRoot,
});
}
const report = shouldFix
? await ensureTsconfigsUpdated({
workspaces: expectedWorkspaces,
tsconfigs: devAndBuildTsconfigs,
repoRoot,
currentWorkspaceRoot: packageRoot,
})
: await lintTsconfigs({
tsconfigs: devAndBuildTsconfigs,
expectedPackageNames,
workspaces,
repoRoot,
currentWorkspaceRoot: packageRoot,
});

printReport(report);

return report;
}

/**
* Lints a package's lint-only TypeScript config to ensure it references the
* expected workspace dependencies. Optionally fixes any issues found.
*
* @param options - The options object.
* @param options.packageRoot - The root directory of the package.
* @param options.repoRoot - The root directory of the repository.
* @param options.manifest - Contents of the package's `package.json` file.
* @param options.workspaces - The workspaces in the repository.
* @param options.expectedPackageNames - Workspace dependencies to reference.
* @param options.shouldFix - Whether to automatically fix issues.
* @returns The report resulting from the lint.
*/
export async function lintPackageLintOnlyTsconfigs({
packageRoot,
repoRoot,
manifest,
workspaces,
expectedPackageNames,
shouldFix,
}: {
packageRoot: string;
repoRoot: string;
manifest: PackageManifest;
workspaces: Workspaces;
expectedPackageNames: Set<string>;
shouldFix: boolean;
}): Promise<TsconfigLintMetaReport> {
const expectedWorkspaces = getSortedWorkspaces({
packageNames: expectedPackageNames,
workspaces,
});
const lintWorkspaces = await filterWorkspacesWithTsconfig({
workspaces: expectedWorkspaces,
repoRoot,
fileName: 'tsconfig.lint.json',
});
// This allows us to increase linting for the whole repo incrementally.
const packageLintWorkspaces = await filterWorkspacesWithTsconfig({
workspaces: [
{
name: manifest.name,
location: path.relative(repoRoot, packageRoot),
},
],
repoRoot,
fileName: 'tsconfig.lint.json',
});
const lintTsconfigsToCheck =
packageLintWorkspaces.length === 0
? []
: [await readTsconfig(packageRoot, 'tsconfig.lint.json')];

const report = shouldFix
? await ensureTsconfigsUpdated({
workspaces: lintWorkspaces,
tsconfigs: lintTsconfigsToCheck,
repoRoot,
currentWorkspaceRoot: packageRoot,
})
: await lintTsconfigs({
tsconfigs: lintTsconfigsToCheck,
expectedPackageNames: new Set(
lintWorkspaces.map((workspace) => workspace.name),
),
workspaces,
repoRoot,
currentWorkspaceRoot: packageRoot,
});

printReport(report);

return report.didPass;
return report;
}

/**
Expand Down Expand Up @@ -101,3 +219,29 @@ function getExpectedWorkspaceDependencies({
),
);
}

/**
* Gets workspace metadata for the given package names in alphabetical order.
*
* @param options - The options object.
* @param options.packageNames - Workspace package names to resolve.
* @param options.workspaces - The workspaces in the repository.
* @returns The corresponding workspaces in alphabetical order.
*/
function getSortedWorkspaces({
packageNames,
workspaces,
}: {
packageNames: Set<string>;
workspaces: Workspaces;
}): Workspace[] {
return [...packageNames]
.sort((a, b) => a.localeCompare(b))
.map((name) => {
const workspace = workspaces.byName.get(name);
if (!workspace) {
throw new Error(`Expected workspace not found for package: ${name}`);
}
return workspace;
});
}
Loading
Loading