@travetto/manifest 3.0.0-rc.3

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 ArcSine Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/manifest/DOC.ts and execute "npx trv doc" to rebuild -->
3
+ # Manifest
4
+ ## Manifest support
5
+
6
+ **Install: @travetto/manifest**
7
+ ```bash
8
+ npm install @travetto/manifest
9
+ ```
10
+
11
+ This module provides functionality for basic path functionality and common typings for manifests
12
+
13
+ ### Module Indexing
14
+ The bootstrap process will also produce an index of all source files, which allows for fast in-memory scanning. This allows for all the automatic discovery that is used within the framework.
package/__index__.ts ADDED
@@ -0,0 +1,10 @@
1
+ /// <reference path="./src/typings.d.ts" />
2
+
3
+ export * from './src/path';
4
+ export * from './src/module';
5
+ export * from './src/manifest-index';
6
+ export * from './src/root-index';
7
+ export * from './src/delta';
8
+ export * from './src/package';
9
+ export * from './src/manifest';
10
+ export * from './src/types';
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@travetto/manifest",
3
+ "version": "3.0.0-rc.3",
4
+ "description": "Manifest support",
5
+ "keywords": [
6
+ "path",
7
+ "package",
8
+ "manifest",
9
+ "travetto",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://travetto.io",
13
+ "license": "MIT",
14
+ "author": {
15
+ "email": "travetto.framework@gmail.com",
16
+ "name": "Travetto Framework"
17
+ },
18
+ "files": [
19
+ "__index__.ts",
20
+ "src",
21
+ "support"
22
+ ],
23
+ "main": "__index__.ts",
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "repository": {
28
+ "url": "https://github.com/travetto/travetto.git",
29
+ "directory": "module/manifest"
30
+ },
31
+ "dependencies": {},
32
+ "travetto": {
33
+ "displayName": "Manifest",
34
+ "docBaseUrl": "https://github.com/travetto/travetto/tree/main"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
package/src/delta.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { type Stats } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import {
4
+ ManifestDelta, ManifestDeltaEvent, ManifestDeltaModule,
5
+ ManifestModule, ManifestModuleFile, ManifestModuleFolderType,
6
+ ManifestRoot
7
+ } from './types';
8
+
9
+ const VALID_SOURCE_FOLDERS = new Set<ManifestModuleFolderType>(['bin', 'src', 'test', 'support', '$index', '$package', 'doc']);
10
+
11
+ /**
12
+ * Produce delta for the manifest
13
+ */
14
+ export class ManifestDeltaUtil {
15
+
16
+ static #getNewest(stat: { mtimeMs: number, ctimeMs: number }): number {
17
+ return Math.max(stat.mtimeMs, stat.ctimeMs);
18
+ }
19
+
20
+ /**
21
+ * Produce delta between two manifest modules, relative to an output folder
22
+ */
23
+ static async #deltaModules(outputFolder: string, left: ManifestDeltaModule, right: ManifestDeltaModule): Promise<ManifestDeltaEvent[]> {
24
+ const out: ManifestDeltaEvent[] = [];
25
+ const getStat = (f: string): Promise<Stats | void> =>
26
+ fs.stat(`${outputFolder}/${left.output}/${f.replace(/[.]ts$/, '.js')}`).catch(() => { });
27
+
28
+ const add = (file: string, type: ManifestDeltaEvent['type']): unknown =>
29
+ out.push({ file, module: left.name, type });
30
+
31
+ for (const el of Object.keys(left.files)) {
32
+ if (!(el in right.files)) {
33
+ const [, , leftTs] = left.files[el];
34
+ const stat = await getStat(el);
35
+ if (stat && leftTs < this.#getNewest(stat)) {
36
+ // If file pre-exists manifest, be cool
37
+ continue;
38
+ }
39
+ add(el, 'added');
40
+ } else {
41
+ const [, , leftTs] = left.files[el];
42
+ const [, , rightTs] = right.files[el];
43
+ if (leftTs !== rightTs) {
44
+ const stat = await getStat(el);
45
+ if (!stat) {
46
+ add(el, 'missing');
47
+ } else if (leftTs > this.#getNewest(stat!)) {
48
+ add(el, 'changed');
49
+ }
50
+ } else {
51
+ const stat = await getStat(el);
52
+ if (!stat) {
53
+ add(el, 'missing');
54
+ } else if (this.#getNewest(stat!) < leftTs) {
55
+ add(el, 'dirty');
56
+ }
57
+ }
58
+ }
59
+ }
60
+ for (const el of Object.keys(right.files)) {
61
+ if (!(el in left.files)) {
62
+ const stat = await getStat(el);
63
+ if (stat) {
64
+ add(el, 'removed');
65
+ }
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /**
72
+ * Collapse all files in a module
73
+ * @param {ManifestModule} m
74
+ * @returns {}
75
+ */
76
+ static #flattenModuleFiles(m: ManifestModule): Record<string, ManifestModuleFile> {
77
+ const out: Record<string, ManifestModuleFile> = {};
78
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
79
+ for (const key of Object.keys(m.files) as (ManifestModuleFolderType[])) {
80
+ if (!VALID_SOURCE_FOLDERS.has(key)) {
81
+ continue;
82
+ }
83
+ for (const [name, type, date] of m.files?.[key] ?? []) {
84
+ if (type === 'ts' || type === 'js' || type === 'package-json') {
85
+ out[name] = [name, type, date];
86
+ }
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * Produce delta between ttwo manifest roots, relative to a single output folder
94
+ */
95
+ static async produceDelta(outputFolder: string, left: ManifestRoot, right: ManifestRoot): Promise<ManifestDelta> {
96
+ const deltaLeft = Object.fromEntries(
97
+ Object.values(left.modules)
98
+ .map(m => [m.name, { ...m, files: this.#flattenModuleFiles(m) }])
99
+ );
100
+
101
+ const deltaRight = Object.fromEntries(
102
+ Object.values(right.modules)
103
+ .map(m => [m.name, { ...m, files: this.#flattenModuleFiles(m) }])
104
+ );
105
+
106
+ const out: Record<string, ManifestDeltaEvent[]> = {};
107
+
108
+ for (const [name, lMod] of Object.entries(deltaLeft)) {
109
+ out[name] = await this.#deltaModules(outputFolder, lMod, deltaRight[name] ?? { files: {}, name });
110
+ }
111
+
112
+ return out;
113
+ }
114
+ }
@@ -0,0 +1,163 @@
1
+ import { PackageUtil } from './package';
2
+ import { path } from './path';
3
+ import { ManifestContext, ManifestProfile, PackageRel, PackageVisitor, PackageVisitReq } from './types';
4
+
5
+ export type ModuleDep = {
6
+ version: string;
7
+ name: string;
8
+ main?: boolean;
9
+ mainSource?: boolean;
10
+ local?: boolean;
11
+ internal?: boolean;
12
+ sourcePath: string;
13
+ childSet: Map<string, Set<PackageRel>>;
14
+ parentSet: Set<string>;
15
+ profileSet: Set<ManifestProfile>;
16
+ };
17
+
18
+ /**
19
+ * Used for walking dependencies for collecting modules for the manifest
20
+ */
21
+ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
22
+
23
+ /**
24
+ * Get main patterns for detecting if a module should be treated as main
25
+ */
26
+ static getMainPatterns(mainModule: string, mergeModules: string[]): RegExp[] {
27
+ const groups: Record<string, string[]> = { [mainModule]: [] };
28
+ for (const el of mergeModules) {
29
+ if (el.includes('/')) {
30
+ const [grp, sub] = el.split('/');
31
+ (groups[`${grp}/`] ??= []).push(sub);
32
+ } else {
33
+ (groups[el] ??= []);
34
+ }
35
+ }
36
+
37
+ return Object.entries(groups)
38
+ .map(([root, subs]) => subs.length ? `${root}(${subs.join('|')})` : root)
39
+ .map(x => new RegExp(`^${x.replace(/[*]/g, '.*?')}$`));
40
+ }
41
+
42
+ constructor(public ctx: ManifestContext) { }
43
+
44
+ #mainPatterns: RegExp[] = [];
45
+
46
+ /**
47
+ * Initialize visitor, and provide global dependencies
48
+ */
49
+ async init(req: PackageVisitReq<ModuleDep>): Promise<PackageVisitReq<ModuleDep>[]> {
50
+ const pkg = PackageUtil.readPackage(req.sourcePath);
51
+ const workspacePkg = PackageUtil.readPackage(this.ctx.workspacePath);
52
+ const workspaceModules = pkg.workspaces?.length ? (await PackageUtil.resolveWorkspaces(req.sourcePath)) : [];
53
+
54
+ this.#mainPatterns = ModuleDependencyVisitor.getMainPatterns(pkg.name, [
55
+ ...pkg.travetto?.mainSource ?? [],
56
+ // Add workspace folders, for tests and docs
57
+ ...workspaceModules.map(x => x.name)
58
+ ]);
59
+
60
+ const globals = [
61
+ ...(workspacePkg.travetto?.globalModules ?? []),
62
+ ...(pkg.travetto?.globalModules ?? [])
63
+ ]
64
+ .map(f => PackageUtil.resolvePackagePath(f));
65
+
66
+ const workspaceModuleDeps = workspaceModules
67
+ .map(entry => path.resolve(req.sourcePath, entry.sourcePath));
68
+
69
+ return [
70
+ ...globals,
71
+ ...workspaceModuleDeps
72
+ ].map(s => PackageUtil.packageReq(s, 'global'));
73
+ }
74
+
75
+ /**
76
+ * Is valid dependency for searching
77
+ */
78
+ valid(req: PackageVisitReq<ModuleDep>): boolean {
79
+ return req.sourcePath === path.cwd() || (
80
+ req.rel !== 'peer' &&
81
+ (!!req.pkg.travetto || req.rel === 'global')
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Create dependency from request
87
+ */
88
+ create(req: PackageVisitReq<ModuleDep>): ModuleDep {
89
+ const { pkg: { name, version, travetto: { profiles = [] } = {}, ...pkg }, sourcePath } = req;
90
+ const profileSet = new Set<ManifestProfile>([
91
+ ...profiles ?? []
92
+ ]);
93
+ const main = name === this.ctx.mainModule;
94
+ const mainSource = main || this.#mainPatterns.some(x => x.test(name));
95
+ const internal = pkg.private === true;
96
+ const local = internal || mainSource || !sourcePath.includes('node_modules');
97
+
98
+ const dep = {
99
+ name, version, sourcePath, main, mainSource, local, internal,
100
+ parentSet: new Set([]), childSet: new Map(), profileSet
101
+ };
102
+
103
+ return dep;
104
+ }
105
+
106
+ /**
107
+ * Visit dependency
108
+ */
109
+ visit(req: PackageVisitReq<ModuleDep>, dep: ModuleDep): void {
110
+ const { parent } = req;
111
+ if (parent && dep.name !== this.ctx.mainModule) {
112
+ dep.parentSet.add(parent.name);
113
+ const set = parent.childSet.get(dep.name) ?? new Set();
114
+ parent.childSet.set(dep.name, set);
115
+ set.add(req.rel);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Propagate profile/relationship information through graph
121
+ */
122
+ complete(deps: Set<ModuleDep>): Set<ModuleDep> {
123
+ const mapping = new Map<string, { parent: Set<string>, child: Map<string, Set<PackageRel>>, el: ModuleDep }>();
124
+ for (const el of deps) {
125
+ mapping.set(el.name, { parent: new Set(el.parentSet), child: new Map(el.childSet), el });
126
+ }
127
+
128
+ const main = mapping.get(this.ctx.mainModule)!;
129
+
130
+ // Visit all direct dependencies and mark
131
+ for (const [name, relSet] of main.child) {
132
+ const childDep = mapping.get(name)!.el;
133
+ if (!relSet.has('dev')) {
134
+ childDep.profileSet.add('std');
135
+ }
136
+ }
137
+
138
+ while (mapping.size > 0) {
139
+ const toProcess = [...mapping.values()].filter(x => x.parent.size === 0);
140
+ if (!toProcess.length) {
141
+ throw new Error(`We have reached a cycle for ${[...mapping.keys()]}`);
142
+ }
143
+ // Propagate
144
+ for (const { el, child } of toProcess) {
145
+ for (const c of child.keys()) {
146
+ const { el: cDep, parent } = mapping.get(c)!;
147
+ parent.delete(el.name); // Remove from child
148
+ for (const prof of el.profileSet) {
149
+ cDep.profileSet.add(prof);
150
+ }
151
+ }
152
+ }
153
+ // Remove from mapping
154
+ for (const { el } of toProcess) {
155
+ mapping.delete(el.name);
156
+ }
157
+ }
158
+
159
+ // Color the main folder as std
160
+ main.el.profileSet.add('std');
161
+ return deps;
162
+ }
163
+ }
@@ -0,0 +1,290 @@
1
+ import fs from 'fs';
2
+
3
+ import { path } from './path';
4
+
5
+ import {
6
+ ManifestModule, ManifestModuleCore, ManifestModuleFile,
7
+ ManifestModuleFileType, ManifestModuleFolderType, ManifestProfile, ManifestRoot
8
+ } from './types';
9
+
10
+ type ScanTest = ((full: string) => boolean) | { test: (full: string) => boolean };
11
+ export type FindConfig = {
12
+ folders?: ManifestModuleFolderType[];
13
+ filter?: ScanTest;
14
+ includeIndex?: boolean;
15
+ profiles?: string[];
16
+ checkProfile?: boolean;
17
+ };
18
+
19
+ export type IndexedFile = {
20
+ id: string;
21
+ import: string;
22
+ module: string;
23
+ source: string;
24
+ output: string;
25
+ relative: string;
26
+ profile: ManifestProfile;
27
+ type: ManifestModuleFileType;
28
+ };
29
+
30
+ export type IndexedModule = ManifestModuleCore & {
31
+ files: Record<ManifestModuleFolderType, IndexedFile[]>;
32
+ workspaceRelative: string;
33
+ };
34
+
35
+ /**
36
+ * Manifest index
37
+ */
38
+ export class ManifestIndex {
39
+
40
+ #manifestFile: string;
41
+ #manifest: ManifestRoot;
42
+ #modules: IndexedModule[];
43
+ #modulesByName: Record<string, IndexedModule> = {};
44
+ #modulesByFolder: Record<string, IndexedModule> = {};
45
+ #root: string;
46
+ #outputToEntry = new Map<string, IndexedFile>();
47
+ #sourceToEntry = new Map<string, IndexedFile>();
48
+ #importToEntry = new Map<string, IndexedFile>();
49
+
50
+ constructor(root: string, manifest: string | ManifestRoot) {
51
+ this.init(root, manifest);
52
+ }
53
+
54
+ #resolveOutput(...parts: string[]): string {
55
+ return path.resolve(this.#root, ...parts);
56
+ }
57
+
58
+ get manifest(): ManifestRoot {
59
+ return this.#manifest;
60
+ }
61
+
62
+ get root(): string {
63
+ return this.#root;
64
+ }
65
+
66
+ get manifestFile(): string {
67
+ return this.#manifestFile;
68
+ }
69
+
70
+ init(root: string, manifestInput: string | ManifestRoot): void {
71
+ this.#root = root;
72
+ this.#manifestFile = typeof manifestInput === 'string' ? manifestInput : manifestInput.manifestFile;
73
+ this.#manifest = typeof manifestInput === 'string' ? JSON.parse(fs.readFileSync(this.#manifestFile, 'utf8')) : manifestInput;
74
+ this.#index();
75
+ }
76
+
77
+ #moduleFiles(m: ManifestModule, files: ManifestModuleFile[]): IndexedFile[] {
78
+ return files.map(([f, type, ts, profile = 'std']) => {
79
+ const source = path.join(m.source, f);
80
+ const js = (type === 'ts' ? f.replace(/[.]ts$/, '.js') : f);
81
+ const output = this.#resolveOutput(m.output, js);
82
+ const modImport = `${m.name}/${js}`;
83
+ let id = modImport.replace(`${m.name}/`, _ => _.replace(/[/]$/, ':'));
84
+ if (type === 'ts' || type === 'js') {
85
+ id = id.replace(/[.]js$/, '');
86
+ }
87
+
88
+ return { id, type, source, output, import: modImport, profile, relative: f, module: m.name };
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Get index of all source files
94
+ */
95
+ #index(): void {
96
+ this.#outputToEntry.clear();
97
+ this.#importToEntry.clear();
98
+ this.#sourceToEntry.clear();
99
+
100
+ this.#modules = Object.values(this.manifest.modules)
101
+ .map(m => ({
102
+ ...m,
103
+ output: this.#resolveOutput(m.output),
104
+ workspaceRelative: m.source.replace(`${this.#manifest.workspacePath}/`, ''),
105
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
106
+ files: Object.fromEntries(
107
+ Object.entries(m.files).map(([folder, files]) => [folder, this.#moduleFiles(m, files ?? [])])
108
+ ) as Record<ManifestModuleFolderType, IndexedFile[]>
109
+ }));
110
+
111
+ for (const mod of this.#modules) {
112
+ for (const files of Object.values(mod.files ?? {})) {
113
+ for (const entry of files) {
114
+ this.#outputToEntry.set(entry.output, entry);
115
+ this.#sourceToEntry.set(entry.source, entry);
116
+ this.#importToEntry.set(entry.import, entry);
117
+ this.#importToEntry.set(entry.import.replace(/[.]js$/, ''), entry);
118
+ }
119
+ }
120
+ }
121
+ this.#modulesByName = Object.fromEntries(this.#modules.map(x => [x.name, x]));
122
+ this.#modulesByFolder = Object.fromEntries(this.#modules.map(x => [x.workspaceRelative, x]));
123
+ }
124
+
125
+ /**
126
+ * Get entry by file (input or output)
127
+ */
128
+ getEntry(file: string): IndexedFile | undefined {
129
+ return this.#outputToEntry.get(file) ?? this.#sourceToEntry.get(file);
130
+ }
131
+
132
+ /**
133
+ * Get all local modules
134
+ * @returns
135
+ */
136
+ getLocalModules(): IndexedModule[] {
137
+ return this.#modules.filter(x => x.local);
138
+ }
139
+
140
+ /**
141
+ * Find files from the index
142
+ * @param folder The sub-folder to check into
143
+ * @param filter The filter to determine if this is a valid support file
144
+ */
145
+ find(config: FindConfig): IndexedFile[] {
146
+ const { filter: f, folders } = config;
147
+ const filter = f ? 'test' in f ? f.test.bind(f) : f : f;
148
+
149
+ let idx = this.#modules;
150
+
151
+ const checkProfile = config.checkProfile ?? true;
152
+
153
+ const activeProfiles = new Set(['std', ...(config.profiles ?? process.env.TRV_PROFILES?.split(/\s*,\s*/g) ?? [])]);
154
+
155
+ if (checkProfile) {
156
+ idx = idx.filter(m => m.profiles.length === 0 || m.profiles.some(p => activeProfiles.has(p)));
157
+ }
158
+
159
+ let searchSpace = folders ?
160
+ idx.flatMap(m => [...folders.flatMap(fo => m.files[fo] ?? []), ...(config.includeIndex ? (m.files.$index ?? []) : [])]) :
161
+ idx.flatMap(m => [...Object.values(m.files)].flat());
162
+
163
+ if (checkProfile) {
164
+ searchSpace = searchSpace.filter(fi => activeProfiles.has(fi.profile));
165
+ }
166
+
167
+ return searchSpace
168
+ .filter(({ type }) => type === 'ts')
169
+ .filter(({ source }) => filter?.(source) ?? true);
170
+ }
171
+
172
+ /**
173
+ * Find files from the index
174
+ * @param filter The filter to determine if this is a valid support file
175
+ */
176
+ findSupport(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
177
+ return this.find({ ...config, folders: ['support'] });
178
+ }
179
+
180
+ /**
181
+ * Find files from the index
182
+ * @param filter The filter to determine if this is a valid support file
183
+ */
184
+ findSrc(config: Omit<FindConfig, 'folder'> = {}): IndexedFile[] {
185
+ return this.find({ ...config, includeIndex: true, folders: ['src'] });
186
+ }
187
+
188
+ /**
189
+ * Find files from the index
190
+ * @param filter The filter to determine if this is a valid support file
191
+ */
192
+ findTest(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
193
+ return this.find({ ...config, folders: ['test'] });
194
+ }
195
+
196
+ /**
197
+ * Is module installed?
198
+ */
199
+ hasModule(name: string): boolean {
200
+ return name in this.manifest.modules;
201
+ }
202
+
203
+ /**
204
+ * Get module
205
+ */
206
+ getModule(name: string): IndexedModule | undefined {
207
+ return this.#modulesByName[name];
208
+ }
209
+
210
+ /**
211
+ * Get module by folder
212
+ */
213
+ getModuleByFolder(folder: string): IndexedModule | undefined {
214
+ return this.#modulesByFolder[folder];
215
+ }
216
+
217
+ /**
218
+ * Resolve import
219
+ */
220
+ resolveFileImport(name: string): string {
221
+ name = !name.endsWith('.d.ts') ? name.replace(/[.]ts$/, '.js') : name;
222
+ return this.#importToEntry.get(name)?.output ?? name;
223
+ }
224
+
225
+ /**
226
+ * Get indexed module from source file
227
+ * @param source
228
+ */
229
+ getFromSource(source: string): IndexedFile | undefined {
230
+ return this.#sourceToEntry.get(source);
231
+ }
232
+
233
+ /**
234
+ * Get indexed module from source file
235
+ * @param source
236
+ */
237
+ getFromImport(imp: string): IndexedFile | undefined {
238
+ return this.#importToEntry.get(imp);
239
+ }
240
+
241
+ /**
242
+ * Get module from source file
243
+ * @param source
244
+ */
245
+ getModuleFromSource(source: string): IndexedModule | undefined {
246
+ const name = this.getFromSource(source)?.module;
247
+ return name ? this.getModule(name) : undefined;
248
+ }
249
+
250
+ /**
251
+ * Build module list from an expression list (e.g. `@travetto/app,-@travetto/log)
252
+ */
253
+ getModuleList(mode: 'local' | 'all', exprList: string = ''): Set<string> {
254
+ const allMods = Object.keys(this.manifest.modules);
255
+ const active = new Set<string>(
256
+ mode === 'local' ? this.getLocalModules().map(x => x.name) :
257
+ (mode === 'all' ? allMods : [])
258
+ );
259
+
260
+ for (const expr of exprList.split(/\s*,\s*/g)) {
261
+ const [, neg, mod] = expr.match(/(-|[+])?([^+\- ]+)$/) ?? [];
262
+ if (mod) {
263
+ const patt = new RegExp(`^${mod.replace(/[*]/g, '.*')}$`);
264
+ for (const m of allMods.filter(x => patt.test(x))) {
265
+ active[neg ? 'delete' : 'add'](m);
266
+ }
267
+ }
268
+ }
269
+ return active;
270
+ }
271
+
272
+ /**
273
+ * Get all modules (transitively) that depend on this module
274
+ */
275
+ getDependentModules(root: IndexedModule): Set<IndexedModule> {
276
+ const seen = new Set<string>();
277
+ const out = new Set<IndexedModule>();
278
+ const toProcess = [root.name];
279
+ while (toProcess.length) {
280
+ const next = toProcess.shift()!;
281
+ if (seen.has(next)) {
282
+ continue;
283
+ }
284
+ const mod = this.getModule(next)!;
285
+ toProcess.push(...mod.parents);
286
+ out.add(mod);
287
+ }
288
+ return out;
289
+ }
290
+ }