@absolutejs/changelog 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // @bun
2
+ // src/apply.ts
3
+ import ts from "typescript";
4
+
2
5
  // src/types.ts
3
6
  var CHANGE_KINDS = [
4
7
  "breaking",
@@ -19,141 +22,272 @@ var isDisruptive = (change) => change.kind === "breaking" || change.kind === "re
19
22
  var isMechanical = (migration) => !("manual" in migration);
20
23
 
21
24
  // src/apply.ts
22
- var IS_CODE = -1;
23
- var IMPORT = /(?:import|export)\s+(?:type\s+)?(?<clause>[\s\S]*?)\s*from\s*(?<quote>['"])(?<module>[^'"]+)\k<quote>/gu;
24
- var importsOf = (source) => {
25
+ var IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
26
+ var SPECIFIER = /^[@a-zA-Z0-9._~-][a-zA-Z0-9._~/-]*$/u;
27
+ var applyEdits = (source, edits) => [...edits].sort((left, right) => right.start - left.start).reduce((out, edit) => out.slice(0, edit.start) + edit.text + out.slice(edit.end), source);
28
+ var kindOf = (path) => /\.[cm]?ts$/u.test(path) ? ts.ScriptKind.TS : ts.ScriptKind.TSX;
29
+ var parse = (source, path) => ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, kindOf(path));
30
+ var belongsTo = (specifier, name) => specifier === name || specifier.startsWith(`${name}/`);
31
+ var moduleTextOf = (statement) => statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : undefined;
32
+ var portsOf = (file) => {
25
33
  const found = [];
26
- for (const match of source.matchAll(IMPORT)) {
27
- const { clause, module } = match.groups ?? {};
28
- if (module === undefined || match.index === undefined)
34
+ for (const statement of file.statements) {
35
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
29
36
  continue;
30
- const braced = clause === undefined ? undefined : /\{([\s\S]*)\}/u.exec(clause);
31
- found.push({
32
- clause: braced?.[1],
33
- end: match.index + match[0].length,
34
- full: match[0],
35
- module,
36
- start: match.index
37
- });
37
+ const module = moduleTextOf(statement);
38
+ if (module !== undefined)
39
+ found.push({ module, statement });
38
40
  }
39
41
  return found;
40
42
  };
41
- var belongsTo = (specifier, name) => specifier === name || specifier.startsWith(`${name}/`);
42
- var skipEnd = (source, index) => {
43
- const character = source[index];
44
- const next = source[index + 1];
45
- if (character === "/" && next === "/") {
46
- const line = source.indexOf(`
47
- `, index);
48
- return line < 0 ? source.length : line;
49
- }
50
- if (character === "/" && next === "*") {
51
- const close = source.indexOf("*/", index + 2);
52
- return close < 0 ? source.length : close + 2;
53
- }
54
- if (character !== "'" && character !== '"' && character !== "`")
55
- return IS_CODE;
56
- let cursor = index + 1;
57
- while (cursor < source.length && source[cursor] !== character)
58
- cursor += source[cursor] === "\\" ? 2 : 1;
59
- return Math.min(cursor + 1, source.length);
60
- };
61
- var codeSpans = (source) => {
62
- const spans = [];
63
- let start = 0;
64
- let index = 0;
65
- const closeSpan = (ends, resumes) => {
66
- spans.push([start, ends]);
67
- start = resumes;
43
+ var namedElementsOf = (statement) => {
44
+ const clause = ts.isImportDeclaration(statement) ? statement.importClause?.namedBindings : statement.exportClause;
45
+ if (clause === undefined)
46
+ return;
47
+ if (ts.isNamedImports(clause) || ts.isNamedExports(clause))
48
+ return clause;
49
+ return;
50
+ };
51
+ var exportedNameOf = (element) => (element.propertyName ?? element.name).text;
52
+ var declaresLocally = (file, name) => {
53
+ let found = false;
54
+ const visit = (node) => {
55
+ if (found)
56
+ return;
57
+ const declares = (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node)) && node.name !== undefined && ts.isIdentifier(node.name) && node.name.text === name;
58
+ if (declares)
59
+ found = true;
60
+ else
61
+ ts.forEachChild(node, visit);
68
62
  };
69
- while (index < source.length) {
70
- const skipped = skipEnd(source, index);
71
- if (skipped > IS_CODE)
72
- closeSpan(index, skipped);
73
- index = skipped === IS_CODE ? index + 1 : skipped;
74
- }
75
- spans.push([start, source.length]);
76
- return spans;
77
- };
78
- var renameInCode = (source, from, into) => {
79
- const pattern = new RegExp(`(?<![.\\w$])${from.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?![\\w$])`, "gu");
80
- let out = "";
81
- let cursor = 0;
82
- for (const [start, end] of codeSpans(source)) {
83
- out += source.slice(cursor, start);
84
- out += source.slice(start, end).replace(pattern, into);
85
- cursor = end;
86
- }
87
- return out + source.slice(cursor);
88
- };
89
- var declaresLocally = (source, name) => new RegExp(`\\b(?:const|let|var|function|class|interface|type|enum)\\s+${name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}\\b`, "u").test(source);
90
- var applyRename = (source, packageName, rename) => {
91
- const statements = importsOf(source).filter((statement) => belongsTo(statement.module, packageName) && statement.clause !== undefined && new RegExp(`(?<![\\w$])${rename.from}(?![\\w$])`, "u").test(statement.clause));
92
- if (statements.length === 0)
93
- return { changed: false, notes: [], source };
94
- const aliased = statements.some((statement) => new RegExp(`(?<![\\w$])${rename.from}\\s+as\\s`, "u").test(statement.clause ?? ""));
95
- if (aliased) {
96
- let out = source;
97
- for (const statement of statements)
98
- out = out.replace(statement.full, statement.full.replace(new RegExp(`(?<![\\w$])${rename.from}(\\s+as\\s)`, "u"), `${rename.to}$1`));
99
- return { changed: true, notes: [], source: out };
100
- }
101
- if (declaresLocally(source, rename.from)) {
102
- let out = source;
103
- for (const statement of statements)
104
- out = out.replace(statement.full, statement.full.replace(new RegExp(`(?<![\\w$])${rename.from}(?![\\w$])`, "u"), `${rename.to} as ${rename.from}`));
63
+ ts.forEachChild(file, visit);
64
+ return found;
65
+ };
66
+ var isReference = (node) => {
67
+ const { parent } = node;
68
+ if (parent === undefined)
69
+ return false;
70
+ if (ts.isPropertyAccessExpression(parent) && parent.name === node)
71
+ return false;
72
+ if (ts.isQualifiedName(parent) && parent.right === node)
73
+ return false;
74
+ if (ts.isPropertyAssignment(parent) && parent.name === node)
75
+ return false;
76
+ if (ts.isBindingElement(parent) && parent.propertyName === node)
77
+ return false;
78
+ if (ts.isJsxAttribute(parent) && parent.name === node)
79
+ return false;
80
+ if ((ts.isMethodDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) || ts.isMethodSignature(parent) || ts.isEnumMember(parent)) && parent.name === node)
81
+ return false;
82
+ if ((ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent)) && true)
83
+ return false;
84
+ return !ts.isDeclarationStatement(parent);
85
+ };
86
+ var referencesTo = (file, name) => {
87
+ const found = [];
88
+ const shorthand = [];
89
+ const visit = (node) => {
90
+ if (ts.isShorthandPropertyAssignment(node) && node.name.text === name)
91
+ shorthand.push(node);
92
+ else if (ts.isIdentifier(node) && node.text === name && isReference(node))
93
+ found.push(node);
94
+ ts.forEachChild(node, visit);
95
+ };
96
+ ts.forEachChild(file, visit);
97
+ return { references: found, shorthand };
98
+ };
99
+ var withoutElement = (elements, dropped, file) => {
100
+ const kept = elements.filter((element) => element !== dropped);
101
+ return kept.length === 0 ? undefined : kept.map((element) => element.getText(file)).join(", ");
102
+ };
103
+ var isTypeOnly = (statement, element) => {
104
+ if (element.isTypeOnly)
105
+ return true;
106
+ return ts.isImportDeclaration(statement) ? statement.importClause?.isTypeOnly === true : statement.isTypeOnly;
107
+ };
108
+ var quoteOf = (statement, file) => statement.moduleSpecifier?.getText(file).startsWith('"') === true ? '"' : "'";
109
+ var renameTargetsIn = (input) => portsOf(input.file).filter(({ module }) => belongsTo(module, input.packageName)).flatMap(({ statement }) => {
110
+ const clause = namedElementsOf(statement);
111
+ if (clause === undefined)
112
+ return [];
113
+ return clause.elements.filter((element) => exportedNameOf(element) === input.from).map((element) => ({ clause, element }));
114
+ });
115
+ var renameOneImport = (input) => {
116
+ const { clause, element } = input.target;
117
+ const { file, rename } = input;
118
+ const already = element.propertyName === undefined && clause.elements.some((other) => other !== element && exportedNameOf(other) === rename.to);
119
+ if (already) {
120
+ const kept = withoutElement(clause.elements, element, file);
105
121
  return {
106
- changed: true,
107
- notes: [
108
- `\`${rename.from}\` is also declared in this file, so the import was aliased instead of the uses being renamed`
122
+ edits: kept === undefined ? [] : [
123
+ {
124
+ end: clause.getEnd() - 1,
125
+ start: clause.getStart(file) + 1,
126
+ text: ` ${kept} `
127
+ }
109
128
  ],
110
- source: out
129
+ moves: true
111
130
  };
112
131
  }
132
+ if (element.propertyName !== undefined)
133
+ return {
134
+ edits: [
135
+ {
136
+ end: element.propertyName.getEnd(),
137
+ start: element.propertyName.getStart(file),
138
+ text: rename.to
139
+ }
140
+ ],
141
+ moves: false
142
+ };
143
+ const shadowed = declaresLocally(file, rename.from);
113
144
  return {
114
- changed: true,
115
- notes: [],
116
- source: renameInCode(source, rename.from, rename.to)
145
+ edits: [
146
+ {
147
+ end: element.getEnd(),
148
+ start: element.getStart(file),
149
+ text: shadowed ? `${rename.to} as ${rename.from}` : rename.to
150
+ }
151
+ ],
152
+ moves: !shadowed,
153
+ ...shadowed ? {
154
+ note: `\`${rename.from}\` is also declared in this file, so the import was aliased instead of the uses being renamed`
155
+ } : {}
117
156
  };
118
157
  };
119
- var applyResubpath = (source, resubpath) => {
120
- const statements = importsOf(source).filter((statement) => statement.module === resubpath.from);
121
- if (statements.length === 0)
158
+ var applyRename = (input) => {
159
+ const { file, packageName, rename, source } = input;
160
+ if (!IDENTIFIER.test(rename.to))
161
+ return {
162
+ changed: false,
163
+ notes: [`\`${rename.to}\` is not a name, so this rename was not applied`],
164
+ source
165
+ };
166
+ const done = renameTargetsIn({ file, from: rename.from, packageName }).map((target) => renameOneImport({ file, rename, target }));
167
+ const edits = done.flatMap((one) => one.edits);
168
+ if (edits.length === 0)
122
169
  return { changed: false, notes: [], source };
123
- let out = source;
124
- for (const statement of statements)
125
- out = out.replace(statement.full, statement.full.replace(resubpath.from, resubpath.to));
126
- return { changed: true, notes: [], source: out };
127
- };
128
- var applyMoved = (source, moved) => {
129
- const pattern = new RegExp(`(?<![\\w$])${moved.symbol}(?![\\w$])`, "u");
130
- const statements = importsOf(source).filter((statement) => statement.module === moved.from && statement.clause !== undefined && pattern.test(statement.clause));
131
- if (statements.length === 0)
170
+ if (done.some((one) => one.moves)) {
171
+ const { references, shorthand } = referencesTo(file, rename.from);
172
+ for (const reference of references)
173
+ edits.push({
174
+ end: reference.getEnd(),
175
+ start: reference.getStart(file),
176
+ text: rename.to
177
+ });
178
+ for (const property of shorthand)
179
+ edits.push({
180
+ end: property.getEnd(),
181
+ start: property.getStart(file),
182
+ text: `${rename.from}: ${rename.to}`
183
+ });
184
+ }
185
+ return {
186
+ changed: true,
187
+ notes: done.flatMap((one) => ("note" in one) && one.note ? [one.note] : []),
188
+ source: applyEdits(source, edits)
189
+ };
190
+ };
191
+ var applyMoved = (input) => {
192
+ const { file, moved, source } = input;
193
+ if (!SPECIFIER.test(moved.to))
194
+ return {
195
+ changed: false,
196
+ notes: [
197
+ `\`${moved.to}\` is not a module specifier, so this move was not applied`
198
+ ],
199
+ source
200
+ };
201
+ const edits = [];
202
+ for (const { module, statement } of portsOf(file)) {
203
+ if (module !== moved.from)
204
+ continue;
205
+ const clause = namedElementsOf(statement);
206
+ if (clause === undefined)
207
+ continue;
208
+ const element = clause.elements.find((candidate) => exportedNameOf(candidate) === moved.symbol);
209
+ if (element === undefined)
210
+ continue;
211
+ const quote = quoteOf(statement, file);
212
+ const kept = withoutElement(clause.elements, element, file);
213
+ const carriesDefault = ts.isImportDeclaration(statement) && (statement.importClause?.name !== undefined || statement.importClause?.isTypeOnly === true);
214
+ const whole = kept === undefined && !carriesDefault && statement.moduleSpecifier;
215
+ if (whole)
216
+ edits.push({
217
+ end: whole.getEnd(),
218
+ start: whole.getStart(file),
219
+ text: `${quote}${moved.to}${quote}`
220
+ });
221
+ if (whole)
222
+ continue;
223
+ const keyword = ts.isExportDeclaration(statement) ? "export" : "import";
224
+ const typed = isTypeOnly(statement, element) ? "type " : "";
225
+ const carried = element.propertyName ? `${moved.symbol} as ${element.name.text}` : element.name.text;
226
+ const carrier = ts.isImportDeclaration(statement) && statement.importClause?.name;
227
+ const emptied = kept === undefined && carrier ? { end: clause.getEnd(), start: carrier.getEnd(), text: "" } : undefined;
228
+ const trimmed = kept === undefined ? undefined : {
229
+ end: clause.getEnd() - 1,
230
+ start: clause.getStart(file) + 1,
231
+ text: ` ${kept} `
232
+ };
233
+ edits.push(...[emptied, trimmed].filter((edit) => edit !== undefined));
234
+ edits.push({
235
+ end: statement.getEnd(),
236
+ start: statement.getEnd(),
237
+ text: `
238
+ ${keyword} ${typed}{ ${carried} } from ${quote}${moved.to}${quote};`
239
+ });
240
+ }
241
+ if (edits.length === 0)
132
242
  return { changed: false, notes: [], source };
133
- let out = source;
134
- for (const statement of statements) {
135
- const clause = statement.clause ?? "";
136
- const kept = clause.split(",").map((part) => part.trim()).filter((part) => part !== "" && !pattern.test(part));
137
- const replacement = kept.length === 0 ? statement.full.replace(moved.from, moved.to) : `${statement.full.replace(clause, ` ${kept.join(", ")} `).replace(/\s+/gu, " ")}
138
- import { ${moved.symbol} } from '${moved.to}';`;
139
- out = out.replace(statement.full, replacement);
243
+ return { changed: true, notes: [], source: applyEdits(source, edits) };
244
+ };
245
+ var applyResubpath = (input) => {
246
+ const { file, resubpath, source } = input;
247
+ if (!SPECIFIER.test(resubpath.to))
248
+ return {
249
+ changed: false,
250
+ notes: [
251
+ `\`${resubpath.to}\` is not a module specifier, so this move was not applied`
252
+ ],
253
+ source
254
+ };
255
+ const edits = [];
256
+ for (const { module, statement } of portsOf(file)) {
257
+ if (module !== resubpath.from || !statement.moduleSpecifier)
258
+ continue;
259
+ const quote = quoteOf(statement, file);
260
+ edits.push({
261
+ end: statement.moduleSpecifier.getEnd(),
262
+ start: statement.moduleSpecifier.getStart(file),
263
+ text: `${quote}${resubpath.to}${quote}`
264
+ });
140
265
  }
141
- return { changed: true, notes: [], source: out };
266
+ if (edits.length === 0)
267
+ return { changed: false, notes: [], source };
268
+ return { changed: true, notes: [], source: applyEdits(source, edits) };
142
269
  };
143
270
  var applyMigration = (source, input) => {
144
271
  const { migration, packageName } = input;
145
272
  if (!isMechanical(migration))
146
273
  return { changed: false, notes: [migration.instruction], source };
274
+ const file = parse(source, input.path ?? "file.tsx");
147
275
  if ("rename" in migration)
148
- return applyRename(source, packageName, migration.rename);
276
+ return applyRename({
277
+ file,
278
+ packageName,
279
+ rename: migration.rename,
280
+ source
281
+ });
149
282
  if ("resubpath" in migration)
150
- return applyResubpath(source, migration.resubpath);
151
- return applyMoved(source, migration.moved);
283
+ return applyResubpath({ file, resubpath: migration.resubpath, source });
284
+ return applyMoved({ file, moved: migration.moved, source });
152
285
  };
153
286
  var applyMigrations = (source, input) => input.migrations.reduce((carried, migration) => {
154
287
  const result = applyMigration(carried.source, {
155
288
  migration,
156
- packageName: input.packageName
289
+ packageName: input.packageName,
290
+ ...input.path === undefined ? {} : { path: input.path }
157
291
  });
158
292
  return {
159
293
  changed: carried.changed || result.changed,
@@ -399,8 +533,8 @@ var parseChangelog = (value) => {
399
533
  return { problems: ["not an object"] };
400
534
  if (!isText(value.name))
401
535
  problems.push("needs a package name");
402
- if (value.contract !== CHANGELOG_CONTRACT)
403
- problems.push(`contract ${JSON.stringify(value.contract)} is not ${CHANGELOG_CONTRACT}`);
536
+ if (typeof value.contract !== "number" || value.contract > CHANGELOG_CONTRACT)
537
+ problems.push(`contract ${JSON.stringify(value.contract)} is newer than ${CHANGELOG_CONTRACT}, which this reader understands`);
404
538
  const raw = value.releases;
405
539
  if (!Array.isArray(raw))
406
540
  problems.push("releases is not a list");
@@ -433,6 +567,10 @@ var migrationsBetween = (changelog, from, upto) => disruptiveBetween(changelog,
433
567
  });
434
568
  var symbolsDisturbedBetween = (changelog, from, upto) => new Set(disruptiveBetween(changelog, from, upto).flatMap(({ change: change2 }) => change2.symbols));
435
569
 
570
+ // src/surface.ts
571
+ import path from "path";
572
+ import ts2 from "typescript";
573
+
436
574
  // src/tarball.ts
437
575
  var TAR_BLOCK = 512;
438
576
  var NAME_LENGTH = 100;
@@ -485,114 +623,185 @@ var readTar = (archive, keep, limits = {}) => {
485
623
  };
486
624
 
487
625
  // src/surface.ts
488
- 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;
489
- var NAMED_EXPORT = /\bexport\s*\{([^}]*)\}/gu;
490
- var BODIED = new Set(["class", "interface", "enum", "namespace"]);
491
- var OPENS = new Set(["{", "(", "["]);
492
- var CLOSES = new Set(["}", ")", "]"]);
493
- var IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
494
- var LAST2 = -1;
495
- var NOWHERE = -1;
496
- var MEMBER = /([A-Za-z_$][\w$]*)\s*[?!]?\s*[:(<]/u;
497
- var isStub = (declaration) => declaration.startsWith("export {");
498
- var declarationEnd = (source, from, bodied) => {
499
- let depth = 0;
500
- for (let index = from;index < source.length; index += 1) {
501
- const character = source[index] ?? "";
502
- if (OPENS.has(character))
503
- depth += 1;
504
- else if (CLOSES.has(character))
505
- depth -= 1;
506
- else if (character === ";" && depth <= 0)
507
- return index + 1;
508
- if (CLOSES.has(character) && depth <= 0 && bodied)
509
- return index + 1;
510
- }
511
- return source.length;
512
- };
513
- var normalize = (declaration) => declaration.replace(/\s+/gu, " ").trim();
514
- var reExported = (body) => body.split(",").map((part) => part.trim().replace(/^type\s+/u, "")).filter((clause) => clause !== "").map((clause) => [
515
- clause.split(/\s+as\s+/u).at(LAST2)?.trim(),
516
- clause
517
- ]).filter((pair) => pair[0] !== undefined && IDENTIFIER.test(pair[0]));
518
- var exportedDeclarations = (source) => {
626
+ var DECLARATION = /\.d\.[cm]?ts$/u;
627
+ var parse2 = (source, name) => ts2.createSourceFile(name, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
628
+ var normalize = (text) => text.replace(/\s+/gu, " ").trim();
629
+ var resolveFrom = (files, from, specifier) => {
630
+ if (!specifier.startsWith("."))
631
+ return;
632
+ const base = path.posix.join(path.posix.dirname(from), specifier);
633
+ const candidates = [
634
+ base,
635
+ `${base.replace(/\.js$/u, "")}.d.ts`,
636
+ `${base.replace(/\.js$/u, "")}.d.mts`,
637
+ `${base.replace(/\.js$/u, "")}.d.cts`,
638
+ `${base}/index.d.ts`,
639
+ `${base}/index.d.mts`,
640
+ `${base}/index.d.cts`
641
+ ];
642
+ return candidates.find((candidate) => files.has(candidate));
643
+ };
644
+ var declaredNameOf = (statement) => {
645
+ if (ts2.isFunctionDeclaration(statement) || ts2.isClassDeclaration(statement) || ts2.isInterfaceDeclaration(statement) || ts2.isTypeAliasDeclaration(statement) || ts2.isEnumDeclaration(statement) || ts2.isModuleDeclaration(statement))
646
+ return statement.name !== undefined && ts2.isIdentifier(statement.name) ? statement.name.text : undefined;
647
+ return;
648
+ };
649
+ var isExported = (statement) => ts2.canHaveModifiers(statement) && ts2.getModifiers(statement)?.some((modifier) => modifier.kind === ts2.SyntaxKind.ExportKeyword) === true;
650
+ var variableNamesOf = (statement) => statement.declarationList.declarations.map((declaration) => declaration.name).filter((name) => ts2.isIdentifier(name)).map((name) => name.text);
651
+ var declarationsIn = (file) => {
519
652
  const found = new Map;
520
- const add = (name, declaration) => {
521
- const existing = found.get(name);
522
- found.set(name, existing === undefined ? declaration : `${existing}
523
- ${declaration}`);
524
- };
525
- for (const match of source.matchAll(DECLARATION)) {
526
- const [, keyword, name] = match;
527
- if (name === undefined || match.index === undefined)
653
+ for (const statement of file.statements) {
654
+ const text = normalize(statement.getText(file));
655
+ const named = declaredNameOf(statement);
656
+ if (named !== undefined)
657
+ found.set(named, text);
658
+ if (!ts2.isVariableStatement(statement))
528
659
  continue;
529
- add(name, normalize(source.slice(match.index, declarationEnd(source, match.index + match[0].length, BODIED.has(keyword ?? "")))));
660
+ for (const name of variableNamesOf(statement))
661
+ found.set(name, text);
530
662
  }
531
- for (const match of source.matchAll(NAMED_EXPORT))
532
- for (const [exported, clause] of reExported(match[1] ?? ""))
533
- add(exported, `export { ${clause} }`);
534
663
  return found;
535
664
  };
536
- var surfaceOfArchive = (archive) => surfaceOfFiles([...readTar(archive, (path) => path.endsWith(".d.ts")).values()].map(asText));
537
- var surfaceOfFiles = (files) => {
665
+ var declaresTypes = (manifest) => {
666
+ if (typeof manifest !== "object" || manifest === null)
667
+ return false;
668
+ const record = new Map(Object.entries(manifest));
669
+ if (typeof record.get("types") === "string")
670
+ return true;
671
+ if (typeof record.get("typings") === "string")
672
+ return true;
673
+ return JSON.stringify(record.get("exports") ?? {}).includes('"types"');
674
+ };
675
+ var contributionOf = (input) => {
676
+ const { file, files, from, local, seen, statement } = input;
677
+ const nothing = [];
678
+ if (ts2.isExportAssignment(statement))
679
+ return [
680
+ [
681
+ statement.isExportEquals === true ? "export=" : "default",
682
+ normalize(statement.getText(file))
683
+ ]
684
+ ];
685
+ const named = declaredNameOf(statement);
686
+ if (named !== undefined && isExported(statement))
687
+ return [
688
+ [named, local.get(named) ?? normalize(statement.getText(file))]
689
+ ];
690
+ if (ts2.isVariableStatement(statement) && isExported(statement)) {
691
+ const text = normalize(statement.getText(file));
692
+ return variableNamesOf(statement).map((name) => [name, text]);
693
+ }
694
+ if (!ts2.isExportDeclaration(statement))
695
+ return nothing;
696
+ const specifier = statement.moduleSpecifier !== undefined && ts2.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : undefined;
697
+ const target = specifier === undefined ? undefined : resolveFrom(files, from, specifier);
698
+ const theirs = target === undefined ? new Map : exportsOf(files, target, new Set(seen));
699
+ const clause = statement.exportClause;
700
+ if (clause === undefined)
701
+ return [...theirs].filter(([name]) => name !== "default" && name !== "export=");
702
+ if (ts2.isNamespaceExport(clause))
703
+ return [[clause.name.text, `export * as ${clause.name.text}`]];
704
+ return clause.elements.map((element) => {
705
+ const sourceName = (element.propertyName ?? element.name).text;
706
+ const text = (specifier === undefined ? local.get(sourceName) : theirs.get(sourceName)) ?? `export { ${sourceName} }`;
707
+ return [element.name.text, text];
708
+ });
709
+ };
710
+ var exportsOf = (files, from, seen) => {
538
711
  const surface = new Map;
539
- for (const source of files)
540
- for (const [name, declaration] of exportedDeclarations(source)) {
541
- const known = surface.get(name);
542
- if (known === undefined || isStub(known) && !isStub(declaration))
543
- surface.set(name, declaration);
544
- }
712
+ const source = files.get(from);
713
+ if (source === undefined || seen.has(from))
714
+ return surface;
715
+ seen.add(from);
716
+ const file = parse2(source, from);
717
+ const local = declarationsIn(file);
718
+ const contributed = file.statements.flatMap((statement) => contributionOf({ file, files, from, local, seen, statement }));
719
+ for (const [name, text] of contributed)
720
+ surface.set(name, text);
721
+ return surface;
722
+ };
723
+ var surfaceOfArchive = (archive) => {
724
+ const held = readTar(archive, (name) => DECLARATION.test(name) || name === "package.json");
725
+ const files = new Map([...held].map(([name, bytes]) => [name, asText(bytes)]));
726
+ const manifest = files.get("package.json");
727
+ let parsed;
728
+ try {
729
+ parsed = manifest === undefined ? undefined : JSON.parse(manifest);
730
+ } catch {
731
+ parsed = undefined;
732
+ }
733
+ const entries = typeEntriesOf(parsed, files);
734
+ return entries.length === 0 ? undefined : surfaceOfFiles(files, entries);
735
+ };
736
+ var surfaceOfFiles = (files, entries) => {
737
+ const surface = new Map;
738
+ for (const entry of entries)
739
+ for (const [name, text] of exportsOf(files, entry, new Set))
740
+ if (!surface.has(name))
741
+ surface.set(name, text);
545
742
  return surface;
546
743
  };
744
+ var typeCandidatesIn = (value) => {
745
+ if (typeof value === "string")
746
+ return [value];
747
+ if (typeof value !== "object" || value === null)
748
+ return [];
749
+ return Object.entries(value).flatMap(([key, nested]) => key === "types" || key === "typings" ? typeCandidatesIn(nested) : typeCandidatesIn(nested));
750
+ };
751
+ var typeEntriesOf = (manifest, files) => {
752
+ const record = typeof manifest === "object" && manifest !== null ? new Map(Object.entries(manifest)) : new Map;
753
+ const claimed = [
754
+ record.get("types"),
755
+ record.get("typings"),
756
+ ...typeCandidatesIn(record.get("exports"))
757
+ ];
758
+ const entries = claimed.filter((value) => typeof value === "string").map((value) => value.replace(/^\.\//u, "")).flatMap((cleaned) => DECLARATION.test(cleaned) ? [cleaned] : [
759
+ cleaned.replace(/\.[cm]?js$/u, ".d.ts"),
760
+ cleaned.replace(/\.[cm]?js$/u, ".d.mts"),
761
+ cleaned.replace(/\.[cm]?js$/u, ".d.cts")
762
+ ]).filter((candidate) => files.has(candidate));
763
+ const fallback = entries.length === 0 && files.has("dist/index.d.ts") ? ["dist/index.d.ts"] : [];
764
+ return [...new Set([...entries, ...fallback])];
765
+ };
547
766
  var SHAPE = {
548
767
  members: "members",
549
768
  parameters: "parameters",
550
769
  types: "types",
551
770
  unknown: "unknown"
552
771
  };
772
+ var isStub = (declaration) => declaration.startsWith("export {");
773
+ var signatureOf = (declaration) => {
774
+ const file = parse2(`${declaration}
775
+ `, "declaration.d.ts");
776
+ const [statement] = file.statements;
777
+ if (statement === undefined)
778
+ return;
779
+ return statement;
780
+ };
553
781
  var parametersOf = (declaration) => {
554
- let depth = 0;
555
- let start = NOWHERE;
556
- for (const [index, character] of [...declaration].entries()) {
557
- if (character === "{" && depth === 0)
558
- return "";
559
- depth += character === "(" ? 1 : 0;
560
- if (character === "(" && depth === 1)
561
- start = index;
562
- depth -= character === ")" ? 1 : 0;
563
- if (character === ")" && depth === 0 && start > NOWHERE)
564
- return declaration.slice(start, index + 1);
565
- }
566
- return "";
782
+ const statement = signatureOf(declaration);
783
+ if (statement === undefined)
784
+ return "";
785
+ const found = [];
786
+ const visit = (node) => {
787
+ if (ts2.isFunctionTypeNode(node) || ts2.isFunctionDeclaration(node) || ts2.isMethodSignature(node) || ts2.isMethodDeclaration(node) || ts2.isConstructSignatureDeclaration(node) || ts2.isConstructorDeclaration(node))
788
+ found.push(node.parameters.map((parameter) => normalize(parameter.getText())).join(", "));
789
+ ts2.forEachChild(node, visit);
790
+ };
791
+ visit(statement);
792
+ return found.join(" | ");
567
793
  };
568
794
  var membersOf = (declaration) => {
569
- const opened = declaration.indexOf("{");
570
- if (opened < 0)
571
- return new Set;
795
+ const statement = signatureOf(declaration);
572
796
  const members = new Set;
573
- let depth = 0;
574
- let segment = "";
575
- const take = () => {
576
- const [, name] = MEMBER.exec(segment.trim()) ?? [];
577
- if (name !== undefined)
578
- members.add(name);
579
- segment = "";
797
+ if (statement === undefined)
798
+ return members;
799
+ const visit = (node) => {
800
+ if ((ts2.isPropertySignature(node) || ts2.isMethodSignature(node) || ts2.isPropertyDeclaration(node) || ts2.isMethodDeclaration(node) || ts2.isEnumMember(node)) && node.name !== undefined && (ts2.isIdentifier(node.name) || ts2.isStringLiteral(node.name)))
801
+ members.add(node.name.text);
802
+ ts2.forEachChild(node, visit);
580
803
  };
581
- for (const character of declaration.slice(opened)) {
582
- const opening = OPENS.has(character);
583
- const closing = CLOSES.has(character);
584
- depth += opening ? 1 : 0;
585
- if (opening && depth === 1)
586
- segment = "";
587
- depth -= closing ? 1 : 0;
588
- if (opening || closing || depth !== 1)
589
- continue;
590
- if (character === ";" || character === ",")
591
- take();
592
- else
593
- segment += character;
594
- }
595
- take();
804
+ visit(statement);
596
805
  return members;
597
806
  };
598
807
  var sameNames = (left, right) => left.size === right.size && [...left].every((name) => right.has(name));
@@ -629,9 +838,9 @@ var diffSurface = (before, after) => {
629
838
  var notable = (diff) => diff.changed.filter((change2) => change2.shape === "parameters" || change2.shape === "members");
630
839
 
631
840
  // src/entries.ts
632
- var loadEntry = async (path) => {
841
+ var loadEntry = async (path2) => {
633
842
  try {
634
- const module = await import(pathToFileURL(path).href);
843
+ const module = await import(pathToFileURL(path2).href);
635
844
  const named = typeof module === "object" && module !== null && "change" in module ? module.change : undefined;
636
845
  const exported = named ?? (typeof module === "object" && module !== null && "default" in module ? module.default : undefined);
637
846
  const read = { exported };
@@ -644,23 +853,28 @@ var loadEntry = async (path) => {
644
853
  }
645
854
  };
646
855
  var ENTRY = /\.[cm]?ts$/u;
647
- var builtSurface = async (packageRoot, out = "dist") => {
648
- const root = join(packageRoot, out);
649
- const sources = [];
856
+ var DECLARATION2 = /\.d\.[cm]?ts$/u;
857
+ var OUTPUT_DIRECTORIES = ["dist", "build", "lib", "types", "out"];
858
+ var builtSurface = async (packageRoot) => {
859
+ const manifest = await readJsonFile(join(packageRoot, "package.json"));
860
+ const files = new Map;
650
861
  const walk = async (directory) => {
651
- const found = await readdir(directory, { withFileTypes: true }).catch(() => []);
862
+ const found = await readdir(join(packageRoot, directory), {
863
+ withFileTypes: true
864
+ }).catch(() => []);
652
865
  await Promise.all(found.map(async (entry) => {
653
- const path = join(directory, entry.name);
866
+ const relative = `${directory}/${entry.name}`;
654
867
  if (entry.isDirectory())
655
- return walk(path);
656
- if (!entry.name.endsWith(".d.ts"))
868
+ return walk(relative);
869
+ if (!DECLARATION2.test(entry.name))
657
870
  return;
658
- sources.push(await readFile(path, "utf8").catch(() => ""));
871
+ files.set(relative, await readFile(join(packageRoot, relative), "utf8").catch(() => ""));
659
872
  return;
660
873
  }));
661
874
  };
662
- await walk(root);
663
- return sources.length === 0 ? undefined : surfaceOfFiles(sources);
875
+ await Promise.all(OUTPUT_DIRECTORIES.map((directory) => walk(directory)));
876
+ const entries = typeEntriesOf(manifest, files);
877
+ return entries.length === 0 ? undefined : surfaceOfFiles(files, entries);
664
878
  };
665
879
  var changelogPaths = (packageRoot) => ({
666
880
  history: join(packageRoot, "changelog", "history.md"),
@@ -686,8 +900,8 @@ var loadUnreleased = async (packageRoot) => {
686
900
  }
687
901
  return { entries, problems };
688
902
  };
689
- var readJsonFile = async (path) => {
690
- const source = await readFile(path, "utf8").catch(() => {
903
+ var readJsonFile = async (path2) => {
904
+ const source = await readFile(path2, "utf8").catch(() => {
691
905
  return;
692
906
  });
693
907
  if (source === undefined)
@@ -699,33 +913,89 @@ var readJsonFile = async (path) => {
699
913
  return;
700
914
  }
701
915
  };
916
+ var strayEntries = async (packageRoot) => {
917
+ const names = await readdir(join(packageRoot, UNRELEASED_DIRECTORY)).catch(() => []);
918
+ return names.filter((name) => !ENTRY.test(name) && name !== ".gitkeep" && !name.startsWith("."));
919
+ };
702
920
  // src/reconcile.ts
921
+ var migrationHolds = (input) => {
922
+ const { after, change: change2, subpaths } = input;
923
+ const wrong = [];
924
+ const { migration } = change2;
925
+ if ("rename" in migration) {
926
+ const { from } = migration.rename;
927
+ const into = migration.rename.to;
928
+ if (!after.has(into))
929
+ wrong.push(`\`${change2.summary}\` renames to \`${into}\`, which this version does not export`);
930
+ if (after.has(from))
931
+ wrong.push(`\`${change2.summary}\` renames \`${from}\` away, but this version still exports it`);
932
+ if (!change2.symbols.includes(from))
933
+ wrong.push(`\`${change2.summary}\` renames \`${from}\`, which is not in its own symbols`);
934
+ }
935
+ if ("moved" in migration) {
936
+ const { symbol } = migration.moved;
937
+ const into = migration.moved.to;
938
+ if (!after.has(symbol))
939
+ wrong.push(`\`${change2.summary}\` moves \`${symbol}\`, which this version does not export`);
940
+ if (subpaths.size > 0 && !subpaths.has(into))
941
+ wrong.push(`\`${change2.summary}\` moves \`${symbol}\` to \`${into}\`, which is not an entry point of this package`);
942
+ }
943
+ if ("resubpath" in migration && subpaths.size > 0 && !subpaths.has(migration.resubpath.to))
944
+ wrong.push(`\`${change2.summary}\` points at \`${migration.resubpath.to}\`, which is not an entry point of this package`);
945
+ if (change2.kind === "removed") {
946
+ for (const symbol of change2.symbols)
947
+ if (after.has(symbol))
948
+ wrong.push(`\`${change2.summary}\` says \`${symbol}\` was removed, but this version still exports it`);
949
+ }
950
+ return wrong;
951
+ };
703
952
  var describeReconciliation = (found) => {
704
953
  const lines = [];
954
+ const moved2 = {
955
+ members: "changed the members of its type",
956
+ parameters: "changed what it is called with",
957
+ removed: "is no longer exported",
958
+ types: "changed its declaration"
959
+ };
705
960
  for (const entry of found.unannounced)
706
- 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`);
961
+ lines.push(`\`${entry.name}\` ${moved2[entry.kind]} and no entry says so`);
707
962
  for (const name of found.unknownSymbols)
708
963
  lines.push(`an entry names \`${name}\`, which this package does not export`);
964
+ lines.push(...found.wrongMigrations);
709
965
  return lines;
710
966
  };
711
967
  var reconcile = (input) => {
712
- const announced = new Set(input.changes.filter(isDisruptive).flatMap((change2) => [...change2.symbols]));
968
+ const disruptive = new Set(input.changes.filter(isDisruptive).flatMap((change2) => [...change2.symbols]));
969
+ const mentioned = new Set(input.changes.flatMap((change2) => [...change2.symbols ?? []]));
713
970
  const unannounced = [
714
- ...input.diff.removed.map((name) => ({ kind: "removed", name })),
715
- ...notable(input.diff).map((change2) => ({
716
- kind: change2.shape === "parameters" ? "parameters" : "members",
717
- name: change2.name
718
- }))
719
- ].filter((entry) => !announced.has(entry.name));
971
+ ...input.diff.removed.filter((name) => !disruptive.has(name)).map((name) => ({ kind: "removed", name })),
972
+ ...input.diff.changed.filter((change2) => !mentioned.has(change2.name) && change2.shape !== "unknown").map((change2) => ({ kind: change2.shape, name: change2.name }))
973
+ ];
720
974
  const unknownSymbols = [
721
975
  ...new Set(input.changes.flatMap((change2) => [...change2.symbols ?? []]).filter((name) => !input.known.has(name)))
722
976
  ];
723
- return { unannounced, unknownSymbols };
977
+ const { after } = input;
978
+ const wrongMigrations = after === undefined ? [] : input.changes.filter(isDisruptive).flatMap((change2) => migrationHolds({
979
+ after,
980
+ change: change2,
981
+ subpaths: input.subpaths ?? new Set
982
+ }));
983
+ return { unannounced, unknownSymbols, wrongMigrations };
724
984
  };
725
985
  // src/registry.ts
726
986
  import { gunzipSync } from "zlib";
727
987
  var ABBREVIATED = "application/vnd.npm.install-v1+json";
728
988
  var DEFAULT_REGISTRY = "https://registry.npmjs.org";
989
+ var KIBIBYTE2 = 1024;
990
+ var MEBIBYTE2 = KIBIBYTE2 * KIBIBYTE2;
991
+ var MAX_ARCHIVE_MEBIBYTES = 96;
992
+ var MAX_ARCHIVE_BYTES = MAX_ARCHIVE_MEBIBYTES * MEBIBYTE2;
993
+ var MAX_DOWNLOAD_MEBIBYTES = 32;
994
+ var MAX_DOWNLOAD_BYTES = MAX_DOWNLOAD_MEBIBYTES * MEBIBYTE2;
995
+ var TIMEOUT_MS = 20000;
996
+ var PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
997
+ var MAX_NAME_LENGTH = 214;
998
+ var isPackageName = (name) => name.length <= MAX_NAME_LENGTH && PACKAGE_NAME.test(name);
729
999
  var isRecord2 = (value) => typeof value === "object" && value !== null;
730
1000
  var readPackument = (body) => {
731
1001
  if (!isRecord2(body) || !isRecord2(body.versions))
@@ -753,20 +1023,30 @@ var packageArchive = async (name, version, options = {}) => {
753
1023
  if (url === undefined)
754
1024
  return;
755
1025
  try {
756
- const response = await call(url);
1026
+ const response = await call(url, {
1027
+ signal: AbortSignal.timeout(TIMEOUT_MS)
1028
+ });
757
1029
  if (!response.ok)
758
1030
  return;
759
- return new Uint8Array(gunzipSync(new Uint8Array(await response.arrayBuffer())));
1031
+ const compressed = new Uint8Array(await response.arrayBuffer());
1032
+ if (compressed.byteLength > MAX_DOWNLOAD_BYTES)
1033
+ return;
1034
+ return new Uint8Array(gunzipSync(compressed, { maxOutputLength: MAX_ARCHIVE_BYTES }));
760
1035
  } catch {
761
1036
  return;
762
1037
  }
763
1038
  };
764
1039
  var packageVersions = async (name, options = {}) => {
1040
+ if (!isPackageName(name))
1041
+ return;
765
1042
  const call = options.fetch ?? fetch;
766
1043
  const url = `${options.registry ?? DEFAULT_REGISTRY}/${name.replace("/", "%2f")}`;
767
1044
  try {
768
- const abbreviated = await call(url, { headers: { accept: ABBREVIATED } });
769
- const response = abbreviated.ok ? abbreviated : await call(url);
1045
+ const abbreviated = await call(url, {
1046
+ headers: { accept: ABBREVIATED },
1047
+ signal: AbortSignal.timeout(TIMEOUT_MS)
1048
+ });
1049
+ const response = abbreviated.ok ? abbreviated : await call(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
770
1050
  if (!response.ok)
771
1051
  return;
772
1052
  return readPackument(await response.json());
@@ -778,7 +1058,7 @@ var publishedChangelog = async (name, version, options = {}) => {
778
1058
  const archive = await packageArchive(name, version, options);
779
1059
  if (!archive)
780
1060
  return;
781
- const file = readTar(archive, (path) => path === CHANGELOG_FILE).get(CHANGELOG_FILE);
1061
+ const file = readTar(archive, (path2) => path2 === CHANGELOG_FILE).get(CHANGELOG_FILE);
782
1062
  if (!file)
783
1063
  return;
784
1064
  try {
@@ -855,6 +1135,7 @@ var renderRelease = (release) => {
855
1135
  ].join(`
856
1136
  `);
857
1137
  };
1138
+ var GENERATED_MARKER = "This file is generated by `absolute-changelog`";
858
1139
  var renderChangelogJson = (changelog) => `${JSON.stringify({
859
1140
  contract: CHANGELOG_CONTRACT,
860
1141
  name: changelog.name,
@@ -880,7 +1161,7 @@ var renderMarkdown = (changelog, options = {}) => {
880
1161
  "",
881
1162
  `All notable changes to \`${changelog.name}\`.`,
882
1163
  "",
883
- "This file is generated by `absolute-changelog` from the entries in",
1164
+ `${GENERATED_MARKER} from the entries in`,
884
1165
  "`changelog/`. Edit an entry, not this file \u2014 and add new ones under",
885
1166
  "`changelog/unreleased/`."
886
1167
  ].join(`
@@ -911,12 +1192,13 @@ export {
911
1192
  changelogPaths,
912
1193
  changesBetween,
913
1194
  compareVersions,
1195
+ declaresTypes,
914
1196
  describeReconciliation,
915
1197
  diffSurface,
916
1198
  disruptiveBetween,
917
- exportedDeclarations,
918
1199
  isDisruptive,
919
1200
  isMechanical,
1201
+ isPackageName,
920
1202
  isPrerelease,
921
1203
  loadUnreleased,
922
1204
  manual,
@@ -942,8 +1224,9 @@ export {
942
1224
  surfaceOfArchive,
943
1225
  surfaceOfFiles,
944
1226
  symbolsDisturbedBetween,
1227
+ typeEntriesOf,
945
1228
  validateChange
946
1229
  };
947
1230
 
948
- //# debugId=F4722D4B88A229AC64756E2164756E21
1231
+ //# debugId=7DC9C4F3C075733D64756E2164756E21
949
1232
  //# sourceMappingURL=index.js.map