@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.
@@ -0,0 +1,110 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+
4
+ import { path } from './path';
5
+ import { ManifestContext, ManifestRoot, ManifestState } from './types';
6
+
7
+ import { ManifestModuleUtil } from './module';
8
+ import { ManifestDeltaUtil } from './delta';
9
+
10
+ /**
11
+ * Manifest utils
12
+ */
13
+ export class ManifestUtil {
14
+
15
+ /**
16
+ * Produce manifest in memory
17
+ */
18
+ static async buildManifest(ctx: ManifestContext): Promise<ManifestRoot> {
19
+ return {
20
+ modules: await ManifestModuleUtil.produceModules(ctx),
21
+ generated: Date.now(),
22
+ ...ctx
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Generate manifest for a given context, and persist
28
+ */
29
+ static async createAndWriteManifest(ctx: ManifestContext): Promise<void> {
30
+ const manifest = await this.buildManifest(ctx);
31
+ await this.writeManifest(ctx, manifest);
32
+ }
33
+
34
+ /**
35
+ * Read manifest from a folder
36
+ */
37
+ static async readManifest(ctx: ManifestContext): Promise<ManifestRoot> {
38
+ const file = path.resolve(ctx.workspacePath, ctx.outputFolder, ctx.manifestFile);
39
+ if (await fs.stat(file).catch(() => false)) {
40
+ try {
41
+ return JSON.parse(await fs.readFile(file, 'utf8'));
42
+ } catch (err) {
43
+ await fs.unlink(file).catch(() => { });
44
+ const final = new Error(`Corrupted manifest ${ctx.manifestFile}: file has been removed, retry`);
45
+ if (err instanceof Error) {
46
+ final.stack = [final.message, ...(err.stack ?? '').split('\n').slice(1)].join('\n');
47
+ }
48
+ throw final;
49
+ }
50
+ } else {
51
+ return {
52
+ modules: {},
53
+ generated: Date.now(),
54
+ ...ctx,
55
+ };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Load state from disk
61
+ */
62
+ static async readState(file: string): Promise<ManifestState> {
63
+ return JSON.parse(await fs.readFile(file, 'utf8'));
64
+ }
65
+
66
+ /**
67
+ * Persist state to disk in a temp file, return said temp file
68
+ */
69
+ static async writeState(state: ManifestState, file?: string): Promise<string> {
70
+ const manifestTemp = file ?? path.resolve(os.tmpdir(), `manifest-state.${Date.now()}${Math.random()}.json`);
71
+ await fs.writeFile(manifestTemp, JSON.stringify(state), 'utf8');
72
+ return manifestTemp;
73
+ }
74
+
75
+ /**
76
+ * Generate the manifest and delta as a single output
77
+ */
78
+ static async produceState(ctx: ManifestContext): Promise<ManifestState> {
79
+ const manifest = await this.buildManifest(ctx);
80
+ const oldManifest = await this.readManifest(ctx);
81
+ const delta = await ManifestDeltaUtil.produceDelta(
82
+ path.resolve(manifest.workspacePath, ctx.outputFolder),
83
+ manifest,
84
+ oldManifest
85
+ );
86
+ return { manifest, delta };
87
+ }
88
+
89
+ /**
90
+ * Resolves a module file, from a context and manifest
91
+ */
92
+ static resolveFile(ctx: ManifestContext, manifest: ManifestRoot, module: string, file: string): string {
93
+ return path.resolve(
94
+ ctx.workspacePath,
95
+ ctx.compilerFolder,
96
+ manifest.modules[module].output,
97
+ file
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Write manifest for a given context
103
+ */
104
+ static async writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<void> {
105
+ // Write manifest in the scenario we are in mono-repo state where everything pre-existed
106
+ const file = path.resolve(ctx.workspacePath, ctx.outputFolder, ctx.manifestFile);
107
+ await fs.mkdir(path.dirname(file), { recursive: true });
108
+ await fs.writeFile(file, JSON.stringify(manifest));
109
+ }
110
+ }
package/src/module.ts ADDED
@@ -0,0 +1,192 @@
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 #getNewest(stat: { mtimeMs: number, ctimeMs: number }): number {
33
+ return Math.max(stat.mtimeMs, stat.ctimeMs);
34
+ }
35
+
36
+ /**
37
+ * Simple file scanning
38
+ */
39
+ static async #scanFolder(folder: string, topFolders = new Set<string>(), topFiles = new Set<string>()): Promise<string[]> {
40
+ const out: string[] = [];
41
+ if (!fs.stat(folder).catch(() => false)) {
42
+ return out;
43
+ }
44
+ const stack: [string, number][] = [[folder, 0]];
45
+ while (stack.length) {
46
+ const popped = stack.pop();
47
+ if (!popped) {
48
+ continue;
49
+ }
50
+
51
+ const [top, depth] = popped;
52
+
53
+ // Don't navigate into sub-folders with package.json's
54
+ if (top !== folder && await fs.stat(`${top}/package.json`).catch(() => false)) {
55
+ continue;
56
+ }
57
+
58
+ for (const sub of await fs.readdir(top)) {
59
+ const stat = await fs.stat(`${top}/${sub}`);
60
+ if (stat.isFile()) {
61
+ if (!sub.startsWith('.') && (depth > 0 || !topFiles.size || topFiles.has(sub))) {
62
+ out.push(`${top}/${sub}`);
63
+ }
64
+ } else {
65
+ if (!sub.includes('node_modules') && !sub.startsWith('.') && (depth > 0 || !topFolders.size || topFolders.has(sub))) {
66
+ stack.push([`${top}/${sub}`, depth + 1]);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Get file type for a file name
76
+ */
77
+ static getFileType(moduleFile: string): ManifestModuleFileType {
78
+ if (moduleFile === 'package.json') {
79
+ return 'package-json';
80
+ } else if (
81
+ moduleFile.startsWith('support/fixtures/') ||
82
+ moduleFile.startsWith('test/fixtures/') ||
83
+ moduleFile.startsWith('support/resources/')
84
+ ) {
85
+ return 'fixture';
86
+ } else if (moduleFile.endsWith('.d.ts')) {
87
+ return 'typings';
88
+ } else {
89
+ const ext = path.extname(moduleFile);
90
+ return EXT_MAPPING[ext] ?? 'unknown';
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get file type for a file name
96
+ */
97
+ static getFileProfile(moduleFile: string): ManifestProfile | undefined {
98
+ if (moduleFile.startsWith('support/transform')) {
99
+ return 'compile';
100
+ } else if (moduleFile.startsWith('support/test/') || moduleFile.startsWith('test/')) {
101
+ return 'test';
102
+ } else if (moduleFile.startsWith('doc/') || moduleFile === 'DOC.ts') {
103
+ return 'doc';
104
+ } else {
105
+ return;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Get folder key
111
+ */
112
+ static getFolderKey(moduleFile: string): ManifestModuleFolderType {
113
+ const folderLocation = moduleFile.indexOf('/');
114
+ if (folderLocation > 0) {
115
+ if (moduleFile.startsWith('test/fixtures/')) {
116
+ return 'test/fixtures';
117
+ } else if (moduleFile.startsWith('support/fixtures/')) {
118
+ return 'support/fixtures';
119
+ } else if (moduleFile.startsWith('support/resources/')) {
120
+ return 'support/resources';
121
+ }
122
+ const key = moduleFile.substring(0, folderLocation);
123
+ switch (key) {
124
+ case 'src':
125
+ case 'bin':
126
+ case 'test':
127
+ case 'doc':
128
+ case 'resources':
129
+ case 'support': return key;
130
+ default: return '$other';
131
+ }
132
+ } else if (moduleFile === 'DOC.ts') {
133
+ return 'doc';
134
+ } else if (INDEX_FILES.has(moduleFile)) {
135
+ return '$index';
136
+ } else if (moduleFile === 'package.json') {
137
+ return '$package';
138
+ } else {
139
+ return '$root';
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Convert file (by ext) to a known file type and also retrieve its latest timestamp
145
+ */
146
+ static async transformFile(moduleFile: string, full: string): Promise<ManifestModuleFile> {
147
+ const res: ManifestModuleFile = [moduleFile, this.getFileType(moduleFile), this.#getNewest(await fs.stat(full))];
148
+ const profile = this.getFileProfile(moduleFile);
149
+ return profile ? [...res, profile] : res;
150
+ }
151
+
152
+ /**
153
+ * Visit a module and describe files, and metadata
154
+ */
155
+ static async describeModule(dep: ModuleDep): Promise<ManifestModule> {
156
+ const { main, mainSource, local, name, version, sourcePath, profileSet, parentSet, internal } = dep;
157
+ const files: ManifestModule['files'] = {};
158
+ const folderSet = new Set<ManifestModuleFolderType>(mainSource ? [] : ['src', 'bin', 'support']);
159
+ const fileSet = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
160
+
161
+ for (const file of await this.#scanFolder(sourcePath, folderSet, fileSet)) {
162
+ // Group by top folder
163
+ const moduleFile = file.replace(`${sourcePath}/`, '');
164
+ const entry = await this.transformFile(moduleFile, file);
165
+ const key = this.getFolderKey(moduleFile);
166
+ (files[key] ??= []).push(entry);
167
+ }
168
+
169
+ // Refine non-main source
170
+ if (!mainSource) {
171
+ files.$root = files.$root?.filter(([file, type]) => type !== 'ts');
172
+ }
173
+
174
+ const profiles = [...profileSet].sort();
175
+ const parents = [...parentSet].sort();
176
+ const output = `node_modules/${name}`;
177
+
178
+ return { main, name, version, local, internal, source: sourcePath, output, files, profiles, parents, };
179
+ }
180
+
181
+ /**
182
+ * Produce all modules for a given manifest folder, adding in some given modules when developing framework
183
+ */
184
+ static async produceModules(ctx: ManifestContext): Promise<Record<string, ManifestModule>> {
185
+ const visitor = new ModuleDependencyVisitor(ctx);
186
+ const declared = await PackageUtil.visitPackages(ctx.mainPath, visitor);
187
+ const sorted = [...declared].sort((a, b) => a.name.localeCompare(b.name));
188
+
189
+ const modules = await Promise.all(sorted.map(x => this.describeModule(x)));
190
+ return Object.fromEntries(modules.map(m => [m.name, m]));
191
+ }
192
+ }
package/src/package.ts ADDED
@@ -0,0 +1,201 @@
1
+ import { readFileSync } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import { createRequire } from 'module';
4
+ import { execSync } from 'child_process';
5
+
6
+ import { Package, PackageDigest, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
7
+ import { path } from './path';
8
+
9
+ export class PackageUtil {
10
+
11
+ static #req = createRequire(`${path.cwd()}/node_modules`);
12
+ static #framework: Package;
13
+ static #cache: Record<string, Package> = {};
14
+ static #workspaces: Record<string, PackageWorkspaceEntry[]> = {};
15
+
16
+ static resolveImport = (library: string): string => this.#req.resolve(library);
17
+
18
+ /**
19
+ * Resolve version path, if file: url
20
+ */
21
+ static resolveVersionPath(rootPath: string, ver: string): string | undefined {
22
+ if (ver.startsWith('file:')) {
23
+ return path.resolve(rootPath, ver.replace('file:', ''));
24
+ } else {
25
+ return;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Find package.json folder for a given dependency
31
+ */
32
+ static resolvePackagePath(name: string): string {
33
+ try {
34
+ return path.dirname(this.resolveImport(`${name}/package.json`));
35
+ } catch {
36
+ try {
37
+ const resolved = this.resolveImport(name);
38
+ return path.join(resolved.split(name)[0], name);
39
+ } catch { }
40
+ }
41
+ throw new Error(`Unable to resolve: ${name}`);
42
+ }
43
+
44
+ /**
45
+ * Build a package visit req
46
+ */
47
+ static packageReq<T>(sourcePath: string, rel: PackageRel): PackageVisitReq<T> {
48
+ return { pkg: this.readPackage(sourcePath), sourcePath, rel };
49
+ }
50
+
51
+ /**
52
+ * Extract all dependencies from a package
53
+ */
54
+ static getAllDependencies<T = unknown>(modulePath: string, rootPath: string): PackageVisitReq<T>[] {
55
+ const pkg = this.readPackage(modulePath);
56
+ const children: Record<string, PackageVisitReq<T>> = {};
57
+ for (const [deps, rel] of [
58
+ [pkg.dependencies, 'prod'],
59
+ [pkg.peerDependencies, 'peer'],
60
+ [pkg.optionalDependencies, 'opt'],
61
+ ...(modulePath === rootPath ? [[pkg.devDependencies, 'dev'] as const] : []),
62
+ ] as const) {
63
+ for (const [name, version] of Object.entries(deps ?? {})) {
64
+ try {
65
+ const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name);
66
+ children[`${name}#${version}`] = this.packageReq<T>(depPath, rel);
67
+ } catch (err) {
68
+ if (rel === 'opt' || (rel === 'peer' && !!pkg.peerDependenciesMeta?.[name].optional)) {
69
+ continue;
70
+ }
71
+ throw err;
72
+ }
73
+ }
74
+ }
75
+ return Object.values(children).sort((a, b) => a.pkg.name.localeCompare(b.pkg.name));
76
+ }
77
+
78
+ /**
79
+ * Read a package.json from a given folder
80
+ */
81
+ static readPackage(modulePath: string): Package {
82
+ return this.#cache[modulePath] ??= JSON.parse(readFileSync(
83
+ modulePath.endsWith('.json') ? modulePath : path.resolve(modulePath, 'package.json'),
84
+ 'utf8'
85
+ ));
86
+ }
87
+
88
+ /**
89
+ * import a package.json from a given module name
90
+ */
91
+ static importPackage(moduleName: string): Package {
92
+ return this.readPackage(this.resolvePackagePath(moduleName));
93
+ }
94
+
95
+ /**
96
+ * Write package
97
+ */
98
+ static async writePackageIfChanged(modulePath: string, pkg: Package): Promise<void> {
99
+ const final = JSON.stringify(pkg, null, 2);
100
+ const target = path.resolve(modulePath, 'package.json');
101
+ const current = (await fs.readFile(target, 'utf8').catch(() => '')).trim();
102
+ if (final !== current) {
103
+ await fs.writeFile(target, `${final}\n`, 'utf8');
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Visit packages with ability to track duplicates
109
+ */
110
+ static async visitPackages<T>(
111
+ rootOrPath: PackageVisitReq<T> | string,
112
+ visitor: PackageVisitor<T>
113
+ ): Promise<Set<T>> {
114
+
115
+ const root = typeof rootOrPath === 'string' ?
116
+ this.packageReq<T>(rootOrPath, 'root') :
117
+ rootOrPath;
118
+
119
+ const seen = new Map<string, T>();
120
+ const queue: PackageVisitReq<T>[] = [...await visitor.init?.(root) ?? [], root];
121
+ const out = new Set<T>();
122
+
123
+ while (queue.length) {
124
+ const req = queue.pop();
125
+
126
+ if (!req || (visitor.valid && !visitor.valid(req))) {
127
+ continue;
128
+ }
129
+
130
+ const key = req.sourcePath;
131
+ if (seen.has(key)) {
132
+ await visitor.visit?.(req, seen.get(key)!);
133
+ } else {
134
+ const dep = await visitor.create(req);
135
+ out.add(dep);
136
+ await visitor.visit?.(req, dep);
137
+ seen.set(key, dep);
138
+ const children = this.getAllDependencies<T>(req.sourcePath, root.sourcePath);
139
+ queue.push(...children.map(x => ({ ...x, parent: dep })));
140
+ }
141
+ }
142
+ return (await visitor.complete?.(out)) ?? out;
143
+ }
144
+
145
+ /**
146
+ * Get version of manifest package
147
+ */
148
+ static getFrameworkVersion(): string {
149
+ return (this.#framework ??= this.importPackage('@travetto/manifest')).version;
150
+ }
151
+
152
+ /**
153
+ * Produce simple digest of
154
+ */
155
+ static digest(pkg: Package): PackageDigest {
156
+ const { main, name, author, license, version } = pkg;
157
+ return { name, main, author, license, version, framework: this.getFrameworkVersion() };
158
+ }
159
+
160
+ /**
161
+ * Find workspace values from rootPath
162
+ */
163
+ static resolveWorkspaces(rootPath: string): PackageWorkspaceEntry[] {
164
+ if (!this.#workspaces[rootPath]) {
165
+ const text = execSync('npm query .workspace', { cwd: rootPath, encoding: 'utf8', env: {} });
166
+ const res: { location: string, name: string }[] = JSON.parse(text);
167
+ this.#workspaces[rootPath] = res.map(d => ({ sourcePath: d.location, name: d.name }));
168
+ }
169
+ return this.#workspaces[rootPath];
170
+ }
171
+
172
+ /**
173
+ * Sync versions across a series of folders
174
+ */
175
+ static async syncVersions(folders: string[], versionMapping: Record<string, string> = {}): Promise<void> {
176
+ const packages = folders.map(folder => {
177
+ const pkg = this.readPackage(folder);
178
+ versionMapping[pkg.name] = `^${pkg.version}`;
179
+ return { folder, pkg };
180
+ });
181
+
182
+ for (const { pkg } of packages) {
183
+ for (const group of [
184
+ pkg.dependencies ?? {},
185
+ pkg.devDependencies ?? {},
186
+ pkg.optionalDependencies ?? {},
187
+ pkg.peerDependencies ?? {}
188
+ ]) {
189
+ for (const [mod, ver] of Object.entries(versionMapping)) {
190
+ if (mod in group && !/^[*]|(file:.*)$/.test(group[mod])) {
191
+ group[mod] = ver;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ for (const { folder, pkg } of packages) {
198
+ await this.writePackageIfChanged(folder, pkg);
199
+ }
200
+ }
201
+ }
package/src/path.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { extname, dirname, resolve, basename, delimiter, join, sep } from 'path';
2
+
3
+ const posix = (file: string): string => file.replaceAll('\\', '/');
4
+
5
+ const cwd = (): string => posix(process.cwd());
6
+
7
+ export const path = {
8
+ cwd,
9
+ toPosix: posix,
10
+ delimiter,
11
+ basename: (file: string): string => posix(basename(file)),
12
+ extname: (file: string): string => posix(extname(file)),
13
+ dirname: (file: string): string => posix(dirname(file)),
14
+ resolve: (...args: string[]): string => posix(resolve(cwd(), ...args.map(f => posix(f)))),
15
+ join: (root: string, ...args: string[]): string => posix(join(posix(root), ...args.map(f => posix(f)))),
16
+ toNative: (file: string): string => file.replaceAll('/', sep)
17
+ };
@@ -0,0 +1,169 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { createRequire } from 'module';
3
+
4
+ import { path } from './path';
5
+ import { IndexedModule, ManifestIndex } from './manifest-index';
6
+ import { FunctionMetadata, Package, PackageDigest } from './types';
7
+ import { PackageUtil } from './package';
8
+
9
+ const METADATA = Symbol.for('@travetto/manifest:metadata');
10
+ type Metadated = { [METADATA]: FunctionMetadata };
11
+
12
+ /**
13
+ * Extended manifest index geared for application execution
14
+ */
15
+ class $RootIndex extends ManifestIndex {
16
+ /**
17
+ * Load all source modules
18
+ */
19
+ static resolveManifestJSON(root: string, file?: string): string {
20
+ file = file || path.resolve(root, 'manifest.json');
21
+
22
+ // IF not a file
23
+ if (!file.endsWith('.json')) {
24
+ try {
25
+ // Try to resolve
26
+ const req = createRequire(path.resolve(root, 'node_modules'));
27
+ file = req.resolve(`${file}/manifest.json`);
28
+ } catch {
29
+ // Fallback to assumed node_modules pattern
30
+ file = `${root}/node_modules/${file}/manifest.json`;
31
+ }
32
+ }
33
+ return file;
34
+ }
35
+
36
+ #config: Package | undefined;
37
+ #srcCache = new Map();
38
+ #metadata = new Map<string, FunctionMetadata>();
39
+
40
+ constructor(output: string = process.env.TRV_OUTPUT ?? process.cwd()) {
41
+ super(output, $RootIndex.resolveManifestJSON(output, process.env.TRV_MANIFEST));
42
+ }
43
+
44
+ /**
45
+ * **WARNING**: This is a destructive operation, and should only be called before loading any code
46
+ * @private
47
+ */
48
+ reinitForModule(module: string): void {
49
+ this.init(this.root, $RootIndex.resolveManifestJSON(this.root, module));
50
+ this.#config = undefined;
51
+ }
52
+
53
+ /**
54
+ * Determines if the manifest root is the root for a monorepo
55
+ */
56
+ isMonoRepoRoot(): boolean {
57
+ return !!this.manifest.monoRepo && this.manifest.workspacePath === this.manifest.mainPath;
58
+ }
59
+
60
+ /**
61
+ * Asynchronously load all source files from manifest
62
+ */
63
+ async loadSource(): Promise<void> {
64
+ for (const { output } of this.findSrc()) {
65
+ await import(output);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get internal id from file name and optionally, class name
71
+ */
72
+ getId(filename: string, clsName?: string): string {
73
+ filename = path.toPosix(filename);
74
+ const id = this.getEntry(filename)?.id ?? filename;
75
+ return clsName ? `${id}○${clsName}` : id;
76
+ }
77
+
78
+ /**
79
+ * Get main module for manifest
80
+ */
81
+ get mainModule(): IndexedModule {
82
+ return this.getModule(this.mainPackage.name)!;
83
+ }
84
+
85
+ /**
86
+ * Get main package for manifest
87
+ */
88
+ get mainPackage(): Package {
89
+ if (!this.#config) {
90
+ const { output: mainFolder } = this.getModule(this.manifest.mainModule)!;
91
+ this.#config = {
92
+ ...{
93
+ name: 'untitled',
94
+ description: 'A Travetto application',
95
+ version: '0.0.0',
96
+ },
97
+ ...PackageUtil.readPackage(mainFolder)
98
+ };
99
+ }
100
+ return this.#config;
101
+ }
102
+
103
+ mainDigest(): PackageDigest {
104
+ return PackageUtil.digest(this.mainPackage);
105
+ }
106
+
107
+ /**
108
+ * Get source file from output location
109
+ * @param outputFile
110
+ */
111
+ getSourceFile(file: string): string {
112
+ if (!this.#srcCache.has(file)) {
113
+ if (file.startsWith('file:')) {
114
+ this.#srcCache.set(file, path.toPosix(fileURLToPath(file)));
115
+ } else {
116
+ this.#srcCache.set(file, path.toPosix(file));
117
+ }
118
+ }
119
+
120
+ const outputFile = this.#srcCache.get(file)!;
121
+ return this.getEntry(outputFile)?.source ?? outputFile;
122
+ }
123
+
124
+ /**
125
+ * Initialize the meta data for a function/class
126
+ * @param cls Class
127
+ * @param `file` Filename
128
+ * @param `hash` Hash of class contents
129
+ * @param `methods` Methods and their hashes
130
+ * @param `abstract` Is the class abstract
131
+ */
132
+ registerFunction(cls: Function, file: string, hash: number, methods?: Record<string, { hash: number }>, abstract?: boolean, synthetic?: boolean): boolean {
133
+ const source = this.getSourceFile(file);
134
+ const id = this.getId(source, cls.name);
135
+ Object.defineProperty(cls, 'Ⲑid', { value: id });
136
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
137
+ (cls as unknown as Metadated)[METADATA] = { id, source, hash, methods, abstract, synthetic };
138
+ this.#metadata.set(id, { id, source, hash, methods, abstract, synthetic });
139
+ return true;
140
+ }
141
+
142
+ /**
143
+ * Retrieve function metadata by function, or function id
144
+ */
145
+ getFunctionMetadataFromClass(cls: Function | undefined): FunctionMetadata | undefined {
146
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
147
+ return (cls as unknown as Metadated)?.[METADATA];
148
+ }
149
+
150
+ /**
151
+ * Retrieve function metadata by function, or function id
152
+ */
153
+ getFunctionMetadata(clsId: string | Function): FunctionMetadata | undefined {
154
+ const id = clsId === undefined ? '' : typeof clsId === 'string' ? clsId : clsId.Ⲑid;
155
+ return this.#metadata.get(id);
156
+ }
157
+ }
158
+
159
+ let index: $RootIndex | undefined;
160
+
161
+ try {
162
+ index = new $RootIndex();
163
+ } catch (err) {
164
+ if (process.env.TRV_THROW_ROOT_INDEX_ERR) {
165
+ throw err;
166
+ }
167
+ }
168
+
169
+ export const RootIndex: $RootIndex = index!;