@travetto/manifest 3.0.0-rc.6 → 3.0.0-rc.7

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/__index__.ts CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  export * from './src/path';
4
4
  export * from './src/module';
5
+ export * from './src/delta';
5
6
  export * from './src/manifest-index';
6
7
  export * from './src/root-index';
7
- export * from './src/delta';
8
8
  export * from './src/package';
9
- export * from './src/manifest';
10
- export * from './src/types';
9
+ export * from './src/util';
10
+ export * from './src/types';
11
+ export * from './src/watch';
@@ -0,0 +1,10 @@
1
+ import type { ManifestContext } from '../src/types';
2
+
3
+ declare namespace ManifestBootstrap {
4
+ /**
5
+ * Get Context for building
6
+ */
7
+ function getManifestContext(folder?: string): Promise<ManifestContext>;
8
+ }
9
+
10
+ export = ManifestBootstrap;
package/bin/context.js ADDED
@@ -0,0 +1,94 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {import('../src/types').Package} Pkg
5
+ * @typedef {import('../src/types').ManifestContext} ManifestContext
6
+ */
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { createRequire } from 'module';
10
+
11
+ /**
12
+ * Returns the package.json
13
+ * @param {string} inputFolder
14
+ * @returns {Promise<Pkg>}
15
+ */
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'));
21
+ }
22
+
23
+ const WS_ROOT = {};
24
+
25
+ /**
26
+ * Get workspace root
27
+ * @return {Promise<string>}
28
+ */
29
+ async function $getWorkspaceRoot(base = process.cwd()) {
30
+ if (base in WS_ROOT) {
31
+ return WS_ROOT[base];
32
+ }
33
+
34
+ let folder = base;
35
+ let prevFolder = '';
36
+ while (folder !== prevFolder) {
37
+ try {
38
+ const pkg = await $getPkg(folder);
39
+ if (!!pkg.workspaces || !!pkg.travetto?.isolated) {
40
+ return (WS_ROOT[base] = folder);
41
+ }
42
+ } catch { }
43
+ if (await fs.stat(path.resolve(folder, '.git')).catch(() => { })) {
44
+ break;
45
+ }
46
+ prevFolder = folder;
47
+ folder = path.dirname(folder);
48
+ }
49
+ return WS_ROOT[base] = base;
50
+ }
51
+
52
+
53
+ /**
54
+ * Gets build context
55
+ * @param {string} [folder]
56
+ * @return {Promise<ManifestContext>}
57
+ */
58
+ export async function getManifestContext(folder) {
59
+ const workspacePath = path.resolve(await $getWorkspaceRoot(folder));
60
+
61
+ // If manifest specified via env var, and is a package name
62
+ if (!folder && process.env.TRV_MODULE) {
63
+ const req = createRequire(`${workspacePath}/node_modules`);
64
+ try {
65
+ folder = path.dirname(req.resolve(`${process.env.TRV_MODULE}/package.json`));
66
+ } catch {
67
+ const workspacePkg = JSON.parse(await fs.readFile(path.resolve(workspacePath, 'package.json'), 'utf8'));
68
+ if (workspacePkg.name === process.env.TRV_MODULE) {
69
+ folder = workspacePath;
70
+ } else {
71
+ throw new Error(`Unable to resolve location for ${folder}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ const mainPath = path.resolve(folder ?? '.');
77
+ const { name: mainModule, workspaces, travetto } = (await $getPkg(mainPath));
78
+ const monoRepo = workspacePath !== mainPath || !!workspaces;
79
+ const outputFolder = travetto?.outputFolder ?? '.trv_output';
80
+
81
+ const moduleType = (await $getPkg(workspacePath)).type ?? 'commonjs';
82
+ const mainFolder = mainPath === workspacePath ? '' : mainPath.replace(`${workspacePath}/`, '');
83
+
84
+ const res = {
85
+ moduleType,
86
+ mainModule,
87
+ mainFolder,
88
+ workspacePath,
89
+ monoRepo,
90
+ outputFolder,
91
+ compilerFolder: '.trv_compiler'
92
+ };
93
+ return res;
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/manifest",
3
- "version": "3.0.0-rc.6",
3
+ "version": "3.0.0-rc.7",
4
4
  "description": "Manifest support",
5
5
  "keywords": [
6
6
  "path",
@@ -15,8 +15,10 @@
15
15
  "email": "travetto.framework@gmail.com",
16
16
  "name": "Travetto Framework"
17
17
  },
18
+ "type": "module",
18
19
  "files": [
19
20
  "__index__.ts",
21
+ "bin",
20
22
  "src",
21
23
  "support"
22
24
  ],
@@ -28,7 +30,14 @@
28
30
  "url": "https://github.com/travetto/travetto.git",
29
31
  "directory": "module/manifest"
30
32
  },
31
- "dependencies": {},
33
+ "peerDependencies": {
34
+ "@parcel/watcher": "^2.1.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@parcel/watcher": {
38
+ "optional": true
39
+ }
40
+ },
32
41
  "travetto": {
33
42
  "displayName": "Manifest",
34
43
  "docBaseUrl": "https://github.com/travetto/travetto/tree/main"
package/src/delta.ts CHANGED
@@ -1,12 +1,19 @@
1
- import { type Stats } from 'fs';
2
1
  import fs from 'fs/promises';
2
+ import path from 'path';
3
+
3
4
  import {
4
- ManifestDelta, ManifestDeltaEvent, ManifestDeltaModule,
5
- ManifestModule, ManifestModuleFile, ManifestModuleFolderType,
6
- ManifestRoot
5
+ ManifestContext, ManifestModule, ManifestModuleCore, ManifestModuleFile,
6
+ ManifestModuleFileType, ManifestModuleFolderType, ManifestRoot
7
7
  } from './types';
8
8
 
9
+ import { ManifestModuleUtil } from './module';
10
+
11
+ type DeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty';
12
+ type DeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> };
13
+ export type DeltaEvent = { file: string, type: DeltaEventType, module: string };
14
+
9
15
  const VALID_SOURCE_FOLDERS = new Set<ManifestModuleFolderType>(['bin', 'src', 'test', 'support', '$index', '$package', 'doc']);
16
+ const VALID_SOURCE_TYPE = new Set<ManifestModuleFileType>(['js', 'ts', 'package-json']);
10
17
 
11
18
  /**
12
19
  * Produce delta for the manifest
@@ -20,50 +27,37 @@ export class ManifestDeltaUtil {
20
27
  /**
21
28
  * Produce delta between two manifest modules, relative to an output folder
22
29
  */
23
- static async #deltaModules(outputFolder: string, left: ManifestDeltaModule, right: ManifestDeltaModule): Promise<ManifestDeltaEvent[]> {
24
- const out: ManifestDeltaEvent[] = [];
25
- const getStat = (f: string): Promise<Stats | void> =>
26
- fs.stat(`${outputFolder}/${left.output}/${f.replace(/[.]ts$/, '.js')}`).catch(() => { });
30
+ static async #deltaModules(ctx: ManifestContext, outputFolder: string, left: DeltaModule): Promise<DeltaEvent[]> {
31
+ const out: DeltaEvent[] = [];
27
32
 
28
- const add = (file: string, type: ManifestDeltaEvent['type']): unknown =>
33
+ const add = (file: string, type: DeltaEvent['type']): unknown =>
29
34
  out.push({ file, module: left.name, type });
30
35
 
36
+ const root = `${ctx.workspacePath}/${ctx.outputFolder}/${left.outputFolder}`;
37
+ const right = new Set(
38
+ (await ManifestModuleUtil.scanFolder(root, left.main))
39
+ .filter(x => (x.endsWith('.ts') || x.endsWith('.js') || x.endsWith('package.json')))
40
+ .map(x => x.replace(`${root}/`, '').replace(/[.][tj]s$/, ''))
41
+ );
42
+
31
43
  for (const el of Object.keys(left.files)) {
32
- if (!(el in right.files)) {
33
- const [, , leftTs] = left.files[el];
34
- const stat = await getStat(el);
35
- if (stat && leftTs < this.#getNewest(stat)) {
36
- // If file pre-exists manifest, be cool
37
- continue;
38
- }
44
+ const output = `${outputFolder}/${left.outputFolder}/${el.replace(/[.]ts$/, '.js')}`;
45
+ const [, , leftTs] = left.files[el];
46
+ const stat = await fs.stat(output).catch(() => { });
47
+ right.delete(el.replace(/[.][tj]s$/, ''));
48
+
49
+ if (!stat) {
39
50
  add(el, 'added');
40
51
  } else {
41
- const [, , leftTs] = left.files[el];
42
- const [, , rightTs] = right.files[el];
43
- if (leftTs !== rightTs) {
44
- const stat = await getStat(el);
45
- if (!stat) {
46
- add(el, 'missing');
47
- } else if (leftTs > this.#getNewest(stat!)) {
48
- add(el, 'changed');
49
- }
50
- } else {
51
- const stat = await getStat(el);
52
- if (!stat) {
53
- add(el, 'missing');
54
- } else if (this.#getNewest(stat!) < leftTs) {
55
- add(el, 'dirty');
56
- }
52
+ const rightTs = this.#getNewest(stat);
53
+ if (leftTs > rightTs) {
54
+ add(el, 'changed');
57
55
  }
58
56
  }
59
57
  }
60
- for (const el of Object.keys(right.files)) {
61
- if (!(el in left.files)) {
62
- const stat = await getStat(el);
63
- if (stat) {
64
- add(el, 'removed');
65
- }
66
- }
58
+ // Deleted
59
+ for (const el of right) {
60
+ add(el, 'removed');
67
61
  }
68
62
  return out;
69
63
  }
@@ -81,7 +75,7 @@ export class ManifestDeltaUtil {
81
75
  continue;
82
76
  }
83
77
  for (const [name, type, date] of m.files?.[key] ?? []) {
84
- if (type === 'ts' || type === 'js' || type === 'package-json') {
78
+ if (VALID_SOURCE_TYPE.has(type)) {
85
79
  out[name] = [name, type, date];
86
80
  }
87
81
  }
@@ -90,23 +84,19 @@ export class ManifestDeltaUtil {
90
84
  }
91
85
 
92
86
  /**
93
- * Produce delta between ttwo manifest roots, relative to a single output folder
87
+ * Produce delta between manifest root and the output folder
94
88
  */
95
- static async produceDelta(outputFolder: string, left: ManifestRoot, right: ManifestRoot): Promise<ManifestDelta> {
89
+ static async produceDelta(ctx: ManifestContext, manifest: ManifestRoot): Promise<DeltaEvent[]> {
96
90
  const deltaLeft = Object.fromEntries(
97
- Object.values(left.modules)
98
- .map(m => [m.name, { ...m, files: this.#flattenModuleFiles(m) }])
99
- );
100
-
101
- const deltaRight = Object.fromEntries(
102
- Object.values(right.modules)
91
+ Object.values(manifest.modules)
103
92
  .map(m => [m.name, { ...m, files: this.#flattenModuleFiles(m) }])
104
93
  );
105
94
 
106
- const out: Record<string, ManifestDeltaEvent[]> = {};
95
+ const out: DeltaEvent[] = [];
96
+ const outputFolder = path.resolve(ctx.workspacePath, ctx.outputFolder);
107
97
 
108
- for (const [name, lMod] of Object.entries(deltaLeft)) {
109
- out[name] = await this.#deltaModules(outputFolder, lMod, deltaRight[name] ?? { files: {}, name });
98
+ for (const lMod of Object.values(deltaLeft)) {
99
+ out.push(...await this.#deltaModules(manifest, outputFolder, lMod));
110
100
  }
111
101
 
112
102
  return out;
@@ -49,7 +49,7 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
49
49
  async init(req: PackageVisitReq<ModuleDep>): Promise<PackageVisitReq<ModuleDep>[]> {
50
50
  const pkg = PackageUtil.readPackage(req.sourcePath);
51
51
  const workspacePkg = PackageUtil.readPackage(this.ctx.workspacePath);
52
- const workspaceModules = pkg.workspaces?.length ? (await PackageUtil.resolveWorkspaces(req.sourcePath)) : [];
52
+ const workspaceModules = pkg.workspaces?.length ? (await PackageUtil.resolveWorkspaces(this.ctx, req.sourcePath)) : [];
53
53
 
54
54
  this.#mainPatterns = ModuleDependencyVisitor.getMainPatterns(pkg.name, [
55
55
  ...pkg.travetto?.mainSource ?? [],
@@ -1,5 +1,3 @@
1
- import fs from 'fs';
2
-
3
1
  import { path } from './path';
4
2
 
5
3
  import {
@@ -7,6 +5,8 @@ import {
7
5
  ManifestModuleFileType, ManifestModuleFolderType, ManifestProfile, ManifestRoot
8
6
  } from './types';
9
7
 
8
+ import { ManifestUtil } from './util';
9
+
10
10
  type ScanTest = ((full: string) => boolean) | { test: (full: string) => boolean };
11
11
  export type FindConfig = {
12
12
  folders?: ManifestModuleFolderType[];
@@ -20,16 +20,17 @@ export type IndexedFile = {
20
20
  id: string;
21
21
  import: string;
22
22
  module: string;
23
- source: string;
24
- output: string;
25
- relative: string;
23
+ sourceFile: string;
24
+ outputFile: string;
25
+ relativeFile: string;
26
26
  profile: ManifestProfile;
27
27
  type: ManifestModuleFileType;
28
28
  };
29
29
 
30
30
  export type IndexedModule = ManifestModuleCore & {
31
+ sourcePath: string;
32
+ outputPath: string;
31
33
  files: Record<ManifestModuleFolderType, IndexedFile[]>;
32
- workspaceRelative: string;
33
34
  };
34
35
 
35
36
  /**
@@ -42,50 +43,51 @@ export class ManifestIndex {
42
43
  #modules: IndexedModule[];
43
44
  #modulesByName: Record<string, IndexedModule> = {};
44
45
  #modulesByFolder: Record<string, IndexedModule> = {};
45
- #root: string;
46
+ #outputRoot: string;
46
47
  #outputToEntry = new Map<string, IndexedFile>();
47
48
  #sourceToEntry = new Map<string, IndexedFile>();
48
49
  #importToEntry = new Map<string, IndexedFile>();
49
50
 
50
- constructor(root: string, manifest: string | ManifestRoot) {
51
- this.init(root, manifest);
51
+ constructor(manifest: string) {
52
+ this.init(manifest);
52
53
  }
53
54
 
54
55
  #resolveOutput(...parts: string[]): string {
55
- return path.resolve(this.#root, ...parts);
56
+ return path.resolve(this.#outputRoot, ...parts);
56
57
  }
57
58
 
58
59
  get manifest(): ManifestRoot {
59
60
  return this.#manifest;
60
61
  }
61
62
 
62
- get root(): string {
63
- return this.#root;
63
+ get outputRoot(): string {
64
+ return this.#outputRoot;
64
65
  }
65
66
 
66
67
  get manifestFile(): string {
67
68
  return this.#manifestFile;
68
69
  }
69
70
 
70
- init(root: string, manifestInput: string | ManifestRoot): void {
71
- this.#root = root;
72
- this.#manifestFile = typeof manifestInput === 'string' ? manifestInput : manifestInput.manifestFile;
73
- this.#manifest = typeof manifestInput === 'string' ? JSON.parse(fs.readFileSync(this.#manifestFile, 'utf8')) : manifestInput;
71
+ init(manifestInput: string): void {
72
+ const { manifest, file } = ManifestUtil.readManifestSync(manifestInput);
73
+ this.#manifest = manifest;
74
+ this.#manifestFile = file;
75
+ this.#outputRoot = path.resolve(this.#manifest.workspacePath, this.#manifest.outputFolder);
74
76
  this.#index();
75
77
  }
76
78
 
77
79
  #moduleFiles(m: ManifestModule, files: ManifestModuleFile[]): IndexedFile[] {
78
80
  return files.map(([f, type, ts, profile = 'std']) => {
79
- const source = path.join(m.source, f);
81
+ const sourceFile = path.resolve(this.#manifest.workspacePath, m.sourceFolder, f);
80
82
  const js = (type === 'ts' ? f.replace(/[.]ts$/, '.js') : f);
81
- const output = this.#resolveOutput(m.output, js);
83
+ const outputFile = this.#resolveOutput(m.outputFolder, js);
82
84
  const modImport = `${m.name}/${js}`;
83
85
  let id = modImport.replace(`${m.name}/`, _ => _.replace(/[/]$/, ':'));
84
86
  if (type === 'ts' || type === 'js') {
85
87
  id = id.replace(/[.]js$/, '');
86
88
  }
87
89
 
88
- return { id, type, source, output, import: modImport, profile, relative: f, module: m.name };
90
+ return { id, type, sourceFile, outputFile, import: modImport, profile, relativeFile: f, module: m.name };
89
91
  });
90
92
  }
91
93
 
@@ -100,8 +102,8 @@ export class ManifestIndex {
100
102
  this.#modules = Object.values(this.manifest.modules)
101
103
  .map(m => ({
102
104
  ...m,
103
- output: this.#resolveOutput(m.output),
104
- workspaceRelative: m.source.replace(`${this.#manifest.workspacePath}/`, ''),
105
+ outputPath: this.#resolveOutput(m.outputFolder),
106
+ sourcePath: path.resolve(this.manifest.workspacePath, m.sourceFolder),
105
107
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
106
108
  files: Object.fromEntries(
107
109
  Object.entries(m.files).map(([folder, files]) => [folder, this.#moduleFiles(m, files ?? [])])
@@ -111,15 +113,16 @@ export class ManifestIndex {
111
113
  for (const mod of this.#modules) {
112
114
  for (const files of Object.values(mod.files ?? {})) {
113
115
  for (const entry of files) {
114
- this.#outputToEntry.set(entry.output, entry);
115
- this.#sourceToEntry.set(entry.source, entry);
116
+ this.#outputToEntry.set(entry.outputFile, entry);
117
+ this.#sourceToEntry.set(entry.sourceFile, entry);
116
118
  this.#importToEntry.set(entry.import, entry);
117
119
  this.#importToEntry.set(entry.import.replace(/[.]js$/, ''), entry);
120
+ this.#importToEntry.set(entry.import.replace(/[.]js$/, '.ts'), entry);
118
121
  }
119
122
  }
120
123
  }
121
124
  this.#modulesByName = Object.fromEntries(this.#modules.map(x => [x.name, x]));
122
- this.#modulesByFolder = Object.fromEntries(this.#modules.map(x => [x.workspaceRelative, x]));
125
+ this.#modulesByFolder = Object.fromEntries(this.#modules.map(x => [x.sourceFolder, x]));
123
126
  }
124
127
 
125
128
  /**
@@ -166,7 +169,7 @@ export class ManifestIndex {
166
169
 
167
170
  return searchSpace
168
171
  .filter(({ type }) => type === 'ts')
169
- .filter(({ source }) => filter?.(source) ?? true);
172
+ .filter(({ sourceFile: source }) => filter?.(source) ?? true);
170
173
  }
171
174
 
172
175
  /**
@@ -218,8 +221,7 @@ export class ManifestIndex {
218
221
  * Resolve import
219
222
  */
220
223
  resolveFileImport(name: string): string {
221
- name = !name.endsWith('.d.ts') ? name.replace(/[.]ts$/, '.js') : name;
222
- return this.#importToEntry.get(name)?.output ?? name;
224
+ return this.#importToEntry.get(name)?.outputFile ?? name;
223
225
  }
224
226
 
225
227
  /**
@@ -247,6 +249,14 @@ export class ManifestIndex {
247
249
  return name ? this.getModule(name) : undefined;
248
250
  }
249
251
 
252
+ /**
253
+ * Get module from import name
254
+ * @param importName
255
+ */
256
+ getModuleFromImport(importName: string): IndexedModule | undefined {
257
+ const name = this.getFromImport(importName)?.module;
258
+ return name ? this.getModule(name) : undefined;
259
+ }
250
260
  /**
251
261
  * Build module list from an expression list (e.g. `@travetto/app,-@travetto/log)
252
262
  */
package/src/module.ts CHANGED
@@ -29,6 +29,8 @@ const INDEX_FILES = new Set([
29
29
 
30
30
  export class ManifestModuleUtil {
31
31
 
32
+ static #scanCache: Record<string, string[]> = {};
33
+
32
34
  static #getNewest(stat: { mtimeMs: number, ctimeMs: number }): number {
33
35
  return Math.max(stat.mtimeMs, stat.ctimeMs);
34
36
  }
@@ -36,8 +38,19 @@ export class ManifestModuleUtil {
36
38
  /**
37
39
  * Simple file scanning
38
40
  */
39
- static async #scanFolder(folder: string, topFolders = new Set<string>(), topFiles = new Set<string>()): Promise<string[]> {
41
+ static async scanFolder(folder: string, mainSource = false): Promise<string[]> {
42
+ if (!mainSource && folder in this.#scanCache) {
43
+ return this.#scanCache[folder];
44
+ }
45
+
46
+ if (!await fs.stat(folder).catch(() => false)) {
47
+ return [];
48
+ }
49
+
50
+ const topFolders = new Set(mainSource ? [] : ['src', 'bin', 'support']);
51
+ const topFiles = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
40
52
  const out: string[] = [];
53
+
41
54
  if (!fs.stat(folder).catch(() => false)) {
42
55
  return out;
43
56
  }
@@ -68,6 +81,11 @@ export class ManifestModuleUtil {
68
81
  }
69
82
  }
70
83
  }
84
+
85
+ if (!mainSource) {
86
+ this.#scanCache[folder] = out;
87
+ }
88
+
71
89
  return out;
72
90
  }
73
91
 
@@ -118,6 +136,8 @@ export class ManifestModuleUtil {
118
136
  return 'support/fixtures';
119
137
  } else if (moduleFile.startsWith('support/resources/')) {
120
138
  return 'support/resources';
139
+ } else if (moduleFile.startsWith('support/transform')) {
140
+ return '$transformer';
121
141
  }
122
142
  const key = moduleFile.substring(0, folderLocation);
123
143
  switch (key) {
@@ -152,13 +172,12 @@ export class ManifestModuleUtil {
152
172
  /**
153
173
  * Visit a module and describe files, and metadata
154
174
  */
155
- static async describeModule(dep: ModuleDep): Promise<ManifestModule> {
175
+ static async describeModule(ctx: ManifestContext, dep: ModuleDep): Promise<ManifestModule> {
156
176
  const { main, mainSource, local, name, version, sourcePath, profileSet, parentSet, internal } = dep;
177
+
157
178
  const files: ManifestModule['files'] = {};
158
- const folderSet = new Set<ManifestModuleFolderType>(mainSource ? [] : ['src', 'bin', 'support']);
159
- const fileSet = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
160
179
 
161
- for (const file of await this.#scanFolder(sourcePath, folderSet, fileSet)) {
180
+ for (const file of await this.scanFolder(sourcePath, mainSource)) {
162
181
  // Group by top folder
163
182
  const moduleFile = file.replace(`${sourcePath}/`, '');
164
183
  const entry = await this.transformFile(moduleFile, file);
@@ -173,9 +192,11 @@ export class ManifestModuleUtil {
173
192
 
174
193
  const profiles = [...profileSet].sort();
175
194
  const parents = [...parentSet].sort();
176
- const output = `node_modules/${name}`;
195
+ const outputFolder = `node_modules/${name}`;
196
+ const sourceFolder = sourcePath === ctx.workspacePath ? '' : sourcePath.replace(`${ctx.workspacePath}/`, '');
177
197
 
178
- return { main, name, version, local, internal, source: sourcePath, output, files, profiles, parents, };
198
+ const res = { main, name, version, local, internal, sourceFolder, outputFolder, files, profiles, parents, };
199
+ return res;
179
200
  }
180
201
 
181
202
  /**
@@ -183,10 +204,10 @@ export class ManifestModuleUtil {
183
204
  */
184
205
  static async produceModules(ctx: ManifestContext): Promise<Record<string, ManifestModule>> {
185
206
  const visitor = new ModuleDependencyVisitor(ctx);
186
- const declared = await PackageUtil.visitPackages(ctx.mainPath, visitor);
207
+ const mainPath = path.resolve(ctx.workspacePath, ctx.mainFolder);
208
+ const declared = await PackageUtil.visitPackages(mainPath, visitor);
187
209
  const sorted = [...declared].sort((a, b) => a.name.localeCompare(b.name));
188
-
189
- const modules = await Promise.all(sorted.map(x => this.describeModule(x)));
210
+ const modules = await Promise.all(sorted.map(x => this.describeModule(ctx, x)));
190
211
  return Object.fromEntries(modules.map(m => [m.name, m]));
191
212
  }
192
213
  }
package/src/package.ts CHANGED
@@ -3,12 +3,12 @@ import fs from 'fs/promises';
3
3
  import { createRequire } from 'module';
4
4
  import { execSync } from 'child_process';
5
5
 
6
- import { Package, PackageDigest, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
6
+ import { ManifestContext, Package, PackageDigest, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
7
7
  import { path } from './path';
8
8
 
9
9
  export class PackageUtil {
10
10
 
11
- static #req = createRequire(`${path.cwd()}/node_modules`);
11
+ static #req = createRequire(path.resolve('node_modules'));
12
12
  static #framework: Package;
13
13
  static #cache: Record<string, Package> = {};
14
14
  static #workspaces: Record<string, PackageWorkspaceEntry[]> = {};
@@ -164,11 +164,18 @@ export class PackageUtil {
164
164
  /**
165
165
  * Find workspace values from rootPath
166
166
  */
167
- static resolveWorkspaces(rootPath: string): PackageWorkspaceEntry[] {
167
+ static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> {
168
168
  if (!this.#workspaces[rootPath]) {
169
- const text = execSync('npm query .workspace', { cwd: rootPath, encoding: 'utf8', env: {} });
170
- const res: { location: string, name: string }[] = JSON.parse(text);
171
- this.#workspaces[rootPath] = res.map(d => ({ sourcePath: d.location, name: d.name }));
169
+ await fs.mkdir(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true });
170
+ const cache = path.resolve(ctx.workspacePath, ctx.outputFolder, 'workspaces.json');
171
+ try {
172
+ return JSON.parse(await fs.readFile(cache, 'utf8'));
173
+ } catch {
174
+ const text = execSync('npm query .workspace', { cwd: rootPath, encoding: 'utf8', env: {} });
175
+ const res: { location: string, name: string }[] = JSON.parse(text);
176
+ const out = this.#workspaces[rootPath] = res.map(d => ({ sourcePath: d.location, name: d.name }));
177
+ await fs.writeFile(cache, JSON.stringify(out), 'utf8');
178
+ }
172
179
  }
173
180
  return this.#workspaces[rootPath];
174
181
  }
package/src/root-index.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { fileURLToPath } from 'url';
2
- import { createRequire } from 'module';
3
-
4
1
  import { path } from './path';
5
2
  import { IndexedModule, ManifestIndex } from './manifest-index';
6
3
  import { FunctionMetadata, Package, PackageDigest } from './types';
@@ -13,40 +10,16 @@ type Metadated = { [METADATA]: FunctionMetadata };
13
10
  * Extended manifest index geared for application execution
14
11
  */
15
12
  class $RootIndex extends ManifestIndex {
16
- /**
17
- * Load all source modules
18
- */
19
- static resolveManifestJSON(root: string, file?: string): string {
20
- file = file || path.resolve(root, 'manifest.json');
21
-
22
- // IF not a file
23
- if (!file.endsWith('.json')) {
24
- try {
25
- // Try to resolve
26
- const req = createRequire(path.resolve(root, 'node_modules'));
27
- file = req.resolve(`${file}/manifest.json`);
28
- } catch {
29
- // Fallback to assumed node_modules pattern
30
- file = `${root}/node_modules/${file}/manifest.json`;
31
- }
32
- }
33
- return file;
34
- }
35
13
 
36
14
  #config: Package | undefined;
37
- #srcCache = new Map();
38
15
  #metadata = new Map<string, FunctionMetadata>();
39
16
 
40
- constructor(output: string = process.env.TRV_OUTPUT ?? process.cwd()) {
41
- super(output, $RootIndex.resolveManifestJSON(output, process.env.TRV_MANIFEST));
42
- }
43
-
44
17
  /**
45
18
  * **WARNING**: This is a destructive operation, and should only be called before loading any code
46
19
  * @private
47
20
  */
48
21
  reinitForModule(module: string): void {
49
- this.init(this.root, $RootIndex.resolveManifestJSON(this.root, module));
22
+ this.init(`${this.outputRoot}/node_modules/${module}`);
50
23
  this.#config = undefined;
51
24
  }
52
25
 
@@ -54,15 +27,15 @@ class $RootIndex extends ManifestIndex {
54
27
  * Determines if the manifest root is the root for a monorepo
55
28
  */
56
29
  isMonoRepoRoot(): boolean {
57
- return !!this.manifest.monoRepo && this.manifest.workspacePath === this.manifest.mainPath;
30
+ return !!this.manifest.monoRepo && !this.manifest.mainFolder;
58
31
  }
59
32
 
60
33
  /**
61
34
  * Asynchronously load all source files from manifest
62
35
  */
63
36
  async loadSource(): Promise<void> {
64
- for (const { output } of this.findSrc()) {
65
- await import(output);
37
+ for (const { import: imp } of this.findSrc()) {
38
+ await import(imp);
66
39
  }
67
40
  }
68
41
 
@@ -87,14 +60,14 @@ class $RootIndex extends ManifestIndex {
87
60
  */
88
61
  get mainPackage(): Package {
89
62
  if (!this.#config) {
90
- const { output: mainFolder } = this.getModule(this.manifest.mainModule)!;
63
+ const { outputPath } = this.getModule(this.manifest.mainModule)!;
91
64
  this.#config = {
92
65
  ...{
93
66
  name: 'untitled',
94
67
  description: 'A Travetto application',
95
68
  version: '0.0.0',
96
69
  },
97
- ...PackageUtil.readPackage(mainFolder)
70
+ ...PackageUtil.readPackage(outputPath)
98
71
  };
99
72
  }
100
73
  return this.#config;
@@ -105,20 +78,11 @@ class $RootIndex extends ManifestIndex {
105
78
  }
106
79
 
107
80
  /**
108
- * Get source file from output location
81
+ * Get source file from import location
109
82
  * @param outputFile
110
83
  */
111
- getSourceFile(file: string): string {
112
- if (!this.#srcCache.has(file)) {
113
- if (file.startsWith('file:')) {
114
- this.#srcCache.set(file, path.toPosix(fileURLToPath(file)));
115
- } else {
116
- this.#srcCache.set(file, path.toPosix(file));
117
- }
118
- }
119
-
120
- const outputFile = this.#srcCache.get(file)!;
121
- return this.getEntry(outputFile)?.source ?? outputFile;
84
+ getSourceFile(importFile: string): string {
85
+ return this.getFromImport(importFile)?.sourceFile ?? importFile;
122
86
  }
123
87
 
124
88
  /**
@@ -129,8 +93,8 @@ class $RootIndex extends ManifestIndex {
129
93
  * @param `methods` Methods and their hashes
130
94
  * @param `abstract` Is the class abstract
131
95
  */
132
- registerFunction(cls: Function, file: string, hash: number, methods?: Record<string, { hash: number }>, abstract?: boolean, synthetic?: boolean): boolean {
133
- const source = this.getSourceFile(file);
96
+ registerFunction(cls: Function, fileOrImport: string, hash: number, methods?: Record<string, { hash: number }>, abstract?: boolean, synthetic?: boolean): boolean {
97
+ const source = this.getSourceFile(fileOrImport);
134
98
  const id = this.getId(source, cls.name);
135
99
  Object.defineProperty(cls, 'Ⲑid', { value: id });
136
100
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -154,14 +118,32 @@ class $RootIndex extends ManifestIndex {
154
118
  const id = clsId === undefined ? '' : typeof clsId === 'string' ? clsId : clsId.Ⲑid;
155
119
  return this.#metadata.get(id);
156
120
  }
121
+
122
+ /**
123
+ * Get local folders that represent the user's controlled input
124
+ */
125
+ getLocalInputFolders(): string[] {
126
+ return this.getLocalModules()
127
+ .flatMap(x =>
128
+ (!this.manifest.monoRepo || x.sourcePath !== this.manifest.workspacePath) ?
129
+ [x.sourcePath] : [...Object.keys(x.files)].filter(y => !y.startsWith('$')).map(y => path.resolve(x.sourcePath, y))
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Get local output folders
135
+ */
136
+ getLocalOutputFolders(): string[] {
137
+ return this.getLocalModules().map(x => x.outputPath);
138
+ }
157
139
  }
158
140
 
159
141
  let index: $RootIndex | undefined;
160
142
 
161
143
  try {
162
- index = new $RootIndex();
144
+ index = new $RootIndex(process.env.TRV_MANIFEST!);
163
145
  } catch (err) {
164
- if (process.env.TRV_THROW_ROOT_INDEX_ERR) {
146
+ if (/prod/i.test(process.env.NODE_ENV ?? '')) {
165
147
  throw err;
166
148
  }
167
149
  }
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@ export type ManifestModuleFolderType =
3
3
  '$root' | '$index' | '$package' |
4
4
  'src' | 'bin' | 'support' | 'resources' | 'test' | 'doc' |
5
5
  'test/fixtures' | 'support/fixtures' | 'support/resources' |
6
- '$other';
6
+ '$other' | '$transformer';
7
7
 
8
8
  export type ManifestProfile = 'compile' | 'test' | 'doc' | 'build' | 'std';
9
9
  export type PackageRel = 'dev' | 'prod' | 'peer' | 'opt' | 'root' | 'global';
@@ -14,8 +14,8 @@ export type ManifestModuleCore = {
14
14
  main?: boolean;
15
15
  local?: boolean;
16
16
  version: string;
17
- source: string;
18
- output: string;
17
+ sourceFolder: string;
18
+ outputFolder: string;
19
19
  profiles: ManifestProfile[];
20
20
  parents: string[];
21
21
  internal?: boolean;
@@ -26,13 +26,13 @@ export type ManifestModule = ManifestModuleCore & {
26
26
  };
27
27
 
28
28
  export type ManifestContext = {
29
- manifestFile: string;
30
29
  mainModule: string;
31
- mainPath: string;
30
+ mainFolder: string;
32
31
  workspacePath: string;
33
32
  outputFolder: string;
34
33
  compilerFolder: string;
35
34
  monoRepo?: boolean;
35
+ moduleType: 'module' | 'commonjs';
36
36
  };
37
37
 
38
38
  export type ManifestRoot = ManifestContext & {
@@ -40,15 +40,6 @@ export type ManifestRoot = ManifestContext & {
40
40
  modules: Record<string, ManifestModule>;
41
41
  };
42
42
 
43
- export type ManifestDeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty';
44
- export type ManifestDeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> };
45
- export type ManifestDeltaEvent = { file: string, type: ManifestDeltaEventType, module: string };
46
- export type ManifestDelta = Record<string, ManifestDeltaEvent[]>;
47
- export type ManifestState = {
48
- manifest: ManifestRoot;
49
- delta: ManifestDelta;
50
- };
51
-
52
43
  export type Package = {
53
44
  name: string;
54
45
  type?: 'module' | 'commonjs';
@@ -116,4 +107,4 @@ export type FunctionMetadata = {
116
107
  methods?: Record<string, { hash: number }>;
117
108
  synthetic?: boolean;
118
109
  abstract?: boolean;
119
- };
110
+ };
package/src/util.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { readFileSync } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import os from 'os';
4
+
5
+ import { path } from './path';
6
+ import { ManifestContext, ManifestRoot } from './types';
7
+ import { ManifestModuleUtil } from './module';
8
+
9
+ export const MANIFEST_FILE = 'manifest.json';
10
+
11
+ /**
12
+ * Manifest utils
13
+ */
14
+ export class ManifestUtil {
15
+
16
+ static async writeJsonWithBuffer(ctx: ManifestContext, filename: string, obj: object): Promise<string> {
17
+ const tempName = path.resolve(ctx.workspacePath, ctx.mainFolder, filename).replace(/[\/\\: ]/g, '_');
18
+ const file = path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule, filename);
19
+ await fs.mkdir(path.dirname(file), { recursive: true });
20
+ const temp = path.resolve(os.tmpdir(), `${tempName}.${Date.now()}`);
21
+ await fs.writeFile(temp, JSON.stringify(obj), 'utf8');
22
+ await fs.copyFile(temp, file);
23
+ fs.unlink(temp);
24
+ return file;
25
+ }
26
+
27
+ /**
28
+ * Build a manifest context
29
+ * @param folder
30
+ */
31
+ static async buildContext(folder?: string): Promise<ManifestContext> {
32
+ const { getManifestContext } = await import('../bin/context.js');
33
+ return getManifestContext(folder);
34
+ }
35
+
36
+ /**
37
+ * Produce manifest in memory
38
+ */
39
+ static async buildManifest(ctx: ManifestContext): Promise<ManifestRoot> {
40
+ return {
41
+ modules: await ManifestModuleUtil.produceModules(ctx),
42
+ generated: Date.now(),
43
+ ...ctx
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Read manifest, synchronously
49
+ *
50
+ * @param file
51
+ * @returns
52
+ */
53
+ static readManifestSync(file: string): { manifest: ManifestRoot, file: string } {
54
+ file = path.resolve(file);
55
+ if (!file.endsWith('.json')) {
56
+ file = path.resolve(file, MANIFEST_FILE);
57
+ }
58
+ const manifest: ManifestRoot = JSON.parse(readFileSync(file, 'utf8'));
59
+ if (!manifest.outputFolder) {
60
+ manifest.outputFolder = path.cwd();
61
+ manifest.workspacePath = path.cwd();
62
+ }
63
+ return { manifest, file };
64
+ }
65
+
66
+ /**
67
+ * Write manifest for a given context, return location
68
+ */
69
+ static writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<string> {
70
+ return this.writeJsonWithBuffer(ctx, MANIFEST_FILE, manifest);
71
+ }
72
+
73
+ /**
74
+ * Rewrite manifest for a given folder
75
+ */
76
+ static async rewriteManifest(source: string): Promise<void> {
77
+ const subCtx = await this.buildContext(source);
78
+ const subManifest = await this.buildManifest(subCtx);
79
+ await this.writeManifest(subCtx, subManifest);
80
+ }
81
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,48 @@
1
+ import fs from 'fs/promises';
2
+
3
+ export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string };
4
+
5
+ type EventListener = (ev: WatchEvent, folder: string) => void;
6
+ type EventFilter = (ev: WatchEvent) => boolean;
7
+ type WatchConfig = { filter?: EventFilter, ignore?: string[] };
8
+
9
+ async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
10
+ try {
11
+ return await import('@parcel/watcher');
12
+ } catch (err) {
13
+ console.error('@parcel/watcher must be installed to use watching functionality');
14
+ throw err;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Leverages @parcel/watcher to watch a series of folders
20
+ * @param folders
21
+ * @param onEvent
22
+ * @private
23
+ */
24
+ export async function watchFolders(folders: string[], onEvent: EventListener, config: WatchConfig = {}): Promise<() => Promise<void>> {
25
+ const lib = await getWatcher();
26
+ const subs = await Promise.all(folders.map(async folder => {
27
+ if (await fs.stat(folder).then(() => true, () => false)) {
28
+ const ignore = (await fs.readdir(folder)).filter(x => x.startsWith('.') && x.length > 2);
29
+ return lib.subscribe(folder, (err, events) => {
30
+ for (const ev of events) {
31
+ const finalEv = { action: ev.type, file: ev.path };
32
+ if (!config.filter || config.filter(finalEv)) {
33
+ onEvent(finalEv, folder);
34
+ }
35
+ }
36
+ }, { ignore: [...ignore, ...config.ignore ?? []] });
37
+ }
38
+ }));
39
+
40
+ // Allow for multiple calls
41
+ let finalProm: Promise<void> | undefined;
42
+ const remove = (): Promise<void> => finalProm ??= Promise.all(subs.map(x => x?.unsubscribe())).then(() => { });
43
+
44
+ // Cleanup on exit
45
+ process.on('exit', remove);
46
+
47
+ return remove;
48
+ }
package/src/manifest.ts DELETED
@@ -1,110 +0,0 @@
1
- import fs from 'fs/promises';
2
- import os from 'os';
3
-
4
- import { path } from './path';
5
- import { ManifestContext, ManifestRoot, ManifestState } from './types';
6
-
7
- import { ManifestModuleUtil } from './module';
8
- import { ManifestDeltaUtil } from './delta';
9
-
10
- /**
11
- * Manifest utils
12
- */
13
- export class ManifestUtil {
14
-
15
- /**
16
- * Produce manifest in memory
17
- */
18
- static async buildManifest(ctx: ManifestContext): Promise<ManifestRoot> {
19
- return {
20
- modules: await ManifestModuleUtil.produceModules(ctx),
21
- generated: Date.now(),
22
- ...ctx
23
- };
24
- }
25
-
26
- /**
27
- * Generate manifest for a given context, and persist
28
- */
29
- static async createAndWriteManifest(ctx: ManifestContext): Promise<void> {
30
- const manifest = await this.buildManifest(ctx);
31
- await this.writeManifest(ctx, manifest);
32
- }
33
-
34
- /**
35
- * Read manifest from a folder
36
- */
37
- static async readManifest(ctx: ManifestContext): Promise<ManifestRoot> {
38
- const file = path.resolve(ctx.workspacePath, ctx.outputFolder, ctx.manifestFile);
39
- if (await fs.stat(file).catch(() => false)) {
40
- try {
41
- return JSON.parse(await fs.readFile(file, 'utf8'));
42
- } catch (err) {
43
- await fs.unlink(file).catch(() => { });
44
- const final = new Error(`Corrupted manifest ${ctx.manifestFile}: file has been removed, retry`);
45
- if (err instanceof Error) {
46
- final.stack = [final.message, ...(err.stack ?? '').split('\n').slice(1)].join('\n');
47
- }
48
- throw final;
49
- }
50
- } else {
51
- return {
52
- modules: {},
53
- generated: Date.now(),
54
- ...ctx,
55
- };
56
- }
57
- }
58
-
59
- /**
60
- * Load state from disk
61
- */
62
- static async readState(file: string): Promise<ManifestState> {
63
- return JSON.parse(await fs.readFile(file, 'utf8'));
64
- }
65
-
66
- /**
67
- * Persist state to disk in a temp file, return said temp file
68
- */
69
- static async writeState(state: ManifestState, file?: string): Promise<string> {
70
- const manifestTemp = file ?? path.resolve(os.tmpdir(), `manifest-state.${Date.now()}${Math.random()}.json`);
71
- await fs.writeFile(manifestTemp, JSON.stringify(state), 'utf8');
72
- return manifestTemp;
73
- }
74
-
75
- /**
76
- * Generate the manifest and delta as a single output
77
- */
78
- static async produceState(ctx: ManifestContext): Promise<ManifestState> {
79
- const manifest = await this.buildManifest(ctx);
80
- const oldManifest = await this.readManifest(ctx);
81
- const delta = await ManifestDeltaUtil.produceDelta(
82
- path.resolve(manifest.workspacePath, ctx.outputFolder),
83
- manifest,
84
- oldManifest
85
- );
86
- return { manifest, delta };
87
- }
88
-
89
- /**
90
- * Resolves a module file, from a context and manifest
91
- */
92
- static resolveFile(ctx: ManifestContext, manifest: ManifestRoot, module: string, file: string): string {
93
- return path.resolve(
94
- ctx.workspacePath,
95
- ctx.compilerFolder,
96
- manifest.modules[module].output,
97
- file
98
- );
99
- }
100
-
101
- /**
102
- * Write manifest for a given context
103
- */
104
- static async writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<void> {
105
- // Write manifest in the scenario we are in mono-repo state where everything pre-existed
106
- const file = path.resolve(ctx.workspacePath, ctx.outputFolder, ctx.manifestFile);
107
- await fs.mkdir(path.dirname(file), { recursive: true });
108
- await fs.writeFile(file, JSON.stringify(manifest));
109
- }
110
- }