@absolutejs/changelog 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,1007 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/cli.ts
5
+ import { existsSync } from "fs";
6
+ import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
7
+ import { join as join2 } from "path";
8
+
9
+ // src/entries.ts
10
+ import { readdir, readFile } from "fs/promises";
11
+ import { join } from "path";
12
+ import { pathToFileURL } from "url";
13
+
14
+ // src/types.ts
15
+ var CHANGE_KINDS = [
16
+ "breaking",
17
+ "removed",
18
+ "added",
19
+ "changed",
20
+ "deprecated",
21
+ "fixed",
22
+ "security",
23
+ "internal"
24
+ ];
25
+ var DISRUPTIVE_KINDS = ["breaking", "removed"];
26
+ var CHANGELOG_CONTRACT = 1;
27
+ var CHANGELOG_FILE = "changelog.json";
28
+ var CHANGELOG_MARKDOWN = "CHANGELOG.md";
29
+ var UNRELEASED_DIRECTORY = "changelog/unreleased";
30
+ var isDisruptive = (change) => change.kind === "breaking" || change.kind === "removed";
31
+ var isMechanical = (migration) => !("manual" in migration);
32
+
33
+ // src/versioning.ts
34
+ var OLDER = -1;
35
+ var LAST = -1;
36
+ var NEWER = 1;
37
+ var VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
38
+ var parseVersion = (version) => {
39
+ const found = VERSION.exec(version.trim());
40
+ if (!found)
41
+ return;
42
+ const [, major, minor, patch, prerelease] = found;
43
+ return {
44
+ prerelease: prerelease === undefined ? [] : prerelease.split("."),
45
+ release: [Number(major), Number(minor), Number(patch)]
46
+ };
47
+ };
48
+ var isNumeric = (value) => /^\d+$/u.test(value);
49
+ var comparePrerelease = (left, right) => {
50
+ if (left.length === 0 || right.length === 0)
51
+ return right.length - left.length;
52
+ for (const [index, mine] of left.entries()) {
53
+ const theirs = right[index];
54
+ if (theirs === undefined)
55
+ return NEWER;
56
+ if (mine === theirs)
57
+ continue;
58
+ if (isNumeric(mine) && isNumeric(theirs))
59
+ return Number(mine) - Number(theirs);
60
+ if (isNumeric(mine))
61
+ return OLDER;
62
+ if (isNumeric(theirs))
63
+ return NEWER;
64
+ return mine < theirs ? OLDER : NEWER;
65
+ }
66
+ return left.length - right.length;
67
+ };
68
+ var compareVersions = (left, right) => {
69
+ const mine = parseVersion(left);
70
+ const theirs = parseVersion(right);
71
+ if (!mine || !theirs)
72
+ return 0;
73
+ for (const [index, part] of mine.release.entries()) {
74
+ const other = theirs.release[index] ?? 0;
75
+ if (part !== other)
76
+ return part - other;
77
+ }
78
+ return comparePrerelease(mine.prerelease, theirs.prerelease);
79
+ };
80
+ var isPrerelease = (version) => (parseVersion(version)?.prerelease.length ?? 0) > 0;
81
+ var releaseKindFor = (changes) => {
82
+ if (changes.some(isDisruptive)) {
83
+ const major = "major";
84
+ return major;
85
+ }
86
+ const asked = changes.some((entry) => entry.kind === "added") ? "minor" : "patch";
87
+ return asked;
88
+ };
89
+ var bumpPrerelease = (parsed) => {
90
+ const { prerelease, release } = parsed;
91
+ const last = prerelease.at(LAST);
92
+ const counted = last !== undefined && isNumeric(last) ? [...prerelease.slice(0, LAST), String(Number(last) + 1)] : [...prerelease, "1"];
93
+ return `${release.join(".")}-${counted.join(".")}`;
94
+ };
95
+ var nextVersion = (input) => {
96
+ const parsed = parseVersion(input.current);
97
+ if (!parsed)
98
+ return;
99
+ const [major, minor, patch] = parsed.release;
100
+ const kind = input.asked ?? (parsed.prerelease.length > 0 ? "prerelease" : releaseKindFor(input.changes));
101
+ if (kind === "prerelease")
102
+ return parsed.prerelease.length > 0 ? bumpPrerelease(parsed) : `${major}.${minor}.${patch + 1}-${input.prereleaseIdentifier ?? "beta"}.1`;
103
+ if (parsed.prerelease.length > 0 && kind === "patch")
104
+ return `${major}.${minor}.${patch}`;
105
+ if (kind === "major")
106
+ return major === 0 ? `${major}.${minor + 1}.0` : `${major + 1}.0.0`;
107
+ return kind === "minor" ? `${major}.${minor + 1}.0` : `${major}.${minor}.${patch + 1}`;
108
+ };
109
+
110
+ // src/read.ts
111
+ var isKind = (value) => typeof value === "string" && CHANGE_KINDS.some((known) => known === value);
112
+ var isDisruptiveKind = (kind) => kind === "breaking" || kind === "removed";
113
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
114
+ var isText = (value) => typeof value === "string" && value.trim().length > 0;
115
+ var textList = (value) => Array.isArray(value) && value.length > 0 && value.every(isText) ? value : undefined;
116
+ var readMigration = (value, where, problems) => {
117
+ if (!isRecord(value) || !isText(value.instruction)) {
118
+ problems.push(`${where}: migration needs an instruction`);
119
+ return;
120
+ }
121
+ const { instruction } = value;
122
+ const { rename } = value;
123
+ if (isRecord(rename) && isText(rename.from) && isText(rename.to))
124
+ return {
125
+ instruction,
126
+ rename: { from: rename.from, to: rename.to }
127
+ };
128
+ const { moved } = value;
129
+ if (isRecord(moved) && isText(moved.symbol) && isText(moved.from) && isText(moved.to))
130
+ return {
131
+ instruction,
132
+ moved: { from: moved.from, symbol: moved.symbol, to: moved.to }
133
+ };
134
+ const { resubpath } = value;
135
+ if (isRecord(resubpath) && isText(resubpath.from) && isText(resubpath.to))
136
+ return {
137
+ instruction,
138
+ resubpath: { from: resubpath.from, to: resubpath.to }
139
+ };
140
+ if (value.manual === true)
141
+ return { instruction, manual: true };
142
+ problems.push(`${where}: migration says neither rename, moved, resubpath nor manual`);
143
+ return;
144
+ };
145
+ var readChange = (value, where, problems) => {
146
+ if (!isRecord(value)) {
147
+ problems.push(`${where}: not an object`);
148
+ return;
149
+ }
150
+ const { kind } = value;
151
+ if (!isKind(kind)) {
152
+ problems.push(`${where}: unknown kind ${JSON.stringify(kind)}`);
153
+ return;
154
+ }
155
+ if (!isText(value.summary)) {
156
+ problems.push(`${where}: needs a summary`);
157
+ return;
158
+ }
159
+ const shared = {
160
+ ...isText(value.detail) ? { detail: value.detail } : {},
161
+ ...isText(value.reference) ? { reference: value.reference } : {},
162
+ summary: value.summary
163
+ };
164
+ const symbols = textList(value.symbols);
165
+ const [first, ...rest] = symbols ?? [];
166
+ if (kind === "breaking" || kind === "removed") {
167
+ const migration = readMigration(value.migration, where, problems);
168
+ if (first === undefined)
169
+ problems.push(`${where}: a ${kind} change must name symbols`);
170
+ if (!migration || first === undefined)
171
+ return;
172
+ const named = [first, ...rest];
173
+ const disruptive = {
174
+ ...shared,
175
+ kind,
176
+ migration,
177
+ symbols: named
178
+ };
179
+ return disruptive;
180
+ }
181
+ if (isDisruptiveKind(kind))
182
+ return;
183
+ const ordinary = {
184
+ ...shared,
185
+ kind,
186
+ ...symbols ? { symbols } : {}
187
+ };
188
+ return ordinary;
189
+ };
190
+ var readRelease = (value, where, problems) => {
191
+ if (!isRecord(value)) {
192
+ problems.push(`${where}: not an object`);
193
+ return;
194
+ }
195
+ if (!isText(value.version) || parseVersion(value.version) === undefined) {
196
+ problems.push(`${where}: version is not a version`);
197
+ return;
198
+ }
199
+ if (!isText(value.date)) {
200
+ problems.push(`${where}: needs a date`);
201
+ return;
202
+ }
203
+ const raw = value.changes;
204
+ if (!Array.isArray(raw) || raw.length === 0) {
205
+ problems.push(`${where}: a release with no changes is not a release`);
206
+ return;
207
+ }
208
+ const changes = raw.map((entry, index) => readChange(entry, `${where}.changes[${index}]`, problems)).filter((entry) => entry !== undefined);
209
+ if (changes.length !== raw.length)
210
+ return;
211
+ const [head, ...tail] = changes;
212
+ if (head === undefined)
213
+ return;
214
+ const release = {
215
+ changes: [head, ...tail],
216
+ date: value.date,
217
+ ...isText(value.note) ? { note: value.note } : {},
218
+ version: value.version
219
+ };
220
+ return release;
221
+ };
222
+ var validateChange = (value, where = "entry") => {
223
+ const problems = [];
224
+ const change = readChange(value, where, problems);
225
+ if (change === undefined) {
226
+ const failed = { problems };
227
+ return failed;
228
+ }
229
+ const read = { change };
230
+ return read;
231
+ };
232
+ var parseChangelog = (value) => {
233
+ const problems = [];
234
+ if (!isRecord(value))
235
+ return { problems: ["not an object"] };
236
+ if (!isText(value.name))
237
+ problems.push("needs a package name");
238
+ if (value.contract !== CHANGELOG_CONTRACT)
239
+ problems.push(`contract ${JSON.stringify(value.contract)} is not ${CHANGELOG_CONTRACT}`);
240
+ const raw = value.releases;
241
+ if (!Array.isArray(raw))
242
+ problems.push("releases is not a list");
243
+ const releases = (Array.isArray(raw) ? raw : []).map((entry, index) => readRelease(entry, `releases[${index}]`, problems)).filter((entry) => entry !== undefined);
244
+ if (problems.length > 0)
245
+ return { problems };
246
+ return {
247
+ changelog: {
248
+ contract: CHANGELOG_CONTRACT,
249
+ name: String(value.name),
250
+ releases: [...releases].sort((left, right) => compareVersions(right.version, left.version))
251
+ }
252
+ };
253
+ };
254
+ var releasesBetween = (changelog, from, upto) => [...changelog.releases].filter((release) => compareVersions(release.version, from) > 0 && compareVersions(release.version, upto) <= 0).sort((left, right) => compareVersions(left.version, right.version));
255
+ var changesBetween = (changelog, from, upto) => releasesBetween(changelog, from, upto).flatMap((release) => release.changes.map((change) => {
256
+ const found = { change, version: release.version };
257
+ return found;
258
+ }));
259
+ var isDisruptiveAt = (found) => isDisruptive(found.change);
260
+ var disruptiveBetween = (changelog, from, upto) => changesBetween(changelog, from, upto).filter(isDisruptiveAt);
261
+ var mechanicalMigrations = (migrations) => migrations.filter((found) => isMechanical(found.migration));
262
+ var migrationsBetween = (changelog, from, upto) => disruptiveBetween(changelog, from, upto).map(({ change, version }) => {
263
+ const found = {
264
+ migration: change.migration,
265
+ symbols: change.symbols,
266
+ version
267
+ };
268
+ return found;
269
+ });
270
+ var symbolsDisturbedBetween = (changelog, from, upto) => new Set(disruptiveBetween(changelog, from, upto).flatMap(({ change }) => change.symbols));
271
+
272
+ // src/tarball.ts
273
+ var TAR_BLOCK = 512;
274
+ var NAME_LENGTH = 100;
275
+ var SIZE_OFFSET = 124;
276
+ var SIZE_LENGTH = 12;
277
+ var TYPE_OFFSET = 156;
278
+ var PREFIX_OFFSET = 345;
279
+ var PREFIX_LENGTH = 155;
280
+ var OCTAL = 8;
281
+ var KIBIBYTE = 1024;
282
+ var MEBIBYTE = KIBIBYTE * KIBIBYTE;
283
+ var DEFAULT_FILE_MEBIBYTES = 4;
284
+ var DEFAULT_TOTAL_MEBIBYTES = 64;
285
+ var DEFAULT_FILE_BYTES = DEFAULT_FILE_MEBIBYTES * MEBIBYTE;
286
+ var DEFAULT_TOTAL_BYTES = DEFAULT_TOTAL_MEBIBYTES * MEBIBYTE;
287
+ var decoder = new TextDecoder;
288
+ var cString = (bytes) => {
289
+ const end = bytes.indexOf(0);
290
+ return decoder.decode(end < 0 ? bytes : bytes.subarray(0, end)).trim();
291
+ };
292
+ var asText = (bytes) => decoder.decode(bytes);
293
+ var readTar = (archive, keep, limits = {}) => {
294
+ const maxFile = limits.maxFileBytes ?? DEFAULT_FILE_BYTES;
295
+ const maxTotal = limits.maxTotalBytes ?? DEFAULT_TOTAL_BYTES;
296
+ const files = new Map;
297
+ let offset = 0;
298
+ let total = 0;
299
+ while (offset + TAR_BLOCK <= archive.length) {
300
+ const header = archive.subarray(offset, offset + TAR_BLOCK);
301
+ const name = cString(header.subarray(0, NAME_LENGTH));
302
+ if (name === "")
303
+ break;
304
+ const prefix = cString(header.subarray(PREFIX_OFFSET, PREFIX_OFFSET + PREFIX_LENGTH));
305
+ const size = Number.parseInt(cString(header.subarray(SIZE_OFFSET, SIZE_OFFSET + SIZE_LENGTH)), OCTAL);
306
+ if (!Number.isSafeInteger(size) || size < 0)
307
+ break;
308
+ const kind = String.fromCharCode(header[TYPE_OFFSET] ?? 0);
309
+ const start = offset + TAR_BLOCK;
310
+ const path = (prefix === "" ? name : `${prefix}/${name}`).replace(/^package\//u, "");
311
+ const plain = kind === "0" || kind === "\x00";
312
+ const wanted = plain && size <= maxFile && keep(path);
313
+ total += wanted ? size : 0;
314
+ if (total > maxTotal)
315
+ break;
316
+ if (wanted)
317
+ files.set(path, archive.subarray(start, start + size));
318
+ offset = start + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
319
+ }
320
+ return files;
321
+ };
322
+
323
+ // src/surface.ts
324
+ var DECLARATION = /\bexport\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(function|const|let|var|class|interface|type|enum|namespace)\s+([A-Za-z_$][\w$]*)/gu;
325
+ var NAMED_EXPORT = /\bexport\s*\{([^}]*)\}/gu;
326
+ var BODIED = new Set(["class", "interface", "enum", "namespace"]);
327
+ var OPENS = new Set(["{", "(", "["]);
328
+ var CLOSES = new Set(["}", ")", "]"]);
329
+ var IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
330
+ var LAST2 = -1;
331
+ var NOWHERE = -1;
332
+ var MEMBER = /([A-Za-z_$][\w$]*)\s*[?!]?\s*[:(<]/u;
333
+ var isStub = (declaration) => declaration.startsWith("export {");
334
+ var declarationEnd = (source, from, bodied) => {
335
+ let depth = 0;
336
+ for (let index = from;index < source.length; index += 1) {
337
+ const character = source[index] ?? "";
338
+ if (OPENS.has(character))
339
+ depth += 1;
340
+ else if (CLOSES.has(character))
341
+ depth -= 1;
342
+ else if (character === ";" && depth <= 0)
343
+ return index + 1;
344
+ if (CLOSES.has(character) && depth <= 0 && bodied)
345
+ return index + 1;
346
+ }
347
+ return source.length;
348
+ };
349
+ var normalize = (declaration) => declaration.replace(/\s+/gu, " ").trim();
350
+ var reExported = (body) => body.split(",").map((part) => part.trim().replace(/^type\s+/u, "")).filter((clause) => clause !== "").map((clause) => [
351
+ clause.split(/\s+as\s+/u).at(LAST2)?.trim(),
352
+ clause
353
+ ]).filter((pair) => pair[0] !== undefined && IDENTIFIER.test(pair[0]));
354
+ var exportedDeclarations = (source) => {
355
+ const found = new Map;
356
+ const add = (name, declaration) => {
357
+ const existing = found.get(name);
358
+ found.set(name, existing === undefined ? declaration : `${existing}
359
+ ${declaration}`);
360
+ };
361
+ for (const match of source.matchAll(DECLARATION)) {
362
+ const [, keyword, name] = match;
363
+ if (name === undefined || match.index === undefined)
364
+ continue;
365
+ add(name, normalize(source.slice(match.index, declarationEnd(source, match.index + match[0].length, BODIED.has(keyword ?? "")))));
366
+ }
367
+ for (const match of source.matchAll(NAMED_EXPORT))
368
+ for (const [exported, clause] of reExported(match[1] ?? ""))
369
+ add(exported, `export { ${clause} }`);
370
+ return found;
371
+ };
372
+ var surfaceOfArchive = (archive) => surfaceOfFiles([...readTar(archive, (path) => path.endsWith(".d.ts")).values()].map(asText));
373
+ var surfaceOfFiles = (files) => {
374
+ const surface = new Map;
375
+ for (const source of files)
376
+ for (const [name, declaration] of exportedDeclarations(source)) {
377
+ const known = surface.get(name);
378
+ if (known === undefined || isStub(known) && !isStub(declaration))
379
+ surface.set(name, declaration);
380
+ }
381
+ return surface;
382
+ };
383
+ var SHAPE = {
384
+ members: "members",
385
+ parameters: "parameters",
386
+ types: "types",
387
+ unknown: "unknown"
388
+ };
389
+ var parametersOf = (declaration) => {
390
+ let depth = 0;
391
+ let start = NOWHERE;
392
+ for (const [index, character] of [...declaration].entries()) {
393
+ if (character === "{" && depth === 0)
394
+ return "";
395
+ depth += character === "(" ? 1 : 0;
396
+ if (character === "(" && depth === 1)
397
+ start = index;
398
+ depth -= character === ")" ? 1 : 0;
399
+ if (character === ")" && depth === 0 && start > NOWHERE)
400
+ return declaration.slice(start, index + 1);
401
+ }
402
+ return "";
403
+ };
404
+ var membersOf = (declaration) => {
405
+ const opened = declaration.indexOf("{");
406
+ if (opened < 0)
407
+ return new Set;
408
+ const members = new Set;
409
+ let depth = 0;
410
+ let segment = "";
411
+ const take = () => {
412
+ const [, name] = MEMBER.exec(segment.trim()) ?? [];
413
+ if (name !== undefined)
414
+ members.add(name);
415
+ segment = "";
416
+ };
417
+ for (const character of declaration.slice(opened)) {
418
+ const opening = OPENS.has(character);
419
+ const closing = CLOSES.has(character);
420
+ depth += opening ? 1 : 0;
421
+ if (opening && depth === 1)
422
+ segment = "";
423
+ depth -= closing ? 1 : 0;
424
+ if (opening || closing || depth !== 1)
425
+ continue;
426
+ if (character === ";" || character === ",")
427
+ take();
428
+ else
429
+ segment += character;
430
+ }
431
+ take();
432
+ return members;
433
+ };
434
+ var sameNames = (left, right) => left.size === right.size && [...left].every((name) => right.has(name));
435
+ var changeShape = (change) => {
436
+ if (isStub(change.before) !== isStub(change.after))
437
+ return SHAPE.unknown;
438
+ if (parametersOf(change.before) !== parametersOf(change.after))
439
+ return SHAPE.parameters;
440
+ if (!sameNames(membersOf(change.before), membersOf(change.after)))
441
+ return SHAPE.members;
442
+ return SHAPE.types;
443
+ };
444
+ var diffSurface = (before, after) => {
445
+ const removed = [];
446
+ const changed = [];
447
+ for (const [name, declaration] of before) {
448
+ const now = after.get(name);
449
+ if (now === undefined)
450
+ removed.push(name);
451
+ else if (now !== declaration)
452
+ changed.push({
453
+ after: now,
454
+ before: declaration,
455
+ name,
456
+ shape: changeShape({ after: now, before: declaration })
457
+ });
458
+ }
459
+ return {
460
+ added: [...after.keys()].filter((name) => !before.has(name)),
461
+ changed,
462
+ removed
463
+ };
464
+ };
465
+ var notable = (diff) => diff.changed.filter((change) => change.shape === "parameters" || change.shape === "members");
466
+
467
+ // src/entries.ts
468
+ var loadEntry = async (path) => {
469
+ try {
470
+ const module = await import(pathToFileURL(path).href);
471
+ const exported = typeof module === "object" && module !== null && "default" in module ? module.default : undefined;
472
+ const read = { exported };
473
+ return read;
474
+ } catch (error) {
475
+ const failed = {
476
+ failure: error instanceof Error ? error.message : String(error)
477
+ };
478
+ return failed;
479
+ }
480
+ };
481
+ var ENTRY = /\.[cm]?ts$/u;
482
+ var builtSurface = async (packageRoot, out = "dist") => {
483
+ const root = join(packageRoot, out);
484
+ const sources = [];
485
+ const walk = async (directory) => {
486
+ const found = await readdir(directory, { withFileTypes: true }).catch(() => []);
487
+ await Promise.all(found.map(async (entry) => {
488
+ const path = join(directory, entry.name);
489
+ if (entry.isDirectory())
490
+ return walk(path);
491
+ if (!entry.name.endsWith(".d.ts"))
492
+ return;
493
+ sources.push(await readFile(path, "utf8").catch(() => ""));
494
+ return;
495
+ }));
496
+ };
497
+ await walk(root);
498
+ return sources.length === 0 ? undefined : surfaceOfFiles(sources);
499
+ };
500
+ var changelogPaths = (packageRoot) => ({
501
+ history: join(packageRoot, "changelog", "history.md"),
502
+ json: join(packageRoot, CHANGELOG_FILE),
503
+ markdown: join(packageRoot, CHANGELOG_MARKDOWN),
504
+ unreleased: join(packageRoot, UNRELEASED_DIRECTORY)
505
+ });
506
+ var loadUnreleased = async (packageRoot) => {
507
+ const directory = join(packageRoot, UNRELEASED_DIRECTORY);
508
+ const names = await readdir(directory).catch(() => []);
509
+ const entries = [];
510
+ const problems = [];
511
+ for (const name of names.filter((file) => ENTRY.test(file)).sort()) {
512
+ const file = join(UNRELEASED_DIRECTORY, name);
513
+ const loaded = await loadEntry(join(directory, name));
514
+ if (loaded.failure !== undefined)
515
+ problems.push(`${file}: ${loaded.failure}`);
516
+ const checked = loaded.failure === undefined ? validateChange(loaded.exported, file) : undefined;
517
+ if (checked?.problems)
518
+ problems.push(...checked.problems);
519
+ if (checked?.change !== undefined)
520
+ entries.push({ change: checked.change, file });
521
+ }
522
+ return { entries, problems };
523
+ };
524
+ var readJsonFile = async (path) => {
525
+ const source = await readFile(path, "utf8").catch(() => {
526
+ return;
527
+ });
528
+ if (source === undefined)
529
+ return;
530
+ try {
531
+ const document = JSON.parse(source);
532
+ return document;
533
+ } catch {
534
+ return;
535
+ }
536
+ };
537
+
538
+ // src/reconcile.ts
539
+ var describeReconciliation = (found) => {
540
+ const lines = [];
541
+ for (const entry of found.unannounced)
542
+ 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`);
543
+ for (const name of found.unknownSymbols)
544
+ lines.push(`an entry names \`${name}\`, which this package does not export`);
545
+ return lines;
546
+ };
547
+ var reconcile = (input) => {
548
+ const announced = new Set(input.changes.filter(isDisruptive).flatMap((change) => [...change.symbols]));
549
+ const unannounced = [
550
+ ...input.diff.removed.map((name) => ({ kind: "removed", name })),
551
+ ...notable(input.diff).map((change) => ({
552
+ kind: change.shape === "parameters" ? "parameters" : "members",
553
+ name: change.name
554
+ }))
555
+ ].filter((entry) => !announced.has(entry.name));
556
+ const unknownSymbols = [
557
+ ...new Set(input.changes.flatMap((change) => [...change.symbols ?? []]).filter((name) => !input.known.has(name)))
558
+ ];
559
+ return { unannounced, unknownSymbols };
560
+ };
561
+
562
+ // src/registry.ts
563
+ import { gunzipSync } from "zlib";
564
+ var ABBREVIATED = "application/vnd.npm.install-v1+json";
565
+ var DEFAULT_REGISTRY = "https://registry.npmjs.org";
566
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
567
+ var readPackument = (body) => {
568
+ if (!isRecord2(body) || !isRecord2(body.versions))
569
+ return;
570
+ const tarballs = {};
571
+ for (const [version, entry] of Object.entries(body.versions)) {
572
+ if (!isRecord2(entry) || !isRecord2(entry.dist))
573
+ continue;
574
+ const { tarball } = entry.dist;
575
+ if (typeof tarball === "string")
576
+ tarballs[version] = tarball;
577
+ }
578
+ const tags = body["dist-tags"];
579
+ const latest = isRecord2(tags) ? tags.latest : undefined;
580
+ return {
581
+ ...typeof latest === "string" ? { latest } : {},
582
+ published: Object.keys(body.versions),
583
+ tarballs
584
+ };
585
+ };
586
+ var packageArchive = async (name, version, options = {}) => {
587
+ const call = options.fetch ?? fetch;
588
+ const versions = await packageVersions(name, options);
589
+ const url = versions?.tarballs[version];
590
+ if (url === undefined)
591
+ return;
592
+ try {
593
+ const response = await call(url);
594
+ if (!response.ok)
595
+ return;
596
+ return new Uint8Array(gunzipSync(new Uint8Array(await response.arrayBuffer())));
597
+ } catch {
598
+ return;
599
+ }
600
+ };
601
+ var packageVersions = async (name, options = {}) => {
602
+ const call = options.fetch ?? fetch;
603
+ try {
604
+ const response = await call(`${options.registry ?? DEFAULT_REGISTRY}/${name.replace("/", "%2f")}`, { headers: { accept: ABBREVIATED } });
605
+ if (!response.ok)
606
+ return;
607
+ return readPackument(await response.json());
608
+ } catch {
609
+ return;
610
+ }
611
+ };
612
+ var publishedChangelog = async (name, version, options = {}) => {
613
+ const archive = await packageArchive(name, version, options);
614
+ if (!archive)
615
+ return;
616
+ const file = readTar(archive, (path) => path === CHANGELOG_FILE).get(CHANGELOG_FILE);
617
+ if (!file)
618
+ return;
619
+ try {
620
+ return parseChangelog(JSON.parse(asText(file))).changelog;
621
+ } catch {
622
+ return;
623
+ }
624
+ };
625
+ var publishedSurface = async (name, version, options = {}) => {
626
+ const archive = await packageArchive(name, version, options);
627
+ return archive === undefined ? undefined : surfaceOfArchive(archive);
628
+ };
629
+
630
+ // src/write.ts
631
+ var HEADING = {
632
+ added: "Added",
633
+ breaking: "Breaking",
634
+ changed: "Changed",
635
+ deprecated: "Deprecated",
636
+ fixed: "Fixed",
637
+ internal: "Internal",
638
+ removed: "Removed",
639
+ security: "Security"
640
+ };
641
+ var ORDER = [
642
+ "breaking",
643
+ "removed",
644
+ "deprecated",
645
+ "security",
646
+ "added",
647
+ "changed",
648
+ "fixed",
649
+ "internal"
650
+ ];
651
+ var migrationLine = (migration) => {
652
+ if ("rename" in migration)
653
+ return `Rename \`${migration.rename.from}\` to \`${migration.rename.to}\`.`;
654
+ if ("moved" in migration)
655
+ return `Import \`${migration.moved.symbol}\` from \`${migration.moved.to}\` instead of \`${migration.moved.from}\`.`;
656
+ if ("resubpath" in migration)
657
+ return `Import from \`${migration.resubpath.to}\` instead of \`${migration.resubpath.from}\`.`;
658
+ return migration.instruction;
659
+ };
660
+ var renderChange = (change) => {
661
+ const symbols = change.symbols && change.symbols.length > 0 ? ` (${change.symbols.map((name) => `\`${name}\``).join(", ")})` : "";
662
+ const lines = [`- **${change.summary}**${symbols}`];
663
+ if (change.detail !== undefined)
664
+ lines.push(` ${change.detail}`);
665
+ if (isDisruptive(change)) {
666
+ lines.push(` _Migration:_ ${change.migration.instruction}`);
667
+ const mechanical = migrationLine(change.migration);
668
+ if (mechanical !== change.migration.instruction)
669
+ lines.push(` _Applied automatically:_ ${mechanical}`);
670
+ }
671
+ if (change.reference !== undefined)
672
+ lines.push(` See ${change.reference}.`);
673
+ return lines.join(`
674
+ `);
675
+ };
676
+ var renderRelease = (release) => {
677
+ const sections = ORDER.map((kind) => {
678
+ const changes = release.changes.filter((entry) => entry.kind === kind);
679
+ if (changes.length === 0)
680
+ return null;
681
+ return [`### ${HEADING[kind]}`, "", ...changes.map(renderChange)].join(`
682
+ `);
683
+ }).filter((section) => section !== null);
684
+ return [
685
+ `## ${release.version} \u2014 ${release.date}`,
686
+ ...release.note === undefined ? [] : ["", release.note],
687
+ "",
688
+ sections.join(`
689
+
690
+ `)
691
+ ].join(`
692
+ `);
693
+ };
694
+ var renderChangelogJson = (changelog) => `${JSON.stringify({
695
+ contract: CHANGELOG_CONTRACT,
696
+ name: changelog.name,
697
+ releases: [...changelog.releases].sort((left, right) => compareVersions(right.version, left.version)).map((release) => ({
698
+ changes: release.changes.map((change) => ({
699
+ ...change.detail === undefined ? {} : { detail: change.detail },
700
+ kind: change.kind,
701
+ ...isDisruptive(change) ? { migration: change.migration } : {},
702
+ ...change.reference === undefined ? {} : { reference: change.reference },
703
+ summary: change.summary,
704
+ ...change.symbols && change.symbols.length > 0 ? { symbols: change.symbols } : {}
705
+ })),
706
+ date: release.date,
707
+ ...release.note === undefined ? {} : { note: release.note },
708
+ version: release.version
709
+ }))
710
+ }, null, "\t")}
711
+ `;
712
+ var renderMarkdown = (changelog, options = {}) => {
713
+ const releases = [...changelog.releases].sort((left, right) => compareVersions(right.version, left.version));
714
+ const head = [
715
+ "# Changelog",
716
+ "",
717
+ `All notable changes to \`${changelog.name}\`.`,
718
+ "",
719
+ "This file is generated by `absolute-changelog` from the entries in",
720
+ "`changelog/`. Edit an entry, not this file \u2014 and add new ones under",
721
+ "`changelog/unreleased/`."
722
+ ].join(`
723
+ `);
724
+ const body = releases.map(renderRelease).join(`
725
+
726
+ `);
727
+ const tail = options.history === undefined || options.history.trim() === "" ? "" : ["---", "", "## Earlier releases", "", options.history.trim()].join(`
728
+ `);
729
+ return `${[head, body, tail].filter((part) => part !== "").join(`
730
+
731
+ `)}
732
+ `;
733
+ };
734
+
735
+ // src/cli.ts
736
+ class CliError extends Error {
737
+ }
738
+ var isChangeKind = (value) => CHANGE_KINDS.some((kind) => kind === value);
739
+ var isReleaseKind = (value) => RELEASE_KINDS.some((kind) => kind === value);
740
+ var RELEASE_KINDS = ["major", "minor", "patch", "prerelease"];
741
+ var SLUG_MAX_LENGTH = 48;
742
+ var ISO_DATE_LENGTH = 10;
743
+ var PREVIEW_LINES = 40;
744
+ var readFlags = (argv) => {
745
+ const flags = {};
746
+ let skip = false;
747
+ for (const [index, argument] of argv.entries()) {
748
+ const consumed = skip;
749
+ skip = false;
750
+ if (consumed || !argument.startsWith("--"))
751
+ continue;
752
+ const [name, inline] = argument.slice(2).split("=");
753
+ if (name === undefined || name === "")
754
+ continue;
755
+ const next = argv[index + 1];
756
+ const takesNext = inline === undefined && next !== undefined && !next.startsWith("--");
757
+ skip = takesNext;
758
+ flags[name] = inline ?? (takesNext && next !== undefined ? next : true);
759
+ }
760
+ return flags;
761
+ };
762
+ var text = (flags, name) => {
763
+ const value = flags[name];
764
+ return typeof value === "string" ? value : undefined;
765
+ };
766
+ var list = (flags, name) => {
767
+ const value = text(flags, name);
768
+ return value === undefined ? [] : value.split(",").map((part) => part.trim()).filter((part) => part !== "");
769
+ };
770
+ var isPackage = (value) => typeof value === "object" && value !== null;
771
+ var readPackage = async (root) => {
772
+ const document = await readJsonFile(join2(root, "package.json"));
773
+ if (!isPackage(document) || typeof document.name !== "string" || typeof document.version !== "string")
774
+ throw new CliError("No package.json with a name and a version here. Run this from a package root.");
775
+ const manifest = {
776
+ ...document,
777
+ name: document.name,
778
+ version: document.version
779
+ };
780
+ return manifest;
781
+ };
782
+ var readChangelog = async (root, name) => {
783
+ const document = await readJsonFile(changelogPaths(root).json);
784
+ if (document === undefined) {
785
+ const empty = {
786
+ contract: CHANGELOG_CONTRACT,
787
+ name,
788
+ releases: []
789
+ };
790
+ return empty;
791
+ }
792
+ const read = parseChangelog(document);
793
+ if (read.problems)
794
+ throw new CliError([`${CHANGELOG_FILE} is not a changelog:`, ...read.problems].join(`
795
+ `));
796
+ return read.changelog;
797
+ };
798
+ var historyOf = async (root) => readFile2(changelogPaths(root).history, "utf8").catch(() => {
799
+ return;
800
+ });
801
+ var writeBoth = async (root, changelog) => {
802
+ const paths = changelogPaths(root);
803
+ const history = await historyOf(root);
804
+ await writeFile(paths.json, renderChangelogJson(changelog));
805
+ await writeFile(paths.markdown, renderMarkdown(changelog, history === undefined ? {} : { history }));
806
+ };
807
+ var slug = (summary) => summary.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, SLUG_MAX_LENGTH) || "change";
808
+ var add = async (root, flags) => {
809
+ const kind = text(flags, "kind");
810
+ const summary = text(flags, "summary");
811
+ if (!isChangeKind(kind))
812
+ throw new CliError(`--kind must be one of: ${CHANGE_KINDS.join(", ")}`);
813
+ if (summary === undefined)
814
+ throw new CliError("--summary is what the change is, in one line");
815
+ const symbols = list(flags, "symbol");
816
+ const disruptive = kind === "breaking" || kind === "removed";
817
+ if (disruptive && symbols.length === 0)
818
+ throw new CliError(`a ${kind} change must name the exports it affects: --symbol serve,createServer`);
819
+ const instruction = text(flags, "instruction");
820
+ if (disruptive && instruction === undefined)
821
+ throw new CliError(`a ${kind} change must say how to migrate: --instruction "\u2026"`);
822
+ const renameTo = text(flags, "rename-to");
823
+ const migration = renameTo !== undefined && symbols[0] !== undefined ? [
824
+ "\tmigration: {",
825
+ ` instruction: ${JSON.stringify(instruction)},`,
826
+ ` rename: { from: ${JSON.stringify(symbols[0])}, to: ${JSON.stringify(renameTo)} }`,
827
+ "\t},"
828
+ ] : [
829
+ ` migration: { instruction: ${JSON.stringify(instruction)}, manual: true },`
830
+ ];
831
+ const body = [
832
+ "import type { Change } from '@absolutejs/changelog';",
833
+ "",
834
+ "const entry: Change = {",
835
+ ` kind: ${JSON.stringify(kind)},`,
836
+ ...disruptive ? migration : [],
837
+ ` summary: ${JSON.stringify(summary)}${symbols.length > 0 ? "," : ""}`,
838
+ ...symbols.length > 0 ? [` symbols: ${JSON.stringify(symbols)}`] : [],
839
+ "};",
840
+ "",
841
+ "export default entry;",
842
+ ""
843
+ ].join(`
844
+ `);
845
+ const directory = join2(root, UNRELEASED_DIRECTORY);
846
+ await mkdir(directory, { recursive: true });
847
+ const file = join2(directory, `${slug(summary)}.ts`);
848
+ await writeFile(file, body);
849
+ console.log(`wrote ${file}`);
850
+ };
851
+ var release = async (root, flags) => {
852
+ const manifest = await readPackage(root);
853
+ const { entries, problems } = await loadUnreleased(root);
854
+ if (problems.length > 0)
855
+ throw new CliError(["These entries are not usable:", ...problems].join(`
856
+ `));
857
+ if (entries.length === 0)
858
+ throw new CliError(`Nothing to release: ${UNRELEASED_DIRECTORY} is empty. Add a change first.`);
859
+ const asked = text(flags, "as");
860
+ if (asked !== undefined && !isReleaseKind(asked))
861
+ throw new CliError(`--as must be one of: ${RELEASE_KINDS.join(", ")}`);
862
+ const changes = entries.map((entry) => entry.change);
863
+ const version = text(flags, "version") ?? nextVersion({
864
+ ...isReleaseKind(asked) ? { asked } : {},
865
+ changes,
866
+ current: manifest.version ?? "0.0.0"
867
+ });
868
+ if (version === undefined)
869
+ throw new CliError(`Could not work out the next version from ${manifest.version}.`);
870
+ const changelog = await readChangelog(root, manifest.name ?? "");
871
+ if (changelog.releases.some((entry) => entry.version === version))
872
+ throw new CliError(`${version} is already in the changelog.`);
873
+ const [first, ...rest] = changes;
874
+ if (first === undefined)
875
+ throw new CliError(`Nothing to release: ${UNRELEASED_DIRECTORY} is empty.`);
876
+ const next = {
877
+ changes: [first, ...rest],
878
+ date: text(flags, "date") ?? new Date().toISOString().slice(0, ISO_DATE_LENGTH),
879
+ version
880
+ };
881
+ const written = {
882
+ ...changelog,
883
+ releases: [next, ...changelog.releases]
884
+ };
885
+ if (flags.dry === true) {
886
+ console.log(renderMarkdown(written).split(`
887
+ `).slice(0, PREVIEW_LINES).join(`
888
+ `));
889
+ console.log(`
890
+ (dry run: ${manifest.version} \u2192 ${version})`);
891
+ return;
892
+ }
893
+ await writeBoth(root, written);
894
+ await writeFile(join2(root, "package.json"), `${JSON.stringify({ ...manifest, version }, null, "\t")}
895
+ `);
896
+ await Promise.all(entries.map((entry) => rm(join2(root, entry.file), { force: true })));
897
+ console.log(`${manifest.name} ${manifest.version} \u2192 ${version} (${entries.length} change${entries.length === 1 ? "" : "s"})`);
898
+ };
899
+ var check = async (root, flags) => {
900
+ const manifest = await readPackage(root);
901
+ const failures = [];
902
+ const { entries, problems } = await loadUnreleased(root);
903
+ failures.push(...problems);
904
+ const document = await readJsonFile(changelogPaths(root).json);
905
+ const read = document === undefined ? undefined : parseChangelog(document);
906
+ if (read?.problems)
907
+ throw new CliError([`${CHANGELOG_FILE} is not a changelog:`, ...read.problems].join(`
908
+ `));
909
+ const current = read?.changelog ?? {
910
+ contract: CHANGELOG_CONTRACT,
911
+ name: manifest.name ?? "",
912
+ releases: []
913
+ };
914
+ const [newest] = current.releases;
915
+ if (newest !== undefined && newest.version !== manifest.version)
916
+ failures.push(`package.json is ${manifest.version} and the newest release in ${CHANGELOG_FILE} is ${newest.version}`);
917
+ if (newest !== undefined && current.name !== manifest.name)
918
+ failures.push(`${CHANGELOG_FILE} is for ${current.name}, not ${manifest.name}`);
919
+ const history = await historyOf(root);
920
+ const expected = renderMarkdown(current, history === undefined ? {} : { history });
921
+ const actual = await readFile2(changelogPaths(root).markdown, "utf8").catch(() => {
922
+ return;
923
+ });
924
+ if (newest !== undefined && actual !== expected)
925
+ failures.push(`${CHANGELOG_MARKDOWN} is not what the entries say it should be \u2014 it is generated; run \`absolute-changelog release\` or restore it`);
926
+ const tsconfig = await readJsonFile(join2(root, "tsconfig.json"));
927
+ const include = isPackage(tsconfig) ? tsconfig.include : undefined;
928
+ if (Array.isArray(include) && !coversEntries(include))
929
+ failures.push('tsconfig.json does not compile changelog/, so nothing is checking the entries \u2014 add "changelog/**/*" to its include list');
930
+ const offline = flags.offline === true;
931
+ const built = await builtSurface(root);
932
+ if (!offline && built !== undefined && newest !== undefined) {
933
+ const published = await publishedSurface(manifest.name ?? "", newest.version);
934
+ if (published === undefined)
935
+ console.warn(` (could not read ${manifest.name}@${newest.version} from the registry; the types were not checked)`);
936
+ failures.push(...published === undefined ? [] : describeReconciliation(reconcile({
937
+ changes: entries.map((entry) => entry.change),
938
+ diff: diffSurface(published, built),
939
+ known: new Set([...published.keys(), ...built.keys()])
940
+ })));
941
+ }
942
+ if (failures.length > 0)
943
+ throw new CliError(["This package\u2019s changelog does not hold:", ...failures].join(`
944
+ `));
945
+ console.log(`${manifest.name}@${manifest.version}: changelog holds (${entries.length} pending, ${current.releases.length} released)`);
946
+ };
947
+ var CHECKING_CONFIGS = ["tsconfig.json", "tsconfig.eslint.json"];
948
+ var coversEntries = (include) => Array.isArray(include) && include.some((entry) => typeof entry === "string" && entry.startsWith("changelog"));
949
+ var includeEntriesIn = async (root, file) => {
950
+ const path = join2(root, file);
951
+ const document = await readJsonFile(path);
952
+ if (!isPackage(document) || !Array.isArray(document.include))
953
+ return false;
954
+ if (coversEntries(document.include))
955
+ return false;
956
+ await writeFile(path, `${JSON.stringify({ ...document, include: [...document.include, "changelog/**/*"] }, null, "\t")}
957
+ `);
958
+ return true;
959
+ };
960
+ var includeEntries = async (root) => {
961
+ const wired = await Promise.all(CHECKING_CONFIGS.map((file) => includeEntriesIn(root, file)));
962
+ return wired.some(Boolean) ? undefined : "no tsconfig include list to wire \u2014 make sure changelog/ is compiled by something";
963
+ };
964
+ var adopt = async (root) => {
965
+ const manifest = await readPackage(root);
966
+ const paths = changelogPaths(root);
967
+ await mkdir(paths.unreleased, { recursive: true });
968
+ await writeFile(join2(paths.unreleased, ".gitkeep"), "# Entries wait here. One file per change; `absolute-changelog release` consumes them.\n");
969
+ if (existsSync(paths.markdown) && !existsSync(paths.history)) {
970
+ const previous = await readFile2(paths.markdown, "utf8");
971
+ await mkdir(join2(root, "changelog"), { recursive: true });
972
+ await writeFile(paths.history, previous.replace(/^#\s+Changelog\s*/u, "").trimStart());
973
+ await rm(paths.markdown, { force: true });
974
+ }
975
+ const files = manifest.files ?? [];
976
+ const wanted = [CHANGELOG_FILE, CHANGELOG_MARKDOWN].filter((entry) => !files.includes(entry));
977
+ if (wanted.length > 0)
978
+ await writeFile(join2(root, "package.json"), `${JSON.stringify({ ...manifest, files: [...files, ...wanted].sort() }, null, "\t")}
979
+ `);
980
+ const note = await includeEntries(root);
981
+ if (note !== undefined)
982
+ console.warn(` (${note})`);
983
+ const changelog = await readChangelog(root, manifest.name ?? "");
984
+ await writeBoth(root, changelog);
985
+ console.log(`${manifest.name}: ready. Add changes with \`absolute-changelog add\`, release with \`absolute-changelog release\`.`);
986
+ };
987
+ var main = async () => {
988
+ const [command = "", ...argv] = process.argv.slice(2);
989
+ const flags = readFlags(argv);
990
+ const root = process.cwd();
991
+ if (command === "add")
992
+ return add(root, flags);
993
+ if (command === "release")
994
+ return release(root, flags);
995
+ if (command === "check")
996
+ return check(root, flags);
997
+ if (command === "adopt")
998
+ return adopt(root);
999
+ throw new CliError("Usage: absolute-changelog <add|release|check|adopt> [options]");
1000
+ };
1001
+ await main().catch((error) => {
1002
+ console.error(error instanceof CliError ? error.message : `absolute-changelog failed: ${error instanceof Error ? error.message : String(error)}`);
1003
+ process.exit(1);
1004
+ });
1005
+
1006
+ //# debugId=E1354AD9C119C96264756E2164756E21
1007
+ //# sourceMappingURL=cli.js.map