@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 +12 -63
- package/__index__.ts +2 -2
- package/bin/context.js +113 -83
- package/package.json +1 -9
- package/src/delta.ts +3 -4
- package/src/file.ts +55 -0
- package/src/module.ts +0 -3
- package/src/package.ts +6 -9
- package/src/path.ts +1 -1
- package/src/types.ts +5 -1
- package/src/util.ts +4 -22
- package/src/watch.ts +0 -205
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#
|
|
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#
|
|
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
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
|
-
*
|
|
13
|
-
* @param {string}
|
|
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 $
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
*
|
|
29
|
+
* Find package.json for a given folder
|
|
27
30
|
* @param {string} dir
|
|
28
|
-
* @return {Promise<
|
|
31
|
+
* @return {Promise<Pkg>}
|
|
29
32
|
*/
|
|
30
|
-
async function $
|
|
33
|
+
async function $findPackage(dir) {
|
|
31
34
|
let prev;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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<
|
|
49
|
+
* @return {Promise<Workspace>}
|
|
52
50
|
*/
|
|
53
|
-
async function $
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @return {Promise<ManifestContext>}
|
|
83
|
+
* Get Compiler url
|
|
84
|
+
* @param {Workspace} ws
|
|
81
85
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
if (/[.](t|j)s$/.test(
|
|
90
|
-
process.env.TRV_MODULE = await $
|
|
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(
|
|
118
|
+
folder = path.dirname(workspace.resolve(`${mod}/package.json`));
|
|
94
119
|
} catch {
|
|
95
|
-
const workspacePkg =
|
|
96
|
-
if (workspacePkg
|
|
97
|
-
folder =
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
const monoRepo = workspacePath !== mainPath || !!workspaces;
|
|
107
|
-
const outputFolder = travetto?.outputFolder ?? '.trv_output';
|
|
129
|
+
return $findPackage(folder ?? '.');
|
|
130
|
+
}
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
"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
|
|
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] ??=
|
|
101
|
+
const res = this.#cache[modulePath] ??= ManifestFileUtil.readAsJsonSync(
|
|
103
102
|
modulePath.endsWith('.json') ? modulePath : path.resolve(modulePath, 'package.json'),
|
|
104
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
}
|