From 151ea94150ba417a91fa795f128793d6b7feac9e Mon Sep 17 00:00:00 2001 From: Dimitrios Marlagkoutsos Date: Thu, 5 Sep 2024 21:09:18 +0100 Subject: [PATCH 1/8] Migrate prolog constraints to yarn with javascript --- constraints.pro | 387 ---------------- package.json | 4 + packages/shims/package.json | 2 +- packages/streams/package.json | 2 +- yarn.config.cjs | 814 ++++++++++++++++++++++++++++++++++ yarn.lock | 20 +- 6 files changed, 839 insertions(+), 390 deletions(-) delete mode 100644 constraints.pro create mode 100644 yarn.config.cjs diff --git a/constraints.pro b/constraints.pro deleted file mode 100644 index 48aa60a24..000000000 --- a/constraints.pro +++ /dev/null @@ -1,387 +0,0 @@ -%=============================================================================== -% Utility predicates -%=============================================================================== - -% True if and only if VersionRange is a value that we would expect to see -% following a package in a "*dependencies" field within a `package.json`. -is_valid_version_range(VersionRange) :- - VersionRange = 'workspace:^'; - VersionRange = 'workspace:~'; - parse_version_range(VersionRange, _, _, _, _). - -% Succeeds if Number can be unified with Atom converted to a number; throws if -% not. -atom_to_number(Atom, Number) :- - atom_chars(Atom, Chars), - number_chars(Number, Chars). - -% True if and only if Atom can be converted to a number. -is_atom_number(Atom) :- - catch(atom_to_number(Atom, _), _, false). - -% True if and only if Modifier can be unified with the leading character of the -% version range ("^" or "~" if present, or "" if not present), Major can be -% unified with the major part of the version string, Minor with the minor, and -% Patch with the patch. -parse_version_range(VersionRange, Modifier, Major, Minor, Patch) :- - % Identify and extract the modifier (^ or ~) from the version string - atom_chars(VersionRange, Chars), - Chars = [PossibleModifier | CharsWithoutPossibleModifier], - ( - ( - PossibleModifier = '^'; - PossibleModifier = '~' - ) -> - ( - Modifier = PossibleModifier, - CharsWithoutModifier = CharsWithoutPossibleModifier - ) ; - ( - is_atom_number(PossibleModifier) -> - ( - Modifier = '', - CharsWithoutModifier = Chars - ) ; - false - ) - ), - atomic_list_concat(CharsWithoutModifier, '', VersionRangeWithoutModifier), - atomic_list_concat(VersionParts, '.', VersionRangeWithoutModifier), - % Validate version string while extracting each part - length(VersionParts, 3), - nth0(0, VersionParts, MajorAtom), - nth0(1, VersionParts, MinorAtom), - nth0(2, VersionParts, PatchAtom), - atom_to_number(MajorAtom, Major), - atom_to_number(MinorAtom, Minor), - atom_to_number(PatchAtom, Patch). - -% True if and only if the first SemVer version range is greater than the second -% SemVer version range. Such a range must match "^MAJOR.MINOR.PATCH", -% "~MAJOR.MINOR.PATCH", "MAJOR.MINOR.PATCH". If two ranges do not have the same -% modifier ("^" or "~"), then they cannot be compared and the first cannot be -% considered as less than the second. -% -% Borrowed from: -npm_version_range_out_of_sync(VersionRange1, VersionRange2) :- - parse_version_range(VersionRange1, VersionRange1Modifier, VersionRange1Major, VersionRange1Minor, VersionRange1Patch), - parse_version_range(VersionRange2, VersionRange2Modifier, VersionRange2Major, VersionRange2Minor, VersionRange2Patch), - VersionRange1Modifier == VersionRange2Modifier, - ( - % 2.0.0 > 1.0.0 - % 2.0.0 > 1.1.0 - % 2.0.0 > 1.0.1 - VersionRange1Major @> VersionRange2Major ; - ( - VersionRange1Major == VersionRange2Major , - ( - % 1.1.0 > 1.0.0 - % 1.1.0 > 1.0.1 - VersionRange1Minor @> VersionRange2Minor ; - ( - VersionRange1Minor == VersionRange2Minor , - % 1.0.1 > 1.0.0 - VersionRange1Patch @> VersionRange2Patch - ) - ) - ) - ). - -% True if and only if WorkspaceBasename can unify with the part of the given -% workspace directory name that results from removing all leading directories. -workspace_basename(WorkspaceCwd, WorkspaceBasename) :- - atomic_list_concat(Parts, '/', WorkspaceCwd), - last(Parts, WorkspaceBasename). - -% True if and only if WorkspacePackageName can unify with the name of the -% package which the workspace represents (which comes from the directory where -% the package is located). Assumes that the package is not in a sub-workspace -% and is not private. -workspace_package_name(WorkspaceCwd, WorkspacePackageName) :- - workspace_basename(WorkspaceCwd, WorkspaceBasename), - workspace_field(WorkspaceCwd, 'private', false), - atom_concat('@metamask/', WorkspaceBasename, WorkspacePackageName). - -% True if RepoName can be unified with the repository name part of RepoUrl, a -% complete URL for a repository on GitHub. This URL must include the ".git" -% extension. -repo_name(RepoUrl, RepoName) :- - Prefix = 'https://github.com/MetaMask/', - atom_length(Prefix, PrefixLength), - Suffix = '.git', - atom_length(Suffix, SuffixLength), - atom_length(RepoUrl, RepoUrlLength), - sub_atom(RepoUrl, 0, PrefixLength, After, Prefix), - sub_atom(RepoUrl, Before, SuffixLength, 0, Suffix), - Start is RepoUrlLength - After + 1, - End is Before + 1, - RepoNameLength is End - Start, - sub_atom(RepoUrl, PrefixLength, RepoNameLength, SuffixLength, RepoName). - -%=============================================================================== -% Constraints -%=============================================================================== - -% All packages, published or otherwise, must have a name. -\+ gen_enforced_field(WorkspaceCwd, 'name', null). - -% The name of the root package can be anything, but the name of a non-root -% package must match its directory (e.g., a package located in "packages/foo" -% must be called "@metamask/foo"). -% -% NOTE: This assumes that the set of non-root workspaces is flat. Nested -% workspaces will be added in a future change. -gen_enforced_field(WorkspaceCwd, 'name', WorkspacePackageName) :- - WorkspaceCwd \= '.', - workspace_package_name(WorkspaceCwd, WorkspacePackageName). - -% All packages, published or otherwise, must have a description. -\+ gen_enforced_field(WorkspaceCwd, 'description', null). -% The description cannot end with a period. -gen_enforced_field(WorkspaceCwd, 'description', DescriptionWithoutTrailingPeriod) :- - workspace_field(WorkspaceCwd, 'description', Description), - atom_length(Description, Length), - LengthLessOne is Length - 1, - sub_atom(Description, LengthLessOne, 1, 0, LastCharacter), - sub_atom(Description, 0, LengthLessOne, 1, DescriptionWithoutPossibleTrailingPeriod), - ( - LastCharacter == '.' -> - DescriptionWithoutTrailingPeriod = DescriptionWithoutPossibleTrailingPeriod ; - DescriptionWithoutTrailingPeriod = Description - ). - -% All published packages must have the same set of NPM keywords. -gen_enforced_field(WorkspaceCwd, 'keywords', ['MetaMask', 'Ethereum']) :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages do not have any NPM keywords. -gen_enforced_field(WorkspaceCwd, 'keywords', null) :- - workspace_field(WorkspaceCwd, 'private', true). - -% The homepage of a published package must match its name (which is in turn -% based on its workspace directory name). -gen_enforced_field(WorkspaceCwd, 'homepage', CorrectHomepageUrl) :- - \+ workspace_field(WorkspaceCwd, 'private', true), - workspace_basename(WorkspaceCwd, WorkspaceBasename), - workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), - repo_name(RepoUrl, RepoName), - atomic_list_concat(['https://github.com/MetaMask/', RepoName, '/tree/main/packages/', WorkspaceBasename, '#readme'], CorrectHomepageUrl). -% Non-published packages do not have a homepage. -gen_enforced_field(WorkspaceCwd, 'homepage', null) :- - workspace_field(WorkspaceCwd, 'private', true). - -% The bugs URL of a published package must point to the Issues page for the -% repository. -gen_enforced_field(WorkspaceCwd, 'bugs.url', CorrectBugsUrl) :- - \+ workspace_field(WorkspaceCwd, 'private', true), - workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), - repo_name(RepoUrl, RepoName), - atomic_list_concat(['https://github.com/MetaMask/', RepoName, '/issues'], CorrectBugsUrl). -% Non-published packages must not have a bugs section. -gen_enforced_field(WorkspaceCwd, 'bugs', null) :- - workspace_field(WorkspaceCwd, 'private', true). - -% All packages must specify Git as the repository type. -gen_enforced_field(WorkspaceCwd, 'repository.type', 'git'). - -% All packages must match the URL of a repo within the MetaMask organization. -gen_enforced_field(WorkspaceCwd, 'repository.url', 'https://github.com/MetaMask/.git') :- - workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), - \+ repo_name(RepoUrl, _). -% The repository URL for non-root packages must match the same URL used for the -% root package. -gen_enforced_field(WorkspaceCwd, 'repository.url', RepoUrl) :- - workspace_field('.', 'repository.url', RepoUrl), - repo_name(RepoUrl, _). - WorkspaceCwd \= '.'. - -% The license for all published packages must be MIT unless otherwise specified. -gen_enforced_field(WorkspaceCwd, 'license', 'MIT') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages do not have a license. -gen_enforced_field(WorkspaceCwd, 'license', null) :- - workspace_field(WorkspaceCwd, 'private', true). - -% The entrypoint for all published packages must be the same. -gen_enforced_field(WorkspaceCwd, 'main', './dist/index.cjs') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -gen_enforced_field(WorkspaceCwd, 'module', './dist/index.mjs') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages must not specify an entrypoint. -gen_enforced_field(WorkspaceCwd, 'main', null) :- - WorkspaceCwd \= 'packages/shims', - WorkspaceCwd \= 'packages/streams', - workspace_field(WorkspaceCwd, 'private', true). -gen_enforced_field(WorkspaceCwd, 'module', null) :- - WorkspaceCwd \= 'packages/shims', - WorkspaceCwd \= 'packages/streams', - workspace_field(WorkspaceCwd, 'private', true). - -% The type definitions entrypoint for all publishable packages must be the same. -gen_enforced_field(WorkspaceCwd, 'types', './dist/index.d.cts') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages must not specify a type definitions entrypoint. -gen_enforced_field(WorkspaceCwd, 'types', null) :- - WorkspaceCwd \= 'packages/shims', - WorkspaceCwd \= 'packages/streams', - workspace_field(WorkspaceCwd, 'private', true). - -% The exports for all published packages must be the same. -% CommonJS -gen_enforced_field(WorkspaceCwd, 'exports["."].require.default', './dist/index.cjs') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -gen_enforced_field(WorkspaceCwd, 'exports["."].require.types', './dist/index.d.cts') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% ESM -gen_enforced_field(WorkspaceCwd, 'exports["."].import.default', './dist/index.mjs') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -gen_enforced_field(WorkspaceCwd, 'exports["."].import.types', './dist/index.d.mts') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% package.json -gen_enforced_field(WorkspaceCwd, 'exports["./package.json"]', './package.json') :- - WorkspaceCwd \= 'packages/extension', - WorkspaceCwd \= '.'. -% The root package must not specify exports. -gen_enforced_field(WorkspaceCwd, 'exports', null) :- - WorkspaceCwd = '.'. - -% Published packages must not have side effects. -gen_enforced_field(WorkspaceCwd, 'sideEffects', false) :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages must not specify side effects. -gen_enforced_field(WorkspaceCwd, 'sideEffects', null) :- - WorkspaceCwd \= 'packages/shims', - WorkspaceCwd \= 'packages/streams', - workspace_field(WorkspaceCwd, 'private', true). - -% The list of files included in published packages must only include files -% generated during the build step. -gen_enforced_field(WorkspaceCwd, 'files', ['dist/']) :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% The root package must specify an empty set of published files. (This is -% required in order to be able to import anything in development-only scripts, -% as otherwise the `node/no-unpublished-require` ESLint rule will disallow it.) -gen_enforced_field(WorkspaceCwd, 'files', []) :- - WorkspaceCwd = '.'. - -% TODO: Add constraint enforcing the existence of _any_ build script in all packages, -% including the root. -% % All packages must have some "build" script. -% gen_enforced_field(WorkspaceCwd, 'scripts.build', '') :- -% WorkspaceCwd \= '.'. - -% All packages except the root and other exceptions must have the same "build:docs" script. -gen_enforced_field(WorkspaceCwd, 'scripts.build:docs', 'typedoc') :- - WorkspaceCwd \= 'packages/extension', - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All packages except the root must have the same "clean" script. -gen_enforced_field(WorkspaceCwd, 'scripts.clean', 'rimraf --glob ./dist \'./*.tsbuildinfo\'') :- - WorkspaceCwd \= '.'. - -% All published packages must have the same "publish:preview" script. -gen_enforced_field(WorkspaceCwd, 'scripts.publish:preview', 'yarn npm publish --tag preview') :- - \+ workspace_field(WorkspaceCwd, 'private', true). - -% All published packages must not have a "prepack" script. -gen_enforced_field(WorkspaceCwd, 'scripts.prepack', null) :- - \+ workspace_field(WorkspaceCwd, 'private', true). - -% The "changelog:validate" script for each published package must run a common -% script with the name of the package as the first argument. -gen_enforced_field(WorkspaceCwd, 'scripts.changelog:validate', CorrectChangelogValidationCommand) :- - \+ workspace_field(WorkspaceCwd, 'private', true), - workspace_field(WorkspaceCwd, 'scripts.changelog:validate', ChangelogValidationCommand), - workspace_package_name(WorkspaceCwd, WorkspacePackageName), - atomic_list_concat(['../../scripts/validate-changelog.sh ', WorkspacePackageName, ' [...]'], CorrectChangelogValidationCommand), - atom_concat('../../scripts/validate-changelog.sh ', WorkspacePackageName, ExpectedPrefix), - \+ atom_concat(ExpectedPrefix, _, ChangelogValidationCommand). - -% The "changelog:update" script for each published package must run a common -% script with the name of the package as the first argument. -gen_enforced_field(WorkspaceCwd, 'scripts.changelog:update', CorrectChangelogUpdateCommand) :- - \+ workspace_field(WorkspaceCwd, 'private', true), - workspace_field(WorkspaceCwd, 'scripts.changelog:update', ChangelogUpdateCommand), - workspace_package_name(WorkspaceCwd, WorkspacePackageName), - atomic_list_concat(['../../scripts/update-changelog.sh ', WorkspacePackageName, ' [...]'], CorrectChangelogUpdateCommand), - atom_concat('../../scripts/update-changelog.sh ', WorkspacePackageName, ExpectedPrefix), - \+ atom_concat(ExpectedPrefix, _, ChangelogUpdateCommand). - -% All non-root packages must have the same "test" script. -gen_enforced_field(WorkspaceCwd, 'scripts.test', 'vitest run --config vitest.config.ts') :- - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All non-root packages must have the same "test:clean" script. -gen_enforced_field(WorkspaceCwd, 'scripts.test:clean', 'yarn test --no-cache --coverage.clean') :- - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All non-root packages must have the same "test:dev" script. -gen_enforced_field(WorkspaceCwd, 'scripts.test:dev', 'yarn test --coverage false') :- - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All non-root packages must have the same "test:verbose" script. -gen_enforced_field(WorkspaceCwd, 'scripts.test:verbose', 'yarn test --reporter verbose') :- - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All non-root packages must have the same "test:watch" script. -gen_enforced_field(WorkspaceCwd, 'scripts.test:watch', 'vitest --config vitest.config.ts') :- - WorkspaceCwd \= 'packages/test-utils', - WorkspaceCwd \= '.'. - -% All dependency ranges must be recognizable (this makes it possible to apply -% the next two rules effectively). -gen_enforced_dependency(WorkspaceCwd, DependencyIdent, 'a range optionally starting with ^ or ~', DependencyType) :- - workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), - \+ ( - DependencyRange = '^1.0.0-rc.12'; % is_valid_version_range does not handle rc suffixes - is_valid_version_range(DependencyRange) - ). - -% All dependency ranges for a package must be synchronized across the monorepo -% (the least version range wins), regardless of which "*dependencies" field -% where the package appears. -gen_enforced_dependency(WorkspaceCwd, DependencyIdent, OtherDependencyRange, DependencyType) :- - workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), - workspace_has_dependency(OtherWorkspaceCwd, DependencyIdent, OtherDependencyRange, OtherDependencyType), - WorkspaceCwd \= OtherWorkspaceCwd, - DependencyRange \= OtherDependencyRange, - npm_version_range_out_of_sync(DependencyRange, OtherDependencyRange). - -% If a dependency is listed under "dependencies", it should not be listed under -% "devDependencies". We match on the same dependency range so that if a -% dependency is listed under both lists, their versions are synchronized and -% then this constraint will apply and remove the "right" duplicate. -gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, DependencyType) :- - workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, 'dependencies'), - workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), - DependencyType == 'devDependencies'. - -% If a workspace package is listed under another workspace package's -% `dependencies`, it should not also be listed under its `devDependencies`. -gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, 'devDependencies') :- - workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, 'dependencies'). - -% The root workspace (and only the root workspace) needs to specify the Yarn -% version required for development. -gen_enforced_field(WorkspaceCwd, 'packageManager', 'yarn@4.2.2') :- - WorkspaceCwd == '.'. -gen_enforced_field(WorkspaceCwd, 'packageManager', null) :- - WorkspaceCwd \= '.'. - -% All packages must specify a minimum Node version of 18. -gen_enforced_field(WorkspaceCwd, 'engines.node', '^18.18 || >=20'). - -% All published packages are public. -gen_enforced_field(WorkspaceCwd, 'publishConfig.access', 'public') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% All published packages are available on the NPM registry. -gen_enforced_field(WorkspaceCwd, 'publishConfig.registry', 'https://registry.npmjs.org/') :- - \+ workspace_field(WorkspaceCwd, 'private', true). -% Non-published packages do not need to specify any publishing settings -% whatsoever. -gen_enforced_field(WorkspaceCwd, 'publishConfig', null) :- - workspace_field(WorkspaceCwd, 'private', true). diff --git a/package.json b/package.json index 3b413a28d..fd82157bd 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "@metamask/eslint-config-typescript": "^13.0.0", "@ts-bridge/cli": "^0.3.0", "@ts-bridge/shims": "^0.1.1", + "@types/lodash": "^4", "@types/node": "^18.18.14", + "@types/semver": "^7", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "@yarnpkg/types": "^4.0.0", @@ -55,9 +57,11 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-vitest": "^0.4.1", + "lodash": "^4.17.21", "prettier": "^2.7.1", "prettier-plugin-packagejson": "^2.3.0", "rimraf": "^6.0.1", + "semver": "^7.6.3", "typedoc": "^0.24.8", "typescript": "~5.5.4", "vite": "^5.3.5", diff --git a/packages/shims/package.json b/packages/shims/package.json index 8ea2ac779..6ac347bfc 100644 --- a/packages/shims/package.json +++ b/packages/shims/package.json @@ -16,7 +16,7 @@ "main": "./dist/index.cjs", "module": "./dist/index.mjs", "files": [ - "dist" + "dist/" ], "scripts": { "build": "node scripts/bundle.js", diff --git a/packages/streams/package.json b/packages/streams/package.json index 80c61fadf..b5d4e971e 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -26,7 +26,7 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.cts", "files": [ - "dist" + "dist/" ], "scripts": { "build": "ts-bridge --project tsconfig.build.json --clean", diff --git a/yarn.config.cjs b/yarn.config.cjs new file mode 100644 index 000000000..b8ba9809f --- /dev/null +++ b/yarn.config.cjs @@ -0,0 +1,814 @@ +// This file is used to define, among other configuration, rules that Yarn will +// execute when you run `yarn constraints`. These rules primarily check the +// manifests of each package in the monorepo to ensure they follow a standard +// format, but also check the presence of certain files as well. + +/** @type {import('@yarnpkg/types')} */ +const { defineConfig } = require('@yarnpkg/types'); +const { readFile } = require('fs/promises'); +const { get } = require('lodash'); +const { basename, resolve } = require('path'); +const semver = require('semver'); +const { inspect } = require('util'); + +const entrypointExceptions = ['shims', 'streams']; +const typedocExceptions = ['test-utils', 'extension']; +const noBuildOrTests = ['test-utils']; + +/** + * Aliases for the Yarn type definitions, to make the code more readable. + * + * @typedef {import('@yarnpkg/types').Yarn} Yarn + * @typedef {import('@yarnpkg/types').Yarn.Constraints.Workspace} Workspace + * @typedef {import('@yarnpkg/types').Yarn.Constraints.Dependency} Dependency + * @typedef {import('@yarnpkg/types').Yarn.Constraints.DependencyType} DependencyType + */ + +module.exports = defineConfig({ + async constraints({ Yarn }) { + const rootWorkspace = Yarn.workspace({ cwd: '.' }); + if (rootWorkspace === null) { + throw new Error('Could not find root workspace'); + } + + const repositoryUri = rootWorkspace.manifest.repository.url.replace( + /\.git$/u, + '', + ); + + for (const workspace of Yarn.workspaces()) { + const workspaceBasename = getWorkspaceBasename(workspace); + const isChildWorkspace = workspace.cwd !== '.'; + const isPrivate = workspace.manifest.private === true; + const dependenciesByIdentAndType = getDependenciesByIdentAndType( + Yarn.dependencies({ workspace }), + ); + + // All packages must have a name + expectWorkspaceField(workspace, 'name'); + + if (isChildWorkspace) { + // All non-root packages must have a name that matches its directory + expectWorkspaceField( + workspace, + 'name', + `@${isPrivate ? 'ocap' : 'metamask'}/${workspaceBasename}`, + ); + + // All non-root packages must have a version. + expectWorkspaceField(workspace, 'version'); + + // // All non-root packages must have a description that does not end with a period. + expectWorkspaceDescription(workspace); + + if (!isPrivate) { + // All non-root packages must have the same set of NPM keywords. + expectWorkspaceField(workspace, 'keywords', ['MetaMask', 'Ethereum']); + + // All non-root packages must have a homepage URL that includes its name. + expectWorkspaceField( + workspace, + 'homepage', + `${repositoryUri}/tree/main/packages/${workspaceBasename}#readme`, + ); + + // All non-root packages must have a URL for reporting bugs that points + // to the Issues page for the repository. + expectWorkspaceField( + workspace, + 'bugs.url', + `${repositoryUri}/issues`, + ); + } + + // All non-root packages must specify a Git repository within the + // MetaMask GitHub organization. + expectWorkspaceField(workspace, 'repository.type', 'git'); + expectWorkspaceField( + workspace, + 'repository.url', + `${repositoryUri}.git`, + ); + + if (isPrivate) { + // Non-published packages should not have a license. + workspace.unset('license'); + } else { + // All published packages must be MIT else they must be private + expectWorkspaceField(workspace, 'license', 'MIT'); + } + + if (!isPrivate) { + // The entrypoint for all published packages must be the same. + expectWorkspaceField(workspace, 'module', './dist/index.mjs'); + expectWorkspaceField(workspace, 'main', './dist/index.cjs'); + // The type definitions entrypoint for all publishable packages must be the same. + expectWorkspaceField( + workspace, + 'exports["."].types', + './dist/types/index.d.cts', + ); + + // The exports for all published packages must be the same. + // CommonJS + expectWorkspaceField( + workspace, + 'exports["."].require.default', + './dist/index.cjs', + ); + expectWorkspaceField( + workspace, + 'exports["."].require.types', + './dist/index.d.cts', + ); + // ESM + expectWorkspaceField( + workspace, + 'exports["."].import.default', + './dist/index.mjs', + ); + expectWorkspaceField( + workspace, + 'exports["."].import.types', + './dist/index.d.mts', + ); + + // Published packages must not have side effects. + expectWorkspaceField(workspace, 'sideEffects', false); + + // All published packages must have the same "publish:preview" script. + expectWorkspaceField( + workspace, + 'scripts.publish:preview', + 'yarn npm publish --tag preview', + ); + + // All published packages must not have a "prepack" script. + expectWorkspaceField(workspace, 'scripts.prepack', null); + + // All non-root package must have valid "changelog:update" and + // "changelog:validate" scripts. + expectCorrectWorkspaceChangelogScripts(workspace); + } + + // Non-published packages must not specify the following keys except from the ones that are exempted + if (isPrivate && !entrypointExceptions.includes(workspaceBasename)) { + workspace.unset('module'); + workspace.unset('main'); + workspace.unset('types'); + workspace.unset('sideEffects'); + } + + // All non-root packages must export a `package.json` file except for the extension package + if (workspaceBasename !== 'extension') { + expectWorkspaceField( + workspace, + 'exports["./package.json"]', + './package.json', + ); + } + + if (!isPrivate || entrypointExceptions.includes(workspaceBasename)) { + // The list of files included in all non-root packages must only include + // files generated during the build process. + expectWorkspaceField(workspace, 'files', ['dist/']); + } + + if (!typedocExceptions.includes(workspaceBasename)) { + // All non-root packages must have the same "build:docs" script. + expectWorkspaceField(workspace, 'scripts.build:docs', 'typedoc'); + } + + // All packages except the root must have the same "clean" script. + expectWorkspaceField( + workspace, + 'scripts.clean', + "rimraf --glob ./dist './*.tsbuildinfo'", + ); + + // No non-root packages may have a "prepack" script. + workspace.unset('scripts.prepack'); + + // All non-root packages must have the same "test" script. + if (!noBuildOrTests.includes(workspaceBasename)) { + expectWorkspaceField( + workspace, + 'scripts.test', + 'vitest run --config vitest.config.ts', + ); + expectWorkspaceField( + workspace, + 'scripts.test', + 'vitest run --config vitest.config.ts', + ); + expectWorkspaceField( + workspace, + 'scripts.test:clean', + 'yarn test --no-cache --coverage.clean', + ); + expectWorkspaceField( + workspace, + 'scripts.test:dev', + 'yarn test --coverage false', + ); + expectWorkspaceField( + workspace, + 'scripts.test:verbose', + 'yarn test --reporter verbose', + ); + expectWorkspaceField( + workspace, + 'scripts.test:watch', + 'vitest --config vitest.config.ts', + ); + } + } + + // Add all packages must have the same "build" script + if (!noBuildOrTests.includes(workspaceBasename)) { + expectWorkspaceField(workspace, 'scripts.build'); + } + + if (!isChildWorkspace) { + // The root package must not specify exports. + workspace.unset('exports'); + + // The root package must specify an empty set of published files. (This + // is required in order to be able to import anything in + // development-only scripts, as otherwise the + // `node/no-unpublished-require` ESLint rule will disallow it.) + expectWorkspaceField(workspace, 'files', []); + } + + // Ensure all dependency ranges are recognizable + expectValidVersionRanges(Yarn, workspace); + + // Ensure dependency ranges are synchronized across the monorepo + expectSynchronizedRanges(Yarn, workspace); + + // Ensure dependencies are not duplicated across dependency types + expectUniqueDependencyTypes(Yarn, workspace); + + // If one workspace package lists another workspace package within + // `peerDependencies`, the dependency range must satisfy the current + // version of that package. + expectUpToDateWorkspacePeerDependencies(Yarn, workspace); + + // No dependency may be listed under both `dependencies` and + // `devDependencies`. + expectDependenciesNotInBothProdAndDev( + workspace, + dependenciesByIdentAndType, + ); + + // If one workspace package (A) lists another workspace package (B) in its + // `dependencies`, and B is a controller package, then we need to ensure + // that B is also listed in A's `peerDependencies` and that the version + // range satisfies the current version of B. + expectControllerDependenciesListedAsPeerDependencies( + Yarn, + workspace, + dependenciesByIdentAndType, + ); + + // The root workspace (and only the root workspace) must specify the Yarn + // version required for development. + if (isChildWorkspace) { + workspace.unset('packageManager'); + } else { + expectWorkspaceField(workspace, 'packageManager', 'yarn@4.2.2'); + } + + // All packages must specify a minimum Node.js version of 18.18. + expectWorkspaceField(workspace, 'engines.node', '^18.18 || >=20'); + + // All non-root public packages should be published to the NPM registry; + // all non-root private packages should not. + if (isPrivate) { + workspace.unset('publishConfig'); + } else { + expectWorkspaceField(workspace, 'publishConfig.access', 'public'); + expectWorkspaceField( + workspace, + 'publishConfig.registry', + 'https://registry.npmjs.org/', + ); + } + + if (!isPrivate) { + // All non-root packages must have a valid README.md file. + await expectReadme(workspace, workspaceBasename); + } + } + + // All version ranges in `dependencies` and `devDependencies` for the same + // dependency across the monorepo must be the same. + expectConsistentDependenciesAndDevDependencies(Yarn); + }, +}); + +/** + * Construct a nested map of dependencies. The inner layer categorizes + * instances of the same dependency by its location in the manifest; the outer + * layer categorizes the inner layer by the name of the dependency. + * + * @param {Dependency[]} dependencies - The list of dependencies to transform. + * @returns {Map>} The resulting map. + */ +function getDependenciesByIdentAndType(dependencies) { + const dependenciesByIdentAndType = new Map(); + + for (const dependency of dependencies) { + const dependenciesForIdent = dependenciesByIdentAndType.get( + dependency.ident, + ); + + if (dependenciesForIdent === undefined) { + dependenciesByIdentAndType.set( + dependency.ident, + new Map([[dependency.type, dependency]]), + ); + } else { + dependenciesForIdent.set(dependency.type, dependency); + } + } + + return dependenciesByIdentAndType; +} + +/** + * Construct a nested map of non-peer dependencies (`dependencies` and + * `devDependencies`). The inner layer categorizes instances of the same + * dependency by the version range specified; the outer layer categorizes the + * inner layer by the name of the dependency itself. + * + * @param {Dependency[]} dependencies - The list of dependencies to transform. + * @returns {Map>} The resulting map. + */ +function getNonPeerDependenciesByIdent(dependencies) { + const nonPeerDependenciesByIdent = new Map(); + + for (const dependency of dependencies) { + if (dependency.type === 'peerDependencies') { + continue; + } + + const dependencyRangesForIdent = nonPeerDependenciesByIdent.get( + dependency.ident, + ); + + if (dependencyRangesForIdent === undefined) { + nonPeerDependenciesByIdent.set( + dependency.ident, + new Map([[dependency.range, [dependency]]]), + ); + } else { + const dependenciesForDependencyRange = dependencyRangesForIdent.get( + dependency.range, + ); + + if (dependenciesForDependencyRange === undefined) { + dependencyRangesForIdent.set(dependency.range, [dependency]); + } else { + dependenciesForDependencyRange.push(dependency); + } + } + } + + return nonPeerDependenciesByIdent; +} + +/** + * Get the basename of the workspace's directory. The workspace directory is + * expected to be in the form `/`, and this function + * will extract ``. + * + * @param {Workspace} workspace - The workspace. + * @returns {string} The name of the workspace. + */ +function getWorkspaceBasename(workspace) { + return basename(workspace.cwd); +} + +/** + * Get the absolute path to a file within the workspace. + * + * @param {Workspace} workspace - The workspace. + * @param {string} path - The path to the file, relative to the workspace root. + * @returns {string} The absolute path to the file. + */ +function getWorkspacePath(workspace, path) { + return resolve(__dirname, workspace.cwd, path); +} + +/** + * Get the contents of a file within the workspace. The file is expected to be + * encoded as UTF-8. + * + * @param {Workspace} workspace - The workspace. + * @param {string} path - The path to the file, relative to the workspace root. + * @returns {Promise} The contents of the file. + */ +async function getWorkspaceFile(workspace, path) { + return await readFile(getWorkspacePath(workspace, path), 'utf8'); +} + +/** + * Expect that the workspace has the given field, and that it is a non-null + * value. If the field is not present, or is null, this will log an error, and + * cause the constraint to fail. + * + * If a value is provided, this will also verify that the field is equal to the + * given value. + * + * @param {Workspace} workspace - The workspace to check. + * @param {string} fieldName - The field to check. + * @param {unknown} [expectedValue] - The value to check. + */ +function expectWorkspaceField(workspace, fieldName, expectedValue = undefined) { + const fieldValue = get(workspace.manifest, fieldName); + + if (expectedValue) { + workspace.set(fieldName, expectedValue); + } else if (fieldValue === undefined || fieldValue === null) { + workspace.error(`Missing required field "${fieldName}".`); + } +} + +/** + * Expect that the workspace has a description, and that it is a non-empty + * string. If the description is not present, or is null, this will log an + * error, and cause the constraint to fail. + * + * This will also verify that the description does not end with a period. + * + * @param {Workspace} workspace - The workspace to check. + */ +function expectWorkspaceDescription(workspace) { + expectWorkspaceField(workspace, 'description'); + + const { description } = workspace.manifest; + + if (typeof description !== 'string') { + workspace.error( + `Expected description to be a string, but got ${typeof description}.`, + ); + return; + } + + if (description === '') { + workspace.error(`Expected description not to be an empty string.`); + return; + } + + if (description.endsWith('.')) { + workspace.set('description', description.slice(0, -1)); + } +} + +/** + * Expect that the workspace has "changelog:update" and "changelog:validate" + * scripts, and that these package scripts call a common script by passing the + * name of the package as the first argument. + * + * @param {Workspace} workspace - The workspace to check. + */ +function expectCorrectWorkspaceChangelogScripts(workspace) { + const scripts = ['update', 'validate'].reduce((obj, variant) => { + const expectedStartString = `../../scripts/${variant}-changelog.sh ${workspace.manifest.name}`; + const script = workspace.manifest.scripts[`changelog:${variant}`] ?? ''; + const match = script.match(new RegExp(`^${expectedStartString}(.*)$`, 'u')); + return { ...obj, [variant]: { expectedStartString, script, match } }; + }, {}); + + if ( + scripts.update.match && + scripts.validate.match && + scripts.update.match[1] !== scripts.validate.match[1] + ) { + workspace.error( + 'Expected package\'s "changelog:validate" and "changelog:update" scripts to pass the same arguments to their underlying scripts', + ); + } + + for (const [ + variant, + { expectedStartString, script, match }, + ] of Object.entries(scripts)) { + expectWorkspaceField(workspace, `scripts.changelog:${variant}`); + + if (script !== '' && !match) { + workspace.error( + `Expected package's "changelog:${variant}" script to be or start with "${expectedStartString}", but it was "${script}".`, + ); + } + } +} + +/** + * Expect that if the workspace package lists another workspace package within + * `peerDependencies`, the dependency range satisfies the current version of + * that package. + * + * @param {Yarn} Yarn - The Yarn "global". + * @param {Workspace} workspace - The workspace to check. + */ +function expectUpToDateWorkspacePeerDependencies(Yarn, workspace) { + for (const dependency of Yarn.dependencies({ workspace })) { + const dependencyWorkspace = Yarn.workspace({ ident: dependency.ident }); + + if ( + dependencyWorkspace !== null && + dependency.type === 'peerDependencies' + ) { + const dependencyWorkspaceVersion = new semver.SemVer( + dependencyWorkspace.manifest.version, + ); + if ( + !semver.satisfies( + dependencyWorkspace.manifest.version, + dependency.range, + ) + ) { + expectWorkspaceField( + workspace, + `peerDependencies["${dependency.ident}"]`, + `^${dependencyWorkspaceVersion.major}.0.0`, + ); + } + } + } +} + +/** + * Expect that a workspace package does not list a dependency in both + * `dependencies` and `devDependencies`. + * + * @param {Workspace} workspace - The workspace to check. + * @param {Map} dependenciesByIdentAndType - Map of + * dependency ident to dependency type and dependency. + */ +function expectDependenciesNotInBothProdAndDev( + workspace, + dependenciesByIdentAndType, +) { + for (const [ + dependencyIdent, + dependencyInstancesByType, + ] of dependenciesByIdentAndType.entries()) { + if ( + dependencyInstancesByType.size > 1 && + !dependencyInstancesByType.has('peerDependencies') + ) { + workspace.error( + `\`${dependencyIdent}\` cannot be listed in both \`dependencies\` and \`devDependencies\``, + ); + } + } +} + +/** + * Expect that if the workspace package lists another workspace package in its + * dependencies, and it is a controller package, that the controller package is + * listed in the workspace's `peerDependencies` and the version range satisfies + * the current version of the controller package. + * + * The expectation in this case is that the client will instantiate B in order + * to pass it into A. Therefore, it needs to list not only A as a dependency, + * but also B. Additionally, the version of B that the client is using with A + * needs to match the version that A itself is expecting internally. + * + * Note that this constraint does not apply for packages that seem to represent + * instantiable controllers but actually represent abstract classes. + * + * @param {Yarn} Yarn - The Yarn "global". + * @param {Workspace} workspace - The workspace to check. + * @param {Map} dependenciesByIdentAndType - Map of + * dependency ident to dependency type and dependency. + */ +function expectControllerDependenciesListedAsPeerDependencies( + Yarn, + workspace, + dependenciesByIdentAndType, +) { + for (const [ + dependencyIdent, + dependencyInstancesByType, + ] of dependenciesByIdentAndType.entries()) { + if (!dependencyInstancesByType.has('dependencies')) { + continue; + } + + const dependencyWorkspace = Yarn.workspace({ ident: dependencyIdent }); + + if ( + dependencyWorkspace !== null && + dependencyIdent.endsWith('-controller') && + dependencyIdent !== '@metamask/base-controller' && + dependencyIdent !== '@metamask/polling-controller' && + !dependencyInstancesByType.has('peerDependencies') + ) { + const dependencyWorkspaceVersion = new semver.SemVer( + dependencyWorkspace.manifest.version, + ); + expectWorkspaceField( + workspace, + `peerDependencies["${dependencyIdent}"]`, + `^${dependencyWorkspaceVersion.major}.0.0`, + ); + } + } +} + +/** + * Ensure that the version ranges in the dependencies are valid. + * + * @param {Yarn} Yarn - The Yarn "global". + * @param {Workspace} workspace - The workspace to check. + */ +function expectValidVersionRanges(Yarn, workspace) { + const isValidVersionRange = (range) => { + return ( + semver.validRange(range) || + range === 'workspace:^' || + range === 'workspace:~' + ); + }; + const dependencies = Yarn.dependencies({ workspace }); + dependencies.forEach((dep) => { + if (!isValidVersionRange(dep.range)) { + throw new Error( + `Invalid version range for dependency ${dep.ident} in ${workspace.cwd}`, + ); + } + }); +} + +/** + * Parse a version range into its components (modifier, major, minor, patch). + * Strips the modifier (^ or ~) from the version and returns the parts. + * + * @param {string} versionRange - The version range to parse. + * @returns {object} The parsed components: modifier, major, minor, and patch. + */ +function parseVersion(versionRange) { + const [modifier, version] = + versionRange.startsWith('^') || versionRange.startsWith('~') + ? [versionRange[0], versionRange.slice(1)] + : ['', versionRange]; + + const [major, minor, patch] = version.split('.').map(Number); + if ([major, minor, patch].some(isNaN)) { + throw new Error(`Invalid version range: ${versionRange}`); + } + + return { modifier, major, minor, patch }; +} + +/** + * Compare two version ranges and determine if they are out of sync. + * Compares only if the modifiers ("^", "~") are the same. + * + * @param {string} range1 - First version range. + * @param {string} range2 - Second version range. + * @returns {boolean} True if range1 is greater than range2. + */ +function versionRangeCompare(range1, range2) { + const parsed1 = parseVersion(range1); + const parsed2 = parseVersion(range2); + + // Only compare if modifiers are the same + if (parsed1.modifier !== parsed2.modifier) { + return false; + } + + // Compare major, minor, patch versions + return ( + parsed1.major > parsed2.major || + (parsed1.major === parsed2.major && parsed1.minor > parsed2.minor) || + (parsed1.major === parsed2.major && + parsed1.minor === parsed2.minor && + parsed1.patch > parsed2.patch) + ); +} + +/** + * Ensures all dependency ranges for a package are synchronized across the monorepo. + * The least version range wins, and inconsistencies throw an error. + * + * @param {Yarn} Yarn - The Yarn "global". + * @param {Workspace} workspace - The current workspace. + */ +function expectSynchronizedRanges(Yarn, workspace) { + const dependencies = Yarn.dependencies({ workspace }); + + dependencies.forEach((dep) => { + const matchingDeps = Yarn.workspaces() + .flatMap((ws) => Yarn.dependencies({ workspace: ws })) + .filter((otherDep) => otherDep.ident === dep.ident); + + matchingDeps.forEach((otherDep) => { + if ( + dep.range !== otherDep.range && + versionRangeCompare(dep.range, otherDep.range) + ) { + throw new Error( + `Dependency ${dep.ident} version out of sync between ${workspace.cwd} and another workspace. Expected ${otherDep.range}, got ${dep.range}.`, + ); + } + }); + }); +} + +/** + * Ensure that the dependency types are unique for a workspace. + * + * @param {Yarn} Yarn - The Yarn "global". + * @param {Workspace} workspace - The workspace to check. + */ +function expectUniqueDependencyTypes(Yarn, workspace) { + const dependencies = Yarn.dependencies({ workspace }); + const depMap = new Map(); + + dependencies.forEach((dep) => { + if (depMap.has(dep.ident)) { + const existingType = depMap.get(dep.ident); + if (existingType !== dep.type) { + throw new Error( + `Dependency ${dep.ident} is listed under both ${existingType} and ${dep.type} in ${workspace.cwd}`, + ); + } + } else { + depMap.set(dep.ident, dep.type); + } + }); +} + +/** + * Ensure all version ranges for a dependency across the monorepo are the same. + * Leaves conflict resolution to the user. `peerDependencies` are handled separately. + * + * @param {Yarn} Yarn - The Yarn "global". + */ +function expectConsistentDependenciesAndDevDependencies(Yarn) { + const nonPeerDependenciesByIdent = getNonPeerDependenciesByIdent( + Yarn.dependencies(), + ); + + for (const [ + dependencyIdent, + dependenciesByRange, + ] of nonPeerDependenciesByIdent.entries()) { + const dependencyRanges = [...dependenciesByRange.keys()].sort(); + if (dependenciesByRange.size > 1) { + for (const dependencies of dependenciesByRange.values()) { + for (const dependency of dependencies) { + dependency.error( + `Expected version range for ${dependencyIdent} (in ${ + dependency.type + }) to be consistent across monorepo. Pick one: ${inspect( + dependencyRanges, + )}`, + ); + } + } + } + } +} + +/** + * Expect that the workspace has a README.md file, and that it is a non-empty + * string. The README.md is expected to: + * + * - Not contain template instructions (unless the workspace is the module + * template itself). + * - Match the version of Node.js specified in the `.nvmrc` file. + * + * @param {Workspace} workspace - The workspace to check. + * @param {string} workspaceBasename - The name of the workspace. + * @returns {Promise} + */ +async function expectReadme(workspace, workspaceBasename) { + const readme = await getWorkspaceFile(workspace, 'README.md'); + + if ( + workspaceBasename !== 'metamask-module-template' && + readme.includes('## Template Instructions') + ) { + workspace.error( + 'The README.md contains template instructions. These instructions should be removed.', + ); + } + + if (!readme.includes(`yarn add @metamask/${workspaceBasename}`)) { + workspace.error( + `The README.md does not contain an example of how to install the package using Yarn (\`yarn add @metamask/${workspaceBasename}\`). Please add an example.`, + ); + } + + if (!readme.includes(`npm install @metamask/${workspaceBasename}`)) { + workspace.error( + `The README.md does not contain an example of how to install the package using npm (\`npm install @metamask/${workspaceBasename}\`). Please add an example.`, + ); + } +} diff --git a/yarn.lock b/yarn.lock index 3f8df0378..49cb9b7cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1289,7 +1289,9 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^13.0.0" "@ts-bridge/cli": "npm:^0.3.0" "@ts-bridge/shims": "npm:^0.1.1" + "@types/lodash": "npm:^4" "@types/node": "npm:^18.18.14" + "@types/semver": "npm:^7" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" "@typescript-eslint/parser": "npm:^8.1.0" "@yarnpkg/types": "npm:^4.0.0" @@ -1302,9 +1304,11 @@ __metadata: eslint-plugin-prettier: "npm:^4.2.1" eslint-plugin-promise: "npm:^6.1.1" eslint-plugin-vitest: "npm:^0.4.1" + lodash: "npm:^4.17.21" prettier: "npm:^2.7.1" prettier-plugin-packagejson: "npm:^2.3.0" rimraf: "npm:^6.0.1" + semver: "npm:^7.6.3" typedoc: "npm:^0.24.8" typescript: "npm:~5.5.4" vite: "npm:^5.3.5" @@ -1732,6 +1736,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4": + version: 4.17.7 + resolution: "@types/lodash@npm:4.17.7" + checksum: 10/b8177f19cf962414a66989837481b13f546afc2e98e8d465bec59e6ac03a59c584eb7053ce511cde3a09c5f3096d22a5ae22cfb56b23f3b0da75b0743b6b1a44 + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -1780,6 +1791,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7": + version: 7.5.8 + resolution: "@types/semver@npm:7.5.8" + checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -5901,7 +5919,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: From 818be8e4b4baccb170cd88e16d82e81f83f66d99 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 5 Sep 2024 21:18:40 +0100 Subject: [PATCH 2/8] Document exceptions --- yarn.config.cjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yarn.config.cjs b/yarn.config.cjs index b8ba9809f..106d22fb6 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -11,9 +11,14 @@ const { basename, resolve } = require('path'); const semver = require('semver'); const { inspect } = require('util'); +// Packages that do not have an entrypoint, types, or sideEffects const entrypointExceptions = ['shims', 'streams']; +// Packages that do not have typedoc const typedocExceptions = ['test-utils', 'extension']; +// Packages that do not have build or tests const noBuildOrTests = ['test-utils']; +// Packages that do not export a `package.json` file +const noPackageJson = ['extension']; /** * Aliases for the Yarn type definitions, to make the code more readable. @@ -160,7 +165,7 @@ module.exports = defineConfig({ } // All non-root packages must export a `package.json` file except for the extension package - if (workspaceBasename !== 'extension') { + if (!noPackageJson.includes(workspaceBasename)) { expectWorkspaceField( workspace, 'exports["./package.json"]', From 058b9c9f3d118ba80eee2077bceff9b5eb1fa3ed Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 5 Sep 2024 21:31:29 +0100 Subject: [PATCH 3/8] fix comment --- yarn.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.config.cjs b/yarn.config.cjs index 106d22fb6..81b614246 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -164,7 +164,7 @@ module.exports = defineConfig({ workspace.unset('sideEffects'); } - // All non-root packages must export a `package.json` file except for the extension package + // All non-root packages must export a `package.json` file except for the ones that are exempted if (!noPackageJson.includes(workspaceBasename)) { expectWorkspaceField( workspace, From f47a71ad26d2d216d80a23e75a271027ac783256 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 5 Sep 2024 21:40:09 +0100 Subject: [PATCH 4/8] remove unused --- package.json | 2 -- yarn.lock | 16 ---------------- 2 files changed, 18 deletions(-) diff --git a/package.json b/package.json index fd82157bd..588a07fc1 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,7 @@ "@metamask/eslint-config-typescript": "^13.0.0", "@ts-bridge/cli": "^0.3.0", "@ts-bridge/shims": "^0.1.1", - "@types/lodash": "^4", "@types/node": "^18.18.14", - "@types/semver": "^7", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "@yarnpkg/types": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 49cb9b7cd..3d83c1212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1289,9 +1289,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^13.0.0" "@ts-bridge/cli": "npm:^0.3.0" "@ts-bridge/shims": "npm:^0.1.1" - "@types/lodash": "npm:^4" "@types/node": "npm:^18.18.14" - "@types/semver": "npm:^7" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" "@typescript-eslint/parser": "npm:^8.1.0" "@yarnpkg/types": "npm:^4.0.0" @@ -1736,13 +1734,6 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4": - version: 4.17.7 - resolution: "@types/lodash@npm:4.17.7" - checksum: 10/b8177f19cf962414a66989837481b13f546afc2e98e8d465bec59e6ac03a59c584eb7053ce511cde3a09c5f3096d22a5ae22cfb56b23f3b0da75b0743b6b1a44 - languageName: node - linkType: hard - "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -1791,13 +1782,6 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 - languageName: node - linkType: hard - "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" From 7229d21cb81c44b4f37c7a316abd45abe812b2c3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Fri, 6 Sep 2024 02:52:27 -0700 Subject: [PATCH 5/8] refactor: (WIP) Enable ts-check in yarn.config.js (#48) This is a half-finished attempt to enable `// @ts-check` in `yarn.config.js`. See review for more details. --- yarn.config.cjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yarn.config.cjs b/yarn.config.cjs index 81b614246..6b9cf9fd1 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -1,11 +1,12 @@ +// @ts-check // This file is used to define, among other configuration, rules that Yarn will // execute when you run `yarn constraints`. These rules primarily check the // manifests of each package in the monorepo to ensure they follow a standard // format, but also check the presence of certain files as well. -/** @type {import('@yarnpkg/types')} */ const { defineConfig } = require('@yarnpkg/types'); const { readFile } = require('fs/promises'); +// @ts-expect-error lodash is not a valid module, but that's OK const { get } = require('lodash'); const { basename, resolve } = require('path'); const semver = require('semver'); @@ -23,10 +24,10 @@ const noPackageJson = ['extension']; /** * Aliases for the Yarn type definitions, to make the code more readable. * - * @typedef {import('@yarnpkg/types').Yarn} Yarn - * @typedef {import('@yarnpkg/types').Yarn.Constraints.Workspace} Workspace + * @typedef {import('@yarnpkg/types').Yarn.Constraints.Context['Yarn']} Yarn * @typedef {import('@yarnpkg/types').Yarn.Constraints.Dependency} Dependency * @typedef {import('@yarnpkg/types').Yarn.Constraints.DependencyType} DependencyType + * @typedef {import('@yarnpkg/types').Yarn.Constraints.Workspace} Workspace */ module.exports = defineConfig({ From 969baec8adbfa21a29b57919ba59da0f35334c17 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 6 Sep 2024 10:59:09 +0100 Subject: [PATCH 6/8] fix types --- yarn.config.cjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/yarn.config.cjs b/yarn.config.cjs index 6b9cf9fd1..347536e25 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -6,7 +6,6 @@ const { defineConfig } = require('@yarnpkg/types'); const { readFile } = require('fs/promises'); -// @ts-expect-error lodash is not a valid module, but that's OK const { get } = require('lodash'); const { basename, resolve } = require('path'); const semver = require('semver'); @@ -480,6 +479,9 @@ function expectWorkspaceDescription(workspace) { * @param {Workspace} workspace - The workspace to check. */ function expectCorrectWorkspaceChangelogScripts(workspace) { + /** + * @type {Record} + */ const scripts = ['update', 'validate'].reduce((obj, variant) => { const expectedStartString = `../../scripts/${variant}-changelog.sh ${workspace.manifest.name}`; const script = workspace.manifest.scripts[`changelog:${variant}`] ?? ''; @@ -551,7 +553,7 @@ function expectUpToDateWorkspacePeerDependencies(Yarn, workspace) { * `dependencies` and `devDependencies`. * * @param {Workspace} workspace - The workspace to check. - * @param {Map} dependenciesByIdentAndType - Map of + * @param {Map>} dependenciesByIdentAndType - Map of * dependency ident to dependency type and dependency. */ function expectDependenciesNotInBothProdAndDev( @@ -589,7 +591,7 @@ function expectDependenciesNotInBothProdAndDev( * * @param {Yarn} Yarn - The Yarn "global". * @param {Workspace} workspace - The workspace to check. - * @param {Map} dependenciesByIdentAndType - Map of + * @param {Map>} dependenciesByIdentAndType - Map of * dependency ident to dependency type and dependency. */ function expectControllerDependenciesListedAsPeerDependencies( From ea980a8d6b8e45327f572dfb9a79d748d50a8f10 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 6 Sep 2024 11:00:41 +0100 Subject: [PATCH 7/8] remove redundant --- yarn.config.cjs | 63 ------------------------------------------------- 1 file changed, 63 deletions(-) diff --git a/yarn.config.cjs b/yarn.config.cjs index 347536e25..77fe09994 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -266,16 +266,6 @@ module.exports = defineConfig({ dependenciesByIdentAndType, ); - // If one workspace package (A) lists another workspace package (B) in its - // `dependencies`, and B is a controller package, then we need to ensure - // that B is also listed in A's `peerDependencies` and that the version - // range satisfies the current version of B. - expectControllerDependenciesListedAsPeerDependencies( - Yarn, - workspace, - dependenciesByIdentAndType, - ); - // The root workspace (and only the root workspace) must specify the Yarn // version required for development. if (isChildWorkspace) { @@ -575,59 +565,6 @@ function expectDependenciesNotInBothProdAndDev( } } -/** - * Expect that if the workspace package lists another workspace package in its - * dependencies, and it is a controller package, that the controller package is - * listed in the workspace's `peerDependencies` and the version range satisfies - * the current version of the controller package. - * - * The expectation in this case is that the client will instantiate B in order - * to pass it into A. Therefore, it needs to list not only A as a dependency, - * but also B. Additionally, the version of B that the client is using with A - * needs to match the version that A itself is expecting internally. - * - * Note that this constraint does not apply for packages that seem to represent - * instantiable controllers but actually represent abstract classes. - * - * @param {Yarn} Yarn - The Yarn "global". - * @param {Workspace} workspace - The workspace to check. - * @param {Map>} dependenciesByIdentAndType - Map of - * dependency ident to dependency type and dependency. - */ -function expectControllerDependenciesListedAsPeerDependencies( - Yarn, - workspace, - dependenciesByIdentAndType, -) { - for (const [ - dependencyIdent, - dependencyInstancesByType, - ] of dependenciesByIdentAndType.entries()) { - if (!dependencyInstancesByType.has('dependencies')) { - continue; - } - - const dependencyWorkspace = Yarn.workspace({ ident: dependencyIdent }); - - if ( - dependencyWorkspace !== null && - dependencyIdent.endsWith('-controller') && - dependencyIdent !== '@metamask/base-controller' && - dependencyIdent !== '@metamask/polling-controller' && - !dependencyInstancesByType.has('peerDependencies') - ) { - const dependencyWorkspaceVersion = new semver.SemVer( - dependencyWorkspace.manifest.version, - ); - expectWorkspaceField( - workspace, - `peerDependencies["${dependencyIdent}"]`, - `^${dependencyWorkspaceVersion.major}.0.0`, - ); - } - } -} - /** * Ensure that the version ranges in the dependencies are valid. * From c409381b41754b66af50f48e022d8d92fbf18561 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 6 Sep 2024 19:13:43 +0100 Subject: [PATCH 8/8] Add lodash types --- .depcheckrc.yml | 1 + package.json | 1 + yarn.lock | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 88adbb10c..decedf5e3 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -10,3 +10,4 @@ ignores: - 'prettier-plugin-packagejson' - 'rimraf' - 'typedoc' + - '@types/lodash' diff --git a/package.json b/package.json index 588a07fc1..8a7e6c75f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@metamask/eslint-config-typescript": "^13.0.0", "@ts-bridge/cli": "^0.3.0", "@ts-bridge/shims": "^0.1.1", + "@types/lodash": "^4.17.7", "@types/node": "^18.18.14", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", diff --git a/yarn.lock b/yarn.lock index 3d83c1212..7587fa559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1289,6 +1289,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^13.0.0" "@ts-bridge/cli": "npm:^0.3.0" "@ts-bridge/shims": "npm:^0.1.1" + "@types/lodash": "npm:^4.17.7" "@types/node": "npm:^18.18.14" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" "@typescript-eslint/parser": "npm:^8.1.0" @@ -1734,6 +1735,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.17.7": + version: 4.17.7 + resolution: "@types/lodash@npm:4.17.7" + checksum: 10/b8177f19cf962414a66989837481b13f546afc2e98e8d465bec59e6ac03a59c584eb7053ce511cde3a09c5f3096d22a5ae22cfb56b23f3b0da75b0743b6b1a44 + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5"