@absolutejs/changelog 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
8
 
9
+ ## 0.5.1 — 2026-09-10
10
+
11
+ ### Fixed
12
+
13
+ - **Report a symbols list that is not one, trim a version where it is read, and refuse a fractional contract** (`parseChangelog`)
14
+
15
+ ## 0.5.0 — 2026-09-10
16
+
17
+ ### Breaking
18
+
19
+ - **A surface records which entry points offer each name, so a name leaving one is a change** (`PackageSurface`, `SurfaceName`, `surfaceOfFiles`, `typeEntriesOf`, `TypeEntry`, `SurfaceDiff`, `Withdrawal`, `Unannounced`)
20
+ _Migration:_ surfaceOfFiles takes { path, specifier } entries and its values are { declaration, entries }; diffSurface reports withdrawn alongside removed.
21
+
22
+ ### Added
23
+
24
+ - **Cache what the registry said for five minutes, so a page asking about twenty packages asks once each** (`forgetRegistry`, `packageVersions`)
25
+
26
+ ### Fixed
27
+
28
+ - **Tell a package that has never been published from a registry that will not answer, so a first release is not refused forever** (`packageVersions`)
29
+
9
30
  ## 0.4.0 — 2026-09-10
10
31
 
11
32
  ### Added
package/changelog.json CHANGED
@@ -2,6 +2,58 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/changelog",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "fixed",
9
+ "summary": "Report a symbols list that is not one, trim a version where it is read, and refuse a fractional contract",
10
+ "symbols": [
11
+ "parseChangelog"
12
+ ]
13
+ }
14
+ ],
15
+ "date": "2026-09-10",
16
+ "version": "0.5.1"
17
+ },
18
+ {
19
+ "changes": [
20
+ {
21
+ "kind": "breaking",
22
+ "migration": {
23
+ "instruction": "surfaceOfFiles takes { path, specifier } entries and its values are { declaration, entries }; diffSurface reports withdrawn alongside removed.",
24
+ "manual": true
25
+ },
26
+ "summary": "A surface records which entry points offer each name, so a name leaving one is a change",
27
+ "symbols": [
28
+ "PackageSurface",
29
+ "SurfaceName",
30
+ "surfaceOfFiles",
31
+ "typeEntriesOf",
32
+ "TypeEntry",
33
+ "SurfaceDiff",
34
+ "Withdrawal",
35
+ "Unannounced"
36
+ ]
37
+ },
38
+ {
39
+ "kind": "added",
40
+ "summary": "Cache what the registry said for five minutes, so a page asking about twenty packages asks once each",
41
+ "symbols": [
42
+ "forgetRegistry",
43
+ "packageVersions"
44
+ ]
45
+ },
46
+ {
47
+ "kind": "fixed",
48
+ "summary": "Tell a package that has never been published from a registry that will not answer, so a first release is not refused forever",
49
+ "symbols": [
50
+ "packageVersions"
51
+ ]
52
+ }
53
+ ],
54
+ "date": "2026-09-10",
55
+ "version": "0.5.0"
56
+ },
5
57
  {
6
58
  "changes": [
7
59
  {
package/dist/cli.js CHANGED
@@ -113,7 +113,14 @@ var isKind = (value) => typeof value === "string" && CHANGE_KINDS.some((known) =
113
113
  var isDisruptiveKind = (kind) => kind === "breaking" || kind === "removed";
114
114
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
115
115
  var isText = (value) => typeof value === "string" && value.trim().length > 0;
116
- var textList = (value) => Array.isArray(value) && value.length > 0 && value.every(isText) ? value : undefined;
116
+ var textList = (value, where, problems) => {
117
+ if (value === undefined)
118
+ return;
119
+ if (Array.isArray(value) && value.length > 0 && value.every(isText))
120
+ return value.filter((entry) => typeof entry === "string");
121
+ problems.push(`${where}: symbols is not a list of names`);
122
+ return;
123
+ };
117
124
  var readMigration = (value, where, problems) => {
118
125
  if (!isRecord(value) || !isText(value.instruction)) {
119
126
  problems.push(`${where}: migration needs an instruction`);
@@ -162,7 +169,7 @@ var readChange = (value, where, problems) => {
162
169
  ...isText(value.reference) ? { reference: value.reference } : {},
163
170
  summary: value.summary
164
171
  };
165
- const symbols = textList(value.symbols);
172
+ const symbols = textList(value.symbols, where, problems);
166
173
  const [first, ...rest] = symbols ?? [];
167
174
  if (kind === "breaking" || kind === "removed") {
168
175
  const migration = readMigration(value.migration, where, problems);
@@ -193,7 +200,8 @@ var readRelease = (value, where, problems) => {
193
200
  problems.push(`${where}: not an object`);
194
201
  return;
195
202
  }
196
- if (!isText(value.version) || parseVersion(value.version) === undefined) {
203
+ const version = isText(value.version) ? value.version.trim() : undefined;
204
+ if (version === undefined || parseVersion(version) === undefined) {
197
205
  problems.push(`${where}: version is not a version`);
198
206
  return;
199
207
  }
@@ -216,7 +224,7 @@ var readRelease = (value, where, problems) => {
216
224
  changes: [head, ...tail],
217
225
  date: value.date,
218
226
  ...isText(value.note) ? { note: value.note } : {},
219
- version: value.version
227
+ version
220
228
  };
221
229
  return release;
222
230
  };
@@ -236,7 +244,7 @@ var parseChangelog = (value) => {
236
244
  return { problems: ["not an object"] };
237
245
  if (!isText(value.name))
238
246
  problems.push("needs a package name");
239
- if (typeof value.contract !== "number" || value.contract > CHANGELOG_CONTRACT)
247
+ if (!Number.isInteger(value.contract) || Number(value.contract) > CHANGELOG_CONTRACT)
240
248
  problems.push(`contract ${JSON.stringify(value.contract)} is newer than ${CHANGELOG_CONTRACT}, which this reader understands`);
241
249
  const raw = value.releases;
242
250
  if (!Array.isArray(raw))
@@ -438,33 +446,55 @@ var surfaceOfArchive = (archive) => {
438
446
  };
439
447
  var surfaceOfFiles = (files, entries) => {
440
448
  const surface = new Map;
441
- for (const entry of entries)
442
- for (const [name, text] of exportsOf(files, entry, new Set))
443
- if (!surface.has(name))
444
- surface.set(name, text);
449
+ const offered = entries.flatMap((entry) => [...exportsOf(files, entry.path, new Set)].map(([name, declaration]) => ({ declaration, name, specifier: entry.specifier })));
450
+ for (const { declaration, name, specifier } of offered) {
451
+ const known = surface.get(name);
452
+ if (known === undefined)
453
+ surface.set(name, { declaration, entries: [specifier] });
454
+ else if (!known.entries.includes(specifier))
455
+ known.entries.push(specifier);
456
+ }
457
+ for (const value of surface.values())
458
+ value.entries.sort();
445
459
  return surface;
446
460
  };
447
- var typeCandidatesIn = (value) => {
461
+ var exportEntries = (value, packageName, specifier) => {
462
+ const here = specifier ?? packageName;
448
463
  if (typeof value === "string")
449
- return [value];
464
+ return [{ specifier: here, value }];
450
465
  if (typeof value !== "object" || value === null)
451
466
  return [];
452
- return Object.entries(value).flatMap(([key, nested]) => key === "types" || key === "typings" ? typeCandidatesIn(nested) : typeCandidatesIn(nested));
467
+ return Object.entries(value).flatMap(([key, nested]) => key.startsWith(".") ? exportEntries(nested, packageName, key === "." ? packageName : `${packageName}${key.slice(1)}`) : exportEntries(nested, packageName, here));
453
468
  };
454
469
  var typeEntriesOf = (manifest, files) => {
455
470
  const record = typeof manifest === "object" && manifest !== null ? new Map(Object.entries(manifest)) : new Map;
456
- const claimed = [
457
- record.get("types"),
458
- record.get("typings"),
459
- ...typeCandidatesIn(record.get("exports"))
471
+ const named = record.get("name");
472
+ const packageName = typeof named === "string" ? named : "";
473
+ const resolved = (value, specifier) => {
474
+ if (typeof value !== "string")
475
+ return [];
476
+ const cleaned = value.replace(/^\.\//u, "");
477
+ const candidates = DECLARATION.test(cleaned) ? [cleaned] : [
478
+ cleaned.replace(/\.[cm]?js$/u, ".d.ts"),
479
+ cleaned.replace(/\.[cm]?js$/u, ".d.mts"),
480
+ cleaned.replace(/\.[cm]?js$/u, ".d.cts")
481
+ ];
482
+ return candidates.filter((candidate) => files.has(candidate)).map((resolvedPath) => ({ path: resolvedPath, specifier }));
483
+ };
484
+ const found = [
485
+ ...resolved(record.get("types"), packageName),
486
+ ...resolved(record.get("typings"), packageName),
487
+ ...exportEntries(record.get("exports"), packageName).flatMap((entry) => resolved(entry.value, entry.specifier))
460
488
  ];
461
- const entries = claimed.filter((value) => typeof value === "string").map((value) => value.replace(/^\.\//u, "")).flatMap((cleaned) => DECLARATION.test(cleaned) ? [cleaned] : [
462
- cleaned.replace(/\.[cm]?js$/u, ".d.ts"),
463
- cleaned.replace(/\.[cm]?js$/u, ".d.mts"),
464
- cleaned.replace(/\.[cm]?js$/u, ".d.cts")
465
- ]).filter((candidate) => files.has(candidate));
466
- const fallback = entries.length === 0 && files.has("dist/index.d.ts") ? ["dist/index.d.ts"] : [];
467
- return [...new Set([...entries, ...fallback])];
489
+ const fallback = found.length === 0 && files.has("dist/index.d.ts") ? [{ path: "dist/index.d.ts", specifier: packageName }] : [];
490
+ const seen = new Set;
491
+ return [...found, ...fallback].filter((entry) => {
492
+ const key = `${entry.specifier}\x00${entry.path}`;
493
+ if (seen.has(key))
494
+ return false;
495
+ seen.add(key);
496
+ return true;
497
+ });
468
498
  };
469
499
  var SHAPE = {
470
500
  members: "members",
@@ -519,23 +549,33 @@ var changeShape = (change) => {
519
549
  };
520
550
  var diffSurface = (before, after) => {
521
551
  const removed = [];
552
+ const withdrawn = [];
522
553
  const changed = [];
523
- for (const [name, declaration] of before) {
554
+ for (const [name, was] of before) {
524
555
  const now = after.get(name);
525
556
  if (now === undefined)
526
557
  removed.push(name);
527
- else if (now !== declaration)
558
+ if (now === undefined)
559
+ continue;
560
+ const lost = was.entries.filter((entry) => !now.entries.includes(entry));
561
+ if (lost.length > 0)
562
+ withdrawn.push({ from: lost, name });
563
+ if (now.declaration !== was.declaration)
528
564
  changed.push({
529
- after: now,
530
- before: declaration,
565
+ after: now.declaration,
566
+ before: was.declaration,
531
567
  name,
532
- shape: changeShape({ after: now, before: declaration })
568
+ shape: changeShape({
569
+ after: now.declaration,
570
+ before: was.declaration
571
+ })
533
572
  });
534
573
  }
535
574
  return {
536
575
  added: [...after.keys()].filter((name) => !before.has(name)),
537
576
  changed,
538
- removed
577
+ removed,
578
+ withdrawn
539
579
  };
540
580
  };
541
581
  var notable = (diff) => diff.changed.filter((change) => change.shape === "parameters" || change.shape === "members");
@@ -671,10 +711,11 @@ var describeReconciliation = (found) => {
671
711
  members: "changed the members of its type",
672
712
  parameters: "changed what it is called with",
673
713
  removed: "is no longer exported",
674
- types: "changed its declaration"
714
+ types: "changed its declaration",
715
+ withdrawn: "is no longer exported from"
675
716
  };
676
717
  for (const entry of found.unannounced)
677
- lines.push(`\`${entry.name}\` ${moved[entry.kind]} and no entry says so`);
718
+ lines.push(entry.kind === "withdrawn" ? `\`${entry.name}\` ${moved.withdrawn} ${(entry.from ?? []).join(", ")} and no entry says so` : `\`${entry.name}\` ${moved[entry.kind]} and no entry says so`);
678
719
  for (const name of found.unknownSymbols)
679
720
  lines.push(`an entry names \`${name}\`, which this package does not export`);
680
721
  lines.push(...found.wrongMigrations);
@@ -685,6 +726,7 @@ var reconcile = (input) => {
685
726
  const mentioned = new Set(input.changes.flatMap((change) => [...change.symbols ?? []]));
686
727
  const unannounced = [
687
728
  ...input.diff.removed.filter((name) => !disruptive.has(name)).map((name) => ({ kind: "removed", name })),
729
+ ...input.diff.withdrawn.filter((entry) => !disruptive.has(entry.name)).map((entry) => ({ from: entry.from, kind: "withdrawn", name: entry.name })),
688
730
  ...input.diff.changed.filter((change) => !mentioned.has(change.name) && change.shape !== "unknown").map((change) => ({ kind: change.shape, name: change.name }))
689
731
  ];
690
732
  const unknownSymbols = [
@@ -710,6 +752,8 @@ var MAX_ARCHIVE_BYTES = MAX_ARCHIVE_MEBIBYTES * MEBIBYTE2;
710
752
  var MAX_DOWNLOAD_MEBIBYTES = 32;
711
753
  var MAX_DOWNLOAD_BYTES = MAX_DOWNLOAD_MEBIBYTES * MEBIBYTE2;
712
754
  var TIMEOUT_MS = 20000;
755
+ var NOT_FOUND = 404;
756
+ var NOTHING_PUBLISHED = { published: [], tarballs: {} };
713
757
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
714
758
  var MAX_NAME_LENGTH = 214;
715
759
  var isPackageName = (name) => name.length <= MAX_NAME_LENGTH && PACKAGE_NAME.test(name);
@@ -753,9 +797,25 @@ var packageArchive = async (name, version, options = {}) => {
753
797
  return;
754
798
  }
755
799
  };
800
+ var packuments = new Map;
801
+ var SECONDS_PER_MINUTE = 60;
802
+ var MILLISECONDS_PER_SECOND = 1000;
803
+ var PACKUMENT_MINUTES = 5;
804
+ var PACKUMENT_TTL_MS = PACKUMENT_MINUTES * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
805
+ var forgetRegistry = () => packuments.clear();
756
806
  var packageVersions = async (name, options = {}) => {
757
807
  if (!isPackageName(name))
758
808
  return;
809
+ const cacheable = options.fetch === undefined && options.registry === undefined;
810
+ const known = cacheable ? packuments.get(name) : undefined;
811
+ if (known !== undefined && Date.now() - known.asked < PACKUMENT_TTL_MS)
812
+ return known.value;
813
+ const value = askRegistry(name, options);
814
+ if (cacheable)
815
+ packuments.set(name, { asked: Date.now(), value });
816
+ return value;
817
+ };
818
+ var askRegistry = async (name, options) => {
759
819
  const call = options.fetch ?? fetch;
760
820
  const url = `${options.registry ?? DEFAULT_REGISTRY}/${name.replace("/", "%2f")}`;
761
821
  try {
@@ -764,9 +824,9 @@ var packageVersions = async (name, options = {}) => {
764
824
  signal: AbortSignal.timeout(TIMEOUT_MS)
765
825
  });
766
826
  const response = abbreviated.ok ? abbreviated : await call(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
767
- if (!response.ok)
768
- return;
769
- return readPackument(await response.json());
827
+ if (response.ok)
828
+ return readPackument(await response.json());
829
+ return response.status === NOT_FOUND ? NOTHING_PUBLISHED : undefined;
770
830
  } catch {
771
831
  return;
772
832
  }
@@ -1111,9 +1171,11 @@ var check = async (root, flags) => {
1111
1171
  const versions = offline ? undefined : await packageVersions(manifest.name ?? "");
1112
1172
  if (!offline && versions === undefined)
1113
1173
  failures.push(`the registry did not answer for ${manifest.name}, so nothing was compared \u2014 pass --offline to skip the comparison deliberately`);
1174
+ if (versions !== undefined && versions.published.length === 0)
1175
+ console.warn(` (${manifest.name} has never been published, so there was nothing to compare)`);
1114
1176
  const baseline = (versions?.published ?? []).filter((version) => compareVersions(version, manifest.version) <= 0).sort(compareVersions).at(LAST2);
1115
- if (versions !== undefined && baseline === undefined && !offline)
1116
- console.warn(` (nothing published at or below ${manifest.version}; this looks like a first release)`);
1177
+ if (versions !== undefined && versions.published.length > 0 && baseline === undefined && !offline)
1178
+ console.warn(` (nothing published at or below ${manifest.version}, so there was nothing to compare)`);
1117
1179
  if (built !== undefined && baseline !== undefined) {
1118
1180
  const published = await publishedSurface(manifest.name ?? "", baseline);
1119
1181
  if (published === undefined)
@@ -1249,5 +1311,5 @@ await main().catch((error) => {
1249
1311
  process.exit(1);
1250
1312
  });
1251
1313
 
1252
- //# debugId=4CD69EF0E551B35864756E2164756E21
1314
+ //# debugId=3D35EBE83A76D8A664756E2164756E21
1253
1315
  //# sourceMappingURL=cli.js.map