@versatiles/release-tool 2.5.0 → 2.7.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/README.md +44 -30
- package/dist/commands/check.d.ts +24 -0
- package/dist/commands/check.js +27 -3
- package/dist/commands/deps-graph.d.ts +22 -0
- package/dist/commands/deps-graph.js +23 -1
- package/dist/commands/doc-command.js +6 -6
- package/dist/commands/doc-typescript.d.ts +39 -3
- package/dist/commands/doc-typescript.js +25 -5
- package/dist/commands/markdown.d.ts +9 -1
- package/dist/commands/markdown.js +167 -38
- package/dist/commands/release-npm.d.ts +44 -1
- package/dist/commands/release-npm.js +195 -49
- package/dist/index.js +28 -13
- package/dist/lib/benchmark.d.ts +119 -0
- package/dist/lib/benchmark.js +148 -0
- package/dist/lib/changelog.d.ts +23 -0
- package/dist/lib/changelog.js +117 -0
- package/dist/lib/errors.d.ts +32 -0
- package/dist/lib/errors.js +47 -0
- package/dist/lib/git.d.ts +92 -0
- package/dist/lib/git.js +120 -8
- package/dist/lib/log.d.ts +61 -1
- package/dist/lib/log.js +76 -3
- package/dist/lib/retry.d.ts +24 -0
- package/dist/lib/retry.js +44 -0
- package/dist/lib/shell.d.ts +131 -10
- package/dist/lib/shell.js +142 -28
- package/dist/lib/utils.d.ts +29 -0
- package/dist/lib/utils.js +43 -2
- package/package.json +27 -14
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { remark } from 'remark';
|
|
2
2
|
import remarkGfm from 'remark-gfm';
|
|
3
3
|
import remarkStringify from 'remark-stringify';
|
|
4
|
+
import { markdownError, notImplementedError } from '../lib/errors.js';
|
|
4
5
|
import { getErrorMessage } from '../lib/utils.js';
|
|
6
|
+
// Custom blockquote handler that preserves GitHub alert syntax
|
|
7
|
+
function blockquoteHandler(node, _parent, state, info) {
|
|
8
|
+
const exit = state.enter('blockquote');
|
|
9
|
+
const tracker = state.createTracker(info);
|
|
10
|
+
tracker.move('> ');
|
|
11
|
+
tracker.shift(2);
|
|
12
|
+
let value = state.indentLines(state.containerFlow(node, tracker.current()), (line, _index, blank) => '>' + (blank ? '' : ' ') + line);
|
|
13
|
+
exit();
|
|
14
|
+
// Unescape GitHub alert markers that remark-stringify escapes
|
|
15
|
+
value = value.replace(/^(>\s*)\\\[!(NOTE|WARNING|TIP|IMPORTANT|CAUTION)\]/m, '$1[!$2]');
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
5
18
|
/**
|
|
6
19
|
* Injects a Markdown segment under a specified heading in a Markdown document.
|
|
7
20
|
* Optionally, the injected segment can be made foldable for better readability.
|
|
@@ -24,7 +37,7 @@ export function injectMarkdown(document, segment, heading, foldable) {
|
|
|
24
37
|
}
|
|
25
38
|
catch (error) {
|
|
26
39
|
// Handle errors during the search for the start index.
|
|
27
|
-
throw
|
|
40
|
+
throw markdownError(`Error while searching for segment "${heading}": ${getErrorMessage(error)}`);
|
|
28
41
|
}
|
|
29
42
|
// Get the depth of the specified heading to maintain the structure.
|
|
30
43
|
const depth = getHeadingDepth(documentAst, startIndex);
|
|
@@ -45,6 +58,9 @@ export function injectMarkdown(document, segment, heading, foldable) {
|
|
|
45
58
|
.use(remarkStringify, {
|
|
46
59
|
bullet: '-',
|
|
47
60
|
rule: '-',
|
|
61
|
+
handlers: {
|
|
62
|
+
blockquote: blockquoteHandler,
|
|
63
|
+
},
|
|
48
64
|
})
|
|
49
65
|
.stringify(documentAst);
|
|
50
66
|
return result;
|
|
@@ -62,7 +78,7 @@ export function updateTOC(main, heading) {
|
|
|
62
78
|
const headingText = extractTextFromMDAsHTML(parseMarkdown(heading));
|
|
63
79
|
// Build the TOC by iterating over each heading in the document.
|
|
64
80
|
const toc = mainAst.children
|
|
65
|
-
.flatMap(c => {
|
|
81
|
+
.flatMap((c) => {
|
|
66
82
|
// Skip non-heading nodes and the specified heading.
|
|
67
83
|
if (c.type !== 'heading' || extractTextFromMDAsHTML(c) === headingText)
|
|
68
84
|
return [];
|
|
@@ -87,23 +103,23 @@ export function updateTOC(main, heading) {
|
|
|
87
103
|
function findSegmentStartIndex(mainAst, headingAst) {
|
|
88
104
|
// Verify the structure of the headingAst.
|
|
89
105
|
if (headingAst.children.length !== 1)
|
|
90
|
-
throw
|
|
106
|
+
throw markdownError('headingAst.children.length !== 1');
|
|
91
107
|
if (headingAst.children[0].type !== 'heading')
|
|
92
|
-
throw
|
|
108
|
+
throw markdownError("headingAst.children[0].type !== 'heading'");
|
|
93
109
|
const sectionDepth = headingAst.children[0].depth;
|
|
94
110
|
const sectionText = extractTextFromMDAsHTML(headingAst);
|
|
95
111
|
// Search for the index of the heading in the main document AST.
|
|
96
112
|
const indexes = mainAst.children.flatMap((c, index) => {
|
|
97
|
-
if (
|
|
113
|
+
if (c.type === 'heading' && c.depth === sectionDepth && extractTextFromMDAsHTML(c).startsWith(sectionText)) {
|
|
98
114
|
return [index];
|
|
99
115
|
}
|
|
100
116
|
return [];
|
|
101
117
|
});
|
|
102
118
|
// Handle the cases of no match or multiple matches.
|
|
103
119
|
if (indexes.length < 1)
|
|
104
|
-
throw
|
|
120
|
+
throw markdownError('section not found');
|
|
105
121
|
if (indexes.length > 1)
|
|
106
|
-
throw
|
|
122
|
+
throw markdownError('too many sections found');
|
|
107
123
|
return indexes[0];
|
|
108
124
|
}
|
|
109
125
|
/**
|
|
@@ -133,7 +149,7 @@ function findNextHeadingIndex(mainAst, startIndex, depth) {
|
|
|
133
149
|
function getHeadingDepth(mainAst, index) {
|
|
134
150
|
const node = mainAst.children[index];
|
|
135
151
|
if (node.type !== 'heading')
|
|
136
|
-
throw
|
|
152
|
+
throw markdownError("node.type !== 'heading'");
|
|
137
153
|
return node.depth;
|
|
138
154
|
}
|
|
139
155
|
/**
|
|
@@ -142,9 +158,9 @@ function getHeadingDepth(mainAst, index) {
|
|
|
142
158
|
* @param depth The depth to which the segment should be indented.
|
|
143
159
|
*/
|
|
144
160
|
function indentSegmentToDepth(segmentAst, depth) {
|
|
145
|
-
segmentAst.children.forEach(node => {
|
|
161
|
+
segmentAst.children.forEach((node) => {
|
|
146
162
|
if (node.type == 'heading')
|
|
147
|
-
return node.depth += depth;
|
|
163
|
+
return (node.depth += depth);
|
|
148
164
|
});
|
|
149
165
|
}
|
|
150
166
|
/**
|
|
@@ -159,53 +175,113 @@ function mergeSegments(mainAst, segmentAst, startIndex, endIndex) {
|
|
|
159
175
|
}
|
|
160
176
|
/**
|
|
161
177
|
* Extracts the textual content from a node in the AST.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* @
|
|
178
|
+
* Recursively processes nodes with children and extracts text values.
|
|
179
|
+
*
|
|
180
|
+
* @param node The AST node (Root or any RootContent type).
|
|
181
|
+
* @returns The extracted text content as HTML-escaped string.
|
|
165
182
|
*/
|
|
166
183
|
function extractTextFromMDAsHTML(node) {
|
|
167
184
|
switch (node.type) {
|
|
185
|
+
// Nodes with direct text value
|
|
168
186
|
case 'inlineCode':
|
|
169
187
|
case 'text':
|
|
170
188
|
return textToHtml(node.value);
|
|
189
|
+
// Nodes with children to recurse into
|
|
171
190
|
case 'heading':
|
|
172
191
|
case 'root':
|
|
173
|
-
|
|
192
|
+
case 'paragraph':
|
|
193
|
+
case 'blockquote':
|
|
194
|
+
case 'listItem':
|
|
195
|
+
case 'tableCell':
|
|
196
|
+
case 'tableRow':
|
|
197
|
+
case 'link':
|
|
198
|
+
case 'emphasis':
|
|
199
|
+
case 'strong':
|
|
200
|
+
case 'delete':
|
|
201
|
+
return node.children.map((child) => extractTextFromMDAsHTML(child)).join('');
|
|
202
|
+
// Nodes with children but need special handling
|
|
203
|
+
case 'list':
|
|
204
|
+
return node.children.map((child) => extractTextFromMDAsHTML(child)).join('');
|
|
205
|
+
case 'table':
|
|
206
|
+
return node.children.map((child) => extractTextFromMDAsHTML(child)).join('');
|
|
207
|
+
// Nodes with alt text
|
|
208
|
+
case 'image':
|
|
209
|
+
return node.alt ? textToHtml(node.alt) : '';
|
|
210
|
+
// Nodes that don't contribute text content
|
|
174
211
|
case 'html':
|
|
212
|
+
case 'code':
|
|
213
|
+
case 'thematicBreak':
|
|
214
|
+
case 'break':
|
|
215
|
+
case 'definition':
|
|
216
|
+
case 'footnoteDefinition':
|
|
217
|
+
case 'footnoteReference':
|
|
218
|
+
case 'imageReference':
|
|
219
|
+
case 'linkReference':
|
|
220
|
+
case 'yaml':
|
|
175
221
|
return '';
|
|
176
|
-
default:
|
|
177
|
-
|
|
178
|
-
|
|
222
|
+
default: {
|
|
223
|
+
// TypeScript exhaustive check - this ensures we handle all types
|
|
224
|
+
const _exhaustiveCheck = node;
|
|
225
|
+
throw markdownError(`unhandled node type: ${node.type}`);
|
|
226
|
+
}
|
|
179
227
|
}
|
|
180
228
|
}
|
|
181
229
|
/**
|
|
182
230
|
* Generates an anchor ID for a Markdown heading based on its text content.
|
|
231
|
+
* Handles all PhrasingContent types that can appear in a heading.
|
|
232
|
+
*
|
|
183
233
|
* @param node The heading node.
|
|
184
|
-
* @returns The generated anchor ID.
|
|
185
|
-
* @throws Error if the child node type is unknown.
|
|
234
|
+
* @returns The generated anchor ID, formatted for use in URLs.
|
|
186
235
|
*/
|
|
187
236
|
function getMDAnchor(node) {
|
|
188
237
|
let text = '';
|
|
189
238
|
for (const c of node.children) {
|
|
190
239
|
// Handle different types of child nodes to construct the anchor text.
|
|
191
240
|
switch (c.type) {
|
|
241
|
+
// Check for explicit ID in HTML
|
|
192
242
|
case 'html': {
|
|
193
243
|
const match = /<a\s.*id\s*=\s*['"]([^'"]+)/i.exec(c.value);
|
|
194
244
|
if (match)
|
|
195
245
|
return match[1];
|
|
196
246
|
break;
|
|
197
247
|
}
|
|
248
|
+
// Nodes with direct text value
|
|
198
249
|
case 'text':
|
|
199
250
|
case 'inlineCode':
|
|
200
251
|
text += c.value;
|
|
201
252
|
break;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
253
|
+
// Nodes with children - recurse to extract text
|
|
254
|
+
case 'emphasis':
|
|
255
|
+
case 'strong':
|
|
256
|
+
case 'delete':
|
|
257
|
+
case 'link':
|
|
258
|
+
for (const child of c.children) {
|
|
259
|
+
if (child.type === 'text' || child.type === 'inlineCode') {
|
|
260
|
+
text += child.value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
// Image - use alt text
|
|
265
|
+
case 'image':
|
|
266
|
+
if (c.alt)
|
|
267
|
+
text += c.alt;
|
|
268
|
+
break;
|
|
269
|
+
// Nodes that don't contribute to anchor text
|
|
270
|
+
case 'break':
|
|
271
|
+
case 'footnoteReference':
|
|
272
|
+
case 'imageReference':
|
|
273
|
+
case 'linkReference':
|
|
274
|
+
break;
|
|
275
|
+
default: {
|
|
276
|
+
// TypeScript exhaustive check
|
|
277
|
+
const _exhaustiveCheck = c;
|
|
278
|
+
throw markdownError(`unhandled phrasing content type: ${c.type}`);
|
|
279
|
+
}
|
|
205
280
|
}
|
|
206
281
|
}
|
|
207
282
|
// Format the text to create a suitable anchor ID.
|
|
208
|
-
text = text
|
|
283
|
+
text = text
|
|
284
|
+
.toLowerCase()
|
|
209
285
|
.replace(/[()]+/g, '')
|
|
210
286
|
.replace(/[^a-z0-9]+/g, '-')
|
|
211
287
|
.replace(/^-+|-+$/g, '');
|
|
@@ -213,6 +289,8 @@ function getMDAnchor(node) {
|
|
|
213
289
|
}
|
|
214
290
|
/**
|
|
215
291
|
* Converts a segment of the AST into a foldable HTML element.
|
|
292
|
+
* Headings become collapsible `<details>` sections.
|
|
293
|
+
*
|
|
216
294
|
* @param ast The AST of the segment to be converted.
|
|
217
295
|
*/
|
|
218
296
|
function convertToFoldable(ast) {
|
|
@@ -220,25 +298,62 @@ function convertToFoldable(ast) {
|
|
|
220
298
|
const children = [];
|
|
221
299
|
ast.children.forEach((c) => {
|
|
222
300
|
switch (c.type) {
|
|
223
|
-
|
|
224
|
-
case 'list':
|
|
225
|
-
case 'paragraph':
|
|
226
|
-
children.push(c);
|
|
227
|
-
break;
|
|
301
|
+
// Headings start new foldable sections
|
|
228
302
|
case 'heading':
|
|
229
303
|
closeDetails(c.depth);
|
|
230
304
|
children.push({ type: 'html', value: '<details>' });
|
|
231
305
|
children.push({ type: 'html', value: `<summary>${lineToHtml(c)}</summary>` });
|
|
232
306
|
openDetails.unshift(c.depth);
|
|
233
307
|
break;
|
|
234
|
-
|
|
235
|
-
|
|
308
|
+
// Block content that gets included in sections
|
|
309
|
+
case 'html':
|
|
310
|
+
case 'list':
|
|
311
|
+
case 'paragraph':
|
|
312
|
+
case 'blockquote':
|
|
313
|
+
case 'code':
|
|
314
|
+
case 'table':
|
|
315
|
+
case 'thematicBreak':
|
|
316
|
+
children.push(c);
|
|
317
|
+
break;
|
|
318
|
+
// Definition content - include as-is
|
|
319
|
+
case 'definition':
|
|
320
|
+
case 'footnoteDefinition':
|
|
321
|
+
children.push(c);
|
|
322
|
+
break;
|
|
323
|
+
// YAML frontmatter - include as-is
|
|
324
|
+
case 'yaml':
|
|
325
|
+
children.push(c);
|
|
326
|
+
break;
|
|
327
|
+
// Inline/phrasing content that shouldn't appear at root level
|
|
328
|
+
// but handle gracefully if present
|
|
329
|
+
case 'text':
|
|
330
|
+
case 'inlineCode':
|
|
331
|
+
case 'emphasis':
|
|
332
|
+
case 'strong':
|
|
333
|
+
case 'delete':
|
|
334
|
+
case 'link':
|
|
335
|
+
case 'image':
|
|
336
|
+
case 'break':
|
|
337
|
+
case 'footnoteReference':
|
|
338
|
+
case 'imageReference':
|
|
339
|
+
case 'linkReference':
|
|
340
|
+
case 'listItem':
|
|
341
|
+
case 'tableCell':
|
|
342
|
+
case 'tableRow':
|
|
343
|
+
// These shouldn't normally appear at root level, but pass through
|
|
344
|
+
children.push(c);
|
|
345
|
+
break;
|
|
346
|
+
default: {
|
|
347
|
+
// TypeScript exhaustive check
|
|
348
|
+
const _exhaustiveCheck = c;
|
|
349
|
+
throw markdownError(`unhandled root content type: ${c.type}`);
|
|
350
|
+
}
|
|
236
351
|
}
|
|
237
352
|
});
|
|
238
353
|
closeDetails(0);
|
|
239
354
|
ast.children = children;
|
|
240
355
|
function closeDetails(depth) {
|
|
241
|
-
while (
|
|
356
|
+
while (openDetails.length > 0 && openDetails[0] >= depth) {
|
|
242
357
|
children.push({ type: 'html', value: '</details>' });
|
|
243
358
|
openDetails.shift();
|
|
244
359
|
}
|
|
@@ -247,6 +362,14 @@ function convertToFoldable(ast) {
|
|
|
247
362
|
function lineToHtml(heading) {
|
|
248
363
|
return `<h${heading.depth}>${nodesToHtml(heading.children)}</h${heading.depth}>`;
|
|
249
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Converts a PhrasingContent node to its HTML representation.
|
|
367
|
+
* Handles all PhrasingContent types defined in mdast.
|
|
368
|
+
*
|
|
369
|
+
* @param node The phrasing content node to convert.
|
|
370
|
+
* @returns The HTML string representation.
|
|
371
|
+
* @throws {VrtError} For reference types that require resolution (footnote, image, link references).
|
|
372
|
+
*/
|
|
250
373
|
export function nodeToHtml(node) {
|
|
251
374
|
switch (node.type) {
|
|
252
375
|
case 'html':
|
|
@@ -273,19 +396,25 @@ export function nodeToHtml(node) {
|
|
|
273
396
|
attributes.push(`title="${node.title}"`);
|
|
274
397
|
return `<img ${attributes.join(' ')} />`;
|
|
275
398
|
}
|
|
276
|
-
|
|
277
|
-
case '
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
399
|
+
// Reference types require definition resolution which is not supported
|
|
400
|
+
case 'footnoteReference':
|
|
401
|
+
throw notImplementedError('footnoteReference - requires definition resolution');
|
|
402
|
+
case 'imageReference':
|
|
403
|
+
throw notImplementedError('imageReference - requires definition resolution');
|
|
404
|
+
case 'linkReference':
|
|
405
|
+
throw notImplementedError('linkReference - requires definition resolution');
|
|
406
|
+
default: {
|
|
407
|
+
// TypeScript exhaustive check
|
|
408
|
+
const _exhaustiveCheck = node;
|
|
409
|
+
throw markdownError(`unhandled phrasing content type: ${node.type}`);
|
|
410
|
+
}
|
|
282
411
|
}
|
|
283
412
|
}
|
|
284
413
|
function nodesToHtml(children) {
|
|
285
414
|
return children.map(nodeToHtml).join('');
|
|
286
415
|
}
|
|
287
416
|
function textToHtml(text) {
|
|
288
|
-
return text.replace(/[^a-z0-9 ,.\-:_?@äöüß]/gi, c => `&#${c.charCodeAt(0)};`);
|
|
417
|
+
return text.replace(/[^a-z0-9 ,.\-:_?@äöüß]/gi, (c) => `&#${c.charCodeAt(0)};`);
|
|
289
418
|
}
|
|
290
419
|
export function parseMarkdown(document) {
|
|
291
420
|
return remark().use(remarkGfm).parse(document);
|
|
@@ -1,2 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Options for the release process.
|
|
4
|
+
*/
|
|
5
|
+
export interface ReleaseOptions {
|
|
6
|
+
/** The project directory containing package.json (default: current directory) */
|
|
7
|
+
directory?: string;
|
|
8
|
+
/** The git branch to release from (default: 'main') */
|
|
9
|
+
branch?: string;
|
|
10
|
+
/** If true, simulate the release without making changes (default: false) */
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Executes the npm release process.
|
|
15
|
+
*
|
|
16
|
+
* This function performs a complete release workflow:
|
|
17
|
+
* 1. Validates git state (correct branch, no uncommitted changes)
|
|
18
|
+
* 2. Pulls latest changes from remote
|
|
19
|
+
* 3. Verifies npm authentication
|
|
20
|
+
* 4. Prompts for new version (with suggestion based on conventional commits)
|
|
21
|
+
* 5. Runs project checks
|
|
22
|
+
* 6. Updates package.json version
|
|
23
|
+
* 7. Updates CHANGELOG.md
|
|
24
|
+
* 8. Publishes to npm (if not private)
|
|
25
|
+
* 9. Creates git commit and tag
|
|
26
|
+
* 10. Pushes to remote and creates GitHub release
|
|
27
|
+
*
|
|
28
|
+
* @param directory - The project directory containing package.json
|
|
29
|
+
* @param branch - The git branch to release from (default: 'main')
|
|
30
|
+
* @param dryRun - If true, simulate the release without making changes
|
|
31
|
+
* @throws {VrtError} If any step in the release process fails
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* // Standard release from main branch
|
|
36
|
+
* await release('/path/to/project');
|
|
37
|
+
*
|
|
38
|
+
* // Dry run to preview release
|
|
39
|
+
* await release('/path/to/project', 'main', true);
|
|
40
|
+
*
|
|
41
|
+
* // Release from a different branch
|
|
42
|
+
* await release('/path/to/project', 'release');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function release(directory: string, branch?: string, dryRun?: boolean): Promise<void>;
|