@absolutejs/changelog 0.1.2 → 0.2.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 +15 -0
- package/changelog.json +97 -40
- package/dist/cli.js +101 -15
- package/dist/cli.js.map +7 -7
- package/dist/entries.d.ts +8 -0
- package/dist/index.js +51 -8
- package/dist/index.js.map +6 -6
- package/dist/read.d.ts +1 -1
- package/dist/reconcile.d.ts +11 -0
- package/dist/types.d.ts +41 -9
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ 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.2.0 — 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Adopting writes a prepublishOnly gate, so no publish can go round the check**
|
|
14
|
+
- **Entries can be written against the package's own API, so a symbol that is not an export is a compile error** (`Change`, `change`)
|
|
15
|
+
- **Reconciliation reports migrations that do not describe what the package did** (`Reconciliation`)
|
|
16
|
+
- **The gate catches a package that stopped shipping the documents, and files in the entry directory that no release will read** (`strayEntries`)
|
|
17
|
+
- **The gate checks that a migration describes what the package actually did** (`reconcile`)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Compare against the newest published version, not the one being published** (`packageVersions`)
|
|
22
|
+
- **Read a document written against an older contract rather than refusing it** (`parseChangelog`)
|
|
23
|
+
|
|
9
24
|
## 0.1.2 — 2026-09-10
|
|
10
25
|
|
|
11
26
|
### Fixed
|
package/changelog.json
CHANGED
|
@@ -1,42 +1,99 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
2
|
+
"contract": 1,
|
|
3
|
+
"name": "@absolutejs/changelog",
|
|
4
|
+
"releases": [
|
|
5
|
+
{
|
|
6
|
+
"changes": [
|
|
7
|
+
{
|
|
8
|
+
"kind": "added",
|
|
9
|
+
"summary": "Adopting writes a prepublishOnly gate, so no publish can go round the check"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "fixed",
|
|
13
|
+
"summary": "Compare against the newest published version, not the one being published",
|
|
14
|
+
"symbols": [
|
|
15
|
+
"packageVersions"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"kind": "added",
|
|
20
|
+
"summary": "Entries can be written against the package's own API, so a symbol that is not an export is a compile error",
|
|
21
|
+
"symbols": [
|
|
22
|
+
"Change",
|
|
23
|
+
"change"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "fixed",
|
|
28
|
+
"summary": "Read a document written against an older contract rather than refusing it",
|
|
29
|
+
"symbols": [
|
|
30
|
+
"parseChangelog"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"kind": "added",
|
|
35
|
+
"summary": "Reconciliation reports migrations that do not describe what the package did",
|
|
36
|
+
"symbols": [
|
|
37
|
+
"Reconciliation"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"kind": "added",
|
|
42
|
+
"summary": "The gate catches a package that stopped shipping the documents, and files in the entry directory that no release will read",
|
|
43
|
+
"symbols": [
|
|
44
|
+
"strayEntries"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"kind": "added",
|
|
49
|
+
"summary": "The gate checks that a migration describes what the package actually did",
|
|
50
|
+
"symbols": [
|
|
51
|
+
"reconcile"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"date": "2026-09-10",
|
|
56
|
+
"version": "0.2.0"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"changes": [
|
|
60
|
+
{
|
|
61
|
+
"kind": "fixed",
|
|
62
|
+
"summary": "Keep each package.json's own indentation when writing it back",
|
|
63
|
+
"symbols": [
|
|
64
|
+
"writeJsonLike"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"date": "2026-09-10",
|
|
69
|
+
"version": "0.1.2"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"changes": [
|
|
73
|
+
{
|
|
74
|
+
"kind": "fixed",
|
|
75
|
+
"summary": "Fall back to the full packument when the registry has not built the abbreviated one yet",
|
|
76
|
+
"symbols": [
|
|
77
|
+
"packageVersions"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"kind": "fixed",
|
|
82
|
+
"summary": "Keep the formatter off the generated changelog copies when adopting"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"date": "2026-09-10",
|
|
86
|
+
"version": "0.1.1"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"changes": [
|
|
90
|
+
{
|
|
91
|
+
"kind": "added",
|
|
92
|
+
"summary": "The changelog contract: typed entries, a gate that checks them against the package's own types, and migrations as data"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"date": "2026-09-10",
|
|
96
|
+
"version": "0.1.0"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
42
99
|
}
|
package/dist/cli.js
CHANGED
|
@@ -235,8 +235,8 @@ var parseChangelog = (value) => {
|
|
|
235
235
|
return { problems: ["not an object"] };
|
|
236
236
|
if (!isText(value.name))
|
|
237
237
|
problems.push("needs a package name");
|
|
238
|
-
if (value.contract !== CHANGELOG_CONTRACT)
|
|
239
|
-
problems.push(`contract ${JSON.stringify(value.contract)} is
|
|
238
|
+
if (typeof value.contract !== "number" || value.contract > CHANGELOG_CONTRACT)
|
|
239
|
+
problems.push(`contract ${JSON.stringify(value.contract)} is newer than ${CHANGELOG_CONTRACT}, which this reader understands`);
|
|
240
240
|
const raw = value.releases;
|
|
241
241
|
if (!Array.isArray(raw))
|
|
242
242
|
problems.push("releases is not a list");
|
|
@@ -535,6 +535,10 @@ var readJsonFile = async (path) => {
|
|
|
535
535
|
return;
|
|
536
536
|
}
|
|
537
537
|
};
|
|
538
|
+
var strayEntries = async (packageRoot) => {
|
|
539
|
+
const names = await readdir(join(packageRoot, UNRELEASED_DIRECTORY)).catch(() => []);
|
|
540
|
+
return names.filter((name) => !ENTRY.test(name) && name !== ".gitkeep" && !name.startsWith("."));
|
|
541
|
+
};
|
|
538
542
|
|
|
539
543
|
// src/json.ts
|
|
540
544
|
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
@@ -549,27 +553,66 @@ var writeJsonLike = async (path, document) => {
|
|
|
549
553
|
};
|
|
550
554
|
|
|
551
555
|
// src/reconcile.ts
|
|
556
|
+
var migrationHolds = (input) => {
|
|
557
|
+
const { after, change, subpaths } = input;
|
|
558
|
+
const wrong = [];
|
|
559
|
+
const { migration } = change;
|
|
560
|
+
if ("rename" in migration) {
|
|
561
|
+
const { from } = migration.rename;
|
|
562
|
+
const into = migration.rename.to;
|
|
563
|
+
if (!after.has(into))
|
|
564
|
+
wrong.push(`\`${change.summary}\` renames to \`${into}\`, which this version does not export`);
|
|
565
|
+
if (after.has(from))
|
|
566
|
+
wrong.push(`\`${change.summary}\` renames \`${from}\` away, but this version still exports it`);
|
|
567
|
+
if (!change.symbols.includes(from))
|
|
568
|
+
wrong.push(`\`${change.summary}\` renames \`${from}\`, which is not in its own symbols`);
|
|
569
|
+
}
|
|
570
|
+
if ("moved" in migration) {
|
|
571
|
+
const { symbol } = migration.moved;
|
|
572
|
+
const into = migration.moved.to;
|
|
573
|
+
if (!after.has(symbol))
|
|
574
|
+
wrong.push(`\`${change.summary}\` moves \`${symbol}\`, which this version does not export`);
|
|
575
|
+
if (subpaths.size > 0 && !subpaths.has(into))
|
|
576
|
+
wrong.push(`\`${change.summary}\` moves \`${symbol}\` to \`${into}\`, which is not an entry point of this package`);
|
|
577
|
+
}
|
|
578
|
+
if ("resubpath" in migration && subpaths.size > 0 && !subpaths.has(migration.resubpath.to))
|
|
579
|
+
wrong.push(`\`${change.summary}\` points at \`${migration.resubpath.to}\`, which is not an entry point of this package`);
|
|
580
|
+
if (change.kind === "removed") {
|
|
581
|
+
for (const symbol of change.symbols)
|
|
582
|
+
if (after.has(symbol))
|
|
583
|
+
wrong.push(`\`${change.summary}\` says \`${symbol}\` was removed, but this version still exports it`);
|
|
584
|
+
}
|
|
585
|
+
return wrong;
|
|
586
|
+
};
|
|
552
587
|
var describeReconciliation = (found) => {
|
|
553
588
|
const lines = [];
|
|
554
589
|
for (const entry of found.unannounced)
|
|
555
590
|
lines.push(entry.kind === "removed" ? `\`${entry.name}\` is no longer exported and no entry says so` : `\`${entry.name}\` changed its ${entry.kind} and no entry says so`);
|
|
556
591
|
for (const name of found.unknownSymbols)
|
|
557
592
|
lines.push(`an entry names \`${name}\`, which this package does not export`);
|
|
593
|
+
lines.push(...found.wrongMigrations);
|
|
558
594
|
return lines;
|
|
559
595
|
};
|
|
560
596
|
var reconcile = (input) => {
|
|
561
|
-
const
|
|
597
|
+
const disruptive = new Set(input.changes.filter(isDisruptive).flatMap((change) => [...change.symbols]));
|
|
598
|
+
const mentioned = new Set(input.changes.flatMap((change) => [...change.symbols ?? []]));
|
|
562
599
|
const unannounced = [
|
|
563
|
-
...input.diff.removed.map((name) => ({ kind: "removed", name })),
|
|
564
|
-
...notable(input.diff).map((change) => ({
|
|
600
|
+
...input.diff.removed.filter((name) => !disruptive.has(name)).map((name) => ({ kind: "removed", name })),
|
|
601
|
+
...notable(input.diff).filter((change) => !mentioned.has(change.name)).map((change) => ({
|
|
565
602
|
kind: change.shape === "parameters" ? "parameters" : "members",
|
|
566
603
|
name: change.name
|
|
567
604
|
}))
|
|
568
|
-
]
|
|
605
|
+
];
|
|
569
606
|
const unknownSymbols = [
|
|
570
607
|
...new Set(input.changes.flatMap((change) => [...change.symbols ?? []]).filter((name) => !input.known.has(name)))
|
|
571
608
|
];
|
|
572
|
-
|
|
609
|
+
const { after } = input;
|
|
610
|
+
const wrongMigrations = after === undefined ? [] : input.changes.filter(isDisruptive).flatMap((change) => migrationHolds({
|
|
611
|
+
after,
|
|
612
|
+
change,
|
|
613
|
+
subpaths: input.subpaths ?? new Set
|
|
614
|
+
}));
|
|
615
|
+
return { unannounced, unknownSymbols, wrongMigrations };
|
|
573
616
|
};
|
|
574
617
|
|
|
575
618
|
// src/registry.ts
|
|
@@ -820,6 +863,12 @@ var writeBoth = async (root, changelog) => {
|
|
|
820
863
|
await writeFile2(paths.markdown, renderMarkdown(changelog, history === undefined ? {} : { history }));
|
|
821
864
|
};
|
|
822
865
|
var slug = (summary) => summary.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, SLUG_MAX_LENGTH) || "change";
|
|
866
|
+
var sourceEntryOf = (root) => {
|
|
867
|
+
for (const candidate of ["src/index.ts", "src/index.tsx", "index.ts"])
|
|
868
|
+
if (existsSync(join2(root, candidate)))
|
|
869
|
+
return `../../${candidate.replace(/\.tsx?$/u, "")}`;
|
|
870
|
+
return;
|
|
871
|
+
};
|
|
823
872
|
var add = async (root, flags) => {
|
|
824
873
|
const kind = text(flags, "kind");
|
|
825
874
|
const summary = text(flags, "summary");
|
|
@@ -843,10 +892,12 @@ var add = async (root, flags) => {
|
|
|
843
892
|
] : [
|
|
844
893
|
` migration: { instruction: ${JSON.stringify(instruction)}, manual: true },`
|
|
845
894
|
];
|
|
895
|
+
const api = sourceEntryOf(root);
|
|
846
896
|
const body = [
|
|
897
|
+
...api === undefined ? [] : [`import type * as Api from '${api}';`],
|
|
847
898
|
"import type { Change } from '@absolutejs/changelog';",
|
|
848
899
|
"",
|
|
849
|
-
|
|
900
|
+
`export const change: Change${api === undefined ? "" : "<typeof Api>"} = {`,
|
|
850
901
|
` kind: ${JSON.stringify(kind)},`,
|
|
851
902
|
...disruptive ? migration : [],
|
|
852
903
|
` summary: ${JSON.stringify(summary)}${symbols.length > 0 ? "," : ""}`,
|
|
@@ -939,23 +990,46 @@ var check = async (root, flags) => {
|
|
|
939
990
|
const include = isPackage(tsconfig) ? tsconfig.include : undefined;
|
|
940
991
|
if (Array.isArray(include) && !coversEntries(include))
|
|
941
992
|
failures.push('tsconfig.json does not compile changelog/, so nothing is checking the entries \u2014 add "changelog/**/*" to its include list');
|
|
993
|
+
for (const stray of await strayEntries(root))
|
|
994
|
+
failures.push(`${UNRELEASED_DIRECTORY}/${stray} is not an entry, so no release will ever read it`);
|
|
995
|
+
const shipped = Array.isArray(manifest.files) ? manifest.files : [];
|
|
996
|
+
for (const file of [CHANGELOG_FILE, CHANGELOG_MARKDOWN])
|
|
997
|
+
if (shipped.length > 0 && !shipped.includes(file))
|
|
998
|
+
failures.push(`package.json does not ship ${file}`);
|
|
942
999
|
const offline = flags.offline === true;
|
|
943
1000
|
const built = await builtSurface(root);
|
|
944
|
-
|
|
945
|
-
|
|
1001
|
+
const versions = offline ? undefined : await packageVersions(manifest.name ?? "");
|
|
1002
|
+
const baseline = (versions?.published ?? []).filter((version) => compareVersions(version, manifest.version) <= 0).sort(compareVersions).at(LAST3);
|
|
1003
|
+
if (!offline && built !== undefined && baseline !== undefined) {
|
|
1004
|
+
const published = await publishedSurface(manifest.name ?? "", baseline);
|
|
946
1005
|
if (published === undefined)
|
|
947
|
-
console.warn(` (could not read ${manifest.name}@${
|
|
1006
|
+
console.warn(` (could not read ${manifest.name}@${baseline} from the registry; the types were not checked)`);
|
|
1007
|
+
const speaking = [
|
|
1008
|
+
...entries.map((entry) => entry.change),
|
|
1009
|
+
...current.releases.filter((entry) => compareVersions(entry.version, baseline) > 0).flatMap((entry) => [...entry.changes])
|
|
1010
|
+
];
|
|
948
1011
|
failures.push(...published === undefined ? [] : describeReconciliation(reconcile({
|
|
949
|
-
|
|
1012
|
+
after: new Set(built.keys()),
|
|
1013
|
+
changes: speaking,
|
|
950
1014
|
diff: diffSurface(published, built),
|
|
951
|
-
known: new Set([...published.keys(), ...built.keys()])
|
|
1015
|
+
known: new Set([...published.keys(), ...built.keys()]),
|
|
1016
|
+
subpaths: subpathsOf(manifest)
|
|
952
1017
|
})));
|
|
953
1018
|
}
|
|
1019
|
+
if (!offline && built === undefined)
|
|
1020
|
+
console.warn(" (no dist to read, so the types were not checked \u2014 run the build before the gate)");
|
|
954
1021
|
if (failures.length > 0)
|
|
955
1022
|
throw new CliError(["This package\u2019s changelog does not hold:", ...failures].join(`
|
|
956
1023
|
`));
|
|
957
1024
|
console.log(`${manifest.name}@${manifest.version}: changelog holds (${entries.length} pending, ${current.releases.length} released)`);
|
|
958
1025
|
};
|
|
1026
|
+
var LAST3 = -1;
|
|
1027
|
+
var subpathsOf = (manifest) => {
|
|
1028
|
+
const { exports } = manifest;
|
|
1029
|
+
if (typeof exports !== "object" || exports === null)
|
|
1030
|
+
return new Set;
|
|
1031
|
+
return new Set(Object.keys(exports).map((key) => key === "." ? manifest.name ?? "" : `${manifest.name}${key.slice(1)}`));
|
|
1032
|
+
};
|
|
959
1033
|
var CHECKING_CONFIGS = ["tsconfig.json", "tsconfig.eslint.json"];
|
|
960
1034
|
var coversEntries = (include) => Array.isArray(include) && include.some((entry) => typeof entry === "string" && entry.startsWith("changelog"));
|
|
961
1035
|
var includeEntriesIn = async (root, file) => {
|
|
@@ -989,6 +1063,16 @@ var ignoreGenerated = async (root) => {
|
|
|
989
1063
|
`)}
|
|
990
1064
|
`);
|
|
991
1065
|
};
|
|
1066
|
+
var PUBLISH_GATE = "absolute-changelog check";
|
|
1067
|
+
var withPublishGate = (scripts) => {
|
|
1068
|
+
const existing = scripts.prepublishOnly;
|
|
1069
|
+
if (typeof existing === "string" && existing.includes(PUBLISH_GATE))
|
|
1070
|
+
return;
|
|
1071
|
+
return {
|
|
1072
|
+
...scripts,
|
|
1073
|
+
prepublishOnly: typeof existing === "string" && existing.trim() !== "" ? `${existing} && ${PUBLISH_GATE}` : PUBLISH_GATE
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
992
1076
|
var adopt = async (root) => {
|
|
993
1077
|
const manifest = await readPackage(root);
|
|
994
1078
|
const paths = changelogPaths(root);
|
|
@@ -1002,9 +1086,11 @@ var adopt = async (root) => {
|
|
|
1002
1086
|
}
|
|
1003
1087
|
const files = manifest.files ?? [];
|
|
1004
1088
|
const wanted = [CHANGELOG_FILE, CHANGELOG_MARKDOWN].filter((entry) => !files.includes(entry));
|
|
1005
|
-
|
|
1089
|
+
const scripts = typeof manifest.scripts === "object" && manifest.scripts !== null ? withPublishGate(manifest.scripts) : undefined;
|
|
1090
|
+
if (wanted.length > 0 || scripts !== undefined)
|
|
1006
1091
|
await writeJsonLike(join2(root, "package.json"), {
|
|
1007
1092
|
...manifest,
|
|
1093
|
+
...scripts === undefined ? {} : { scripts },
|
|
1008
1094
|
files: [...files, ...wanted].sort()
|
|
1009
1095
|
});
|
|
1010
1096
|
await ignoreGenerated(root);
|
|
@@ -1034,5 +1120,5 @@ await main().catch((error) => {
|
|
|
1034
1120
|
process.exit(1);
|
|
1035
1121
|
});
|
|
1036
1122
|
|
|
1037
|
-
//# debugId=
|
|
1123
|
+
//# debugId=7CE32A12E2B0BC0C64756E2164756E21
|
|
1038
1124
|
//# sourceMappingURL=cli.js.map
|