-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Workspace.ts
230 lines (180 loc) Β· 8.29 KB
/
Workspace.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import {PortablePath, npath, ppath, xfs, Filename} from '@yarnpkg/fslib';
import fastGlob from 'fast-glob';
import {HardDependencies, Manifest} from './Manifest';
import {Project} from './Project';
import {WorkspaceResolver} from './WorkspaceResolver';
import * as formatUtils from './formatUtils';
import * as hashUtils from './hashUtils';
import * as semverUtils from './semverUtils';
import * as structUtils from './structUtils';
import {Descriptor, Locator} from './types';
export class Workspace {
public readonly project: Project;
public readonly cwd: PortablePath;
// @ts-expect-error: This variable is set during the setup process
public readonly relativeCwd: PortablePath;
// @ts-expect-error: This variable is set during the setup process
public readonly anchoredDescriptor: Descriptor;
// @ts-expect-error: This variable is set during the setup process
public readonly anchoredLocator: Locator;
public readonly workspacesCwds: Set<PortablePath> = new Set();
// @ts-expect-error: This variable is set during the setup process
public manifest: Manifest;
constructor(workspaceCwd: PortablePath, {project}: {project: Project}) {
this.project = project;
this.cwd = workspaceCwd;
}
async setup() {
this.manifest = await Manifest.tryFind(this.cwd) ?? new Manifest();
// We use ppath.relative to guarantee that the default hash will be consistent even if the project is installed on different OS / path
// @ts-expect-error: It's ok to initialize it now, even if it's readonly (setup is called right after construction)
this.relativeCwd = ppath.relative(this.project.cwd, this.cwd) || PortablePath.dot;
const ident = this.manifest.name ? this.manifest.name : structUtils.makeIdent(null, `${this.computeCandidateName()}-${hashUtils.makeHash<string>(this.relativeCwd).substring(0, 6)}`);
// @ts-expect-error: It's ok to initialize it now, even if it's readonly (setup is called right after construction)
this.anchoredDescriptor = structUtils.makeDescriptor(ident, `${WorkspaceResolver.protocol}${this.relativeCwd}`);
// @ts-expect-error: It's ok to initialize it now, even if it's readonly (setup is called right after construction)
this.anchoredLocator = structUtils.makeLocator(ident, `${WorkspaceResolver.protocol}${this.relativeCwd}`);
const patterns = this.manifest.workspaceDefinitions.map(({pattern}) => pattern);
if (patterns.length === 0)
return;
const relativeCwds = await fastGlob(patterns, {
cwd: npath.fromPortablePath(this.cwd),
onlyDirectories: true,
ignore: [`**/node_modules`, `**/.git`, `**/.yarn`],
});
// fast-glob returns results in arbitrary order
relativeCwds.sort();
await relativeCwds.reduce(async (previousTask, relativeCwd) => {
const candidateCwd = ppath.resolve(this.cwd, npath.toPortablePath(relativeCwd));
const exists = await xfs.existsPromise(ppath.join(candidateCwd, `package.json`));
// Ensure candidateCwds are added in order
await previousTask;
if (exists) {
this.workspacesCwds.add(candidateCwd);
}
}, Promise.resolve());
}
get anchoredPackage() {
const pkg = this.project.storedPackages.get(this.anchoredLocator.locatorHash);
if (!pkg)
throw new Error(`Assertion failed: Expected workspace ${structUtils.prettyWorkspace(this.project.configuration, this)} (${formatUtils.pretty(this.project.configuration, ppath.join(this.cwd, Filename.manifest), formatUtils.Type.PATH)}) to have been resolved. Run "yarn install" to update the lockfile`);
return pkg;
}
accepts(range: string) {
const protocolIndex = range.indexOf(`:`);
const protocol = protocolIndex !== -1
? range.slice(0, protocolIndex + 1)
: null;
const pathname = protocolIndex !== -1
? range.slice(protocolIndex + 1)
: range;
if (protocol === WorkspaceResolver.protocol && ppath.normalize(pathname as PortablePath) === this.relativeCwd)
return true;
if (protocol === WorkspaceResolver.protocol && (pathname === `*` || pathname === `^` || pathname === `~`))
return true;
const semverRange = semverUtils.validRange(pathname);
if (!semverRange)
return false;
if (protocol === WorkspaceResolver.protocol)
return semverRange.test(this.manifest.version ?? `0.0.0`);
if (!this.project.configuration.get(`enableTransparentWorkspaces`))
return false;
if (this.manifest.version !== null)
return semverRange.test(this.manifest.version);
return false;
}
computeCandidateName() {
if (this.cwd === this.project.cwd) {
return `root-workspace`;
} else {
return `${ppath.basename(this.cwd)}` || `unnamed-workspace`;
}
}
/**
* Find workspaces marked as dependencies/devDependencies of the current workspace recursively.
*
* @param rootWorkspace root workspace
* @param project project
*
* @returns all the workspaces marked as dependencies
*/
getRecursiveWorkspaceDependencies({dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) {
const workspaceList = new Set<Workspace>();
const visitWorkspace = (workspace: Workspace) => {
for (const dependencyType of dependencies) {
// Quick note: it means that if we have, say, a workspace in
// dev dependencies but not in dependencies, this workspace will be
// traversed (even if dependencies traditionally override dev
// dependencies). It's not clear which behaviour is better, but
// at least it's consistent.
for (const descriptor of workspace.manifest[dependencyType].values()) {
const foundWorkspace = this.project.tryWorkspaceByDescriptor(descriptor);
if (foundWorkspace === null || workspaceList.has(foundWorkspace))
continue;
workspaceList.add(foundWorkspace);
visitWorkspace(foundWorkspace);
}
}
};
visitWorkspace(this);
return workspaceList;
}
/**
* Find workspaces which include the current workspace as a dependency/devDependency recursively.
*
* @param rootWorkspace root workspace
* @param project project
*
* @returns all the workspaces marked as dependents
*/
getRecursiveWorkspaceDependents({dependencies = Manifest.hardDependencies}: {dependencies?: Array<HardDependencies>} = {}) {
const workspaceList = new Set<Workspace>();
const visitWorkspace = (workspace: Workspace) => {
for (const projectWorkspace of this.project.workspaces) {
const isDependent = dependencies.some(dependencyType => {
return [...projectWorkspace.manifest[dependencyType].values()].some(descriptor => {
const foundWorkspace = this.project.tryWorkspaceByDescriptor(descriptor);
return foundWorkspace !== null && structUtils.areLocatorsEqual(foundWorkspace.anchoredLocator, workspace.anchoredLocator);
});
});
if (isDependent && !workspaceList.has(projectWorkspace)) {
workspaceList.add(projectWorkspace);
visitWorkspace(projectWorkspace);
}
}
};
visitWorkspace(this);
return workspaceList;
}
/**
* Retrieves all the child workspaces of a given root workspace recursively
*
* @param rootWorkspace root workspace
* @param project project
*
* @returns all the child workspaces
*/
getRecursiveWorkspaceChildren() {
const workspaceSet = new Set<Workspace>([this]);
for (const workspace of workspaceSet) {
for (const childWorkspaceCwd of workspace.workspacesCwds) {
const childWorkspace = this.project.workspacesByCwd.get(childWorkspaceCwd);
if (childWorkspace) {
workspaceSet.add(childWorkspace);
}
}
}
workspaceSet.delete(this);
return Array.from(workspaceSet);
}
async persistManifest() {
const data = {};
this.manifest.exportTo(data);
const path = ppath.join(this.cwd, Manifest.fileName);
const content = `${JSON.stringify(data, null, this.manifest.indent)}\n`;
await xfs.changeFilePromise(path, content, {
automaticNewlines: true,
});
this.manifest.raw = data;
}
}