@effected/package-json 0.7.3 → 0.9.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.js CHANGED
@@ -10,7 +10,7 @@ import { isValidExpression } from "@effected/spdx";
10
10
  *
11
11
  * @public
12
12
  */
13
- var InvalidSpdxLicenseError = class extends Schema.TaggedErrorClass()("InvalidSpdxLicenseError", {
13
+ var InvalidSpdxLicenseError = class extends Schema.TaggedError()("InvalidSpdxLicenseError", {
14
14
  /** The raw input string that failed validation. */
15
15
  input: Schema.String }) {
16
16
  get message() {
package/Package.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Dependency } from "./Dependency.js";
2
2
  import { DevEnginesSchema } from "./DevEngines.js";
3
3
  import { InvalidSpdxLicenseError, SpdxLicense, isValidSpdx } from "./License.js";
4
- import { renderJson, resolveIndent } from "./internal/format.js";
4
+ import { renderJson, resolveFormatOptions } from "./internal/format.js";
5
+ import { makeWire } from "./internal/wire.js";
5
6
  import { PackageManager } from "./PackageManager.js";
6
7
  import { InvalidPackageNameError, PackageName } from "./PackageName.js";
7
8
  import { Person } from "./Person.js";
@@ -80,42 +81,13 @@ const RepositoryField = Schema.Union([Schema.String, Schema.Record(Schema.String
80
81
  *
81
82
  * @public
82
83
  */
83
- var PackageDecodeError = class extends Schema.TaggedErrorClass()("PackageDecodeError", {
84
+ var PackageDecodeError = class extends Schema.TaggedError()("PackageDecodeError", {
84
85
  /** The underlying `SchemaError`, preserved structurally rather than stringified. */
85
86
  cause: Schema.Defect() }) {
86
87
  get message() {
87
88
  return "Failed to decode package.json";
88
89
  }
89
90
  };
90
- const resolveFormatOptions = (options) => ({
91
- indent: resolveIndent(options?.indent, options?.sourceText),
92
- sort: options?.sort ?? true,
93
- stripEmpty: options?.stripEmpty ?? true,
94
- newline: options?.newline ?? true
95
- });
96
- const RawJson = Schema.Record(Schema.String, Schema.Unknown);
97
- const makeWire = (Class) => {
98
- const knownKeys = new Set(Object.keys(Class.fields).filter((k) => k !== "rest"));
99
- return RawJson.pipe(Schema.decodeTo(Class, SchemaTransformation.transform({
100
- decode: (raw) => {
101
- const known = {};
102
- const rest = {};
103
- for (const [key, value] of Object.entries(raw)) if (knownKeys.has(key)) known[key] = value;
104
- else rest[key] = value;
105
- return {
106
- ...known,
107
- rest
108
- };
109
- },
110
- encode: (encoded) => {
111
- const { rest, ...known } = encoded;
112
- return {
113
- ...rest ?? {},
114
- ...known
115
- };
116
- }
117
- })));
118
- };
119
91
  /**
120
92
  * A package.json document as a rich `Schema.Class`: typed known fields, a
121
93
  * `rest` catch-all preserving unknown top-level fields across a read/edit/write
@@ -1,4 +1,6 @@
1
1
  import { Package } from "./Package.js";
2
+ import { PackageJsonFormat } from "./PackageJsonFormat.js";
3
+ import { PackageManifest } from "./PackageManifest.js";
2
4
  import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
3
5
 
4
6
  //#region src/PackageJsonFile.ts
@@ -8,7 +10,7 @@ import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
8
10
  *
9
11
  * @public
10
12
  */
11
- var PackageJsonReadError = class extends Schema.TaggedErrorClass()("PackageJsonReadError", {
13
+ var PackageJsonReadError = class extends Schema.TaggedError()("PackageJsonReadError", {
12
14
  /** The path that could not be read. */
13
15
  path: Schema.String,
14
16
  /** The underlying failure, preserved structurally. */
@@ -24,7 +26,7 @@ var PackageJsonReadError = class extends Schema.TaggedErrorClass()("PackageJsonR
24
26
  *
25
27
  * @public
26
28
  */
27
- var PackageJsonNotFoundError = class extends Schema.TaggedErrorClass()("PackageJsonNotFoundError", {
29
+ var PackageJsonNotFoundError = class extends Schema.TaggedError()("PackageJsonNotFoundError", {
28
30
  /** The path where package.json was expected. */
29
31
  path: Schema.String }) {
30
32
  get message() {
@@ -36,7 +38,7 @@ path: Schema.String }) {
36
38
  *
37
39
  * @public
38
40
  */
39
- var PackageJsonParseError = class extends Schema.TaggedErrorClass()("PackageJsonParseError", {
41
+ var PackageJsonParseError = class extends Schema.TaggedError()("PackageJsonParseError", {
40
42
  /** The path whose contents failed to parse as JSON. */
41
43
  path: Schema.String,
42
44
  /** The underlying `SyntaxError`, preserved structurally. */
@@ -53,7 +55,7 @@ var PackageJsonParseError = class extends Schema.TaggedErrorClass()("PackageJson
53
55
  *
54
56
  * @public
55
57
  */
56
- var PackageJsonWriteError = class extends Schema.TaggedErrorClass()("PackageJsonWriteError", {
58
+ var PackageJsonWriteError = class extends Schema.TaggedError()("PackageJsonWriteError", {
57
59
  /** The path that could not be written. */
58
60
  path: Schema.String,
59
61
  /** The underlying filesystem failure, preserved structurally. Narrowed to the write failure only. */
@@ -88,40 +90,63 @@ var PackageJsonFile = class PackageJsonFile extends Context.Service()("@effected
88
90
  static make = Effect.gen(function* () {
89
91
  const fs = yield* FileSystem.FileSystem;
90
92
  const path = yield* Path.Path;
91
- return {
92
- read: Effect.fn("PackageJsonFile.read")(function* (target) {
93
- const content = yield* fs.readFileString(target).pipe(Effect.mapError((cause) => cause.reason._tag === "NotFound" ? new PackageJsonNotFoundError({ path: target }) : new PackageJsonReadError({
93
+ const readText = (target) => fs.readFileString(target).pipe(Effect.mapError((cause) => cause.reason._tag === "NotFound" ? new PackageJsonNotFoundError({ path: target }) : new PackageJsonReadError({
94
+ path: target,
95
+ cause
96
+ })));
97
+ const readJson = (target) => Effect.gen(function* () {
98
+ const content = yield* readText(target);
99
+ return yield* Effect.try({
100
+ try: () => JSON.parse(content),
101
+ catch: (cause) => new PackageJsonParseError({
94
102
  path: target,
95
103
  cause
96
- })));
97
- const json = yield* Effect.try({
98
- try: () => JSON.parse(content),
99
- catch: (cause) => new PackageJsonParseError({
100
- path: target,
101
- cause
102
- })
103
- });
104
- return yield* Package.decode(json);
104
+ })
105
+ });
106
+ });
107
+ const withPreservedSource = (target, options) => Effect.gen(function* () {
108
+ if (options?.indent !== "preserve" || options.sourceText !== void 0) return options;
109
+ const existing = yield* fs.readFileString(target).pipe(Effect.catch(() => Effect.succeed(void 0)));
110
+ return existing === void 0 ? options : {
111
+ ...options,
112
+ sourceText: existing
113
+ };
114
+ });
115
+ const writeText = (target, json) => Effect.gen(function* () {
116
+ const directory = path.dirname(target);
117
+ yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError((cause) => new PackageJsonWriteError({
118
+ path: target,
119
+ cause
120
+ })));
121
+ yield* fs.writeFileString(target, json).pipe(Effect.mapError((cause) => new PackageJsonWriteError({
122
+ path: target,
123
+ cause
124
+ })));
125
+ });
126
+ return {
127
+ read: Effect.fn("PackageJsonFile.read")(function* (target) {
128
+ return yield* Package.decode(yield* readJson(target));
105
129
  }),
106
130
  write: Effect.fn("PackageJsonFile.write")(function* (target, pkg, options) {
107
- let effective = options;
108
- if (options?.indent === "preserve" && options.sourceText === void 0) {
109
- const existing = yield* fs.readFileString(target).pipe(Effect.catch(() => Effect.succeed(void 0)));
110
- if (existing !== void 0) effective = {
111
- ...options,
112
- sourceText: existing
113
- };
114
- }
115
- const json = pkg.toJsonString(effective);
116
- const directory = path.dirname(target);
117
- yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError((cause) => new PackageJsonWriteError({
118
- path: target,
119
- cause
120
- })));
121
- yield* fs.writeFileString(target, json).pipe(Effect.mapError((cause) => new PackageJsonWriteError({
131
+ const effective = yield* withPreservedSource(target, options);
132
+ yield* writeText(target, pkg.toJsonString(effective));
133
+ }),
134
+ readManifest: Effect.fn("PackageJsonFile.readManifest")(function* (target) {
135
+ return yield* PackageManifest.decode(yield* readJson(target));
136
+ }),
137
+ writeManifest: Effect.fn("PackageJsonFile.writeManifest")(function* (target, manifest, options) {
138
+ const effective = yield* withPreservedSource(target, options);
139
+ yield* writeText(target, manifest.toJsonString(effective));
140
+ }),
141
+ modify: Effect.fn("PackageJsonFile.modify")(function* (target, edits) {
142
+ const source = yield* readText(target);
143
+ let text = source;
144
+ for (const edit of edits) text = yield* PackageJsonFormat.modifyToString(text, edit.path, edit.value).pipe(Effect.catchTag("PackageJsonSyntaxError", (cause) => new PackageJsonParseError({
122
145
  path: target,
123
146
  cause
124
147
  })));
148
+ if (text !== source) yield* writeText(target, text);
149
+ return text;
125
150
  })
126
151
  };
127
152
  });
@@ -1,5 +1,6 @@
1
- import { renderJson, resolveIndent, sortKeys } from "./internal/format.js";
2
- import { Result, Schema } from "effect";
1
+ import { detectIndent, renderJson, resolveIndent, sortKeys } from "./internal/format.js";
2
+ import { JsoncEdit, JsoncModifier } from "@effected/jsonc";
3
+ import { Effect, Result, Schema } from "effect";
3
4
 
4
5
  //#region src/PackageJsonFormat.ts
5
6
  /**
@@ -14,7 +15,7 @@ import { Result, Schema } from "effect";
14
15
  *
15
16
  * @public
16
17
  */
17
- var PackageJsonSyntaxError = class extends Schema.TaggedErrorClass()("PackageJsonSyntaxError", {
18
+ var PackageJsonSyntaxError = class extends Schema.TaggedError()("PackageJsonSyntaxError", {
18
19
  /** Which syntactic precondition failed. */
19
20
  reason: Schema.Literals(["invalid-json", "not-an-object"]),
20
21
  /** The underlying `SyntaxError` for `"invalid-json"`, preserved structurally. */
@@ -26,6 +27,28 @@ var PackageJsonSyntaxError = class extends Schema.TaggedErrorClass()("PackageJso
26
27
  };
27
28
  const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28
29
  /**
30
+ * Indicates that a surgical modification could not be applied: the value on
31
+ * the navigation path is not the container kind the next path segment
32
+ * requires. The underlying `@effected/jsonc` `JsoncModificationError` —
33
+ * which names the expected container and the 1-based depth of the mismatch —
34
+ * is preserved on the structured `cause` field, never stringified.
35
+ *
36
+ * Raised by {@link PackageJsonFormat.modify} and
37
+ * {@link PackageJsonFormat.modifyToString}.
38
+ *
39
+ * @public
40
+ */
41
+ var PackageJsonModifyError = class extends Schema.TaggedError()("PackageJsonModifyError", {
42
+ /** The field path whose navigation failed. */
43
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Number])),
44
+ /** The underlying `JsoncModificationError`, preserved structurally. */
45
+ cause: Schema.Defect()
46
+ }) {
47
+ get message() {
48
+ return `Failed to modify package.json at path [${this.path.join(", ")}]`;
49
+ }
50
+ };
51
+ /**
29
52
  * Decode-free canonical sort and format statics. Not instantiable.
30
53
  *
31
54
  * @remarks
@@ -44,7 +67,7 @@ const isJsonObject = (value) => typeof value === "object" && value !== null && !
44
67
  *
45
68
  * @public
46
69
  */
47
- var PackageJsonFormat = class {
70
+ var PackageJsonFormat = class PackageJsonFormat {
48
71
  constructor() {}
49
72
  /**
50
73
  * Order a package.json object's keys canonically **without decoding it into
@@ -132,7 +155,79 @@ var PackageJsonFormat = class {
132
155
  newline: options?.newline ?? true
133
156
  }));
134
157
  }
158
+ /**
159
+ * Compute the surgical edits that set, replace or delete the value at
160
+ * `path` **without decoding, sorting or reformatting anything else**. The
161
+ * opposite posture to {@link PackageJsonFormat.formatToString}: where the
162
+ * formatter's job is the canonical order, the mutator's job is to leave
163
+ * every untouched byte untouched — key order, indentation, line endings and
164
+ * the trailing newline all survive, because only the edited span changes.
165
+ * That is what makes the result reviewable when a tool commits a one-field
166
+ * change to someone else's repository.
167
+ *
168
+ * Built on `@effected/jsonc`'s scanner-based edit engine. Inserted content
169
+ * matches the source's own style: indentation (tab vs N spaces) is detected
170
+ * from the first indented line and the line ending from the first `\r\n`.
171
+ *
172
+ * Passing `value === undefined` deletes the target key (including its
173
+ * comma) — the `@effected/jsonc` / `@effected/yaml` modify convention. A
174
+ * missing insertion target appends after the last key of its container.
175
+ *
176
+ * @param source - the package.json file contents (strict JSON — npm does
177
+ * not accept comments, and neither does this)
178
+ * @param path - the field path, e.g. `["packageManager"]` or
179
+ * `["devEngines", "runtime", "version"]`
180
+ * @param value - the plain JSON value to write, or `undefined` to delete
181
+ * @returns the edits to apply via `JsoncEdit.applyAll` — or use
182
+ * {@link PackageJsonFormat.modifyToString} for the applied text in one step
183
+ */
184
+ static modify = Effect.fn("PackageJsonFormat.modify")(function* (source, path, value) {
185
+ let parsed;
186
+ try {
187
+ parsed = JSON.parse(source);
188
+ } catch (cause) {
189
+ return yield* new PackageJsonSyntaxError({
190
+ reason: "invalid-json",
191
+ cause
192
+ });
193
+ }
194
+ if (!isJsonObject(parsed)) return yield* new PackageJsonSyntaxError({ reason: "not-an-object" });
195
+ const indent = detectIndent(source);
196
+ const formattingOptions = {
197
+ insertSpaces: indent !== " ",
198
+ tabSize: indent === void 0 || indent === " " ? 2 : indent.length,
199
+ eol: source.includes("\r\n") ? "\r\n" : "\n"
200
+ };
201
+ return yield* JsoncModifier.modify(source, path, value, { formattingOptions }).pipe(Effect.catchTag("JsoncModificationError", (cause) => new PackageJsonModifyError({
202
+ path,
203
+ cause
204
+ })));
205
+ });
206
+ /**
207
+ * Modify `source` and apply the resulting edits in one step
208
+ * (`JsoncEdit.applyAll` composed over {@link PackageJsonFormat.modify}).
209
+ * Text in, text out; every byte outside the edited span is preserved.
210
+ * Inherits the modify error channel: {@link PackageJsonSyntaxError} when
211
+ * the source is not a JSON object, {@link PackageJsonModifyError} when the
212
+ * path cannot be navigated.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * import { PackageJsonFormat } from "@effected/package-json";
217
+ * import { Effect } from "effect";
218
+ *
219
+ * const program = PackageJsonFormat.modifyToString(
220
+ * '{\n "private": true,\n "packageManager": "pnpm@11.2.0"\n}\n',
221
+ * ["packageManager"],
222
+ * "pnpm@11.3.0",
223
+ * ); // only the packageManager value changes; every other byte survives
224
+ * ```
225
+ */
226
+ static modifyToString = Effect.fn("PackageJsonFormat.modifyToString")(function* (source, path, value) {
227
+ const edits = yield* PackageJsonFormat.modify(source, path, value);
228
+ return JsoncEdit.applyAll(source, edits);
229
+ });
135
230
  };
136
231
 
137
232
  //#endregion
138
- export { PackageJsonFormat, PackageJsonSyntaxError };
233
+ export { PackageJsonFormat, PackageJsonModifyError, PackageJsonSyntaxError };
package/PackageManager.js CHANGED
@@ -4,7 +4,7 @@ import { SemVer } from "@effected/semver";
4
4
 
5
5
  //#region src/PackageManager.ts
6
6
  const PACKAGE_MANAGER_NAME_RE = /^[a-z]+$/;
7
- const invalid = (input, message) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message }));
7
+ const invalid = (input, message) => Effect.fail(new SchemaIssue.InvalidValue({ message }, input));
8
8
  /**
9
9
  * A structured `packageManager` value with `name`, `version` and an optional
10
10
  * `integrity` hash.
@@ -0,0 +1,124 @@
1
+ import { CorepackIntegrityHash } from "@effected/npm";
2
+ import { Effect, Exit, Option, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
3
+ import { Range, SemVer } from "@effected/semver";
4
+
5
+ //#region src/PackageManagerRange.ts
6
+ const PACKAGE_MANAGER_NAME_RE = /^[a-z]+$/;
7
+ const invalid = (input, message) => Effect.fail(new SchemaIssue.InvalidValue({ message }, input));
8
+ /** `Schema.String` refined to parse as a semver range (`Range.parseResult` succeeds). The check is erased from the built type. */
9
+ const SemVerRangeString = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length > 0 && Result.isSuccess(Range.parseResult(value)) ? void 0 : "Expected a semver range (an exact version, a caret/tilde range, a comparator set, ...)")));
10
+ /**
11
+ * A structured `packageManager` value whose version position is a semver
12
+ * **range**, carried verbatim: `name`, `range` and an optional `integrity`
13
+ * hash.
14
+ *
15
+ * @remarks
16
+ * The range-tolerant sibling of {@link PackageManager}. The strict class
17
+ * models the corepack pin — an exact version, which is all corepack itself
18
+ * accepts — and stays strict; this class models the field as pnpm reads it,
19
+ * where a range such as `^11.20.0` is a supported spelling that pnpm resolves
20
+ * to a concrete version. An exact version is a valid range, so every string
21
+ * the strict codec accepts decodes here too; {@link PackageManagerRange.isExact}
22
+ * is how a caller tracks which form the manifest actually carried.
23
+ *
24
+ * The `range` field is the manifest's text **verbatim** — validated to parse
25
+ * as a semver range but never normalized, so encoding is byte-identical to
26
+ * the accepted input and reading a manifest never rewrites the field.
27
+ * Interpretation is a derived getter, following `Repository`'s
28
+ * carry-verbatim posture.
29
+ *
30
+ * The first `+` after the `@` begins the integrity component, exactly as in
31
+ * the strict grammar — the version position of this field never carries
32
+ * semver build metadata.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { PackageManagerRange } from "@effected/package-json";
37
+ * import { Effect, Schema } from "effect";
38
+ *
39
+ * const program = Effect.gen(function* () {
40
+ * const pm = yield* Schema.decodeUnknownEffect(PackageManagerRange.FromString)("pnpm@^11.20.0");
41
+ * console.log(pm.name, pm.range, pm.isExact); // "pnpm" "^11.20.0" false
42
+ * });
43
+ * ```
44
+ *
45
+ * @public
46
+ */
47
+ var PackageManagerRange = class PackageManagerRange extends Schema.Class("PackageManagerRange")({
48
+ /** The package-manager name (e.g. `pnpm`). Any lowercase name — the same latitude as {@link PackageManager}, for the same evidence. */
49
+ name: Schema.String,
50
+ /**
51
+ * The version position, verbatim: a semver range (`^11.20.0`,
52
+ * `>=10 <12`, ...) or an exact version (`11.2.0`). Validated to parse
53
+ * through `@effected/semver`'s `Range.parseResult`; never normalized, so
54
+ * the field round-trips byte-identically.
55
+ */
56
+ range: SemVerRangeString,
57
+ /**
58
+ * The optional integrity hash (e.g. `sha512.abc`): `@effected/npm`'s
59
+ * `CorepackIntegrityHash`. Meaningful only alongside an exact range —
60
+ * an integrity pins one artifact — but carried whenever the manifest
61
+ * carries it, because fidelity outranks plausibility in a field model.
62
+ */
63
+ integrity: Schema.Option(CorepackIntegrityHash)
64
+ }) {
65
+ /**
66
+ * Schema transformation between the `"name@range[+integrity]"` string and a
67
+ * {@link PackageManagerRange}.
68
+ *
69
+ * @remarks
70
+ * Decoding splits on the first `@`, then on the first `+` — which always
71
+ * begins the integrity, never semver build metadata — and validates each
72
+ * component: the name against the lowercase grammar, the range through
73
+ * `@effected/semver`'s `Range.parseResult`, the integrity through
74
+ * `CorepackIntegrityHash`. Every failure is a typed decode failure naming
75
+ * the component that failed. Encoding reconstructs the string from the
76
+ * verbatim parts, so it is byte-identical to any input this codec accepts.
77
+ */
78
+ static FromString = Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(PackageManagerRange), SchemaTransformation.transformOrFail({
79
+ decode: (input) => {
80
+ const at = input.indexOf("@");
81
+ if (at === -1) return invalid(input, `Invalid packageManager format: "${input}"`);
82
+ const name = input.slice(0, at);
83
+ if (!PACKAGE_MANAGER_NAME_RE.test(name)) return invalid(input, `Invalid packageManager name: "${name}"`);
84
+ const rest = input.slice(at + 1);
85
+ const plus = rest.indexOf("+");
86
+ const range = plus === -1 ? rest : rest.slice(0, plus);
87
+ if (range.length === 0 || Result.isFailure(Range.parseResult(range))) return invalid(input, `Invalid packageManager range: "${range}"`);
88
+ if (plus === -1) return Effect.succeed(PackageManagerRange.make({
89
+ name,
90
+ range,
91
+ integrity: Option.none()
92
+ }));
93
+ const rawIntegrity = rest.slice(plus + 1);
94
+ const decoded = Schema.decodeUnknownExit(CorepackIntegrityHash)(rawIntegrity);
95
+ if (Exit.isFailure(decoded)) return invalid(input, `Invalid packageManager integrity: "${rawIntegrity}"`);
96
+ return Effect.succeed(PackageManagerRange.make({
97
+ name,
98
+ range,
99
+ integrity: Option.some(decoded.value)
100
+ }));
101
+ },
102
+ encode: (pm) => Effect.succeed(Option.match(pm.integrity, {
103
+ onNone: () => `${pm.name}@${pm.range}`,
104
+ onSome: (integrity) => `${pm.name}@${pm.range}+${integrity}`
105
+ }))
106
+ })));
107
+ /**
108
+ * Whether the range is an exact, pinnable version (`11.2.0`) rather than a
109
+ * genuine range (`^11.20.0`) — decided by `SemVer.isPinnable` over the
110
+ * verbatim text, so `=11.2.0` and other range spellings of a single
111
+ * version report `false`. This is the exactness a consumer tracks when it
112
+ * must re-emit the same spelling it read.
113
+ */
114
+ get isExact() {
115
+ return SemVer.isPinnable(this.range);
116
+ }
117
+ /** Whether an integrity hash is present. */
118
+ get hasIntegrity() {
119
+ return Option.isSome(this.integrity);
120
+ }
121
+ };
122
+
123
+ //#endregion
124
+ export { PackageManagerRange };
@@ -0,0 +1,93 @@
1
+ import { renderJson, resolveFormatOptions } from "./internal/format.js";
2
+ import { makeWire } from "./internal/wire.js";
3
+ import { PackageName } from "./PackageName.js";
4
+ import { Package, PackageDecodeError } from "./Package.js";
5
+ import { PackageManagerRange } from "./PackageManagerRange.js";
6
+ import { Effect, Schema } from "effect";
7
+ import { SemVer } from "@effected/semver";
8
+
9
+ //#region src/PackageManifest.ts
10
+ /**
11
+ * A package.json document as it exists on disk, publishable or not: every
12
+ * field of {@link Package} with `name` and `version` optional and
13
+ * `packageManager` accepting the range spelling.
14
+ *
15
+ * @remarks
16
+ * **Lenient about absence, strict about shape.** npm requires `name` and
17
+ * `version` only for a package that will be published; the idiomatic private
18
+ * workspace root (`{ "private": true, "packageManager": "pnpm@11.2.0" }`)
19
+ * carries neither, and {@link Package.decode} rightly rejects it — the strict
20
+ * model's contract is publishability. This class decodes that shape, and any
21
+ * other manifest, so long as every field that IS present satisfies its typed
22
+ * codec: a present `version` must still be strict semver (`"1.0"` fails
23
+ * typed), a present `name` must still satisfy the npm grammar, a present
24
+ * `packageManager` must still parse — though here the version position may be
25
+ * a semver range (`pnpm@^11.20.0`), decoded as {@link PackageManagerRange}.
26
+ * For total tolerance of malformed fields, use the decode-free
27
+ * {@link PackageJsonFormat} text path (or `@effected/npm`'s shape-blind
28
+ * `Manifest`) — silently carrying a value the type claims to have validated
29
+ * would be a lie, and silently dropping it would break round-trip fidelity.
30
+ *
31
+ * The model is deliberately lean — fields, the `rest` catch-all wire codec,
32
+ * {@link PackageManifest.decode} and {@link PackageManifest.toJsonString} —
33
+ * because the write half of a manifest-editing tool is the surgical
34
+ * {@link PackageJsonFormat.modifyToString} / `PackageJsonFile.modify` path,
35
+ * which never goes through a model at all. Mutation statics live on the
36
+ * strict {@link Package}.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { PackageManifest } from "@effected/package-json";
41
+ * import { Effect, Option } from "effect";
42
+ *
43
+ * const program = Effect.gen(function* () {
44
+ * const root = yield* PackageManifest.decode({ private: true, packageManager: "pnpm@^11.20.0" });
45
+ * console.log(root.isPrivate, root.packageManager?.isExact); // true false
46
+ * });
47
+ * ```
48
+ *
49
+ * @public
50
+ */
51
+ var PackageManifest = class PackageManifest extends Schema.Class("PackageManifest")({
52
+ ...Package.fields,
53
+ name: Schema.optionalKey(PackageName),
54
+ version: Schema.optionalKey(SemVer.FromString),
55
+ packageManager: Schema.optionalKey(PackageManagerRange.FromString)
56
+ }) {
57
+ /**
58
+ * The wire codec: an open JSON object ↔ a {@link PackageManifest} instance,
59
+ * partitioning unknown keys into `rest` and flattening them back on encode —
60
+ * the same transform {@link Package.schema} uses, over this class's fields.
61
+ */
62
+ static schema = makeWire(PackageManifest);
63
+ /**
64
+ * Decode an unknown JSON value into a {@link PackageManifest}, normalizing
65
+ * any `SchemaError` to a typed {@link PackageDecodeError} at the boundary.
66
+ *
67
+ * @param input - the parsed package.json JSON value (e.g. from `JSON.parse`)
68
+ * @returns an Effect resolving to the decoded `PackageManifest`
69
+ * @throws (typed) `PackageDecodeError` when a present field does not satisfy
70
+ * its codec
71
+ */
72
+ static decode = Effect.fn("PackageManifest.decode")(function* (input) {
73
+ return yield* Schema.decodeUnknownEffect(PackageManifest.schema)(input).pipe(Effect.catchTag("SchemaError", (cause) => new PackageDecodeError({ cause })));
74
+ });
75
+ /** Whether the manifest is marked private. */
76
+ get isPrivate() {
77
+ return this.private ?? false;
78
+ }
79
+ /**
80
+ * Serialize to a formatted package.json string: encode through the wire
81
+ * codec (flattening `rest`), then apply the canonical key order, dependency
82
+ * sorting and empty-map stripping unless the options opt out. Pure, and
83
+ * shared with `Package.toJsonString` down to the same internal renderer.
84
+ * Absent `name` / `version` keys stay absent — nothing is invented.
85
+ */
86
+ toJsonString(options) {
87
+ const raw = Schema.encodeUnknownSync(PackageManifest.schema)(this);
88
+ return renderJson(raw, resolveFormatOptions(options));
89
+ }
90
+ };
91
+
92
+ //#endregion
93
+ export { PackageManifest };
package/PackageName.js CHANGED
@@ -9,7 +9,7 @@ import { Option, Schema } from "effect";
9
9
  *
10
10
  * @public
11
11
  */
12
- var InvalidPackageNameError = class extends Schema.TaggedErrorClass()("InvalidPackageNameError", {
12
+ var InvalidPackageNameError = class extends Schema.TaggedError()("InvalidPackageNameError", {
13
13
  /** The raw input string that failed validation. */
14
14
  input: Schema.String }) {
15
15
  get message() {
@@ -9,7 +9,7 @@ import { Context, Effect, HashMap, Layer, Option, Result, Schema } from "effect"
9
9
  *
10
10
  * @public
11
11
  */
12
- var PackageValidationError = class extends Schema.TaggedErrorClass()("PackageValidationError", {
12
+ var PackageValidationError = class extends Schema.TaggedError()("PackageValidationError", {
13
13
  /** The aggregated rule failures. */
14
14
  failures: Schema.Array(Schema.Struct({
15
15
  rule: Schema.String,
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { JsoncEdit, JsoncEdit as JsoncEdit$1, JsoncPath, JsoncPath as JsoncPath$1 } from "@effected/jsonc";
1
2
  import { CatalogResolver, DependencyKind, DependencyProtocol, DependencyProtocol as DependencyProtocol$1, DependencySpecifier, DependencySpecifierBrand, InvalidDependencySpecifierError, WorkspaceResolver, isValidDependencySpecifier } from "@effected/npm";
2
3
  import { InvalidVersionError, Range, SemVer } from "@effected/semver";
3
4
  import { Brand, Context, Effect, FileSystem, HashMap, Layer, Option, Path, Result, Schema } from "effect";
@@ -745,117 +746,6 @@ declare class Package extends Package_base {
745
746
  toJsonString(options?: PackageFormatOptions): string;
746
747
  }
747
748
  //#endregion
748
- //#region src/PackageJsonFile.d.ts
749
- declare const PackageJsonReadError_base: Schema.Class<PackageJsonReadError, Schema.TaggedStruct<"PackageJsonReadError", {
750
- /** The path that could not be read. */
751
- readonly path: Schema.String;
752
- /** The underlying failure, preserved structurally. */
753
- readonly cause: Schema.Defect;
754
- }>, import("effect/Cause").YieldableError>;
755
- /**
756
- * Indicates that a package.json file could not be read from the filesystem
757
- * (a filesystem error other than not-found).
758
- *
759
- * @public
760
- */
761
- declare class PackageJsonReadError extends PackageJsonReadError_base {
762
- get message(): string;
763
- }
764
- declare const PackageJsonNotFoundError_base: Schema.Class<PackageJsonNotFoundError, Schema.TaggedStruct<"PackageJsonNotFoundError", {
765
- /** The path where package.json was expected. */
766
- readonly path: Schema.String;
767
- }>, import("effect/Cause").YieldableError>;
768
- /**
769
- * Indicates that no package.json file exists at the expected path. Carries its
770
- * own tag for `catchTag` routing.
771
- *
772
- * @public
773
- */
774
- declare class PackageJsonNotFoundError extends PackageJsonNotFoundError_base {
775
- get message(): string;
776
- }
777
- declare const PackageJsonParseError_base: Schema.Class<PackageJsonParseError, Schema.TaggedStruct<"PackageJsonParseError", {
778
- /** The path whose contents failed to parse as JSON. */
779
- readonly path: Schema.String;
780
- /** The underlying `SyntaxError`, preserved structurally. */
781
- readonly cause: Schema.Defect;
782
- }>, import("effect/Cause").YieldableError>;
783
- /**
784
- * Indicates that a package.json file's contents are not valid JSON.
785
- *
786
- * @public
787
- */
788
- declare class PackageJsonParseError extends PackageJsonParseError_base {
789
- get message(): string;
790
- }
791
- declare const PackageJsonWriteError_base: Schema.Class<PackageJsonWriteError, Schema.TaggedStruct<"PackageJsonWriteError", {
792
- /** The path that could not be written. */
793
- readonly path: Schema.String;
794
- /** The underlying filesystem failure, preserved structurally. Narrowed to the write failure only. */
795
- readonly cause: Schema.Defect;
796
- }>, import("effect/Cause").YieldableError>;
797
- /**
798
- * Indicates that a package.json file could not be written to the filesystem.
799
- * Narrowed to the filesystem-write failure only — never a resolution or encode
800
- * error.
801
- *
802
- * @public
803
- */
804
- declare class PackageJsonWriteError extends PackageJsonWriteError_base {
805
- get message(): string;
806
- }
807
- /**
808
- * The shape of the {@link PackageJsonFile} service — the value produced by
809
- * {@link PackageJsonFile.make} and carried by its layer.
810
- *
811
- * @public
812
- */
813
- interface PackageJsonFileShape {
814
- /**
815
- * Read and decode a package.json file. Fails with `PackageJsonNotFoundError`
816
- * (ENOENT), `PackageJsonReadError` (other fs errors), `PackageJsonParseError`
817
- * (invalid JSON) or `PackageDecodeError` (schema decode).
818
- */
819
- readonly read: (path: string) => Effect.Effect<Package, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageDecodeError>;
820
- /**
821
- * Serialize and write a package.json file. Fails with
822
- * `PackageJsonWriteError`. With `indent: "preserve"` and no explicit
823
- * `sourceText`, the existing file at `path` (when readable) supplies the
824
- * source text whose indentation is preserved.
825
- */
826
- readonly write: (path: string, pkg: Package, options?: PackageFormatOptions) => Effect.Effect<void, PackageJsonWriteError>;
827
- }
828
- declare const PackageJsonFile_base: Context.ServiceClass<PackageJsonFile, "@effected/package-json/PackageJsonFile", PackageJsonFileShape>;
829
- /**
830
- * Reads and writes package.json over core `FileSystem` / `Path`. The layer
831
- * requires those services; provide `@effect/platform-node`'s `NodeFileSystem` /
832
- * `NodePath` (or a bun equivalent) at the application boundary.
833
- *
834
- * @example
835
- * ```ts
836
- * import { PackageJsonFile } from "@effected/package-json";
837
- * import { NodeFileSystem, NodePath } from "@effect/platform-node";
838
- * import { Effect } from "effect";
839
- *
840
- * const program = Effect.gen(function* () {
841
- * const files = yield* PackageJsonFile;
842
- * const pkg = yield* files.read("./package.json");
843
- * console.log(pkg.name);
844
- * }).pipe(Effect.provide(PackageJsonFile.layer), Effect.provide(NodeFileSystem.layer), Effect.provide(NodePath.layer));
845
- * ```
846
- *
847
- * @public
848
- */
849
- declare class PackageJsonFile extends PackageJsonFile_base {
850
- /** Build the service implementation from `FileSystem` / `Path` in context; use {@link PackageJsonFile.layer} to provide it. */
851
- static readonly make: Effect.Effect<PackageJsonFileShape, never, FileSystem.FileSystem | Path.Path>;
852
- /**
853
- * The live layer. Requires core `FileSystem` / `Path`, provided by the
854
- * consumer's platform implementation at the edge.
855
- */
856
- static readonly layer: Layer.Layer<PackageJsonFile, never, FileSystem.FileSystem | Path.Path>;
857
- }
858
- //#endregion
859
749
  //#region src/PackageJsonFormat.d.ts
860
750
  declare const PackageJsonSyntaxError_base: Schema.Class<PackageJsonSyntaxError, Schema.TaggedStruct<"PackageJsonSyntaxError", {
861
751
  /** Which syntactic precondition failed. */
@@ -907,6 +797,27 @@ interface PackageFormatTextOptions {
907
797
  /** Append a trailing newline (default `true`). */
908
798
  readonly newline?: boolean;
909
799
  }
800
+ declare const PackageJsonModifyError_base: Schema.Class<PackageJsonModifyError, Schema.TaggedStruct<"PackageJsonModifyError", {
801
+ /** The field path whose navigation failed. */
802
+ readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Number]>>;
803
+ /** The underlying `JsoncModificationError`, preserved structurally. */
804
+ readonly cause: Schema.Defect;
805
+ }>, import("effect/Cause").YieldableError>;
806
+ /**
807
+ * Indicates that a surgical modification could not be applied: the value on
808
+ * the navigation path is not the container kind the next path segment
809
+ * requires. The underlying `@effected/jsonc` `JsoncModificationError` —
810
+ * which names the expected container and the 1-based depth of the mismatch —
811
+ * is preserved on the structured `cause` field, never stringified.
812
+ *
813
+ * Raised by {@link PackageJsonFormat.modify} and
814
+ * {@link PackageJsonFormat.modifyToString}.
815
+ *
816
+ * @public
817
+ */
818
+ declare class PackageJsonModifyError extends PackageJsonModifyError_base {
819
+ get message(): string;
820
+ }
910
821
  /**
911
822
  * Decode-free canonical sort and format statics. Not instantiable.
912
823
  *
@@ -996,6 +907,416 @@ declare class PackageJsonFormat {
996
907
  * ```
997
908
  */
998
909
  static formatToString(source: string, options?: PackageFormatTextOptions): Result.Result<string, PackageJsonSyntaxError>;
910
+ /**
911
+ * Compute the surgical edits that set, replace or delete the value at
912
+ * `path` **without decoding, sorting or reformatting anything else**. The
913
+ * opposite posture to {@link PackageJsonFormat.formatToString}: where the
914
+ * formatter's job is the canonical order, the mutator's job is to leave
915
+ * every untouched byte untouched — key order, indentation, line endings and
916
+ * the trailing newline all survive, because only the edited span changes.
917
+ * That is what makes the result reviewable when a tool commits a one-field
918
+ * change to someone else's repository.
919
+ *
920
+ * Built on `@effected/jsonc`'s scanner-based edit engine. Inserted content
921
+ * matches the source's own style: indentation (tab vs N spaces) is detected
922
+ * from the first indented line and the line ending from the first `\r\n`.
923
+ *
924
+ * Passing `value === undefined` deletes the target key (including its
925
+ * comma) — the `@effected/jsonc` / `@effected/yaml` modify convention. A
926
+ * missing insertion target appends after the last key of its container.
927
+ *
928
+ * @param source - the package.json file contents (strict JSON — npm does
929
+ * not accept comments, and neither does this)
930
+ * @param path - the field path, e.g. `["packageManager"]` or
931
+ * `["devEngines", "runtime", "version"]`
932
+ * @param value - the plain JSON value to write, or `undefined` to delete
933
+ * @returns the edits to apply via `JsoncEdit.applyAll` — or use
934
+ * {@link PackageJsonFormat.modifyToString} for the applied text in one step
935
+ */
936
+ static readonly modify: (source: string, path: JsoncPath$1, value: unknown) => Effect.Effect<readonly JsoncEdit$1[], PackageJsonModifyError | PackageJsonSyntaxError, never>;
937
+ /**
938
+ * Modify `source` and apply the resulting edits in one step
939
+ * (`JsoncEdit.applyAll` composed over {@link PackageJsonFormat.modify}).
940
+ * Text in, text out; every byte outside the edited span is preserved.
941
+ * Inherits the modify error channel: {@link PackageJsonSyntaxError} when
942
+ * the source is not a JSON object, {@link PackageJsonModifyError} when the
943
+ * path cannot be navigated.
944
+ *
945
+ * @example
946
+ * ```ts
947
+ * import { PackageJsonFormat } from "@effected/package-json";
948
+ * import { Effect } from "effect";
949
+ *
950
+ * const program = PackageJsonFormat.modifyToString(
951
+ * '{\n "private": true,\n "packageManager": "pnpm@11.2.0"\n}\n',
952
+ * ["packageManager"],
953
+ * "pnpm@11.3.0",
954
+ * ); // only the packageManager value changes; every other byte survives
955
+ * ```
956
+ */
957
+ static readonly modifyToString: (source: string, path: JsoncPath$1, value: unknown) => Effect.Effect<string, PackageJsonModifyError | PackageJsonSyntaxError, never>;
958
+ }
959
+ //#endregion
960
+ //#region src/PackageManagerRange.d.ts
961
+ declare const PackageManagerRange_base: Schema.Class<PackageManagerRange, Schema.Struct<{
962
+ /** The package-manager name (e.g. `pnpm`). Any lowercase name — the same latitude as {@link PackageManager}, for the same evidence. */
963
+ readonly name: Schema.String;
964
+ /**
965
+ * The version position, verbatim: a semver range (`^11.20.0`,
966
+ * `>=10 <12`, ...) or an exact version (`11.2.0`). Validated to parse
967
+ * through `@effected/semver`'s `Range.parseResult`; never normalized, so
968
+ * the field round-trips byte-identically.
969
+ */
970
+ readonly range: Schema.String;
971
+ /**
972
+ * The optional integrity hash (e.g. `sha512.abc`): `@effected/npm`'s
973
+ * `CorepackIntegrityHash`. Meaningful only alongside an exact range —
974
+ * an integrity pins one artifact — but carried whenever the manifest
975
+ * carries it, because fidelity outranks plausibility in a field model.
976
+ */
977
+ readonly integrity: Schema.Option<Schema.brand<Schema.String, "IntegrityHash">>;
978
+ }>, {}>;
979
+ /**
980
+ * A structured `packageManager` value whose version position is a semver
981
+ * **range**, carried verbatim: `name`, `range` and an optional `integrity`
982
+ * hash.
983
+ *
984
+ * @remarks
985
+ * The range-tolerant sibling of {@link PackageManager}. The strict class
986
+ * models the corepack pin — an exact version, which is all corepack itself
987
+ * accepts — and stays strict; this class models the field as pnpm reads it,
988
+ * where a range such as `^11.20.0` is a supported spelling that pnpm resolves
989
+ * to a concrete version. An exact version is a valid range, so every string
990
+ * the strict codec accepts decodes here too; {@link PackageManagerRange.isExact}
991
+ * is how a caller tracks which form the manifest actually carried.
992
+ *
993
+ * The `range` field is the manifest's text **verbatim** — validated to parse
994
+ * as a semver range but never normalized, so encoding is byte-identical to
995
+ * the accepted input and reading a manifest never rewrites the field.
996
+ * Interpretation is a derived getter, following `Repository`'s
997
+ * carry-verbatim posture.
998
+ *
999
+ * The first `+` after the `@` begins the integrity component, exactly as in
1000
+ * the strict grammar — the version position of this field never carries
1001
+ * semver build metadata.
1002
+ *
1003
+ * @example
1004
+ * ```ts
1005
+ * import { PackageManagerRange } from "@effected/package-json";
1006
+ * import { Effect, Schema } from "effect";
1007
+ *
1008
+ * const program = Effect.gen(function* () {
1009
+ * const pm = yield* Schema.decodeUnknownEffect(PackageManagerRange.FromString)("pnpm@^11.20.0");
1010
+ * console.log(pm.name, pm.range, pm.isExact); // "pnpm" "^11.20.0" false
1011
+ * });
1012
+ * ```
1013
+ *
1014
+ * @public
1015
+ */
1016
+ declare class PackageManagerRange extends PackageManagerRange_base {
1017
+ /**
1018
+ * Schema transformation between the `"name@range[+integrity]"` string and a
1019
+ * {@link PackageManagerRange}.
1020
+ *
1021
+ * @remarks
1022
+ * Decoding splits on the first `@`, then on the first `+` — which always
1023
+ * begins the integrity, never semver build metadata — and validates each
1024
+ * component: the name against the lowercase grammar, the range through
1025
+ * `@effected/semver`'s `Range.parseResult`, the integrity through
1026
+ * `CorepackIntegrityHash`. Every failure is a typed decode failure naming
1027
+ * the component that failed. Encoding reconstructs the string from the
1028
+ * verbatim parts, so it is byte-identical to any input this codec accepts.
1029
+ */
1030
+ static readonly FromString: Schema.Codec<PackageManagerRange, string>;
1031
+ /**
1032
+ * Whether the range is an exact, pinnable version (`11.2.0`) rather than a
1033
+ * genuine range (`^11.20.0`) — decided by `SemVer.isPinnable` over the
1034
+ * verbatim text, so `=11.2.0` and other range spellings of a single
1035
+ * version report `false`. This is the exactness a consumer tracks when it
1036
+ * must re-emit the same spelling it read.
1037
+ */
1038
+ get isExact(): boolean;
1039
+ /** Whether an integrity hash is present. */
1040
+ get hasIntegrity(): boolean;
1041
+ }
1042
+ //#endregion
1043
+ //#region src/PackageManifest.d.ts
1044
+ declare const PackageManifest_base: Schema.Class<PackageManifest, Schema.Struct<{
1045
+ readonly description: Schema.optionalKey<Schema.String>;
1046
+ readonly private: Schema.optionalKey<Schema.Boolean>;
1047
+ readonly type: Schema.optionalKey<Schema.Literals<readonly ["module", "commonjs"]>>;
1048
+ readonly main: Schema.optionalKey<Schema.String>;
1049
+ readonly license: Schema.optionalKey<Schema.brand<Schema.String, "SpdxLicense">>;
1050
+ readonly author: Schema.optionalKey<Schema.Codec<Person, string | {
1051
+ readonly [k: string]: unknown;
1052
+ }, never, never>>;
1053
+ readonly contributors: Schema.optionalKey<Schema.$Array<Schema.Codec<Person, string | {
1054
+ readonly [k: string]: unknown;
1055
+ }, never, never>>>;
1056
+ readonly maintainers: Schema.optionalKey<Schema.$Array<Schema.Codec<Person, string | {
1057
+ readonly [k: string]: unknown;
1058
+ }, never, never>>>;
1059
+ readonly keywords: Schema.optionalKey<Schema.$Array<Schema.String>>;
1060
+ readonly repository: Schema.optionalKey<Schema.Codec<Repository, string | {
1061
+ readonly [k: string]: unknown;
1062
+ }, never, never>>;
1063
+ readonly bugs: Schema.optionalKey<Schema.Codec<Bugs, string | {
1064
+ readonly [k: string]: unknown;
1065
+ }, never, never>>;
1066
+ readonly homepage: Schema.optionalKey<Schema.String>;
1067
+ readonly dependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
1068
+ readonly devDependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
1069
+ readonly peerDependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
1070
+ readonly optionalDependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
1071
+ readonly peerDependenciesMeta: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Struct<{
1072
+ readonly optional: Schema.optionalKey<Schema.Boolean>;
1073
+ }>>>;
1074
+ readonly scripts: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
1075
+ readonly bin: Schema.optionalKey<Schema.Union<readonly [Schema.String, Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.$Record<Schema.String, Schema.String>, never, never>]>>;
1076
+ readonly engines: Schema.optionalKey<Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.$Record<Schema.String, Schema.String>, never, never>>;
1077
+ readonly exports: Schema.optionalKey<Schema.Union<readonly [Schema.String, Schema.$Record<Schema.String, Schema.Unknown>]>>;
1078
+ readonly publishConfig: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
1079
+ readonly devEngines: Schema.optionalKey<Schema.Struct<{
1080
+ readonly packageManager: Schema.optionalKey<typeof DevEngineOrArray>;
1081
+ readonly runtime: Schema.optionalKey<typeof DevEngineOrArray>;
1082
+ readonly os: Schema.optionalKey<typeof DevEngineOrArray>;
1083
+ readonly cpu: Schema.optionalKey<typeof DevEngineOrArray>;
1084
+ readonly libc: Schema.optionalKey<typeof DevEngineOrArray>;
1085
+ }>>;
1086
+ readonly rest: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
1087
+ readonly name: Schema.optionalKey<Schema.Union<readonly [Schema.brand<Schema.String, "ScopedPackageName">, Schema.brand<Schema.String, "UnscopedPackageName">]> & {
1088
+ isValid: (name: string) => boolean;
1089
+ scope: (name: string) => import("effect/Option").Option<string>;
1090
+ unscoped: (name: string) => string;
1091
+ isScoped: (name: string) => boolean;
1092
+ }>;
1093
+ readonly version: Schema.optionalKey<Schema.Codec<SemVer, string, never, never>>;
1094
+ readonly packageManager: Schema.optionalKey<Schema.Codec<PackageManagerRange, string, never, never>>;
1095
+ }>, {}>;
1096
+ /**
1097
+ * A package.json document as it exists on disk, publishable or not: every
1098
+ * field of {@link Package} with `name` and `version` optional and
1099
+ * `packageManager` accepting the range spelling.
1100
+ *
1101
+ * @remarks
1102
+ * **Lenient about absence, strict about shape.** npm requires `name` and
1103
+ * `version` only for a package that will be published; the idiomatic private
1104
+ * workspace root (`{ "private": true, "packageManager": "pnpm@11.2.0" }`)
1105
+ * carries neither, and {@link Package.decode} rightly rejects it — the strict
1106
+ * model's contract is publishability. This class decodes that shape, and any
1107
+ * other manifest, so long as every field that IS present satisfies its typed
1108
+ * codec: a present `version` must still be strict semver (`"1.0"` fails
1109
+ * typed), a present `name` must still satisfy the npm grammar, a present
1110
+ * `packageManager` must still parse — though here the version position may be
1111
+ * a semver range (`pnpm@^11.20.0`), decoded as {@link PackageManagerRange}.
1112
+ * For total tolerance of malformed fields, use the decode-free
1113
+ * {@link PackageJsonFormat} text path (or `@effected/npm`'s shape-blind
1114
+ * `Manifest`) — silently carrying a value the type claims to have validated
1115
+ * would be a lie, and silently dropping it would break round-trip fidelity.
1116
+ *
1117
+ * The model is deliberately lean — fields, the `rest` catch-all wire codec,
1118
+ * {@link PackageManifest.decode} and {@link PackageManifest.toJsonString} —
1119
+ * because the write half of a manifest-editing tool is the surgical
1120
+ * {@link PackageJsonFormat.modifyToString} / `PackageJsonFile.modify` path,
1121
+ * which never goes through a model at all. Mutation statics live on the
1122
+ * strict {@link Package}.
1123
+ *
1124
+ * @example
1125
+ * ```ts
1126
+ * import { PackageManifest } from "@effected/package-json";
1127
+ * import { Effect, Option } from "effect";
1128
+ *
1129
+ * const program = Effect.gen(function* () {
1130
+ * const root = yield* PackageManifest.decode({ private: true, packageManager: "pnpm@^11.20.0" });
1131
+ * console.log(root.isPrivate, root.packageManager?.isExact); // true false
1132
+ * });
1133
+ * ```
1134
+ *
1135
+ * @public
1136
+ */
1137
+ declare class PackageManifest extends PackageManifest_base {
1138
+ /**
1139
+ * The wire codec: an open JSON object ↔ a {@link PackageManifest} instance,
1140
+ * partitioning unknown keys into `rest` and flattening them back on encode —
1141
+ * the same transform {@link Package.schema} uses, over this class's fields.
1142
+ */
1143
+ static readonly schema: Schema.Codec<PackageManifest, {
1144
+ readonly [k: string]: unknown;
1145
+ }>;
1146
+ /**
1147
+ * Decode an unknown JSON value into a {@link PackageManifest}, normalizing
1148
+ * any `SchemaError` to a typed {@link PackageDecodeError} at the boundary.
1149
+ *
1150
+ * @param input - the parsed package.json JSON value (e.g. from `JSON.parse`)
1151
+ * @returns an Effect resolving to the decoded `PackageManifest`
1152
+ * @throws (typed) `PackageDecodeError` when a present field does not satisfy
1153
+ * its codec
1154
+ */
1155
+ static readonly decode: (input: unknown) => Effect.Effect<PackageManifest, PackageDecodeError, never>;
1156
+ /** Whether the manifest is marked private. */
1157
+ get isPrivate(): boolean;
1158
+ /**
1159
+ * Serialize to a formatted package.json string: encode through the wire
1160
+ * codec (flattening `rest`), then apply the canonical key order, dependency
1161
+ * sorting and empty-map stripping unless the options opt out. Pure, and
1162
+ * shared with `Package.toJsonString` down to the same internal renderer.
1163
+ * Absent `name` / `version` keys stay absent — nothing is invented.
1164
+ */
1165
+ toJsonString(options?: PackageFormatOptions): string;
1166
+ }
1167
+ //#endregion
1168
+ //#region src/PackageJsonFile.d.ts
1169
+ declare const PackageJsonReadError_base: Schema.Class<PackageJsonReadError, Schema.TaggedStruct<"PackageJsonReadError", {
1170
+ /** The path that could not be read. */
1171
+ readonly path: Schema.String;
1172
+ /** The underlying failure, preserved structurally. */
1173
+ readonly cause: Schema.Defect;
1174
+ }>, import("effect/Cause").YieldableError>;
1175
+ /**
1176
+ * Indicates that a package.json file could not be read from the filesystem
1177
+ * (a filesystem error other than not-found).
1178
+ *
1179
+ * @public
1180
+ */
1181
+ declare class PackageJsonReadError extends PackageJsonReadError_base {
1182
+ get message(): string;
1183
+ }
1184
+ declare const PackageJsonNotFoundError_base: Schema.Class<PackageJsonNotFoundError, Schema.TaggedStruct<"PackageJsonNotFoundError", {
1185
+ /** The path where package.json was expected. */
1186
+ readonly path: Schema.String;
1187
+ }>, import("effect/Cause").YieldableError>;
1188
+ /**
1189
+ * Indicates that no package.json file exists at the expected path. Carries its
1190
+ * own tag for `catchTag` routing.
1191
+ *
1192
+ * @public
1193
+ */
1194
+ declare class PackageJsonNotFoundError extends PackageJsonNotFoundError_base {
1195
+ get message(): string;
1196
+ }
1197
+ declare const PackageJsonParseError_base: Schema.Class<PackageJsonParseError, Schema.TaggedStruct<"PackageJsonParseError", {
1198
+ /** The path whose contents failed to parse as JSON. */
1199
+ readonly path: Schema.String;
1200
+ /** The underlying `SyntaxError`, preserved structurally. */
1201
+ readonly cause: Schema.Defect;
1202
+ }>, import("effect/Cause").YieldableError>;
1203
+ /**
1204
+ * Indicates that a package.json file's contents are not valid JSON.
1205
+ *
1206
+ * @public
1207
+ */
1208
+ declare class PackageJsonParseError extends PackageJsonParseError_base {
1209
+ get message(): string;
1210
+ }
1211
+ declare const PackageJsonWriteError_base: Schema.Class<PackageJsonWriteError, Schema.TaggedStruct<"PackageJsonWriteError", {
1212
+ /** The path that could not be written. */
1213
+ readonly path: Schema.String;
1214
+ /** The underlying filesystem failure, preserved structurally. Narrowed to the write failure only. */
1215
+ readonly cause: Schema.Defect;
1216
+ }>, import("effect/Cause").YieldableError>;
1217
+ /**
1218
+ * Indicates that a package.json file could not be written to the filesystem.
1219
+ * Narrowed to the filesystem-write failure only — never a resolution or encode
1220
+ * error.
1221
+ *
1222
+ * @public
1223
+ */
1224
+ declare class PackageJsonWriteError extends PackageJsonWriteError_base {
1225
+ get message(): string;
1226
+ }
1227
+ /**
1228
+ * One surgical field edit for {@link PackageJsonFile}'s `modify`: set `value`
1229
+ * at `path`, or delete the key there when `value` is `undefined` (the
1230
+ * `@effected/jsonc` / `@effected/yaml` modify convention — deletion is spelled
1231
+ * with an explicit `value: undefined`, so it is always deliberate).
1232
+ *
1233
+ * @public
1234
+ */
1235
+ interface PackageFieldEdit {
1236
+ /** The field path, e.g. `["packageManager"]` or `["devEngines", "runtime", "version"]`. */
1237
+ readonly path: JsoncPath$1;
1238
+ /** The plain JSON value to write, or `undefined` to delete the target key. */
1239
+ readonly value: unknown;
1240
+ }
1241
+ /**
1242
+ * The shape of the {@link PackageJsonFile} service — the value produced by
1243
+ * {@link PackageJsonFile.make} and carried by its layer.
1244
+ *
1245
+ * @public
1246
+ */
1247
+ interface PackageJsonFileShape {
1248
+ /**
1249
+ * Read and decode a package.json file. Fails with `PackageJsonNotFoundError`
1250
+ * (ENOENT), `PackageJsonReadError` (other fs errors), `PackageJsonParseError`
1251
+ * (invalid JSON) or `PackageDecodeError` (schema decode).
1252
+ */
1253
+ readonly read: (path: string) => Effect.Effect<Package, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageDecodeError>;
1254
+ /**
1255
+ * Serialize and write a package.json file. Fails with
1256
+ * `PackageJsonWriteError`. With `indent: "preserve"` and no explicit
1257
+ * `sourceText`, the existing file at `path` (when readable) supplies the
1258
+ * source text whose indentation is preserved.
1259
+ */
1260
+ readonly write: (path: string, pkg: Package, options?: PackageFormatOptions) => Effect.Effect<void, PackageJsonWriteError>;
1261
+ /**
1262
+ * Read and decode a package.json file through the presence-lenient
1263
+ * {@link PackageManifest} — the read that accepts the private
1264
+ * workspace-root shape (`{ "private": true, "packageManager": ... }`)
1265
+ * `read` rejects. Same error channel as `read`; a present field that does
1266
+ * not satisfy its codec still fails as `PackageDecodeError`.
1267
+ */
1268
+ readonly readManifest: (path: string) => Effect.Effect<PackageManifest, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageDecodeError>;
1269
+ /**
1270
+ * Serialize and write a {@link PackageManifest}. Fails with
1271
+ * `PackageJsonWriteError`. Shares `write`'s `indent: "preserve"` behavior:
1272
+ * with no explicit `sourceText`, the existing file at `path` (when
1273
+ * readable) supplies the source text whose indentation is preserved.
1274
+ */
1275
+ readonly writeManifest: (path: string, manifest: PackageManifest, options?: PackageFormatOptions) => Effect.Effect<void, PackageJsonWriteError>;
1276
+ /**
1277
+ * Apply surgical field edits to a package.json file **without decoding
1278
+ * it**: one read, each {@link PackageFieldEdit} applied in order through
1279
+ * `PackageJsonFormat.modifyToString`, one write — skipped when the result
1280
+ * is byte-identical to what was read. Every byte outside the edited spans
1281
+ * is preserved (key order, indentation, line endings, trailing newline),
1282
+ * which is what keeps a one-field change reviewable in someone else's
1283
+ * repository. Succeeds with the file's final text.
1284
+ *
1285
+ * Invalid JSON at `path` fails as `PackageJsonParseError` — the same tag
1286
+ * `read` uses for it — and an unnavigable edit path as
1287
+ * `PackageJsonModifyError`.
1288
+ */
1289
+ readonly modify: (path: string, edits: ReadonlyArray<PackageFieldEdit>) => Effect.Effect<string, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageJsonModifyError | PackageJsonWriteError>;
1290
+ }
1291
+ declare const PackageJsonFile_base: Context.ServiceClass<PackageJsonFile, "@effected/package-json/PackageJsonFile", PackageJsonFileShape>;
1292
+ /**
1293
+ * Reads and writes package.json over core `FileSystem` / `Path`. The layer
1294
+ * requires those services; provide `@effect/platform-node`'s `NodeFileSystem` /
1295
+ * `NodePath` (or a bun equivalent) at the application boundary.
1296
+ *
1297
+ * @example
1298
+ * ```ts
1299
+ * import { PackageJsonFile } from "@effected/package-json";
1300
+ * import { NodeFileSystem, NodePath } from "@effect/platform-node";
1301
+ * import { Effect } from "effect";
1302
+ *
1303
+ * const program = Effect.gen(function* () {
1304
+ * const files = yield* PackageJsonFile;
1305
+ * const pkg = yield* files.read("./package.json");
1306
+ * console.log(pkg.name);
1307
+ * }).pipe(Effect.provide(PackageJsonFile.layer), Effect.provide(NodeFileSystem.layer), Effect.provide(NodePath.layer));
1308
+ * ```
1309
+ *
1310
+ * @public
1311
+ */
1312
+ declare class PackageJsonFile extends PackageJsonFile_base {
1313
+ /** Build the service implementation from `FileSystem` / `Path` in context; use {@link PackageJsonFile.layer} to provide it. */
1314
+ static readonly make: Effect.Effect<PackageJsonFileShape, never, FileSystem.FileSystem | Path.Path>;
1315
+ /**
1316
+ * The live layer. Requires core `FileSystem` / `Path`, provided by the
1317
+ * consumer's platform implementation at the edge.
1318
+ */
1319
+ static readonly layer: Layer.Layer<PackageJsonFile, never, FileSystem.FileSystem | Path.Path>;
999
1320
  }
1000
1321
  //#endregion
1001
1322
  //#region src/PackageValidator.d.ts
@@ -1097,5 +1418,5 @@ declare class PackageValidator extends PackageValidator_base {
1097
1418
  }): Layer.Layer<PackageValidator>;
1098
1419
  }
1099
1420
  //#endregion
1100
- export { BinField, Bugs, Dependency, type DependencyKind, DependencyMapField, type DependencyProtocol, DependencySpecifier, type DependencySpecifierBrand, DevEngine, DevEngineOrArray, type DevEngines, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, Package, PackageDecodeError, type PackageFormatOptions, type PackageFormatTextOptions, type PackageIndent, PackageJsonFile, type PackageJsonFileShape, PackageJsonFormat, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonSyntaxError, PackageJsonWriteError, PackageManager, PackageName, type PackagePatch, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, Repository, RepositoryField, type RuleFailure, ScopedPackageName, SpdxLicense, StringMapField, type UnresolvedDependency, UnscopedPackageName, type ValidationRule, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
1421
+ export { BinField, Bugs, Dependency, type DependencyKind, DependencyMapField, type DependencyProtocol, DependencySpecifier, type DependencySpecifierBrand, DevEngine, DevEngineOrArray, type DevEngines, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, JsoncEdit, type JsoncPath, Package, PackageDecodeError, type PackageFieldEdit, type PackageFormatOptions, type PackageFormatTextOptions, type PackageIndent, PackageJsonFile, type PackageJsonFileShape, PackageJsonFormat, PackageJsonModifyError, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonSyntaxError, PackageJsonWriteError, PackageManager, PackageManagerRange, PackageManifest, PackageName, type PackagePatch, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, Repository, RepositoryField, type RuleFailure, ScopedPackageName, SpdxLicense, StringMapField, type UnresolvedDependency, UnscopedPackageName, type ValidationRule, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
1101
1422
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -6,9 +6,12 @@ import { InvalidPackageNameError, PackageName, ScopedPackageName, UnscopedPackag
6
6
  import { Person } from "./Person.js";
7
7
  import { Bugs, Repository } from "./Repository.js";
8
8
  import { BinField, DependencyMapField, ExportsField, Package, PackageDecodeError, PeerDependenciesMetaField, PublishConfigField, RepositoryField, StringMapField } from "./Package.js";
9
+ import { PackageJsonFormat, PackageJsonModifyError, PackageJsonSyntaxError } from "./PackageJsonFormat.js";
10
+ import { PackageManagerRange } from "./PackageManagerRange.js";
11
+ import { PackageManifest } from "./PackageManifest.js";
9
12
  import { PackageJsonFile, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonWriteError } from "./PackageJsonFile.js";
10
- import { PackageJsonFormat, PackageJsonSyntaxError } from "./PackageJsonFormat.js";
11
13
  import { PackageValidationError, PackageValidator, defaultRules, noLocalDepsRule, noUnresolvedDepsRule } from "./PackageValidator.js";
14
+ import { JsoncEdit } from "@effected/jsonc";
12
15
  import { DependencySpecifier, InvalidDependencySpecifierError, isValidDependencySpecifier } from "@effected/npm";
13
16
 
14
- export { BinField, Bugs, Dependency, DependencyMapField, DependencySpecifier, DevEngine, DevEngineOrArray, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, Package, PackageDecodeError, PackageJsonFile, PackageJsonFormat, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonSyntaxError, PackageJsonWriteError, PackageManager, PackageName, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, Repository, RepositoryField, ScopedPackageName, SpdxLicense, StringMapField, UnscopedPackageName, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
17
+ export { BinField, Bugs, Dependency, DependencyMapField, DependencySpecifier, DevEngine, DevEngineOrArray, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, JsoncEdit, Package, PackageDecodeError, PackageJsonFile, PackageJsonFormat, PackageJsonModifyError, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonSyntaxError, PackageJsonWriteError, PackageManager, PackageManagerRange, PackageManifest, PackageName, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, Repository, RepositoryField, ScopedPackageName, SpdxLicense, StringMapField, UnscopedPackageName, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
@@ -210,6 +210,18 @@ const resolveIndent = (indent, sourceText) => {
210
210
  return indent ?? DEFAULT_INDENT;
211
211
  };
212
212
  /**
213
+ * Resolve the public `PackageFormatOptions` bag (mirrored structurally here —
214
+ * this module cannot import `Package.ts` without closing a cycle) to the
215
+ * concrete {@link renderJson} options. Shared by `Package.toJsonString` and
216
+ * `PackageManifest.toJsonString` so the two serializers cannot drift.
217
+ */
218
+ const resolveFormatOptions = (options) => ({
219
+ indent: resolveIndent(options?.indent, options?.sourceText),
220
+ sort: options?.sort ?? true,
221
+ stripEmpty: options?.stripEmpty ?? true,
222
+ newline: options?.newline ?? true
223
+ });
224
+ /**
213
225
  * Render an already-encoded package.json record to a JSON string, applying the
214
226
  * empty-map strip, canonical key ordering and a trailing newline unless the
215
227
  * corresponding options opt out.
@@ -222,4 +234,4 @@ const renderJson = (raw, options) => {
222
234
  };
223
235
 
224
236
  //#endregion
225
- export { detectIndent, renderJson, resolveIndent, sortKeys, stripEmptyDependencyMaps };
237
+ export { detectIndent, renderJson, resolveFormatOptions, resolveIndent, sortKeys, stripEmptyDependencyMaps };
@@ -0,0 +1,35 @@
1
+ import { Schema, SchemaTransformation } from "effect";
2
+
3
+ //#region src/internal/wire.ts
4
+ const RawJson = Schema.Record(Schema.String, Schema.Unknown);
5
+ /**
6
+ * Build the open-JSON ↔ class wire codec for a `Schema.Class` carrying a
7
+ * `rest` catch-all field. Generic over the class so `Package`, its
8
+ * `.extend()`ed subclasses and `PackageManifest` all share the one
9
+ * implementation and cannot drift.
10
+ */
11
+ const makeWire = (Class) => {
12
+ const knownKeys = new Set(Object.keys(Class.fields).filter((k) => k !== "rest"));
13
+ return RawJson.pipe(Schema.decodeTo(Class, SchemaTransformation.transform({
14
+ decode: (raw) => {
15
+ const known = {};
16
+ const rest = Object.create(null);
17
+ for (const [key, value] of Object.entries(raw)) if (knownKeys.has(key)) known[key] = value;
18
+ else rest[key] = value;
19
+ return {
20
+ ...known,
21
+ rest
22
+ };
23
+ },
24
+ encode: (encoded) => {
25
+ const { rest, ...known } = encoded;
26
+ return {
27
+ ...rest ?? {},
28
+ ...known
29
+ };
30
+ }
31
+ })));
32
+ };
33
+
34
+ //#endregion
35
+ export { makeWire };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/package-json",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "package.json parsing, editing, validation and file IO as Effect schemas.",
6
6
  "keywords": [
@@ -38,12 +38,13 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "dependencies": {
41
- "@effected/npm": "^0.8.2",
42
- "@effected/semver": "^0.3.2",
43
- "@effected/spdx": "^0.1.2"
41
+ "@effected/jsonc": "^0.6.0",
42
+ "@effected/npm": "^0.9.0",
43
+ "@effected/semver": "^0.4.0",
44
+ "@effected/spdx": "^0.2.0"
44
45
  },
45
46
  "peerDependencies": {
46
- "effect": "4.0.0-beta.101"
47
+ "effect": "4.0.0-beta.107"
47
48
  },
48
49
  "engines": {
49
50
  "node": ">=24.11.0"