@graftland/package-manager 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +17 -2
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +41 -0
- package/dist/manifest.js.map +1 -0
- package/package.json +32 -3
- package/src/index.ts +391 -0
- package/src/manifest.ts +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @graftland/package-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installs dependencies with npm, pnpm or Yarn, and rolls back on failure.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @graftland/package-manager effect
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`make` detects the project's package manager. Its `install` adds, updates or
|
|
10
|
+
removes dependencies, and restores `package.json` and the lockfile if installing
|
|
11
|
+
fails. `graft add` and `graft remove` use it.
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
- [Command line](https://graftland.github.io/graft/cli/)
|
|
16
|
+
|
|
17
|
+
Part of [Graft](https://github.com/graftland/graft). Licensed under
|
|
18
|
+
[Apache 2.0](https://github.com/graftland/graft/blob/main/LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Changes, PackageName } from "./manifest.js";
|
|
2
|
+
import { Effect, FileSystem, HashMap, Path, PlatformError, Schema } from "effect";
|
|
3
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
4
|
+
export declare const Agent: Schema.Literals<readonly ["npm", "pnpm", "yarn-classic", "yarn-berry"]>;
|
|
5
|
+
export type Agent = typeof Agent.Type;
|
|
6
|
+
declare const PackageManagerError_base: Schema.Class<PackageManagerError, Schema.TaggedStruct<"PackageManagerError", {
|
|
7
|
+
readonly operation: Schema.Literals<readonly ["detect", "install", "recover", "state"]>;
|
|
8
|
+
readonly message: Schema.String;
|
|
9
|
+
readonly exitCode: Schema.optionalKey<Schema.Int>;
|
|
10
|
+
}>, import("effect/Cause").YieldableError & {
|
|
11
|
+
readonly brand: unique symbol;
|
|
12
|
+
}>;
|
|
13
|
+
export declare class PackageManagerError extends PackageManagerError_base {}
|
|
14
|
+
export type Error = PackageManagerError | PlatformError.PlatformError | Schema.SchemaError;
|
|
15
|
+
export interface Options {
|
|
16
|
+
/** Project directory, containing package.json. Use the workspace root for workspace installs. */
|
|
17
|
+
readonly directory: string;
|
|
18
|
+
/** Explicit selection takes precedence over detection. */
|
|
19
|
+
readonly agent?: Agent;
|
|
20
|
+
/** Optional npm_config_user_agent supplied by the caller; used only as a fallback. */
|
|
21
|
+
readonly userAgent?: string;
|
|
22
|
+
}
|
|
23
|
+
/** packageManager, then lockfile, then caller user-agent, then npm. Conflicting lockfiles fail. */
|
|
24
|
+
export declare const detectAgent: (options: Options) => Effect.Effect<"npm" | "pnpm" | "yarn-berry" | "yarn-classic", PackageManagerError | PlatformError.PlatformError | Schema.SchemaError, FileSystem.FileSystem | Path.Path>;
|
|
25
|
+
export interface PackageState {
|
|
26
|
+
readonly spec: string;
|
|
27
|
+
/** Missing optional or not-yet-installed dependencies have no version/path. */
|
|
28
|
+
readonly version: string | undefined;
|
|
29
|
+
readonly path: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Direct dependencies only, keyed by their manifest names (including aliases). */
|
|
32
|
+
export type State = HashMap.HashMap<string, PackageState>;
|
|
33
|
+
export interface PackageChange {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly before: PackageState | undefined;
|
|
36
|
+
readonly after: PackageState | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface StateDiff {
|
|
39
|
+
readonly added: ReadonlyArray<PackageChange>;
|
|
40
|
+
readonly removed: ReadonlyArray<PackageChange>;
|
|
41
|
+
readonly updated: ReadonlyArray<PackageChange>;
|
|
42
|
+
}
|
|
43
|
+
export declare const diffState: (before: State, after: State) => StateDiff;
|
|
44
|
+
export interface InstallResult extends StateDiff {
|
|
45
|
+
readonly agent: Agent;
|
|
46
|
+
}
|
|
47
|
+
export interface PackageManager {
|
|
48
|
+
readonly directory: string;
|
|
49
|
+
readonly agent: Agent;
|
|
50
|
+
readonly readState: Effect.Effect<State, Error>;
|
|
51
|
+
/** Serialized per manager. Empty changes installs the current manifest, including first installs. */
|
|
52
|
+
readonly install: (changes: Changes) => Effect.Effect<InstallResult, Error>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Own one manager per project. Failed or interrupted installs restore the exact
|
|
56
|
+
* manifest/lockfiles, then attempt to repair node_modules when a lockfile existed.
|
|
57
|
+
* Recovery failures are combined with the original cause. No crash recovery is promised.
|
|
58
|
+
*/
|
|
59
|
+
export declare const make: (options: Options) => Effect.Effect<PackageManager, Error, FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner>;
|
|
60
|
+
export { Changes, PackageName };
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;qBA0Ba,OAAK,OAAA;YAMN,eAAe,MAAM;;;;;;WAGd;;qBAFN,4BAA4B;YAQ7B,QACX,sBAAsB,cAAc,gBAAgB,OAAO;iBAC3C;;WAEP;;WAEA,QAAQ;;WAER;;;qBAuBG,cAAW,SAAA,YAAA,OAAA,uDAAA,sBAAA,cAAA,gBAAA,OAAA,aAAA,WAAA,aAAA,KAAA;iBAyDP;WACP;;WAEA;WACA;;;YAGE,QAAQ,QAAQ,gBAAgB;iBAC3B;WACP;WACA,QAAQ;WACR,OAAO;;iBAEA;WACP,OAAO,cAAc;WACrB,SAAS,cAAc;WACvB,SAAS,cAAc;;qBAEpB,YAAS,QAAY,OAAK,OAAS,UAAQ;iBA0BvC,sBAAsB;WAC7B,OAAO;;iBAEA;WACP;WACA,OAAO;WACP,WAAW,OAAO,OAAO,OAAO;;WAEhC,UAAU,SAAS,YAAY,OAAO,OAAO,eAAe;;;;;;;qBAQzD,OAAI,SACP,YACP,OAAO,OACT,gBACA,OACA,WAAW,aAAa,KAAK,OAAO,oBAAoB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Changes, PackageName, applyChanges, decodeManifest, encodeManifest, fields } from "./manifest.js";
|
|
2
|
+
import { Array, Effect, Exit, FileSystem, HashMap, Option, Path, Record, Schema, Semaphore, String } from "effect";
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
|
+
// src/index.ts
|
|
5
|
+
/** Non-interactive package installation. Provide platform services at the application boundary. */
|
|
6
|
+
const Agent = Schema.Literals([
|
|
7
|
+
"npm",
|
|
8
|
+
"pnpm",
|
|
9
|
+
"yarn-classic",
|
|
10
|
+
"yarn-berry"
|
|
11
|
+
]);
|
|
12
|
+
var PackageManagerError = class extends Schema.TaggedError()("PackageManagerError", {
|
|
13
|
+
operation: Schema.Literals([
|
|
14
|
+
"detect",
|
|
15
|
+
"install",
|
|
16
|
+
"recover",
|
|
17
|
+
"state"
|
|
18
|
+
]),
|
|
19
|
+
message: Schema.String,
|
|
20
|
+
exitCode: Schema.optionalKey(Schema.Int)
|
|
21
|
+
}) {};
|
|
22
|
+
const lockfiles = [
|
|
23
|
+
"package-lock.json",
|
|
24
|
+
"npm-shrinkwrap.json",
|
|
25
|
+
"pnpm-lock.yaml",
|
|
26
|
+
"yarn.lock"
|
|
27
|
+
];
|
|
28
|
+
const backupFiles = [
|
|
29
|
+
"package.json",
|
|
30
|
+
...lockfiles,
|
|
31
|
+
".npmrc",
|
|
32
|
+
"pnpm-workspace.yaml",
|
|
33
|
+
".yarnrc",
|
|
34
|
+
".yarnrc.yml",
|
|
35
|
+
".yarn/install-state.gz",
|
|
36
|
+
".pnp.cjs",
|
|
37
|
+
".pnp.loader.mjs"
|
|
38
|
+
];
|
|
39
|
+
const executable = (agent) => String.startsWith("yarn")(agent) ? "yarn" : agent;
|
|
40
|
+
/** packageManager, then lockfile, then caller user-agent, then npm. Conflicting lockfiles fail. */
|
|
41
|
+
const detectAgent = Effect.fnUntraced(function* (options) {
|
|
42
|
+
if (options.agent !== void 0) return yield* Schema.decodeEffect(Agent)(options.agent);
|
|
43
|
+
const fs = yield* FileSystem.FileSystem;
|
|
44
|
+
const path = yield* Path.Path;
|
|
45
|
+
const manifest = yield* fs.readFileString(path.join(options.directory, "package.json")).pipe(Effect.flatMap(decodeManifest));
|
|
46
|
+
if (manifest.packageManager !== void 0) {
|
|
47
|
+
const match = /^(npm|pnpm|yarn)@(\d+)\.\d+\.\d+(?:[-+].*)?$/.exec(manifest.packageManager);
|
|
48
|
+
if (match === null) return yield* PackageManagerError.make({
|
|
49
|
+
operation: "detect",
|
|
50
|
+
message: `Unsupported packageManager: ${manifest.packageManager}`
|
|
51
|
+
});
|
|
52
|
+
return match[1] === "yarn" ? match[2] === "1" ? "yarn-classic" : "yarn-berry" : match[1];
|
|
53
|
+
}
|
|
54
|
+
const found = yield* Effect.filter(lockfiles, (file) => fs.exists(path.join(options.directory, file)));
|
|
55
|
+
const kinds = Array.dedupe(Array.map(found, (file) => file === "pnpm-lock.yaml" ? "pnpm" : file === "yarn.lock" ? "yarn" : "npm"));
|
|
56
|
+
if (kinds.length > 1) return yield* PackageManagerError.make({
|
|
57
|
+
operation: "detect",
|
|
58
|
+
message: "Conflicting lockfiles; select an agent explicitly"
|
|
59
|
+
});
|
|
60
|
+
if (kinds[0] === "yarn") {
|
|
61
|
+
const lock = yield* fs.readFileString(path.join(options.directory, "yarn.lock"));
|
|
62
|
+
return /^__metadata:/m.test(lock) ? "yarn-berry" : "yarn-classic";
|
|
63
|
+
}
|
|
64
|
+
if (kinds[0] !== void 0) return kinds[0];
|
|
65
|
+
const caller = /^(npm|pnpm|yarn)\/(\d+)/.exec(options.userAgent ?? "");
|
|
66
|
+
return caller?.[1] === "yarn" ? caller[2] === "1" ? "yarn-classic" : "yarn-berry" : caller?.[1] === "pnpm" ? "pnpm" : "npm";
|
|
67
|
+
});
|
|
68
|
+
const diffState = (before, after) => {
|
|
69
|
+
const changes = Array.map(Array.sort(Array.dedupe([...HashMap.keys(before), ...HashMap.keys(after)]), String.Order), (name) => ({
|
|
70
|
+
name,
|
|
71
|
+
before: Option.getOrUndefined(HashMap.get(before, name)),
|
|
72
|
+
after: Option.getOrUndefined(HashMap.get(after, name))
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
added: Array.filter(changes, (change) => change.before === void 0),
|
|
76
|
+
removed: Array.filter(changes, (change) => change.after === void 0),
|
|
77
|
+
updated: Array.filter(changes, ({ before, after }) => before !== void 0 && after !== void 0 && (before.spec !== after.spec || before.version !== after.version || before.path !== after.path))
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Own one manager per project. Failed or interrupted installs restore the exact
|
|
82
|
+
* manifest/lockfiles, then attempt to repair node_modules when a lockfile existed.
|
|
83
|
+
* Recovery failures are combined with the original cause. No crash recovery is promised.
|
|
84
|
+
*/
|
|
85
|
+
const make = (options) => Effect.gen(function* () {
|
|
86
|
+
const fs = yield* FileSystem.FileSystem;
|
|
87
|
+
const path = yield* Path.Path;
|
|
88
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
89
|
+
const directory = path.resolve(options.directory);
|
|
90
|
+
const agent = yield* detectAgent({
|
|
91
|
+
...options,
|
|
92
|
+
directory
|
|
93
|
+
});
|
|
94
|
+
const lock = yield* Semaphore.make(1);
|
|
95
|
+
const manifestFile = path.join(directory, "package.json");
|
|
96
|
+
const readManifest = fs.readFileString(manifestFile).pipe(Effect.flatMap(decodeManifest));
|
|
97
|
+
const installed = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ version: Schema.optionalKey(Schema.String) })));
|
|
98
|
+
const stateOf = Effect.fnUntraced(function* (manifest) {
|
|
99
|
+
if (yield* fs.exists(path.join(directory, ".pnp.cjs"))) return yield* PackageManagerError.make({
|
|
100
|
+
operation: "state",
|
|
101
|
+
message: "Plug'n'Play is not supported; configure Yarn with nodeLinker: node-modules"
|
|
102
|
+
});
|
|
103
|
+
const deps = Array.reduce(fields, {}, (all, field) => ({
|
|
104
|
+
...all,
|
|
105
|
+
...manifest[field]
|
|
106
|
+
}));
|
|
107
|
+
const entries = yield* Effect.forEach(Record.toEntries(deps), ([name, spec]) => Effect.gen(function* () {
|
|
108
|
+
const file = path.join(directory, "node_modules", name, "package.json");
|
|
109
|
+
if (!(yield* fs.exists(file))) return [name, {
|
|
110
|
+
spec,
|
|
111
|
+
version: void 0,
|
|
112
|
+
path: void 0
|
|
113
|
+
}];
|
|
114
|
+
return [name, {
|
|
115
|
+
spec,
|
|
116
|
+
version: (yield* fs.readFileString(file).pipe(Effect.flatMap(installed))).version,
|
|
117
|
+
path: yield* fs.realPath(path.dirname(file))
|
|
118
|
+
}];
|
|
119
|
+
}));
|
|
120
|
+
return HashMap.fromIterable(entries);
|
|
121
|
+
});
|
|
122
|
+
const readState = Effect.flatMap(readManifest, stateOf);
|
|
123
|
+
const run = Effect.fnUntraced(function* (operation) {
|
|
124
|
+
const recovery = operation === "recover";
|
|
125
|
+
const args = agent === "npm" ? [
|
|
126
|
+
"install",
|
|
127
|
+
"--no-audit",
|
|
128
|
+
"--no-fund",
|
|
129
|
+
...recovery ? ["--prefer-offline"] : []
|
|
130
|
+
] : agent === "pnpm" ? [
|
|
131
|
+
"install",
|
|
132
|
+
recovery ? "--frozen-lockfile" : "--no-frozen-lockfile",
|
|
133
|
+
"--config.confirmModulesPurge=false"
|
|
134
|
+
] : agent === "yarn-classic" ? [
|
|
135
|
+
"install",
|
|
136
|
+
"--non-interactive",
|
|
137
|
+
...recovery ? ["--frozen-lockfile", "--prefer-offline"] : []
|
|
138
|
+
] : ["install", ...recovery ? ["--immutable"] : []];
|
|
139
|
+
const code = yield* spawner.exitCode(ChildProcess.make(executable(agent), args, {
|
|
140
|
+
cwd: directory,
|
|
141
|
+
extendEnv: true,
|
|
142
|
+
stdin: "ignore",
|
|
143
|
+
stdout: "inherit",
|
|
144
|
+
stderr: "inherit",
|
|
145
|
+
env: {
|
|
146
|
+
CI: "true",
|
|
147
|
+
...agent === "yarn-berry" ? {
|
|
148
|
+
YARN_ENABLE_IMMUTABLE_INSTALLS: recovery ? "true" : "false",
|
|
149
|
+
YARN_NODE_LINKER: "node-modules"
|
|
150
|
+
} : {}
|
|
151
|
+
}
|
|
152
|
+
})).pipe(Effect.timeoutOrElse({
|
|
153
|
+
duration: "10 minutes",
|
|
154
|
+
orElse: () => PackageManagerError.make({
|
|
155
|
+
operation,
|
|
156
|
+
message: `${executable(agent)} ${operation} timed out`
|
|
157
|
+
})
|
|
158
|
+
}));
|
|
159
|
+
if (code !== 0) return yield* PackageManagerError.make({
|
|
160
|
+
operation,
|
|
161
|
+
exitCode: code,
|
|
162
|
+
message: `${executable(agent)} ${operation} exited with code ${code}`
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
const install = (changes) => lock.withPermits(1)(Effect.gen(function* () {
|
|
166
|
+
const validated = yield* Schema.decodeEffect(Changes)(changes);
|
|
167
|
+
const original = yield* fs.readFileString(manifestFile);
|
|
168
|
+
const manifest = yield* decodeManifest(original);
|
|
169
|
+
const before = yield* stateOf(manifest);
|
|
170
|
+
const text = yield* encodeManifest(applyChanges(manifest, validated), original);
|
|
171
|
+
const backups = yield* Effect.forEach(backupFiles, (name) => Effect.gen(function* () {
|
|
172
|
+
const file = path.join(directory, name);
|
|
173
|
+
return {
|
|
174
|
+
name,
|
|
175
|
+
file,
|
|
176
|
+
bytes: (yield* fs.exists(file)) ? yield* fs.readFile(file).pipe(Effect.asSome) : Option.none()
|
|
177
|
+
};
|
|
178
|
+
}));
|
|
179
|
+
const restore = Array.reduce(backups, Effect.void, (previous, { file, bytes }) => previous.pipe(Effect.onExit(() => Option.match(bytes, {
|
|
180
|
+
onNone: () => fs.remove(file, { force: true }),
|
|
181
|
+
onSome: (bytes) => fs.writeFile(file, bytes)
|
|
182
|
+
}))));
|
|
183
|
+
const agentLocks = agent === "npm" ? ["package-lock.json", "npm-shrinkwrap.json"] : agent === "pnpm" ? ["pnpm-lock.yaml"] : ["yarn.lock"];
|
|
184
|
+
const hasLock = Array.some(backups, ({ name, bytes }) => Array.contains(agentLocks, name) && Option.isSome(bytes));
|
|
185
|
+
return yield* Effect.acquireUseRelease(Effect.void, () => Effect.gen(function* () {
|
|
186
|
+
yield* fs.writeFileString(manifestFile, text);
|
|
187
|
+
yield* run("install");
|
|
188
|
+
const after = yield* readState;
|
|
189
|
+
return {
|
|
190
|
+
agent,
|
|
191
|
+
...diffState(before, after)
|
|
192
|
+
};
|
|
193
|
+
}), (_, exit) => Exit.isSuccess(exit) ? Effect.void : restore.pipe(Effect.andThen(hasLock ? run("recover") : Effect.void), Effect.onExit(() => restore)));
|
|
194
|
+
}));
|
|
195
|
+
return {
|
|
196
|
+
directory,
|
|
197
|
+
agent,
|
|
198
|
+
readState: lock.withPermits(1)(readState),
|
|
199
|
+
install
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
export { Agent, Changes, PackageManagerError, PackageName, detectAgent, diffState, make };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["Str","Arr"],"sources":["../src/index.ts"],"sourcesContent":["/** Non-interactive package installation. Provide platform services at the application boundary. */\nimport {\n\tArray as Arr,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tHashMap,\n\tOption,\n\tPath,\n\tRecord,\n\tSchema,\n\tSemaphore,\n\tString as Str,\n} from \"effect\"\nimport type { PlatformError } from \"effect\"\nimport { ChildProcess, ChildProcessSpawner } from \"effect/process\"\nimport {\n\tapplyChanges,\n\tChanges,\n\tdecodeManifest,\n\tencodeManifest,\n\tfields,\n\ttype Manifest,\n} from \"./manifest\"\nexport { Changes, PackageName } from \"./manifest\"\n\nexport const Agent = Schema.Literals([\n\t\"npm\",\n\t\"pnpm\",\n\t\"yarn-classic\",\n\t\"yarn-berry\",\n])\nexport type Agent = typeof Agent.Type\nexport class PackageManagerError extends Schema.TaggedError<\n\tPackageManagerError,\n\t{ readonly brand: unique symbol }\n>()(\"PackageManagerError\", {\n\toperation: Schema.Literals([\"detect\", \"install\", \"recover\", \"state\"]),\n\tmessage: Schema.String,\n\texitCode: Schema.optionalKey(Schema.Int),\n}) {}\nexport type Error =\n\tPackageManagerError | PlatformError.PlatformError | Schema.SchemaError\nexport interface Options {\n\t/** Project directory, containing package.json. Use the workspace root for workspace installs. */\n\treadonly directory: string\n\t/** Explicit selection takes precedence over detection. */\n\treadonly agent?: Agent\n\t/** Optional npm_config_user_agent supplied by the caller; used only as a fallback. */\n\treadonly userAgent?: string\n}\nconst lockfiles = [\n\t\"package-lock.json\",\n\t\"npm-shrinkwrap.json\",\n\t\"pnpm-lock.yaml\",\n\t\"yarn.lock\",\n] as const\nconst backupFiles = [\n\t\"package.json\",\n\t...lockfiles,\n\t\".npmrc\",\n\t\"pnpm-workspace.yaml\",\n\t\".yarnrc\",\n\t\".yarnrc.yml\",\n\t\".yarn/install-state.gz\",\n\t\".pnp.cjs\",\n\t\".pnp.loader.mjs\",\n]\nconst executable = (agent: Agent) =>\n\tStr.startsWith(\"yarn\")(agent) ? \"yarn\" : agent\n\n/** packageManager, then lockfile, then caller user-agent, then npm. Conflicting lockfiles fail. */\nexport const detectAgent = Effect.fnUntraced(function* (options: Options) {\n\tif (options.agent !== undefined)\n\t\treturn yield* Schema.decodeEffect(Agent)(options.agent)\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tconst manifest = yield* fs\n\t\t.readFileString(path.join(options.directory, \"package.json\"))\n\t\t.pipe(Effect.flatMap(decodeManifest))\n\tif (manifest.packageManager !== undefined) {\n\t\tconst match = /^(npm|pnpm|yarn)@(\\d+)\\.\\d+\\.\\d+(?:[-+].*)?$/.exec(\n\t\t\tmanifest.packageManager,\n\t\t)\n\t\tif (match === null)\n\t\t\treturn yield* PackageManagerError.make({\n\t\t\t\toperation: \"detect\",\n\t\t\t\tmessage: `Unsupported packageManager: ${manifest.packageManager}`,\n\t\t\t})\n\t\treturn match[1] === \"yarn\"\n\t\t\t? match[2] === \"1\"\n\t\t\t\t? \"yarn-classic\"\n\t\t\t\t: \"yarn-berry\"\n\t\t\t: (match[1] as \"npm\" | \"pnpm\")\n\t}\n\tconst found = yield* Effect.filter(lockfiles, (file) =>\n\t\tfs.exists(path.join(options.directory, file)),\n\t)\n\tconst kinds = Arr.dedupe(\n\t\tArr.map(found, (file) =>\n\t\t\tfile === \"pnpm-lock.yaml\"\n\t\t\t\t? \"pnpm\"\n\t\t\t\t: file === \"yarn.lock\"\n\t\t\t\t\t? \"yarn\"\n\t\t\t\t\t: \"npm\",\n\t\t),\n\t)\n\tif (kinds.length > 1)\n\t\treturn yield* PackageManagerError.make({\n\t\t\toperation: \"detect\",\n\t\t\tmessage: \"Conflicting lockfiles; select an agent explicitly\",\n\t\t})\n\tif (kinds[0] === \"yarn\") {\n\t\tconst lock = yield* fs.readFileString(\n\t\t\tpath.join(options.directory, \"yarn.lock\"),\n\t\t)\n\t\treturn /^__metadata:/m.test(lock) ? \"yarn-berry\" : \"yarn-classic\"\n\t}\n\tif (kinds[0] !== undefined) return kinds[0]\n\tconst caller = /^(npm|pnpm|yarn)\\/(\\d+)/.exec(options.userAgent ?? \"\")\n\treturn caller?.[1] === \"yarn\"\n\t\t? caller[2] === \"1\"\n\t\t\t? \"yarn-classic\"\n\t\t\t: \"yarn-berry\"\n\t\t: caller?.[1] === \"pnpm\"\n\t\t\t? \"pnpm\"\n\t\t\t: \"npm\"\n})\n\nexport interface PackageState {\n\treadonly spec: string\n\t/** Missing optional or not-yet-installed dependencies have no version/path. */\n\treadonly version: string | undefined\n\treadonly path: string | undefined\n}\n/** Direct dependencies only, keyed by their manifest names (including aliases). */\nexport type State = HashMap.HashMap<string, PackageState>\nexport interface PackageChange {\n\treadonly name: string\n\treadonly before: PackageState | undefined\n\treadonly after: PackageState | undefined\n}\nexport interface StateDiff {\n\treadonly added: ReadonlyArray<PackageChange>\n\treadonly removed: ReadonlyArray<PackageChange>\n\treadonly updated: ReadonlyArray<PackageChange>\n}\nexport const diffState = (before: State, after: State): StateDiff => {\n\tconst changes = Arr.map(\n\t\tArr.sort(\n\t\t\tArr.dedupe([...HashMap.keys(before), ...HashMap.keys(after)]),\n\t\t\tStr.Order,\n\t\t),\n\t\t(name) => ({\n\t\t\tname,\n\t\t\tbefore: Option.getOrUndefined(HashMap.get(before, name)),\n\t\t\tafter: Option.getOrUndefined(HashMap.get(after, name)),\n\t\t}),\n\t)\n\treturn {\n\t\tadded: Arr.filter(changes, (change) => change.before === undefined),\n\t\tremoved: Arr.filter(changes, (change) => change.after === undefined),\n\t\tupdated: Arr.filter(\n\t\t\tchanges,\n\t\t\t({ before, after }) =>\n\t\t\t\tbefore !== undefined &&\n\t\t\t\tafter !== undefined &&\n\t\t\t\t(before.spec !== after.spec ||\n\t\t\t\t\tbefore.version !== after.version ||\n\t\t\t\t\tbefore.path !== after.path),\n\t\t),\n\t}\n}\nexport interface InstallResult extends StateDiff {\n\treadonly agent: Agent\n}\nexport interface PackageManager {\n\treadonly directory: string\n\treadonly agent: Agent\n\treadonly readState: Effect.Effect<State, Error>\n\t/** Serialized per manager. Empty changes installs the current manifest, including first installs. */\n\treadonly install: (changes: Changes) => Effect.Effect<InstallResult, Error>\n}\n\n/**\n * Own one manager per project. Failed or interrupted installs restore the exact\n * manifest/lockfiles, then attempt to repair node_modules when a lockfile existed.\n * Recovery failures are combined with the original cause. No crash recovery is promised.\n */\nexport const make = (\n\toptions: Options,\n): Effect.Effect<\n\tPackageManager,\n\tError,\n\tFileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner\n> =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst path = yield* Path.Path\n\t\tconst spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n\t\tconst directory = path.resolve(options.directory)\n\t\tconst agent = yield* detectAgent({ ...options, directory })\n\t\tconst lock = yield* Semaphore.make(1)\n\t\tconst manifestFile = path.join(directory, \"package.json\")\n\t\tconst readManifest = fs\n\t\t\t.readFileString(manifestFile)\n\t\t\t.pipe(Effect.flatMap(decodeManifest))\n\t\tconst installed = Schema.decodeUnknownEffect(\n\t\t\tSchema.fromJsonString(\n\t\t\t\tSchema.Struct({ version: Schema.optionalKey(Schema.String) }),\n\t\t\t),\n\t\t)\n\t\tconst stateOf = Effect.fnUntraced(function* (manifest: Manifest) {\n\t\t\tif (yield* fs.exists(path.join(directory, \".pnp.cjs\")))\n\t\t\t\treturn yield* PackageManagerError.make({\n\t\t\t\t\toperation: \"state\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Plug'n'Play is not supported; configure Yarn with nodeLinker: node-modules\",\n\t\t\t\t})\n\t\t\tconst deps = Arr.reduce(\n\t\t\t\tfields,\n\t\t\t\t{} as Readonly<Record<string, string>>,\n\t\t\t\t(all, field) => ({ ...all, ...manifest[field] }),\n\t\t\t)\n\t\t\tconst entries = yield* Effect.forEach(\n\t\t\t\tRecord.toEntries(deps),\n\t\t\t\t([name, spec]) =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tconst file = path.join(\n\t\t\t\t\t\t\tdirectory,\n\t\t\t\t\t\t\t\"node_modules\",\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\"package.json\",\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif (!(yield* fs.exists(file)))\n\t\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t{ spec, version: undefined, path: undefined },\n\t\t\t\t\t\t\t] as const\n\t\t\t\t\t\tconst pkg = yield* fs\n\t\t\t\t\t\t\t.readFileString(file)\n\t\t\t\t\t\t\t.pipe(Effect.flatMap(installed))\n\t\t\t\t\t\treturn [\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tspec,\n\t\t\t\t\t\t\t\tversion: pkg.version,\n\t\t\t\t\t\t\t\tpath: yield* fs.realPath(path.dirname(file)),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t] as const\n\t\t\t\t\t}),\n\t\t\t)\n\t\t\treturn HashMap.fromIterable<string, PackageState>(entries)\n\t\t})\n\t\tconst readState = Effect.flatMap(readManifest, stateOf)\n\t\tconst run = Effect.fnUntraced(function* (operation: \"install\" | \"recover\") {\n\t\t\tconst recovery = operation === \"recover\"\n\t\t\tconst args =\n\t\t\t\tagent === \"npm\"\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t\"install\",\n\t\t\t\t\t\t\t\"--no-audit\",\n\t\t\t\t\t\t\t\"--no-fund\",\n\t\t\t\t\t\t\t...(recovery ? [\"--prefer-offline\"] : []),\n\t\t\t\t\t\t]\n\t\t\t\t\t: agent === \"pnpm\"\n\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\"install\",\n\t\t\t\t\t\t\t\trecovery ? \"--frozen-lockfile\" : \"--no-frozen-lockfile\",\n\t\t\t\t\t\t\t\t\"--config.confirmModulesPurge=false\",\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t: agent === \"yarn-classic\"\n\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\"install\",\n\t\t\t\t\t\t\t\t\t\"--non-interactive\",\n\t\t\t\t\t\t\t\t\t...(recovery\n\t\t\t\t\t\t\t\t\t\t? [\"--frozen-lockfile\", \"--prefer-offline\"]\n\t\t\t\t\t\t\t\t\t\t: []),\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t: [\"install\", ...(recovery ? [\"--immutable\"] : [])]\n\t\t\tconst code = yield* spawner\n\t\t\t\t.exitCode(\n\t\t\t\t\tChildProcess.make(executable(agent), args, {\n\t\t\t\t\t\tcwd: directory,\n\t\t\t\t\t\textendEnv: true,\n\t\t\t\t\t\tstdin: \"ignore\",\n\t\t\t\t\t\tstdout: \"inherit\",\n\t\t\t\t\t\tstderr: \"inherit\",\n\t\t\t\t\t\tenv: {\n\t\t\t\t\t\t\tCI: \"true\",\n\t\t\t\t\t\t\t...(agent === \"yarn-berry\"\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\tYARN_ENABLE_IMMUTABLE_INSTALLS: recovery ? \"true\" : \"false\",\n\t\t\t\t\t\t\t\t\t\tYARN_NODE_LINKER: \"node-modules\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.timeoutOrElse({\n\t\t\t\t\t\tduration: \"10 minutes\",\n\t\t\t\t\t\torElse: () =>\n\t\t\t\t\t\t\tPackageManagerError.make({\n\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\tmessage: `${executable(agent)} ${operation} timed out`,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\tif (code !== 0)\n\t\t\t\treturn yield* PackageManagerError.make({\n\t\t\t\t\toperation,\n\t\t\t\t\texitCode: code,\n\t\t\t\t\tmessage: `${executable(agent)} ${operation} exited with code ${code}`,\n\t\t\t\t})\n\t\t})\n\t\tconst install = (changes: Changes) =>\n\t\t\tlock.withPermits(1)(\n\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\tconst validated = yield* Schema.decodeEffect(Changes)(changes)\n\t\t\t\t\tconst original = yield* fs.readFileString(manifestFile)\n\t\t\t\t\tconst manifest = yield* decodeManifest(original)\n\t\t\t\t\tconst before = yield* stateOf(manifest)\n\t\t\t\t\tconst text = yield* encodeManifest(\n\t\t\t\t\t\tapplyChanges(manifest, validated),\n\t\t\t\t\t\toriginal,\n\t\t\t\t\t)\n\t\t\t\t\tconst backups = yield* Effect.forEach(backupFiles, (name) =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tconst file = path.join(directory, name)\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\tfile,\n\t\t\t\t\t\t\t\tbytes: (yield* fs.exists(file))\n\t\t\t\t\t\t\t\t\t? yield* fs.readFile(file).pipe(Effect.asSome)\n\t\t\t\t\t\t\t\t\t: Option.none<Uint8Array>(),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t\t// Every file gets a restoration attempt; onExit combines failures.\n\t\t\t\t\tconst restore = Arr.reduce<\n\t\t\t\t\t\t(typeof backups)[number],\n\t\t\t\t\t\tEffect.Effect<void, PlatformError.PlatformError>\n\t\t\t\t\t>(backups, Effect.void, (previous, { file, bytes }) =>\n\t\t\t\t\t\tprevious.pipe(\n\t\t\t\t\t\t\tEffect.onExit(() =>\n\t\t\t\t\t\t\t\tOption.match(bytes, {\n\t\t\t\t\t\t\t\t\tonNone: () => fs.remove(file, { force: true }),\n\t\t\t\t\t\t\t\t\tonSome: (bytes) => fs.writeFile(file, bytes),\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t\tconst agentLocks =\n\t\t\t\t\t\tagent === \"npm\"\n\t\t\t\t\t\t\t? [\"package-lock.json\", \"npm-shrinkwrap.json\"]\n\t\t\t\t\t\t\t: agent === \"pnpm\"\n\t\t\t\t\t\t\t\t? [\"pnpm-lock.yaml\"]\n\t\t\t\t\t\t\t\t: [\"yarn.lock\"]\n\t\t\t\t\tconst hasLock = Arr.some(\n\t\t\t\t\t\tbackups,\n\t\t\t\t\t\t({ name, bytes }) =>\n\t\t\t\t\t\t\tArr.contains(agentLocks, name) && Option.isSome(bytes),\n\t\t\t\t\t)\n\t\t\t\t\treturn yield* Effect.acquireUseRelease(\n\t\t\t\t\t\tEffect.void,\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\t\tyield* fs.writeFileString(manifestFile, text)\n\t\t\t\t\t\t\t\tyield* run(\"install\")\n\t\t\t\t\t\t\t\tconst after = yield* readState\n\t\t\t\t\t\t\t\treturn { agent, ...diffState(before, after) }\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t(_, exit) =>\n\t\t\t\t\t\t\tExit.isSuccess(exit)\n\t\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t\t: restore.pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.andThen(hasLock ? run(\"recover\") : Effect.void),\n\t\t\t\t\t\t\t\t\t\t// Recovery itself may rewrite the manifest or lockfile, including on failure.\n\t\t\t\t\t\t\t\t\t\tEffect.onExit(() => restore),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t}),\n\t\t\t)\n\t\treturn {\n\t\t\tdirectory,\n\t\t\tagent,\n\t\t\treadState: lock.withPermits(1)(readState),\n\t\t\tinstall,\n\t\t}\n\t})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
export declare const PackageName: Schema.String;
|
|
3
|
+
export declare const Changes: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Null]>>;
|
|
4
|
+
/** A spec adds or updates a dependency; null removes it from all dependency fields. */
|
|
5
|
+
export type Changes = typeof Changes.Type;
|
|
6
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","names":[],"sources":["../src/manifest.ts"],"mappings":";qBAEa,aAAW,OAAA;qBAkBX,SAAO,OAAA,QAAA,OAAA,QAAA,OAAA,gBAAA,OAAA,QAAA,OAAA;;YAKR,iBAAiB,QAAQ"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Array, Effect, Record, Schema, String } from "effect";
|
|
2
|
+
// src/manifest.ts
|
|
3
|
+
const PackageName = Schema.String.check(Schema.isPattern(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i), Schema.isMaxLength(214));
|
|
4
|
+
const Dependencies = Schema.Record(PackageName, Schema.String);
|
|
5
|
+
const Manifest = Schema.StructWithRest(Schema.Struct({
|
|
6
|
+
packageManager: Schema.optionalKey(Schema.String),
|
|
7
|
+
dependencies: Schema.optionalKey(Dependencies),
|
|
8
|
+
devDependencies: Schema.optionalKey(Dependencies),
|
|
9
|
+
optionalDependencies: Schema.optionalKey(Dependencies)
|
|
10
|
+
}), [Schema.Record(Schema.String, Schema.Unknown)]);
|
|
11
|
+
const decodeManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest));
|
|
12
|
+
const Changes = Schema.Record(PackageName, Schema.Union([Schema.String.check(Schema.isPattern(/\S/)), Schema.Null]));
|
|
13
|
+
const fields = [
|
|
14
|
+
"dependencies",
|
|
15
|
+
"devDependencies",
|
|
16
|
+
"optionalDependencies"
|
|
17
|
+
];
|
|
18
|
+
const applyChanges = (manifest, changes) => {
|
|
19
|
+
let next = manifest;
|
|
20
|
+
for (const [name, spec] of Record.toEntries(changes)) {
|
|
21
|
+
const existing = Array.filter(fields, (field) => Record.has(next[field] ?? {}, name));
|
|
22
|
+
const targets = spec === null ? fields : existing.length > 0 ? existing : ["dependencies"];
|
|
23
|
+
for (const field of targets) {
|
|
24
|
+
const deps = next[field] ?? {};
|
|
25
|
+
next = {
|
|
26
|
+
...next,
|
|
27
|
+
[field]: spec === null ? Record.remove(deps, name) : Record.set(deps, name, spec)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return next;
|
|
32
|
+
};
|
|
33
|
+
const encodeManifest = Effect.fnUntraced(function* (manifest, original) {
|
|
34
|
+
const space = /^[{[]\r?\n([ \t]+)/.exec(original)?.[1] ?? 2;
|
|
35
|
+
const text = yield* Schema.encodeEffect(Schema.fromJsonString(Manifest, { space }))(manifest);
|
|
36
|
+
const eol = String.includes("\r\n")(original) ? "\r\n" : "\n";
|
|
37
|
+
return String.replaceAll("\n", eol)(text) + (/\r?\n$/.test(original) ? eol : "");
|
|
38
|
+
});
|
|
39
|
+
export { Changes, Manifest, PackageName, applyChanges, decodeManifest, encodeManifest, fields };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","names":["Arr","Str"],"sources":["../src/manifest.ts"],"sourcesContent":["import { Array as Arr, Effect, Record, Schema, String as Str } from \"effect\"\n\nexport const PackageName = Schema.String.check(\n\tSchema.isPattern(/^(?:@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*$/i),\n\tSchema.isMaxLength(214),\n)\nconst Dependencies = Schema.Record(PackageName, Schema.String)\nexport const Manifest = Schema.StructWithRest(\n\tSchema.Struct({\n\t\tpackageManager: Schema.optionalKey(Schema.String),\n\t\tdependencies: Schema.optionalKey(Dependencies),\n\t\tdevDependencies: Schema.optionalKey(Dependencies),\n\t\toptionalDependencies: Schema.optionalKey(Dependencies),\n\t}),\n\t[Schema.Record(Schema.String, Schema.Unknown)],\n)\nexport type Manifest = typeof Manifest.Type\nexport const decodeManifest = Schema.decodeUnknownEffect(\n\tSchema.fromJsonString(Manifest),\n)\nexport const Changes = Schema.Record(\n\tPackageName,\n\tSchema.Union([Schema.String.check(Schema.isPattern(/\\S/)), Schema.Null]),\n)\n/** A spec adds or updates a dependency; null removes it from all dependency fields. */\nexport type Changes = typeof Changes.Type\nexport const fields = [\n\t\"dependencies\",\n\t\"devDependencies\",\n\t\"optionalDependencies\",\n] as const\n\nexport const applyChanges = (\n\tmanifest: Manifest,\n\tchanges: Changes,\n): Manifest => {\n\tlet next = manifest\n\tfor (const [name, spec] of Record.toEntries(changes)) {\n\t\tconst existing = Arr.filter(fields, (field) =>\n\t\t\tRecord.has(next[field] ?? {}, name),\n\t\t)\n\t\tconst targets =\n\t\t\tspec === null\n\t\t\t\t? fields\n\t\t\t\t: existing.length > 0\n\t\t\t\t\t? existing\n\t\t\t\t\t: ([\"dependencies\"] as const)\n\t\tfor (const field of targets) {\n\t\t\tconst deps = next[field] ?? {}\n\t\t\tnext = {\n\t\t\t\t...next,\n\t\t\t\t[field]:\n\t\t\t\t\tspec === null\n\t\t\t\t\t\t? Record.remove(deps, name)\n\t\t\t\t\t\t: Record.set(deps, name, spec),\n\t\t\t}\n\t\t}\n\t}\n\treturn next\n}\n\nexport const encodeManifest = Effect.fnUntraced(function* (\n\tmanifest: Manifest,\n\toriginal: string,\n) {\n\tconst space = /^[{[]\\r?\\n([ \\t]+)/.exec(original)?.[1] ?? 2\n\tconst text = yield* Schema.encodeEffect(\n\t\tSchema.fromJsonString(Manifest, { space }),\n\t)(manifest)\n\tconst eol = Str.includes(\"\\r\\n\")(original) ? \"\\r\\n\" : \"\\n\"\n\treturn Str.replaceAll(\"\\n\", eol)(text) + (/\\r?\\n$/.test(original) ? eol : \"\")\n})\n"],"mappings":";;AAEA,MAAa,cAAc,OAAO,OAAO,MACxC,OAAO,UAAU,2DAA2D,GAC5E,OAAO,YAAY,GAAG,CACvB;AACA,MAAM,eAAe,OAAO,OAAO,aAAa,OAAO,MAAM;AAC7D,MAAa,WAAW,OAAO,eAC9B,OAAO,OAAO;CACb,gBAAgB,OAAO,YAAY,OAAO,MAAM;CAChD,cAAc,OAAO,YAAY,YAAY;CAC7C,iBAAiB,OAAO,YAAY,YAAY;CAChD,sBAAsB,OAAO,YAAY,YAAY;AACtD,CAAC,GACD,CAAC,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,CAAC,CAC9C;AAEA,MAAa,iBAAiB,OAAO,oBACpC,OAAO,eAAe,QAAQ,CAC/B;AACA,MAAa,UAAU,OAAO,OAC7B,aACA,OAAO,MAAM,CAAC,OAAO,OAAO,MAAM,OAAO,UAAU,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC,CACxE;AAGA,MAAa,SAAS;CACrB;CACA;CACA;AACD;AAEA,MAAa,gBACZ,UACA,YACc;CACd,IAAI,OAAO;CACX,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,UAAU,OAAO,GAAG;EACrD,MAAM,WAAWA,MAAI,OAAO,SAAS,UACpC,OAAO,IAAI,KAAK,UAAU,CAAC,GAAG,IAAI,CACnC;EACA,MAAM,UACL,SAAS,OACN,SACA,SAAS,SAAS,IACjB,WACC,CAAC,cAAc;EACrB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAO,KAAK,UAAU,CAAC;GAC7B,OAAO;IACN,GAAG;KACF,QACA,SAAS,OACN,OAAO,OAAO,MAAM,IAAI,IACxB,OAAO,IAAI,MAAM,MAAM,IAAI;GAChC;EACD;CACD;CACA,OAAO;AACR;AAEA,MAAa,iBAAiB,OAAO,WAAW,WAC/C,UACA,UACC;CACD,MAAM,QAAQ,qBAAqB,KAAK,QAAQ,CAAC,GAAG,MAAM;CAC1D,MAAM,OAAO,OAAO,OAAO,aAC1B,OAAO,eAAe,UAAU,EAAE,MAAM,CAAC,CAC1C,CAAC,CAAC,QAAQ;CACV,MAAM,MAAMC,OAAI,SAAS,MAAM,CAAC,CAAC,QAAQ,IAAI,SAAS;CACtD,OAAOA,OAAI,WAAW,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,KAAK,QAAQ,IAAI,MAAM;AAC3E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graftland/package-manager",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Installs dependencies with npm, pnpm or Yarn, and rolls back on failure",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/graftland/graft.git",
|
|
10
|
+
"directory": "packages/package-manager"
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"effect": "4.1.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@effect/vitest": "4.1.2",
|
|
26
|
+
"effect": "4.1.2",
|
|
27
|
+
"vitest": "5.1.0",
|
|
28
|
+
"@effect/platform-node": "4.1.2"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsdown",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
}
|
|
6
35
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/** Non-interactive package installation. Provide platform services at the application boundary. */
|
|
2
|
+
import {
|
|
3
|
+
Array as Arr,
|
|
4
|
+
Effect,
|
|
5
|
+
Exit,
|
|
6
|
+
FileSystem,
|
|
7
|
+
HashMap,
|
|
8
|
+
Option,
|
|
9
|
+
Path,
|
|
10
|
+
Record,
|
|
11
|
+
Schema,
|
|
12
|
+
Semaphore,
|
|
13
|
+
String as Str,
|
|
14
|
+
} from "effect"
|
|
15
|
+
import type { PlatformError } from "effect"
|
|
16
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
17
|
+
import {
|
|
18
|
+
applyChanges,
|
|
19
|
+
Changes,
|
|
20
|
+
decodeManifest,
|
|
21
|
+
encodeManifest,
|
|
22
|
+
fields,
|
|
23
|
+
type Manifest,
|
|
24
|
+
} from "./manifest"
|
|
25
|
+
export { Changes, PackageName } from "./manifest"
|
|
26
|
+
|
|
27
|
+
export const Agent = Schema.Literals([
|
|
28
|
+
"npm",
|
|
29
|
+
"pnpm",
|
|
30
|
+
"yarn-classic",
|
|
31
|
+
"yarn-berry",
|
|
32
|
+
])
|
|
33
|
+
export type Agent = typeof Agent.Type
|
|
34
|
+
export class PackageManagerError extends Schema.TaggedError<
|
|
35
|
+
PackageManagerError,
|
|
36
|
+
{ readonly brand: unique symbol }
|
|
37
|
+
>()("PackageManagerError", {
|
|
38
|
+
operation: Schema.Literals(["detect", "install", "recover", "state"]),
|
|
39
|
+
message: Schema.String,
|
|
40
|
+
exitCode: Schema.optionalKey(Schema.Int),
|
|
41
|
+
}) {}
|
|
42
|
+
export type Error =
|
|
43
|
+
PackageManagerError | PlatformError.PlatformError | Schema.SchemaError
|
|
44
|
+
export interface Options {
|
|
45
|
+
/** Project directory, containing package.json. Use the workspace root for workspace installs. */
|
|
46
|
+
readonly directory: string
|
|
47
|
+
/** Explicit selection takes precedence over detection. */
|
|
48
|
+
readonly agent?: Agent
|
|
49
|
+
/** Optional npm_config_user_agent supplied by the caller; used only as a fallback. */
|
|
50
|
+
readonly userAgent?: string
|
|
51
|
+
}
|
|
52
|
+
const lockfiles = [
|
|
53
|
+
"package-lock.json",
|
|
54
|
+
"npm-shrinkwrap.json",
|
|
55
|
+
"pnpm-lock.yaml",
|
|
56
|
+
"yarn.lock",
|
|
57
|
+
] as const
|
|
58
|
+
const backupFiles = [
|
|
59
|
+
"package.json",
|
|
60
|
+
...lockfiles,
|
|
61
|
+
".npmrc",
|
|
62
|
+
"pnpm-workspace.yaml",
|
|
63
|
+
".yarnrc",
|
|
64
|
+
".yarnrc.yml",
|
|
65
|
+
".yarn/install-state.gz",
|
|
66
|
+
".pnp.cjs",
|
|
67
|
+
".pnp.loader.mjs",
|
|
68
|
+
]
|
|
69
|
+
const executable = (agent: Agent) =>
|
|
70
|
+
Str.startsWith("yarn")(agent) ? "yarn" : agent
|
|
71
|
+
|
|
72
|
+
/** packageManager, then lockfile, then caller user-agent, then npm. Conflicting lockfiles fail. */
|
|
73
|
+
export const detectAgent = Effect.fnUntraced(function* (options: Options) {
|
|
74
|
+
if (options.agent !== undefined)
|
|
75
|
+
return yield* Schema.decodeEffect(Agent)(options.agent)
|
|
76
|
+
const fs = yield* FileSystem.FileSystem
|
|
77
|
+
const path = yield* Path.Path
|
|
78
|
+
const manifest = yield* fs
|
|
79
|
+
.readFileString(path.join(options.directory, "package.json"))
|
|
80
|
+
.pipe(Effect.flatMap(decodeManifest))
|
|
81
|
+
if (manifest.packageManager !== undefined) {
|
|
82
|
+
const match = /^(npm|pnpm|yarn)@(\d+)\.\d+\.\d+(?:[-+].*)?$/.exec(
|
|
83
|
+
manifest.packageManager,
|
|
84
|
+
)
|
|
85
|
+
if (match === null)
|
|
86
|
+
return yield* PackageManagerError.make({
|
|
87
|
+
operation: "detect",
|
|
88
|
+
message: `Unsupported packageManager: ${manifest.packageManager}`,
|
|
89
|
+
})
|
|
90
|
+
return match[1] === "yarn"
|
|
91
|
+
? match[2] === "1"
|
|
92
|
+
? "yarn-classic"
|
|
93
|
+
: "yarn-berry"
|
|
94
|
+
: (match[1] as "npm" | "pnpm")
|
|
95
|
+
}
|
|
96
|
+
const found = yield* Effect.filter(lockfiles, (file) =>
|
|
97
|
+
fs.exists(path.join(options.directory, file)),
|
|
98
|
+
)
|
|
99
|
+
const kinds = Arr.dedupe(
|
|
100
|
+
Arr.map(found, (file) =>
|
|
101
|
+
file === "pnpm-lock.yaml"
|
|
102
|
+
? "pnpm"
|
|
103
|
+
: file === "yarn.lock"
|
|
104
|
+
? "yarn"
|
|
105
|
+
: "npm",
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
if (kinds.length > 1)
|
|
109
|
+
return yield* PackageManagerError.make({
|
|
110
|
+
operation: "detect",
|
|
111
|
+
message: "Conflicting lockfiles; select an agent explicitly",
|
|
112
|
+
})
|
|
113
|
+
if (kinds[0] === "yarn") {
|
|
114
|
+
const lock = yield* fs.readFileString(
|
|
115
|
+
path.join(options.directory, "yarn.lock"),
|
|
116
|
+
)
|
|
117
|
+
return /^__metadata:/m.test(lock) ? "yarn-berry" : "yarn-classic"
|
|
118
|
+
}
|
|
119
|
+
if (kinds[0] !== undefined) return kinds[0]
|
|
120
|
+
const caller = /^(npm|pnpm|yarn)\/(\d+)/.exec(options.userAgent ?? "")
|
|
121
|
+
return caller?.[1] === "yarn"
|
|
122
|
+
? caller[2] === "1"
|
|
123
|
+
? "yarn-classic"
|
|
124
|
+
: "yarn-berry"
|
|
125
|
+
: caller?.[1] === "pnpm"
|
|
126
|
+
? "pnpm"
|
|
127
|
+
: "npm"
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
export interface PackageState {
|
|
131
|
+
readonly spec: string
|
|
132
|
+
/** Missing optional or not-yet-installed dependencies have no version/path. */
|
|
133
|
+
readonly version: string | undefined
|
|
134
|
+
readonly path: string | undefined
|
|
135
|
+
}
|
|
136
|
+
/** Direct dependencies only, keyed by their manifest names (including aliases). */
|
|
137
|
+
export type State = HashMap.HashMap<string, PackageState>
|
|
138
|
+
export interface PackageChange {
|
|
139
|
+
readonly name: string
|
|
140
|
+
readonly before: PackageState | undefined
|
|
141
|
+
readonly after: PackageState | undefined
|
|
142
|
+
}
|
|
143
|
+
export interface StateDiff {
|
|
144
|
+
readonly added: ReadonlyArray<PackageChange>
|
|
145
|
+
readonly removed: ReadonlyArray<PackageChange>
|
|
146
|
+
readonly updated: ReadonlyArray<PackageChange>
|
|
147
|
+
}
|
|
148
|
+
export const diffState = (before: State, after: State): StateDiff => {
|
|
149
|
+
const changes = Arr.map(
|
|
150
|
+
Arr.sort(
|
|
151
|
+
Arr.dedupe([...HashMap.keys(before), ...HashMap.keys(after)]),
|
|
152
|
+
Str.Order,
|
|
153
|
+
),
|
|
154
|
+
(name) => ({
|
|
155
|
+
name,
|
|
156
|
+
before: Option.getOrUndefined(HashMap.get(before, name)),
|
|
157
|
+
after: Option.getOrUndefined(HashMap.get(after, name)),
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
return {
|
|
161
|
+
added: Arr.filter(changes, (change) => change.before === undefined),
|
|
162
|
+
removed: Arr.filter(changes, (change) => change.after === undefined),
|
|
163
|
+
updated: Arr.filter(
|
|
164
|
+
changes,
|
|
165
|
+
({ before, after }) =>
|
|
166
|
+
before !== undefined &&
|
|
167
|
+
after !== undefined &&
|
|
168
|
+
(before.spec !== after.spec ||
|
|
169
|
+
before.version !== after.version ||
|
|
170
|
+
before.path !== after.path),
|
|
171
|
+
),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export interface InstallResult extends StateDiff {
|
|
175
|
+
readonly agent: Agent
|
|
176
|
+
}
|
|
177
|
+
export interface PackageManager {
|
|
178
|
+
readonly directory: string
|
|
179
|
+
readonly agent: Agent
|
|
180
|
+
readonly readState: Effect.Effect<State, Error>
|
|
181
|
+
/** Serialized per manager. Empty changes installs the current manifest, including first installs. */
|
|
182
|
+
readonly install: (changes: Changes) => Effect.Effect<InstallResult, Error>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Own one manager per project. Failed or interrupted installs restore the exact
|
|
187
|
+
* manifest/lockfiles, then attempt to repair node_modules when a lockfile existed.
|
|
188
|
+
* Recovery failures are combined with the original cause. No crash recovery is promised.
|
|
189
|
+
*/
|
|
190
|
+
export const make = (
|
|
191
|
+
options: Options,
|
|
192
|
+
): Effect.Effect<
|
|
193
|
+
PackageManager,
|
|
194
|
+
Error,
|
|
195
|
+
FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
|
|
196
|
+
> =>
|
|
197
|
+
Effect.gen(function* () {
|
|
198
|
+
const fs = yield* FileSystem.FileSystem
|
|
199
|
+
const path = yield* Path.Path
|
|
200
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
201
|
+
const directory = path.resolve(options.directory)
|
|
202
|
+
const agent = yield* detectAgent({ ...options, directory })
|
|
203
|
+
const lock = yield* Semaphore.make(1)
|
|
204
|
+
const manifestFile = path.join(directory, "package.json")
|
|
205
|
+
const readManifest = fs
|
|
206
|
+
.readFileString(manifestFile)
|
|
207
|
+
.pipe(Effect.flatMap(decodeManifest))
|
|
208
|
+
const installed = Schema.decodeUnknownEffect(
|
|
209
|
+
Schema.fromJsonString(
|
|
210
|
+
Schema.Struct({ version: Schema.optionalKey(Schema.String) }),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
const stateOf = Effect.fnUntraced(function* (manifest: Manifest) {
|
|
214
|
+
if (yield* fs.exists(path.join(directory, ".pnp.cjs")))
|
|
215
|
+
return yield* PackageManagerError.make({
|
|
216
|
+
operation: "state",
|
|
217
|
+
message:
|
|
218
|
+
"Plug'n'Play is not supported; configure Yarn with nodeLinker: node-modules",
|
|
219
|
+
})
|
|
220
|
+
const deps = Arr.reduce(
|
|
221
|
+
fields,
|
|
222
|
+
{} as Readonly<Record<string, string>>,
|
|
223
|
+
(all, field) => ({ ...all, ...manifest[field] }),
|
|
224
|
+
)
|
|
225
|
+
const entries = yield* Effect.forEach(
|
|
226
|
+
Record.toEntries(deps),
|
|
227
|
+
([name, spec]) =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
const file = path.join(
|
|
230
|
+
directory,
|
|
231
|
+
"node_modules",
|
|
232
|
+
name,
|
|
233
|
+
"package.json",
|
|
234
|
+
)
|
|
235
|
+
if (!(yield* fs.exists(file)))
|
|
236
|
+
return [
|
|
237
|
+
name,
|
|
238
|
+
{ spec, version: undefined, path: undefined },
|
|
239
|
+
] as const
|
|
240
|
+
const pkg = yield* fs
|
|
241
|
+
.readFileString(file)
|
|
242
|
+
.pipe(Effect.flatMap(installed))
|
|
243
|
+
return [
|
|
244
|
+
name,
|
|
245
|
+
{
|
|
246
|
+
spec,
|
|
247
|
+
version: pkg.version,
|
|
248
|
+
path: yield* fs.realPath(path.dirname(file)),
|
|
249
|
+
},
|
|
250
|
+
] as const
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
return HashMap.fromIterable<string, PackageState>(entries)
|
|
254
|
+
})
|
|
255
|
+
const readState = Effect.flatMap(readManifest, stateOf)
|
|
256
|
+
const run = Effect.fnUntraced(function* (operation: "install" | "recover") {
|
|
257
|
+
const recovery = operation === "recover"
|
|
258
|
+
const args =
|
|
259
|
+
agent === "npm"
|
|
260
|
+
? [
|
|
261
|
+
"install",
|
|
262
|
+
"--no-audit",
|
|
263
|
+
"--no-fund",
|
|
264
|
+
...(recovery ? ["--prefer-offline"] : []),
|
|
265
|
+
]
|
|
266
|
+
: agent === "pnpm"
|
|
267
|
+
? [
|
|
268
|
+
"install",
|
|
269
|
+
recovery ? "--frozen-lockfile" : "--no-frozen-lockfile",
|
|
270
|
+
"--config.confirmModulesPurge=false",
|
|
271
|
+
]
|
|
272
|
+
: agent === "yarn-classic"
|
|
273
|
+
? [
|
|
274
|
+
"install",
|
|
275
|
+
"--non-interactive",
|
|
276
|
+
...(recovery
|
|
277
|
+
? ["--frozen-lockfile", "--prefer-offline"]
|
|
278
|
+
: []),
|
|
279
|
+
]
|
|
280
|
+
: ["install", ...(recovery ? ["--immutable"] : [])]
|
|
281
|
+
const code = yield* spawner
|
|
282
|
+
.exitCode(
|
|
283
|
+
ChildProcess.make(executable(agent), args, {
|
|
284
|
+
cwd: directory,
|
|
285
|
+
extendEnv: true,
|
|
286
|
+
stdin: "ignore",
|
|
287
|
+
stdout: "inherit",
|
|
288
|
+
stderr: "inherit",
|
|
289
|
+
env: {
|
|
290
|
+
CI: "true",
|
|
291
|
+
...(agent === "yarn-berry"
|
|
292
|
+
? {
|
|
293
|
+
YARN_ENABLE_IMMUTABLE_INSTALLS: recovery ? "true" : "false",
|
|
294
|
+
YARN_NODE_LINKER: "node-modules",
|
|
295
|
+
}
|
|
296
|
+
: {}),
|
|
297
|
+
},
|
|
298
|
+
}),
|
|
299
|
+
)
|
|
300
|
+
.pipe(
|
|
301
|
+
Effect.timeoutOrElse({
|
|
302
|
+
duration: "10 minutes",
|
|
303
|
+
orElse: () =>
|
|
304
|
+
PackageManagerError.make({
|
|
305
|
+
operation,
|
|
306
|
+
message: `${executable(agent)} ${operation} timed out`,
|
|
307
|
+
}),
|
|
308
|
+
}),
|
|
309
|
+
)
|
|
310
|
+
if (code !== 0)
|
|
311
|
+
return yield* PackageManagerError.make({
|
|
312
|
+
operation,
|
|
313
|
+
exitCode: code,
|
|
314
|
+
message: `${executable(agent)} ${operation} exited with code ${code}`,
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
const install = (changes: Changes) =>
|
|
318
|
+
lock.withPermits(1)(
|
|
319
|
+
Effect.gen(function* () {
|
|
320
|
+
const validated = yield* Schema.decodeEffect(Changes)(changes)
|
|
321
|
+
const original = yield* fs.readFileString(manifestFile)
|
|
322
|
+
const manifest = yield* decodeManifest(original)
|
|
323
|
+
const before = yield* stateOf(manifest)
|
|
324
|
+
const text = yield* encodeManifest(
|
|
325
|
+
applyChanges(manifest, validated),
|
|
326
|
+
original,
|
|
327
|
+
)
|
|
328
|
+
const backups = yield* Effect.forEach(backupFiles, (name) =>
|
|
329
|
+
Effect.gen(function* () {
|
|
330
|
+
const file = path.join(directory, name)
|
|
331
|
+
return {
|
|
332
|
+
name,
|
|
333
|
+
file,
|
|
334
|
+
bytes: (yield* fs.exists(file))
|
|
335
|
+
? yield* fs.readFile(file).pipe(Effect.asSome)
|
|
336
|
+
: Option.none<Uint8Array>(),
|
|
337
|
+
}
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
// Every file gets a restoration attempt; onExit combines failures.
|
|
341
|
+
const restore = Arr.reduce<
|
|
342
|
+
(typeof backups)[number],
|
|
343
|
+
Effect.Effect<void, PlatformError.PlatformError>
|
|
344
|
+
>(backups, Effect.void, (previous, { file, bytes }) =>
|
|
345
|
+
previous.pipe(
|
|
346
|
+
Effect.onExit(() =>
|
|
347
|
+
Option.match(bytes, {
|
|
348
|
+
onNone: () => fs.remove(file, { force: true }),
|
|
349
|
+
onSome: (bytes) => fs.writeFile(file, bytes),
|
|
350
|
+
}),
|
|
351
|
+
),
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
const agentLocks =
|
|
355
|
+
agent === "npm"
|
|
356
|
+
? ["package-lock.json", "npm-shrinkwrap.json"]
|
|
357
|
+
: agent === "pnpm"
|
|
358
|
+
? ["pnpm-lock.yaml"]
|
|
359
|
+
: ["yarn.lock"]
|
|
360
|
+
const hasLock = Arr.some(
|
|
361
|
+
backups,
|
|
362
|
+
({ name, bytes }) =>
|
|
363
|
+
Arr.contains(agentLocks, name) && Option.isSome(bytes),
|
|
364
|
+
)
|
|
365
|
+
return yield* Effect.acquireUseRelease(
|
|
366
|
+
Effect.void,
|
|
367
|
+
() =>
|
|
368
|
+
Effect.gen(function* () {
|
|
369
|
+
yield* fs.writeFileString(manifestFile, text)
|
|
370
|
+
yield* run("install")
|
|
371
|
+
const after = yield* readState
|
|
372
|
+
return { agent, ...diffState(before, after) }
|
|
373
|
+
}),
|
|
374
|
+
(_, exit) =>
|
|
375
|
+
Exit.isSuccess(exit)
|
|
376
|
+
? Effect.void
|
|
377
|
+
: restore.pipe(
|
|
378
|
+
Effect.andThen(hasLock ? run("recover") : Effect.void),
|
|
379
|
+
// Recovery itself may rewrite the manifest or lockfile, including on failure.
|
|
380
|
+
Effect.onExit(() => restore),
|
|
381
|
+
),
|
|
382
|
+
)
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
return {
|
|
386
|
+
directory,
|
|
387
|
+
agent,
|
|
388
|
+
readState: lock.withPermits(1)(readState),
|
|
389
|
+
install,
|
|
390
|
+
}
|
|
391
|
+
})
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Array as Arr, Effect, Record, Schema, String as Str } from "effect"
|
|
2
|
+
|
|
3
|
+
export const PackageName = Schema.String.check(
|
|
4
|
+
Schema.isPattern(/^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i),
|
|
5
|
+
Schema.isMaxLength(214),
|
|
6
|
+
)
|
|
7
|
+
const Dependencies = Schema.Record(PackageName, Schema.String)
|
|
8
|
+
export const Manifest = Schema.StructWithRest(
|
|
9
|
+
Schema.Struct({
|
|
10
|
+
packageManager: Schema.optionalKey(Schema.String),
|
|
11
|
+
dependencies: Schema.optionalKey(Dependencies),
|
|
12
|
+
devDependencies: Schema.optionalKey(Dependencies),
|
|
13
|
+
optionalDependencies: Schema.optionalKey(Dependencies),
|
|
14
|
+
}),
|
|
15
|
+
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
16
|
+
)
|
|
17
|
+
export type Manifest = typeof Manifest.Type
|
|
18
|
+
export const decodeManifest = Schema.decodeUnknownEffect(
|
|
19
|
+
Schema.fromJsonString(Manifest),
|
|
20
|
+
)
|
|
21
|
+
export const Changes = Schema.Record(
|
|
22
|
+
PackageName,
|
|
23
|
+
Schema.Union([Schema.String.check(Schema.isPattern(/\S/)), Schema.Null]),
|
|
24
|
+
)
|
|
25
|
+
/** A spec adds or updates a dependency; null removes it from all dependency fields. */
|
|
26
|
+
export type Changes = typeof Changes.Type
|
|
27
|
+
export const fields = [
|
|
28
|
+
"dependencies",
|
|
29
|
+
"devDependencies",
|
|
30
|
+
"optionalDependencies",
|
|
31
|
+
] as const
|
|
32
|
+
|
|
33
|
+
export const applyChanges = (
|
|
34
|
+
manifest: Manifest,
|
|
35
|
+
changes: Changes,
|
|
36
|
+
): Manifest => {
|
|
37
|
+
let next = manifest
|
|
38
|
+
for (const [name, spec] of Record.toEntries(changes)) {
|
|
39
|
+
const existing = Arr.filter(fields, (field) =>
|
|
40
|
+
Record.has(next[field] ?? {}, name),
|
|
41
|
+
)
|
|
42
|
+
const targets =
|
|
43
|
+
spec === null
|
|
44
|
+
? fields
|
|
45
|
+
: existing.length > 0
|
|
46
|
+
? existing
|
|
47
|
+
: (["dependencies"] as const)
|
|
48
|
+
for (const field of targets) {
|
|
49
|
+
const deps = next[field] ?? {}
|
|
50
|
+
next = {
|
|
51
|
+
...next,
|
|
52
|
+
[field]:
|
|
53
|
+
spec === null
|
|
54
|
+
? Record.remove(deps, name)
|
|
55
|
+
: Record.set(deps, name, spec),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return next
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const encodeManifest = Effect.fnUntraced(function* (
|
|
63
|
+
manifest: Manifest,
|
|
64
|
+
original: string,
|
|
65
|
+
) {
|
|
66
|
+
const space = /^[{[]\r?\n([ \t]+)/.exec(original)?.[1] ?? 2
|
|
67
|
+
const text = yield* Schema.encodeEffect(
|
|
68
|
+
Schema.fromJsonString(Manifest, { space }),
|
|
69
|
+
)(manifest)
|
|
70
|
+
const eol = Str.includes("\r\n")(original) ? "\r\n" : "\n"
|
|
71
|
+
return Str.replaceAll("\n", eol)(text) + (/\r?\n$/.test(original) ? eol : "")
|
|
72
|
+
})
|