@effected/package-json 0.3.0 → 0.4.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.
@@ -0,0 +1,138 @@
1
+ import { renderJson, resolveIndent, sortKeys } from "./internal/format.js";
2
+ import { Result, Schema } from "effect";
3
+
4
+ //#region src/PackageJsonFormat.ts
5
+ /**
6
+ * Indicates that a text input could not be treated as a package.json document:
7
+ * either it is not valid JSON (`"invalid-json"`, carrying the underlying
8
+ * `SyntaxError` on `cause`) or it parsed to something other than a JSON object
9
+ * (`"not-an-object"` — an array, a scalar or `null`).
10
+ *
11
+ * Raised by {@link PackageJsonFormat.formatToString}. This is a *syntactic*
12
+ * failure only; it says nothing about whether the document is a valid package
13
+ * manifest, which the decode-free path deliberately does not check.
14
+ *
15
+ * @public
16
+ */
17
+ var PackageJsonSyntaxError = class extends Schema.TaggedErrorClass()("PackageJsonSyntaxError", {
18
+ /** Which syntactic precondition failed. */
19
+ reason: Schema.Literals(["invalid-json", "not-an-object"]),
20
+ /** The underlying `SyntaxError` for `"invalid-json"`, preserved structurally. */
21
+ cause: Schema.optionalKey(Schema.Defect())
22
+ }) {
23
+ get message() {
24
+ return this.reason === "invalid-json" ? "package.json text is not valid JSON" : "package.json text is not a JSON object";
25
+ }
26
+ };
27
+ const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28
+ /**
29
+ * Decode-free canonical sort and format statics. Not instantiable.
30
+ *
31
+ * @remarks
32
+ * The guarantee both statics make is that they are **source-preserving**:
33
+ * neither decodes into a `Package`, so neither can normalize a field encoding.
34
+ * String-form `author` shorthand, unknown fields, unusual value shapes and
35
+ * empty maps all survive untouched, because they are never looked at. Key
36
+ * order, indentation and the trailing newline are the only things that change.
37
+ *
38
+ * That is what makes this usable as a lint-hook handler where the strict path
39
+ * is not: any syntactically valid JSON object formats, including the
40
+ * version-less workspace roots and `{"private": true}` manifests that
41
+ * `Package.decode` rejects. Reach for `Package.decode` +
42
+ * `Package.toJsonString` instead when the job needs the validated model and an
43
+ * invalid manifest should fail loudly.
44
+ *
45
+ * @public
46
+ */
47
+ var PackageJsonFormat = class {
48
+ constructor() {}
49
+ /**
50
+ * Order a package.json object's keys canonically **without decoding it into
51
+ * a `Package`**: known top-level keys in `sort-package-json`'s order, then
52
+ * unknown public keys alphabetically, then `_`-prefixed keys, with the
53
+ * dependency maps and `scripts` / `engines` / `bin` alphabetized.
54
+ *
55
+ * Value in, value out — for hosts that already hold parsed JSON and never
56
+ * want a string. {@link PackageJsonFormat.formatToString} is the same
57
+ * ordering for hosts holding file text. Pure and total.
58
+ *
59
+ * Returns a new object; nested values are shared by reference rather than
60
+ * cloned, except the maps whose own keys are reordered. A value that is not
61
+ * a JSON object (an array, a scalar, `null`) is returned unchanged rather
62
+ * than mangled, so a mistyped `Json` union cannot silently lose data.
63
+ *
64
+ * Reordering keys is the whole of it — **no key is ever added or removed**,
65
+ * which is what lets the return type be the input type `T` and makes this a
66
+ * drop-in. Use {@link PackageJsonFormat.formatToString} with `stripEmpty`
67
+ * when removing empty maps is wanted; it returns a string and so carries no
68
+ * such obligation.
69
+ *
70
+ * @param value - the parsed package.json object
71
+ * @returns a new object with canonically ordered keys
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { PackageJsonFormat } from "@effected/package-json";
76
+ *
77
+ * const sorted = PackageJsonFormat.sortValue({ version: "1.0.0", name: "p" });
78
+ * // => { name: "p", version: "1.0.0" }
79
+ * ```
80
+ */
81
+ static sortValue(value) {
82
+ if (!isJsonObject(value)) return value;
83
+ return sortKeys(value);
84
+ }
85
+ /**
86
+ * Sort and format package.json text **without decoding it into a
87
+ * `Package`**. Text in, text out — for hosts that hold file contents and
88
+ * cannot afford a decode. {@link PackageJsonFormat.sortValue} is the same
89
+ * ordering for hosts that already hold parsed JSON.
90
+ *
91
+ * Any syntactically valid JSON object formats, whatever it contains: a
92
+ * version-less root, `{"private": true}`, a malformed `packageManager`
93
+ * integrity. Nothing is decoded, so nothing is normalized — string-form
94
+ * `author` shorthand, unknown fields, unusual value shapes and empty maps
95
+ * all survive byte-for-byte. Only key order, indentation and the trailing
96
+ * newline change.
97
+ *
98
+ * Pure and synchronous: it returns a `Result` rather than an `Effect`, so
99
+ * synchronous hosts can call it directly. Lift it with `Effect.fromResult`.
100
+ *
101
+ * @param source - the package.json file contents
102
+ * @param options - formatting options; see {@link PackageFormatTextOptions}
103
+ * @returns the formatted text, or a {@link PackageJsonSyntaxError}
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { PackageJsonFormat } from "@effected/package-json";
108
+ * import { Effect, Result } from "effect";
109
+ *
110
+ * const formatted = PackageJsonFormat.formatToString('{"private": true}');
111
+ * if (Result.isSuccess(formatted)) console.log(formatted.success);
112
+ *
113
+ * // In an Effect program:
114
+ * const program = Effect.fromResult(PackageJsonFormat.formatToString('{"private": true}'));
115
+ * ```
116
+ */
117
+ static formatToString(source, options) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(source);
121
+ } catch (cause) {
122
+ return Result.fail(new PackageJsonSyntaxError({
123
+ reason: "invalid-json",
124
+ cause
125
+ }));
126
+ }
127
+ if (!isJsonObject(parsed)) return Result.fail(new PackageJsonSyntaxError({ reason: "not-an-object" }));
128
+ return Result.succeed(renderJson(parsed, {
129
+ indent: resolveIndent(options?.indent ?? "preserve", source),
130
+ sort: options?.sort ?? true,
131
+ stripEmpty: options?.stripEmpty ?? false,
132
+ newline: options?.newline ?? true
133
+ }));
134
+ }
135
+ };
136
+
137
+ //#endregion
138
+ export { PackageJsonFormat, PackageJsonSyntaxError };
package/Person.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Schema, SchemaTransformation } from "effect";
1
+ import { Effect, Option, Schema, SchemaTransformation } from "effect";
2
2
 
3
3
  //#region src/Person.ts
4
4
  const parsePersonString = (input) => {
@@ -14,8 +14,56 @@ const parsePersonString = (input) => {
14
14
  ...urlMatch !== null ? { url: urlMatch[1] } : {}
15
15
  });
16
16
  };
17
+ const serializePerson = (person) => {
18
+ let result = person.name;
19
+ if (person.email !== void 0) result += ` <${person.email}>`;
20
+ if (person.url !== void 0) result += ` (${person.url})`;
21
+ return result;
22
+ };
23
+ const wireForms = /* @__PURE__ */ new WeakMap();
24
+ const rememberWire = (person, wire) => {
25
+ wireForms.set(person, wire);
26
+ return person;
27
+ };
28
+ const KNOWN_KEYS = /* @__PURE__ */ new Set([
29
+ "name",
30
+ "email",
31
+ "url"
32
+ ]);
33
+ const sameRest = (a, b) => JSON.stringify(a ?? {}) === JSON.stringify(b ?? {});
34
+ const isFaithful = (wire, person) => {
35
+ if (typeof wire === "string") {
36
+ const parsed = parsePersonString(wire);
37
+ return parsed.name === person.name && parsed.email === person.email && parsed.url === person.url;
38
+ }
39
+ const rest = restOf(wire);
40
+ return wire.name === person.name && wire.email === person.email && wire.url === person.url && sameRest(rest, person.rest);
41
+ };
42
+ const PersonFields = Schema.Struct({
43
+ name: Schema.String,
44
+ email: Schema.optionalKey(Schema.String),
45
+ url: Schema.optionalKey(Schema.String)
46
+ });
47
+ const decodePersonFields = Schema.decodeUnknownEffect(PersonFields);
48
+ const restOf = (raw) => {
49
+ const rest = {};
50
+ for (const [key, value] of Object.entries(raw)) if (!KNOWN_KEYS.has(key)) rest[key] = value;
51
+ return rest;
52
+ };
53
+ const encodePersonObject = (person) => {
54
+ const wire = wireForms.get(person);
55
+ if (wire !== void 0 && typeof wire !== "string" && isFaithful(wire, person)) return wire;
56
+ const known = { name: person.name };
57
+ if (person.email !== void 0) known.email = person.email;
58
+ if (person.url !== void 0) known.url = person.url;
59
+ return {
60
+ ...person.rest ?? {},
61
+ ...known
62
+ };
63
+ };
17
64
  /**
18
- * A structured person object with `name` and optional `email` / `url`.
65
+ * A structured person object with `name`, optional `email` / `url`, and a
66
+ * `rest` catch-all preserving any additional keys across a read/write cycle.
19
67
  *
20
68
  * @public
21
69
  */
@@ -25,26 +73,74 @@ var Person = class Person extends Schema.Class("Person")({
25
73
  /** The optional email address. */
26
74
  email: Schema.optionalKey(Schema.String),
27
75
  /** The optional homepage URL. */
28
- url: Schema.optionalKey(Schema.String)
76
+ url: Schema.optionalKey(Schema.String),
77
+ /** Any additional keys, preserved verbatim and flattened back on encode. */
78
+ rest: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown))
29
79
  }) {
80
+ /**
81
+ * The object wire codec: an open JSON object ↔ a {@link Person}, partitioning
82
+ * unknown keys into `rest` and flattening them back on encode so the on-disk
83
+ * shape never carries a literal `rest` key.
84
+ */
85
+ static schema = Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.decodeTo(Schema.instanceOf(Person), SchemaTransformation.transformOrFail({
86
+ decode: (raw) => decodePersonFields(raw).pipe(Effect.mapError((error) => error.issue), Effect.map((fields) => {
87
+ const rest = restOf(raw);
88
+ return rememberWire(Person.make({
89
+ ...fields,
90
+ ...Object.keys(rest).length > 0 ? { rest } : {}
91
+ }), raw);
92
+ })),
93
+ encode: (person) => Effect.succeed(encodePersonObject(person))
94
+ })));
30
95
  /**
31
96
  * Schema transformation between the `"Name <email> (url)"` shorthand string
32
- * and a {@link Person}.
97
+ * and a {@link Person}. Decoding remembers the input text so that encoding
98
+ * reproduces it verbatim; see {@link Person.wireStringOf}.
33
99
  */
34
100
  static FromString = Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(Person), SchemaTransformation.transform({
35
- decode: (input) => parsePersonString(input),
101
+ decode: (input) => rememberWire(parsePersonString(input), input),
36
102
  encode: (person) => {
37
- let result = person.name;
38
- if (person.email !== void 0) result += ` <${person.email}>`;
39
- if (person.url !== void 0) result += ` (${person.url})`;
40
- return result;
103
+ const wire = wireForms.get(person);
104
+ return typeof wire === "string" && isFaithful(wire, person) ? wire : serializePerson(person);
41
105
  }
42
106
  })));
43
107
  /**
44
108
  * The `author` / `contributors` value: either the shorthand string or the
45
109
  * structured object, always decoded to a {@link Person}.
110
+ *
111
+ * The wire form is preserved across a round trip — a person read from the
112
+ * shorthand string encodes back to that string, byte for byte, and one read
113
+ * from an object encodes back to an object with its unknown keys intact.
114
+ * Formatting a manifest therefore never rewrites one legal encoding into the
115
+ * other.
116
+ *
117
+ * Provenance belongs to the instance, so a person that is *rebuilt* (rather
118
+ * than carried through unchanged) has none and encodes in the canonical
119
+ * object form. Editing an unrelated field of the surrounding `Package`
120
+ * carries the same person instance through and preserves its encoding.
121
+ */
122
+ static FromValue = Schema.Union([Person.schema, Schema.String]).pipe(Schema.decodeTo(Schema.instanceOf(Person), SchemaTransformation.transform({
123
+ decode: (input) => typeof input === "string" ? rememberWire(parsePersonString(input), input) : input,
124
+ encode: (person) => {
125
+ const wire = wireForms.get(person);
126
+ return typeof wire === "string" && isFaithful(wire, person) ? wire : person;
127
+ }
128
+ })));
129
+ /**
130
+ * The shorthand text this person was decoded from, when it was decoded from
131
+ * the string form and still matches its fields; `None` for a person built
132
+ * from an object or by hand.
133
+ *
134
+ * Exposed so callers can tell which encoding a manifest used without
135
+ * re-reading the file.
136
+ *
137
+ * @param person - the person to inspect
138
+ * @returns the original shorthand text, or `None`
46
139
  */
47
- static FromValue = Schema.Union([Person, Person.FromString]);
140
+ static wireStringOf(person) {
141
+ const wire = wireForms.get(person);
142
+ return typeof wire === "string" && isFaithful(wire, person) ? Option.some(wire) : Option.none();
143
+ }
48
144
  };
49
145
 
50
146
  //#endregion
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@effected%2Fpackage-json?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/package-json)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
- [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
6
+ [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
7
7
 
8
8
  package.json parsing, editing, validation and file IO as Effect schemas. `Package` is a `Schema.Class` with the manifest's known fields typed — `name` is a branded npm name, `version` is a real `SemVer`, `packageManager` decodes into `{ name, version, integrity }` — and a `rest` catch-all that carries every unknown top-level key through a read, edit and write cycle without losing it. Editing is immutable and dual-signature, validation is a rule set you can replace, and `catalog:` / `workspace:` specifiers expand through the `@effected/npm` resolver contracts as an explicit step you opt into.
9
9
 
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CatalogResolver, DependencyKind, DependencyProtocol, DependencyProtocol as DependencyProtocol$1, DependencySpecifier, DependencySpecifierBrand, InvalidDependencySpecifierError, WorkspaceResolver, isValidDependencySpecifier } from "@effected/npm";
2
2
  import { InvalidVersionError, Range, SemVer } from "@effected/semver";
3
- import { Brand, Context, Effect, FileSystem, HashMap, Layer, Option, Path, Schema } from "effect";
3
+ import { Brand, Context, Effect, FileSystem, HashMap, Layer, Option, Path, Result, Schema } from "effect";
4
4
  //#region src/Dependency.d.ts
5
5
  declare const Dependency_base: Schema.Class<Dependency, Schema.Struct<{
6
6
  /** The package name. */
@@ -236,23 +236,60 @@ declare const Person_base: Schema.Class<Person, Schema.Struct<{
236
236
  readonly email: Schema.optionalKey<Schema.String>;
237
237
  /** The optional homepage URL. */
238
238
  readonly url: Schema.optionalKey<Schema.String>;
239
+ /** Any additional keys, preserved verbatim and flattened back on encode. */
240
+ readonly rest: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Unknown>>;
239
241
  }>, {}>;
240
242
  /**
241
- * A structured person object with `name` and optional `email` / `url`.
243
+ * A structured person object with `name`, optional `email` / `url`, and a
244
+ * `rest` catch-all preserving any additional keys across a read/write cycle.
242
245
  *
243
246
  * @public
244
247
  */
245
248
  declare class Person extends Person_base {
249
+ /**
250
+ * The object wire codec: an open JSON object ↔ a {@link Person}, partitioning
251
+ * unknown keys into `rest` and flattening them back on encode so the on-disk
252
+ * shape never carries a literal `rest` key.
253
+ */
254
+ static readonly schema: Schema.Codec<Person, {
255
+ readonly [k: string]: unknown;
256
+ }>;
246
257
  /**
247
258
  * Schema transformation between the `"Name <email> (url)"` shorthand string
248
- * and a {@link Person}.
259
+ * and a {@link Person}. Decoding remembers the input text so that encoding
260
+ * reproduces it verbatim; see {@link Person.wireStringOf}.
249
261
  */
250
262
  static readonly FromString: Schema.Codec<Person, string>;
251
263
  /**
252
264
  * The `author` / `contributors` value: either the shorthand string or the
253
265
  * structured object, always decoded to a {@link Person}.
266
+ *
267
+ * The wire form is preserved across a round trip — a person read from the
268
+ * shorthand string encodes back to that string, byte for byte, and one read
269
+ * from an object encodes back to an object with its unknown keys intact.
270
+ * Formatting a manifest therefore never rewrites one legal encoding into the
271
+ * other.
272
+ *
273
+ * Provenance belongs to the instance, so a person that is *rebuilt* (rather
274
+ * than carried through unchanged) has none and encodes in the canonical
275
+ * object form. Editing an unrelated field of the surrounding `Package`
276
+ * carries the same person instance through and preserves its encoding.
277
+ */
278
+ static readonly FromValue: Schema.Codec<Person, string | {
279
+ readonly [k: string]: unknown;
280
+ }>;
281
+ /**
282
+ * The shorthand text this person was decoded from, when it was decoded from
283
+ * the string form and still matches its fields; `None` for a person built
284
+ * from an object or by hand.
285
+ *
286
+ * Exposed so callers can tell which encoding a manifest used without
287
+ * re-reading the file.
288
+ *
289
+ * @param person - the person to inspect
290
+ * @returns the original shorthand text, or `None`
254
291
  */
255
- static readonly FromValue: Schema.Union<[typeof Person, Schema.Codec<Person, string>]>;
292
+ static wireStringOf(person: Person): Option.Option<string>;
256
293
  }
257
294
  //#endregion
258
295
  //#region src/Package.d.ts
@@ -379,8 +416,12 @@ declare const Package_base: Schema.Class<Package, Schema.Struct<{
379
416
  readonly type: Schema.optionalKey<Schema.Literals<readonly ["module", "commonjs"]>>;
380
417
  readonly main: Schema.optionalKey<Schema.String>;
381
418
  readonly license: Schema.optionalKey<Schema.brand<Schema.String, "SpdxLicense">>;
382
- readonly author: Schema.optionalKey<Schema.Union<[typeof Person, Schema.Codec<Person, string, never, never>]>>;
383
- readonly contributors: Schema.optionalKey<Schema.$Array<Schema.Union<[typeof Person, Schema.Codec<Person, string, never, never>]>>>;
419
+ readonly author: Schema.optionalKey<Schema.Codec<Person, string | {
420
+ readonly [k: string]: unknown;
421
+ }, never, never>>;
422
+ readonly contributors: Schema.optionalKey<Schema.$Array<Schema.Codec<Person, string | {
423
+ readonly [k: string]: unknown;
424
+ }, never, never>>>;
384
425
  readonly repository: Schema.optionalKey<Schema.Union<readonly [Schema.String, Schema.$Record<Schema.String, Schema.Unknown>]>>;
385
426
  readonly dependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
386
427
  readonly devDependencies: Schema.decodeTo<Schema.HashMap<Schema.String, Schema.String>, Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.String>, never>, never, never>;
@@ -678,6 +719,148 @@ declare class PackageJsonFile extends PackageJsonFile_base {
678
719
  static readonly layer: Layer.Layer<PackageJsonFile, never, FileSystem.FileSystem | Path.Path>;
679
720
  }
680
721
  //#endregion
722
+ //#region src/PackageJsonFormat.d.ts
723
+ declare const PackageJsonSyntaxError_base: Schema.Class<PackageJsonSyntaxError, Schema.TaggedStruct<"PackageJsonSyntaxError", {
724
+ /** Which syntactic precondition failed. */
725
+ readonly reason: Schema.Literals<readonly ["invalid-json", "not-an-object"]>;
726
+ /** The underlying `SyntaxError` for `"invalid-json"`, preserved structurally. */
727
+ readonly cause: Schema.optionalKey<Schema.Defect>;
728
+ }>, import("effect/Cause").YieldableError>;
729
+ /**
730
+ * Indicates that a text input could not be treated as a package.json document:
731
+ * either it is not valid JSON (`"invalid-json"`, carrying the underlying
732
+ * `SyntaxError` on `cause`) or it parsed to something other than a JSON object
733
+ * (`"not-an-object"` — an array, a scalar or `null`).
734
+ *
735
+ * Raised by {@link PackageJsonFormat.formatToString}. This is a *syntactic*
736
+ * failure only; it says nothing about whether the document is a valid package
737
+ * manifest, which the decode-free path deliberately does not check.
738
+ *
739
+ * @public
740
+ */
741
+ declare class PackageJsonSyntaxError extends PackageJsonSyntaxError_base {
742
+ get message(): string;
743
+ }
744
+ /**
745
+ * Options for {@link PackageJsonFormat.formatToString}.
746
+ *
747
+ * Deliberately not `PackageFormatOptions`: there is no `sourceText` member,
748
+ * because the text being formatted *is* the source text, and the defaults for
749
+ * `indent` and `stripEmpty` differ — see each member.
750
+ *
751
+ * @public
752
+ */
753
+ interface PackageFormatTextOptions {
754
+ /**
755
+ * Indentation: a spaces count, `"tab"`, or `"preserve"`. Defaults to
756
+ * `"preserve"` — unlike `Package.toJsonString`, this path always has the
757
+ * original text in hand, and reformatting a file in place should not
758
+ * silently restyle its indentation.
759
+ */
760
+ readonly indent?: number | "tab" | "preserve";
761
+ /** Order top-level keys canonically and alphabetize dependency maps (default `true`). */
762
+ readonly sort?: boolean;
763
+ /**
764
+ * Strip dependency-map keys whose value is an empty object (default
765
+ * `false`). The strict path defaults this on because the model materializes
766
+ * absent maps as empty ones; here an empty map is a key the author actually
767
+ * wrote, and removing it would be a silent edit rather than a format.
768
+ */
769
+ readonly stripEmpty?: boolean;
770
+ /** Append a trailing newline (default `true`). */
771
+ readonly newline?: boolean;
772
+ }
773
+ /**
774
+ * Decode-free canonical sort and format statics. Not instantiable.
775
+ *
776
+ * @remarks
777
+ * The guarantee both statics make is that they are **source-preserving**:
778
+ * neither decodes into a `Package`, so neither can normalize a field encoding.
779
+ * String-form `author` shorthand, unknown fields, unusual value shapes and
780
+ * empty maps all survive untouched, because they are never looked at. Key
781
+ * order, indentation and the trailing newline are the only things that change.
782
+ *
783
+ * That is what makes this usable as a lint-hook handler where the strict path
784
+ * is not: any syntactically valid JSON object formats, including the
785
+ * version-less workspace roots and `{"private": true}` manifests that
786
+ * `Package.decode` rejects. Reach for `Package.decode` +
787
+ * `Package.toJsonString` instead when the job needs the validated model and an
788
+ * invalid manifest should fail loudly.
789
+ *
790
+ * @public
791
+ */
792
+ declare class PackageJsonFormat {
793
+ private constructor();
794
+ /**
795
+ * Order a package.json object's keys canonically **without decoding it into
796
+ * a `Package`**: known top-level keys in `sort-package-json`'s order, then
797
+ * unknown public keys alphabetically, then `_`-prefixed keys, with the
798
+ * dependency maps and `scripts` / `engines` / `bin` alphabetized.
799
+ *
800
+ * Value in, value out — for hosts that already hold parsed JSON and never
801
+ * want a string. {@link PackageJsonFormat.formatToString} is the same
802
+ * ordering for hosts holding file text. Pure and total.
803
+ *
804
+ * Returns a new object; nested values are shared by reference rather than
805
+ * cloned, except the maps whose own keys are reordered. A value that is not
806
+ * a JSON object (an array, a scalar, `null`) is returned unchanged rather
807
+ * than mangled, so a mistyped `Json` union cannot silently lose data.
808
+ *
809
+ * Reordering keys is the whole of it — **no key is ever added or removed**,
810
+ * which is what lets the return type be the input type `T` and makes this a
811
+ * drop-in. Use {@link PackageJsonFormat.formatToString} with `stripEmpty`
812
+ * when removing empty maps is wanted; it returns a string and so carries no
813
+ * such obligation.
814
+ *
815
+ * @param value - the parsed package.json object
816
+ * @returns a new object with canonically ordered keys
817
+ *
818
+ * @example
819
+ * ```ts
820
+ * import { PackageJsonFormat } from "@effected/package-json";
821
+ *
822
+ * const sorted = PackageJsonFormat.sortValue({ version: "1.0.0", name: "p" });
823
+ * // => { name: "p", version: "1.0.0" }
824
+ * ```
825
+ */
826
+ static sortValue<T extends {
827
+ readonly [k: string]: unknown;
828
+ }>(value: T): T;
829
+ /**
830
+ * Sort and format package.json text **without decoding it into a
831
+ * `Package`**. Text in, text out — for hosts that hold file contents and
832
+ * cannot afford a decode. {@link PackageJsonFormat.sortValue} is the same
833
+ * ordering for hosts that already hold parsed JSON.
834
+ *
835
+ * Any syntactically valid JSON object formats, whatever it contains: a
836
+ * version-less root, `{"private": true}`, a malformed `packageManager`
837
+ * integrity. Nothing is decoded, so nothing is normalized — string-form
838
+ * `author` shorthand, unknown fields, unusual value shapes and empty maps
839
+ * all survive byte-for-byte. Only key order, indentation and the trailing
840
+ * newline change.
841
+ *
842
+ * Pure and synchronous: it returns a `Result` rather than an `Effect`, so
843
+ * synchronous hosts can call it directly. Lift it with `Effect.fromResult`.
844
+ *
845
+ * @param source - the package.json file contents
846
+ * @param options - formatting options; see {@link PackageFormatTextOptions}
847
+ * @returns the formatted text, or a {@link PackageJsonSyntaxError}
848
+ *
849
+ * @example
850
+ * ```ts
851
+ * import { PackageJsonFormat } from "@effected/package-json";
852
+ * import { Effect, Result } from "effect";
853
+ *
854
+ * const formatted = PackageJsonFormat.formatToString('{"private": true}');
855
+ * if (Result.isSuccess(formatted)) console.log(formatted.success);
856
+ *
857
+ * // In an Effect program:
858
+ * const program = Effect.fromResult(PackageJsonFormat.formatToString('{"private": true}'));
859
+ * ```
860
+ */
861
+ static formatToString(source: string, options?: PackageFormatTextOptions): Result.Result<string, PackageJsonSyntaxError>;
862
+ }
863
+ //#endregion
681
864
  //#region src/PackageValidator.d.ts
682
865
  /**
683
866
  * A single validation-rule failure.
@@ -777,5 +960,5 @@ declare class PackageValidator extends PackageValidator_base {
777
960
  }): Layer.Layer<PackageValidator>;
778
961
  }
779
962
  //#endregion
780
- export { BinField, Dependency, type DependencyKind, DependencyMapField, type DependencyProtocol, DependencySpecifier, type DependencySpecifierBrand, DevEngine, DevEngineOrArray, type DevEngines, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, Package, PackageDecodeError, type PackageFormatOptions, type PackageIndent, PackageJsonFile, type PackageJsonFileShape, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonWriteError, PackageManager, PackageName, type PackagePatch, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, RepositoryField, type RuleFailure, ScopedPackageName, SpdxLicense, StringMapField, type UnresolvedDependency, UnscopedPackageName, type ValidationRule, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
963
+ export { BinField, 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, RepositoryField, type RuleFailure, ScopedPackageName, SpdxLicense, StringMapField, type UnresolvedDependency, UnscopedPackageName, type ValidationRule, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
781
964
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -6,7 +6,8 @@ import { InvalidPackageNameError, PackageName, ScopedPackageName, UnscopedPackag
6
6
  import { Person } from "./Person.js";
7
7
  import { BinField, DependencyMapField, ExportsField, Package, PackageDecodeError, PeerDependenciesMetaField, PublishConfigField, RepositoryField, StringMapField } from "./Package.js";
8
8
  import { PackageJsonFile, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonWriteError } from "./PackageJsonFile.js";
9
+ import { PackageJsonFormat, PackageJsonSyntaxError } from "./PackageJsonFormat.js";
9
10
  import { PackageValidationError, PackageValidator, defaultRules, noLocalDepsRule, noUnresolvedDepsRule } from "./PackageValidator.js";
10
11
  import { DependencySpecifier, InvalidDependencySpecifierError, isValidDependencySpecifier } from "@effected/npm";
11
12
 
12
- export { BinField, Dependency, DependencyMapField, DependencySpecifier, DevEngine, DevEngineOrArray, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, Package, PackageDecodeError, PackageJsonFile, PackageJsonNotFoundError, PackageJsonParseError, PackageJsonReadError, PackageJsonWriteError, PackageManager, PackageName, PackageValidationError, PackageValidator, PeerDependenciesMetaField, Person, PublishConfigField, RepositoryField, ScopedPackageName, SpdxLicense, StringMapField, UnscopedPackageName, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
13
+ export { BinField, 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, RepositoryField, ScopedPackageName, SpdxLicense, StringMapField, UnscopedPackageName, defaultRules, isUnresolvedDependency, isValidDependencySpecifier, isValidSpdx, noLocalDepsRule, noUnresolvedDepsRule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/package-json",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "description": "package.json parsing, editing, validation and file IO as Effect schemas.",
6
6
  "keywords": [
@@ -32,17 +32,18 @@
32
32
  "exports": {
33
33
  ".": {
34
34
  "types": "./index.d.ts",
35
- "import": "./index.js"
35
+ "import": "./index.js",
36
+ "default": "./index.js"
36
37
  },
37
38
  "./package.json": "./package.json"
38
39
  },
39
40
  "dependencies": {
40
- "@effected/npm": "0.2.0",
41
- "@effected/semver": "0.1.0",
41
+ "@effected/npm": "0.2.2",
42
+ "@effected/semver": "0.2.0",
42
43
  "spdx-expression-parse": "^4.0.0"
43
44
  },
44
45
  "peerDependencies": {
45
- "effect": "4.0.0-beta.98"
46
+ "effect": "4.0.0-beta.99"
46
47
  },
47
48
  "engines": {
48
49
  "node": ">=24.11.0"
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.10"
8
+ "packageVersion": "7.58.11"
9
9
  }
10
10
  ]
11
11
  }