272 lines
5.9 KiB
TypeScript
272 lines
5.9 KiB
TypeScript
import unified, { Transformer } from "unified";
|
|
import markdown from "remark-parse";
|
|
import stringify from "remark-stringify";
|
|
import vFile, { VFile } from "vfile";
|
|
import { Node, Position } from "unist";
|
|
import { version } from "punycode";
|
|
|
|
type MarkdownRootNode = {
|
|
type: "root";
|
|
children: MarkdownNode[];
|
|
};
|
|
|
|
interface HeadingNode {
|
|
type: "heading";
|
|
depth: number;
|
|
children: MarkdownNode[];
|
|
position: Position;
|
|
}
|
|
|
|
interface DefinitionNode {
|
|
type: "definition";
|
|
identifier: string;
|
|
label: string;
|
|
url: string;
|
|
position?: Position;
|
|
}
|
|
|
|
interface ListNode {
|
|
type: "list";
|
|
ordered: boolean;
|
|
start: any;
|
|
spread: boolean;
|
|
url: string;
|
|
children: object[];
|
|
position: Position;
|
|
}
|
|
|
|
interface ParagraphNode {
|
|
type: "paragraph";
|
|
children: object[];
|
|
position: Position;
|
|
}
|
|
|
|
interface LinkReferenceNode {
|
|
type: "linkReference";
|
|
identifier: string;
|
|
label: string;
|
|
referenceType: string;
|
|
children: TextNode[];
|
|
position?: Position;
|
|
}
|
|
|
|
interface TextNode {
|
|
type: "text";
|
|
value: string;
|
|
position?: Position;
|
|
}
|
|
|
|
type MarkdownNode =
|
|
| HeadingNode
|
|
| DefinitionNode
|
|
| ListNode
|
|
| ParagraphNode
|
|
| LinkReferenceNode
|
|
| TextNode;
|
|
|
|
interface Options {
|
|
version: string;
|
|
releaseDate: string;
|
|
}
|
|
|
|
function releaseTransformation({ version, releaseDate }: Options) {
|
|
return transformer as Transformer;
|
|
|
|
function transformer(tree: MarkdownRootNode, file: VFile) {
|
|
const previousVersion = determinePreviousVersion(tree);
|
|
convertUnreleasedSectionToNewRelease(tree, version, releaseDate);
|
|
addEmptyUnreleasedSection(tree);
|
|
updateCompareUrls(tree, version, previousVersion);
|
|
|
|
return tree as Node;
|
|
}
|
|
}
|
|
|
|
function determinePreviousVersion(tree: MarkdownRootNode): string {
|
|
const children = tree.children;
|
|
|
|
const versions = children.filter(
|
|
node => node.type === "heading" && node.depth === 2
|
|
);
|
|
|
|
let unreleasedSection = versions.shift();
|
|
|
|
let previousRelease = versions.shift() as HeadingNode | undefined;
|
|
|
|
if (!previousRelease) {
|
|
throw new Error("Could not determine the release prior to this one!");
|
|
}
|
|
|
|
const linkReference = previousRelease.children[0];
|
|
|
|
if (!linkReference || linkReference.type !== "linkReference") {
|
|
throw new Error(
|
|
"Invalid changelog format, previous version is not a link reference"
|
|
);
|
|
}
|
|
|
|
const linkReferenceTextNode = linkReference.children[0];
|
|
|
|
if (!linkReferenceTextNode) {
|
|
throw new Error(
|
|
"Invalid changelog format, link reference does not have a text"
|
|
);
|
|
}
|
|
|
|
return linkReferenceTextNode.value;
|
|
}
|
|
|
|
function convertUnreleasedSectionToNewRelease(
|
|
tree: MarkdownRootNode,
|
|
version: string,
|
|
releaseDate: string
|
|
) {
|
|
const children = tree.children;
|
|
|
|
// the unreleased section should always be at the top
|
|
const unreleasedSection = children.find(
|
|
node => node.type === "heading" && node.depth === 2
|
|
) as HeadingNode | undefined;
|
|
|
|
if (!unreleasedSection) {
|
|
throw new Error(
|
|
"Invalid changelog format, could not find Unreleased section"
|
|
);
|
|
}
|
|
|
|
const child = unreleasedSection.children.shift();
|
|
|
|
if (
|
|
!child ||
|
|
unreleasedSection.children.length > 0 ||
|
|
child.type !== "linkReference"
|
|
) {
|
|
throw new Error(
|
|
"Invalid changelog format, Unreleased section should only be a link reference"
|
|
);
|
|
}
|
|
|
|
const value = ` - ${releaseDate}`;
|
|
|
|
const newReleaseSection: [LinkReferenceNode, TextNode] = [
|
|
{
|
|
type: "linkReference",
|
|
identifier: version,
|
|
label: version,
|
|
referenceType: "shortcut",
|
|
position: child.position,
|
|
children: [
|
|
{
|
|
type: "text",
|
|
value: version
|
|
}
|
|
]
|
|
},
|
|
{
|
|
type: "text",
|
|
value: value
|
|
}
|
|
];
|
|
|
|
unreleasedSection.children = newReleaseSection;
|
|
}
|
|
|
|
function addEmptyUnreleasedSection(tree: MarkdownRootNode) {
|
|
const children = tree.children;
|
|
|
|
const firstHeadingSectionIndex = children.findIndex(
|
|
node => node.type === "heading" && node.depth === 2
|
|
);
|
|
|
|
const beforeFirstHeading = children.slice(0, firstHeadingSectionIndex);
|
|
const afterFirstHeading = children.slice(firstHeadingSectionIndex);
|
|
|
|
tree.children = [
|
|
...beforeFirstHeading,
|
|
{
|
|
type: "heading",
|
|
depth: 2,
|
|
position: {} as Position,
|
|
children: [
|
|
{
|
|
type: "linkReference",
|
|
identifier: "unreleased",
|
|
label: "Unreleased",
|
|
referenceType: "shortcut",
|
|
children: [
|
|
{
|
|
type: "text",
|
|
value: "Unreleased"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
...afterFirstHeading
|
|
];
|
|
}
|
|
|
|
function updateCompareUrls(
|
|
tree: MarkdownRootNode,
|
|
newVersion: string,
|
|
previousVersion: string
|
|
) {
|
|
const children = tree.children;
|
|
|
|
const firstDefinitionNodeIndex = children.findIndex(
|
|
node => node.type === "definition"
|
|
);
|
|
|
|
if (firstDefinitionNodeIndex === -1) {
|
|
throw new Error(
|
|
"Invalid changelog format, unable to find definitions section"
|
|
);
|
|
}
|
|
|
|
const beforeFirstDefinition = children.slice(0, firstDefinitionNodeIndex);
|
|
const definitions = children.slice(firstDefinitionNodeIndex);
|
|
|
|
const firstDefinition = definitions.shift() as DefinitionNode;
|
|
|
|
const unreleasedCompareUrl = firstDefinition.url.replace(
|
|
previousVersion,
|
|
newVersion
|
|
);
|
|
const previousVersionCompareUrl = firstDefinition.url.replace(
|
|
"HEAD",
|
|
newVersion
|
|
);
|
|
|
|
tree.children = [
|
|
...beforeFirstDefinition,
|
|
{
|
|
type: "definition",
|
|
identifier: "unreleased",
|
|
url: unreleasedCompareUrl,
|
|
label: "Unreleased"
|
|
},
|
|
{
|
|
type: "definition",
|
|
identifier: newVersion,
|
|
url: previousVersionCompareUrl,
|
|
label: newVersion
|
|
},
|
|
...definitions
|
|
];
|
|
}
|
|
|
|
export default async function updateChangelog(
|
|
file: VFile,
|
|
version: string,
|
|
releaseDate: string
|
|
): Promise<VFile> {
|
|
return await unified()
|
|
.use(markdown)
|
|
.use(releaseTransformation, {
|
|
version,
|
|
releaseDate
|
|
})
|
|
.use(stringify)
|
|
.process(file);
|
|
}
|