@travetto/manifest 3.0.3 → 3.1.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 +43 -36
- package/bin/context.js +24 -12
- package/package.json +1 -1
- package/src/dependencies.ts +5 -2
- package/src/manifest-index.ts +1 -1
- package/src/package.ts +14 -65
- package/src/root-index.ts +16 -23
- package/src/types.ts +3 -3
- package/src/util.ts +1 -0
- package/src/watch.ts +129 -70
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ During the compilation process, it is helpful to know how the output content dif
|
|
|
36
36
|
## Class and Function Metadata
|
|
37
37
|
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.
|
|
38
38
|
|
|
39
|
-
`Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RootIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/root-index.ts#
|
|
39
|
+
`Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RootIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/root-index.ts#L11) using the `getFunctionMetadata` method.
|
|
40
40
|
|
|
41
41
|
**Code: Test Class**
|
|
42
42
|
```typescript
|
|
@@ -90,17 +90,27 @@ By default, all paths within the framework are assumed to be in a POSIX style, a
|
|
|
90
90
|
## File Watching
|
|
91
91
|
The module also leverages [fetch](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 [fetch](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
92
|
|
|
93
|
-
**Code: Watch
|
|
93
|
+
**Code: Watch Configuration**
|
|
94
94
|
```typescript
|
|
95
|
-
export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string };
|
|
96
|
-
export type WatchEventFilter = (ev: WatchEvent) => boolean;
|
|
95
|
+
export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
|
|
97
96
|
|
|
98
|
-
export type
|
|
99
|
-
export type WatchConfig = {
|
|
97
|
+
export type WatchFolder = {
|
|
100
98
|
/**
|
|
101
|
-
*
|
|
99
|
+
* Source folder
|
|
102
100
|
*/
|
|
103
|
-
|
|
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;
|
|
104
114
|
/**
|
|
105
115
|
* List of top level folders to ignore
|
|
106
116
|
*/
|
|
@@ -113,27 +123,21 @@ export type WatchConfig = {
|
|
|
113
123
|
* Include files that start with '.'
|
|
114
124
|
*/
|
|
115
125
|
includeHidden?: boolean;
|
|
116
|
-
/**
|
|
117
|
-
* Should watching prevent normal exiting, until watch is removed?
|
|
118
|
-
*/
|
|
119
|
-
persistent?: boolean;
|
|
120
126
|
};
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
* Watch files for a given folder
|
|
124
|
-
* @param folder
|
|
125
|
-
* @param onEvent
|
|
126
|
-
* @param options
|
|
127
|
-
*/
|
|
128
|
-
export async function watchFolderImmediate(
|
|
129
|
-
folder: string,
|
|
130
|
-
onEvent: WatchEventListener,
|
|
131
|
-
options: WatchConfig = {}
|
|
132
|
-
): Promise<() => Promise<void>> {
|
|
128
|
+
export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<void> };
|
|
133
129
|
```
|
|
134
130
|
|
|
135
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.
|
|
136
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
|
+
|
|
137
141
|
## Anatomy of a Manifest
|
|
138
142
|
|
|
139
143
|
**Code: Manifest for @travetto/manifest**
|
|
@@ -149,6 +153,9 @@ This method allows for watching one or more folders, and registering a callback
|
|
|
149
153
|
"toolFolder": ".trv_build",
|
|
150
154
|
"compilerFolder": ".trv_compiler",
|
|
151
155
|
"packageManager": "npm",
|
|
156
|
+
"version": "3.1.0-rc.0",
|
|
157
|
+
"description": "Support for project indexing, manifesting, along with file watching",
|
|
158
|
+
"frameworkVersion": "3.1.0-rc.0",
|
|
152
159
|
"modules": {
|
|
153
160
|
"@travetto/manifest": {
|
|
154
161
|
"main": true,
|
|
@@ -238,19 +245,19 @@ The modules represent all of the [Travetto](https://travetto.dev)-aware dependen
|
|
|
238
245
|
|
|
239
246
|
### Module Files
|
|
240
247
|
The module files are a simple categorization of files into a predetermined set of folders:
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* src - Code that should be automatically loaded at runtime. All .ts files under the src
|
|
245
|
-
* test - Code that contains test files. All .ts files under the test
|
|
246
|
-
* test/fixtures - Test resource files, pertains to the main module only. Located under test/fixtures
|
|
247
|
-
* resources - Packaged resource, meant to pertain to the main module only. Files, under resources
|
|
248
|
-
* support - All .ts files under the support
|
|
249
|
-
* support/resources - Packaged resource files, meant to be included by other modules, under support/resources
|
|
250
|
-
* support/fixtures - Test resources meant to shared across modules. Under support/fixtures
|
|
251
|
-
* doc - Documentation files. All .ts files under the doc
|
|
252
|
-
*
|
|
253
|
-
* bin - Entry point .js files. All .js files under the bin
|
|
248
|
+
* `$root` - All uncategorized files at the module root
|
|
249
|
+
* `$index` - `__index__.ts`, `index.ts` files at the root of the project
|
|
250
|
+
* `$package` - The [Package JSON](https://docs.npmjs.com/cli/v9/configuring-npm/package-json) for the project
|
|
251
|
+
* `src` - Code that should be automatically loaded at runtime. All .ts files under the `src/` folder
|
|
252
|
+
* `test` - Code that contains test files. All .ts files under the `test/` folder
|
|
253
|
+
* `test/fixtures` - Test resource files, pertains to the main module only. Located under `test/fixtures/`
|
|
254
|
+
* `resources` - Packaged resource, meant to pertain to the main module only. Files, under `resources/`
|
|
255
|
+
* `support` - All .ts files under the `support/` folder
|
|
256
|
+
* `support/resources` - Packaged resource files, meant to be included by other modules, under `support/resources/`
|
|
257
|
+
* `support/fixtures` - Test resources meant to shared across modules. Under `support/fixtures/`
|
|
258
|
+
* `doc` - Documentation files. `DOC.tsx` and All .ts/.tsx files under the `doc/` folder
|
|
259
|
+
* `$transformer` - All .ts files under the pattern `support/transform*`. These are used during compilation and never at runtime
|
|
260
|
+
* `bin` - Entry point .js files. All .js files under the `bin/` folder
|
|
254
261
|
Within each file there is a pattern of either a 3 or 4 element array:
|
|
255
262
|
|
|
256
263
|
**Code: Sample file**
|
package/bin/context.js
CHANGED
|
@@ -23,20 +23,27 @@ async function $getPkg(inputFolder) {
|
|
|
23
23
|
const WS_ROOT = {};
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Get module
|
|
27
|
-
* @param {string}
|
|
28
|
-
* @return {Promise<string
|
|
26
|
+
* Get module root for a given folder
|
|
27
|
+
* @param {string} dir
|
|
28
|
+
* @return {Promise<string>}
|
|
29
29
|
*/
|
|
30
|
-
async function $
|
|
31
|
-
let dir = path.dirname(file);
|
|
30
|
+
async function $getModuleRoot(dir) {
|
|
32
31
|
let prev;
|
|
33
32
|
while (dir !== prev && !(await fs.stat(path.resolve(dir, 'package.json')).catch(() => false))) {
|
|
34
33
|
prev = dir;
|
|
35
34
|
dir = path.dirname(dir);
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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, () => { });
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -74,6 +81,7 @@ async function $getWorkspaceRoot(base = process.cwd()) {
|
|
|
74
81
|
*/
|
|
75
82
|
export async function getManifestContext(folder) {
|
|
76
83
|
const workspacePath = path.resolve(await $getWorkspaceRoot(folder));
|
|
84
|
+
const req = createRequire(`${workspacePath}/node_modules`);
|
|
77
85
|
|
|
78
86
|
// If manifest specified via env var, and is a package name
|
|
79
87
|
if (!folder && process.env.TRV_MODULE) {
|
|
@@ -81,7 +89,6 @@ export async function getManifestContext(folder) {
|
|
|
81
89
|
if (/[.](t|j)s$/.test(process.env.TRV_MODULE)) {
|
|
82
90
|
process.env.TRV_MODULE = await $getModuleFromFile(process.env.TRV_MODULE) ?? process.env.TRV_MODULE;
|
|
83
91
|
}
|
|
84
|
-
const req = createRequire(`${workspacePath}/node_modules`);
|
|
85
92
|
try {
|
|
86
93
|
folder = path.dirname(req.resolve(`${process.env.TRV_MODULE}/package.json`));
|
|
87
94
|
} catch {
|
|
@@ -94,8 +101,8 @@ export async function getManifestContext(folder) {
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
const mainPath = path.resolve(folder ?? '.');
|
|
98
|
-
const { name: mainModule, workspaces, travetto } = (await $getPkg(mainPath));
|
|
104
|
+
const mainPath = await $getModuleRoot(path.resolve(folder ?? '.'));
|
|
105
|
+
const { name: mainModule, workspaces, travetto, version, description } = (await $getPkg(mainPath));
|
|
99
106
|
const monoRepo = workspacePath !== mainPath || !!workspaces;
|
|
100
107
|
const outputFolder = travetto?.outputFolder ?? '.trv_output';
|
|
101
108
|
|
|
@@ -104,6 +111,8 @@ export async function getManifestContext(folder) {
|
|
|
104
111
|
/** @type {'yarn'|'npm'} */
|
|
105
112
|
const packageManager = await fs.stat(path.resolve(workspacePath, 'yarn.lock')).then(() => 'yarn', () => 'npm');
|
|
106
113
|
|
|
114
|
+
const { version: frameworkVersion } = JSON.parse(await fs.readFile(req.resolve('@travetto/manifest/package.json'), 'utf8'));
|
|
115
|
+
|
|
107
116
|
const res = {
|
|
108
117
|
moduleType,
|
|
109
118
|
mainModule: mainModule ?? 'untitled', // When root package.json is missing a name
|
|
@@ -113,7 +122,10 @@ export async function getManifestContext(folder) {
|
|
|
113
122
|
outputFolder,
|
|
114
123
|
toolFolder: '.trv_build',
|
|
115
124
|
compilerFolder: '.trv_compiler',
|
|
116
|
-
packageManager
|
|
125
|
+
packageManager,
|
|
126
|
+
version,
|
|
127
|
+
description,
|
|
128
|
+
frameworkVersion
|
|
117
129
|
};
|
|
118
130
|
return res;
|
|
119
131
|
}
|
package/package.json
CHANGED
package/src/dependencies.ts
CHANGED
|
@@ -39,9 +39,12 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
|
|
|
39
39
|
.map(x => new RegExp(`^${x.replace(/[*]/g, '.*?')}$`));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
constructor(public ctx: ManifestContext) {
|
|
42
|
+
constructor(public ctx: ManifestContext) {
|
|
43
|
+
this.#mainSourcePath = path.resolve(this.ctx.workspacePath, this.ctx.mainFolder);
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
#mainPatterns: RegExp[] = [];
|
|
47
|
+
#mainSourcePath: string;
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* Initialize visitor, and provide global dependencies
|
|
@@ -76,7 +79,7 @@ export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> {
|
|
|
76
79
|
* Is valid dependency for searching
|
|
77
80
|
*/
|
|
78
81
|
valid(req: PackageVisitReq<ModuleDep>): boolean {
|
|
79
|
-
return req.sourcePath ===
|
|
82
|
+
return req.sourcePath === this.#mainSourcePath || (
|
|
80
83
|
req.rel !== 'peer' &&
|
|
81
84
|
(!!req.pkg.travetto || req.pkg.private === true || !req.sourcePath.includes('node_modules') || req.rel === 'global')
|
|
82
85
|
);
|
package/src/manifest-index.ts
CHANGED
|
@@ -261,7 +261,7 @@ export class ManifestIndex {
|
|
|
261
261
|
return name ? this.getModule(name) : undefined;
|
|
262
262
|
}
|
|
263
263
|
/**
|
|
264
|
-
* Build module list from an expression list (e.g. `@travetto/
|
|
264
|
+
* Build module list from an expression list (e.g. `@travetto/rest,-@travetto/log)
|
|
265
265
|
*/
|
|
266
266
|
getModuleList(mode: 'local' | 'all', exprList: string = ''): Set<string> {
|
|
267
267
|
const allMods = Object.keys(this.#manifest.modules);
|
package/src/package.ts
CHANGED
|
@@ -3,16 +3,24 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
|
|
6
|
-
import { ManifestContext, Package,
|
|
6
|
+
import { ManifestContext, Package, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
|
|
7
7
|
import { path } from './path';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Utilities for querying, traversing and reading package.json files.
|
|
11
|
+
*/
|
|
9
12
|
export class PackageUtil {
|
|
10
13
|
|
|
11
14
|
static #req = createRequire(path.resolve('node_modules'));
|
|
12
|
-
static #framework: Package;
|
|
13
15
|
static #cache: Record<string, Package> = {};
|
|
14
16
|
static #workspaces: Record<string, PackageWorkspaceEntry[]> = {};
|
|
15
17
|
|
|
18
|
+
static #exec<T>(cwd: string, cmd: string): Promise<T> {
|
|
19
|
+
const env = { PATH: process.env.PATH, NODE_PATH: process.env.NODE_PATH };
|
|
20
|
+
const text = execSync(cmd, { cwd, encoding: 'utf8', env, stdio: ['pipe', 'pipe'] }).toString().trim();
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Clear out cached package file reads
|
|
18
26
|
*/
|
|
@@ -108,18 +116,6 @@ export class PackageUtil {
|
|
|
108
116
|
return this.readPackage(this.resolvePackagePath(moduleName));
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
/**
|
|
112
|
-
* Write package
|
|
113
|
-
*/
|
|
114
|
-
static async writePackageIfChanged(modulePath: string, pkg: Package): Promise<void> {
|
|
115
|
-
const final = JSON.stringify(pkg, null, 2);
|
|
116
|
-
const target = path.resolve(modulePath, 'package.json');
|
|
117
|
-
const current = (await fs.readFile(target, 'utf8').catch(() => '')).trim();
|
|
118
|
-
if (final !== current) {
|
|
119
|
-
await fs.writeFile(target, `${final}\n`, 'utf8');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
119
|
/**
|
|
124
120
|
* Visit packages with ability to track duplicates
|
|
125
121
|
*/
|
|
@@ -158,21 +154,6 @@ export class PackageUtil {
|
|
|
158
154
|
return (await visitor.complete?.(out)) ?? out;
|
|
159
155
|
}
|
|
160
156
|
|
|
161
|
-
/**
|
|
162
|
-
* Get version of manifest package
|
|
163
|
-
*/
|
|
164
|
-
static getFrameworkVersion(): string {
|
|
165
|
-
return (this.#framework ??= this.importPackage('@travetto/manifest')).version;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Produce simple digest of
|
|
170
|
-
*/
|
|
171
|
-
static digest(pkg: Package): PackageDigest {
|
|
172
|
-
const { main, name, author, license, version } = pkg;
|
|
173
|
-
return { name, main, author, license, version, framework: this.getFrameworkVersion() };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
157
|
/**
|
|
177
158
|
* Find workspace values from rootPath
|
|
178
159
|
*/
|
|
@@ -186,15 +167,13 @@ export class PackageUtil {
|
|
|
186
167
|
let out: PackageWorkspaceEntry[];
|
|
187
168
|
switch (ctx.packageManager) {
|
|
188
169
|
case 'npm': {
|
|
189
|
-
const
|
|
190
|
-
out =
|
|
191
|
-
.map((d: { location: string, name: string }) => ({ sourcePath: d.location, name: d.name }));
|
|
170
|
+
const res = await this.#exec<{ location: string, name: string }[]>(rootPath, 'npm query .workspace');
|
|
171
|
+
out = res.map(d => ({ sourcePath: d.location, name: d.name }));
|
|
192
172
|
break;
|
|
193
173
|
}
|
|
194
174
|
case 'yarn': {
|
|
195
|
-
const
|
|
196
|
-
out = Object.entries
|
|
197
|
-
.map(([name, { location }]) => ({ sourcePath: location, name }));
|
|
175
|
+
const res = await this.#exec<Record<string, { location: string }>>(rootPath, 'npm query .workspace');
|
|
176
|
+
out = Object.entries(res).map(([name, { location }]) => ({ sourcePath: location, name }));
|
|
198
177
|
break;
|
|
199
178
|
}
|
|
200
179
|
}
|
|
@@ -206,34 +185,4 @@ export class PackageUtil {
|
|
|
206
185
|
}
|
|
207
186
|
return this.#workspaces[rootPath];
|
|
208
187
|
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Sync versions across a series of folders
|
|
212
|
-
*/
|
|
213
|
-
static async syncVersions(folders: string[], versionMapping: Record<string, string> = {}): Promise<void> {
|
|
214
|
-
const packages = folders.map(folder => {
|
|
215
|
-
const pkg = this.readPackage(folder, true);
|
|
216
|
-
versionMapping[pkg.name] = `^${pkg.version}`;
|
|
217
|
-
return { folder, pkg };
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
for (const { pkg } of packages) {
|
|
221
|
-
for (const group of [
|
|
222
|
-
pkg.dependencies ?? {},
|
|
223
|
-
pkg.devDependencies ?? {},
|
|
224
|
-
pkg.optionalDependencies ?? {},
|
|
225
|
-
pkg.peerDependencies ?? {}
|
|
226
|
-
]) {
|
|
227
|
-
for (const [mod, ver] of Object.entries(versionMapping)) {
|
|
228
|
-
if (mod in group && !/^[*]|(file:.*)$/.test(group[mod])) {
|
|
229
|
-
group[mod] = ver;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
for (const { folder, pkg } of packages) {
|
|
236
|
-
await this.writePackageIfChanged(folder, pkg);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
188
|
}
|
package/src/root-index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { path } from './path';
|
|
2
2
|
import { IndexedModule, ManifestIndex } from './manifest-index';
|
|
3
|
-
import { FunctionMetadata,
|
|
4
|
-
import { PackageUtil } from './package';
|
|
3
|
+
import { FunctionMetadata, ManifestContext, Package } from './types';
|
|
5
4
|
|
|
6
5
|
const METADATA = Symbol.for('@travetto/manifest:metadata');
|
|
7
6
|
type Metadated = { [METADATA]: FunctionMetadata };
|
|
@@ -11,7 +10,6 @@ type Metadated = { [METADATA]: FunctionMetadata };
|
|
|
11
10
|
*/
|
|
12
11
|
class $RootIndex extends ManifestIndex {
|
|
13
12
|
|
|
14
|
-
#config: Package | undefined;
|
|
15
13
|
#metadata = new Map<string, FunctionMetadata>();
|
|
16
14
|
|
|
17
15
|
/**
|
|
@@ -20,7 +18,6 @@ class $RootIndex extends ManifestIndex {
|
|
|
20
18
|
*/
|
|
21
19
|
reinitForModule(module: string): void {
|
|
22
20
|
this.init(`${this.outputRoot}/node_modules/${module}`);
|
|
23
|
-
this.#config = undefined;
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
/**
|
|
@@ -49,32 +46,28 @@ class $RootIndex extends ManifestIndex {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
* Get main module
|
|
49
|
+
* Get main module name
|
|
53
50
|
*/
|
|
54
|
-
get
|
|
55
|
-
return this.
|
|
51
|
+
get mainModuleName(): string {
|
|
52
|
+
return this.manifest.mainModule;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
/**
|
|
59
|
-
* Get main
|
|
56
|
+
* Get main module for manifest
|
|
60
57
|
*/
|
|
61
|
-
get
|
|
62
|
-
|
|
63
|
-
const { outputPath } = this.getModule(this.manifest.mainModule)!;
|
|
64
|
-
this.#config = {
|
|
65
|
-
...{
|
|
66
|
-
name: 'untitled',
|
|
67
|
-
description: 'A Travetto application',
|
|
68
|
-
version: '0.0.0',
|
|
69
|
-
},
|
|
70
|
-
...PackageUtil.readPackage(outputPath)
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return this.#config;
|
|
58
|
+
get mainModule(): IndexedModule {
|
|
59
|
+
return this.getModule(this.mainModuleName)!;
|
|
74
60
|
}
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Digest manifest
|
|
64
|
+
*/
|
|
65
|
+
manifestDigest(): Pick<ManifestContext, 'mainModule' | 'frameworkVersion' | 'version'> {
|
|
66
|
+
return {
|
|
67
|
+
mainModule: this.manifest.mainModule,
|
|
68
|
+
frameworkVersion: this.manifest.frameworkVersion,
|
|
69
|
+
version: this.manifest.version,
|
|
70
|
+
};
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
/**
|
package/src/types.ts
CHANGED
|
@@ -35,6 +35,9 @@ export type ManifestContext = {
|
|
|
35
35
|
monoRepo?: boolean;
|
|
36
36
|
moduleType: 'module' | 'commonjs';
|
|
37
37
|
packageManager: 'yarn' | 'npm';
|
|
38
|
+
frameworkVersion: string;
|
|
39
|
+
description: string;
|
|
40
|
+
version: string;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
export type ManifestRoot = ManifestContext & {
|
|
@@ -86,9 +89,6 @@ export type Package = {
|
|
|
86
89
|
publishConfig?: { access?: 'restricted' | 'public' };
|
|
87
90
|
};
|
|
88
91
|
|
|
89
|
-
export type PackageDigestField = 'name' | 'main' | 'author' | 'license' | 'version';
|
|
90
|
-
export type PackageDigest = Pick<Package, PackageDigestField> & { framework: string };
|
|
91
|
-
|
|
92
92
|
type OrProm<T> = T | Promise<T>;
|
|
93
93
|
|
|
94
94
|
export type PackageVisitReq<T> = { pkg: Package, rel: PackageRel, sourcePath: string, parent?: T };
|
package/src/util.ts
CHANGED
|
@@ -76,6 +76,7 @@ export class ManifestUtil {
|
|
|
76
76
|
file = path.resolve(file, MANIFEST_FILE);
|
|
77
77
|
}
|
|
78
78
|
const manifest: ManifestRoot = JSON.parse(readFileSync(file, 'utf8'));
|
|
79
|
+
// Support packaged environments, by allowing empty outputFolder
|
|
79
80
|
if (!manifest.outputFolder) {
|
|
80
81
|
manifest.outputFolder = path.cwd();
|
|
81
82
|
manifest.workspacePath = path.cwd();
|
package/src/watch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { watch, Stats } from 'fs';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
|
|
3
4
|
import { path } from './path';
|
|
4
5
|
|
|
5
6
|
async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
|
|
@@ -11,15 +12,25 @@ async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
|
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string };
|
|
15
|
-
export type WatchEventFilter = (ev: WatchEvent) => boolean;
|
|
15
|
+
export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
|
|
16
16
|
|
|
17
|
-
export type
|
|
18
|
-
|
|
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;
|
|
19
30
|
/**
|
|
20
|
-
*
|
|
31
|
+
* Only look at immediate folder
|
|
21
32
|
*/
|
|
22
|
-
|
|
33
|
+
immediate?: boolean;
|
|
23
34
|
/**
|
|
24
35
|
* List of top level folders to ignore
|
|
25
36
|
*/
|
|
@@ -32,25 +43,66 @@ export type WatchConfig = {
|
|
|
32
43
|
* Include files that start with '.'
|
|
33
44
|
*/
|
|
34
45
|
includeHidden?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Should watching prevent normal exiting, until watch is removed?
|
|
37
|
-
*/
|
|
38
|
-
persistent?: boolean;
|
|
39
46
|
};
|
|
40
47
|
|
|
48
|
+
export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<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
|
+
|
|
41
100
|
/**
|
|
42
|
-
* Watch files for a given folder
|
|
43
|
-
* @param folder
|
|
44
|
-
* @param onEvent
|
|
45
|
-
* @param options
|
|
101
|
+
* Watch immediate files for a given folder
|
|
46
102
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
options: WatchConfig = {}
|
|
51
|
-
): Promise<() => Promise<void>> {
|
|
52
|
-
const watchPath = path.resolve(folder);
|
|
53
|
-
const watcher = watch(watchPath, { persistent: options.persistent, encoding: 'utf8' });
|
|
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' });
|
|
54
106
|
const lastStats: Record<string, Stats | undefined> = {};
|
|
55
107
|
const invalidFilter = (el: string): boolean =>
|
|
56
108
|
(el === '.' || el === '..' || (!options.includeHidden && el.startsWith('.')) || !!options.ignore?.includes(el));
|
|
@@ -62,6 +114,9 @@ export async function watchFolderImmediate(
|
|
|
62
114
|
const file = path.resolve(watchPath, el);
|
|
63
115
|
lastStats[file] = await fs.stat(file);
|
|
64
116
|
}
|
|
117
|
+
|
|
118
|
+
const target = options.target ?? options.src;
|
|
119
|
+
|
|
65
120
|
watcher.on('change', async (type: string, file: string): Promise<void> => {
|
|
66
121
|
if (invalidFilter(file)) {
|
|
67
122
|
return;
|
|
@@ -78,69 +133,73 @@ export async function watchFolderImmediate(
|
|
|
78
133
|
}
|
|
79
134
|
let ev: WatchEvent;
|
|
80
135
|
if (prevStat && !stat) {
|
|
81
|
-
ev = { action: 'delete', file };
|
|
136
|
+
ev = { action: 'delete', file, folder: target };
|
|
82
137
|
} else if (!prevStat && stat) {
|
|
83
|
-
ev = { action: 'create', file };
|
|
138
|
+
ev = { action: 'create', file, folder: target };
|
|
84
139
|
} else {
|
|
85
|
-
ev = { action: 'update', file };
|
|
140
|
+
ev = { action: 'update', file, folder: target };
|
|
86
141
|
}
|
|
87
142
|
if (!options.filter || options.filter(ev)) {
|
|
88
|
-
|
|
143
|
+
queue.add(ev);
|
|
89
144
|
}
|
|
90
145
|
});
|
|
91
|
-
|
|
92
|
-
|
|
146
|
+
|
|
147
|
+
queue.registerOnClose(() => watcher.close());
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param folders
|
|
98
|
-
* @param onEvent
|
|
99
|
-
* @param options
|
|
151
|
+
* Watch recursive files for a given folder
|
|
100
152
|
*/
|
|
101
|
-
|
|
102
|
-
folders: string[] | [folder: string, targetFolder: string][] | (readonly [folder: string, targetFolder: string])[],
|
|
103
|
-
onEvent: WatchEventListener,
|
|
104
|
-
config: WatchConfig = {}
|
|
105
|
-
): Promise<() => Promise<void>> {
|
|
153
|
+
async function watchFolderRecursive(queue: Queue<WatchEvent>, options: WatchFolder): Promise<void> {
|
|
106
154
|
const lib = await getWatcher();
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const finalEv = { action: ev.type, file: path.toPosix(ev.path) };
|
|
120
|
-
if (ev.type === 'delete' && validFolders.has(finalEv.file)) {
|
|
121
|
-
return process.exit(0); // Exit when watched folder is removed
|
|
122
|
-
}
|
|
123
|
-
const isHidden = !config.includeHidden && finalEv.file.replace(targetFolder, '').includes('/.');
|
|
124
|
-
const matches = !isHidden && (!config.filter || config.filter(finalEv));
|
|
125
|
-
if (matches) {
|
|
126
|
-
onEvent(finalEv, targetFolder);
|
|
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
|
+
for (const ev of events) {
|
|
162
|
+
const finalEv = { action: ev.type, file: path.toPosix(ev.path), folder: target };
|
|
163
|
+
if (ev.type !== 'delete') {
|
|
164
|
+
const stats = await fs.stat(finalEv.file);
|
|
165
|
+
if ((stats.ctimeMs - Date.now()) < DEDUPE_THRESHOLD) {
|
|
166
|
+
ev.type = 'create'; // Force create on newly stated files
|
|
127
167
|
}
|
|
128
168
|
}
|
|
129
|
-
}, { ignore: [...ignore, ...config.ignore ?? []] });
|
|
130
|
-
}
|
|
131
|
-
}));
|
|
132
|
-
|
|
133
|
-
// Allow for multiple calls
|
|
134
|
-
let finalProm: Promise<void> | undefined;
|
|
135
|
-
const remove = (): Promise<void> => finalProm ??= Promise.all(subs.map(x => x?.unsubscribe())).then(() => { });
|
|
136
169
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
if (ev.type === 'delete' && finalEv.file === options.src) {
|
|
171
|
+
return queue.close();
|
|
172
|
+
}
|
|
173
|
+
const isHidden = !options.includeHidden && finalEv.file.replace(target, '').includes('/.');
|
|
174
|
+
const matches = !isHidden && (!options.filter || options.filter(finalEv));
|
|
175
|
+
if (matches) {
|
|
176
|
+
queue.add(finalEv);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, { ignore: [...ignore, ...options.ignore ?? []] });
|
|
180
|
+
queue.registerOnClose(() => cleanup.unsubscribe());
|
|
140
181
|
}
|
|
182
|
+
}
|
|
141
183
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Watch a series of folders
|
|
186
|
+
* @param folders
|
|
187
|
+
* @param onEvent
|
|
188
|
+
* @param options
|
|
189
|
+
*/
|
|
190
|
+
export function watchFolders(
|
|
191
|
+
folders: string[] | WatchFolder[],
|
|
192
|
+
config: Omit<WatchFolder, 'src' | 'target'> = {}
|
|
193
|
+
): WatchStream {
|
|
194
|
+
const queue = new Queue<WatchEvent>();
|
|
195
|
+
for (const folder of folders) {
|
|
196
|
+
if (typeof folder === 'string') {
|
|
197
|
+
watchFolderRecursive(queue, { ...config, src: folder });
|
|
198
|
+
} else if (!folder.immediate) {
|
|
199
|
+
watchFolderRecursive(queue, { ...config, ...folder });
|
|
200
|
+
} else {
|
|
201
|
+
watchFolderImmediate(queue, { ...config, ...folder });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return queue;
|
|
146
205
|
}
|