@travetto/manifest 3.3.1 → 3.4.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,6 @@ This module aims to be the boundary between the file system and the code. The m
19
19
  * Class and Function Metadata
20
20
  * Runtime Indexing
21
21
  * Path Normalization
22
- * File Watching
23
22
 
24
23
  ## Project Manifesting
25
24
  The project manifest fulfills two main goals: Compile-time Support, and Runtime Knowledge of the project.
@@ -31,7 +30,7 @@ During the compilation process, the compiler needs to know every file that is el
31
30
  Additionally, once the code has been compiled (or even bundled after that), the executing process needs to know what files are available for loading, and any patterns necessary for knowing which files to load versus which ones to ignore. This allows for dynamic loading of modules/files without knowledge/access to the file system, and in a more performant manner.
32
31
 
33
32
  ## Manifest Delta
34
- During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L21) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled.
33
+ During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L20) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled.
35
34
 
36
35
  ## Class and Function Metadata
37
36
  For the framework to work properly, metadata needs to be collected about files, classes and functions to uniquely identify them, with support for detecting changes during live reloads. To achieve this, every `class` is decorated with an additional field of `Ⲑid`. `Ⲑid` represents a computed id that is tied to the file/class combination.
@@ -85,58 +84,7 @@ Once the manifest is created, the application runtime can now read this manifest
85
84
  * Providing contextual information when provided a filename, import name, etc (e.g. logging, testing output)
86
85
 
87
86
  ## Path Normalization
88
- By default, all paths within the framework are assumed to be in a POSIX style, and all input paths are converted to the POSIX style. This works appropriately within a Unix and a Windows environment. This module offers up [path](https://github.com/travetto/travetto/tree/main/module/manifest/src/path.ts#L8) as an equivalent to [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library. This allows for consistent behavior across all file-interactions, and also allows for easy analysis if [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library is ever imported.
89
-
90
- ## File Watching
91
- The module also leverages [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher), to expose a single function of `watchFolders`. Only the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework") module packages [@parcel/watcher](https://www.npmjs.com/package/@parcel/watcher) as a direct dependency. This means, that in production, by default all watch operations will fail with a missing dependency.
92
-
93
- **Code: Watch Configuration**
94
- ```typescript
95
- export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
96
-
97
- export type WatchFolder = {
98
- /**
99
- * Source folder
100
- */
101
- src: string;
102
- /**
103
- * Target folder name, useful for deconstructing
104
- */
105
- target?: string;
106
- /**
107
- * Filter events
108
- */
109
- filter?: (ev: WatchEvent) => boolean;
110
- /**
111
- * Only look at immediate folder
112
- */
113
- immediate?: boolean;
114
- /**
115
- * List of top level folders to ignore
116
- */
117
- ignore?: string[];
118
- /**
119
- * If watching a folder that doesn't exist, should it be created?
120
- */
121
- createMissing?: boolean;
122
- /**
123
- * Include files that start with '.'
124
- */
125
- includeHidden?: boolean;
126
- };
127
-
128
- export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<void>, add: (item: WatchEvent | WatchEvent[]) => void };
129
- ```
130
-
131
- This method allows for watching one or more folders, and registering a callback that will fire every time a file changes, and which of the registered folders it was triggered within. The return of the `watchFolders` is a cleanup method, that when invoked will remove and stop all watching behavior.
132
-
133
- **Code: Watch Configuration**
134
- ```typescript
135
- export function watchFolders(
136
- folders: string[] | WatchFolder[],
137
- config: Omit<WatchFolder, 'src' | 'target'> = {}
138
- ): WatchStream {
139
- ```
87
+ By default, all paths within the framework are assumed to be in a POSIX style, and all input paths are converted to the POSIX style. This works appropriately within a Unix and a Windows environment. This module offers up [path](https://github.com/travetto/travetto/tree/main/module/manifest/src/path.ts#L21) as an equivalent to [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library. This allows for consistent behavior across all file-interactions, and also allows for easy analysis if [Node](https://nodejs.org)'s [http](https://nodejs.org/api/path.html) library is ever imported.
140
88
 
141
89
  ## Anatomy of a Manifest
142
90
 
@@ -144,18 +92,19 @@ export function watchFolders(
144
92
  ```typescript
145
93
  {
146
94
  "generated": 1868155200000,
147
- "moduleType": "commonjs",
148
- "mainModule": "@travetto/manifest",
149
- "mainFolder": "module/manifest",
150
95
  "workspacePath": "<generated>",
151
96
  "monoRepo": true,
152
- "outputFolder": ".trv_output",
153
- "toolFolder": ".trv_build",
154
- "compilerFolder": ".trv_compiler",
155
97
  "packageManager": "npm",
98
+ "moduleType": "commonjs",
99
+ "outputFolder": ".trv/output",
100
+ "toolFolder": ".trv/tool",
101
+ "compilerFolder": ".trv/compiler",
102
+ "compilerUrl": "http://127.0.0.1:22154",
103
+ "frameworkVersion": "x.x.x",
104
+ "mainModule": "@travetto/manifest",
105
+ "mainFolder": "module/manifest",
156
106
  "version": "x.x.x",
157
107
  "description": "Support for project indexing, manifesting, along with file watching",
158
- "frameworkVersion": "x.x.x",
159
108
  "modules": {
160
109
  "@travetto/manifest": {
161
110
  "main": true,
@@ -195,6 +144,7 @@ export function watchFolders(
195
144
  "src": [
196
145
  [ "src/delta.ts", "ts", 1868155200000 ],
197
146
  [ "src/dependencies.ts", "ts", 1868155200000 ],
147
+ [ "src/file.ts", "ts", 1868155200000 ],
198
148
  [ "src/manifest-index.ts", "ts", 1868155200000 ],
199
149
  [ "src/module.ts", "ts", 1868155200000 ],
200
150
  [ "src/package.ts", "ts", 1868155200000 ],
@@ -202,8 +152,7 @@ export function watchFolders(
202
152
  [ "src/root-index.ts", "ts", 1868155200000 ],
203
153
  [ "src/types.ts", "ts", 1868155200000 ],
204
154
  [ "src/typings.d.ts", "typings", 1868155200000 ],
205
- [ "src/util.ts", "ts", 1868155200000 ],
206
- [ "src/watch.ts", "ts", 1868155200000 ]
155
+ [ "src/util.ts", "ts", 1868155200000 ]
207
156
  ],
208
157
  "bin": [
209
158
  [ "bin/context.d.ts", "typings", 1868155200000 ],
package/__index__.ts CHANGED
@@ -7,5 +7,5 @@ export * from './src/manifest-index';
7
7
  export * from './src/root-index';
8
8
  export * from './src/package';
9
9
  export * from './src/util';
10
- export * from './src/types';
11
- export * from './src/watch';
10
+ export * from './src/file';
11
+ export * from './src/types';
package/bin/context.js CHANGED
@@ -1,131 +1,161 @@
1
1
  // @ts-check
2
2
 
3
3
  /**
4
- * @typedef {import('../src/types').Package} Pkg
4
+ * @typedef {import('../src/types').Package & { path:string }} Pkg
5
+ * @typedef {Pkg & { mono: boolean, manager: 'yarn'|'npm', resolve: (file:string) => string}} Workspace
5
6
  * @typedef {import('../src/types').ManifestContext} ManifestContext
6
7
  */
7
8
  import fs from 'fs/promises';
8
9
  import path from 'path';
9
10
  import { createRequire } from 'module';
10
11
 
12
+ /** @type {Record<string, Workspace>} */ const WS_ROOT = {};
13
+ const TOOL_FOLDER = '.trv/tool';
14
+ const COMPILER_FOLDER = '.trv/compiler';
15
+ const OUTPUT_FOLDER = '.trv/output';
16
+
11
17
  /**
12
- * Returns the package.json
13
- * @param {string} inputFolder
14
- * @returns {Promise<Pkg>}
18
+ * Read package.json or return undefined if missing
19
+ * @param {string} dir
20
+ * @returns {Promise<Pkg|undefined>}
15
21
  */
16
- async function $getPkg(inputFolder) {
17
- if (!inputFolder.endsWith('.json')) {
18
- inputFolder = path.resolve(inputFolder, 'package.json');
19
- }
20
- return JSON.parse(await fs.readFile(inputFolder, 'utf8'));
22
+ async function $readPackage(dir) {
23
+ dir = dir.endsWith('.json') ? path.dirname(dir) : dir;
24
+ return await fs.readFile(path.resolve(dir, 'package.json'), 'utf8')
25
+ .then(v => ({ ...JSON.parse(v), path: path.resolve(dir) }), () => undefined);
21
26
  }
22
27
 
23
- const WS_ROOT = {};
24
-
25
28
  /**
26
- * Get module root for a given folder
29
+ * Find package.json for a given folder
27
30
  * @param {string} dir
28
- * @return {Promise<string>}
31
+ * @return {Promise<Pkg>}
29
32
  */
30
- async function $getModuleRoot(dir) {
33
+ async function $findPackage(dir) {
31
34
  let prev;
32
- while (dir !== prev && !(await fs.stat(path.resolve(dir, 'package.json')).catch(() => false))) {
33
- prev = dir;
34
- dir = path.dirname(dir);
35
+ let pkg, curr = path.resolve(dir);
36
+ while (!pkg && curr !== prev) {
37
+ pkg = await $readPackage(curr);
38
+ [prev, curr] = [curr, path.dirname(curr)];
39
+ }
40
+ if (!pkg) {
41
+ throw new Error('Could not find a package.json');
42
+ } else {
43
+ return pkg;
35
44
  }
36
- return dir;
37
- }
38
-
39
-
40
- /**
41
- * Get module name from a given file
42
- * @param {string} file
43
- * @return {Promise<string|void>}
44
- */
45
- async function $getModuleFromFile(file) {
46
- return $getPkg(await $getModuleRoot(path.dirname(file))).then(v => v.name, () => { });
47
45
  }
48
46
 
49
47
  /**
50
48
  * Get workspace root
51
- * @return {Promise<string>}
49
+ * @return {Promise<Workspace>}
52
50
  */
53
- async function $getWorkspaceRoot(base = process.cwd()) {
54
- if (base in WS_ROOT) {
55
- return WS_ROOT[base];
56
- }
57
-
51
+ async function $resolveWorkspace(base = process.cwd()) {
52
+ if (base in WS_ROOT) { return WS_ROOT[base]; }
58
53
  let folder = base;
59
- let prevFolder = '';
60
- while (folder !== prevFolder) {
61
- try {
62
- const pkg = await $getPkg(folder);
63
- if (!!pkg.workspaces || !!pkg.travetto?.isolated) {
64
- return (WS_ROOT[base] = folder);
65
- }
66
- } catch { }
67
- if (await fs.stat(path.resolve(folder, '.git')).catch(() => { })) {
54
+ let prev;
55
+ /** @type {Pkg|undefined} */
56
+ let prevPkg, pkg;
57
+
58
+ while (prev !== folder) {
59
+ [prev, prevPkg] = [folder, pkg];
60
+ pkg = await $readPackage(folder) ?? pkg;
61
+ if (
62
+ (pkg && (!!pkg.workspaces || !!pkg.travetto?.isolated)) || // if we have a monorepo root, or we are isolated
63
+ await fs.stat(path.resolve(folder, '.git')).catch(() => { }) // we made it to the source repo root
64
+ ) {
68
65
  break;
69
66
  }
70
- prevFolder = folder;
71
67
  folder = path.dirname(folder);
72
68
  }
73
- return WS_ROOT[base] = base;
74
- }
75
69
 
70
+ if (!pkg) {
71
+ throw new Error('Could not find a package.json');
72
+ }
73
+
74
+ return WS_ROOT[base] = {
75
+ ...pkg,
76
+ manager: await fs.stat(path.resolve(pkg.path, 'yarn.lock')).catch(() => { }) ? 'yarn' : 'npm',
77
+ resolve: createRequire(`${pkg.path}/node_modules`).resolve.bind(null),
78
+ mono: !!pkg.workspaces || (!pkg.travetto?.isolated && !!prevPkg) // Workspaces or nested projects
79
+ };
80
+ }
76
81
 
77
82
  /**
78
- * Gets build context
79
- * @param {string} [folder]
80
- * @return {Promise<ManifestContext>}
83
+ * Get Compiler url
84
+ * @param {Workspace} ws
81
85
  */
82
- export async function getManifestContext(folder) {
83
- const workspacePath = path.resolve(await $getWorkspaceRoot(folder));
84
- const req = createRequire(`${workspacePath}/node_modules`);
86
+ async function $getCompilerUrl(ws) {
87
+ let out = ws.travetto?.compilerUrl;
88
+ if (!out) {
89
+ const file = path.resolve(ws.path, ws.travetto?.toolFolder ?? TOOL_FOLDER, 'compiler.url');
90
+ // eslint-disable-next-line no-bitwise
91
+ const port = (Math.abs([...file].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000;
92
+ out = `http://localhost:${port}`;
93
+ try { await fs.stat(file); } catch {
94
+ await fs.mkdir(path.dirname(file), { recursive: true });
95
+ await fs.writeFile(file, out, 'utf8');
96
+ }
97
+ }
98
+ return out.replace('localhost', '127.0.0.1');
99
+ }
85
100
 
86
- // If manifest specified via env var, and is a package name
101
+ /**
102
+ * Resolve module folder
103
+ * @param {Workspace} workspace
104
+ * @param {string|undefined} folder
105
+ */
106
+ async function $resolveModule(workspace, folder) {
107
+ let mod;
87
108
  if (!folder && process.env.TRV_MODULE) {
88
- // If module is actually a file, try to detect
89
- if (/[.](t|j)s$/.test(process.env.TRV_MODULE)) {
90
- process.env.TRV_MODULE = await $getModuleFromFile(process.env.TRV_MODULE) ?? process.env.TRV_MODULE;
109
+ mod = process.env.TRV_MODULE;
110
+ if (/[.](t|j)s$/.test(mod)) { // Rewrite from file to module
111
+ process.env.TRV_MODULE = mod = await $findPackage(path.dirname(mod))
112
+ .then(v => v.name, () => '');
91
113
  }
114
+ }
115
+
116
+ if (mod) { // If module provided in lieu of folder
92
117
  try {
93
- folder = path.dirname(req.resolve(`${process.env.TRV_MODULE}/package.json`));
118
+ folder = path.dirname(workspace.resolve(`${mod}/package.json`));
94
119
  } catch {
95
- const workspacePkg = JSON.parse(await fs.readFile(path.resolve(workspacePath, 'package.json'), 'utf8'));
96
- if (workspacePkg.name === process.env.TRV_MODULE) {
97
- folder = workspacePath;
120
+ const workspacePkg = await $readPackage(workspace.path);
121
+ if (workspacePkg?.name === mod) {
122
+ folder = workspace.path;
98
123
  } else {
99
124
  throw new Error(`Unable to resolve location for ${folder}`);
100
125
  }
101
126
  }
102
127
  }
103
128
 
104
- const mainPath = await $getModuleRoot(path.resolve(folder ?? '.'));
105
- const { name: mainModule, workspaces, travetto, version, description } = (await $getPkg(mainPath));
106
- const monoRepo = workspacePath !== mainPath || !!workspaces;
107
- const outputFolder = travetto?.outputFolder ?? '.trv_output';
129
+ return $findPackage(folder ?? '.');
130
+ }
108
131
 
109
- const moduleType = (await $getPkg(workspacePath)).type ?? 'commonjs';
110
- const mainFolder = mainPath === workspacePath ? '' : mainPath.replace(`${workspacePath}/`, '');
111
- /** @type {'yarn'|'npm'} */
112
- const packageManager = await fs.stat(path.resolve(workspacePath, 'yarn.lock')).then(() => 'yarn', () => 'npm');
132
+ /**
133
+ * Gets build context
134
+ * @param {string} [folder]
135
+ * @return {Promise<ManifestContext>}
136
+ */
137
+ export async function getManifestContext(folder) {
138
+ const workspace = await $resolveWorkspace(folder);
113
139
 
114
- const { version: frameworkVersion } = JSON.parse(await fs.readFile(req.resolve('@travetto/manifest/package.json'), 'utf8'));
140
+ const [mod, framework, compilerUrl] = await Promise.all([
141
+ $resolveModule(workspace, folder),
142
+ $readPackage(workspace.resolve('@travetto/manifest/package.json')),
143
+ $getCompilerUrl(workspace),
144
+ ]);
115
145
 
116
- const res = {
117
- moduleType,
118
- mainModule: mainModule ?? 'untitled', // When root package.json is missing a name
119
- mainFolder,
120
- workspacePath,
121
- monoRepo,
122
- outputFolder,
123
- toolFolder: '.trv_build',
124
- compilerFolder: '.trv_compiler',
125
- packageManager,
126
- version,
127
- description,
128
- frameworkVersion
146
+ return {
147
+ workspacePath: workspace.path,
148
+ monoRepo: workspace.mono,
149
+ packageManager: workspace.manager,
150
+ moduleType: workspace.type ?? 'commonjs',
151
+ outputFolder: workspace.travetto?.outputFolder ?? OUTPUT_FOLDER,
152
+ toolFolder: workspace.travetto?.toolFolder ?? TOOL_FOLDER,
153
+ compilerFolder: workspace.travetto?.compilerFolder ?? COMPILER_FOLDER,
154
+ compilerUrl,
155
+ frameworkVersion: framework?.version ?? '1.0.0',
156
+ mainModule: mod.name ?? 'untitled',
157
+ mainFolder: mod.path === workspace.path ? '' : mod.path.replace(`${workspace.path}/`, ''),
158
+ version: mod.version,
159
+ description: mod.description
129
160
  };
130
- return res;
131
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/manifest",
3
- "version": "3.3.1",
3
+ "version": "3.4.0-rc.0",
4
4
  "description": "Support for project indexing, manifesting, along with file watching",
5
5
  "keywords": [
6
6
  "path",
@@ -30,14 +30,6 @@
30
30
  "url": "https://github.com/travetto/travetto.git",
31
31
  "directory": "module/manifest"
32
32
  },
33
- "peerDependencies": {
34
- "@parcel/watcher": "^2.1.0"
35
- },
36
- "peerDependenciesMeta": {
37
- "@parcel/watcher": {
38
- "optional": true
39
- }
40
- },
41
33
  "travetto": {
42
34
  "displayName": "Manifest",
43
35
  "docBaseUrl": "https://github.com/travetto/travetto/tree/main"
package/src/delta.ts CHANGED
@@ -1,12 +1,11 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
-
4
1
  import {
5
2
  ManifestContext, ManifestModule, ManifestModuleCore, ManifestModuleFile,
6
3
  ManifestModuleFileType, ManifestModuleFolderType, ManifestRoot
7
4
  } from './types';
8
5
 
9
6
  import { ManifestModuleUtil } from './module';
7
+ import { ManifestFileUtil } from './file';
8
+ import { path } from './path';
10
9
 
11
10
  type DeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty';
12
11
  type DeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> };
@@ -46,7 +45,7 @@ export class ManifestDeltaUtil {
46
45
  for (const el of Object.keys(left.files)) {
47
46
  const output = ManifestModuleUtil.sourceToOutputExt(`${outputFolder}/${left.outputFolder}/${el}`);
48
47
  const [, , leftTs] = left.files[el];
49
- const stat = await fs.stat(output).catch(() => { });
48
+ const stat = await ManifestFileUtil.statFile(output);
50
49
  right.delete(ManifestModuleUtil.sourceToBlankExt(el));
51
50
 
52
51
  if (!stat) {
package/src/file.ts ADDED
@@ -0,0 +1,55 @@
1
+ import os from 'os';
2
+ import fs from 'fs/promises';
3
+ import { readFileSync } from 'fs';
4
+
5
+ import { path } from './path';
6
+ import type { ManifestContext } from './types';
7
+
8
+ export class ManifestFileUtil {
9
+ /**
10
+ * Write file and copy over when ready
11
+ */
12
+ static async bufferedFileWrite(file: string, content: string | object): Promise<string> {
13
+ const ext = path.extname(file);
14
+ const tempName = `${path.basename(file, ext)}.${process.ppid}.${process.pid}.${Date.now()}.${Math.random()}${ext}`;
15
+ await fs.mkdir(path.dirname(file), { recursive: true });
16
+ const temp = path.resolve(os.tmpdir(), tempName);
17
+ await fs.writeFile(temp, typeof content === 'string' ? content : JSON.stringify(content), 'utf8');
18
+ await fs.copyFile(temp, file);
19
+ fs.unlink(temp);
20
+ return file;
21
+ }
22
+
23
+ /**
24
+ * Read as json
25
+ */
26
+ static async readAsJson<T = unknown>(file: string): Promise<T> {
27
+ return JSON.parse(await fs.readFile(file, 'utf8'));
28
+ }
29
+
30
+ /**
31
+ * Read as json, sync
32
+ */
33
+ static readAsJsonSync<T = unknown>(file: string): T {
34
+ return JSON.parse(readFileSync(file, 'utf8'));
35
+ }
36
+
37
+ /**
38
+ * Stat file
39
+ */
40
+ static statFile(file: string): Promise<{ mtimeMs: number, ctimeMs: number } | undefined> {
41
+ return fs.stat(file).catch(() => undefined);
42
+ }
43
+
44
+ /**
45
+ * Resolve tool path for usage
46
+ */
47
+ static toolPath(ctx: ManifestContext | { manifest: ManifestContext }, rel: string, moduleSpecific = false): string {
48
+ ctx = 'manifest' in ctx ? ctx.manifest : ctx;
49
+ const parts = [rel];
50
+ if (moduleSpecific) {
51
+ parts.unshift('node_modules', ctx.mainModule);
52
+ }
53
+ return path.resolve(ctx.workspacePath, ctx.toolFolder, ...parts);
54
+ }
55
+ }
package/src/module.ts CHANGED
@@ -56,9 +56,6 @@ export class ManifestModuleUtil {
56
56
  const topFiles = new Set(mainSource ? [] : [...INDEX_FILES, 'package.json']);
57
57
  const out: string[] = [];
58
58
 
59
- if (!fs.stat(folder).catch(() => false)) {
60
- return out;
61
- }
62
59
  const stack: [string, number][] = [[folder, 0]];
63
60
  while (stack.length) {
64
61
  const popped = stack.pop();
package/src/package.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { readFileSync } from 'fs';
2
- import fs from 'fs/promises';
3
1
  import { createRequire } from 'module';
4
2
  import { execSync } from 'child_process';
5
3
 
6
4
  import { ManifestContext, Package, PackageRel, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types';
7
5
  import { path } from './path';
6
+ import { ManifestFileUtil } from './file';
8
7
 
9
8
  /**
10
9
  * Utilities for querying, traversing and reading package.json files.
@@ -99,10 +98,9 @@ export class PackageUtil {
99
98
  if (forceRead) {
100
99
  delete this.#cache[modulePath];
101
100
  }
102
- const res = this.#cache[modulePath] ??= JSON.parse(readFileSync(
101
+ const res = this.#cache[modulePath] ??= ManifestFileUtil.readAsJsonSync(
103
102
  modulePath.endsWith('.json') ? modulePath : path.resolve(modulePath, 'package.json'),
104
- 'utf8'
105
- ));
103
+ );
106
104
 
107
105
  res.name ??= 'untitled'; // If a package.json (root-only) is missing a name, allows for npx execution
108
106
 
@@ -159,11 +157,10 @@ export class PackageUtil {
159
157
  */
160
158
  static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> {
161
159
  if (!this.#workspaces[rootPath]) {
162
- await fs.mkdir(path.resolve(ctx.workspacePath, ctx.outputFolder), { recursive: true });
163
160
  const cache = path.resolve(ctx.workspacePath, ctx.outputFolder, 'workspaces.json');
164
161
  try {
165
- return JSON.parse(await fs.readFile(cache, 'utf8'));
166
- } catch {
162
+ return await ManifestFileUtil.readAsJson(cache);
163
+ } catch (err) {
167
164
  let out: PackageWorkspaceEntry[];
168
165
  switch (ctx.packageManager) {
169
166
  case 'npm': {
@@ -180,7 +177,7 @@ export class PackageUtil {
180
177
 
181
178
  this.#workspaces[rootPath] = out;
182
179
 
183
- await fs.writeFile(cache, JSON.stringify(out), 'utf8');
180
+ await ManifestFileUtil.bufferedFileWrite(cache, out);
184
181
  }
185
182
  }
186
183
  return this.#workspaces[rootPath];
package/src/path.ts CHANGED
@@ -9,7 +9,7 @@ const toPosix = (file: string): string => file.replaceAll('\\', '/');
9
9
  /**
10
10
  * Converts a given file name by replace all slashes, with platform dependent path separators
11
11
  */
12
- const toNative = (file: string): string => file.replaceAll('/', sep);
12
+ const toNative = (file: string): string => file.replace(/[\\\/]+/g, sep);
13
13
 
14
14
  const cwd = (): string => toPosix(process.cwd());
15
15
 
package/src/types.ts CHANGED
@@ -36,8 +36,9 @@ export type ManifestContext = {
36
36
  moduleType: 'module' | 'commonjs';
37
37
  packageManager: 'yarn' | 'npm';
38
38
  frameworkVersion: string;
39
- description: string;
39
+ description?: string;
40
40
  version: string;
41
+ compilerUrl: string;
41
42
  };
42
43
 
43
44
  export type ManifestRoot = ManifestContext & {
@@ -83,6 +84,9 @@ export type Package = {
83
84
  docBaseUrl?: string;
84
85
  docOutputs?: string[];
85
86
  outputFolder?: string;
87
+ toolFolder?: string;
88
+ compilerFolder?: string;
89
+ compilerUrl?: string;
86
90
  };
87
91
  workspaces?: string[];
88
92
  private?: boolean;
package/src/util.ts CHANGED
@@ -1,10 +1,7 @@
1
- import { readFileSync } from 'fs';
2
- import fs from 'fs/promises';
3
- import os from 'os';
4
-
5
1
  import { path } from './path';
6
2
  import { ManifestContext, ManifestRoot } from './types';
7
3
  import { ManifestModuleUtil } from './module';
4
+ import { ManifestFileUtil } from './file';
8
5
 
9
6
  const MANIFEST_FILE = 'manifest.json';
10
7
 
@@ -12,20 +9,6 @@ const MANIFEST_FILE = 'manifest.json';
12
9
  * Manifest utils
13
10
  */
14
11
  export class ManifestUtil {
15
- /**
16
- * Write file and copy over when ready
17
- */
18
- static async writeFileWithBuffer(file: string, content: string): Promise<string> {
19
- const ext = path.extname(file);
20
- const tempName = `${path.basename(file, ext)}.${process.ppid}.${process.pid}.${Date.now()}.${Math.random()}${ext}`;
21
- await fs.mkdir(path.dirname(file), { recursive: true });
22
- const temp = path.resolve(os.tmpdir(), tempName);
23
- await fs.writeFile(temp, content, 'utf8');
24
- await fs.copyFile(temp, file);
25
- fs.unlink(temp);
26
- return file;
27
- }
28
-
29
12
  /**
30
13
  * Build a manifest context
31
14
  * @param folder
@@ -75,7 +58,7 @@ export class ManifestUtil {
75
58
  if (!file.endsWith('.json')) {
76
59
  file = path.resolve(file, MANIFEST_FILE);
77
60
  }
78
- const manifest: ManifestRoot = JSON.parse(readFileSync(file, 'utf8'));
61
+ const manifest: ManifestRoot = ManifestFileUtil.readAsJsonSync(file);
79
62
  // Support packaged environments, by allowing empty outputFolder
80
63
  if (!manifest.outputFolder) {
81
64
  manifest.outputFolder = path.cwd();
@@ -88,7 +71,7 @@ export class ManifestUtil {
88
71
  * Write manifest for a given context, return location
89
72
  */
90
73
  static writeManifest(ctx: ManifestContext, manifest: ManifestRoot): Promise<string> {
91
- return this.writeFileWithBuffer(
74
+ return ManifestFileUtil.bufferedFileWrite(
92
75
  path.resolve(ctx.workspacePath, ctx.outputFolder, 'node_modules', ctx.mainModule, MANIFEST_FILE),
93
76
  JSON.stringify(manifest)
94
77
  );
@@ -102,8 +85,7 @@ export class ManifestUtil {
102
85
  location = path.resolve(location, MANIFEST_FILE);
103
86
  }
104
87
 
105
- await fs.mkdir(path.dirname(location), { recursive: true });
106
- await fs.writeFile(location, JSON.stringify(manifest), 'utf8');
88
+ await ManifestFileUtil.bufferedFileWrite(location, JSON.stringify(manifest));
107
89
 
108
90
  return location;
109
91
  }
package/src/watch.ts DELETED
@@ -1,205 +0,0 @@
1
- import { watch, Stats } from 'fs';
2
- import fs from 'fs/promises';
3
-
4
- import { path } from './path';
5
-
6
- async function getWatcher(): Promise<typeof import('@parcel/watcher')> {
7
- try {
8
- return await import('@parcel/watcher');
9
- } catch (err) {
10
- console.error('@parcel/watcher must be installed to use watching functionality');
11
- throw err;
12
- }
13
- }
14
-
15
- export type WatchEvent = { action: 'create' | 'update' | 'delete', file: string, folder: string };
16
-
17
- export type WatchFolder = {
18
- /**
19
- * Source folder
20
- */
21
- src: string;
22
- /**
23
- * Target folder name, useful for deconstructing
24
- */
25
- target?: string;
26
- /**
27
- * Filter events
28
- */
29
- filter?: (ev: WatchEvent) => boolean;
30
- /**
31
- * Only look at immediate folder
32
- */
33
- immediate?: boolean;
34
- /**
35
- * List of top level folders to ignore
36
- */
37
- ignore?: string[];
38
- /**
39
- * If watching a folder that doesn't exist, should it be created?
40
- */
41
- createMissing?: boolean;
42
- /**
43
- * Include files that start with '.'
44
- */
45
- includeHidden?: boolean;
46
- };
47
-
48
- export type WatchStream = AsyncIterable<WatchEvent> & { close: () => Promise<void>, add: (item: WatchEvent | WatchEvent[]) => void };
49
-
50
- const DEDUPE_THRESHOLD = 50;
51
-
52
- class Queue<X> implements AsyncIterator<X>, AsyncIterable<X> {
53
- #queue: X[] = [];
54
- #done = false;
55
- #ready: Promise<void>;
56
- #fire: (() => void);
57
- #onClose: (() => (void | Promise<void>))[] = [];
58
- #recentKeys = new Map<string, number>();
59
-
60
- constructor() {
61
- this.#ready = new Promise(r => this.#fire = r);
62
- }
63
-
64
- // Allow for iteration
65
- [Symbol.asyncIterator](): AsyncIterator<X> { return this; }
66
-
67
- async next(): Promise<IteratorResult<X>> {
68
- while (!this.#done && !this.#queue.length) {
69
- this.#recentKeys = new Map([...this.#recentKeys.entries()] // Cull before waiting
70
- .filter(([, time]) => (Date.now() - time) < DEDUPE_THRESHOLD));
71
- await this.#ready;
72
- this.#ready = new Promise(r => this.#fire = r);
73
- }
74
- return { value: this.#queue.shift()!, done: this.#done };
75
- }
76
-
77
- add(item: X | X[]): void {
78
- const now = Date.now();
79
- for (const value of Array.isArray(item) ? item : [item]) {
80
- const key = JSON.stringify(value);
81
- if ((now - (this.#recentKeys.get(key) ?? 0)) > DEDUPE_THRESHOLD) {
82
- this.#queue.push(value);
83
- this.#recentKeys.set(key, now);
84
- this.#fire();
85
- }
86
- }
87
- }
88
-
89
- registerOnClose(handler: () => (void | Promise<void>)): void {
90
- this.#onClose.push(handler);
91
- }
92
-
93
- async close(): Promise<void> {
94
- this.#done = true;
95
- this.#fire();
96
- await Promise.all(this.#onClose.map(x => x()));
97
- }
98
- }
99
-
100
- /**
101
- * Watch immediate files for a given folder
102
- */
103
- async function watchFolderImmediate(queue: Queue<WatchEvent>, options: WatchFolder): Promise<void> {
104
- const watchPath = path.resolve(options.src);
105
- const watcher = watch(watchPath, { persistent: true, encoding: 'utf8' });
106
- const lastStats: Record<string, Stats | undefined> = {};
107
- const invalidFilter = (el: string): boolean =>
108
- (el === '.' || el === '..' || (!options.includeHidden && el.startsWith('.')) || !!options.ignore?.includes(el));
109
-
110
- for (const el of await fs.readdir(watchPath)) {
111
- if (invalidFilter(el)) {
112
- continue;
113
- }
114
- const file = path.resolve(watchPath, el);
115
- lastStats[file] = await fs.stat(file);
116
- }
117
-
118
- const target = options.target ?? options.src;
119
-
120
- watcher.on('change', async (type: string, file: string): Promise<void> => {
121
- if (invalidFilter(file)) {
122
- return;
123
- }
124
-
125
- file = path.resolve(watchPath, file);
126
-
127
- const stat = await fs.stat(file).catch(() => undefined);
128
- const prevStat = lastStats[file];
129
- lastStats[file] = stat;
130
-
131
- if (prevStat?.mtimeMs === stat?.mtimeMs) {
132
- return;
133
- }
134
- let ev: WatchEvent;
135
- if (prevStat && !stat) {
136
- ev = { action: 'delete', file, folder: target };
137
- } else if (!prevStat && stat) {
138
- ev = { action: 'create', file, folder: target };
139
- } else {
140
- ev = { action: 'update', file, folder: target };
141
- }
142
- if (!options.filter || options.filter(ev)) {
143
- queue.add(ev);
144
- }
145
- });
146
-
147
- queue.registerOnClose(() => watcher.close());
148
- }
149
-
150
- /**
151
- * Watch recursive files for a given folder
152
- */
153
- async function watchFolderRecursive(queue: Queue<WatchEvent>, options: WatchFolder): Promise<void> {
154
- const lib = await getWatcher();
155
- const target = options.target ?? options.src;
156
-
157
- if (await fs.stat(options.src).then(() => true, () => options.createMissing)) {
158
- await fs.mkdir(options.src, { recursive: true });
159
- const ignore = (await fs.readdir(options.src)).filter(x => x.startsWith('.') && x.length > 2);
160
- const cleanup = await lib.subscribe(options.src, async (err, events) => {
161
- 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
167
- }
168
- }
169
-
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());
181
- }
182
- }
183
-
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;
205
- }