@travetto/manifest 3.3.2 → 3.4.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/README.md CHANGED
@@ -19,7 +19,6 @@ This module aims to be the boundary between the file system and the code. The m
19
19
  * Class and Function Metadata
20
20
  * Runtime Indexing
21
21
  * Path Normalization
22
- * File Watching
23
22
 
24
23
  ## Project Manifesting
25
24
  The project manifest fulfills two main goals: Compile-time Support, and Runtime Knowledge of the project.
@@ -31,7 +30,7 @@ During the compilation process, the compiler needs to know every file that is el
31
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.
32
31
 
33
32
  ## Manifest Delta
34
- 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.
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.
35
34
 
36
35
  ## Class and Function Metadata
37
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.
@@ -87,75 +86,25 @@ Once the manifest is created, the application runtime can now read this manifest
87
86
  ## Path Normalization
88
87
  By default, all paths within the framework are assumed to be in a POSIX style, and all input paths are converted to the POSIX style. This works appropriately within a Unix and a Windows environment. This module offers up [path](https://github.com/travetto/travetto/tree/main/module/manifest/src/path.ts#L21) as an equivalent to [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library. This allows for consistent behavior across all file-interactions, and also allows for easy analysis if [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library is ever imported.
89
88
 
90
- ## File Watching
91
- The module also leverages [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher), to expose a single function of `watchFolders`. Only the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework") module packages [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher) as a direct dependency. This means, that in production, by default all watch operations will fail with a missing dependency.
92
-
93
- **Code: Watch Configuration**
94
- ```typescript
95
- export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
96
-
97
- export type WatchFolder = {
98
- /**
99
- * Source folder
100
- */
101
- src: string;
102
- /**
103
- * Target folder name, useful for deconstructing
104
- */
105
- target?: string;
106
- /**
107
- * Filter events
108
- */
109
- filter?: (ev: WatchEvent) => boolean;
110
- /**
111
- * Only look at immediate folder
112
- */
113
- immediate?: boolean;
114
- /**
115
- * List of top level folders to ignore
116
- */
117
- ignore?: string[];
118
- /**
119
- * If watching a folder that doesn't exist, should it be created?
120
- */
121
- createMissing?: boolean;
122
- /**
123
- * Include files that start with '.'
124
- */
125
- includeHidden?: boolean;
126
- };
127
-
128
- export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<void>, add: (item: WatchEvent | WatchEvent[]) => void };
129
- ```
130
-
131
- This method allows for watching one or more folders, and registering a callback that will fire every time a file changes, and which of the registered folders it was triggered within. The return of the `watchFolders` is a cleanup method, that when invoked will remove and stop all watching behavior.
132
-
133
- **Code: Watch Configuration**
134
- ```typescript
135
- export function watchFolders(
136
- folders: string[] | WatchFolder[],
137
- config: Omit<WatchFolder, 'src' | 'target'> = {}
138
- ): WatchStream {
139
- ```
140
-
141
89
  ## Anatomy of a Manifest
142
90
 
143
91
  **Code: Manifest for @travetto/manifest**
144
92
  ```typescript
145
93
  {
146
94
  "generated": 1868155200000,
147
- "moduleType": "commonjs",
148
- "mainModule": "@travetto/manifest",
149
- "mainFolder": "module/manifest",
150
95
  "workspacePath": "<generated>",
151
96
  "monoRepo": true,
152
- "outputFolder": ".trv_output",
153
- "toolFolder": ".trv_build",
154
- "compilerFolder": ".trv_compiler",
155
97
  "packageManager": "npm",
98
+ "moduleType": "commonjs",
99
+ "outputFolder": ".trv/output",
100
+ "toolFolder": ".trv/tool",
101
+ "compilerFolder": ".trv/compiler",
102
+ "compilerUrl": "http://127.0.0.1:26803",
103
+ "frameworkVersion": "x.x.x",
104
+ "mainModule": "@travetto/manifest",
105
+ "mainFolder": "module/manifest",
156
106
  "version": "x.x.x",
157
107
  "description": "Support for project indexing, manifesting, along with file watching",
158
- "frameworkVersion": "x.x.x",
159
108
  "modules": {
160
109
  "@travetto/manifest": {
161
110
  "main": true,
@@ -165,6 +114,9 @@ export function watchFolders(
165
114
  "internal": false,
166
115
  "sourceFolder": "module/manifest",
167
116
  "outputFolder": "node_modules/@travetto/manifest",
117
+ "roles": [ "std" ],
118
+ "parents": [],
119
+ "prod": true,
168
120
  "files": {
169
121
  "$root": [
170
122
  [ "DOC.html", "unknown", 1868155200000 ],
@@ -195,6 +147,7 @@ export function watchFolders(
195
147
  "src": [
196
148
  [ "src/delta.ts", "ts", 1868155200000 ],
197
149
  [ "src/dependencies.ts", "ts", 1868155200000 ],
150
+ [ "src/file.ts", "ts", 1868155200000 ],
198
151
  [ "src/manifest-index.ts", "ts", 1868155200000 ],
199
152
  [ "src/module.ts", "ts", 1868155200000 ],
200
153
  [ "src/package.ts", "ts", 1868155200000 ],
@@ -202,16 +155,13 @@ export function watchFolders(
202
155
  [ "src/root-index.ts", "ts", 1868155200000 ],
203
156
  [ "src/types.ts", "ts", 1868155200000 ],
204
157
  [ "src/typings.d.ts", "typings", 1868155200000 ],
205
- [ "src/util.ts", "ts", 1868155200000 ],
206
- [ "src/watch.ts", "ts", 1868155200000 ]
158
+ [ "src/util.ts", "ts", 1868155200000 ]
207
159
  ],
208
160
  "bin": [
209
161
  [ "bin/context.d.ts", "typings", 1868155200000 ],
210
162
  [ "bin/context.js", "js", 1868155200000 ]
211
163
  ]
212
- },
213
- "profiles": [ "std" ],
214
- "parents": []
164
+ }
215
165
  }
216
166
  }
217
167
  }
package/__index__.ts CHANGED
@@ -7,5 +7,5 @@ export * from './src/manifest-index';
7
7
  export * from './src/root-index';
8
8
  export * from './src/package';
9
9
  export * from './src/util';
10
- export * from './src/types';
11
- export * from './src/watch';
10
+ export * from './src/file';
11
+ export * from './src/types';
package/bin/context.js CHANGED
@@ -1,131 +1,161 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @typedef {import('../src/types').Package} Pkg
4
+ * @typedef {import('../src/types').Package & { path:string }} Pkg
5
+ * @typedef {Pkg & { mono: boolean, manager: 'yarn'|'npm', resolve: (file:string) => string}} Workspace
5
6
  * @typedef {import('../src/types').ManifestContext} ManifestContext
6
7
  */
7
8
  import fs from 'fs/promises';
8
9
  import path from 'path';
9
10
  import { createRequire } from 'module';
10
11
 
12
+ /** @type {Record<string, Workspace>} */ const WS_ROOT = {};
13
+ const TOOL_FOLDER = '.trv/tool';
14
+ const COMPILER_FOLDER = '.trv/compiler';
15
+ const OUTPUT_FOLDER = '.trv/output';
16
+
11
17
  /**
12
- * Returns the package.json
13
- * @param {string} inputFolder
14
- * @returns {Promise<Pkg>}
18
+ * Read package.json or return undefined if missing
19
+ * @param {string} dir
20
+ * @returns {Promise<Pkg|undefined>}
15
21
  */
16
- async function $getPkg(inputFolder) {
17
- if (!inputFolder.endsWith('.json')) {
18
- inputFolder = path.resolve(inputFolder, 'package.json');
19
- }
20
- return JSON.parse(await fs.readFile(inputFolder, 'utf8'));
22
+ async function $readPackage(dir) {
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);
21
26
  }
22
27
 
23
- const WS_ROOT = {};
24
-
25
28
  /**
26
- * Get module root for a given folder
29
+ * Find package.json for a given folder
27
30
  * @param {string} dir
28
- * @return {Promise<string>}
31
+ * @return {Promise<Pkg>}
29
32
  */
30
- async function $getModuleRoot(dir) {
33
+ async function $findPackage(dir) {
31
34
  let prev;
32
- while (dir !== prev && !(await fs.stat(path.resolve(dir, 'package.json')).catch(() => false))) {
33
- prev = dir;
34
- dir = path.dirname(dir);
35
+ let pkg, curr = path.resolve(dir);
36
+ while (!pkg && curr !== prev) {
37
+ pkg = await $readPackage(curr);
38
+ [prev, curr] = [curr, path.dirname(curr)];
39
+ }
40
+ if (!pkg) {
41
+ throw new Error('Could not find a package.json');
42
+ } else {
43
+ return pkg;
35
44
  }
36
- return dir;
37
- }
38
-
39
-
40
- /**
41
- * Get module name from a given file
42
- * @param {string} file
43
- * @return {Promise<string|void>}
44
- */
45
- async function $getModuleFromFile(file) {
46
- return $getPkg(await $getModuleRoot(path.dirname(file))).then(v => v.name, () => { });
47
45
  }
48
46
 
49
47
  /**
50
48
  * Get workspace root
51
- * @return {Promise<string>}
49
+ * @return {Promise<Workspace>}
52
50
  */
53
- async function $getWorkspaceRoot(base = process.cwd()) {
54
- if (base in WS_ROOT) {
55
- return WS_ROOT[base];
56
- }
57
-
51
+ async function $resolveWorkspace(base = process.cwd()) {
52
+ if (base in WS_ROOT) { return WS_ROOT[base]; }
58
53
  let folder = base;
59
- let prevFolder = '';
60
- while (folder !== prevFolder) {
61
- try {
62
- const pkg = await $getPkg(folder);
63
- if (!!pkg.workspaces || !!pkg.travetto?.isolated) {
64
- return (WS_ROOT[base] = folder);
65
- }
66
- } catch { }
67
- if (await fs.stat(path.resolve(folder, '.git')).catch(() => { })) {
54
+ let prev;
55
+ /** @type {Pkg|undefined} */
56
+ let prevPkg, pkg;
57
+
58
+ while (prev !== folder) {
59
+ [prev, prevPkg] = [folder, pkg];
60
+ pkg = await $readPackage(folder) ?? pkg;
61
+ if (
62
+ (pkg && (!!pkg.workspaces || !!pkg.travetto?.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
64
+ ) {
68
65
  break;
69
66
  }
70
- prevFolder = folder;
71
67
  folder = path.dirname(folder);
72
68
  }
73
- return WS_ROOT[base] = base;
74
- }
75
69
 
70
+ if (!pkg) {
71
+ throw new Error('Could not find a package.json');
72
+ }
73
+
74
+ return WS_ROOT[base] = {
75
+ ...pkg,
76
+ manager: await fs.stat(path.resolve(pkg.path, 'yarn.lock')).catch(() => { }) ? 'yarn' : 'npm',
77
+ resolve: createRequire(`${pkg.path}/node_modules`).resolve.bind(null),
78
+ mono: !!pkg.workspaces || (!pkg.travetto?.isolated && !!prevPkg) // Workspaces or nested projects
79
+ };
80
+ }
76
81
 
77
82
  /**
78
- * Gets build context
79
- * @param {string} [folder]
80
- * @return {Promise<ManifestContext>}
83
+ * Get Compiler url
84
+ * @param {Workspace} ws
81
85
  */
82
- export async function getManifestContext(folder) {
83
- const workspacePath = path.resolve(await $getWorkspaceRoot(folder));
84
- const req = createRequire(`${workspacePath}/node_modules`);
86
+ async function $getCompilerUrl(ws) {
87
+ let out = ws.travetto?.compilerUrl;
88
+ if (!out) {
89
+ const file = path.resolve(ws.path, ws.travetto?.toolFolder ?? TOOL_FOLDER, 'compiler.url');
90
+ // eslint-disable-next-line no-bitwise
91
+ const port = (Math.abs([...file].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000;
92
+ 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
+ }
97
+ }
98
+ return out.replace('localhost', '127.0.0.1');
99
+ }
85
100
 
86
- // If manifest specified via env var, and is a package name
101
+ /**
102
+ * Resolve module folder
103
+ * @param {Workspace} workspace
104
+ * @param {string|undefined} folder
105
+ */
106
+ async function $resolveModule(workspace, folder) {
107
+ let mod;
87
108
  if (!folder && process.env.TRV_MODULE) {
88
- // If module is actually a file, try to detect
89
- if (/[.](t|j)s$/.test(process.env.TRV_MODULE)) {
90
- process.env.TRV_MODULE = await $getModuleFromFile(process.env.TRV_MODULE) ?? process.env.TRV_MODULE;
109
+ mod = process.env.TRV_MODULE;
110
+ if (/[.](t|j)s$/.test(mod)) { // Rewrite from file to module
111
+ process.env.TRV_MODULE = mod = await $findPackage(path.dirname(mod))
112
+ .then(v => v.name, () => '');
91
113
  }
114
+ }
115
+
116
+ if (mod) { // If module provided in lieu of folder
92
117
  try {
93
- folder = path.dirname(req.resolve(`${process.env.TRV_MODULE}/package.json`));
118
+ folder = path.dirname(workspace.resolve(`${mod}/package.json`));
94
119
  } catch {
95
- const workspacePkg = JSON.parse(await fs.readFile(path.resolve(workspacePath, 'package.json'), 'utf8'));
96
- if (workspacePkg.name === process.env.TRV_MODULE) {
97
- folder = workspacePath;
120
+ const workspacePkg = await $readPackage(workspace.path);
121
+ if (workspacePkg?.name === mod) {
122
+ folder = workspace.path;
98
123
  } else {
99
124
  throw new Error(`Unable to resolve location for ${folder}`);
100
125
  }
101
126
  }
102
127
  }
103
128
 
104
- const mainPath = await $getModuleRoot(path.resolve(folder ?? '.'));
105
- const { name: mainModule, workspaces, travetto, version, description } = (await $getPkg(mainPath));
106
- const monoRepo = workspacePath !== mainPath || !!workspaces;
107
- const outputFolder = travetto?.outputFolder ?? '.trv_output';
129
+ return $findPackage(folder ?? '.');
130
+ }
108
131
 
109
- const moduleType = (await $getPkg(workspacePath)).type ?? 'commonjs';
110
- const mainFolder = mainPath === workspacePath ? '' : mainPath.replace(`${workspacePath}/`, '');
111
- /** @type {'yarn'|'npm'} */
112
- const packageManager = await fs.stat(path.resolve(workspacePath, 'yarn.lock')).then(() => 'yarn', () => 'npm');
132
+ /**
133
+ * Gets build context
134
+ * @param {string} [folder]
135
+ * @return {Promise<ManifestContext>}
136
+ */
137
+ export async function getManifestContext(folder) {
138
+ const workspace = await $resolveWorkspace(folder);
113
139
 
114
- const { version: frameworkVersion } = JSON.parse(await fs.readFile(req.resolve('@travetto/manifest/package.json'), 'utf8'));
140
+ const [mod, framework, compilerUrl] = await Promise.all([
141
+ $resolveModule(workspace, folder),
142
+ $readPackage(workspace.resolve('@travetto/manifest/package.json')),
143
+ $getCompilerUrl(workspace),
144
+ ]);
115
145
 
116
- const res = {
117
- moduleType,
118
- mainModule: mainModule ?? 'untitled', // When root package.json is missing a name
119
- mainFolder,
120
- workspacePath,
121
- monoRepo,
122
- outputFolder,
123
- toolFolder: '.trv_build',
124
- compilerFolder: '.trv_compiler',
125
- packageManager,
126
- version,
127
- description,
128
- frameworkVersion
146
+ return {
147
+ workspacePath: workspace.path,
148
+ monoRepo: workspace.mono,
149
+ packageManager: workspace.manager,
150
+ moduleType: workspace.type ?? 'commonjs',
151
+ outputFolder: workspace.travetto?.outputFolder ?? OUTPUT_FOLDER,
152
+ toolFolder: workspace.travetto?.toolFolder ?? TOOL_FOLDER,
153
+ compilerFolder: workspace.travetto?.compilerFolder ?? COMPILER_FOLDER,
154
+ compilerUrl,
155
+ frameworkVersion: framework?.version ?? '1.0.0',
156
+ mainModule: mod.name ?? 'untitled',
157
+ mainFolder: mod.path === workspace.path ? '' : mod.path.replace(`${workspace.path}/`, ''),
158
+ version: mod.version,
159
+ description: mod.description
129
160
  };
130
- return res;
131
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/manifest",
3
- "version": "3.3.2",
3
+ "version": "3.4.0-rc.1",
4
4
  "description": "Support for project indexing, manifesting, along with file watching",
5
5
  "keywords": [
6
6
  "path",
@@ -30,14 +30,6 @@
30
30
  "url": "https://github.com/travetto/travetto.git",
31
31
  "directory": "module/manifest"
32
32
  },
33
- "peerDependencies": {
34
- "@parcel/watcher": "^2.1.0"
35
- },
36
- "peerDependenciesMeta": {
37
- "@parcel/watcher": {
38
- "optional": true
39
- }
40
- },
41
33
  "travetto": {
42
34
  "displayName": "Manifest",
43
35
  "docBaseUrl": "https://github.com/travetto/travetto/tree/main"
package/src/delta.ts CHANGED
@@ -1,12 +1,11 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
-
4
1
  import {
5
2
  ManifestContext, ManifestModule, ManifestModuleCore, ManifestModuleFile,
6
3
  ManifestModuleFileType, ManifestModuleFolderType, ManifestRoot
7
4
  } from './types';
8
5
 
9
6
  import { ManifestModuleUtil } from './module';
7
+ import { ManifestFileUtil } from './file';
8
+ import { path } from './path';
10
9
 
11
10
  type DeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty';
12
11
  type DeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> };
@@ -46,7 +45,7 @@ export class ManifestDeltaUtil {
46
45
  for (const el of Object.keys(left.files)) {
47
46
  const output = ManifestModuleUtil.sourceToOutputExt(`${outputFolder}/${left.outputFolder}/${el}`);
48
47
  const [, , leftTs] = left.files[el];
49
- const stat = await fs.stat(output).catch(() => { });
48
+ const stat = await ManifestFileUtil.statFile(output);
50
49
  right.delete(ManifestModuleUtil.sourceToBlankExt(el));
51
50
 
52
51
  if (!stat) {
@@ -1,8 +1,9 @@
1
1
  import { PackageUtil } from './package';
2
2
  import { path } from './path';
3
- import { ManifestContext, ManifestProfile, PackageRel, PackageVisitor, PackageVisitReq } from './types';
3
+ import { ManifestContext, ManifestModuleRole, PackageVisitor, PackageVisitReq, Package } from './types';
4
4
 
5
5
  export type ModuleDep = {
6
+ pkg: Package;
6
7
  version: string;
7
8
  name: string;
8
9
  main?: boolean;
@@ -10,9 +11,11 @@ export type ModuleDep = {
10
11
  local?: boolean;
11
12
  internal?: boolean;
12
13
  sourcePath: string;
13
- childSet: Map<string, Set<PackageRel>>;
14
+ childSet: Set<string>;
14
15
  parentSet: Set<string>;
15
- profileSet: Set<ManifestProfile>;
16
+ roleSet: Set<ManifestModuleRole>;
17
+ prod: boolean;
18
+ topLevel?: boolean;
16
19
  };
17
20
 
18
21
  /**
@@ -60,19 +63,13 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
60
63
  ...workspaceModules.map(x => x.name)
61
64
  ]);
62
65
 
63
- const globals = [
64
- ...(workspacePkg.travetto?.globalModules ?? []),
65
- ...(pkg.travetto?.globalModules ?? [])
66
- ]
67
- .map(f => PackageUtil.resolvePackagePath(f));
66
+ const globals = (workspacePkg.travetto?.globalModules ?? [])
67
+ .map(name => PackageUtil.packageReq<ModuleDep>(PackageUtil.resolvePackagePath(name), name in (workspacePkg.dependencies ?? {}), true));
68
68
 
69
69
  const workspaceModuleDeps = workspaceModules
70
- .map(entry => path.resolve(req.sourcePath, entry.sourcePath));
70
+ .map(entry => PackageUtil.packageReq<ModuleDep>(path.resolve(req.sourcePath, entry.sourcePath), false, true));
71
71
 
72
- return [
73
- ...globals,
74
- ...workspaceModuleDeps
75
- ].map(s => PackageUtil.packageReq(s, 'global'));
72
+ return [...globals, ...workspaceModuleDeps];
76
73
  }
77
74
 
78
75
  /**
@@ -80,8 +77,7 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
80
77
  */
81
78
  valid(req: PackageVisitReq<ModuleDep>): boolean {
82
79
  return req.sourcePath === this.#mainSourcePath || (
83
- req.rel !== 'peer' &&
84
- (!!req.pkg.travetto || req.pkg.private === true || !req.sourcePath.includes('node_modules') || req.rel === 'global')
80
+ (!!req.pkg.travetto || req.pkg.private === true || !req.sourcePath.includes('node_modules'))
85
81
  );
86
82
  }
87
83
 
@@ -89,18 +85,16 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
89
85
  * Create dependency from request
90
86
  */
91
87
  create(req: PackageVisitReq<ModuleDep>): ModuleDep {
92
- const { pkg: { name, version, travetto: { profiles = [] } = {}, ...pkg }, sourcePath } = req;
93
- const profileSet = new Set<ManifestProfile>([
94
- ...profiles ?? []
95
- ]);
88
+ const { pkg, sourcePath } = req;
89
+ const { name, version } = pkg;
96
90
  const main = name === this.ctx.mainModule;
97
91
  const mainSource = main || this.#mainPatterns.some(x => x.test(name));
98
92
  const internal = pkg.private === true;
99
93
  const local = internal || mainSource || !sourcePath.includes('node_modules');
100
94
 
101
95
  const dep = {
102
- name, version, sourcePath, main, mainSource, local, internal,
103
- parentSet: new Set([]), childSet: new Map(), profileSet
96
+ name, version, sourcePath, main, mainSource, local, internal, pkg: req.pkg,
97
+ parentSet: new Set([]), childSet: new Set([]), roleSet: new Set([]), prod: req.prod, topLevel: req.topLevel
104
98
  };
105
99
 
106
100
  return dep;
@@ -113,28 +107,30 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
113
107
  const { parent } = req;
114
108
  if (parent && dep.name !== this.ctx.mainModule) {
115
109
  dep.parentSet.add(parent.name);
116
- const set = parent.childSet.get(dep.name) ?? new Set();
117
- parent.childSet.set(dep.name, set);
118
- set.add(req.rel);
110
+ parent.childSet.add(dep.name);
119
111
  }
120
112
  }
121
113
 
122
114
  /**
123
- * Propagate profile/relationship information through graph
115
+ * Propagate prod, role information through graph
124
116
  */
125
117
  complete(deps: Set<ModuleDep>): Set<ModuleDep> {
126
- const mapping = new Map<string, { parent: Set<string>, child: Map<string, Set<PackageRel>>, el: ModuleDep }>();
118
+ const mapping = new Map<string, { parent: Set<string>, child: Set<string>, el: ModuleDep }>();
127
119
  for (const el of deps) {
128
- mapping.set(el.name, { parent: new Set(el.parentSet), child: new Map(el.childSet), el });
120
+ mapping.set(el.name, { parent: new Set(el.parentSet), child: new Set(el.childSet), el });
129
121
  }
130
122
 
131
123
  const main = mapping.get(this.ctx.mainModule)!;
132
124
 
133
125
  // Visit all direct dependencies and mark
134
- for (const [name, relSet] of main.child) {
135
- const childDep = mapping.get(name)!.el;
136
- if (!relSet.has('dev')) {
137
- childDep.profileSet.add('std');
126
+ for (const { el } of mapping.values()) {
127
+ if (el.topLevel) {
128
+ el.roleSet = new Set(el.pkg.travetto?.roles ?? []);
129
+ if (!el.roleSet.size) {
130
+ el.roleSet.add('std');
131
+ }
132
+ } else if (!main.child.has(el.name)) { // Not a direct descendent
133
+ el.prod = false;
138
134
  }
139
135
  }
140
136
 
@@ -148,9 +144,10 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
148
144
  for (const c of child.keys()) {
149
145
  const { el: cDep, parent } = mapping.get(c)!;
150
146
  parent.delete(el.name); // Remove from child
151
- for (const prof of el.profileSet) {
152
- cDep.profileSet.add(prof);
147
+ for (const role of el.roleSet) {
148
+ cDep.roleSet.add(role);
153
149
  }
150
+ cDep.prod ||= el.prod; // Allow prod to trickle down as needed
154
151
  }
155
152
  }
156
153
  // Remove from mapping
@@ -159,8 +156,10 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
159
156
  }
160
157
  }
161
158
 
162
- // Color the main folder as std
163
- main.el.profileSet.add('std');
159
+ // Color parent as final step
160
+ main.el.prod = true;
161
+ main.el.roleSet.add('std');
162
+
164
163
  return deps;
165
164
  }
166
165
  }
package/src/file.ts ADDED
@@ -0,0 +1,55 @@
1
+ import os from 'os';
2
+ import fs from 'fs/promises';
3
+ import { readFileSync } from 'fs';
4
+
5
+ import { path } from './path';
6
+ import type { ManifestContext } from './types';
7
+
8
+ export class ManifestFileUtil {
9
+ /**
10
+ * Write file and copy over when ready
11
+ */
12
+ static async bufferedFileWrite(file: string, content: string | object): Promise<string> {
13
+ const ext = path.extname(file);
14
+ const tempName = `${path.basename(file, ext)}.${process.ppid}.${process.pid}.${Date.now()}.${Math.random()}${ext}`;
15
+ await fs.mkdir(path.dirname(file), { recursive: true });
16
+ const temp = path.resolve(os.tmpdir(), tempName);
17
+ await fs.writeFile(temp, typeof content === 'string' ? content : JSON.stringify(content), 'utf8');
18
+ await fs.copyFile(temp, file);
19
+ fs.unlink(temp);
20
+ return file;
21
+ }
22
+
23
+ /**
24
+ * Read as json
25
+ */
26
+ static async readAsJson<T = unknown>(file: string): Promise<T> {
27
+ return JSON.parse(await fs.readFile(file, 'utf8'));
28
+ }
29
+
30
+ /**
31
+ * Read as json, sync
32
+ */
33
+ static readAsJsonSync<T = unknown>(file: string): T {
34
+ return JSON.parse(readFileSync(file, 'utf8'));
35
+ }
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.mainModule);
52
+ }
53
+ return path.resolve(ctx.workspacePath, ctx.toolFolder, ...parts);
54
+ }
55
+ }
@@ -3,18 +3,16 @@ import { path } from './path';
3
3
 
4
4
  import {
5
5
  ManifestModule, ManifestModuleCore, ManifestModuleFile,
6
- ManifestModuleFileType, ManifestModuleFolderType, ManifestProfile, ManifestRoot
6
+ ManifestModuleFileType, ManifestModuleFolderType, ManifestModuleRole, ManifestRoot
7
7
  } from './types';
8
8
 
9
9
  import { ManifestUtil } from './util';
10
10
 
11
- type ScanTest = ((full: string) => boolean) | { test: (full: string) => boolean };
12
11
  export type FindConfig = {
13
- folders?: ManifestModuleFolderType[];
14
- filter?: ScanTest;
15
- includeIndex?: boolean;
16
- profiles?: string[];
17
- checkProfile?: boolean;
12
+ folder?: (folder: ManifestModuleFolderType) => boolean;
13
+ module?: (module: IndexedModule) => boolean;
14
+ file?: (file: IndexedFile) => boolean;
15
+ sourceOnly?: boolean;
18
16
  };
19
17
 
20
18
  export type IndexedFile = {
@@ -24,7 +22,7 @@ export type IndexedFile = {
24
22
  sourceFile: string;
25
23
  outputFile: string;
26
24
  relativeFile: string;
27
- profile: ManifestProfile;
25
+ role: ManifestModuleRole;
28
26
  type: ManifestModuleFileType;
29
27
  };
30
28
 
@@ -34,6 +32,12 @@ export type IndexedModule = ManifestModuleCore & {
34
32
  files: Record<ManifestModuleFolderType, IndexedFile[]>;
35
33
  };
36
34
 
35
+ const TypedObject: {
36
+ keys<T = unknown, K extends keyof T = keyof T>(o: T): K[];
37
+ fromEntries<K extends string | symbol, V>(items: ([K, V] | readonly [K, V])[]): Record<K, V>;
38
+ entries<K extends Record<symbol | string, unknown>>(record: K): [keyof K, K[keyof K]][];
39
+ } & ObjectConstructor = Object;
40
+
37
41
  /**
38
42
  * Manifest index
39
43
  */
@@ -78,7 +82,7 @@ export class ManifestIndex {
78
82
  }
79
83
 
80
84
  #moduleFiles(m: ManifestModule, files: ManifestModuleFile[]): IndexedFile[] {
81
- return files.map(([f, type, ts, profile = 'std']) => {
85
+ return files.map(([f, type, ts, role = 'std']) => {
82
86
  const isSource = type === 'ts' || type === 'js';
83
87
  const sourceFile = path.resolve(this.#manifest.workspacePath, m.sourceFolder, f);
84
88
  const js = isSource ? ManifestModuleUtil.sourceToOutputExt(f) : f;
@@ -89,7 +93,7 @@ export class ManifestIndex {
89
93
  id = ManifestModuleUtil.sourceToBlankExt(id);
90
94
  }
91
95
 
92
- return { id, type, sourceFile, outputFile, import: modImport, profile, relativeFile: f, module: m.name };
96
+ return { id, type, sourceFile, outputFile, import: modImport, role, relativeFile: f, module: m.name };
93
97
  });
94
98
  }
95
99
 
@@ -143,58 +147,27 @@ export class ManifestIndex {
143
147
 
144
148
  /**
145
149
  * Find files from the index
146
- * @param folder The sub-folder to check into
147
- * @param filter The filter to determine if this is a valid support file
150
+ * @param config The configuration for controlling the find process
148
151
  */
149
152
  find(config: FindConfig): IndexedFile[] {
150
- const { filter: f, folders } = config;
151
- const filter = f ? 'test' in f ? f.test.bind(f) : f : f;
152
-
153
- let idx = this.#modules;
154
-
155
- const checkProfile = config.checkProfile ?? true;
156
-
157
- const activeProfiles = new Set(['std', ...(config.profiles ?? process.env.TRV_PROFILES?.split(/\s*,\s*/g) ?? [])]);
158
-
159
- if (checkProfile) {
160
- idx = idx.filter(m => m.profiles.length === 0 || m.profiles.some(p => activeProfiles.has(p)));
161
- }
162
-
163
- let searchSpace = folders ?
164
- idx.flatMap(m => [...folders.flatMap(fo => m.files[fo] ?? []), ...(config.includeIndex ? (m.files.$index ?? []) : [])]) :
165
- idx.flatMap(m => [...Object.values(m.files)].flat());
166
-
167
- if (checkProfile) {
168
- searchSpace = searchSpace.filter(fi => activeProfiles.has(fi.profile));
153
+ const searchSpace: IndexedFile[] = [];
154
+ for (const m of this.#modules) {
155
+ if (config.module?.(m) ?? true) {
156
+ for (const [folder, files] of TypedObject.entries(m.files)) {
157
+ if (config.folder?.(folder) ?? true) {
158
+ for (const file of files) {
159
+ if (
160
+ (config.file?.(file) ?? true) &&
161
+ (config.sourceOnly === false || file.type === 'ts')
162
+ ) {
163
+ searchSpace.push(file);
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
169
  }
170
-
171
- return searchSpace
172
- .filter(({ type }) => type === 'ts')
173
- .filter(({ sourceFile: source }) => filter?.(source) ?? true);
174
- }
175
-
176
- /**
177
- * Find files from the index
178
- * @param filter The filter to determine if this is a valid support file
179
- */
180
- findSupport(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
181
- return this.find({ ...config, folders: ['support'] });
182
- }
183
-
184
- /**
185
- * Find files from the index
186
- * @param filter The filter to determine if this is a valid support file
187
- */
188
- findSrc(config: Omit<FindConfig, 'folder'> = {}): IndexedFile[] {
189
- return this.find({ ...config, includeIndex: true, folders: ['src'] });
190
- }
191
-
192
- /**
193
- * Find files from the index
194
- * @param filter The filter to determine if this is a valid support file
195
- */
196
- findTest(config: Omit<FindConfig, 'folder'>): IndexedFile[] {
197
- return this.find({ ...config, folders: ['test'] });
170
+ return searchSpace;
198
171
  }
199
172
 
200
173
  /**
package/src/module.ts CHANGED
@@ -4,7 +4,8 @@ import { path } from './path';
4
4
  import {
5
5
  ManifestContext,
6
6
  ManifestModule, ManifestModuleFile, ManifestModuleFileType,
7
- ManifestModuleFolderType, ManifestProfile
7
+ ManifestModuleFolderType,
8
+ ManifestModuleRole
8
9
  } from './types';
9
10
  import { ModuleDep, ModuleDependencyVisitor } from './dependencies';
10
11
  import { PackageUtil } from './package';
@@ -56,9 +57,6 @@ export class ManifestModuleUtil {
56
57
  const topFiles = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
57
58
  const out: string[] = [];
58
59
 
59
- if (!fs.stat(folder).catch(() => false)) {
60
- return out;
61
- }
62
60
  const stack: [string, number][] = [[folder, 0]];
63
61
  while (stack.length) {
64
62
  const popped = stack.pop();
@@ -117,7 +115,7 @@ export class ManifestModuleUtil {
117
115
  /**
118
116
  * Get file type for a file name
119
117
  */
120
- static getFileProfile(moduleFile: string): ManifestProfile | undefined {
118
+ static getFileRole(moduleFile: string): ManifestModuleRole | undefined {
121
119
  if (moduleFile.startsWith('support/transform')) {
122
120
  return 'compile';
123
121
  } else if (moduleFile.startsWith('support/test/') || moduleFile.startsWith('test/')) {
@@ -170,15 +168,15 @@ export class ManifestModuleUtil {
170
168
  */
171
169
  static async transformFile(moduleFile: string, full: string): Promise<ManifestModuleFile> {
172
170
  const res: ManifestModuleFile = [moduleFile, this.getFileType(moduleFile), this.#getNewest(await fs.stat(full))];
173
- const profile = this.getFileProfile(moduleFile);
174
- return profile ? [...res, profile] : res;
171
+ const role = this.getFileRole(moduleFile);
172
+ return role ? [...res, role] : res;
175
173
  }
176
174
 
177
175
  /**
178
176
  * Visit a module and describe files, and metadata
179
177
  */
180
178
  static async describeModule(ctx: ManifestContext, dep: ModuleDep): Promise<ManifestModule> {
181
- const { main, mainSource, local, name, version, sourcePath, profileSet, parentSet, internal } = dep;
179
+ const { main, mainSource, local, name, version, sourcePath, roleSet, prod, parentSet, internal } = dep;
182
180
 
183
181
  const files: ManifestModule['files'] = {};
184
182
 
@@ -195,12 +193,12 @@ export class ManifestModuleUtil {
195
193
  files.$root = files.$root?.filter(([file, type]) => type !== 'ts');
196
194
  }
197
195
 
198
- const profiles = [...profileSet].sort();
196
+ const roles = [...roleSet ?? []].sort();
199
197
  const parents = [...parentSet].sort();
200
198
  const outputFolder = `node_modules/${name}`;
201
199
  const sourceFolder = sourcePath === ctx.workspacePath ? '' : sourcePath.replace(`${ctx.workspacePath}/`, '');
202
200
 
203
- const res = { main, name, version, local, internal, sourceFolder, outputFolder, files, profiles, parents, };
201
+ const res = { main, name, version, local, internal, sourceFolder, outputFolder, roles, parents, prod, files };
204
202
  return res;
205
203
  }
206
204
 
package/src/package.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { readFileSync } from 'fs';
2
- import fs from 'fs/promises';
3
1
  import { createRequire } from 'module';
4
2
  import { execSync } from 'child_process';
5
3
 
6
- import { ManifestContext, Package, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
4
+ import { ManifestContext, Package, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
7
5
  import { path } from './path';
6
+ import { ManifestFileUtil } from './file';
8
7
 
9
8
  /**
10
9
  * Utilities for querying, traversing and reading package.json files.
@@ -60,8 +59,8 @@ export class PackageUtil {
60
59
  /**
61
60
  * Build a package visit req
62
61
  */
63
- static packageReq<T>(sourcePath: string, rel: PackageRel): PackageVisitReq<T> {
64
- return { pkg: this.readPackage(sourcePath), sourcePath, rel };
62
+ static packageReq<T>(sourcePath: string, prod: boolean, topLevel?: boolean): PackageVisitReq<T> {
63
+ return { pkg: this.readPackage(sourcePath), sourcePath, prod, topLevel };
65
64
  }
66
65
 
67
66
  /**
@@ -71,22 +70,13 @@ export class PackageUtil {
71
70
  const pkg = this.readPackage(modulePath);
72
71
  const children: Record<string, PackageVisitReq<T>> = {};
73
72
  const local = modulePath === rootPath && !modulePath.includes('node_modules');
74
- for (const [deps, rel] of [
75
- [pkg.dependencies, 'prod'],
76
- [pkg.peerDependencies, 'peer'],
77
- [pkg.optionalDependencies, 'opt'],
78
- ...(local ? [[pkg.devDependencies, 'dev'] as const] : []),
73
+ for (const [deps, prod] of [
74
+ [pkg.dependencies, true],
75
+ ...(local ? [[pkg.devDependencies, false] as const] : []),
79
76
  ] as const) {
80
77
  for (const [name, version] of Object.entries(deps ?? {})) {
81
- try {
82
- const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name);
83
- children[`${name}#${version}`] = this.packageReq<T>(depPath, rel);
84
- } catch (err) {
85
- if (rel === 'opt' || (rel === 'peer' && !!pkg.peerDependenciesMeta?.[name].optional)) {
86
- continue;
87
- }
88
- throw err;
89
- }
78
+ const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name);
79
+ children[`${name}#${version}`] = this.packageReq<T>(depPath, prod, false);
90
80
  }
91
81
  }
92
82
  return Object.values(children).sort((a, b) => a.pkg.name.localeCompare(b.pkg.name));
@@ -99,10 +89,9 @@ export class PackageUtil {
99
89
  if (forceRead) {
100
90
  delete this.#cache[modulePath];
101
91
  }
102
- const res = this.#cache[modulePath] ??= JSON.parse(readFileSync(
92
+ const res = this.#cache[modulePath] ??= ManifestFileUtil.readAsJsonSync(
103
93
  modulePath.endsWith('.json') ? modulePath : path.resolve(modulePath, 'package.json'),
104
- 'utf8'
105
- ));
94
+ );
106
95
 
107
96
  res.name ??= 'untitled'; // If a package.json (root-only) is missing a name, allows for npx execution
108
97
 
@@ -125,7 +114,7 @@ export class PackageUtil {
125
114
  ): Promise<Set<T>> {
126
115
 
127
116
  const root = typeof rootOrPath === 'string' ?
128
- this.packageReq<T>(rootOrPath, 'root') :
117
+ this.packageReq<T>(rootOrPath, false, true) :
129
118
  rootOrPath;
130
119
 
131
120
  const seen = new Map<string, T>();
@@ -159,11 +148,10 @@ export class PackageUtil {
159
148
  */
160
149
  static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> {
161
150
  if (!this.#workspaces[rootPath]) {
162
- await fs.mkdir(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true });
163
151
  const cache = path.resolve(ctx.workspacePath, ctx.outputFolder, 'workspaces.json');
164
152
  try {
165
- return JSON.parse(await fs.readFile(cache, 'utf8'));
166
- } catch {
153
+ return await ManifestFileUtil.readAsJson(cache);
154
+ } catch (err) {
167
155
  let out: PackageWorkspaceEntry[];
168
156
  switch (ctx.packageManager) {
169
157
  case 'npm': {
@@ -180,7 +168,7 @@ export class PackageUtil {
180
168
 
181
169
  this.#workspaces[rootPath] = out;
182
170
 
183
- await fs.writeFile(cache, JSON.stringify(out), 'utf8');
171
+ await ManifestFileUtil.bufferedFileWrite(cache, out);
184
172
  }
185
173
  }
186
174
  return this.#workspaces[rootPath];
package/src/path.ts CHANGED
@@ -9,7 +9,7 @@ const toPosix = (file: string): string => file.replaceAll('\\', '/');
9
9
  /**
10
10
  * Converts a given file name by replace all slashes, with platform dependent path separators
11
11
  */
12
- const toNative = (file: string): string => file.replaceAll('/', sep);
12
+ const toNative = (file: string): string => file.replace(/[\\\/]+/g, sep);
13
13
 
14
14
  const cwd = (): string => toPosix(process.cwd());
15
15
 
package/src/root-index.ts CHANGED
@@ -27,15 +27,6 @@ class $RootIndex extends ManifestIndex {
27
27
  return !!this.manifest.monoRepo && !this.manifest.mainFolder;
28
28
  }
29
29
 
30
- /**
31
- * Asynchronously load all source files from manifest
32
- */
33
- async loadSource(): Promise<void> {
34
- for (const { import: imp } of this.findSrc()) {
35
- await import(imp);
36
- }
37
- }
38
-
39
30
  /**
40
31
  * Get internal id from file name and optionally, class name
41
32
  */
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type NodeModuleType = 'module' | 'commonjs';
2
+
1
3
  export type ManifestModuleFileType = 'typings' | 'ts' | 'js' | 'json' | 'package-json' | 'unknown' | 'fixture' | 'md';
2
4
  export type ManifestModuleFolderType =
3
5
  '$root' | '$index' | '$package' |
@@ -5,10 +7,9 @@ export type ManifestModuleFolderType =
5
7
  'test/fixtures' | 'support/fixtures' | 'support/resources' |
6
8
  '$other' | '$transformer';
7
9
 
8
- export type ManifestProfile = 'compile' | 'test' | 'doc' | 'build' | 'std';
9
- export type PackageRel = 'dev' | 'prod' | 'peer' | 'opt' | 'root' | 'global';
10
+ export type ManifestModuleRole = 'std' | 'test' | 'doc' | 'compile' | 'build';
10
11
 
11
- export type ManifestModuleFile = [string, ManifestModuleFileType, number] | [string, ManifestModuleFileType, number, ManifestProfile];
12
+ export type ManifestModuleFile = [string, ManifestModuleFileType, number] | [string, ManifestModuleFileType, number, ManifestModuleRole];
12
13
  export type ManifestModuleCore = {
13
14
  name: string;
14
15
  main?: boolean;
@@ -16,7 +17,8 @@ export type ManifestModuleCore = {
16
17
  version: string;
17
18
  sourceFolder: string;
18
19
  outputFolder: string;
19
- profiles: ManifestProfile[];
20
+ prod: boolean;
21
+ roles: ManifestModuleRole[];
20
22
  parents: string[];
21
23
  internal?: boolean;
22
24
  };
@@ -33,11 +35,12 @@ export type ManifestContext = {
33
35
  toolFolder: string;
34
36
  compilerFolder: string;
35
37
  monoRepo?: boolean;
36
- moduleType: 'module' | 'commonjs';
38
+ moduleType: NodeModuleType;
37
39
  packageManager: 'yarn' | 'npm';
38
40
  frameworkVersion: string;
39
- description: string;
41
+ description?: string;
40
42
  version: string;
43
+ compilerUrl: string;
41
44
  };
42
45
 
43
46
  export type ManifestRoot = ManifestContext & {
@@ -47,7 +50,7 @@ export type ManifestRoot = ManifestContext & {
47
50
 
48
51
  export type Package = {
49
52
  name: string;
50
- type?: 'module' | 'commonjs';
53
+ type?: NodeModuleType;
51
54
  version: string;
52
55
  description?: string;
53
56
  license?: string;
@@ -75,7 +78,7 @@ export type Package = {
75
78
  travetto?: {
76
79
  isolated?: boolean;
77
80
  displayName?: string;
78
- profiles?: ManifestProfile[];
81
+ roles?: ManifestModuleRole[];
79
82
  globalModules?: string[];
80
83
  mainSource?: string[];
81
84
  docOutput?: string[];
@@ -83,6 +86,9 @@ export type Package = {
83
86
  docBaseUrl?: string;
84
87
  docOutputs?: string[];
85
88
  outputFolder?: string;
89
+ toolFolder?: string;
90
+ compilerFolder?: string;
91
+ compilerUrl?: string;
86
92
  };
87
93
  workspaces?: string[];
88
94
  private?: boolean;
@@ -91,7 +97,7 @@ export type Package = {
91
97
 
92
98
  type OrProm<T> = T | Promise<T>;
93
99
 
94
- export type PackageVisitReq<T> = { pkg: Package, rel: PackageRel, sourcePath: string, parent?: T };
100
+ export type PackageVisitReq<T> = { pkg: Package, prod: boolean, sourcePath: string, parent?: T, topLevel?: boolean };
95
101
  export type PackageVisitor<T> = {
96
102
  init?(req: PackageVisitReq<T>): OrProm<undefined | void | PackageVisitReq<T>[]>;
97
103
  valid?(req: PackageVisitReq<T>): boolean;
package/src/util.ts CHANGED
@@ -1,10 +1,7 @@
1
- import { readFileSync } from 'fs';
2
- import fs from 'fs/promises';
3
- import os from 'os';
4
-
5
1
  import { path } from './path';
6
2
  import { ManifestContext, ManifestRoot } from './types';
7
3
  import { ManifestModuleUtil } from './module';
4
+ import { ManifestFileUtil } from './file';
8
5
 
9
6
  const MANIFEST_FILE = 'manifest.json';
10
7
 
@@ -12,20 +9,6 @@ const MANIFEST_FILE = 'manifest.json';
12
9
  * Manifest utils
13
10
  */
14
11
  export class ManifestUtil {
15
- /**
16
- * Write file and copy over when ready
17
- */
18
- static async writeFileWithBuffer(file: string, content: string): Promise<string> {
19
- const ext = path.extname(file);
20
- const tempName = `${path.basename(file, ext)}.${process.ppid}.${process.pid}.${Date.now()}.${Math.random()}${ext}`;
21
- await fs.mkdir(path.dirname(file), { recursive: true });
22
- const temp = path.resolve(os.tmpdir(), tempName);
23
- await fs.writeFile(temp, content, 'utf8');
24
- await fs.copyFile(temp, file);
25
- fs.unlink(temp);
26
- return file;
27
- }
28
-
29
12
  /**
30
13
  * Build a manifest context
31
14
  * @param folder
@@ -55,7 +38,7 @@ export class ManifestUtil {
55
38
  // If in prod mode, only include std modules
56
39
  modules: Object.fromEntries(
57
40
  Object.values(manifest.modules)
58
- .filter(x => x.profiles.includes('std'))
41
+ .filter(x => x.prod)
59
42
  .map(m => [m.name, m])
60
43
  ),
61
44
  // Mark output folder/workspace path as portable
@@ -75,7 +58,7 @@ export class ManifestUtil {
75
58
  if (!file.endsWith('.json')) {
76
59
  file = path.resolve(file, MANIFEST_FILE);
77
60
  }
78
- const manifest: ManifestRoot = JSON.parse(readFileSync(file, 'utf8'));
61
+ const manifest: ManifestRoot = ManifestFileUtil.readAsJsonSync(file);
79
62
  // Support packaged environments, by allowing empty outputFolder
80
63
  if (!manifest.outputFolder) {
81
64
  manifest.outputFolder = path.cwd();
@@ -88,7 +71,7 @@ export class ManifestUtil {
88
71
  * Write manifest for a given context, return location
89
72
  */
90
73
  static writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<string> {
91
- return this.writeFileWithBuffer(
74
+ return ManifestFileUtil.bufferedFileWrite(
92
75
  path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule, MANIFEST_FILE),
93
76
  JSON.stringify(manifest)
94
77
  );
@@ -102,8 +85,7 @@ export class ManifestUtil {
102
85
  location = path.resolve(location, MANIFEST_FILE);
103
86
  }
104
87
 
105
- await fs.mkdir(path.dirname(location), { recursive: true });
106
- await fs.writeFile(location, JSON.stringify(manifest), 'utf8');
88
+ await ManifestFileUtil.bufferedFileWrite(location, JSON.stringify(manifest));
107
89
 
108
90
  return location;
109
91
  }
package/src/watch.ts DELETED
@@ -1,208 +0,0 @@
1
- import { watch, Stats } from 'fs';
2
- import fs from 'fs/promises';
3
-
4
- import { path } from './path';
5
-
6
- async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
7
- try {
8
- return await import('@parcel/watcher');
9
- } catch (err) {
10
- console.error('@parcel/watcher must be installed to use watching functionality');
11
- throw err;
12
- }
13
- }
14
-
15
- export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
16
-
17
- export type WatchFolder = {
18
- /**
19
- * Source folder
20
- */
21
- src: string;
22
- /**
23
- * Target folder name, useful for deconstructing
24
- */
25
- target?: string;
26
- /**
27
- * Filter events
28
- */
29
- filter?: (ev: WatchEvent) => boolean;
30
- /**
31
- * Only look at immediate folder
32
- */
33
- immediate?: boolean;
34
- /**
35
- * List of top level folders to ignore
36
- */
37
- ignore?: string[];
38
- /**
39
- * If watching a folder that doesn't exist, should it be created?
40
- */
41
- createMissing?: boolean;
42
- /**
43
- * Include files that start with '.'
44
- */
45
- includeHidden?: boolean;
46
- };
47
-
48
- export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<void>, add: (item: WatchEvent | WatchEvent[]) => void };
49
-
50
- const DEDUPE_THRESHOLD = 50;
51
-
52
- class Queue<X> implements AsyncIterator<X>, AsyncIterable<X> {
53
- #queue: X[] = [];
54
- #done = false;
55
- #ready: Promise<void>;
56
- #fire: (() => void);
57
- #onClose: (() => (void | Promise<void>))[] = [];
58
- #recentKeys = new Map<string, number>();
59
-
60
- constructor() {
61
- this.#ready = new Promise(r => this.#fire = r);
62
- }
63
-
64
- // Allow for iteration
65
- [Symbol.asyncIterator](): AsyncIterator<X> { return this; }
66
-
67
- async next(): Promise<IteratorResult<X>> {
68
- while (!this.#done && !this.#queue.length) {
69
- this.#recentKeys = new Map([...this.#recentKeys.entries()] // Cull before waiting
70
- .filter(([, time]) => (Date.now() - time) < DEDUPE_THRESHOLD));
71
- await this.#ready;
72
- this.#ready = new Promise(r => this.#fire = r);
73
- }
74
- return { value: this.#queue.shift()!, done: this.#done };
75
- }
76
-
77
- add(item: X | X[]): void {
78
- const now = Date.now();
79
- for (const value of Array.isArray(item) ? item : [item]) {
80
- const key = JSON.stringify(value);
81
- if ((now - (this.#recentKeys.get(key) ?? 0)) > DEDUPE_THRESHOLD) {
82
- this.#queue.push(value);
83
- this.#recentKeys.set(key, now);
84
- this.#fire();
85
- }
86
- }
87
- }
88
-
89
- registerOnClose(handler: () => (void | Promise<void>)): void {
90
- this.#onClose.push(handler);
91
- }
92
-
93
- async close(): Promise<void> {
94
- this.#done = true;
95
- this.#fire();
96
- await Promise.all(this.#onClose.map(x => x()));
97
- }
98
- }
99
-
100
- /**
101
- * Watch immediate files for a given folder
102
- */
103
- async function watchFolderImmediate(queue: Queue<WatchEvent>, options: WatchFolder): Promise<void> {
104
- const watchPath = path.resolve(options.src);
105
- const watcher = watch(watchPath, { persistent: true, encoding: 'utf8' });
106
- const lastStats: Record<string, Stats | undefined> = {};
107
- const invalidFilter = (el: string): boolean =>
108
- (el === '.' || el === '..' || (!options.includeHidden && el.startsWith('.')) || !!options.ignore?.includes(el));
109
-
110
- for (const el of await fs.readdir(watchPath)) {
111
- if (invalidFilter(el)) {
112
- continue;
113
- }
114
- const file = path.resolve(watchPath, el);
115
- lastStats[file] = await fs.stat(file);
116
- }
117
-
118
- const target = options.target ?? options.src;
119
-
120
- watcher.on('change', async (type: string, file: string): Promise<void> => {
121
- if (invalidFilter(file)) {
122
- return;
123
- }
124
-
125
- file = path.resolve(watchPath, file);
126
-
127
- const stat = await fs.stat(file).catch(() => undefined);
128
- const prevStat = lastStats[file];
129
- lastStats[file] = stat;
130
-
131
- if (prevStat?.mtimeMs === stat?.mtimeMs) {
132
- return;
133
- }
134
- let ev: WatchEvent;
135
- if (prevStat && !stat) {
136
- ev = { action: 'delete', file, folder: target };
137
- } else if (!prevStat && stat) {
138
- ev = { action: 'create', file, folder: target };
139
- } else {
140
- ev = { action: 'update', file, folder: target };
141
- }
142
- if (!options.filter || options.filter(ev)) {
143
- queue.add(ev);
144
- }
145
- });
146
-
147
- queue.registerOnClose(() => watcher.close());
148
- }
149
-
150
- /**
151
- * Watch recursive files for a given folder
152
- */
153
- async function watchFolderRecursive(queue: Queue<WatchEvent>, options: WatchFolder): Promise<void> {
154
- const lib = await getWatcher();
155
- const target = options.target ?? options.src;
156
-
157
- if (await fs.stat(options.src).then(() => true, () => options.createMissing)) {
158
- await fs.mkdir(options.src, { recursive: true });
159
- const ignore = (await fs.readdir(options.src)).filter(x => x.startsWith('.') && x.length > 2);
160
- const cleanup = await lib.subscribe(options.src, async (err, events) => {
161
- if (err) {
162
- process.send?.({ type: 'log', message: `Watch error: ${err}` });
163
- }
164
- for (const ev of events) {
165
- const finalEv = { action: ev.type, file: path.toPosix(ev.path), folder: target };
166
- if (ev.type !== 'delete') {
167
- const stats = await fs.stat(finalEv.file);
168
- if ((stats.ctimeMs - Date.now()) < DEDUPE_THRESHOLD) {
169
- ev.type = 'create'; // Force create on newly stated files
170
- }
171
- }
172
-
173
- if (ev.type === 'delete' && finalEv.file === options.src) {
174
- return queue.close();
175
- }
176
- const isHidden = !options.includeHidden && finalEv.file.replace(target, '').includes('/.');
177
- const matches = !isHidden && (!options.filter || options.filter(finalEv));
178
- if (matches) {
179
- queue.add(finalEv);
180
- }
181
- }
182
- }, { ignore: [...ignore, ...options.ignore ?? []] });
183
- queue.registerOnClose(() => cleanup.unsubscribe());
184
- }
185
- }
186
-
187
- /**
188
- * Watch a series of folders
189
- * @param folders
190
- * @param onEvent
191
- * @param options
192
- */
193
- export function watchFolders(
194
- folders: string[] | WatchFolder[],
195
- config: Omit<WatchFolder, 'src' | 'target'> = {}
196
- ): WatchStream {
197
- const queue = new Queue<WatchEvent>();
198
- for (const folder of folders) {
199
- if (typeof folder === 'string') {
200
- watchFolderRecursive(queue, { ...config, src: folder });
201
- } else if (!folder.immediate) {
202
- watchFolderRecursive(queue, { ...config, ...folder });
203
- } else {
204
- watchFolderImmediate(queue, { ...config, ...folder });
205
- }
206
- }
207
- return queue;
208
- }