@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 +21 -0
- package/README.md +14 -0
- package/__index__.ts +10 -0
- package/package.json +39 -0
- package/src/delta.ts +114 -0
- package/src/dependencies.ts +163 -0
- package/src/manifest-index.ts +290 -0
- package/src/manifest.ts +110 -0
- package/src/module.ts +192 -0
- package/src/package.ts +201 -0
- package/src/path.ts +17 -0
- package/src/root-index.ts +169 -0
- package/src/types.ts +119 -0
- package/src/typings.d.ts +3 -0
- package/support/transformer.function-metadata.ts +137 -0
package/src/manifest.ts
ADDED
|
@@ -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!;
|