@lage-run/hasher 1.1.2 → 1.2.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/CHANGELOG.json +5 -50
- package/CHANGELOG.md +4 -20
- package/lib/PackageTree.d.ts +9 -5
- package/lib/PackageTree.js +75 -54
- package/lib/TargetHasher.d.ts +1 -1
- package/lib/TargetHasher.js +27 -24
- package/package.json +4 -2
package/CHANGELOG.json
CHANGED
|
@@ -2,61 +2,16 @@
|
|
|
2
2
|
"name": "@lage-run/hasher",
|
|
3
3
|
"entries": [
|
|
4
4
|
{
|
|
5
|
-
"date": "
|
|
6
|
-
"version": "1.1
|
|
7
|
-
"tag": "@lage-run/hasher_v1.1
|
|
5
|
+
"date": "Tue, 25 Jun 2024 22:03:26 GMT",
|
|
6
|
+
"version": "1.2.1",
|
|
7
|
+
"tag": "@lage-run/hasher_v1.2.1",
|
|
8
8
|
"comments": {
|
|
9
9
|
"patch": [
|
|
10
10
|
{
|
|
11
11
|
"author": "kchau@microsoft.com_msteamsmdb",
|
|
12
12
|
"package": "@lage-run/hasher",
|
|
13
|
-
"commit": "
|
|
14
|
-
"comment": "
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"date": "Sun, 05 May 2024 22:55:45 GMT",
|
|
21
|
-
"version": "1.1.1",
|
|
22
|
-
"tag": "@lage-run/hasher_v1.1.1",
|
|
23
|
-
"comments": {
|
|
24
|
-
"patch": [
|
|
25
|
-
{
|
|
26
|
-
"author": "kchau@microsoft.com",
|
|
27
|
-
"package": "@lage-run/hasher",
|
|
28
|
-
"commit": "1e36de04ab83fc0cde38062fc1543e4b12902166",
|
|
29
|
-
"comment": "fixing hashing issues related to rust panic"
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"date": "Wed, 17 Apr 2024 23:20:47 GMT",
|
|
36
|
-
"version": "1.1.0",
|
|
37
|
-
"tag": "@lage-run/hasher_v1.1.0",
|
|
38
|
-
"comments": {
|
|
39
|
-
"none": [
|
|
40
|
-
{
|
|
41
|
-
"author": "elcraig@microsoft.com",
|
|
42
|
-
"package": "@lage-run/hasher",
|
|
43
|
-
"commit": "fb4fcb8419cc778210104d7d04102fc95df13d5b",
|
|
44
|
-
"comment": "Update formatting"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"date": "Fri, 15 Mar 2024 04:35:11 GMT",
|
|
51
|
-
"version": "1.1.0",
|
|
52
|
-
"tag": "@lage-run/hasher_v1.1.0",
|
|
53
|
-
"comments": {
|
|
54
|
-
"minor": [
|
|
55
|
-
{
|
|
56
|
-
"author": "kchau@microsoft.com",
|
|
57
|
-
"package": "@lage-run/hasher",
|
|
58
|
-
"commit": "71283d4af8888454c953e5228c97bfbb471c15ba",
|
|
59
|
-
"comment": "perf optimizations"
|
|
13
|
+
"commit": "440bbd19c317982a187383a5a35befbea71f22c5",
|
|
14
|
+
"comment": "reverting all the hasher changes"
|
|
60
15
|
}
|
|
61
16
|
]
|
|
62
17
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
# Change Log - @lage-run/hasher
|
|
2
2
|
|
|
3
|
-
This log was last generated on
|
|
3
|
+
This log was last generated on Tue, 25 Jun 2024 22:03:26 GMT and should not be manually modified.
|
|
4
4
|
|
|
5
5
|
<!-- Start content -->
|
|
6
6
|
|
|
7
|
-
## 1.1
|
|
7
|
+
## 1.2.1
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Tue, 25 Jun 2024 22:03:26 GMT
|
|
10
10
|
|
|
11
11
|
### Patches
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
## 1.1.1
|
|
16
|
-
|
|
17
|
-
Sun, 05 May 2024 22:55:45 GMT
|
|
18
|
-
|
|
19
|
-
### Patches
|
|
20
|
-
|
|
21
|
-
- fixing hashing issues related to rust panic (kchau@microsoft.com)
|
|
22
|
-
|
|
23
|
-
## 1.1.0
|
|
24
|
-
|
|
25
|
-
Fri, 15 Mar 2024 04:35:11 GMT
|
|
26
|
-
|
|
27
|
-
### Minor changes
|
|
28
|
-
|
|
29
|
-
- perf optimizations (kchau@microsoft.com)
|
|
13
|
+
- reverting all the hasher changes (kchau@microsoft.com_msteamsmdb)
|
|
30
14
|
|
|
31
15
|
## 1.0.7
|
|
32
16
|
|
package/lib/PackageTree.d.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { type PackageInfos } from "workspace-tools";
|
|
2
|
+
export interface PackageTreeOptions {
|
|
2
3
|
root: string;
|
|
4
|
+
packageInfos: PackageInfos;
|
|
3
5
|
includeUntracked: boolean;
|
|
4
6
|
}
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
8
|
+
* Package Tree keeps a data structure to quickly find all files in a package.
|
|
9
|
+
*
|
|
10
|
+
* TODO: add a watcher to make sure the tree is up to date during a "watched" run.
|
|
7
11
|
*/
|
|
8
12
|
export declare class PackageTree {
|
|
9
13
|
#private;
|
|
10
14
|
private options;
|
|
11
|
-
constructor(options:
|
|
15
|
+
constructor(options: PackageTreeOptions);
|
|
12
16
|
reset(): void;
|
|
13
17
|
initialize(): Promise<void>;
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
addToPackageTree(filePaths: string[]): Promise<void>;
|
|
19
|
+
getPackageFiles(packageName: string, patterns: string[]): string[];
|
|
16
20
|
}
|
package/lib/PackageTree.js
CHANGED
|
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "PackageTree", {
|
|
|
11
11
|
const _execa = /*#__PURE__*/ _interop_require_default(require("execa"));
|
|
12
12
|
const _path = /*#__PURE__*/ _interop_require_default(require("path"));
|
|
13
13
|
const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
|
|
14
|
+
const _micromatch = /*#__PURE__*/ _interop_require_default(require("micromatch"));
|
|
14
15
|
function _check_private_redeclaration(obj, privateCollection) {
|
|
15
16
|
if (privateCollection.has(obj)) {
|
|
16
17
|
throw new TypeError("Cannot initialize the same private elements twice on an object");
|
|
@@ -51,16 +52,6 @@ function _class_private_field_set(receiver, privateMap, value) {
|
|
|
51
52
|
_class_apply_descriptor_set(receiver, descriptor, value);
|
|
52
53
|
return value;
|
|
53
54
|
}
|
|
54
|
-
function _class_private_method_get(receiver, privateSet, fn) {
|
|
55
|
-
if (!privateSet.has(receiver)) {
|
|
56
|
-
throw new TypeError("attempted to get private field on non-instance");
|
|
57
|
-
}
|
|
58
|
-
return fn;
|
|
59
|
-
}
|
|
60
|
-
function _class_private_method_init(obj, privateSet) {
|
|
61
|
-
_check_private_redeclaration(obj, privateSet);
|
|
62
|
-
privateSet.add(obj);
|
|
63
|
-
}
|
|
64
55
|
function _define_property(obj, key, value) {
|
|
65
56
|
if (key in obj) {
|
|
66
57
|
Object.defineProperty(obj, key, {
|
|
@@ -79,7 +70,7 @@ function _interop_require_default(obj) {
|
|
|
79
70
|
default: obj
|
|
80
71
|
};
|
|
81
72
|
}
|
|
82
|
-
var _tree = /*#__PURE__*/ new WeakMap(), _packageFiles = /*#__PURE__*/ new WeakMap(), _memoizedPackageFiles = /*#__PURE__*/ new WeakMap()
|
|
73
|
+
var _tree = /*#__PURE__*/ new WeakMap(), _packageFiles = /*#__PURE__*/ new WeakMap(), _memoizedPackageFiles = /*#__PURE__*/ new WeakMap();
|
|
83
74
|
class PackageTree {
|
|
84
75
|
reset() {
|
|
85
76
|
_class_private_field_set(this, _tree, {});
|
|
@@ -87,19 +78,86 @@ class PackageTree {
|
|
|
87
78
|
_class_private_field_set(this, _memoizedPackageFiles, {});
|
|
88
79
|
}
|
|
89
80
|
async initialize() {
|
|
81
|
+
const { root , includeUntracked , packageInfos } = this.options;
|
|
90
82
|
this.reset();
|
|
83
|
+
// Generate path tree of all packages in workspace (scale: ~2000 * ~3)
|
|
84
|
+
for (const info of Object.values(packageInfos)){
|
|
85
|
+
const packagePath = _path.default.dirname(info.packageJsonPath);
|
|
86
|
+
const pathParts = _path.default.relative(root, packagePath).split(/[\\/]/);
|
|
87
|
+
let currentNode = _class_private_field_get(this, _tree);
|
|
88
|
+
for (const part of pathParts){
|
|
89
|
+
currentNode[part] = currentNode[part] || {};
|
|
90
|
+
currentNode = currentNode[part];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Get all files in the workspace (scale: ~2000) according to git
|
|
94
|
+
const lsFilesResults = await (0, _execa.default)("git", [
|
|
95
|
+
"ls-files",
|
|
96
|
+
"-z"
|
|
97
|
+
], {
|
|
98
|
+
cwd: root
|
|
99
|
+
});
|
|
100
|
+
if (lsFilesResults.exitCode === 0) {
|
|
101
|
+
const files = lsFilesResults.stdout.split("\0").filter((f)=>Boolean(f) && _fs.default.existsSync(_path.default.join(root, f)));
|
|
102
|
+
this.addToPackageTree(files);
|
|
103
|
+
}
|
|
104
|
+
if (includeUntracked) {
|
|
105
|
+
// Also get all untracked files in the workspace according to git
|
|
106
|
+
const lsOtherResults = await (0, _execa.default)("git", [
|
|
107
|
+
"ls-files",
|
|
108
|
+
"-o",
|
|
109
|
+
"--exclude-standard"
|
|
110
|
+
], {
|
|
111
|
+
cwd: root
|
|
112
|
+
});
|
|
113
|
+
if (lsOtherResults.exitCode === 0) {
|
|
114
|
+
const files = lsOtherResults.stdout.split("\0").filter(Boolean);
|
|
115
|
+
this.addToPackageTree(files);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
91
118
|
}
|
|
92
|
-
async
|
|
93
|
-
|
|
119
|
+
async addToPackageTree(filePaths) {
|
|
120
|
+
// key: path/to/package (packageRoot), value: array of a tuple of [file, hash]
|
|
121
|
+
const packageFiles = _class_private_field_get(this, _packageFiles);
|
|
122
|
+
for (const entry of filePaths){
|
|
123
|
+
const pathParts = entry.split(/[\\/]/);
|
|
124
|
+
let node = _class_private_field_get(this, _tree);
|
|
125
|
+
const packagePathParts = [];
|
|
126
|
+
for (const part of pathParts){
|
|
127
|
+
if (node[part]) {
|
|
128
|
+
node = node[part];
|
|
129
|
+
packagePathParts.push(part);
|
|
130
|
+
} else {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const packageRoot = packagePathParts.join("/");
|
|
135
|
+
packageFiles[packageRoot] = packageFiles[packageRoot] || [];
|
|
136
|
+
packageFiles[packageRoot].push(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
getPackageFiles(packageName, patterns) {
|
|
140
|
+
const { root , packageInfos } = this.options;
|
|
141
|
+
const packagePath = _path.default.relative(root, _path.default.dirname(packageInfos[packageName].packageJsonPath)).replace(/\\/g, "/");
|
|
142
|
+
const packageFiles = _class_private_field_get(this, _packageFiles)[packagePath];
|
|
143
|
+
if (!packageFiles) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const key = `${packageName}\0${patterns.join("\0")}`;
|
|
94
147
|
if (!_class_private_field_get(this, _memoizedPackageFiles)[key]) {
|
|
95
|
-
const
|
|
96
|
-
|
|
148
|
+
const packagePatterns = patterns.map((pattern)=>{
|
|
149
|
+
if (pattern.startsWith("!")) {
|
|
150
|
+
return `!${_path.default.join(packagePath, pattern.slice(1)).replace(/\\/g, "/")}`;
|
|
151
|
+
}
|
|
152
|
+
return _path.default.join(packagePath, pattern).replace(/\\/g, "/");
|
|
153
|
+
});
|
|
154
|
+
_class_private_field_get(this, _memoizedPackageFiles)[key] = (0, _micromatch.default)(packageFiles, packagePatterns, {
|
|
155
|
+
dot: true
|
|
156
|
+
});
|
|
97
157
|
}
|
|
98
158
|
return _class_private_field_get(this, _memoizedPackageFiles)[key];
|
|
99
159
|
}
|
|
100
|
-
cleanup() {}
|
|
101
160
|
constructor(options){
|
|
102
|
-
_class_private_method_init(this, _findFilesFromGitTree);
|
|
103
161
|
_define_property(this, "options", void 0);
|
|
104
162
|
_class_private_field_init(this, _tree, {
|
|
105
163
|
writable: true,
|
|
@@ -119,40 +177,3 @@ class PackageTree {
|
|
|
119
177
|
_class_private_field_set(this, _memoizedPackageFiles, {});
|
|
120
178
|
}
|
|
121
179
|
}
|
|
122
|
-
async function findFilesFromGitTree(packagePath, patterns) {
|
|
123
|
-
const { includeUntracked } = this.options;
|
|
124
|
-
const cwd = _path.default.isAbsolute(packagePath) ? packagePath : _path.default.join(this.options.root, packagePath);
|
|
125
|
-
const trackedPromise = (0, _execa.default)("git", [
|
|
126
|
-
"ls-files",
|
|
127
|
-
"-z",
|
|
128
|
-
...patterns.filter((p)=>!p.startsWith("!")).map((p)=>`:(glob)${p}`),
|
|
129
|
-
...patterns.filter((p)=>p.startsWith("!")).map((p)=>`:(exclude,glob)${p.slice(1)}`)
|
|
130
|
-
], {
|
|
131
|
-
cwd
|
|
132
|
-
}).then((lsFilesResults)=>{
|
|
133
|
-
if (lsFilesResults.exitCode === 0) {
|
|
134
|
-
return lsFilesResults.stdout.split("\0").filter((f)=>Boolean(f) && _fs.default.existsSync(_path.default.join(cwd, f)));
|
|
135
|
-
}
|
|
136
|
-
return [];
|
|
137
|
-
});
|
|
138
|
-
const untrackedPromise = includeUntracked ? (0, _execa.default)("git", [
|
|
139
|
-
"ls-files",
|
|
140
|
-
"-z",
|
|
141
|
-
"-o",
|
|
142
|
-
"--exclude-standard",
|
|
143
|
-
...patterns.filter((p)=>!p.startsWith("!")).map((p)=>`:(glob)${p}`),
|
|
144
|
-
...patterns.filter((p)=>p.startsWith("!")).map((p)=>`:(exclude,glob)${p.slice(1)}`)
|
|
145
|
-
], {
|
|
146
|
-
cwd
|
|
147
|
-
}).then((lsOtherResults)=>{
|
|
148
|
-
if (lsOtherResults.exitCode === 0) {
|
|
149
|
-
return lsOtherResults.stdout.split("\0").filter((f)=>Boolean(f));
|
|
150
|
-
}
|
|
151
|
-
return [];
|
|
152
|
-
}) : Promise.resolve([]);
|
|
153
|
-
const [trackedFiles, untrackedFiles] = await Promise.all([
|
|
154
|
-
trackedPromise,
|
|
155
|
-
untrackedPromise
|
|
156
|
-
]);
|
|
157
|
-
return trackedFiles.concat(untrackedFiles);
|
|
158
|
-
}
|
package/lib/TargetHasher.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare class TargetHasher {
|
|
|
32
32
|
private options;
|
|
33
33
|
logger: Logger | undefined;
|
|
34
34
|
fileHasher: FileHasher;
|
|
35
|
-
packageTree: PackageTree;
|
|
35
|
+
packageTree: PackageTree | undefined;
|
|
36
36
|
initializedPromise: Promise<unknown> | undefined;
|
|
37
37
|
packageInfos: PackageInfos;
|
|
38
38
|
workspaceInfo: WorkspaceInfo | undefined;
|
package/lib/TargetHasher.js
CHANGED
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "TargetHasher", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _globhasher = require("glob-hasher");
|
|
12
|
+
const _fastglob = /*#__PURE__*/ _interop_require_default(require("fast-glob"));
|
|
12
13
|
const _fs = /*#__PURE__*/ _interop_require_default(require("fs"));
|
|
13
14
|
const _path = /*#__PURE__*/ _interop_require_default(require("path"));
|
|
14
15
|
const _workspacetools = require("workspace-tools");
|
|
@@ -108,19 +109,26 @@ class TargetHasher {
|
|
|
108
109
|
await this.initializedPromise;
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
this.initializedPromise = Promise.all([
|
|
113
|
+
this.fileHasher.readManifest().then(()=>(0, _fastglob.default)(environmentGlob, {
|
|
114
|
+
cwd: root
|
|
115
|
+
})).then((files)=>this.fileHasher.hash(files)).then((hash)=>this.globalInputsHash = hash),
|
|
116
|
+
(0, _workspacetools.getWorkspacesAsync)(root).then((workspaceInfo)=>this.workspaceInfo = workspaceInfo).then(()=>{
|
|
117
|
+
this.packageInfos = this.getPackageInfos(this.workspaceInfo);
|
|
118
|
+
this.dependencyMap = (0, _workspacetools.createDependencyMap)(this.packageInfos, {
|
|
119
|
+
withDevDependencies: true,
|
|
120
|
+
withPeerDependencies: false
|
|
121
|
+
});
|
|
122
|
+
this.packageTree = new _PackageTree.PackageTree({
|
|
123
|
+
root,
|
|
124
|
+
packageInfos: this.packageInfos,
|
|
125
|
+
// TODO: (optimization) false if process.env.TF_BUILD || process.env.CI
|
|
126
|
+
includeUntracked: true
|
|
127
|
+
});
|
|
128
|
+
return this.packageTree.initialize();
|
|
129
|
+
}),
|
|
130
|
+
(0, _workspacetools.parseLockFile)(root).then((lockInfo)=>this.lockInfo = lockInfo)
|
|
131
|
+
]);
|
|
124
132
|
await this.initializedPromise;
|
|
125
133
|
if (this.logger !== undefined) {
|
|
126
134
|
const globalInputsHash = (0, _hashStrings.hashStrings)(Object.values(this.globalInputsHash ?? {}));
|
|
@@ -137,11 +145,13 @@ class TargetHasher {
|
|
|
137
145
|
if (!target.inputs) {
|
|
138
146
|
throw new Error("Root-level targets must have `inputs` defined if it has cache enabled.");
|
|
139
147
|
}
|
|
140
|
-
const files = await
|
|
148
|
+
const files = await (0, _fastglob.default)(target.inputs, {
|
|
149
|
+
cwd: root
|
|
150
|
+
});
|
|
141
151
|
const fileFashes = (0, _globhasher.hash)(files, {
|
|
142
152
|
cwd: root
|
|
143
153
|
}) ?? {};
|
|
144
|
-
const hashes = Object.values(fileFashes)
|
|
154
|
+
const hashes = Object.values(fileFashes);
|
|
145
155
|
return (0, _hashStrings.hashStrings)(hashes);
|
|
146
156
|
}
|
|
147
157
|
// 1. add hash of target's inputs
|
|
@@ -165,9 +175,7 @@ class TargetHasher {
|
|
|
165
175
|
const packagePatterns = this.expandInputPatterns(inputs, target);
|
|
166
176
|
const files = [];
|
|
167
177
|
for (const [pkg, patterns] of Object.entries(packagePatterns)){
|
|
168
|
-
const
|
|
169
|
-
const packagePath = _path.default.relative(root, _path.default.dirname(this.packageInfos[pkg].packageJsonPath)).replace(/\\/g, "/");
|
|
170
|
-
const packageFiles = await this.packageTree.findFilesInPath(packagePath, patterns);
|
|
178
|
+
const packageFiles = this.packageTree.getPackageFiles(pkg, patterns);
|
|
171
179
|
files.push(...packageFiles);
|
|
172
180
|
}
|
|
173
181
|
const fileHashes = this.fileHasher.hash(files) ?? {}; // this list is sorted by file name
|
|
@@ -189,8 +197,7 @@ class TargetHasher {
|
|
|
189
197
|
return hashString;
|
|
190
198
|
}
|
|
191
199
|
async cleanup() {
|
|
192
|
-
this.
|
|
193
|
-
this.fileHasher.writeManifest();
|
|
200
|
+
await this.fileHasher.writeManifest();
|
|
194
201
|
}
|
|
195
202
|
constructor(options){
|
|
196
203
|
_define_property(this, "options", void 0);
|
|
@@ -216,9 +223,5 @@ class TargetHasher {
|
|
|
216
223
|
this.fileHasher = new _FileHasher.FileHasher({
|
|
217
224
|
root
|
|
218
225
|
});
|
|
219
|
-
this.packageTree = new _PackageTree.PackageTree({
|
|
220
|
-
root,
|
|
221
|
-
includeUntracked: true
|
|
222
|
-
});
|
|
223
226
|
}
|
|
224
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lage-run/hasher",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Hasher for Lage Targets",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/microsoft/lage"
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
"@lage-run/logger": "^1.3.0",
|
|
20
20
|
"execa": "5.1.1",
|
|
21
21
|
"workspace-tools": "0.36.4",
|
|
22
|
+
"fast-glob": "3.3.1",
|
|
22
23
|
"glob-hasher": "^1.4.2",
|
|
23
|
-
"graceful-fs": "4.2.11"
|
|
24
|
+
"graceful-fs": "4.2.11",
|
|
25
|
+
"micromatch": "4.0.5"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"@lage-run/monorepo-fixture": "*",
|