@travetto/manifest 3.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/package.ts ADDED
@@ -0,0 +1,220 @@
1
+ import { readFileSync } from 'fs';
2
+ import fs from 'fs/promises';
3
+ import { createRequire } from 'module';
4
+ import { execSync } from 'child_process';
5
+
6
+ import { ManifestContext, Package, PackageDigest, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
7
+ import { path } from './path';
8
+
9
+ export class PackageUtil {
10
+
11
+ static #req = createRequire(path.resolve('node_modules'));
12
+ static #framework: Package;
13
+ static #cache: Record<string, Package> = {};
14
+ static #workspaces: Record<string, PackageWorkspaceEntry[]> = {};
15
+
16
+ /**
17
+ * Clear out cached package file reads
18
+ */
19
+ static clearCache(): void {
20
+ this.#cache = {};
21
+ this.#workspaces = {};
22
+ }
23
+
24
+ static resolveImport = (library: string): string => this.#req.resolve(library);
25
+
26
+ /**
27
+ * Resolve version path, if file: url
28
+ */
29
+ static resolveVersionPath(rootPath: string, ver: string): string | undefined {
30
+ if (ver.startsWith('file:')) {
31
+ return path.resolve(rootPath, ver.replace('file:', ''));
32
+ } else {
33
+ return;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Find package.json folder for a given dependency
39
+ */
40
+ static resolvePackagePath(name: string): string {
41
+ try {
42
+ return path.dirname(this.resolveImport(`${name}/package.json`));
43
+ } catch {
44
+ try {
45
+ const resolved = this.resolveImport(name);
46
+ return path.join(resolved.split(name)[0], name);
47
+ } catch { }
48
+ }
49
+ throw new Error(`Unable to resolve: ${name}`);
50
+ }
51
+
52
+ /**
53
+ * Build a package visit req
54
+ */
55
+ static packageReq<T>(sourcePath: string, rel: PackageRel): PackageVisitReq<T> {
56
+ return { pkg: this.readPackage(sourcePath), sourcePath, rel };
57
+ }
58
+
59
+ /**
60
+ * Extract all dependencies from a package
61
+ */
62
+ static getAllDependencies<T = unknown>(modulePath: string, rootPath: string): PackageVisitReq<T>[] {
63
+ const pkg = this.readPackage(modulePath);
64
+ const children: Record<string, PackageVisitReq<T>> = {};
65
+ const local = modulePath === rootPath && !modulePath.includes('node_modules');
66
+ for (const [deps, rel] of [
67
+ [pkg.dependencies, 'prod'],
68
+ [pkg.peerDependencies, 'peer'],
69
+ [pkg.optionalDependencies, 'opt'],
70
+ ...(local ? [[pkg.devDependencies, 'dev'] as const] : []),
71
+ ] as const) {
72
+ for (const [name, version] of Object.entries(deps ?? {})) {
73
+ try {
74
+ const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name);
75
+ children[`${name}#${version}`] = this.packageReq<T>(depPath, rel);
76
+ } catch (err) {
77
+ if (rel === 'opt' || (rel === 'peer' && !!pkg.peerDependenciesMeta?.[name].optional)) {
78
+ continue;
79
+ }
80
+ throw err;
81
+ }
82
+ }
83
+ }
84
+ return Object.values(children).sort((a, b) => a.pkg.name.localeCompare(b.pkg.name));
85
+ }
86
+
87
+ /**
88
+ * Read a package.json from a given folder
89
+ */
90
+ static readPackage(modulePath: string, forceRead = false): Package {
91
+ if (forceRead) {
92
+ delete this.#cache[modulePath];
93
+ }
94
+ return this.#cache[modulePath] ??= JSON.parse(readFileSync(
95
+ modulePath.endsWith('.json') ? modulePath : path.resolve(modulePath, 'package.json'),
96
+ 'utf8'
97
+ ));
98
+ }
99
+
100
+ /**
101
+ * import a package.json from a given module name
102
+ */
103
+ static importPackage(moduleName: string): Package {
104
+ return this.readPackage(this.resolvePackagePath(moduleName));
105
+ }
106
+
107
+ /**
108
+ * Write package
109
+ */
110
+ static async writePackageIfChanged(modulePath: string, pkg: Package): Promise<void> {
111
+ const final = JSON.stringify(pkg, null, 2);
112
+ const target = path.resolve(modulePath, 'package.json');
113
+ const current = (await fs.readFile(target, 'utf8').catch(() => '')).trim();
114
+ if (final !== current) {
115
+ await fs.writeFile(target, `${final}\n`, 'utf8');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Visit packages with ability to track duplicates
121
+ */
122
+ static async visitPackages<T>(
123
+ rootOrPath: PackageVisitReq<T> | string,
124
+ visitor: PackageVisitor<T>
125
+ ): Promise<Set<T>> {
126
+
127
+ const root = typeof rootOrPath === 'string' ?
128
+ this.packageReq<T>(rootOrPath, 'root') :
129
+ rootOrPath;
130
+
131
+ const seen = new Map<string, T>();
132
+ const queue: PackageVisitReq<T>[] = [...await visitor.init?.(root) ?? [], root];
133
+ const out = new Set<T>();
134
+
135
+ while (queue.length) {
136
+ const req = queue.pop();
137
+
138
+ if (!req || (visitor.valid && !visitor.valid(req))) {
139
+ continue;
140
+ }
141
+
142
+ const key = req.sourcePath;
143
+ if (seen.has(key)) {
144
+ await visitor.visit?.(req, seen.get(key)!);
145
+ } else {
146
+ const dep = await visitor.create(req);
147
+ out.add(dep);
148
+ await visitor.visit?.(req, dep);
149
+ seen.set(key, dep);
150
+ const children = this.getAllDependencies<T>(req.sourcePath, root.sourcePath);
151
+ queue.push(...children.map(x => ({ ...x, parent: dep })));
152
+ }
153
+ }
154
+ return (await visitor.complete?.(out)) ?? out;
155
+ }
156
+
157
+ /**
158
+ * Get version of manifest package
159
+ */
160
+ static getFrameworkVersion(): string {
161
+ return (this.#framework ??= this.importPackage('@travetto/manifest')).version;
162
+ }
163
+
164
+ /**
165
+ * Produce simple digest of
166
+ */
167
+ static digest(pkg: Package): PackageDigest {
168
+ const { main, name, author, license, version } = pkg;
169
+ return { name, main, author, license, version, framework: this.getFrameworkVersion() };
170
+ }
171
+
172
+ /**
173
+ * Find workspace values from rootPath
174
+ */
175
+ static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> {
176
+ if (!this.#workspaces[rootPath]) {
177
+ await fs.mkdir(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true });
178
+ const cache = path.resolve(ctx.workspacePath, ctx.outputFolder, 'workspaces.json');
179
+ try {
180
+ return JSON.parse(await fs.readFile(cache, 'utf8'));
181
+ } catch {
182
+ const text = execSync('npm query .workspace', { cwd: rootPath, encoding: 'utf8', env: { PATH: process.env.PATH, NODE_PATH: process.env.NODE_PATH } });
183
+ const res: { location: string, name: string }[] = JSON.parse(text);
184
+ const out = this.#workspaces[rootPath] = res.map(d => ({ sourcePath: d.location, name: d.name }));
185
+ await fs.writeFile(cache, JSON.stringify(out), 'utf8');
186
+ }
187
+ }
188
+ return this.#workspaces[rootPath];
189
+ }
190
+
191
+ /**
192
+ * Sync versions across a series of folders
193
+ */
194
+ static async syncVersions(folders: string[], versionMapping: Record<string, string> = {}): Promise<void> {
195
+ const packages = folders.map(folder => {
196
+ const pkg = this.readPackage(folder, true);
197
+ versionMapping[pkg.name] = `^${pkg.version}`;
198
+ return { folder, pkg };
199
+ });
200
+
201
+ for (const { pkg } of packages) {
202
+ for (const group of [
203
+ pkg.dependencies ?? {},
204
+ pkg.devDependencies ?? {},
205
+ pkg.optionalDependencies ?? {},
206
+ pkg.peerDependencies ?? {}
207
+ ]) {
208
+ for (const [mod, ver] of Object.entries(versionMapping)) {
209
+ if (mod in group && !/^[*]|(file:.*)$/.test(group[mod])) {
210
+ group[mod] = ver;
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ for (const { folder, pkg } of packages) {
217
+ await this.writePackageIfChanged(folder, pkg);
218
+ }
219
+ }
220
+ }
package/src/path.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { extname, dirname, resolve, basename, delimiter, join, sep } from 'path';
2
+
3
+ const posix = (file: string): string => file.replaceAll('\\', '/');
4
+
5
+ const cwd = (): string => posix(process.cwd());
6
+
7
+ export const path = {
8
+ cwd,
9
+ toPosix: posix,
10
+ delimiter,
11
+ basename: (file: string): string => posix(basename(file)),
12
+ extname: (file: string): string => posix(extname(file)),
13
+ dirname: (file: string): string => posix(dirname(file)),
14
+ resolve: (...args: string[]): string => posix(resolve(cwd(), ...args.map(f => posix(f)))),
15
+ join: (root: string, ...args: string[]): string => posix(join(posix(root), ...args.map(f => posix(f)))),
16
+ toNative: (file: string): string => file.replaceAll('/', sep)
17
+ };
@@ -0,0 +1,133 @@
1
+ import { path } from './path';
2
+ import { IndexedModule, ManifestIndex } from './manifest-index';
3
+ import { FunctionMetadata, Package, PackageDigest } from './types';
4
+ import { PackageUtil } from './package';
5
+
6
+ const METADATA = Symbol.for('@travetto/manifest:metadata');
7
+ type Metadated = { [METADATA]: FunctionMetadata };
8
+
9
+ /**
10
+ * Extended manifest index geared for application execution
11
+ */
12
+ class $RootIndex extends ManifestIndex {
13
+
14
+ #config: Package | undefined;
15
+ #metadata = new Map<string, FunctionMetadata>();
16
+
17
+ /**
18
+ * **WARNING**: This is a destructive operation, and should only be called before loading any code
19
+ * @private
20
+ */
21
+ reinitForModule(module: string): void {
22
+ this.init(`${this.outputRoot}/node_modules/${module}`);
23
+ this.#config = undefined;
24
+ }
25
+
26
+ /**
27
+ * Determines if the manifest root is the root for a monorepo
28
+ */
29
+ isMonoRepoRoot(): boolean {
30
+ return !!this.manifest.monoRepo && !this.manifest.mainFolder;
31
+ }
32
+
33
+ /**
34
+ * Asynchronously load all source files from manifest
35
+ */
36
+ async loadSource(): Promise<void> {
37
+ for (const { import: imp } of this.findSrc()) {
38
+ await import(imp);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get internal id from file name and optionally, class name
44
+ */
45
+ getId(filename: string, clsName?: string): string {
46
+ filename = path.toPosix(filename);
47
+ const id = this.getEntry(filename)?.id ?? filename;
48
+ return clsName ? `${id}○${clsName}` : id;
49
+ }
50
+
51
+ /**
52
+ * Get main module for manifest
53
+ */
54
+ get mainModule(): IndexedModule {
55
+ return this.getModule(this.mainPackage.name)!;
56
+ }
57
+
58
+ /**
59
+ * Get main package for manifest
60
+ */
61
+ get mainPackage(): Package {
62
+ if (!this.#config) {
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;
74
+ }
75
+
76
+ mainDigest(): PackageDigest {
77
+ return PackageUtil.digest(this.mainPackage);
78
+ }
79
+
80
+ /**
81
+ * Get source file from import location
82
+ * @param outputFile
83
+ */
84
+ getSourceFile(importFile: string): string {
85
+ return this.getFromImport(importFile)?.sourceFile ?? importFile;
86
+ }
87
+
88
+ /**
89
+ * Initialize the meta data for a function/class
90
+ * @param cls Class
91
+ * @param `file` Filename
92
+ * @param `hash` Hash of class contents
93
+ * @param `methods` Methods and their hashes
94
+ * @param `abstract` Is the class abstract
95
+ */
96
+ registerFunction(cls: Function, fileOrImport: string, hash: number, methods?: Record<string, { hash: number }>, abstract?: boolean, synthetic?: boolean): boolean {
97
+ const source = this.getSourceFile(fileOrImport);
98
+ const id = this.getId(source, cls.name);
99
+ Object.defineProperty(cls, 'Ⲑid', { value: id });
100
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
101
+ (cls as unknown as Metadated)[METADATA] = { id, source, hash, methods, abstract, synthetic };
102
+ this.#metadata.set(id, { id, source, hash, methods, abstract, synthetic });
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * Retrieve function metadata by function, or function id
108
+ */
109
+ getFunctionMetadataFromClass(cls: Function | undefined): FunctionMetadata | undefined {
110
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
111
+ return (cls as unknown as Metadated)?.[METADATA];
112
+ }
113
+
114
+ /**
115
+ * Retrieve function metadata by function, or function id
116
+ */
117
+ getFunctionMetadata(clsId: string | Function): FunctionMetadata | undefined {
118
+ const id = clsId === undefined ? '' : typeof clsId === 'string' ? clsId : clsId.Ⲑid;
119
+ return this.#metadata.get(id);
120
+ }
121
+ }
122
+
123
+ let index: $RootIndex | undefined;
124
+
125
+ try {
126
+ index = new $RootIndex(process.env.TRV_MANIFEST!);
127
+ } catch (err) {
128
+ if (/prod/i.test(process.env.NODE_ENV ?? '')) {
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ export const RootIndex: $RootIndex = index!;
package/src/types.ts ADDED
@@ -0,0 +1,111 @@
1
+ export type ManifestModuleFileType = 'typings' | 'ts' | 'js' | 'json' | 'package-json' | 'unknown' | 'fixture' | 'md';
2
+ export type ManifestModuleFolderType =
3
+ '$root' | '$index' | '$package' |
4
+ 'src' | 'bin' | 'support' | 'resources' | 'test' | 'doc' |
5
+ 'test/fixtures' | 'support/fixtures' | 'support/resources' |
6
+ '$other' | '$transformer';
7
+
8
+ export type ManifestProfile = 'compile' | 'test' | 'doc' | 'build' | 'std';
9
+ export type PackageRel = 'dev' | 'prod' | 'peer' | 'opt' | 'root' | 'global';
10
+
11
+ export type ManifestModuleFile = [string, ManifestModuleFileType, number] | [string, ManifestModuleFileType, number, ManifestProfile];
12
+ export type ManifestModuleCore = {
13
+ name: string;
14
+ main?: boolean;
15
+ local?: boolean;
16
+ version: string;
17
+ sourceFolder: string;
18
+ outputFolder: string;
19
+ profiles: ManifestProfile[];
20
+ parents: string[];
21
+ internal?: boolean;
22
+ };
23
+
24
+ export type ManifestModule = ManifestModuleCore & {
25
+ files: Partial<Record<ManifestModuleFolderType, ManifestModuleFile[]>>;
26
+ };
27
+
28
+ export type ManifestContext = {
29
+ mainModule: string;
30
+ mainFolder: string;
31
+ workspacePath: string;
32
+ outputFolder: string;
33
+ toolFolder: string;
34
+ compilerFolder: string;
35
+ monoRepo?: boolean;
36
+ moduleType: 'module' | 'commonjs';
37
+ };
38
+
39
+ export type ManifestRoot = ManifestContext & {
40
+ generated: number;
41
+ modules: Record<string, ManifestModule>;
42
+ };
43
+
44
+ export type Package = {
45
+ name: string;
46
+ type?: 'module' | 'commonjs';
47
+ version: string;
48
+ description?: string;
49
+ license?: string;
50
+ repository?: {
51
+ url: string;
52
+ directory?: string;
53
+ };
54
+ author?: {
55
+ email?: string;
56
+ name?: string;
57
+ };
58
+ main: string;
59
+ homepage?: string;
60
+ files?: string[];
61
+ bin?: Record<string, string>;
62
+ scripts?: Record<string, string>;
63
+ engines?: Record<string, string>;
64
+ keywords?: string[];
65
+
66
+ dependencies?: Record<string, string>;
67
+ devDependencies?: Record<string, string>;
68
+ peerDependencies?: Record<string, string>;
69
+ peerDependenciesMeta?: Record<string, { optional?: boolean }>;
70
+ optionalDependencies?: Record<string, string>;
71
+ travetto?: {
72
+ isolated?: boolean;
73
+ displayName?: string;
74
+ profiles?: ManifestProfile[];
75
+ globalModules?: string[];
76
+ mainSource?: string[];
77
+ docOutput?: string[];
78
+ docRoot?: string;
79
+ docBaseUrl?: string;
80
+ docOutputs?: string[];
81
+ outputFolder?: string;
82
+ };
83
+ workspaces?: string[];
84
+ private?: boolean;
85
+ publishConfig?: { access?: 'restricted' | 'public' };
86
+ };
87
+
88
+ export type PackageDigestField = 'name' | 'main' | 'author' | 'license' | 'version';
89
+ export type PackageDigest = Pick<Package, PackageDigestField> & { framework: string };
90
+
91
+ type OrProm<T> = T | Promise<T>;
92
+
93
+ export type PackageVisitReq<T> = { pkg: Package, rel: PackageRel, sourcePath: string, parent?: T };
94
+ export type PackageVisitor<T> = {
95
+ init?(req: PackageVisitReq<T>): OrProm<undefined | void | PackageVisitReq<T>[]>;
96
+ valid?(req: PackageVisitReq<T>): boolean;
97
+ create(req: PackageVisitReq<T>): OrProm<T>;
98
+ visit?(req: PackageVisitReq<T>, item: T): OrProm<void>;
99
+ complete?(values: Set<T>): OrProm<Set<T> | undefined>;
100
+ };
101
+
102
+ export type PackageWorkspaceEntry = { name: string, sourcePath: string };
103
+
104
+ export type FunctionMetadata = {
105
+ id: string;
106
+ source: string;
107
+ hash?: number;
108
+ methods?: Record<string, { hash: number }>;
109
+ synthetic?: boolean;
110
+ abstract?: boolean;
111
+ };
@@ -0,0 +1,3 @@
1
+ declare interface Function {
2
+ Ⲑid: string;
3
+ }
package/src/util.ts ADDED
@@ -0,0 +1,115 @@
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
+ const MANIFEST_FILE = 'manifest.json';
10
+
11
+ /**
12
+ * Manifest utils
13
+ */
14
+ export class ManifestUtil {
15
+ /**
16
+ * Write file and copy over when ready
17
+ */
18
+ static async #writeJsonWithBuffer(ctx: ManifestContext, filename: string, obj: object): Promise<string> {
19
+ const tempName = path.resolve(ctx.workspacePath, ctx.mainFolder, filename).replace(/[\/\\: ]/g, '_');
20
+ const file = path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule, filename);
21
+ await fs.mkdir(path.dirname(file), { recursive: true });
22
+ const temp = path.resolve(os.tmpdir(), `${tempName}.${Date.now()}`);
23
+ await fs.writeFile(temp, JSON.stringify(obj), 'utf8');
24
+ await fs.copyFile(temp, file);
25
+ fs.unlink(temp);
26
+ return file;
27
+ }
28
+
29
+ /**
30
+ * Build a manifest context
31
+ * @param folder
32
+ */
33
+ static async buildContext(folder?: string): Promise<ManifestContext> {
34
+ const { getManifestContext } = await import('../bin/context.js');
35
+ return getManifestContext(folder);
36
+ }
37
+
38
+ /**
39
+ * Produce manifest in memory
40
+ */
41
+ static async buildManifest(ctx: ManifestContext): Promise<ManifestRoot> {
42
+ return {
43
+ modules: await ManifestModuleUtil.produceModules(ctx),
44
+ generated: Date.now(),
45
+ ...ctx
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Produce a production manifest from a given manifest
51
+ */
52
+ static createProductionManifest(manifest: ManifestRoot): ManifestRoot {
53
+ return {
54
+ ...manifest,
55
+ // If in prod mode, only include std modules
56
+ modules: Object.fromEntries(
57
+ Object.values(manifest.modules)
58
+ .filter(x => x.profiles.includes('std'))
59
+ .map(m => [m.name, m])
60
+ ),
61
+ // Mark output folder/workspace path as portable
62
+ outputFolder: '',
63
+ workspacePath: '',
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Read manifest, synchronously
69
+ *
70
+ * @param file
71
+ * @returns
72
+ */
73
+ static readManifestSync(file: string): { manifest: ManifestRoot, file: string } {
74
+ file = path.resolve(file);
75
+ if (!file.endsWith('.json')) {
76
+ file = path.resolve(file, MANIFEST_FILE);
77
+ }
78
+ const manifest: ManifestRoot = JSON.parse(readFileSync(file, 'utf8'));
79
+ if (!manifest.outputFolder) {
80
+ manifest.outputFolder = path.cwd();
81
+ manifest.workspacePath = path.cwd();
82
+ }
83
+ return { manifest, file };
84
+ }
85
+
86
+ /**
87
+ * Write manifest for a given context, return location
88
+ */
89
+ static writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<string> {
90
+ return this.#writeJsonWithBuffer(ctx, MANIFEST_FILE, manifest);
91
+ }
92
+
93
+ /**
94
+ * Write a manifest to a specific file, if no file extension provided, the file is assumed to be a folder
95
+ */
96
+ static async writeManifestToFile(location: string, manifest: ManifestRoot): Promise<string> {
97
+ if (!location.endsWith('.json')) {
98
+ location = path.resolve(location, MANIFEST_FILE);
99
+ }
100
+
101
+ await fs.mkdir(path.dirname(location), { recursive: true });
102
+ await fs.writeFile(location, JSON.stringify(manifest), 'utf8');
103
+
104
+ return location;
105
+ }
106
+
107
+ /**
108
+ * Rewrite manifest for a given folder
109
+ */
110
+ static async rewriteManifest(source: string): Promise<void> {
111
+ const subCtx = await this.buildContext(source);
112
+ const subManifest = await this.buildManifest(subCtx);
113
+ await this.writeManifest(subCtx, subManifest);
114
+ }
115
+ }
package/src/watch.ts ADDED
@@ -0,0 +1,56 @@
1
+ import fs from 'fs/promises';
2
+ import { path } from './path';
3
+
4
+ export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string };
5
+
6
+ export type WatchEventListener = (ev: WatchEvent, folder: string) => void;
7
+ type EventFilter = (ev: WatchEvent) => boolean;
8
+ type WatchConfig = { filter?: EventFilter, ignore?: string[], createMissing?: boolean };
9
+
10
+ async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
11
+ try {
12
+ return await import('@parcel/watcher');
13
+ } catch (err) {
14
+ console.error('@parcel/watcher must be installed to use watching functionality');
15
+ throw err;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Leverages @parcel/watcher to watch a series of folders
21
+ * @param folders
22
+ * @param onEvent
23
+ * @private
24
+ */
25
+ export async function watchFolders(folders: string[], onEvent: WatchEventListener, config: WatchConfig = {}): Promise<() => Promise<void>> {
26
+ const lib = await getWatcher();
27
+ const createMissing = config.createMissing ?? false;
28
+ const validFolders = new Set(folders);
29
+
30
+ const subs = await Promise.all(folders.map(async folder => {
31
+ if (await fs.stat(folder).then(() => true, () => createMissing)) {
32
+ await fs.mkdir(folder, { recursive: true });
33
+ const ignore = (await fs.readdir(folder)).filter(x => x.startsWith('.') && x.length > 2);
34
+ return lib.subscribe(folder, (err, events) => {
35
+ for (const ev of events) {
36
+ if (ev.type === 'delete' && validFolders.has(path.toPosix(ev.path))) {
37
+ return process.exit(0); // Exit when watched folder is removed
38
+ }
39
+ const finalEv = { action: ev.type, file: ev.path };
40
+ if (!config.filter || config.filter(finalEv)) {
41
+ onEvent(finalEv, folder);
42
+ }
43
+ }
44
+ }, { ignore: [...ignore, ...config.ignore ?? []] });
45
+ }
46
+ }));
47
+
48
+ // Allow for multiple calls
49
+ let finalProm: Promise<void> | undefined;
50
+ const remove = (): Promise<void> => finalProm ??= Promise.all(subs.map(x => x?.unsubscribe())).then(() => { });
51
+
52
+ // Cleanup on exit
53
+ process.on('exit', remove);
54
+
55
+ return remove;
56
+ }