Skip to content

Commit e6b17bf

Browse files
authored
Migrates the constraints on the Yarn repository from Prolog to JS (#5553)
**What's the problem this PR addresses?** We were still using the old constraint engine. **How did you fix it?** Migrated it to the newer JS engine. **Checklist** <!--- Don't worry if you miss something, chores are automatically tested. --> <!--- This checklist exists to help you remember doing the chores when you submit a PR. --> <!--- Put an `x` in all the boxes that apply. --> - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [x] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [x] I will check that all automated PR checks pass before the PR gets reviewed.
1 parent a260a26 commit e6b17bf

File tree

8 files changed

+220
-94
lines changed

8 files changed

+220
-94
lines changed

.pnp.cjs

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.yarn/versions/9d863e8d.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
releases:
2+
"@yarnpkg/cli": major
3+
"@yarnpkg/plugin-constraints": major
4+
"@yarnpkg/types": major
5+
6+
declined:
7+
- "@yarnpkg/plugin-compat"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-init"
11+
- "@yarnpkg/plugin-interactive-tools"
12+
- "@yarnpkg/plugin-nm"
13+
- "@yarnpkg/plugin-npm-cli"
14+
- "@yarnpkg/plugin-pack"
15+
- "@yarnpkg/plugin-patch"
16+
- "@yarnpkg/plugin-pnp"
17+
- "@yarnpkg/plugin-pnpm"
18+
- "@yarnpkg/plugin-stage"
19+
- "@yarnpkg/plugin-typescript"
20+
- "@yarnpkg/plugin-version"
21+
- "@yarnpkg/plugin-workspace-tools"
22+
- "@yarnpkg/builder"
23+
- "@yarnpkg/core"
24+
- "@yarnpkg/doctor"

constraints.pro

+5-91
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,5 @@
1-
constraints_min_version(1).
2-
3-
% This file is written in Prolog
4-
% It contains rules that the project must respect.
5-
% In order to see them in action, run `yarn constraints source`
6-
7-
% This rule will enforce that a workspace MUST depend on the same version of a dependency as the one used by the other workspaces
8-
% We allow Docusaurus to have different dependencies for now; will be addressed later (when we remove Gatsby)
9-
gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange2, DependencyType) :-
10-
% Iterates over all dependencies from all workspaces
11-
workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType),
12-
WorkspaceCwd \= 'packages/docusaurus',
13-
% Iterates over similarly-named dependencies from all workspaces (again)
14-
workspace_has_dependency(OtherWorkspaceCwd, DependencyIdent, DependencyRange2, DependencyType2),
15-
OtherWorkspaceCwd \= 'packages/docusaurus',
16-
% Ignore peer dependencies
17-
DependencyType \= 'peerDependencies',
18-
DependencyType2 \= 'peerDependencies',
19-
% Ignore devDependencies on other workspaces
20-
(
21-
(DependencyType = 'devDependencies'; DependencyType2 = 'devDependencies') ->
22-
\+ workspace_ident(DependencyCwd, DependencyIdent)
23-
;
24-
true
25-
).
26-
27-
% This rule will prevent workspaces from depending on non-workspace versions of available workspaces
28-
gen_enforced_dependency(WorkspaceCwd, DependencyIdent, 'workspace:^', DependencyType) :-
29-
% Iterates over all dependencies from all workspaces
30-
workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType),
31-
% Only consider those that target something that could be a workspace
32-
workspace_ident(DependencyCwd, DependencyIdent).
33-
34-
% This rule enforces that all packages must not depend on inquirer - we use enquirer instead
35-
gen_enforced_dependency(WorkspaceCwd, 'inquirer', null, DependencyType) :-
36-
workspace_has_dependency(WorkspaceCwd, 'inquirer', _, DependencyType).
37-
38-
% This rule enforces that all packages that depend on TypeScript must also depend on tslib
39-
gen_enforced_dependency(WorkspaceCwd, 'tslib', 'range', 'dependencies') :-
40-
% Iterates over all TypeScript dependencies from all workspaces
41-
workspace_has_dependency(WorkspaceCwd, 'typescript', _, DependencyType),
42-
% Ignores the case when TypeScript is a peer dependency
43-
DependencyType \= 'peerDependencies',
44-
% Only proceed if the workspace doesn't already depend on tslib
45-
\+ workspace_has_dependency(WorkspaceCwd, 'tslib', _, _).
46-
47-
% This rule will enforce that all packages must have a "BSD-2-Clause" license field
48-
gen_enforced_field(WorkspaceCwd, 'license', 'BSD-2-Clause').
49-
50-
% This rule will enforce that all packages must have an correct engines.node field
51-
% Keep in sync with the range inside packages/yarnpkg-cli/sources/main.ts
52-
gen_enforced_field(WorkspaceCwd, 'engines.node', '>=18.12.0').
53-
54-
% Required to make the package work with the GitHub Package Registry
55-
gen_enforced_field(WorkspaceCwd, 'repository.type', 'git').
56-
gen_enforced_field(WorkspaceCwd, 'repository.url', 'ssh://[email protected]/yarnpkg/berry.git').
57-
gen_enforced_field(WorkspaceCwd, 'repository.directory', WorkspaceCwd).
58-
59-
% This rule will require that the plugins that aren't embed in the CLI list a specific script that'll
60-
% be called as part of our release process (to rebuild them in the context of our repository)
61-
gen_enforced_field(WorkspaceCwd, 'scripts.update-local', '<any value>') :-
62-
% Obtain the path for the CLI
63-
workspace_ident(CliCwd, '@yarnpkg/cli'),
64-
% Iterates over all workspaces whose name is prefixed with "@yarnpkg/plugin-"
65-
workspace_ident(WorkspaceCwd, WorkspaceIdent),
66-
atom_concat('@yarnpkg/plugin-', _, WorkspaceIdent),
67-
% Select those that are not included in the CLI bundle array
68-
\+ workspace_field_test(CliCwd, '@yarnpkg/builder.bundles.standard', '$$.includes($0)', [WorkspaceIdent]),
69-
% Only if they don't have a script set
70-
\+ workspace_field(WorkspaceCwd, 'scripts.update-local', _).
71-
72-
inline_compile('@yarnpkg/eslint-config').
73-
inline_compile('@yarnpkg/libui').
74-
75-
gen_enforced_field(WorkspaceCwd, 'scripts.prepack', 'run build:compile "$(pwd)"') :-
76-
workspace(WorkspaceCwd),
77-
% This package is built using Rollup, so we allow it to configure its build scripts itself
78-
\+ workspace_ident(WorkspaceCwd, '@yarnpkg/pnp'),
79-
% Those packages use a different build
80-
\+ (workspace_ident(WorkspaceCwd, WorkspaceIdent), inline_compile(WorkspaceIdent)),
81-
% Private packages aren't covered
82-
\+ workspace_field_test(WorkspaceCwd, 'private', 'true').
83-
84-
gen_enforced_field(WorkspaceCwd, 'scripts.postpack', 'rm -rf lib') :-
85-
workspace(WorkspaceCwd),
86-
% This package is built using Rollup, so we allow it to configure its build scripts itself
87-
\+ workspace_ident(WorkspaceCwd, '@yarnpkg/pnp'),
88-
% Those packages use a different build
89-
\+ (workspace_ident(WorkspaceCwd, WorkspaceIdent), inline_compile(WorkspaceIdent)),
90-
% Private packages aren't covered
91-
\+ workspace_field_test(WorkspaceCwd, 'private', 'true').
1+
% ## Prolog constraints are deprecated ! ##
2+
%
3+
% Check this other file in this same repository to see the new way to write constraints, using JS/TS:
4+
%
5+
% yarn.config.js

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"node": ">=18.12.0"
9595
},
9696
"dependencies": {
97+
"@yarnpkg/types": "workspace:^",
9798
"chalk": "^3.0.0"
9899
}
99100
}

packages/plugin-constraints/sources/ModernEngine.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class ModernEngine implements constraintUtils.Engine {
99

1010
private createEnvironment() {
1111
const workspaces = new constraintUtils.Index<Yarn.Constraints.Workspace>([`cwd`, `ident`]);
12-
const dependencies = new constraintUtils.Index<Yarn.Constraints.Dependency>([`type`, `ident`]);
12+
const dependencies = new constraintUtils.Index<Yarn.Constraints.Dependency>([`workspace`, `type`, `ident`]);
1313

1414
const result: constraintUtils.ProcessResult = {
1515
manifestUpdates: new Map(),
@@ -42,7 +42,7 @@ export class ModernEngine implements constraintUtils.Engine {
4242
};
4343

4444
const workspaceItem = workspaces.insert({
45-
cwd: workspace.cwd,
45+
cwd: workspace.relativeCwd,
4646
ident,
4747
manifest,
4848
set: setFn,
@@ -93,6 +93,9 @@ export class ModernEngine implements constraintUtils.Engine {
9393
workspaces: filter => {
9494
return env.workspaces.find(filter);
9595
},
96+
dependency: ((filter?: Yarn.Constraints.DependencyFilter) => {
97+
return env.dependencies.find(filter)[0] ?? null;
98+
}) as any,
9699
dependencies: filter => {
97100
return env.dependencies.find(filter);
98101
},

packages/yarnpkg-types/sources/constraints.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export type Workspace = {
6767
/**
6868
* Raw manifest object for the workspace.
6969
*/
70-
manifest: PartialObject;
70+
manifest: any;
7171

7272
/**
7373
* Report an error unless the workspace lists the specified property
@@ -113,6 +113,11 @@ export type WorkspaceFilter = {
113113
};
114114

115115
export type DependencyFilter = {
116+
/**
117+
* Only return dependencies from the given workspace.
118+
*/
119+
workspace?: Workspace;
120+
116121
/**
117122
* Only return dependencies with the given name.
118123
*/
@@ -135,6 +140,14 @@ export type Yarn = {
135140
*/
136141
workspaces(filter?: WorkspaceFilter): Array<Workspace>;
137142

143+
/**
144+
* Select a unique workspace according to the provided filter.
145+
*
146+
* @param filter
147+
*/
148+
dependency(): Dependency;
149+
dependency(filter?: DependencyFilter): Dependency | null;
150+
138151
/**
139152
* Select all dependencies according to the provided filter.
140153
*

yarn.config.js

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// @ts-check
2+
3+
/** @type {import('@yarnpkg/types')} */
4+
const {defineConfig} = require(`@yarnpkg/types`);
5+
6+
/**
7+
* @typedef {import('@yarnpkg/types').Yarn.Constraints.Context} Context
8+
* @typedef {import('@yarnpkg/types').Yarn.Constraints.Workspace} Workspace
9+
* @typedef {import('@yarnpkg/types').Yarn.Constraints.Dependency} Dependency
10+
*/
11+
12+
/**
13+
* This rule will enforce that a workspace MUST depend on the same version of a dependency as the one used by the other workspaces
14+
* We allow Docusaurus to have different dependencies for now; will be addressed later (when we remove Gatsby)
15+
* @param {Context} context
16+
*/
17+
function enforceConsistentDependenciesAcrossTheProject({Yarn}) {
18+
for (const dependency of Yarn.dependencies()) {
19+
if (dependency.workspace.cwd === `packages/docusaurus`)
20+
continue;
21+
22+
if (dependency.type === `peerDependencies`)
23+
continue;
24+
25+
for (const otherDependency of Yarn.dependencies({ident: dependency.ident})) {
26+
if (otherDependency.workspace.cwd === `packages/docusaurus`)
27+
continue;
28+
29+
if (otherDependency.type === `peerDependencies`)
30+
continue;
31+
32+
if ((dependency.type === `devDependencies` || otherDependency.type === `devDependencies`) && Yarn.workspace({ident: otherDependency.ident}))
33+
continue;
34+
35+
dependency.update(otherDependency.range);
36+
}
37+
}
38+
}
39+
40+
/**
41+
* This rule will enforce that a workspace MUST depend on the same version of a dependency as the one used by the other workspaces
42+
* We allow Docusaurus to have different dependencies for now; will be addressed later (when we remove Gatsby)
43+
* @param {Context} context
44+
*/
45+
function enforceWorkspaceDependenciesWhenPossible({Yarn}) {
46+
for (const dependency of Yarn.dependencies()) {
47+
if (!Yarn.workspace({ident: dependency.ident}))
48+
continue;
49+
50+
dependency.update(`workspace:^`);
51+
}
52+
}
53+
54+
/**
55+
* @param {Context} context
56+
* @param {string} ident
57+
* @param {string} explanation
58+
*/
59+
function forbidDependency({Yarn}, ident, explanation) {
60+
for (const dependency of Yarn.dependencies({ident})) {
61+
dependency.error(explanation);
62+
}
63+
}
64+
65+
/**
66+
* @param {Context} context
67+
* @param {Record<string, ((workspace: Workspace) => any) | string>} fields
68+
*/
69+
function enforceFieldsOnAllWorkspaces({Yarn}, fields) {
70+
for (const workspace of Yarn.workspaces()) {
71+
for (const [field, value] of Object.entries(fields)) {
72+
workspace.set(field, typeof value === `function` ? value(workspace) : value);
73+
}
74+
}
75+
}
76+
77+
/**
78+
* @param {Context} context
79+
*/
80+
function enforceUpdateLocalScripts({Yarn}) {
81+
const cli = Yarn.workspace({ident: `@yarnpkg/cli`});
82+
if (!cli)
83+
throw new Error(`Assertion failed: We need the @yarnpkg/cli workspace to be present`);
84+
85+
for (const workspace of Yarn.workspaces()) {
86+
if (!workspace.ident?.startsWith(`@yarnpkg/plugin-`))
87+
continue;
88+
89+
if (cli.manifest[`@yarnpkg/builder`].bundles.standard.includes(workspace.ident))
90+
continue;
91+
92+
if (!workspace.manifest.scripts?.[`update-local`]) {
93+
workspace.error(`This workspace is missing an update-local script`);
94+
}
95+
}
96+
}
97+
98+
/**
99+
* @param {Context} context
100+
*/
101+
function enforcePrepackScripts({Yarn}) {
102+
const OMIT_FROM_PREPACK = new Set([
103+
// This package is built using Rollup, so we allow it to configure its build scripts itself
104+
`@yarnpkg/pnp`,
105+
// Those packages use a different build
106+
`@yarnpkg/eslint-config`,
107+
`@yarnpkg/libui`,
108+
]);
109+
110+
for (const workspace of Yarn.workspaces()) {
111+
if (workspace.manifest.private)
112+
continue;
113+
114+
if (!workspace.ident || OMIT_FROM_PREPACK.has(workspace.ident))
115+
continue;
116+
117+
workspace.set(`scripts.prepack`, `run build:compile "$(pwd)"`);
118+
workspace.set(`scripts.postpack`, `rm -rf lib`);
119+
}
120+
}
121+
122+
/**
123+
* @param {Context} context
124+
* @param {string} ident
125+
* @param {string} otherIdent
126+
* @param {boolean} mustExist
127+
*/
128+
function enforceDependencyRelationship({Yarn}, ident, otherIdent, mustExist) {
129+
for (const dependency of Yarn.dependencies({ident})) {
130+
if (dependency.type === `peerDependencies`)
131+
continue;
132+
133+
const hasOtherDependency = Yarn.dependency({
134+
workspace: dependency.workspace,
135+
ident: otherIdent,
136+
});
137+
138+
if (mustExist) {
139+
if (hasOtherDependency)
140+
continue;
141+
142+
dependency.error(`The presence of ${ident} in ${dependency.type} mandates the presence of ${otherIdent}`);
143+
} else {
144+
if (!hasOtherDependency)
145+
continue;
146+
147+
dependency.error(`The presence of ${ident} in ${dependency.type} forbids the presence of ${otherIdent}`);
148+
}
149+
}
150+
}
151+
152+
module.exports = defineConfig({
153+
constraints: async ctx => {
154+
enforceConsistentDependenciesAcrossTheProject(ctx);
155+
enforceWorkspaceDependenciesWhenPossible(ctx);
156+
forbidDependency(ctx, `inquirer`, `Don't depend on inquirer - we use enquirer instead`);
157+
enforceDependencyRelationship(ctx, `typescript`, `tslib`, true);
158+
enforceUpdateLocalScripts(ctx);
159+
enforcePrepackScripts(ctx);
160+
enforceFieldsOnAllWorkspaces(ctx, {
161+
license: `BSD-2-Clause`,
162+
[`engines.node`]: `>=18.12.0`,
163+
[`repository.type`]: `git`,
164+
[`repository.url`]: `ssh://[email protected]/yarnpkg/berry.git`,
165+
[`repository.directory`]: workspace => workspace.cwd,
166+
});
167+
},
168+
});

yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -7351,6 +7351,7 @@ __metadata:
73517351
"@yarnpkg/fslib": "workspace:^"
73527352
"@yarnpkg/libzip": "workspace:^"
73537353
"@yarnpkg/sdks": "workspace:^"
7354+
"@yarnpkg/types": "workspace:^"
73547355
chalk: "npm:^3.0.0"
73557356
clipanion: "npm:^3.2.1"
73567357
esbuild-wasm: "npm:0.17.5"

0 commit comments

Comments
 (0)