@travetto/manifest 4.0.0-rc.0 → 4.0.0-rc.2

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/README.md CHANGED
@@ -30,12 +30,12 @@ During the compilation process, the compiler needs to know every file that is el
30
30
  Additionally, once the code has been compiled (or even bundled after that), the executing process needs to know what files are available for loading, and any patterns necessary for knowing which files to load versus which ones to ignore. This allows for dynamic loading of modules/files without knowledge/access to the file system, and in a more performant manner.
31
31
 
32
32
  ## Manifest Delta
33
- During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L20) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled.
33
+ During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L21) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled.
34
34
 
35
35
  ## Class and Function Metadata
36
36
  For the framework to work properly, metadata needs to be collected about files, classes and functions to uniquely identify them, with support for detecting changes during live reloads. To achieve this, every `class` is decorated with an additional field of `Ⲑid`. `Ⲑid` represents a computed id that is tied to the file/class combination.
37
37
 
38
- `Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RuntimeIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/runtime.ts#L11) using the `getFunctionMetadata` method.
38
+ `Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RuntimeIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/runtime.ts#L14) using the `getFunctionMetadata` method.
39
39
 
40
40
  **Code: Test Class**
41
41
  ```typescript
@@ -97,13 +97,15 @@ By default, all paths within the framework are assumed to be in a POSIX style, a
97
97
  "path": "<generated>",
98
98
  "mono": true,
99
99
  "manager": "npm",
100
- "type": "commonjs"
100
+ "type": "commonjs",
101
+ "defaultEnv": "local"
101
102
  },
102
103
  "build": {
103
104
  "compilerFolder": ".trv/compiler",
104
105
  "compilerUrl": "http://127.0.0.1:26803",
105
106
  "compilerModuleFolder": "module/compiler",
106
- "outputFolder": ".trv/output"
107
+ "outputFolder": ".trv/output",
108
+ "toolFolder": ".trv/tool"
107
109
  },
108
110
  "main": {
109
111
  "name": "@travetto/manifest",
@@ -114,15 +116,15 @@ By default, all paths within the framework are assumed to be in a POSIX style, a
114
116
  "modules": {
115
117
  "@travetto/manifest": {
116
118
  "main": true,
119
+ "prod": true,
117
120
  "name": "@travetto/manifest",
118
121
  "version": "x.x.x",
119
- "local": true,
122
+ "workspace": true,
120
123
  "internal": false,
121
124
  "sourceFolder": "module/manifest",
122
125
  "outputFolder": "node_modules/@travetto/manifest",
123
126
  "roles": [ "std" ],
124
127
  "parents": [],
125
- "prod": true,
126
128
  "files": {
127
129
  "$root": [
128
130
  [ "DOC.html", "unknown", 1868155200000 ],
@@ -160,8 +162,11 @@ By default, all paths within the framework are assumed to be in a POSIX style, a
160
162
  [ "src/package.ts", "ts", 1868155200000 ],
161
163
  [ "src/path.ts", "ts", 1868155200000 ],
162
164
  [ "src/runtime.ts", "ts", 1868155200000 ],
163
- [ "src/types.ts", "ts", 1868155200000 ],
164
- [ "src/util.ts", "ts", 1868155200000 ]
165
+ [ "src/util.ts", "ts", 1868155200000 ],
166
+ [ "src/types/common.ts", "ts", 1868155200000 ],
167
+ [ "src/types/context.ts", "ts", 1868155200000 ],
168
+ [ "src/types/manifest.ts", "ts", 1868155200000 ],
169
+ [ "src/types/package.ts", "ts", 1868155200000 ]
165
170
  ],
166
171
  "bin": [
167
172
  [ "bin/context.d.ts", "typings", 1868155200000 ],
package/__index__.ts CHANGED
@@ -8,4 +8,7 @@ export * from './src/runtime';
8
8
  export * from './src/package';
9
9
  export * from './src/util';
10
10
  export * from './src/file';
11
- export * from './src/types';
11
+ export * from './src/types/context';
12
+ export * from './src/types/package';
13
+ export * from './src/types/manifest';
14
+ export * from './src/types/common';
package/bin/context.d.ts CHANGED
@@ -4,7 +4,7 @@ declare namespace ManifestBootstrap {
4
4
  /**
5
5
  * Get Context for building
6
6
  */
7
- function getManifestContext(folder?: string): Promise<ManifestContext>;
7
+ function getManifestContext(folder?: string): ManifestContext;
8
8
  }
9
9
 
10
10
  export = ManifestBootstrap;
package/bin/context.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @typedef {import('../src/types').Package & { path:string }} Pkg
4
+ * @typedef {import('../src/types/package').Package & { path:string }} Pkg
5
5
  * @typedef {Pkg & { mono: boolean, manager: 'yarn'|'npm', resolve: (file:string) => string}} Workspace
6
- * @typedef {import('../src/types').ManifestContext} ManifestContext
6
+ * @typedef {import('../src/types/context').ManifestContext} ManifestContext
7
7
  */
8
- import fs from 'node:fs/promises';
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { createRequire } from 'node:module';
11
11
 
@@ -17,24 +17,26 @@ const OUTPUT_FOLDER = '.trv/output';
17
17
  /**
18
18
  * Read package.json or return undefined if missing
19
19
  * @param {string} dir
20
- * @returns {Promise<Pkg|undefined>}
20
+ * @returns {Pkg|undefined}
21
21
  */
22
- async function $readPackage(dir) {
22
+ function $readPackage(dir) {
23
23
  dir = dir.endsWith('.json') ? path.dirname(dir) : dir;
24
- return await fs.readFile(path.resolve(dir, 'package.json'), 'utf8')
25
- .then(v => ({ ...JSON.parse(v), path: path.resolve(dir) }), () => undefined);
24
+ try {
25
+ const v = readFileSync(path.resolve(dir, 'package.json'), 'utf8');
26
+ return ({ ...JSON.parse(v), path: path.resolve(dir) });
27
+ } catch { }
26
28
  }
27
29
 
28
30
  /**
29
31
  * Find package.json for a given folder
30
32
  * @param {string} dir
31
- * @return {Promise<Pkg>}
33
+ * @return {Pkg}
32
34
  */
33
- async function $findPackage(dir) {
35
+ function $findPackage(dir) {
34
36
  let prev;
35
37
  let pkg, curr = path.resolve(dir);
36
38
  while (!pkg && curr !== prev) {
37
- pkg = await $readPackage(curr);
39
+ pkg = $readPackage(curr);
38
40
  [prev, curr] = [curr, path.dirname(curr)];
39
41
  }
40
42
  if (!pkg) {
@@ -46,9 +48,9 @@ async function $findPackage(dir) {
46
48
 
47
49
  /**
48
50
  * Get workspace root
49
- * @return {Promise<Workspace>}
51
+ * @return {Workspace}
50
52
  */
51
- async function $resolveWorkspace(base = process.cwd()) {
53
+ function $resolveWorkspace(base = process.cwd()) {
52
54
  if (base in WS_ROOT) { return WS_ROOT[base]; }
53
55
  let folder = base;
54
56
  let prev;
@@ -57,10 +59,10 @@ async function $resolveWorkspace(base = process.cwd()) {
57
59
 
58
60
  while (prev !== folder) {
59
61
  [prev, prevPkg] = [folder, pkg];
60
- pkg = await $readPackage(folder) ?? pkg;
62
+ pkg = $readPackage(folder) ?? pkg;
61
63
  if (
62
64
  (pkg && (!!pkg.workspaces || !!pkg.travetto?.build?.isolated)) || // if we have a monorepo root, or we are isolated
63
- await fs.stat(path.resolve(folder, '.git')).catch(() => { }) // we made it to the source repo root
65
+ existsSync(path.resolve(folder, '.git')) // we made it to the source repo root
64
66
  ) {
65
67
  break;
66
68
  }
@@ -75,7 +77,7 @@ async function $resolveWorkspace(base = process.cwd()) {
75
77
  ...pkg,
76
78
  name: pkg.name ?? 'untitled',
77
79
  type: pkg.type,
78
- manager: await fs.stat(path.resolve(pkg.path, 'yarn.lock')).catch(() => { }) ? 'yarn' : 'npm',
80
+ manager: existsSync(path.resolve(pkg.path, 'yarn.lock')) ? 'yarn' : 'npm',
79
81
  resolve: createRequire(`${pkg.path}/node_modules`).resolve.bind(null),
80
82
  mono: !!pkg.workspaces || (!pkg.travetto?.build?.isolated && !!prevPkg) // Workspaces or nested projects
81
83
  };
@@ -84,15 +86,16 @@ async function $resolveWorkspace(base = process.cwd()) {
84
86
  /**
85
87
  * Get Compiler url
86
88
  * @param {Workspace} ws
89
+ * @param {string} toolFolder
87
90
  */
88
- async function $getCompilerUrl(ws) {
89
- const file = path.resolve(ws.path, TOOL_FOLDER, 'build.compilerUrl');
91
+ function $getCompilerUrl(ws, toolFolder) {
92
+ const file = path.resolve(ws.path, toolFolder, 'build.compilerUrl');
90
93
  // eslint-disable-next-line no-bitwise
91
94
  const port = (Math.abs([...file].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000;
92
95
  const out = `http://localhost:${port}`;
93
- try { await fs.stat(file); } catch {
94
- await fs.mkdir(path.dirname(file), { recursive: true });
95
- await fs.writeFile(file, out, 'utf8');
96
+ if (!existsSync(file)) {
97
+ mkdirSync(path.dirname(file), { recursive: true });
98
+ writeFileSync(file, out, 'utf8');
96
99
  }
97
100
  return out;
98
101
  }
@@ -102,13 +105,16 @@ async function $getCompilerUrl(ws) {
102
105
  * @param {Workspace} workspace
103
106
  * @param {string|undefined} folder
104
107
  */
105
- async function $resolveModule(workspace, folder) {
108
+ function $resolveModule(workspace, folder) {
106
109
  let mod;
107
110
  if (!folder && process.env.TRV_MODULE) {
108
111
  mod = process.env.TRV_MODULE;
109
112
  if (/[.](t|j)sx?$/.test(mod)) { // Rewrite from file to module
110
- process.env.TRV_MODULE = mod = await $findPackage(path.dirname(mod))
111
- .then(v => v.name, () => '');
113
+ try {
114
+ process.env.TRV_MODULE = mod = $findPackage(path.dirname(mod)).name;
115
+ } catch {
116
+ process.env.TRV_MODULE = mod = '';
117
+ }
112
118
  }
113
119
  }
114
120
 
@@ -116,7 +122,7 @@ async function $resolveModule(workspace, folder) {
116
122
  try {
117
123
  folder = path.dirname(workspace.resolve(`${mod}/package.json`));
118
124
  } catch {
119
- const workspacePkg = await $readPackage(workspace.path);
125
+ const workspacePkg = $readPackage(workspace.path);
120
126
  if (workspacePkg?.name === mod) {
121
127
  folder = workspace.path;
122
128
  } else {
@@ -131,12 +137,13 @@ async function $resolveModule(workspace, folder) {
131
137
  /**
132
138
  * Gets build context
133
139
  * @param {string} [folder]
134
- * @return {Promise<ManifestContext>}
140
+ * @return {ManifestContext}
135
141
  */
136
- export async function getManifestContext(folder) {
137
- const workspace = await $resolveWorkspace(folder);
138
- const mod = await $resolveModule(workspace, folder);
142
+ export function getManifestContext(folder) {
143
+ const workspace = $resolveWorkspace(folder);
144
+ const mod = $resolveModule(workspace, folder);
139
145
  const build = workspace.travetto?.build ?? {};
146
+ const toolFolder = build.toolFolder ?? TOOL_FOLDER;
140
147
 
141
148
  return {
142
149
  workspace: {
@@ -144,13 +151,15 @@ export async function getManifestContext(folder) {
144
151
  path: workspace.path,
145
152
  mono: workspace.mono,
146
153
  manager: workspace.manager,
147
- type: workspace.type ?? 'commonjs'
154
+ type: workspace.type ?? 'commonjs',
155
+ defaultEnv: workspace.travetto?.defaultEnv ?? 'local'
148
156
  },
149
157
  build: {
150
158
  compilerFolder: build.compilerFolder ?? COMPILER_FOLDER,
151
- compilerUrl: build.compilerUrl ?? await $getCompilerUrl(workspace),
159
+ compilerUrl: build.compilerUrl ?? $getCompilerUrl(workspace, toolFolder),
152
160
  compilerModuleFolder: path.dirname(workspace.resolve('@travetto/compiler/package.json')).replace(`${workspace.path}/`, ''),
153
161
  outputFolder: build.outputFolder ?? OUTPUT_FOLDER,
162
+ toolFolder
154
163
  },
155
164
  main: {
156
165
  name: mod.name ?? 'untitled',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/manifest",
3
- "version": "4.0.0-rc.0",
3
+ "version": "4.0.0-rc.2",
4
4
  "description": "Support for project indexing, manifesting, along with file watching",
5
5
  "keywords": [
6
6
  "path",
package/src/delta.ts CHANGED
@@ -1,18 +1,19 @@
1
- import {
2
- ManifestContext, ManifestModule, ManifestModuleCore, ManifestModuleFile,
3
- ManifestModuleFileType, ManifestModuleFolderType, ManifestRoot
4
- } from './types';
1
+ import fs from 'node:fs/promises';
5
2
 
6
3
  import { ManifestModuleUtil } from './module';
7
- import { ManifestFileUtil } from './file';
8
4
  import { path } from './path';
9
5
 
6
+ import type { ManifestModule, ManifestModuleCore, ManifestModuleFile, ManifestRoot } from './types/manifest';
7
+ import type { ManifestModuleFileType, ManifestModuleFolderType } from './types/common';
8
+ import type { ManifestContext } from './types/context';
9
+
10
10
  type DeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty';
11
11
  type DeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> };
12
12
  export type DeltaEvent = { file: string, type: DeltaEventType, module: string, sourceFile: string };
13
13
 
14
14
  const VALID_SOURCE_FOLDERS = new Set<ManifestModuleFolderType>(['bin', 'src', 'test', 'support', '$index', '$package', 'doc']);
15
- const VALID_SOURCE_TYPE = new Set<ManifestModuleFileType>(['js', 'ts', 'package-json']);
15
+ const VALID_OUTPUT_TYPE = new Set<ManifestModuleFileType>(['js', 'ts', 'package-json']);
16
+ const VALID_SOURCE_TYPE = new Set<ManifestModuleFileType>([...VALID_OUTPUT_TYPE, 'typings']);
16
17
 
17
18
  /**
18
19
  * Produce delta for the manifest
@@ -37,7 +38,7 @@ export class ManifestDeltaUtil {
37
38
  (await ManifestModuleUtil.scanFolder(root, left.main))
38
39
  .filter(x => {
39
40
  const type = ManifestModuleUtil.getFileType(x);
40
- return type === 'ts' || type === 'typings' || type === 'js' || type === 'package-json';
41
+ return VALID_SOURCE_TYPE.has(type);
41
42
  })
42
43
  .map(x => ManifestModuleUtil.sourceToBlankExt(x.replace(`${root}/`, '')))
43
44
  );
@@ -45,7 +46,7 @@ export class ManifestDeltaUtil {
45
46
  for (const el of Object.keys(left.files)) {
46
47
  const output = ManifestModuleUtil.sourceToOutputExt(`${outputFolder}/${left.outputFolder}/${el}`);
47
48
  const [, , leftTs] = left.files[el];
48
- const stat = await ManifestFileUtil.statFile(output);
49
+ const stat = await fs.stat(output).catch(() => undefined);
49
50
  right.delete(ManifestModuleUtil.sourceToBlankExt(el));
50
51
 
51
52
  if (!stat) {
@@ -77,7 +78,7 @@ export class ManifestDeltaUtil {
77
78
  continue;
78
79
  }
79
80
  for (const [name, type, date] of m.files?.[key] ?? []) {
80
- if (VALID_SOURCE_TYPE.has(type)) {
81
+ if (VALID_OUTPUT_TYPE.has(type)) {
81
82
  out[name] = [name, type, date];
82
83
  }
83
84
  }
@@ -1,137 +1,126 @@
1
1
  import { PackageUtil } from './package';
2
2
  import { path } from './path';
3
- import { ManifestContext, ManifestModuleRole, PackageVisitor, PackageVisitReq, Package, ManifestDepCore } from './types';
4
3
 
5
- export type ModuleDep = ManifestDepCore & {
4
+ import type { Package, PackageDepType } from './types/package';
5
+ import type { ManifestContext } from './types/context';
6
+ import type { PackageModule } from './types/manifest';
7
+
8
+ type CreateOpts = Partial<Pick<PackageModule, 'main' | 'workspace' | 'prod'>> & { roleRoot?: boolean, parent?: PackageModule };
9
+
10
+ type Req = {
11
+ /** Request package */
6
12
  pkg: Package;
7
- mainLike?: boolean;
8
- sourcePath: string;
9
- childSet: Set<string>;
10
- parentSet: Set<string>;
11
- roleSet: Set<ManifestModuleRole>;
12
- topLevel?: boolean;
13
+ /** Children to visit */
14
+ children: Record<string, string>;
15
+ /** Value */
16
+ value: PackageModule;
17
+ /** Parent */
18
+ parent?: PackageModule;
13
19
  };
14
20
 
15
21
  /**
16
22
  * Used for walking dependencies for collecting modules for the manifest
17
23
  */
18
- export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
24
+ export class PackageModuleVisitor {
19
25
 
20
26
  constructor(public ctx: ManifestContext) {
21
27
  this.#mainSourcePath = path.resolve(this.ctx.workspace.path, this.ctx.main.folder);
22
28
  }
23
29
 
24
- #mainLikeModules = new Set<string>();
25
30
  #mainSourcePath: string;
26
-
27
- /**
28
- * Main source path for searching
29
- */
30
- get rootPath(): string {
31
- return this.#mainSourcePath;
32
- }
31
+ #cache: Record<string, PackageModule> = {};
32
+ #workspaceModules: Map<string, string>;
33
33
 
34
34
  /**
35
35
  * Initialize visitor, and provide global dependencies
36
36
  */
37
- async init(req: PackageVisitReq<ModuleDep>): Promise<PackageVisitReq<ModuleDep>[]> {
38
- const pkg = PackageUtil.readPackage(req.sourcePath);
39
- const workspacePkg = PackageUtil.readPackage(this.ctx.workspace.path);
40
- const workspaceModules = pkg.workspaces?.length ? (await PackageUtil.resolveWorkspaces(this.ctx, req.sourcePath)) : [];
41
-
42
- this.#mainLikeModules = new Set([
43
- pkg.name,
44
- ...Object.entries(pkg.travetto?.build?.withModules ?? []).filter(x => x[1] === 'main').map(x => x[0]),
45
- // Add workspace folders, for tests and docs
46
- ...workspaceModules.map(x => x.name)
47
- ]);
48
-
49
- const globals = Object.keys(workspacePkg.travetto?.build?.withModules ?? [])
50
- .map(name => PackageUtil.packageReq<ModuleDep>(PackageUtil.resolvePackagePath(name), name in (workspacePkg.dependencies ?? {}), true));
51
-
52
- const workspaceModuleDeps = workspaceModules
53
- .map(entry => PackageUtil.packageReq<ModuleDep>(path.resolve(req.sourcePath, entry.sourcePath), false, true));
37
+ async init(): Promise<Iterable<Req>> {
38
+ const mainReq = this.#create(this.#mainSourcePath, { main: true, workspace: true, roleRoot: true, prod: true });
39
+ const globals = [mainReq];
40
+ this.#workspaceModules = new Map(
41
+ (await PackageUtil.resolveWorkspaces(this.ctx)).map(x => [x.name, x.path])
42
+ );
54
43
 
55
- return [...globals, ...workspaceModuleDeps];
56
- }
44
+ // Treat all workspace modules as main modules
45
+ if (this.ctx.workspace.mono && !this.ctx.main.folder) {
46
+ for (const [, loc] of this.#workspaceModules) {
47
+ globals.push(this.#create(loc, { main: true, workspace: true, roleRoot: true, parent: mainReq.value }));
48
+ }
49
+ } else {
50
+ // If we have 'withModules' at workspace root
51
+ const root = PackageUtil.readPackage(this.ctx.workspace.path);
52
+ for (const [name, type] of Object.entries(root.travetto?.build?.withModules ?? {})) {
53
+ globals.push(this.#create(PackageUtil.resolvePackagePath(name),
54
+ { main: type === 'main', workspace: true, parent: mainReq.value }
55
+ ));
56
+ }
57
+ }
57
58
 
58
- /**
59
- * Is valid dependency for searching
60
- */
61
- valid(req: PackageVisitReq<ModuleDep>): boolean {
62
- return req.sourcePath === this.#mainSourcePath || (
63
- (!!req.pkg.travetto || req.pkg.private === true || !req.sourcePath.includes('node_modules'))
64
- );
59
+ return globals;
65
60
  }
66
61
 
67
62
  /**
68
- * Create dependency from request
63
+ * Build a package module
69
64
  */
70
- create(req: PackageVisitReq<ModuleDep>): ModuleDep {
71
- const { pkg, sourcePath } = req;
72
- const { name, version } = pkg;
73
- const main = name === this.ctx.main.name;
74
- const mainLike = main || this.#mainLikeModules.has(name);
75
- const internal = pkg.private === true;
76
- const local = internal || mainLike || !sourcePath.includes('node_modules');
77
-
78
- const dep: ModuleDep = {
79
- name, version, sourcePath, main, mainLike, local, internal, pkg: req.pkg,
80
- parentSet: new Set([]), childSet: new Set([]), roleSet: new Set([]), prod: req.prod, topLevel: req.topLevel
65
+ #create(sourcePath: string, { main, workspace, prod = false, roleRoot = false, parent }: CreateOpts = {}): Req {
66
+ const pkg = PackageUtil.readPackage(sourcePath);
67
+ const value = this.#cache[sourcePath] ??= {
68
+ main,
69
+ prod,
70
+ name: pkg.name,
71
+ version: pkg.version,
72
+ workspace: workspace ?? this.#workspaceModules.has(pkg.name),
73
+ internal: pkg.private === true,
74
+ sourceFolder: sourcePath === this.ctx.workspace.path ? '' : sourcePath.replace(`${this.ctx.workspace.path}/`, ''),
75
+ outputFolder: `node_modules/${pkg.name}`,
76
+ state: {
77
+ childSet: new Set(), parentSet: new Set(), roleSet: new Set(), roleRoot,
78
+ travetto: pkg.travetto, prodDeps: new Set(Object.keys(pkg.dependencies ?? {}))
79
+ }
81
80
  };
82
81
 
83
- return dep;
84
- }
85
-
86
- /**
87
- * Visit dependency
88
- */
89
- visit(req: PackageVisitReq<ModuleDep>, dep: ModuleDep): void {
90
- const { parent } = req;
91
- if (parent && dep.name !== this.ctx.main.name) {
92
- dep.parentSet.add(parent.name);
93
- parent.childSet.add(dep.name);
94
- }
82
+ const deps: PackageDepType[] = ['dependencies', ...(value.main ? ['devDependencies'] as const : [])];
83
+ const children = Object.fromEntries(deps.flatMap(x => Object.entries(pkg[x] ?? {})));
84
+ return { pkg, value, children, parent };
95
85
  }
96
86
 
97
87
  /**
98
88
  * Propagate prod, role information through graph
99
89
  */
100
- complete(deps: Set<ModuleDep>): Set<ModuleDep> {
101
- const mapping = new Map<string, { parent: Set<string>, child: Set<string>, el: ModuleDep }>();
102
- for (const el of deps) {
103
- mapping.set(el.name, { parent: new Set(el.parentSet), child: new Set(el.childSet), el });
104
- }
105
-
106
- const main = mapping.get(this.ctx.main.name)!;
107
-
108
- // Visit all direct dependencies and mark
109
- for (const { el } of mapping.values()) {
110
- if (!main.child.has(el.name)) { // Not a direct descendent
111
- el.prod = false;
112
- }
113
- if (main.child.has(el.name) || (el.topLevel && el !== main.el)) { // Direct descendant
114
- el.roleSet = new Set(el.pkg.travetto?.roles ?? []);
115
- if (!el.roleSet.size) {
116
- el.roleSet.add('std');
117
- }
90
+ async #complete(mods: Iterable<PackageModule>): Promise<PackageModule[]> {
91
+ const mapping = new Map([...mods].map(el => [el.name, { parent: new Set(el.state.parentSet), el }]));
92
+
93
+ // All first-level dependencies should have role filled in (for propagation)
94
+ for (const dep of [...mods].filter(x => x.state.roleRoot)) {
95
+ dep.state.roleSet.clear(); // Ensure the roleRoot is empty
96
+ for (const c of dep.state.childSet) { // Visit children
97
+ const cDep = mapping.get(c)!.el;
98
+ if (cDep.state.roleRoot) { continue; }
99
+ // Set roles for all top level modules
100
+ cDep.state.roleSet = new Set(cDep.state.travetto?.roles ?? ['std']);
118
101
  }
119
102
  }
120
103
 
104
+ // Visit all nodes
121
105
  while (mapping.size > 0) {
122
106
  const toProcess = [...mapping.values()].filter(x => x.parent.size === 0);
123
107
  if (!toProcess.length) {
124
108
  throw new Error(`We have reached a cycle for ${[...mapping.keys()]}`);
125
109
  }
126
- // Propagate
127
- for (const { el, child } of toProcess) {
128
- for (const c of child.keys()) {
129
- const { el: cDep, parent } = mapping.get(c)!;
130
- parent.delete(el.name); // Remove from child
131
- for (const role of el.roleSet) {
132
- cDep.roleSet.add(role);
110
+ // Propagate to children
111
+ for (const { el } of toProcess) {
112
+ for (const c of el.state.childSet) {
113
+ const child = mapping.get(c);
114
+ if (!child) { continue; }
115
+ child.parent.delete(el.name);
116
+ // Propagate roles from parent to child
117
+ if (!child.el.state.roleRoot) {
118
+ for (const role of el.state.roleSet) {
119
+ child.el.state.roleSet.add(role);
120
+ }
133
121
  }
134
- cDep.prod ||= el.prod; // Allow prod to trickle down as needed
122
+ // Allow prod to trickle down as needed
123
+ child.el.prod ||= (el.prod && el.state.prodDeps.has(c));
135
124
  }
136
125
  }
137
126
  // Remove from mapping
@@ -140,10 +129,47 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
140
129
  }
141
130
  }
142
131
 
143
- // Color parent as final step
144
- main.el.prod = true;
145
- main.el.roleSet.add('std');
132
+ // Mark as standard at the end
133
+ for (const dep of [...mods].filter(x => x.state.roleRoot)) {
134
+ dep.state.roleSet = new Set(['std']);
135
+ }
136
+
137
+ return [...mods].sort((a, b) => a.name.localeCompare(b.name));
138
+ }
139
+
140
+
141
+ /**
142
+ * Visit packages with ability to track duplicates
143
+ */
144
+ async visit(): Promise<Iterable<PackageModule>> {
145
+ const seen = new Set<PackageModule>();
146
+ const queue = [...await this.init()];
147
+
148
+ while (queue.length) {
149
+ const { value: node, parent, children, pkg } = queue.shift()!; // Visit initial set first
150
+ if (!node || (!node.workspace && !node.state.travetto)) {
151
+ continue;
152
+ }
153
+
154
+ // Track parentage
155
+ if (node.name !== this.ctx.main.name && parent) {
156
+ node.state.parentSet.add(parent.name);
157
+ parent.state.childSet.add(node.name);
158
+ }
159
+
160
+ if (seen.has(node)) {
161
+ continue;
162
+ } else {
163
+ seen.add(node);
164
+ }
165
+
166
+ const next = Object.entries(children)
167
+ .map(([n, v]) => PackageUtil.resolveVersionPath(pkg, v) ?? PackageUtil.resolvePackagePath(n))
168
+ .map(loc => this.#create(loc, { parent: node }));
169
+
170
+ queue.push(...next);
171
+ }
146
172
 
147
- return deps;
173
+ return await this.#complete(seen);
148
174
  }
149
175
  }
package/src/file.ts CHANGED
@@ -3,7 +3,6 @@ import fs from 'node:fs/promises';
3
3
  import { readFileSync } from 'node:fs';
4
4
 
5
5
  import { path } from './path';
6
- import type { ManifestContext } from './types';
7
6
 
8
7
  export class ManifestFileUtil {
9
8
  /**
@@ -16,7 +15,7 @@ export class ManifestFileUtil {
16
15
  const temp = path.resolve(os.tmpdir(), tempName);
17
16
  await fs.writeFile(temp, typeof content === 'string' ? content : JSON.stringify(content), 'utf8');
18
17
  await fs.copyFile(temp, file);
19
- fs.unlink(temp);
18
+ fs.unlink(temp); // Don't wait for completion
20
19
  return file;
21
20
  }
22
21
 
@@ -33,23 +32,4 @@ export class ManifestFileUtil {
33
32
  static readAsJsonSync<T = unknown>(file: string): T {
34
33
  return JSON.parse(readFileSync(file, 'utf8'));
35
34
  }
36
-
37
- /**
38
- * Stat file
39
- */
40
- static statFile(file: string): Promise<{ mtimeMs: number, ctimeMs: number } | undefined> {
41
- return fs.stat(file).catch(() => undefined);
42
- }
43
-
44
- /**
45
- * Resolve tool path for usage
46
- */
47
- static toolPath(ctx: ManifestContext | { manifest: ManifestContext }, rel: string, moduleSpecific = false): string {
48
- ctx = 'manifest' in ctx ? ctx.manifest : ctx;
49
- const parts = [rel];
50
- if (moduleSpecific) {
51
- parts.unshift('node_modules', ctx.main.name);
52
- }
53
- return path.resolve(ctx.workspace.path, '.trv/tool', ...parts);
54
- }
55
35
  }