@effected/package-json 0.1.0 → 0.3.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/Package.js CHANGED
@@ -1,11 +1,11 @@
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";
8
- import { CatalogResolver, WorkspaceResolver } from "@effected/npm";
8
+ import { CatalogResolver, DependencySpecifier, WorkspaceResolver } from "@effected/npm";
9
9
  import { Effect, Function, HashMap, Option, Pipeable, Schema, SchemaTransformation } from "effect";
10
10
  import { SemVer } from "@effected/semver";
11
11
 
@@ -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
@@ -106,23 +106,12 @@ const makeWire = (Class) => {
106
106
  encode: (encoded) => {
107
107
  const { rest, ...known } = encoded;
108
108
  return {
109
- ...known,
110
- ...rest ?? {}
109
+ ...rest ?? {},
110
+ ...known
111
111
  };
112
112
  }
113
113
  })));
114
114
  };
115
- const applyWorkspaceModifier = (specifier, version) => {
116
- const modifier = specifier.slice(10);
117
- if (modifier === "*" || modifier === "") return version;
118
- if (modifier === "^") return `^${version}`;
119
- if (modifier === "~") return `~${version}`;
120
- return modifier;
121
- };
122
- const catalogName = (specifier) => {
123
- const name = specifier.slice(8);
124
- return name.length === 0 ? Option.none() : Option.some(name);
125
- };
126
115
  /**
127
116
  * A package.json document as a rich `Schema.Class`: typed known fields, a
128
117
  * `rest` catch-all preserving unknown top-level fields across a read/edit/write
@@ -290,20 +279,33 @@ var Package = class Package extends Schema.Class("Package")({
290
279
  static removeScript = Function.dual(2, (pkg, name) => pkg.copyWith({ scripts: HashMap.remove(pkg.scripts, name) }));
291
280
  /**
292
281
  * Resolve `catalog:` and `workspace:` specifiers across all four dependency
293
- * maps using the `CatalogResolver` and `WorkspaceResolver` from context.
294
- * Specifiers the resolvers return `None` for are left unchanged. This is the
295
- * explicit resolution step `PackageJsonFile.write` never resolves.
282
+ * maps using the `CatalogResolver` and `WorkspaceResolver` from context,
283
+ * classifying and projecting through `@effected/npm`'s `DependencySpecifier`
284
+ * statics (`workspace:` uses the pnpm publish-time projection; the alias
285
+ * form `workspace:<name>@<range>` resolves the TARGET package's version and
286
+ * becomes the published `npm:<name>@<range>` alias). Specifiers the
287
+ * resolvers return `None` for are left unchanged — resolution still
288
+ * succeeds. A `CatalogResolver` whose catalog assembly failed surfaces
289
+ * typed as `@effected/npm`'s `CatalogAssemblyError`, alongside the
290
+ * contracts' `DependencyResolutionError`. This is the explicit resolution
291
+ * step — `PackageJsonFile.write` never resolves.
292
+ *
293
+ * @remarks
294
+ * Leaves unresolvable specifiers unchanged. For fail-typed manifest
295
+ * resolution over the tolerant model, see `@effected/npm`'s
296
+ * `Manifest#resolve`.
296
297
  */
297
298
  static resolve = Effect.fn("Package.resolve")(function* (pkg) {
298
299
  const workspace = yield* WorkspaceResolver;
299
300
  const catalog = yield* CatalogResolver;
300
301
  const resolveMap = Effect.fn("Package.resolve.map")(function* (map) {
301
302
  let next = map;
302
- for (const [name, specifier] of HashMap.entries(map)) if (specifier.startsWith("workspace:")) {
303
- const version = yield* workspace.versionOf(name);
304
- if (Option.isSome(version)) next = HashMap.set(next, name, applyWorkspaceModifier(specifier, version.value));
305
- } else if (specifier.startsWith("catalog:")) {
306
- const range = yield* catalog.rangeOf(name, catalogName(specifier));
303
+ for (const [name, specifier] of HashMap.entries(map)) if (DependencySpecifier.isWorkspace(specifier)) {
304
+ const target = Option.getOrElse(DependencySpecifier.workspaceTargetOf(specifier), () => name);
305
+ const version = yield* workspace.versionOf(target);
306
+ if (Option.isSome(version)) next = HashMap.set(next, name, DependencySpecifier.resolveWorkspace(specifier, version.value));
307
+ } else if (DependencySpecifier.isCatalog(specifier)) {
308
+ const range = yield* catalog.rangeOf(name, DependencySpecifier.catalogNameOf(specifier));
307
309
  if (Option.isSome(range)) next = HashMap.set(next, name, range.value);
308
310
  }
309
311
  return next;
@@ -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
@@ -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.
@@ -180,7 +182,7 @@ console.log(Effect.runSync(program));
180
182
  // => ["^4.0.0", "^1.4.0"]
181
183
  ```
182
184
 
183
- The `workspace:` range modifier is honored: `workspace:*` takes the bare version, `workspace:^` and `workspace:~` prefix it, and an explicit modifier is used as-is.
185
+ The `workspace:` range modifier is honored: `workspace:*` takes the bare version, `workspace:^` and `workspace:~` prefix it, and an explicit modifier is used as-is. The projection is `@effected/npm`'s `DependencySpecifier` statics with full pnpm publish semantics: the alias form `workspace:<name>@<range>` resolves the *target* package's version and becomes the `npm:<name>@<range>` alias pnpm publishes, and a blank catalog name selects the default catalog. A failed catalog assembly surfaces typed as `@effected/npm`'s `CatalogAssemblyError`, alongside the contracts' `DependencyResolutionError`.
184
186
 
185
187
  ## Errors
186
188
 
@@ -206,7 +208,7 @@ Every failure is a `Schema.TaggedErrorClass` routed with `Effect.catchTag`. Caus
206
208
  - `Package.schema` / `Package.wireFor` — the open-JSON ↔ class wire codec, and the factory that builds one for a `.extend()`ed subclass so its custom fields decode as typed members instead of falling into `rest`.
207
209
  - `PackageJsonFile` — the IO surface: `read` and `write` over core `FileSystem` / `Path`, with the platform implementation supplied at the edge.
208
210
  - `PackageValidator` — rule-based validation aggregating every failure, with the default rule set, a parameterized `layerRules` factory, and the publish-gate rules `noUnresolvedDepsRule` and `noLocalDepsRule`.
209
- - `Package.resolve` — `catalog:` and `workspace:` expansion over the `@effected/npm` contracts, as an explicit step that `write` never performs for you.
211
+ - `Package.resolve` — `catalog:` and `workspace:` expansion over the `@effected/npm` contracts with pnpm's publish-time projection (alias form included), as an explicit step that `write` never performs for you.
210
212
  - `PackageName`, `DependencySpecifier`, `Dependency`, `SpdxLicense`, `PackageManager`, `Person`, `DevEngine` — the leaf concepts, each owning its own statics, brand and error, usable independently of `Package`.
211
213
  - Field schemas (`DependencyMapField`, `BinField`, `ExportsField`, `RepositoryField`, `PublishConfigField`, `PeerDependenciesMetaField`, `StringMapField`) exported for subclasses that extend the model.
212
214
 
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`). */
@@ -525,11 +542,23 @@ declare class Package extends Package_base {
525
542
  };
526
543
  /**
527
544
  * Resolve `catalog:` and `workspace:` specifiers across all four dependency
528
- * maps using the `CatalogResolver` and `WorkspaceResolver` from context.
529
- * Specifiers the resolvers return `None` for are left unchanged. This is the
530
- * explicit resolution step `PackageJsonFile.write` never resolves.
545
+ * maps using the `CatalogResolver` and `WorkspaceResolver` from context,
546
+ * classifying and projecting through `@effected/npm`'s `DependencySpecifier`
547
+ * statics (`workspace:` uses the pnpm publish-time projection; the alias
548
+ * form `workspace:<name>@<range>` resolves the TARGET package's version and
549
+ * becomes the published `npm:<name>@<range>` alias). Specifiers the
550
+ * resolvers return `None` for are left unchanged — resolution still
551
+ * succeeds. A `CatalogResolver` whose catalog assembly failed surfaces
552
+ * typed as `@effected/npm`'s `CatalogAssemblyError`, alongside the
553
+ * contracts' `DependencyResolutionError`. This is the explicit resolution
554
+ * step — `PackageJsonFile.write` never resolves.
555
+ *
556
+ * @remarks
557
+ * Leaves unresolvable specifiers unchanged. For fail-typed manifest
558
+ * resolution over the tolerant model, see `@effected/npm`'s
559
+ * `Manifest#resolve`.
531
560
  */
532
- static readonly resolve: (pkg: Package) => Effect.Effect<Package, import("@effected/npm").DependencyResolutionError, CatalogResolver | WorkspaceResolver>;
561
+ static readonly resolve: (pkg: Package) => Effect.Effect<Package, import("@effected/npm").CatalogAssemblyError | import("@effected/npm").DependencyResolutionError, CatalogResolver | WorkspaceResolver>;
533
562
  /**
534
563
  * Serialize to a formatted package.json string: encode through the wire
535
564
  * codec (flattening `rest`), then apply the canonical key order, dependency
@@ -610,7 +639,12 @@ interface PackageJsonFileShape {
610
639
  * (invalid JSON) or `PackageDecodeError` (schema decode).
611
640
  */
612
641
  readonly read: (path: string) => Effect.Effect<Package, PackageJsonReadError | PackageJsonNotFoundError | PackageJsonParseError | PackageDecodeError>;
613
- /** 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
+ */
614
648
  readonly write: (path: string, pkg: Package, options?: PackageFormatOptions) => Effect.Effect<void, PackageJsonWriteError>;
615
649
  }
616
650
  declare const PackageJsonFile_base: Context.ServiceClass<PackageJsonFile, "@effected/package-json/PackageJsonFile", PackageJsonFileShape>;
@@ -743,5 +777,5 @@ declare class PackageValidator extends PackageValidator_base {
743
777
  }): Layer.Layer<PackageValidator>;
744
778
  }
745
779
  //#endregion
746
- 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 };
747
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.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "package.json parsing, editing, validation and file IO as Effect schemas.",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "./package.json": "./package.json"
38
38
  },
39
39
  "dependencies": {
40
- "@effected/npm": "0.1.0",
40
+ "@effected/npm": "0.2.0",
41
41
  "@effected/semver": "0.1.0",
42
42
  "spdx-expression-parse": "^4.0.0"
43
43
  },
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.9"
8
+ "packageVersion": "7.58.10"
9
9
  }
10
10
  ]
11
11
  }