@vltpkg/workspaces 1.0.0-rc.23 → 1.0.0-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ import type { DepID } from '@vltpkg/dep-id';
2
+ import { PackageJson } from '@vltpkg/package-json';
3
+ import type { NormalizedManifest } from '@vltpkg/types';
4
+ import type { DepResults } from '@vltpkg/graph-run';
5
+ import { PathScurry } from 'path-scurry';
6
+ export type WorkspacesLoadedConfig = {
7
+ workspace?: string[];
8
+ 'workspace-group'?: string[];
9
+ };
10
+ /**
11
+ * The object passed to the constructor or {@link Monorepo#load} to limit which
12
+ * {@link Workspace Workspaces} get loaded.
13
+ */
14
+ export type LoadQuery = {
15
+ /**
16
+ * A glob pattern string, or an array of them. Only workspaces found
17
+ * in paths matched will be loaded.
18
+ */
19
+ paths?: string[] | string;
20
+ /**
21
+ * A string, or an array of strings. If set, only workspaces in the
22
+ * specified groups named will be included, if set.
23
+ */
24
+ groups?: string[] | string;
25
+ };
26
+ /**
27
+ * Canonical form of the {@link WorkspaceConfig}, used
28
+ * internally for consistency.
29
+ */
30
+ export type WorkspaceConfigObject = Record<string, string[]>;
31
+ /**
32
+ * Allowed datatype in the `workspaces` field of the `vlt.json` file.
33
+ */
34
+ export type WorkspaceConfig = string[] | WorkspaceConfigObject | string;
35
+ /**
36
+ * Turn a {@link WorkspaceConfig} into a
37
+ * {@link WorkspaceConfigObject}, or throw if it's not valid.
38
+ */
39
+ export declare const asWSConfig: (conf: unknown, path?: string) => WorkspaceConfigObject;
40
+ /**
41
+ * Throw if the provided value is not a valid {@link WorkspaceConfig}
42
+ */
43
+ export declare const assertWSConfig: (conf: unknown, path?: string) => asserts conf is WorkspaceConfig;
44
+ export type MonorepoOptions = {
45
+ /**
46
+ * A {@link PackageJson} object, for sharing manifest caches
47
+ */
48
+ packageJson?: PackageJson;
49
+ /**
50
+ * A {@link PathScurry} object, for use in globs
51
+ */
52
+ scurry?: PathScurry;
53
+ /**
54
+ * Parsed normalized contents of the workspaces from a `vlt.json`
55
+ * file
56
+ */
57
+ config?: WorkspaceConfigObject;
58
+ /**
59
+ * If set, then {@link Monorepo#load} will be called immediately with
60
+ * this argument.
61
+ */
62
+ load?: LoadQuery;
63
+ };
64
+ /**
65
+ * Class representing a Monorepo containing multiple workspaces.
66
+ *
67
+ * Does not automatically look up the root, but that can be provided by
68
+ * running `Config.load()`, since it stops seeking the route when a
69
+ * `vlt.json` file is encountered.
70
+ */
71
+ export declare class Monorepo {
72
+ #private;
73
+ /** The project root where vlt.json is found */
74
+ projectRoot: string;
75
+ /** Scurry object to cache all filesystem calls (mostly globs) */
76
+ scurry: PathScurry;
77
+ packageJson: PackageJson;
78
+ /**
79
+ * Number of {@link Workspace} objects loaded in this Monorepo
80
+ */
81
+ get size(): number;
82
+ constructor(projectRoot: string, options?: MonorepoOptions);
83
+ /**
84
+ * Load the workspace definitions from vlt.json,
85
+ * canonicalizing the result into the effective `{[group:string]:string[]}`
86
+ * form.
87
+ *
88
+ * Eg:
89
+ * - `"src/*"` => `{packages:["src/*"]}`
90
+ * - `{"apps": "src/*"}` => `{apps: ["src/*"]}`
91
+ */
92
+ get config(): WorkspaceConfigObject;
93
+ /**
94
+ * Iterating the Monorepo object yields the workspace objects, in as close to
95
+ * topological dependency order as possible.
96
+ */
97
+ [Symbol.iterator](): Generator<Workspace, void, void>;
98
+ /**
99
+ * Iterating the Monorepo object yields the workspace objects, in as close to
100
+ * topological dependency order as possible.
101
+ */
102
+ [Symbol.asyncIterator](): AsyncGenerator<Workspace, void, void>;
103
+ /**
104
+ * By default, loads all workspaces reachable in the Monorepo.
105
+ *
106
+ * If provided with one (`string`)or more (`string[]`) group names in
107
+ * the {@link LoadQuery#groups} field, then only Workspaces in the named
108
+ * group(s) will be considered. Note that group names are unique string
109
+ * matches, not globs.
110
+ *
111
+ * If provided with a set of arbitrary path arguments, then only paths
112
+ * patching the provided pattern(s) will be included.
113
+ *
114
+ * These two options intersect, so
115
+ * `load({groups:'foo', paths:'./foo/[xy]*'})` will only load the workspaces
116
+ * in the group `foo` that match the paths glob.
117
+ */
118
+ load(query?: LoadQuery): this;
119
+ /**
120
+ * Return the array of workspace dependencies that are found in
121
+ * the loaded set, for use in calculating dependency graph order for
122
+ * build operations.
123
+ *
124
+ * This does *not* get the full set of dependencies, or expand any
125
+ * `workspace:` dependencies that are not loaded.
126
+ *
127
+ * Call with the `forceLoad` param set to `true` to attempt a full
128
+ * load if any deps are not currently loaded.
129
+ */
130
+ getDeps(ws: Workspace, forceLoad?: boolean): Workspace[];
131
+ onCycle(_ws: Workspace, _cycle: Workspace[], _depPath: Workspace[]): void;
132
+ /**
133
+ * Return the set of workspaces in the named group.
134
+ * If the group is not one we know about, then undefined is returned.
135
+ */
136
+ group(group: string): Set<Workspace> | undefined;
137
+ /**
138
+ * Get a loaded workspace by path or name.
139
+ *
140
+ * Note that this can only return workspaces that were ingested via a
141
+ * previous call to {@link Monorepo#load}.
142
+ */
143
+ get(nameOrPath: string): Workspace | undefined;
144
+ /**
145
+ * get the list of all loaded workspace names used as keys
146
+ */
147
+ names(): Generator<string, void, unknown>;
148
+ /**
149
+ * get the list of all loaded workspace paths used as keys
150
+ */
151
+ paths(): Generator<string, void, unknown>;
152
+ /**
153
+ * get the workspace objects in no particular order.
154
+ * this is ever so slightly faster than iterating, because it doesn't
155
+ * explore the graph to yield results in topological dependency order,
156
+ * and should be used instead when order doesn't matter.
157
+ */
158
+ values(): Generator<Workspace, void, unknown>;
159
+ /**
160
+ * Get all the keys (package names and paths) for loaded workspaces.
161
+ * Union of {@link Monorepo#names} and {@link Monorepo#paths}
162
+ */
163
+ keys(): Generator<string, void, unknown>;
164
+ /**
165
+ * Filter the monorepo object yielding the workspace objects that matches
166
+ * either of the {@link WorkspacesLoadedConfig} options provided, in as close
167
+ * to topological dependency order as possible.
168
+ */
169
+ filter({ workspace: namesOrPaths, 'workspace-group': groupName, }: WorkspacesLoadedConfig): Generator<Workspace, void, unknown>;
170
+ /**
171
+ * Run an operation asynchronously over all loaded workspaces
172
+ *
173
+ * If the `forceLoad` param is true, then it will attempt to do a full load
174
+ * when encountering a `workspace:` dependency that isn't loaded.
175
+ *
176
+ * Note that because the return type appears in the parameters of the
177
+ * operation function, it must be set explicitly either in the operation
178
+ * function signature or by calling `run<MyType>` or it'll fall back to
179
+ * `unknown`, similar to `Array.reduce()`, and for the same reason.
180
+ */
181
+ run<R>(operation: (s: Workspace, signal: AbortSignal, depResults: DepResults<Workspace, R>) => Promise<R> | R, forceLoad?: boolean): Promise<Map<Workspace, R>>;
182
+ /**
183
+ * Run an operation synchronously over all loaded workspaces
184
+ *
185
+ * If the `forceLoad` param is true, then it will attempt to do a full load
186
+ * when encountering a `workspace:` dependency that isn't loaded.
187
+ *
188
+ * Note that because the return type appears in the parameters of the
189
+ * operation function, it must be set explicitly either in the operation
190
+ * function signature or by calling `runSync<MyType>` or it'll fall back to
191
+ * `unknown`, similar to `Array.reduce()`, and for the same reason.
192
+ */
193
+ runSync<R>(operation: (s: Workspace, signal: AbortSignal, depResults: DepResults<Workspace, R>) => R, forceLoad?: boolean): Map<Workspace, R>;
194
+ /**
195
+ * Convenience method to instantiate and load in one call.
196
+ * Returns undefined if the project is not a monorepo workspaces
197
+ * root, otherwise returns the loaded Monorepo.
198
+ */
199
+ static maybeLoad(projectRoot: string, options?: MonorepoOptions): Monorepo | undefined;
200
+ /**
201
+ * Convenience method to instantiate and load in one call.
202
+ * Throws if called on a directory that is not a workspaces root.
203
+ */
204
+ static load(projectRoot: string, options?: MonorepoOptions): Monorepo;
205
+ }
206
+ export declare const workspaceCache: Map<string, Workspace>;
207
+ /**
208
+ * Class representing a single Workspace in a {@link Monorepo}
209
+ */
210
+ export declare class Workspace {
211
+ #private;
212
+ id: DepID;
213
+ path: string;
214
+ fullpath: string;
215
+ manifest: NormalizedManifest;
216
+ groups: string[];
217
+ name: string;
218
+ constructor(path: string, manifest: NormalizedManifest, fullpath: string);
219
+ get keys(): string[];
220
+ }
package/dist/index.js ADDED
@@ -0,0 +1,558 @@
1
+ import { joinDepIDTuple } from '@vltpkg/dep-id';
2
+ import { error } from '@vltpkg/error-cause';
3
+ import { PackageJson } from '@vltpkg/package-json';
4
+ import { load } from '@vltpkg/vlt-json';
5
+ import { globSync } from 'glob';
6
+ import { graphRun, graphRunSync } from '@vltpkg/graph-run';
7
+ import { minimatch } from 'minimatch';
8
+ import { basename, posix, resolve } from 'node:path';
9
+ import { PathScurry } from 'path-scurry';
10
+ /**
11
+ * Check if an error (potentially wrapped by @vltpkg/error-cause) is
12
+ * caused by a JSON syntax error. The error chain from PackageJson.read()
13
+ * is: Error { cause: { path, cause: SyntaxError } }
14
+ */
15
+ /* c8 ignore start - defensive helper, primary path tested via workspace integration */
16
+ const isSyntaxError = (err) => {
17
+ if (err instanceof SyntaxError)
18
+ return true;
19
+ if (err instanceof Error) {
20
+ const cause = err.cause;
21
+ if (cause instanceof SyntaxError)
22
+ return true;
23
+ if (cause && typeof cause === 'object' && 'cause' in cause) {
24
+ return (cause.cause instanceof SyntaxError);
25
+ }
26
+ }
27
+ return false;
28
+ };
29
+ /**
30
+ * Turn a {@link WorkspaceConfig} into a
31
+ * {@link WorkspaceConfigObject}, or throw if it's not valid.
32
+ */
33
+ export const asWSConfig = (conf, path) => {
34
+ assertWSConfig(conf, path);
35
+ return (typeof conf === 'string' ? { packages: [conf] }
36
+ : Array.isArray(conf) ? { packages: conf }
37
+ : Object.fromEntries(Object.entries(conf).map(([k, v]) => [
38
+ k,
39
+ typeof v === 'string' ? [v] : v,
40
+ ])));
41
+ };
42
+ /**
43
+ * Throw if the provided value is not a valid {@link WorkspaceConfig}
44
+ */
45
+ export const assertWSConfig = (conf, path) => {
46
+ if (typeof conf === 'string')
47
+ return;
48
+ if (Array.isArray(conf)) {
49
+ for (const c of conf) {
50
+ if (typeof c !== 'string') {
51
+ throw error('Invalid workspace definition', {
52
+ path,
53
+ found: c,
54
+ wanted: 'string',
55
+ });
56
+ }
57
+ }
58
+ return;
59
+ }
60
+ if (conf && typeof conf === 'object') {
61
+ for (const [group, value] of Object.entries(conf)) {
62
+ if (typeof value === 'string')
63
+ continue;
64
+ if (Array.isArray(value)) {
65
+ for (const c of value) {
66
+ if (typeof c !== 'string') {
67
+ throw error('Invalid workspace definition', {
68
+ path,
69
+ name: group,
70
+ found: c,
71
+ wanted: 'string',
72
+ });
73
+ }
74
+ }
75
+ continue;
76
+ }
77
+ throw error('Invalid workspace definition', {
78
+ path,
79
+ name: group,
80
+ found: value,
81
+ wanted: 'string | string[]',
82
+ });
83
+ }
84
+ return;
85
+ }
86
+ throw error('Invalid workspace definition', {
87
+ path,
88
+ found: conf,
89
+ wanted: 'string | string[] | { [group: string]: string | string[] }',
90
+ });
91
+ };
92
+ /**
93
+ * Class representing a Monorepo containing multiple workspaces.
94
+ *
95
+ * Does not automatically look up the root, but that can be provided by
96
+ * running `Config.load()`, since it stops seeking the route when a
97
+ * `vlt.json` file is encountered.
98
+ */
99
+ export class Monorepo {
100
+ /** The project root where vlt.json is found */
101
+ projectRoot;
102
+ /** Scurry object to cache all filesystem calls (mostly globs) */
103
+ scurry;
104
+ // maps both name and path to the workspace objects
105
+ #workspaces = new Map();
106
+ #groups = new Map();
107
+ #config;
108
+ packageJson;
109
+ /**
110
+ * Number of {@link Workspace} objects loaded in this Monorepo
111
+ */
112
+ get size() {
113
+ return [...this.values()].length;
114
+ }
115
+ constructor(projectRoot, options = {}) {
116
+ this.projectRoot = resolve(projectRoot);
117
+ this.scurry = options.scurry ?? new PathScurry(projectRoot);
118
+ this.packageJson = options.packageJson ?? new PackageJson();
119
+ this.#config = options.config;
120
+ if (options.load)
121
+ this.load(options.load);
122
+ }
123
+ /**
124
+ * Load the workspace definitions from vlt.json,
125
+ * canonicalizing the result into the effective `{[group:string]:string[]}`
126
+ * form.
127
+ *
128
+ * Eg:
129
+ * - `"src/*"` => `{packages:["src/*"]}`
130
+ * - `{"apps": "src/*"}` => `{apps: ["src/*"]}`
131
+ */
132
+ get config() {
133
+ if (this.#config)
134
+ return this.#config;
135
+ this.#config = asWSConfig(load('workspaces', assertWSConfig) ?? {});
136
+ return this.#config;
137
+ }
138
+ /**
139
+ * Iterating the Monorepo object yields the workspace objects, in as close to
140
+ * topological dependency order as possible.
141
+ */
142
+ *[Symbol.iterator]() {
143
+ const [ws] = [...this.values()];
144
+ if (!ws)
145
+ return;
146
+ // leverage the fact that graphRun returns results in
147
+ // as close to topological order as possible.
148
+ for (const workspace of this.runSync(() => { }).keys()) {
149
+ yield workspace;
150
+ }
151
+ }
152
+ /**
153
+ * Iterating the Monorepo object yields the workspace objects, in as close to
154
+ * topological dependency order as possible.
155
+ */
156
+ async *[Symbol.asyncIterator]() {
157
+ const [ws] = [...this.values()];
158
+ if (!ws)
159
+ return;
160
+ for (const workspace of (await this.run(() => { })).keys()) {
161
+ yield workspace;
162
+ }
163
+ }
164
+ /**
165
+ * By default, loads all workspaces reachable in the Monorepo.
166
+ *
167
+ * If provided with one (`string`)or more (`string[]`) group names in
168
+ * the {@link LoadQuery#groups} field, then only Workspaces in the named
169
+ * group(s) will be considered. Note that group names are unique string
170
+ * matches, not globs.
171
+ *
172
+ * If provided with a set of arbitrary path arguments, then only paths
173
+ * patching the provided pattern(s) will be included.
174
+ *
175
+ * These two options intersect, so
176
+ * `load({groups:'foo', paths:'./foo/[xy]*'})` will only load the workspaces
177
+ * in the group `foo` that match the paths glob.
178
+ */
179
+ load(query = {}) {
180
+ const paths = new Set(typeof query.paths === 'string' ?
181
+ [query.paths]
182
+ : (query.paths ?? []));
183
+ const groups = new Set(typeof query.groups === 'string' ?
184
+ [query.groups]
185
+ : (query.groups ?? []));
186
+ const groupsExpanded = {};
187
+ for (const [group, pattern] of Object.entries(this.config)) {
188
+ if (groups.size && !groups.has(group))
189
+ continue;
190
+ groupsExpanded[group] = this.#glob(pattern);
191
+ }
192
+ const filter = paths.size ? this.#glob([...paths]) : paths;
193
+ // if we specified paths, but none matched, nothing to do
194
+ if (paths.size && !filter.size)
195
+ return this;
196
+ for (const [group, matches] of Object.entries(groupsExpanded)) {
197
+ for (const path of matches) {
198
+ if (filter.size && !filter.has(path))
199
+ continue;
200
+ this.#loadWS(path, group);
201
+ }
202
+ }
203
+ return this;
204
+ }
205
+ // Either load a workspace from disk, or from our internal set,
206
+ // and assign it to the named group
207
+ #loadWS(path, group) {
208
+ const fullpath = resolve(this.projectRoot, path);
209
+ const loaded = this.#workspaces.get(fullpath);
210
+ if (loaded)
211
+ return loaded;
212
+ const fromCache = workspaceCache.get(fullpath);
213
+ const manifest = fromCache?.manifest ?? this.packageJson.read(fullpath);
214
+ const ws = fromCache ?? new Workspace(path, manifest, fullpath);
215
+ if (group)
216
+ ws.groups.push(group);
217
+ // Check for duplicate workspace names
218
+ const existingWS = this.#workspaces.get(ws.name);
219
+ if (existingWS && existingWS.fullpath !== ws.fullpath) {
220
+ throw error('Duplicate workspace name found', {
221
+ name: ws.name,
222
+ path: this.projectRoot,
223
+ wanted: ws.fullpath,
224
+ found: existingWS.fullpath,
225
+ });
226
+ }
227
+ this.#workspaces.set(ws.fullpath, ws);
228
+ this.#workspaces.set(ws.path, ws);
229
+ this.#workspaces.set(ws.name, ws);
230
+ for (const name of ws.groups) {
231
+ const group = this.#groups.get(name) ?? new Set();
232
+ group.add(ws);
233
+ this.#groups.set(name, group);
234
+ }
235
+ return ws;
236
+ }
237
+ // can't be cached, because it's dependent on the matches set
238
+ // but still worthwhile to have it defined in one place
239
+ #globOptions(matches, parseErrors) {
240
+ // if the entry or any of its parent dirs are already matched,
241
+ // then we should not explore further down that directory tree.
242
+ // if we hit the projectRoot then stop searching.
243
+ const inMatches = (p) => {
244
+ return (!!p?.relativePosix() &&
245
+ (matches.has(p.relativePosix()) || inMatches(p.parent)));
246
+ };
247
+ return {
248
+ root: this.projectRoot,
249
+ cwd: this.projectRoot,
250
+ posix: true,
251
+ scurry: this.scurry,
252
+ withFileTypes: false,
253
+ ignore: {
254
+ childrenIgnored: p => basename(p.relativePosix()) === 'node_modules' ||
255
+ inMatches(p),
256
+ // ignore if fails to load package.json
257
+ ignored: p => {
258
+ p.lstatSync();
259
+ const rel = p.relativePosix();
260
+ if (!rel)
261
+ return true;
262
+ const maybeDelete = [];
263
+ for (const m of matches) {
264
+ if (rel.startsWith(m + '/'))
265
+ return true;
266
+ if (m.startsWith(rel + '/')) {
267
+ maybeDelete.push(m);
268
+ }
269
+ }
270
+ if (!p.isDirectory())
271
+ return true;
272
+ const pj = p.resolve('package.json').lstatSync();
273
+ if (!pj?.isFile())
274
+ return true;
275
+ try {
276
+ this.packageJson.read(p.fullpath());
277
+ }
278
+ catch (err) {
279
+ // Track JSON parse errors for later reporting.
280
+ // We can't throw here because the glob is still running
281
+ // and we don't yet know if this path is a true workspace
282
+ // match or a subdirectory of another workspace.
283
+ if (parseErrors && isSyntaxError(err)) {
284
+ parseErrors.set(rel, err);
285
+ }
286
+ return true;
287
+ }
288
+ for (const m of maybeDelete) {
289
+ matches.delete(m);
290
+ }
291
+ matches.add(rel);
292
+ return false;
293
+ },
294
+ },
295
+ };
296
+ }
297
+ #glob(pattern) {
298
+ const matches = new Set();
299
+ const parseErrors = new Map();
300
+ globSync(pattern, this.#globOptions(matches, parseErrors));
301
+ // After the glob completes, check for JSON parse errors in paths
302
+ // that are NOT nested inside an already-matched workspace.
303
+ // Nested directories (like app/bar/badjson inside workspace app/bar)
304
+ // are legitimately ignored, but top-level workspace matches with
305
+ // broken package.json should surface an error.
306
+ for (const [rel, err] of parseErrors) {
307
+ let isNested = false;
308
+ for (const m of matches) {
309
+ if (rel.startsWith(m + '/')) {
310
+ isNested = true;
311
+ break;
312
+ }
313
+ }
314
+ if (!isNested) {
315
+ throw error(`Failed to parse package.json in workspace "${rel}"`, {
316
+ path: resolve(this.projectRoot, rel, 'package.json'),
317
+ cause: err,
318
+ });
319
+ }
320
+ }
321
+ return matches;
322
+ }
323
+ /**
324
+ * Return the array of workspace dependencies that are found in
325
+ * the loaded set, for use in calculating dependency graph order for
326
+ * build operations.
327
+ *
328
+ * This does *not* get the full set of dependencies, or expand any
329
+ * `workspace:` dependencies that are not loaded.
330
+ *
331
+ * Call with the `forceLoad` param set to `true` to attempt a full
332
+ * load if any deps are not currently loaded.
333
+ */
334
+ getDeps(ws, forceLoad = false) {
335
+ // load manifest and find workspace: deps
336
+ // filter by those loaded
337
+ const { manifest } = ws;
338
+ const depWorkspaces = [];
339
+ let didForceLoad = false;
340
+ for (const depType of [
341
+ 'dependencies',
342
+ 'devDependencies',
343
+ 'optionalDependencies',
344
+ 'peerDependencies',
345
+ ]) {
346
+ const deps = manifest[depType];
347
+ if (!deps)
348
+ continue;
349
+ for (const [dep, spec] of Object.entries(deps)) {
350
+ if (spec.startsWith('workspace:')) {
351
+ let depWS = this.#workspaces.get(dep);
352
+ if (!depWS) {
353
+ if (!forceLoad)
354
+ continue;
355
+ if (didForceLoad)
356
+ continue;
357
+ didForceLoad = true;
358
+ this.load();
359
+ depWS = this.#workspaces.get(dep);
360
+ if (!depWS)
361
+ continue;
362
+ }
363
+ depWorkspaces.push(depWS);
364
+ }
365
+ }
366
+ }
367
+ return depWorkspaces;
368
+ }
369
+ onCycle(_ws, _cycle, _depPath) {
370
+ // XXX - process logging? Need to say something like:
371
+ // Cyclical workspace dependency warning!
372
+ // When evaluating dependency ${ws.name} via ${
373
+ // path.map(ws => ws.name).join(' -> ')
374
+ // }, a dependency cycle was detected: ${
375
+ // cycle.map(ws => ws.name).join(' -> ')
376
+ // }. Operation will continue, but dependency order not guaranteed.`
377
+ }
378
+ /**
379
+ * Return the set of workspaces in the named group.
380
+ * If the group is not one we know about, then undefined is returned.
381
+ */
382
+ group(group) {
383
+ return this.#groups.get(group);
384
+ }
385
+ /**
386
+ * Get a loaded workspace by path or name.
387
+ *
388
+ * Note that this can only return workspaces that were ingested via a
389
+ * previous call to {@link Monorepo#load}.
390
+ */
391
+ get(nameOrPath) {
392
+ return this.#workspaces.get(nameOrPath);
393
+ }
394
+ /**
395
+ * get the list of all loaded workspace names used as keys
396
+ */
397
+ *names() {
398
+ for (const [key, ws] of this.#workspaces) {
399
+ if (key === ws.name)
400
+ yield key;
401
+ }
402
+ }
403
+ /**
404
+ * get the list of all loaded workspace paths used as keys
405
+ */
406
+ *paths() {
407
+ for (const [key, ws] of this.#workspaces) {
408
+ if (key === ws.path)
409
+ yield key;
410
+ }
411
+ }
412
+ /**
413
+ * get the workspace objects in no particular order.
414
+ * this is ever so slightly faster than iterating, because it doesn't
415
+ * explore the graph to yield results in topological dependency order,
416
+ * and should be used instead when order doesn't matter.
417
+ */
418
+ *values() {
419
+ const seen = new Set();
420
+ for (const ws of this.#workspaces.values()) {
421
+ if (seen.has(ws.fullpath))
422
+ continue;
423
+ seen.add(ws.fullpath);
424
+ yield ws;
425
+ }
426
+ }
427
+ /**
428
+ * Get all the keys (package names and paths) for loaded workspaces.
429
+ * Union of {@link Monorepo#names} and {@link Monorepo#paths}
430
+ */
431
+ *keys() {
432
+ for (const ws of this.values()) {
433
+ yield ws.path;
434
+ if (ws.name !== ws.path)
435
+ yield ws.name;
436
+ }
437
+ }
438
+ /**
439
+ * Filter the monorepo object yielding the workspace objects that matches
440
+ * either of the {@link WorkspacesLoadedConfig} options provided, in as close
441
+ * to topological dependency order as possible.
442
+ */
443
+ *filter({ workspace: namesOrPaths, 'workspace-group': groupName, }) {
444
+ const globPatternChecks = namesOrPaths?.map(glob => minimatch.filter(posix.join(glob)));
445
+ for (const ws of this) {
446
+ // check if any group has any of the provided group names
447
+ if (groupName?.some(i => ws.groups.includes(i))) {
448
+ yield ws;
449
+ continue;
450
+ }
451
+ // check if any workspace-provided name directly matches any of the
452
+ // configured workspaces by either name or file path
453
+ if (namesOrPaths
454
+ ?.map(i => posix.join(i))
455
+ .some(i => ws.keys.includes(i))) {
456
+ yield ws;
457
+ continue;
458
+ }
459
+ // check if one of the workspace values are matching glob patterns
460
+ if (ws.keys.some(key => globPatternChecks?.some(fn => fn(key)))) {
461
+ yield ws;
462
+ }
463
+ }
464
+ }
465
+ /**
466
+ * Run an operation asynchronously over all loaded workspaces
467
+ *
468
+ * If the `forceLoad` param is true, then it will attempt to do a full load
469
+ * when encountering a `workspace:` dependency that isn't loaded.
470
+ *
471
+ * Note that because the return type appears in the parameters of the
472
+ * operation function, it must be set explicitly either in the operation
473
+ * function signature or by calling `run<MyType>` or it'll fall back to
474
+ * `unknown`, similar to `Array.reduce()`, and for the same reason.
475
+ */
476
+ async run(operation, forceLoad = false) {
477
+ const [ws, ...rest] = [...this.#workspaces.values()];
478
+ if (!ws) {
479
+ throw error('No workspaces loaded', undefined, this.run);
480
+ }
481
+ return graphRun({
482
+ graph: [ws, ...rest],
483
+ getDeps: ws => this.getDeps(ws, forceLoad),
484
+ visit: async (ws, signal, _, depResults) => await operation(ws, signal, depResults),
485
+ onCycle: (ws, cycle, path) => this.onCycle(ws, cycle, path),
486
+ });
487
+ }
488
+ /**
489
+ * Run an operation synchronously over all loaded workspaces
490
+ *
491
+ * If the `forceLoad` param is true, then it will attempt to do a full load
492
+ * when encountering a `workspace:` dependency that isn't loaded.
493
+ *
494
+ * Note that because the return type appears in the parameters of the
495
+ * operation function, it must be set explicitly either in the operation
496
+ * function signature or by calling `runSync<MyType>` or it'll fall back to
497
+ * `unknown`, similar to `Array.reduce()`, and for the same reason.
498
+ */
499
+ runSync(operation, forceLoad = false) {
500
+ const [ws, ...rest] = [...this.#workspaces.values()];
501
+ if (!ws) {
502
+ throw error('No workspaces loaded', undefined, this.run);
503
+ }
504
+ return graphRunSync({
505
+ graph: [ws, ...rest],
506
+ getDeps: ws => this.getDeps(ws, forceLoad),
507
+ visit: (ws, signal, _, depResults) => operation(ws, signal, depResults),
508
+ onCycle: (ws, cycle, path) => this.onCycle(ws, cycle, path),
509
+ });
510
+ }
511
+ /**
512
+ * Convenience method to instantiate and load in one call.
513
+ * Returns undefined if the project is not a monorepo workspaces
514
+ * root, otherwise returns the loaded Monorepo.
515
+ */
516
+ static maybeLoad(projectRoot, options = { load: {} }) {
517
+ const config = load('workspaces', assertWSConfig);
518
+ if (!config)
519
+ return;
520
+ return new Monorepo(projectRoot, { load: {}, ...options });
521
+ }
522
+ /**
523
+ * Convenience method to instantiate and load in one call.
524
+ * Throws if called on a directory that is not a workspaces root.
525
+ */
526
+ static load(projectRoot, options = { load: {} }) {
527
+ const { load = {} } = options;
528
+ return new Monorepo(projectRoot, { ...options, load });
529
+ }
530
+ }
531
+ export const workspaceCache = new Map();
532
+ /**
533
+ * Class representing a single Workspace in a {@link Monorepo}
534
+ */
535
+ export class Workspace {
536
+ id;
537
+ path;
538
+ fullpath;
539
+ manifest;
540
+ groups = [];
541
+ name;
542
+ #keys;
543
+ constructor(path, manifest, fullpath) {
544
+ this.id = joinDepIDTuple(['workspace', path]);
545
+ workspaceCache.set(fullpath, this);
546
+ this.path = path;
547
+ this.fullpath = fullpath;
548
+ this.manifest = manifest;
549
+ this.name = manifest.name ?? path;
550
+ }
551
+ get keys() {
552
+ if (this.#keys) {
553
+ return this.#keys;
554
+ }
555
+ this.#keys = [this.name, this.path, this.fullpath];
556
+ return this.#keys;
557
+ }
558
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/workspaces",
3
3
  "description": "Utility for working with vlt workspaces",
4
- "version": "1.0.0-rc.23",
4
+ "version": "1.0.0-rc.24",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -12,12 +12,12 @@
12
12
  "email": "support@vlt.sh"
13
13
  },
14
14
  "dependencies": {
15
- "@vltpkg/dep-id": "1.0.0-rc.23",
16
- "@vltpkg/error-cause": "1.0.0-rc.23",
17
- "@vltpkg/graph-run": "1.0.0-rc.23",
18
- "@vltpkg/package-json": "1.0.0-rc.23",
19
- "@vltpkg/types": "1.0.0-rc.23",
20
- "@vltpkg/vlt-json": "1.0.0-rc.23",
15
+ "@vltpkg/dep-id": "1.0.0-rc.24",
16
+ "@vltpkg/error-cause": "1.0.0-rc.24",
17
+ "@vltpkg/graph-run": "1.0.0-rc.24",
18
+ "@vltpkg/package-json": "1.0.0-rc.24",
19
+ "@vltpkg/types": "1.0.0-rc.24",
20
+ "@vltpkg/vlt-json": "1.0.0-rc.24",
21
21
  "glob": "^13.0.0",
22
22
  "minimatch": "^10.1.1",
23
23
  "path-scurry": "^2.0.1"