@travetto/manifest 3.4.0 → 4.0.0-rc.1

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/src/package.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { createRequire } from 'module';
2
- import { execSync } from 'child_process';
1
+ import { createRequire } from 'node:module';
2
+ import { execSync } from 'node:child_process';
3
3
 
4
- import { ManifestContext, Package, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
5
4
  import { path } from './path';
6
5
  import { ManifestFileUtil } from './file';
7
6
 
7
+ import { PackagePath, type Package, type PackageVisitor, type PackageWorkspaceEntry } from './types/package';
8
+ import type { ManifestContext } from './types/context';
9
+ import type { NodePackageManager } from './types/common';
10
+
8
11
  /**
9
12
  * Utilities for querying, traversing and reading package.json files.
10
13
  */
11
14
  export class PackageUtil {
12
15
 
13
- static #req = createRequire(path.resolve('node_modules'));
16
+ static #resolvers: Record<string, (imp: string) => string> = {};
14
17
  static #cache: Record<string, Package> = {};
15
18
  static #workspaces: Record<string, PackageWorkspaceEntry[]> = {};
16
19
 
@@ -21,21 +24,19 @@ export class PackageUtil {
21
24
  }
22
25
 
23
26
  /**
24
- * Clear out cached package file reads
27
+ * Resolve import given a manifest context
25
28
  */
26
- static clearCache(): void {
27
- this.#cache = {};
28
- this.#workspaces = {};
29
+ static resolveImport(imp: string, root?: string): string {
30
+ const loc = path.resolve(root ?? '.', 'node_modules');
31
+ return (this.#resolvers[loc] ??= createRequire(loc).resolve.bind(null))(imp);
29
32
  }
30
33
 
31
- static resolveImport = (library: string): string => this.#req.resolve(library);
32
-
33
34
  /**
34
35
  * Resolve version path, if file: url
35
36
  */
36
- static resolveVersionPath(rootPath: string, ver: string): string | undefined {
37
+ static resolveVersionPath(root: Package, ver: string): string | undefined {
37
38
  if (ver.startsWith('file:')) {
38
- return path.resolve(rootPath, ver.replace('file:', ''));
39
+ return path.resolve(this.getPackagePath(root), ver.replace('file:', ''));
39
40
  } else {
40
41
  return;
41
42
  }
@@ -44,43 +45,18 @@ export class PackageUtil {
44
45
  /**
45
46
  * Find package.json folder for a given dependency
46
47
  */
47
- static resolvePackagePath(name: string): string {
48
+ static resolvePackagePath(name: string, root?: string): string {
48
49
  try {
49
- return path.dirname(this.resolveImport(`${name}/package.json`));
50
+ return path.dirname(this.resolveImport(`${name}/package.json`, root));
50
51
  } catch {
51
52
  try {
52
- const resolved = this.resolveImport(name);
53
+ const resolved = this.resolveImport(name, root);
53
54
  return path.join(resolved.split(name)[0], name);
54
55
  } catch { }
55
56
  }
56
57
  throw new Error(`Unable to resolve: ${name}`);
57
58
  }
58
59
 
59
- /**
60
- * Build a package visit req
61
- */
62
- static packageReq<T>(sourcePath: string, prod: boolean, topLevel?: boolean): PackageVisitReq<T> {
63
- return { pkg: this.readPackage(sourcePath), sourcePath, prod, topLevel };
64
- }
65
-
66
- /**
67
- * Extract all dependencies from a package
68
- */
69
- static getAllDependencies<T = unknown>(modulePath: string, local: boolean): PackageVisitReq<T>[] {
70
- const pkg = this.readPackage(modulePath);
71
- const children: Record<string, PackageVisitReq<T>> = {};
72
- for (const [deps, prod] of [
73
- [pkg.dependencies, true],
74
- ...(local ? [[pkg.devDependencies, false] as const] : []),
75
- ] as const) {
76
- for (const [name, version] of Object.entries(deps ?? {})) {
77
- const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name);
78
- children[`${name}#${version}`] = this.packageReq<T>(depPath, prod, false);
79
- }
80
- }
81
- return Object.values(children).sort((a, b) => a.pkg.name.localeCompare(b.pkg.name));
82
- }
83
-
84
60
  /**
85
61
  * Read a package.json from a given folder
86
62
  */
@@ -94,90 +70,80 @@ export class PackageUtil {
94
70
 
95
71
  res.name ??= 'untitled'; // If a package.json (root-only) is missing a name, allows for npx execution
96
72
 
73
+ res[PackagePath] = modulePath;
97
74
  return res;
98
75
  }
99
76
 
100
77
  /**
101
- * import a package.json from a given module name
78
+ * Get the package path
102
79
  */
103
- static importPackage(moduleName: string): Package {
104
- return this.readPackage(this.resolvePackagePath(moduleName));
80
+ static getPackagePath(pkg: Package): string {
81
+ return pkg[PackagePath];
105
82
  }
106
83
 
107
84
  /**
108
85
  * Visit packages with ability to track duplicates
109
86
  */
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, false, true) :
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>();
87
+ static async visitPackages<T>(visitor: PackageVisitor<T>): Promise<Iterable<T>> {
88
+ const seen = new Set<T>();
89
+ const queue = [...await visitor.init()];
122
90
 
123
91
  while (queue.length) {
124
- const req = queue.pop();
92
+ const node = queue.shift()!; // Visit initial set first
125
93
 
126
- if (!req || (visitor.valid && !visitor.valid(req))) {
94
+ if (!visitor.valid(node)) {
127
95
  continue;
128
96
  }
129
97
 
130
- const key = req.sourcePath;
131
- if (seen.has(key)) {
132
- await visitor.visit?.(req, seen.get(key)!);
98
+ visitor.visit(node);
99
+
100
+ if (seen.has(node.value)) {
101
+ continue;
133
102
  } 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>(
139
- req.sourcePath,
140
- // We consider a module local if its not in the node_modules
141
- !req.sourcePath.includes('node_modules') && (
142
- // And its the root or we are in a monorepo
143
- root.sourcePath === req.sourcePath ||
144
- !!root.pkg.workspaces
145
- )
146
- );
147
- queue.push(...children.map(x => ({ ...x, parent: dep })));
103
+ seen.add(node.value);
148
104
  }
105
+
106
+ const children = Object.entries(node.children)
107
+ .map(([n, v]) => this.readPackage(this.resolveVersionPath(node.pkg, v) ?? this.resolvePackagePath(n)))
108
+ .map(pkg => ({ ...visitor.create(pkg), parent: node.value }));
109
+
110
+ queue.push(...children);
149
111
  }
150
- return (await visitor.complete?.(out)) ?? out;
112
+
113
+ return await visitor.complete(seen);
151
114
  }
152
115
 
153
116
  /**
154
117
  * Find workspace values from rootPath
155
118
  */
156
- static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> {
157
- if (!this.#workspaces[rootPath]) {
158
- const cache = path.resolve(ctx.workspacePath, ctx.outputFolder, 'workspaces.json');
159
- try {
160
- return await ManifestFileUtil.readAsJson(cache);
161
- } catch (err) {
119
+ static async resolveWorkspaces(ctx: ManifestContext): Promise<PackageWorkspaceEntry[]> {
120
+ const rootPath = ctx.workspace.path;
121
+ const cache = path.resolve(rootPath, ctx.build.outputFolder, 'workspaces.json');
122
+ return this.#workspaces[rootPath] ??= await ManifestFileUtil.readAsJson<PackageWorkspaceEntry[]>(cache)
123
+ .catch(async () => {
162
124
  let out: PackageWorkspaceEntry[];
163
- switch (ctx.packageManager) {
125
+ switch (ctx.workspace.manager) {
126
+ case 'yarn':
164
127
  case 'npm': {
165
128
  const res = await this.#exec<{ location: string, name: string }[]>(rootPath, 'npm query .workspace');
166
- out = res.map(d => ({ sourcePath: d.location, name: d.name }));
167
- break;
168
- }
169
- case 'yarn': {
170
- const res = await this.#exec<Record<string, { location: string }>>(rootPath, 'npm query .workspace');
171
- out = Object.entries(res).map(([name, { location }]) => ({ sourcePath: location, name }));
129
+ out = res.map(d => ({ path: path.resolve(ctx.workspace.path, d.location), name: d.name }));
172
130
  break;
173
131
  }
174
132
  }
175
-
176
- this.#workspaces[rootPath] = out;
177
-
178
133
  await ManifestFileUtil.bufferedFileWrite(cache, out);
179
- }
134
+ return out;
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Get an install command for a given npm module
140
+ */
141
+ static getInstallCommand(ctx: { workspace: { manager: NodePackageManager } }, pkg: string, prod = false): string {
142
+ let install: string;
143
+ switch (ctx.workspace.manager) {
144
+ case 'npm': install = `npm i ${prod ? '' : '--save-dev '}${pkg}`; break;
145
+ case 'yarn': install = `yarn add ${prod ? '' : '--dev '}${pkg}`; break;
180
146
  }
181
- return this.#workspaces[rootPath];
147
+ return install;
182
148
  }
183
149
  }
package/src/path.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type * as pathMod from 'path';
2
- import { extname, dirname, basename, resolve, join } from 'path/posix';
3
- import { sep, resolve as nativeResolve, join as nativeJoin } from 'path';
1
+ import type * as pathMod from 'node:path';
2
+ import { extname, dirname, basename, resolve, join } from 'node:path/posix';
3
+ import { sep, resolve as nativeResolve, join as nativeJoin } from 'node:path';
4
4
 
5
5
  /**
6
6
  * Converts a given file name by replace all slashes, with forward slashes
@@ -1,6 +1,9 @@
1
1
  import { path } from './path';
2
- import { IndexedModule, ManifestIndex } from './manifest-index';
3
- import { FunctionMetadata, ManifestContext } from './types';
2
+ import { ManifestIndex } from './manifest-index';
3
+
4
+ import type { FunctionMetadata } from './types/common';
5
+ import type { IndexedModule, ManifestModule } from './types/manifest';
6
+ import type { ManifestContext } from './types/context';
4
7
 
5
8
  const METADATA = Symbol.for('@travetto/manifest:metadata');
6
9
  type Metadated = { [METADATA]: FunctionMetadata };
@@ -8,7 +11,7 @@ type Metadated = { [METADATA]: FunctionMetadata };
8
11
  /**
9
12
  * Extended manifest index geared for application execution
10
13
  */
11
- class $RootIndex extends ManifestIndex {
14
+ class $RuntimeIndex extends ManifestIndex {
12
15
 
13
16
  #metadata = new Map<string, FunctionMetadata>();
14
17
 
@@ -20,13 +23,6 @@ class $RootIndex extends ManifestIndex {
20
23
  this.init(`${this.outputRoot}/node_modules/${module}`);
21
24
  }
22
25
 
23
- /**
24
- * Determines if the manifest root is the root for a monorepo
25
- */
26
- isMonoRepoRoot(): boolean {
27
- return !!this.manifest.monoRepo && !this.manifest.mainFolder;
28
- }
29
-
30
26
  /**
31
27
  * Get internal id from file name and optionally, class name
32
28
  */
@@ -36,29 +32,11 @@ class $RootIndex extends ManifestIndex {
36
32
  return clsName ? `${id}○${clsName}` : id;
37
33
  }
38
34
 
39
- /**
40
- * Get main module name
41
- */
42
- get mainModuleName(): string {
43
- return this.manifest.mainModule;
44
- }
45
-
46
35
  /**
47
36
  * Get main module for manifest
48
37
  */
49
38
  get mainModule(): IndexedModule {
50
- return this.getModule(this.mainModuleName)!;
51
- }
52
-
53
- /**
54
- * Digest manifest
55
- */
56
- manifestDigest(): Pick<ManifestContext, 'mainModule' | 'frameworkVersion' | 'version'> {
57
- return {
58
- mainModule: this.manifest.mainModule,
59
- frameworkVersion: this.manifest.frameworkVersion,
60
- version: this.manifest.version,
61
- };
39
+ return this.getModule(this.manifest.main.name)!;
62
40
  }
63
41
 
64
42
  /**
@@ -107,23 +85,59 @@ class $RootIndex extends ManifestIndex {
107
85
  * Resolve module path to folder, with support for main module and monorepo support
108
86
  */
109
87
  resolveModulePath(modulePath: string): string {
110
- const main = this.manifest.mainModule;
111
- const workspace = this.manifest.workspacePath;
88
+ const main = this.manifest.main.name;
89
+ const workspace = this.manifest.workspace.path;
112
90
  const [base, sub] = modulePath
113
91
  .replace(/^(@@?)(#|$)/g, (_, v, r) => `${v === '@' ? main : workspace}${r}`)
114
92
  .split('#');
115
93
  return path.resolve(this.hasModule(base) ? this.getModule(base)!.sourcePath : base, sub ?? '.');
116
94
  }
117
- }
118
95
 
119
- let index: $RootIndex | undefined;
96
+ /**
97
+ * Get manifest module by name
98
+ */
99
+ getManifestModule(mod: string): ManifestModule {
100
+ return this.manifest.modules[mod];
101
+ }
120
102
 
121
- try {
122
- index = new $RootIndex(process.env.TRV_MANIFEST!);
123
- } catch (err) {
124
- if (/prod/i.test(process.env.NODE_ENV ?? '')) {
125
- throw err;
103
+ /**
104
+ * Get manifest modules
105
+ */
106
+ getManifestModules(): ManifestModule[] {
107
+ return Object.values(this.manifest.modules);
126
108
  }
127
109
  }
128
110
 
129
- export const RootIndex: $RootIndex = index!;
111
+ export const RuntimeIndex = new $RuntimeIndex(process.env.TRV_MANIFEST!);
112
+
113
+ const build = <T extends object>(inp: T, props: (keyof ManifestContext)[]): T & ManifestContext => {
114
+ for (const prop of props) {
115
+ Object.defineProperty(inp, prop, { configurable: false, get: () => RuntimeIndex.manifest[prop] });
116
+ }
117
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
118
+ return inp as T & ManifestContext;
119
+ };
120
+
121
+ export const RuntimeContext = build({
122
+ /**
123
+ * Produce a workspace relative path
124
+ * @param rel The relative path
125
+ */
126
+ workspaceRelative(...rel: string[]): string {
127
+ return path.resolve(RuntimeIndex.manifest.workspace.path, ...rel);
128
+ },
129
+ /**
130
+ * Produce a workspace path for tooling, with '@' being replaced by node_module/name folder
131
+ * @param rel The relative path
132
+ */
133
+ toolPath(...rel: string[]): string {
134
+ rel = rel.flatMap(x => x === '@' ? ['node_modules', RuntimeIndex.manifest.main.name] : [x]);
135
+ return path.resolve(RuntimeIndex.manifest.workspace.path, RuntimeIndex.manifest.build.toolFolder, ...rel);
136
+ },
137
+ /**
138
+ * Are we running from a mono-root?
139
+ */
140
+ get monoRoot(): boolean {
141
+ return !!RuntimeIndex.manifest.workspace.mono && !RuntimeIndex.manifest.main.folder;
142
+ }
143
+ }, ['main', 'workspace']);
@@ -0,0 +1,20 @@
1
+ export type NodeModuleType = 'module' | 'commonjs';
2
+ export type NodePackageManager = 'yarn' | 'npm';
3
+
4
+ export type ManifestModuleFileType = 'typings' | 'ts' | 'js' | 'json' | 'package-json' | 'unknown' | 'fixture' | 'md';
5
+ export type ManifestModuleFolderType =
6
+ '$root' | '$index' | '$package' |
7
+ 'src' | 'bin' | 'support' | 'resources' | 'test' | 'doc' |
8
+ 'test/fixtures' | 'support/fixtures' | 'support/resources' |
9
+ '$other' | '$transformer';
10
+
11
+ export type ManifestModuleRole = 'std' | 'test' | 'doc' | 'compile' | 'build';
12
+
13
+ export type FunctionMetadata = {
14
+ id: string;
15
+ source: string;
16
+ hash?: number;
17
+ methods?: Record<string, { hash: number }>;
18
+ synthetic?: boolean;
19
+ abstract?: boolean;
20
+ };
@@ -0,0 +1,40 @@
1
+ import type { NodeModuleType, NodePackageManager } from './common';
2
+
3
+ export type ManifestContext = {
4
+ workspace: {
5
+ /** Workspace path for module */
6
+ path: string;
7
+ /** The module name for the workspace root */
8
+ name: string;
9
+ /** Is the workspace a monorepo? */
10
+ mono?: boolean;
11
+ /** The module type of the workspace */
12
+ type: NodeModuleType;
13
+ /** The package manager of the workspace */
14
+ manager: NodePackageManager;
15
+ /** The default env name */
16
+ defaultEnv: string;
17
+ };
18
+ build: {
19
+ /** Compiler folder, relative to workspace */
20
+ compilerFolder: string;
21
+ /** Compiler module folder */
22
+ compilerModuleFolder: string;
23
+ /** URL for the compiler server */
24
+ compilerUrl: string;
25
+ /** Code output folder, relative to workspace */
26
+ outputFolder: string;
27
+ /** Location of development-time tool output */
28
+ toolFolder: string;
29
+ };
30
+ main: {
31
+ /** Main module for manifest */
32
+ name: string;
33
+ /** Folder, relative to workspace for main module */
34
+ folder: string;
35
+ /** Description of the main module */
36
+ description?: string;
37
+ /** Version of the main module */
38
+ version: string;
39
+ };
40
+ };
@@ -0,0 +1,79 @@
1
+ import type { ManifestModuleFileType, ManifestModuleFolderType, ManifestModuleRole } from './common';
2
+ import type { ManifestContext } from './context';
3
+ import { Package } from './package';
4
+
5
+ export type ManifestModuleFile = [string, ManifestModuleFileType, number] | [string, ManifestModuleFileType, number, ManifestModuleRole];
6
+
7
+ export type ManifestDepCore = {
8
+ /** Package name */
9
+ name: string;
10
+ /** Package version */
11
+ version: string;
12
+ /** Is this the main module */
13
+ main?: boolean;
14
+ /** Is this a module that is part of the workspace */
15
+ workspace?: boolean;
16
+ /** Should this module be deployed to prod? */
17
+ prod: boolean;
18
+ /** Is the module intended to be published? */
19
+ internal?: boolean;
20
+ };
21
+
22
+ export type ManifestModuleCore = ManifestDepCore & {
23
+ sourceFolder: string;
24
+ outputFolder: string;
25
+ roles: ManifestModuleRole[];
26
+ parents: string[];
27
+ };
28
+
29
+ export type ManifestModule = ManifestModuleCore & {
30
+ files: Partial<Record<ManifestModuleFolderType, ManifestModuleFile[]>>;
31
+ };
32
+
33
+ export type ManifestRoot = ManifestContext & {
34
+ generated: number;
35
+ modules: Record<string, ManifestModule>;
36
+ };
37
+
38
+ export type FindConfig = {
39
+ folder?: (folder: ManifestModuleFolderType) => boolean;
40
+ module?: (module: IndexedModule) => boolean;
41
+ file?: (file: IndexedFile) => boolean;
42
+ sourceOnly?: boolean;
43
+ };
44
+
45
+ export type IndexedFile = {
46
+ id: string;
47
+ import: string;
48
+ module: string;
49
+ sourceFile: string;
50
+ outputFile: string;
51
+ relativeFile: string;
52
+ role: ManifestModuleRole;
53
+ type: ManifestModuleFileType;
54
+ };
55
+
56
+ export type IndexedModule = ManifestModuleCore & {
57
+ sourcePath: string;
58
+ outputPath: string;
59
+ files: Record<ManifestModuleFolderType, IndexedFile[]>;
60
+ children: Set<string>;
61
+ };
62
+
63
+ /** Module dependency, used in dependency visiting */
64
+ export type PackageModule = Omit<ManifestModule, 'files' | 'parents' | 'roles'> & {
65
+ state: {
66
+ /** Role root? */
67
+ roleRoot?: boolean;
68
+ /** Travetto package info */
69
+ travetto?: Package['travetto'];
70
+ /** Prod dependencies */
71
+ prodDeps: Set<string>;
72
+ /** Set of parent package names */
73
+ parentSet: Set<string>;
74
+ /** Set of child package names */
75
+ childSet: Set<string>;
76
+ /** Defined roles for a given module */
77
+ roleSet: Set<ManifestModuleRole>;
78
+ };
79
+ };
@@ -0,0 +1,75 @@
1
+ import type { ManifestModuleRole, NodeModuleType } from './common';
2
+ import type { ManifestContext } from './context';
3
+
4
+ export const PackagePath = Symbol.for('@travetto/manifest:package-path');
5
+
6
+ export type Package = {
7
+ [PackagePath]: string;
8
+ name: string;
9
+ type?: NodeModuleType;
10
+ version: string;
11
+ description?: string;
12
+ license?: string;
13
+ repository?: {
14
+ url: string;
15
+ directory?: string;
16
+ };
17
+ author?: {
18
+ email?: string;
19
+ name?: string;
20
+ };
21
+ main: string;
22
+ homepage?: string;
23
+ files?: string[];
24
+ bin?: Record<string, string>;
25
+ scripts?: Record<string, string>;
26
+ engines?: Record<string, string>;
27
+ keywords?: string[];
28
+
29
+ dependencies?: Record<string, string>;
30
+ devDependencies?: Record<string, string>;
31
+ peerDependencies?: Record<string, string>;
32
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
33
+ optionalDependencies?: Record<string, string>;
34
+ travetto?: {
35
+ displayName?: string;
36
+ roles?: ManifestModuleRole[];
37
+ doc?: {
38
+ output?: string[];
39
+ root?: string;
40
+ baseUrl?: string;
41
+ outputs?: string[];
42
+ };
43
+ defaultEnv?: string;
44
+ build?: Partial<ManifestContext['build']> & {
45
+ isolated?: boolean;
46
+ withModules?: Record<string, 'main' | true>;
47
+ };
48
+ };
49
+ workspaces?: string[];
50
+ private?: boolean;
51
+ publishConfig?: { access?: 'restricted' | 'public' };
52
+ };
53
+
54
+ export type PackageDepType = 'dependencies' | 'devDependencies' | 'optionalDependencies' | 'peerDependencies';
55
+
56
+ export type PackageVisitReq<T> = {
57
+ /** Request package */
58
+ pkg: Package;
59
+ /** Children to visit */
60
+ children: Record<string, string>;
61
+ /** Value */
62
+ value: T;
63
+ /** Parent */
64
+ parent?: T;
65
+ };
66
+
67
+ export type PackageVisitor<T> = {
68
+ create(pkg: Package): PackageVisitReq<T>;
69
+ init(): Promise<Iterable<PackageVisitReq<T>>>;
70
+ valid(req: PackageVisitReq<T>): boolean;
71
+ visit(req: PackageVisitReq<T>): void;
72
+ complete(values: Iterable<T>): Promise<Iterable<T>>;
73
+ };
74
+
75
+ export type PackageWorkspaceEntry = { name: string, path: string };