@absolutejs/changelog 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ 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.0 — 2026-09-10
10
+
11
+ ### Breaking
12
+
13
+ - **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`)
14
+ _Migration:_ surfaceOfFiles takes { path, specifier } entries and its values are { declaration, entries }; diffSurface reports withdrawn alongside removed.
15
+
16
+ ### Added
17
+
18
+ - **Cache what the registry said for five minutes, so a page asking about twenty packages asks once each** (`forgetRegistry`, `packageVersions`)
19
+
20
+ ### Fixed
21
+
22
+ - **Tell a package that has never been published from a registry that will not answer, so a first release is not refused forever** (`packageVersions`)
23
+
24
+ ## 0.4.0 — 2026-09-10
25
+
26
+ ### Added
27
+
28
+ - **A tsconfig with comments is read and wired without being reformatted**
29
+
30
+ ### Fixed
31
+
32
+ - **Rollout scripts: an empty --version, a JSONC tsconfig, and a repository that could not be read no longer pass in silence**
33
+
9
34
  ## 0.3.0 — 2026-09-10
10
35
 
11
36
  ### Breaking
package/changelog.json CHANGED
@@ -2,6 +2,59 @@
2
2
  "contract": 1,
3
3
  "name": "@absolutejs/changelog",
4
4
  "releases": [
5
+ {
6
+ "changes": [
7
+ {
8
+ "kind": "breaking",
9
+ "migration": {
10
+ "instruction": "surfaceOfFiles takes { path, specifier } entries and its values are { declaration, entries }; diffSurface reports withdrawn alongside removed.",
11
+ "manual": true
12
+ },
13
+ "summary": "A surface records which entry points offer each name, so a name leaving one is a change",
14
+ "symbols": [
15
+ "PackageSurface",
16
+ "SurfaceName",
17
+ "surfaceOfFiles",
18
+ "typeEntriesOf",
19
+ "TypeEntry",
20
+ "SurfaceDiff",
21
+ "Withdrawal",
22
+ "Unannounced"
23
+ ]
24
+ },
25
+ {
26
+ "kind": "added",
27
+ "summary": "Cache what the registry said for five minutes, so a page asking about twenty packages asks once each",
28
+ "symbols": [
29
+ "forgetRegistry",
30
+ "packageVersions"
31
+ ]
32
+ },
33
+ {
34
+ "kind": "fixed",
35
+ "summary": "Tell a package that has never been published from a registry that will not answer, so a first release is not refused forever",
36
+ "symbols": [
37
+ "packageVersions"
38
+ ]
39
+ }
40
+ ],
41
+ "date": "2026-09-10",
42
+ "version": "0.5.0"
43
+ },
44
+ {
45
+ "changes": [
46
+ {
47
+ "kind": "added",
48
+ "summary": "A tsconfig with comments is read and wired without being reformatted"
49
+ },
50
+ {
51
+ "kind": "fixed",
52
+ "summary": "Rollout scripts: an empty --version, a JSONC tsconfig, and a repository that could not be read no longer pass in silence"
53
+ }
54
+ ],
55
+ "date": "2026-09-10",
56
+ "version": "0.4.0"
57
+ },
5
58
  {
6
59
  "changes": [
7
60
  {
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  // src/cli.ts
5
5
  import { existsSync } from "fs";
6
+ import ts2 from "typescript";
6
7
  import { mkdir, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
7
8
  import { join as join2 } from "path";
8
9
 
@@ -437,33 +438,55 @@ var surfaceOfArchive = (archive) => {
437
438
  };
438
439
  var surfaceOfFiles = (files, entries) => {
439
440
  const surface = new Map;
440
- for (const entry of entries)
441
- for (const [name, text] of exportsOf(files, entry, new Set))
442
- if (!surface.has(name))
443
- surface.set(name, text);
441
+ const offered = entries.flatMap((entry) => [...exportsOf(files, entry.path, new Set)].map(([name, declaration]) => ({ declaration, name, specifier: entry.specifier })));
442
+ for (const { declaration, name, specifier } of offered) {
443
+ const known = surface.get(name);
444
+ if (known === undefined)
445
+ surface.set(name, { declaration, entries: [specifier] });
446
+ else if (!known.entries.includes(specifier))
447
+ known.entries.push(specifier);
448
+ }
449
+ for (const value of surface.values())
450
+ value.entries.sort();
444
451
  return surface;
445
452
  };
446
- var typeCandidatesIn = (value) => {
453
+ var exportEntries = (value, packageName, specifier) => {
454
+ const here = specifier ?? packageName;
447
455
  if (typeof value === "string")
448
- return [value];
456
+ return [{ specifier: here, value }];
449
457
  if (typeof value !== "object" || value === null)
450
458
  return [];
451
- return Object.entries(value).flatMap(([key, nested]) => key === "types" || key === "typings" ? typeCandidatesIn(nested) : typeCandidatesIn(nested));
459
+ return Object.entries(value).flatMap(([key, nested]) => key.startsWith(".") ? exportEntries(nested, packageName, key === "." ? packageName : `${packageName}${key.slice(1)}`) : exportEntries(nested, packageName, here));
452
460
  };
453
461
  var typeEntriesOf = (manifest, files) => {
454
462
  const record = typeof manifest === "object" && manifest !== null ? new Map(Object.entries(manifest)) : new Map;
455
- const claimed = [
456
- record.get("types"),
457
- record.get("typings"),
458
- ...typeCandidatesIn(record.get("exports"))
463
+ const named = record.get("name");
464
+ const packageName = typeof named === "string" ? named : "";
465
+ const resolved = (value, specifier) => {
466
+ if (typeof value !== "string")
467
+ return [];
468
+ const cleaned = value.replace(/^\.\//u, "");
469
+ const candidates = DECLARATION.test(cleaned) ? [cleaned] : [
470
+ cleaned.replace(/\.[cm]?js$/u, ".d.ts"),
471
+ cleaned.replace(/\.[cm]?js$/u, ".d.mts"),
472
+ cleaned.replace(/\.[cm]?js$/u, ".d.cts")
473
+ ];
474
+ return candidates.filter((candidate) => files.has(candidate)).map((resolvedPath) => ({ path: resolvedPath, specifier }));
475
+ };
476
+ const found = [
477
+ ...resolved(record.get("types"), packageName),
478
+ ...resolved(record.get("typings"), packageName),
479
+ ...exportEntries(record.get("exports"), packageName).flatMap((entry) => resolved(entry.value, entry.specifier))
459
480
  ];
460
- const entries = claimed.filter((value) => typeof value === "string").map((value) => value.replace(/^\.\//u, "")).flatMap((cleaned) => DECLARATION.test(cleaned) ? [cleaned] : [
461
- cleaned.replace(/\.[cm]?js$/u, ".d.ts"),
462
- cleaned.replace(/\.[cm]?js$/u, ".d.mts"),
463
- cleaned.replace(/\.[cm]?js$/u, ".d.cts")
464
- ]).filter((candidate) => files.has(candidate));
465
- const fallback = entries.length === 0 && files.has("dist/index.d.ts") ? ["dist/index.d.ts"] : [];
466
- return [...new Set([...entries, ...fallback])];
481
+ const fallback = found.length === 0 && files.has("dist/index.d.ts") ? [{ path: "dist/index.d.ts", specifier: packageName }] : [];
482
+ const seen = new Set;
483
+ return [...found, ...fallback].filter((entry) => {
484
+ const key = `${entry.specifier}\x00${entry.path}`;
485
+ if (seen.has(key))
486
+ return false;
487
+ seen.add(key);
488
+ return true;
489
+ });
467
490
  };
468
491
  var SHAPE = {
469
492
  members: "members",
@@ -518,23 +541,33 @@ var changeShape = (change) => {
518
541
  };
519
542
  var diffSurface = (before, after) => {
520
543
  const removed = [];
544
+ const withdrawn = [];
521
545
  const changed = [];
522
- for (const [name, declaration] of before) {
546
+ for (const [name, was] of before) {
523
547
  const now = after.get(name);
524
548
  if (now === undefined)
525
549
  removed.push(name);
526
- else if (now !== declaration)
550
+ if (now === undefined)
551
+ continue;
552
+ const lost = was.entries.filter((entry) => !now.entries.includes(entry));
553
+ if (lost.length > 0)
554
+ withdrawn.push({ from: lost, name });
555
+ if (now.declaration !== was.declaration)
527
556
  changed.push({
528
- after: now,
529
- before: declaration,
557
+ after: now.declaration,
558
+ before: was.declaration,
530
559
  name,
531
- shape: changeShape({ after: now, before: declaration })
560
+ shape: changeShape({
561
+ after: now.declaration,
562
+ before: was.declaration
563
+ })
532
564
  });
533
565
  }
534
566
  return {
535
567
  added: [...after.keys()].filter((name) => !before.has(name)),
536
568
  changed,
537
- removed
569
+ removed,
570
+ withdrawn
538
571
  };
539
572
  };
540
573
  var notable = (diff) => diff.changed.filter((change) => change.shape === "parameters" || change.shape === "members");
@@ -670,10 +703,11 @@ var describeReconciliation = (found) => {
670
703
  members: "changed the members of its type",
671
704
  parameters: "changed what it is called with",
672
705
  removed: "is no longer exported",
673
- types: "changed its declaration"
706
+ types: "changed its declaration",
707
+ withdrawn: "is no longer exported from"
674
708
  };
675
709
  for (const entry of found.unannounced)
676
- lines.push(`\`${entry.name}\` ${moved[entry.kind]} and no entry says so`);
710
+ 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`);
677
711
  for (const name of found.unknownSymbols)
678
712
  lines.push(`an entry names \`${name}\`, which this package does not export`);
679
713
  lines.push(...found.wrongMigrations);
@@ -684,6 +718,7 @@ var reconcile = (input) => {
684
718
  const mentioned = new Set(input.changes.flatMap((change) => [...change.symbols ?? []]));
685
719
  const unannounced = [
686
720
  ...input.diff.removed.filter((name) => !disruptive.has(name)).map((name) => ({ kind: "removed", name })),
721
+ ...input.diff.withdrawn.filter((entry) => !disruptive.has(entry.name)).map((entry) => ({ from: entry.from, kind: "withdrawn", name: entry.name })),
687
722
  ...input.diff.changed.filter((change) => !mentioned.has(change.name) && change.shape !== "unknown").map((change) => ({ kind: change.shape, name: change.name }))
688
723
  ];
689
724
  const unknownSymbols = [
@@ -709,6 +744,8 @@ var MAX_ARCHIVE_BYTES = MAX_ARCHIVE_MEBIBYTES * MEBIBYTE2;
709
744
  var MAX_DOWNLOAD_MEBIBYTES = 32;
710
745
  var MAX_DOWNLOAD_BYTES = MAX_DOWNLOAD_MEBIBYTES * MEBIBYTE2;
711
746
  var TIMEOUT_MS = 20000;
747
+ var NOT_FOUND = 404;
748
+ var NOTHING_PUBLISHED = { published: [], tarballs: {} };
712
749
  var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
713
750
  var MAX_NAME_LENGTH = 214;
714
751
  var isPackageName = (name) => name.length <= MAX_NAME_LENGTH && PACKAGE_NAME.test(name);
@@ -752,9 +789,25 @@ var packageArchive = async (name, version, options = {}) => {
752
789
  return;
753
790
  }
754
791
  };
792
+ var packuments = new Map;
793
+ var SECONDS_PER_MINUTE = 60;
794
+ var MILLISECONDS_PER_SECOND = 1000;
795
+ var PACKUMENT_MINUTES = 5;
796
+ var PACKUMENT_TTL_MS = PACKUMENT_MINUTES * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
797
+ var forgetRegistry = () => packuments.clear();
755
798
  var packageVersions = async (name, options = {}) => {
756
799
  if (!isPackageName(name))
757
800
  return;
801
+ const cacheable = options.fetch === undefined && options.registry === undefined;
802
+ const known = cacheable ? packuments.get(name) : undefined;
803
+ if (known !== undefined && Date.now() - known.asked < PACKUMENT_TTL_MS)
804
+ return known.value;
805
+ const value = askRegistry(name, options);
806
+ if (cacheable)
807
+ packuments.set(name, { asked: Date.now(), value });
808
+ return value;
809
+ };
810
+ var askRegistry = async (name, options) => {
758
811
  const call = options.fetch ?? fetch;
759
812
  const url = `${options.registry ?? DEFAULT_REGISTRY}/${name.replace("/", "%2f")}`;
760
813
  try {
@@ -763,9 +816,9 @@ var packageVersions = async (name, options = {}) => {
763
816
  signal: AbortSignal.timeout(TIMEOUT_MS)
764
817
  });
765
818
  const response = abbreviated.ok ? abbreviated : await call(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
766
- if (!response.ok)
767
- return;
768
- return readPackument(await response.json());
819
+ if (response.ok)
820
+ return readPackument(await response.json());
821
+ return response.status === NOT_FOUND ? NOTHING_PUBLISHED : undefined;
769
822
  } catch {
770
823
  return;
771
824
  }
@@ -1090,8 +1143,8 @@ var check = async (root, flags) => {
1090
1143
  });
1091
1144
  if (actual !== expected)
1092
1145
  failures.push(`${CHANGELOG_MARKDOWN} is not what the entries say it should be \u2014 it is generated; run \`absolute-changelog release\` or restore it`);
1093
- const tsconfig = await readJsonFile(join2(root, "tsconfig.json"));
1094
- const include = isPackage(tsconfig) ? tsconfig.include : undefined;
1146
+ const tsconfig = await readTsconfig(join2(root, "tsconfig.json"));
1147
+ const include = includeOf(tsconfig?.config);
1095
1148
  if (Array.isArray(include) && !coversEntries(include))
1096
1149
  failures.push('tsconfig.json does not compile changelog/, so nothing is checking the entries \u2014 add "changelog/**/*" to its include list');
1097
1150
  for (const stray of await strayEntries(root))
@@ -1110,9 +1163,11 @@ var check = async (root, flags) => {
1110
1163
  const versions = offline ? undefined : await packageVersions(manifest.name ?? "");
1111
1164
  if (!offline && versions === undefined)
1112
1165
  failures.push(`the registry did not answer for ${manifest.name}, so nothing was compared \u2014 pass --offline to skip the comparison deliberately`);
1166
+ if (versions !== undefined && versions.published.length === 0)
1167
+ console.warn(` (${manifest.name} has never been published, so there was nothing to compare)`);
1113
1168
  const baseline = (versions?.published ?? []).filter((version) => compareVersions(version, manifest.version) <= 0).sort(compareVersions).at(LAST2);
1114
- if (versions !== undefined && baseline === undefined && !offline)
1115
- console.warn(` (nothing published at or below ${manifest.version}; this looks like a first release)`);
1169
+ if (versions !== undefined && versions.published.length > 0 && baseline === undefined && !offline)
1170
+ console.warn(` (nothing published at or below ${manifest.version}, so there was nothing to compare)`);
1116
1171
  if (built !== undefined && baseline !== undefined) {
1117
1172
  const published = await publishedSurface(manifest.name ?? "", baseline);
1118
1173
  if (published === undefined)
@@ -1142,18 +1197,35 @@ var subpathsOf = (manifest) => {
1142
1197
  return new Set(Object.keys(exports).map((key) => key === "." ? manifest.name ?? "" : `${manifest.name}${key.slice(1)}`));
1143
1198
  };
1144
1199
  var CHECKING_CONFIGS = ["tsconfig.json", "tsconfig.eslint.json"];
1200
+ var readTsconfig = async (path2) => {
1201
+ const source = await readFile3(path2, "utf8").catch(() => {
1202
+ return;
1203
+ });
1204
+ if (source === undefined)
1205
+ return;
1206
+ const read = ts2.parseConfigFileTextToJson(path2, source);
1207
+ if (read.error !== undefined)
1208
+ return;
1209
+ const { config } = read;
1210
+ const parsed = config;
1211
+ return { config: parsed, source };
1212
+ };
1145
1213
  var coversEntries = (include) => Array.isArray(include) && include.some((entry) => typeof entry === "string" && entry.startsWith("changelog"));
1214
+ var includeOf = (config) => typeof config === "object" && config !== null ? new Map(Object.entries(config)).get("include") : undefined;
1146
1215
  var includeEntriesIn = async (root, file) => {
1147
1216
  const path2 = join2(root, file);
1148
- const document = await readJsonFile(path2);
1149
- if (!isPackage(document) || !Array.isArray(document.include))
1217
+ const read = await readTsconfig(path2);
1218
+ if (read === undefined)
1150
1219
  return false;
1151
- if (coversEntries(document.include))
1220
+ const include = includeOf(read.config);
1221
+ if (!Array.isArray(include) || coversEntries(include))
1152
1222
  return false;
1153
- await writeJsonLike(path2, {
1154
- ...document,
1155
- include: [...document.include, "changelog/**/*"]
1156
- });
1223
+ const opened = /"include"\s*:\s*\[/u.exec(read.source);
1224
+ if (opened === null)
1225
+ return false;
1226
+ const opens = opened.index + opened[0].length;
1227
+ await writeFile2(path2, `${read.source.slice(0, opens)}
1228
+ "changelog/**/*",${read.source.slice(opens)}`);
1157
1229
  return true;
1158
1230
  };
1159
1231
  var includeEntries = async (root) => {
@@ -1231,5 +1303,5 @@ await main().catch((error) => {
1231
1303
  process.exit(1);
1232
1304
  });
1233
1305
 
1234
- //# debugId=0E6F1175A8DDD6A464756E2164756E21
1306
+ //# debugId=220FF2C6ABB0241464756E2164756E21
1235
1307
  //# sourceMappingURL=cli.js.map