@effected/package-json 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Package.js CHANGED
@@ -1,7 +1,7 @@
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 } from "./internal/format.js";
4
+ import { renderJson, resolveIndent } from "./internal/format.js";
5
5
  import { PackageManager } from "./PackageManager.js";
6
6
  import { InvalidPackageNameError, PackageName } from "./PackageName.js";
7
7
  import { Person } from "./Person.js";
@@ -84,7 +84,7 @@ cause: Schema.Defect() }) {
84
84
  }
85
85
  };
86
86
  const resolveFormatOptions = (options) => ({
87
- indent: options?.indent ?? 2,
87
+ indent: resolveIndent(options?.indent, options?.sourceText),
88
88
  sort: options?.sort ?? true,
89
89
  stripEmpty: options?.stripEmpty ?? true,
90
90
  newline: options?.newline ?? true
@@ -104,7 +104,15 @@ var PackageJsonFile = class PackageJsonFile extends Context.Service()("@effected
104
104
  return yield* Package.decode(json);
105
105
  }),
106
106
  write: Effect.fn("PackageJsonFile.write")(function* (target, pkg, options) {
107
- const json = pkg.toJsonString(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);
108
116
  const directory = path.dirname(target);
109
117
  yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.mapError((cause) => new PackageJsonWriteError({
110
118
  path: target,
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
 
@@ -37,6 +37,8 @@ pnpm add @effected/package-json effect @effect/platform-node
37
37
 
38
38
  Requires Node.js >=24.11.0.
39
39
 
40
+ All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
41
+
40
42
  `effect` v4 is the only peer dependency. `@effected/semver` and `@effected/npm` come along as ordinary dependencies — they back the `version` field and the resolver contracts — and `spdx-expression-parse` is the one external package in the tree, used to validate SPDX license expressions.
41
43
 
42
44
  Reading and writing files needs a `FileSystem` and a `Path` implementation, provided once at the edge, from `@effect/platform-node` on Node. Everything except `PackageJsonFile` is pure and needs no platform layer at all.
package/index.d.ts CHANGED
@@ -327,14 +327,31 @@ declare const PackageDecodeError_base: Schema.Class<PackageDecodeError, Schema.T
327
327
  declare class PackageDecodeError extends PackageDecodeError_base {
328
328
  get message(): string;
329
329
  }
330
+ /**
331
+ * Indentation for serialized package.json output: a spaces count, `"tab"` for
332
+ * real tab indentation, or `"preserve"` to reuse the indentation detected from
333
+ * the original source text (falling back to the two-space default when no
334
+ * source text is available).
335
+ *
336
+ * @public
337
+ */
338
+ type PackageIndent = number | "tab" | "preserve";
330
339
  /**
331
340
  * Options for {@link Package.toJsonString} and `PackageJsonFile.write`.
332
341
  *
333
342
  * @public
334
343
  */
335
344
  interface PackageFormatOptions {
336
- /** Indentation width in spaces (default `2`). */
337
- readonly indent?: number;
345
+ /** Indentation: a spaces count, `"tab"`, or `"preserve"` (default `2`). */
346
+ readonly indent?: PackageIndent;
347
+ /**
348
+ * The original source text backing `indent: "preserve"`: its indentation
349
+ * (tab vs N spaces, detected from the first indented line) is reused.
350
+ * Ignored for other `indent` values. When absent, `PackageJsonFile.write`
351
+ * supplies the existing file's text automatically; the pure
352
+ * {@link Package.toJsonString} falls back to the default indentation.
353
+ */
354
+ readonly sourceText?: string;
338
355
  /** Order top-level keys canonically and alphabetize dependency maps (default `true`). */
339
356
  readonly sort?: boolean;
340
357
  /** Strip empty dependency-map keys (default `true`). */
@@ -622,7 +639,12 @@ interface PackageJsonFileShape {
622
639
  * (invalid JSON) or `PackageDecodeError` (schema decode).
623
640
  */
624
641
  readonly read: (path: string) => Effect.Effect<Package, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageDecodeError>;
625
- /** Serialize and write a package.json file. Fails with `PackageJsonWriteError`. */
642
+ /**
643
+ * Serialize and write a package.json file. Fails with
644
+ * `PackageJsonWriteError`. With `indent: "preserve"` and no explicit
645
+ * `sourceText`, the existing file at `path` (when readable) supplies the
646
+ * source text whose indentation is preserved.
647
+ */
626
648
  readonly write: (path: string, pkg: Package, options?: PackageFormatOptions) => Effect.Effect<void, PackageJsonWriteError>;
627
649
  }
628
650
  declare const PackageJsonFile_base: Context.ServiceClass<PackageJsonFile, "@effected/package-json/PackageJsonFile", PackageJsonFileShape>;
@@ -755,5 +777,5 @@ declare class PackageValidator extends PackageValidator_base {
755
777
  }): Layer.Layer<PackageValidator>;
756
778
  }
757
779
  //#endregion
758
- export { BinField, Dependency, type DependencyKind, DependencyMapField, type DependencyProtocol, DependencySpecifier, type DependencySpecifierBrand, DevEngine, DevEngineOrArray, type DevEngines, DevEnginesSchema, ExportsField, InvalidDependencySpecifierError, InvalidPackageNameError, InvalidSpdxLicenseError, Package, PackageDecodeError, type PackageFormatOptions, 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 };
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 };
759
781
  //# sourceMappingURL=index.d.ts.map
@@ -2,94 +2,213 @@
2
2
  const KEY_INDEX = new Map([
3
3
  "$schema",
4
4
  "name",
5
+ "displayName",
5
6
  "version",
7
+ "stableVersion",
6
8
  "private",
7
9
  "description",
10
+ "categories",
8
11
  "keywords",
9
12
  "homepage",
10
13
  "bugs",
11
14
  "repository",
12
15
  "funding",
13
16
  "license",
17
+ "qna",
14
18
  "author",
19
+ "maintainers",
15
20
  "contributors",
21
+ "publisher",
22
+ "sideEffects",
16
23
  "type",
17
24
  "imports",
18
25
  "exports",
19
26
  "main",
27
+ "svelte",
28
+ "umd:main",
29
+ "jsdelivr",
30
+ "unpkg",
20
31
  "module",
32
+ "source",
33
+ "jsnext:main",
21
34
  "browser",
35
+ "react-native",
36
+ "types",
37
+ "typesVersions",
38
+ "typings",
39
+ "style",
40
+ "example",
41
+ "examplestyle",
42
+ "assets",
22
43
  "bin",
23
44
  "man",
24
- "files",
25
45
  "directories",
46
+ "files",
26
47
  "workspaces",
48
+ "binary",
27
49
  "scripts",
50
+ "betterScripts",
51
+ "wireit",
52
+ "l10n",
53
+ "contributes",
54
+ "activationEvents",
55
+ "husky",
56
+ "simple-git-hooks",
57
+ "pre-commit",
58
+ "commitlint",
59
+ "lint-staged",
60
+ "nano-staged",
28
61
  "config",
62
+ "nodemonConfig",
63
+ "browserify",
64
+ "babel",
65
+ "browserslist",
66
+ "xo",
67
+ "prettier",
68
+ "eslintConfig",
69
+ "eslintIgnore",
70
+ "npmpkgjsonlint",
71
+ "npmPackageJsonLintConfig",
72
+ "npmpackagejsonlint",
73
+ "release",
74
+ "remarkConfig",
75
+ "stylelint",
76
+ "ava",
77
+ "jest",
78
+ "jest-junit",
79
+ "jest-stare",
80
+ "mocha",
81
+ "nyc",
82
+ "c8",
83
+ "tap",
84
+ "oclif",
85
+ "resolutions",
86
+ "overrides",
29
87
  "dependencies",
30
88
  "devDependencies",
89
+ "dependenciesMeta",
31
90
  "peerDependencies",
32
91
  "peerDependenciesMeta",
33
92
  "optionalDependencies",
93
+ "bundledDependencies",
34
94
  "bundleDependencies",
35
- "overrides",
95
+ "extensionPack",
96
+ "extensionDependencies",
97
+ "flat",
98
+ "packageManager",
36
99
  "engines",
100
+ "engineStrict",
37
101
  "devEngines",
102
+ "volta",
103
+ "languageName",
38
104
  "os",
39
105
  "cpu",
106
+ "preferGlobal",
40
107
  "publishConfig",
41
- "packageManager"
108
+ "icon",
109
+ "badges",
110
+ "galleryBanner",
111
+ "preview",
112
+ "markdown",
113
+ "pnpm"
42
114
  ].map((k, i) => [k, i]));
43
115
  /**
44
- * Deterministic, locale-independent string comparison by code point. A bare
116
+ * Deterministic, locale-independent string comparison by code unit. A bare
45
117
  * `localeCompare` sorts differently across ICU builds/locales; package.json key
46
118
  * order must be stable everywhere.
47
119
  */
48
120
  const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
49
- const DEPENDENCY_KEYS = /* @__PURE__ */ new Set([
121
+ /**
122
+ * Top-level map fields whose entries are alphabetized when sorting. The
123
+ * dependency maps for canonical presentation; `scripts` / `engines` / `bin`
124
+ * additionally because the `Package` model carries them as `HashMap`s, whose
125
+ * encode order is hash order — source order is already gone, so a deterministic
126
+ * alphabetical order is strictly better. `sort-package-json` sorts `engines`
127
+ * and `bin` identically; its `scripts` sort is a grouped sort that agrees with
128
+ * plain code-unit order except for `pre*`/`post*` script pairing.
129
+ */
130
+ const SORTED_MAP_KEYS = /* @__PURE__ */ new Set([
50
131
  "dependencies",
51
132
  "devDependencies",
52
133
  "peerDependencies",
53
134
  "optionalDependencies",
54
- "bundleDependencies"
135
+ "bundleDependencies",
136
+ "scripts",
137
+ "engines",
138
+ "bin"
55
139
  ]);
56
- /** Alphabetize the entries of a plain-object dependency map. */
57
- const sortDependencyMap = (value) => {
140
+ /** Alphabetize the entries of a plain-object map field. */
141
+ const sortMapEntries = (value) => {
58
142
  const result = {};
59
143
  for (const key of Object.keys(value).sort(byCodePoint)) result[key] = value[key];
60
144
  return result;
61
145
  };
62
146
  const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
63
147
  /**
64
- * Order top-level keys canonically (known keys by {@link KEY_ORDER}, the rest
65
- * alphabetically) and alphabetize dependency-map entries.
148
+ * Order top-level keys canonically (known keys by {@link KEY_ORDER}, then
149
+ * unknown public keys alphabetically, then unknown `_`-prefixed keys
150
+ * alphabetically) and alphabetize the {@link SORTED_MAP_KEYS} map entries.
66
151
  */
67
152
  const sortKeys = (obj) => {
68
153
  const known = [];
69
- const rest = [];
154
+ const restPublic = [];
155
+ const restPrivate = [];
70
156
  for (const key of Object.keys(obj)) if (KEY_INDEX.has(key)) known.push([key, obj[key]]);
71
- else rest.push([key, obj[key]]);
157
+ else if (key.startsWith("_")) restPrivate.push([key, obj[key]]);
158
+ else restPublic.push([key, obj[key]]);
72
159
  known.sort((a, b) => KEY_INDEX.get(a[0]) - KEY_INDEX.get(b[0]));
73
- rest.sort((a, b) => byCodePoint(a[0], b[0]));
160
+ restPublic.sort((a, b) => byCodePoint(a[0], b[0]));
161
+ restPrivate.sort((a, b) => byCodePoint(a[0], b[0]));
74
162
  const result = {};
75
- for (const [key, value] of [...known, ...rest]) result[key] = DEPENDENCY_KEYS.has(key) && isPlainObject(value) ? sortDependencyMap(value) : value;
163
+ for (const [key, value] of [
164
+ ...known,
165
+ ...restPublic,
166
+ ...restPrivate
167
+ ]) result[key] = SORTED_MAP_KEYS.has(key) && isPlainObject(value) ? sortMapEntries(value) : value;
76
168
  return result;
77
169
  };
78
- const DEPENDENCY_MAP_KEYS = [
170
+ const STRIP_EMPTY_KEYS = [
79
171
  "dependencies",
80
172
  "devDependencies",
81
173
  "peerDependencies",
82
- "optionalDependencies"
174
+ "optionalDependencies",
175
+ "scripts"
83
176
  ];
84
- /** Remove dependency-map keys whose value is an empty object. */
177
+ /** Remove map keys whose value is an empty object. */
85
178
  const stripEmptyDependencyMaps = (raw) => {
86
179
  const result = { ...raw };
87
- for (const key of DEPENDENCY_MAP_KEYS) {
180
+ for (const key of STRIP_EMPTY_KEYS) {
88
181
  const value = result[key];
89
182
  if (isPlainObject(value) && Object.keys(value).length === 0) delete result[key];
90
183
  }
91
184
  return result;
92
185
  };
186
+ const DEFAULT_INDENT = 2;
187
+ /**
188
+ * Detect the indentation of a JSON source text from its first indented line:
189
+ * `"\t"` for tab indentation, otherwise the leading run of spaces. Returns
190
+ * `undefined` when no line is indented.
191
+ */
192
+ const detectIndent = (source) => {
193
+ for (const line of source.split("\n")) {
194
+ const match = /^(\t+| +)\S/.exec(line);
195
+ if (match !== null) {
196
+ const indent = match[1];
197
+ return indent.startsWith(" ") ? " " : indent;
198
+ }
199
+ }
200
+ };
201
+ /**
202
+ * Resolve a `PackageFormatOptions.indent` value to the `JSON.stringify` indent
203
+ * argument: `"tab"` becomes a real tab, `"preserve"` reuses the indentation
204
+ * detected from `sourceText` (falling back to the two-space default when no
205
+ * source text or no indented line is available), and a number passes through.
206
+ */
207
+ const resolveIndent = (indent, sourceText) => {
208
+ if (indent === "tab") return " ";
209
+ if (indent === "preserve") return sourceText === void 0 ? DEFAULT_INDENT : detectIndent(sourceText) ?? DEFAULT_INDENT;
210
+ return indent ?? DEFAULT_INDENT;
211
+ };
93
212
  /**
94
213
  * Render an already-encoded package.json record to a JSON string, applying the
95
214
  * empty-map strip, canonical key ordering and a trailing newline unless the
@@ -103,4 +222,4 @@ const renderJson = (raw, options) => {
103
222
  };
104
223
 
105
224
  //#endregion
106
- export { renderJson, sortKeys, stripEmptyDependencyMaps };
225
+ export { detectIndent, renderJson, resolveIndent, sortKeys, stripEmptyDependencyMaps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/package-json",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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.1",
42
+ "@effected/semver": "0.1.1",
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.9"
8
+ "packageVersion": "7.58.11"
9
9
  }
10
10
  ]
11
11
  }