@travetto/manifest 3.0.0-rc.10

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,318 @@
1
+ import { path } from './path';
2
+
3
+ import {
4
+ ManifestModule, ManifestModuleCore, ManifestModuleFile,
5
+ ManifestModuleFileType, ManifestModuleFolderType, ManifestProfile, ManifestRoot
6
+ } from './types';
7
+
8
+ import { ManifestUtil } from './util';
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
+ sourceFile: string;
24
+ outputFile: string;
25
+ relativeFile: string;
26
+ profile: ManifestProfile;
27
+ type: ManifestModuleFileType;
28
+ };
29
+
30
+ export type IndexedModule = ManifestModuleCore & {
31
+ sourcePath: string;
32
+ outputPath: string;
33
+ files: Record<ManifestModuleFolderType, IndexedFile[]>;
34
+ };
35
+
36
+ /**
37
+ * Manifest index
38
+ */
39
+ export class ManifestIndex {
40
+
41
+ #manifestFile: string;
42
+ #manifest: ManifestRoot;
43
+ #modules: IndexedModule[];
44
+ #modulesByName: Record<string, IndexedModule> = {};
45
+ #modulesByFolder: Record<string, IndexedModule> = {};
46
+ #outputRoot: string;
47
+ #outputToEntry = new Map<string, IndexedFile>();
48
+ #sourceToEntry = new Map<string, IndexedFile>();
49
+ #importToEntry = new Map<string, IndexedFile>();
50
+
51
+ constructor(manifest: string) {
52
+ this.init(manifest);
53
+ }
54
+
55
+ #resolveOutput(...parts: string[]): string {
56
+ return path.resolve(this.#outputRoot, ...parts);
57
+ }
58
+
59
+ get manifest(): ManifestRoot {
60
+ return this.#manifest;
61
+ }
62
+
63
+ get outputRoot(): string {
64
+ return this.#outputRoot;
65
+ }
66
+
67
+ get manifestFile(): string {
68
+ return this.#manifestFile;
69
+ }
70
+
71
+ init(manifestInput: string): void {
72
+ const { manifest, file } = ManifestUtil.readManifestSync(manifestInput);
73
+ this.#manifest = manifest;
74
+ this.#manifestFile = file;
75
+ this.#outputRoot = path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder);
76
+ this.#index();
77
+ }
78
+
79
+ #moduleFiles(m: ManifestModule, files: ManifestModuleFile[]): IndexedFile[] {
80
+ return files.map(([f, type, ts, profile = 'std']) => {
81
+ const sourceFile = path.resolve(this.#manifest.workspacePath, m.sourceFolder, f);
82
+ const js = (type === 'ts' ? f.replace(/[.]ts$/, '.js') : f);
83
+ const outputFile = this.#resolveOutput(m.outputFolder, js);
84
+ const modImport = `${m.name}/${js}`;
85
+ let id = modImport.replace(`${m.name}/`, _ => _.replace(/[/]$/, ':'));
86
+ if (type === 'ts' || type === 'js') {
87
+ id = id.replace(/[.]js$/, '');
88
+ }
89
+
90
+ return { id, type, sourceFile, outputFile, import: modImport, profile, relativeFile: f, module: m.name };
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Get index of all source files
96
+ */
97
+ #index(): void {
98
+ this.#outputToEntry.clear();
99
+ this.#importToEntry.clear();
100
+ this.#sourceToEntry.clear();
101
+
102
+ this.#modules = Object.values(this.#manifest.modules)
103
+ .map(m => ({
104
+ ...m,
105
+ outputPath: this.#resolveOutput(m.outputFolder),
106
+ sourcePath: path.resolve(this.#manifest.workspacePath, m.sourceFolder),
107
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
108
+ files: Object.fromEntries(
109
+ Object.entries(m.files).map(([folder, files]) => [folder, this.#moduleFiles(m, files ?? [])])
110
+ ) as Record<ManifestModuleFolderType, IndexedFile[]>
111
+ }));
112
+
113
+ for (const mod of this.#modules) {
114
+ for (const files of Object.values(mod.files ?? {})) {
115
+ for (const entry of files) {
116
+ this.#outputToEntry.set(entry.outputFile, entry);
117
+ this.#sourceToEntry.set(entry.sourceFile, entry);
118
+ this.#importToEntry.set(entry.import, entry);
119
+ this.#importToEntry.set(entry.import.replace(/[.]js$/, ''), entry);
120
+ this.#importToEntry.set(entry.import.replace(/[.]js$/, '.ts'), entry);
121
+ }
122
+ }
123
+ }
124
+ this.#modulesByName = Object.fromEntries(this.#modules.map(x => [x.name, x]));
125
+ this.#modulesByFolder = Object.fromEntries(this.#modules.map(x => [x.sourceFolder, x]));
126
+ }
127
+
128
+ /**
129
+ * Get entry by file (input or output)
130
+ */
131
+ getEntry(file: string): IndexedFile | undefined {
132
+ return this.#outputToEntry.get(file) ?? this.#sourceToEntry.get(file);
133
+ }
134
+
135
+ /**
136
+ * Get all local modules
137
+ * @returns
138
+ */
139
+ getLocalModules(): IndexedModule[] {
140
+ return this.#modules.filter(x => x.local);
141
+ }
142
+
143
+ /**
144
+ * Find files from the index
145
+ * @param folder The sub-folder to check into
146
+ * @param filter The filter to determine if this is a valid support file
147
+ */
148
+ find(config: FindConfig): IndexedFile[] {
149
+ const { filter: f, folders } = config;
150
+ const filter = f ? 'test' in f ? f.test.bind(f) : f : f;
151
+
152
+ let idx = this.#modules;
153
+
154
+ const checkProfile = config.checkProfile ?? true;
155
+
156
+ const activeProfiles = new Set(['std', ...(config.profiles ?? process.env.TRV_PROFILES?.split(/\s*,\s*/g) ?? [])]);
157
+
158
+ if (checkProfile) {
159
+ idx = idx.filter(m => m.profiles.length === 0 || m.profiles.some(p => activeProfiles.has(p)));
160
+ }
161
+
162
+ let searchSpace = folders ?
163
+ idx.flatMap(m => [...folders.flatMap(fo => m.files[fo] ?? []), ...(config.includeIndex ? (m.files.$index ?? []) : [])]) :
164
+ idx.flatMap(m => [...Object.values(m.files)].flat());
165
+
166
+ if (checkProfile) {
167
+ searchSpace = searchSpace.filter(fi => activeProfiles.has(fi.profile));
168
+ }
169
+
170
+ return searchSpace
171
+ .filter(({ type }) => type === 'ts')
172
+ .filter(({ sourceFile: source }) => filter?.(source) ?? true);
173
+ }
174
+
175
+ /**
176
+ * Find files from the index
177
+ * @param filter The filter to determine if this is a valid support file
178
+ */
179
+ findSupport(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
180
+ return this.find({ ...config, folders: ['support'] });
181
+ }
182
+
183
+ /**
184
+ * Find files from the index
185
+ * @param filter The filter to determine if this is a valid support file
186
+ */
187
+ findSrc(config: Omit<FindConfig, 'folder'> = {}): IndexedFile[] {
188
+ return this.find({ ...config, includeIndex: true, folders: ['src'] });
189
+ }
190
+
191
+ /**
192
+ * Find files from the index
193
+ * @param filter The filter to determine if this is a valid support file
194
+ */
195
+ findTest(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
196
+ return this.find({ ...config, folders: ['test'] });
197
+ }
198
+
199
+ /**
200
+ * Is module installed?
201
+ */
202
+ hasModule(name: string): boolean {
203
+ return name in this.#manifest.modules;
204
+ }
205
+
206
+ /**
207
+ * Get module
208
+ */
209
+ getModule(name: string): IndexedModule | undefined {
210
+ return this.#modulesByName[name];
211
+ }
212
+
213
+ /**
214
+ * Get module by folder
215
+ */
216
+ getModuleByFolder(folder: string): IndexedModule | undefined {
217
+ return this.#modulesByFolder[folder];
218
+ }
219
+
220
+ /**
221
+ * Resolve import
222
+ */
223
+ resolveFileImport(name: string): string {
224
+ return this.#importToEntry.get(name)?.outputFile ?? name;
225
+ }
226
+
227
+ /**
228
+ * Get indexed module from source file
229
+ * @param source
230
+ */
231
+ getFromSource(source: string): IndexedFile | undefined {
232
+ return this.#sourceToEntry.get(source);
233
+ }
234
+
235
+ /**
236
+ * Get indexed module from source file
237
+ * @param source
238
+ */
239
+ getFromImport(imp: string): IndexedFile | undefined {
240
+ return this.#importToEntry.get(imp);
241
+ }
242
+
243
+ /**
244
+ * Get module from source file
245
+ * @param source
246
+ */
247
+ getModuleFromSource(source: string): IndexedModule | undefined {
248
+ const name = this.getFromSource(source)?.module;
249
+ return name ? this.getModule(name) : undefined;
250
+ }
251
+
252
+ /**
253
+ * Get module from import name
254
+ * @param importName
255
+ */
256
+ getModuleFromImport(importName: string): IndexedModule | undefined {
257
+ const name = this.getFromImport(importName)?.module;
258
+ return name ? this.getModule(name) : undefined;
259
+ }
260
+ /**
261
+ * Build module list from an expression list (e.g. `@travetto/app,-@travetto/log)
262
+ */
263
+ getModuleList(mode: 'local' | 'all', exprList: string = ''): Set<string> {
264
+ const allMods = Object.keys(this.#manifest.modules);
265
+ const active = new Set<string>(
266
+ mode === 'local' ? this.getLocalModules().map(x => x.name) :
267
+ (mode === 'all' ? allMods : [])
268
+ );
269
+
270
+ for (const expr of exprList.split(/\s*,\s*/g)) {
271
+ const [, neg, mod] = expr.match(/(-|[+])?([^+\- ]+)$/) ?? [];
272
+ if (mod) {
273
+ const patt = new RegExp(`^${mod.replace(/[*]/g, '.*')}$`);
274
+ for (const m of allMods.filter(x => patt.test(x))) {
275
+ active[neg ? 'delete' : 'add'](m);
276
+ }
277
+ }
278
+ }
279
+ return active;
280
+ }
281
+
282
+ /**
283
+ * Get all modules (transitively) that depend on this module
284
+ */
285
+ getDependentModules(root: IndexedModule): Set<IndexedModule> {
286
+ const seen = new Set<string>();
287
+ const out = new Set<IndexedModule>();
288
+ const toProcess = [root.name];
289
+ while (toProcess.length) {
290
+ const next = toProcess.shift()!;
291
+ if (seen.has(next)) {
292
+ continue;
293
+ }
294
+ const mod = this.getModule(next)!;
295
+ toProcess.push(...mod.parents);
296
+ out.add(mod);
297
+ }
298
+ return out;
299
+ }
300
+
301
+ /**
302
+ * Get local folders that represent the user's controlled input
303
+ */
304
+ getLocalInputFolders(): string[] {
305
+ return this.getLocalModules()
306
+ .flatMap(x =>
307
+ (!this.manifest.monoRepo || x.sourcePath !== this.manifest.workspacePath) ?
308
+ [x.sourcePath] : [...Object.keys(x.files)].filter(y => !y.startsWith('$')).map(y => path.resolve(x.sourcePath, y))
309
+ );
310
+ }
311
+
312
+ /**
313
+ * Get local output folders
314
+ */
315
+ getLocalOutputFolders(): string[] {
316
+ return this.getLocalModules().map(x => x.outputPath);
317
+ }
318
+ }
package/src/module.ts ADDED
@@ -0,0 +1,213 @@
1
+ import fs from 'fs/promises';
2
+
3
+ import { path } from './path';
4
+ import {
5
+ ManifestContext,
6
+ ManifestModule, ManifestModuleFile, ManifestModuleFileType,
7
+ ManifestModuleFolderType, ManifestProfile
8
+ } from './types';
9
+ import { ModuleDep, ModuleDependencyVisitor } from './dependencies';
10
+ import { PackageUtil } from './package';
11
+
12
+ const EXT_MAPPING: Record<string, ManifestModuleFileType> = {
13
+ '.js': 'js',
14
+ '.mjs': 'js',
15
+ '.cjs': 'js',
16
+ '.json': 'json',
17
+ '.ts': 'ts',
18
+ '.md': 'md'
19
+ };
20
+
21
+ const INDEX_FILES = new Set([
22
+ 'index.ts',
23
+ 'index.js',
24
+ '__index__.ts',
25
+ '__index__.js',
26
+ '__index.ts',
27
+ '__index.js'
28
+ ]);
29
+
30
+ export class ManifestModuleUtil {
31
+
32
+ static #scanCache: Record<string, string[]> = {};
33
+
34
+ static #getNewest(stat: { mtimeMs: number, ctimeMs: number }): number {
35
+ return Math.max(stat.mtimeMs, stat.ctimeMs);
36
+ }
37
+
38
+ /**
39
+ * Simple file scanning
40
+ */
41
+ static async scanFolder(folder: string, mainSource = false): Promise<string[]> {
42
+ if (!mainSource && folder in this.#scanCache) {
43
+ return this.#scanCache[folder];
44
+ }
45
+
46
+ if (!await fs.stat(folder).catch(() => false)) {
47
+ return [];
48
+ }
49
+
50
+ const topFolders = new Set(mainSource ? [] : ['src', 'bin', 'support']);
51
+ const topFiles = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
52
+ const out: string[] = [];
53
+
54
+ if (!fs.stat(folder).catch(() => false)) {
55
+ return out;
56
+ }
57
+ const stack: [string, number][] = [[folder, 0]];
58
+ while (stack.length) {
59
+ const popped = stack.pop();
60
+ if (!popped) {
61
+ continue;
62
+ }
63
+
64
+ const [top, depth] = popped;
65
+
66
+ // Don't navigate into sub-folders with package.json's
67
+ if (top !== folder && await fs.stat(`${top}/package.json`).catch(() => false)) {
68
+ continue;
69
+ }
70
+
71
+ for (const sub of await fs.readdir(top)) {
72
+ const stat = await fs.stat(`${top}/${sub}`);
73
+ if (stat.isFile()) {
74
+ if (!sub.startsWith('.') && (depth > 0 || !topFiles.size || topFiles.has(sub))) {
75
+ out.push(`${top}/${sub}`);
76
+ }
77
+ } else {
78
+ if (!sub.includes('node_modules') && !sub.startsWith('.') && (depth > 0 || !topFolders.size || topFolders.has(sub))) {
79
+ stack.push([`${top}/${sub}`, depth + 1]);
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ if (!mainSource) {
86
+ this.#scanCache[folder] = out;
87
+ }
88
+
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * Get file type for a file name
94
+ */
95
+ static getFileType(moduleFile: string): ManifestModuleFileType {
96
+ if (moduleFile === 'package.json') {
97
+ return 'package-json';
98
+ } else if (
99
+ moduleFile.startsWith('support/fixtures/') ||
100
+ moduleFile.startsWith('test/fixtures/') ||
101
+ moduleFile.startsWith('support/resources/')
102
+ ) {
103
+ return 'fixture';
104
+ } else if (moduleFile.endsWith('.d.ts')) {
105
+ return 'typings';
106
+ } else {
107
+ const ext = path.extname(moduleFile);
108
+ return EXT_MAPPING[ext] ?? 'unknown';
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get file type for a file name
114
+ */
115
+ static getFileProfile(moduleFile: string): ManifestProfile | undefined {
116
+ if (moduleFile.startsWith('support/transform')) {
117
+ return 'compile';
118
+ } else if (moduleFile.startsWith('support/test/') || moduleFile.startsWith('test/')) {
119
+ return 'test';
120
+ } else if (moduleFile.startsWith('doc/') || moduleFile === 'DOC.ts') {
121
+ return 'doc';
122
+ } else {
123
+ return;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get folder key
129
+ */
130
+ static getFolderKey(moduleFile: string): ManifestModuleFolderType {
131
+ const folderLocation = moduleFile.indexOf('/');
132
+ if (folderLocation > 0) {
133
+ if (moduleFile.startsWith('test/fixtures/')) {
134
+ return 'test/fixtures';
135
+ } else if (moduleFile.startsWith('support/fixtures/')) {
136
+ return 'support/fixtures';
137
+ } else if (moduleFile.startsWith('support/resources/')) {
138
+ return 'support/resources';
139
+ } else if (moduleFile.startsWith('support/transform')) {
140
+ return '$transformer';
141
+ }
142
+ const key = moduleFile.substring(0, folderLocation);
143
+ switch (key) {
144
+ case 'src':
145
+ case 'bin':
146
+ case 'test':
147
+ case 'doc':
148
+ case 'resources':
149
+ case 'support': return key;
150
+ default: return '$other';
151
+ }
152
+ } else if (moduleFile === 'DOC.ts') {
153
+ return 'doc';
154
+ } else if (INDEX_FILES.has(moduleFile)) {
155
+ return '$index';
156
+ } else if (moduleFile === 'package.json') {
157
+ return '$package';
158
+ } else {
159
+ return '$root';
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Convert file (by ext) to a known file type and also retrieve its latest timestamp
165
+ */
166
+ static async transformFile(moduleFile: string, full: string): Promise<ManifestModuleFile> {
167
+ const res: ManifestModuleFile = [moduleFile, this.getFileType(moduleFile), this.#getNewest(await fs.stat(full))];
168
+ const profile = this.getFileProfile(moduleFile);
169
+ return profile ? [...res, profile] : res;
170
+ }
171
+
172
+ /**
173
+ * Visit a module and describe files, and metadata
174
+ */
175
+ static async describeModule(ctx: ManifestContext, dep: ModuleDep): Promise<ManifestModule> {
176
+ const { main, mainSource, local, name, version, sourcePath, profileSet, parentSet, internal } = dep;
177
+
178
+ const files: ManifestModule['files'] = {};
179
+
180
+ for (const file of await this.scanFolder(sourcePath, mainSource)) {
181
+ // Group by top folder
182
+ const moduleFile = file.replace(`${sourcePath}/`, '');
183
+ const entry = await this.transformFile(moduleFile, file);
184
+ const key = this.getFolderKey(moduleFile);
185
+ (files[key] ??= []).push(entry);
186
+ }
187
+
188
+ // Refine non-main source
189
+ if (!mainSource) {
190
+ files.$root = files.$root?.filter(([file, type]) => type !== 'ts');
191
+ }
192
+
193
+ const profiles = [...profileSet].sort();
194
+ const parents = [...parentSet].sort();
195
+ const outputFolder = `node_modules/${name}`;
196
+ const sourceFolder = sourcePath === ctx.workspacePath ? '' : sourcePath.replace(`${ctx.workspacePath}/`, '');
197
+
198
+ const res = { main, name, version, local, internal, sourceFolder, outputFolder, files, profiles, parents, };
199
+ return res;
200
+ }
201
+
202
+ /**
203
+ * Produce all modules for a given manifest folder, adding in some given modules when developing framework
204
+ */
205
+ static async produceModules(ctx: ManifestContext): Promise<Record<string, ManifestModule>> {
206
+ const visitor = new ModuleDependencyVisitor(ctx);
207
+ const mainPath = path.resolve(ctx.workspacePath, ctx.mainFolder);
208
+ const declared = await PackageUtil.visitPackages(mainPath, visitor);
209
+ const sorted = [...declared].sort((a, b) => a.name.localeCompare(b.name));
210
+ const modules = await Promise.all(sorted.map(x => this.describeModule(ctx, x)));
211
+ return Object.fromEntries(modules.map(m => [m.name, m]));
212
+ }
213
+ }