Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions lib/changelog-emitter/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
// @ts-check
import eslint from "@eslint/js";
import tsEslint from "typescript-eslint";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

export default tsEslint.config(
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export default [
{
ignores: ["**/dist/**/*", "**/.temp/**/*"],
ignores: [
"**/dist/**/*",
"**/.temp/**/*",
"eslint.config.js",
"vitest.config.ts",
],
},
eslint.configs.recommended,
...tsEslint.configs.recommended,
);
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
},
},
];
56 changes: 45 additions & 11 deletions lib/changelog-emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export function generateChangelog(
namespace: Namespace,
parentVersions?: Version[],
): Changelog {
const changelog: Changelog = {};
const changelog: Changelog = {
versions: [],
logs: {},
};

// Try to get versions for this namespace, or use parent versions
let allVersions = getAllVersions(context.program, namespace);
Expand All @@ -49,13 +52,14 @@ export function generateChangelog(
parentVersions,
);
// Merge sub-namespace changelog into main changelog
for (const [schemaName, entries] of Object.entries(subChangelog)) {
changelog[schemaName] = entries;
}
mergeChangelogs(changelog, subChangelog);
}
return changelog;
}

// Extract version strings
changelog.versions = allVersions.map((v) => getVersionString(v));

// Process models in current namespace
const models = Array.from(namespace.models.values()) as Model[];
for (const model of models) {
Expand All @@ -75,20 +79,52 @@ export function generateChangelog(
for (const subNamespace of subNamespaces) {
const subChangelog = generateChangelog(context, subNamespace, allVersions);
// Merge sub-namespace changelog into main changelog
for (const [schemaName, entries] of Object.entries(subChangelog)) {
changelog[schemaName] = entries;
}
mergeChangelogs(changelog, subChangelog);
}

return changelog;
}

// #############################################################################
// # Helper functions
// #############################################################################

/**
* Get the version string from a Version object
*/
function getVersionString(version: Version): string {
if (
version.enumMember?.value &&
typeof version.enumMember.value === "string"
) {
return version.enumMember.value;
}
return version.name;
}

/**
* Merge two changelogs together
*/
function mergeChangelogs(target: Changelog, source: Changelog): void {
// Merge versions (keep unique, sorted)
const versionSet = new Set([...target.versions, ...source.versions]);
target.versions = Array.from(versionSet).sort();

// Merge logs
for (const [schemaName, entries] of Object.entries(source.logs)) {
target.logs[schemaName] = entries;
}
}

export async function $onEmit(context: EmitContext) {
const globalNamespace = context.program.getGlobalNamespaceType();
const topLevelNamespaces = Array.from(globalNamespace.namespaces.values());

// Collect changelogs from all namespaces into a single changelog
const combinedChangelog: Changelog = {};
const combinedChangelog: Changelog = {
versions: [],
logs: {},
};

// Iterate through all top-level namespaces (excluding the global TypeSpec namespace)
for (const namespace of topLevelNamespaces) {
Expand All @@ -101,9 +137,7 @@ export async function $onEmit(context: EmitContext) {
const changelog = generateChangelog(context, namespace);

// Merge this namespace's changelog into the combined changelog
for (const [schemaName, entries] of Object.entries(changelog)) {
combinedChangelog[schemaName] = entries;
}
mergeChangelogs(combinedChangelog, changelog);
}

// Emit the combined changelog as a single file
Expand Down
39 changes: 30 additions & 9 deletions lib/changelog-emitter/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
export interface ChangelogEntry {
version: string;
changes: string[];
}

export interface Changelog {
[schemaName: string]: ChangelogEntry[];
}

/**
* Target types that mirror the TypeSpec versioning decorators.
* The values are designed to work directly with message templates.
Expand All @@ -23,3 +14,33 @@ export enum TargetType {
Scalar = "scalar",
Interface = "interface",
}

export enum Action {
Added = "added",
Removed = "removed",
Renamed = "renamed",
MadeRequired = "madeRequired",
MadeOptional = "madeOptional",
TypeChanged = "typeChanged",
}

export interface ChangeRecord {
message: string;
action: Action;
targetKind: TargetType;
currTargetName?: string;
prevTargetName?: string;
currDataType?: string;
prevDataType?: string;
}

export type Logs = {
[schemaName: string]: {
[version: string]: ChangeRecord[];
};
};

export interface Changelog {
versions: string[];
logs: Logs;
}
68 changes: 31 additions & 37 deletions lib/changelog-emitter/src/utils/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
getRemovedOnVersions,
Version,
} from "@typespec/versioning";
import { ChangelogEntry } from "../types.js";
import { getOrCreateEntry, getVersionString } from "./index.js";
import { Changelog, ChangeRecord } from "../types.js";
import { getOrCreateChanges, getVersionString } from "./index.js";
import { Log } from "./logging.js";
import { TargetType } from "../types.js";

Expand Down Expand Up @@ -57,41 +57,35 @@ export function logEnumChanges(
context: EmitContext,
enumType: Enum,
allVersions: Version[],
changelog: { [schemaName: string]: ChangelogEntry[] },
changelog: Changelog,
): void {
// Skip the Versions enum
if (enumType.name === "Versions") {
return;
}

const enumName = enumType.name;
const enumChangelog: ChangelogEntry[] = [];

// Initialize logs for this schema if not exists
if (!changelog.logs[enumName]) {
changelog.logs[enumName] = {};
}
const enumLogs = changelog.logs[enumName];

// Process enum-level changes
logEnumAdditions(context, enumType, allVersions, enumChangelog);
logEnumRemovals(context, enumType, enumChangelog);
logEnumAdditions(context, enumType, allVersions, enumLogs);
logEnumRemovals(context, enumType, enumLogs);

// Process enum members
const members = Array.from(enumType.members.values()) as EnumMember[];
for (const member of members) {
logEnumMemberAdditions(context, member, enumChangelog);
logEnumMemberRemovals(context, member, enumChangelog);
logEnumMemberAdditions(context, member, enumLogs);
logEnumMemberRemovals(context, member, enumLogs);
}

// Only include enums that have changes
if (enumChangelog.length > 0) {
// Sort by version index to maintain order
enumChangelog.sort((a, b) => {
const versionA = allVersions.find(
(v) => getVersionString(v) === a.version,
);
const versionB = allVersions.find(
(v) => getVersionString(v) === b.version,
);
return (versionA?.index || 0) - (versionB?.index || 0);
});

changelog[enumName] = enumChangelog;
// If no changes were logged, remove the empty entry
if (Object.keys(enumLogs).length === 0) {
delete changelog.logs[enumName];
}
}

Expand All @@ -106,24 +100,24 @@ function logEnumAdditions(
context: EmitContext,
enumType: Enum,
allVersions: Version[],
enumChangelog: ChangelogEntry[],
enumLogs: { [version: string]: ChangeRecord[] },
): void {
const enumAddedVersions = getAddedOnVersions(context.program, enumType);

if (enumAddedVersions && enumAddedVersions.length > 0) {
for (const version of enumAddedVersions) {
const entry = getOrCreateEntry(enumChangelog, getVersionString(version));
entry.changes.push(Log.added(TargetType.Enum, enumType.name));
const changes = getOrCreateChanges(enumLogs, getVersionString(version));
changes.push(Log.added(TargetType.Enum, enumType.name));
}
} else {
// If no explicit @added, assume it was created in the first version
const firstVersion = allVersions[0];
if (firstVersion) {
const entry = getOrCreateEntry(
enumChangelog,
const changes = getOrCreateChanges(
enumLogs,
getVersionString(firstVersion),
);
entry.changes.push(Log.added(TargetType.Enum, enumType.name));
changes.push(Log.added(TargetType.Enum, enumType.name));
}
}
}
Expand All @@ -134,13 +128,13 @@ function logEnumAdditions(
function logEnumRemovals(
context: EmitContext,
enumType: Enum,
enumChangelog: ChangelogEntry[],
enumLogs: { [version: string]: ChangeRecord[] },
): void {
const enumRemovedVersions = getRemovedOnVersions(context.program, enumType);
if (enumRemovedVersions && enumRemovedVersions.length > 0) {
for (const version of enumRemovedVersions) {
const entry = getOrCreateEntry(enumChangelog, getVersionString(version));
entry.changes.push(Log.removed(TargetType.Enum, enumType.name));
const changes = getOrCreateChanges(enumLogs, getVersionString(version));
changes.push(Log.removed(TargetType.Enum, enumType.name));
}
}
}
Expand All @@ -155,13 +149,13 @@ function logEnumRemovals(
function logEnumMemberAdditions(
context: EmitContext,
member: EnumMember,
enumChangelog: ChangelogEntry[],
enumLogs: { [version: string]: ChangeRecord[] },
): void {
const memberAddedVersions = getAddedOnVersions(context.program, member);
if (memberAddedVersions && memberAddedVersions.length > 0) {
for (const version of memberAddedVersions) {
const entry = getOrCreateEntry(enumChangelog, getVersionString(version));
entry.changes.push(Log.added(TargetType.EnumMember, member.name));
const changes = getOrCreateChanges(enumLogs, getVersionString(version));
changes.push(Log.added(TargetType.EnumMember, member.name));
}
}
}
Expand All @@ -172,13 +166,13 @@ function logEnumMemberAdditions(
function logEnumMemberRemovals(
context: EmitContext,
member: EnumMember,
enumChangelog: ChangelogEntry[],
enumLogs: { [version: string]: ChangeRecord[] },
): void {
const memberRemovedVersions = getRemovedOnVersions(context.program, member);
if (memberRemovedVersions && memberRemovedVersions.length > 0) {
for (const version of memberRemovedVersions) {
const entry = getOrCreateEntry(enumChangelog, getVersionString(version));
entry.changes.push(Log.removed(TargetType.EnumMember, member.name));
const changes = getOrCreateChanges(enumLogs, getVersionString(version));
changes.push(Log.removed(TargetType.EnumMember, member.name));
}
}
}
29 changes: 12 additions & 17 deletions lib/changelog-emitter/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangelogEntry } from "../types.js";
import { ChangeRecord } from "../types.js";
import { Version } from "@typespec/versioning";

/**
Expand All @@ -22,27 +22,22 @@ export function getVersionString(version: Version): string {
}

/**
* Get or create a changelog entry for a specific version.
* If an entry for the version already exists, it returns that entry.
* Otherwise, it creates a new entry with an empty changes array.
* Get or create a changes array for a specific schema and version.
* If changes for the version already exist, return that array.
* Otherwise, create a new empty array and return it.
*
* @param changelog - The changelog array to search or add to
* @param schemaLogs - The logs for a specific schema { [version: string]: ChangeRecord[] }
* @param versionName - The name of the version to find or create
* @returns The existing or newly created changelog entry
* @returns The existing or newly created changes array
*/
export function getOrCreateEntry(
changelog: ChangelogEntry[],
export function getOrCreateChanges(
schemaLogs: { [version: string]: ChangeRecord[] },
versionName: string,
): ChangelogEntry {
let entry = changelog.find((e) => e.version === versionName);
if (!entry) {
entry = {
version: versionName,
changes: [],
};
changelog.push(entry);
): ChangeRecord[] {
if (!schemaLogs[versionName]) {
schemaLogs[versionName] = [];
}
return entry;
return schemaLogs[versionName];
}

/**
Expand Down
Loading