@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 +26 -24
- package/PackageJsonFile.js +9 -1
- package/README.md +4 -2
- package/index.d.ts +42 -8
- package/internal/format.js +138 -19
- package/package.json +2 -2
- package/tsdoc-metadata.json +1 -1
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
|
|
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
|
-
...
|
|
110
|
-
...
|
|
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
|
-
*
|
|
295
|
-
*
|
|
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 (
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
package/PackageJsonFile.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
337
|
-
readonly indent?:
|
|
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
|
-
*
|
|
530
|
-
*
|
|
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
|
-
/**
|
|
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
|
package/internal/format.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|
57
|
-
const
|
|
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},
|
|
65
|
-
* alphabetically
|
|
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
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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
|
|
170
|
+
const STRIP_EMPTY_KEYS = [
|
|
79
171
|
"dependencies",
|
|
80
172
|
"devDependencies",
|
|
81
173
|
"peerDependencies",
|
|
82
|
-
"optionalDependencies"
|
|
174
|
+
"optionalDependencies",
|
|
175
|
+
"scripts"
|
|
83
176
|
];
|
|
84
|
-
/** Remove
|
|
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
|
|
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.
|
|
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.
|
|
40
|
+
"@effected/npm": "0.2.0",
|
|
41
41
|
"@effected/semver": "0.1.0",
|
|
42
42
|
"spdx-expression-parse": "^4.0.0"
|
|
43
43
|
},
|