@eventcatalog/core 2.7.2 → 2.7.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @eventcatalog/core
2
2
 
3
+ ## 2.7.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 1c56b8d: feat(core): added automatic diffs for changelogs for json, yml and avro files
8
+
3
9
  ## 2.7.2
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eventcatalog/core",
3
3
  "type": "module",
4
- "version": "2.7.2",
4
+ "version": "2.7.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -50,6 +50,8 @@
50
50
  "astro-pagefind": "^1.5.0",
51
51
  "astro-seo": "^0.8.4",
52
52
  "dagre": "^0.8.5",
53
+ "diff": "^7.0.0",
54
+ "diff2html": "^3.4.48",
53
55
  "glob": "^10.4.1",
54
56
  "html-to-image": "^1.11.11",
55
57
  "lodash.merge": "4.6.2",
@@ -67,6 +69,7 @@
67
69
  },
68
70
  "devDependencies": {
69
71
  "@parcel/watcher": "^2.4.1",
72
+ "@types/diff": "^5.2.2",
70
73
  "@types/lodash.merge": "4.6.9",
71
74
  "@types/react": "^18.3.3",
72
75
  "@types/react-dom": "^18.3.0",
@@ -30,6 +30,7 @@ const changelogs = defineCollection({
30
30
  catalog: z
31
31
  .object({
32
32
  path: z.string(),
33
+ absoluteFilePath: z.string(),
33
34
  filePath: z.string(),
34
35
  publicPath: z.string(),
35
36
  type: z.string(),
@@ -6,8 +6,11 @@ import type { PageTypes } from '@types';
6
6
  import { getChangeLogs } from '@utils/changelogs/changelogs';
7
7
  import { RectangleGroupIcon, ServerIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline';
8
8
  import { pageDataLoader } from '@utils/pages/pages';
9
+ import 'diff2html/bundles/css/diff2html.min.css';
9
10
 
10
11
  import { buildUrl } from '@utils/url-builder';
12
+ import { getVersions, getPreviousVersion } from '@utils/collections/util';
13
+ import { getDiffsForCurrentAndPreviousVersion } from '@utils/collections/file-diffs';
11
14
 
12
15
  export async function getStaticPaths() {
13
16
  const itemTypes: PageTypes[] = ['events', 'commands', 'services', 'domains'];
@@ -22,6 +25,8 @@ export async function getStaticPaths() {
22
25
  },
23
26
  props: {
24
27
  type: itemTypes[index],
28
+ allVersionsForCollection: getVersions(items).versions,
29
+ allCollectionItems: items,
25
30
  ...item,
26
31
  },
27
32
  }))
@@ -29,6 +34,7 @@ export async function getStaticPaths() {
29
34
  }
30
35
 
31
36
  const props = Astro.props;
37
+ let collectionItem = props;
32
38
  const logs = await getChangeLogs(props);
33
39
 
34
40
  const { data } = props;
@@ -44,15 +50,30 @@ const renderedLogs = await logs.map(async (log) => {
44
50
 
45
51
  const logsToRender = await Promise.all(renderedLogs);
46
52
 
47
- const logList = logsToRender.map((log, index) => ({
48
- id: log.id,
49
- url: buildUrl(`/docs/${props.collection}/${props.data.id}`),
50
- isLatest: log.data.version === latestVersion,
51
- version: log.data.version,
52
- createdAt: log.data.createdAt,
53
- badges: log.data.badges || [],
54
- Content: log.Content,
55
- }));
53
+ const logListPromise = logsToRender.map(async (log, index) => {
54
+ const currentLogVersion = log.data.version;
55
+ const previousLogVersion = log.data.version ? getPreviousVersion(log.data.version, props.allVersionsForCollection) : '';
56
+ return {
57
+ id: log.id,
58
+ url: buildUrl(`/docs/${props.collection}/${props.data.id}`),
59
+ isLatest: log.data.version === latestVersion,
60
+ version: log.data.version,
61
+ createdAt: log.data.createdAt,
62
+ badges: log.data.badges || [],
63
+ Content: log.Content,
64
+ diffs:
65
+ currentLogVersion && previousLogVersion
66
+ ? await getDiffsForCurrentAndPreviousVersion(
67
+ currentLogVersion,
68
+ previousLogVersion,
69
+ collectionItem.data.id,
70
+ props.allCollectionItems
71
+ )
72
+ : [],
73
+ };
74
+ });
75
+
76
+ const logList = await Promise.all(logListPromise);
56
77
 
57
78
  const getBadge = () => {
58
79
  if (props.collection === 'services') {
@@ -154,6 +175,7 @@ const badges = [getBadge()];
154
175
  <div class="prose prose-md !max-w-none py-2">
155
176
  <log.Content />
156
177
  </div>
178
+ {log.diffs && log.diffs.map((diff) => <div id="diff-container" set:html={diff} />)}
157
179
  </div>
158
180
  </div>
159
181
  </li>
@@ -0,0 +1,132 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { diffLines, type Change } from 'diff';
4
+ import { html, parse } from 'diff2html';
5
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from './util';
6
+ import type { CollectionEntry } from 'astro:content';
7
+ import type { CollectionTypes } from '@types';
8
+
9
+ const FILE_EXTENSIONS_TO_INCLUDE = ['.json', '.avro', '.yaml', '.yml'];
10
+
11
+ /**
12
+ * Generates a diff string in a unified diff format for two versions of a file.
13
+ * @param fileName - The name of the file being compared
14
+ * @param oldStr - The content of the old version of the file
15
+ * @param newStr - The content of the new version of the file
16
+ * @returns A string representing the diff in unified format
17
+ */
18
+ function generateDiffString(fileName: string, oldStr: string, newStr: string): string {
19
+ const diff = diffLines(oldStr, newStr);
20
+
21
+ // Check if there are any changes
22
+ const hasChanges = diff.some((part) => part.added || part.removed);
23
+
24
+ if (!hasChanges) return '';
25
+
26
+ let diffString = `diff --git a/${fileName} b/${fileName}\n`;
27
+ diffString += `--- a/${fileName}\n`;
28
+ diffString += `+++ b/${fileName}\n`;
29
+
30
+ diff.forEach((part: Change) => {
31
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
32
+ const lines = part.value.split('\n');
33
+ // Remove the last element if it's an empty string (which happens if the last line ends with a newline)
34
+ if (lines[lines.length - 1] === '') lines.pop();
35
+
36
+ lines.forEach((line: string) => {
37
+ diffString += `${prefix}${line}\n`;
38
+ });
39
+ });
40
+
41
+ return diffString;
42
+ }
43
+
44
+ /**
45
+ * Retrieves the list of files for diffing in a collection.
46
+ * @param collection - The collection entry to get files from
47
+ * @returns An array of objects containing file names and their directory paths
48
+ */
49
+ export async function getFilesForDiffInCollection(
50
+ collection: CollectionEntry<CollectionTypes>
51
+ ): Promise<Array<{ file: string; dir: string }>> {
52
+ // @ts-ignore
53
+ const pathToFolder = collection.catalog?.absoluteFilePath;
54
+ if (!pathToFolder) return [];
55
+
56
+ const dir = dirname(pathToFolder);
57
+ const allFilesInDirectory = await readdir(dir);
58
+
59
+ return allFilesInDirectory
60
+ .filter((file) => FILE_EXTENSIONS_TO_INCLUDE.some((ext) => file.endsWith(ext)))
61
+ .map((file) => ({ file, dir }));
62
+ }
63
+
64
+ /**
65
+ * Generates diffs for files between two versions of a collection.
66
+ * @param collections - Array of all collection entries
67
+ * @param id - The ID of the collection to compare
68
+ * @param versionA - The first version to compare
69
+ * @param versionB - The second version to compare
70
+ * @returns An array of diff strings for matching files between versions
71
+ */
72
+ async function getFilesDiffsBetweenVersions(
73
+ id: string,
74
+ collections: CollectionEntry<CollectionTypes>[],
75
+ versionA: string,
76
+ versionB: string
77
+ ): Promise<string[]> {
78
+ const [collectionA, collectionB] = await Promise.all([
79
+ getItemsFromCollectionByIdAndSemverOrLatest(collections, id, versionA),
80
+ getItemsFromCollectionByIdAndSemverOrLatest(collections, id, versionB),
81
+ ]);
82
+
83
+ if (collectionA.length === 0 || collectionB.length === 0) return [];
84
+
85
+ const [filesForCollectionA, filesForCollectionB] = await Promise.all([
86
+ getFilesForDiffInCollection(collectionA[0]),
87
+ getFilesForDiffInCollection(collectionB[0]),
88
+ ]);
89
+
90
+ if (filesForCollectionA.length === 0 || filesForCollectionB.length === 0) return [];
91
+
92
+ const matchingFiles = filesForCollectionA.filter((fileA) => filesForCollectionB.some((fileB) => fileB.file === fileA.file));
93
+
94
+ const filesToDiff = matchingFiles.map((file) => ({
95
+ file: file.file,
96
+ dirA: filesForCollectionA.find((f) => f.file === file.file)?.dir,
97
+ dirB: filesForCollectionB.find((f) => f.file === file.file)?.dir,
98
+ }));
99
+
100
+ const diffs = await Promise.all(
101
+ filesToDiff.map(async (file) => {
102
+ const [contentA, contentB] = await Promise.all([
103
+ file.dirA ? readFile(join(file.dirA, file.file), 'utf-8') : '',
104
+ file.dirB ? readFile(join(file.dirB, file.file), 'utf-8') : '',
105
+ ]);
106
+ return generateDiffString(file.file, contentB, contentA);
107
+ })
108
+ );
109
+
110
+ return diffs.filter((diff) => diff !== '');
111
+ }
112
+
113
+ /**
114
+ * Generates HTML diffs for files in a collection between the current and previous version.
115
+ * @param id - The ID of the collection
116
+ * @param version - The current version of the collection
117
+ * @param allCollectionItems - Array of all collection items
118
+ * @param versions - Array of all available versions
119
+ * @returns An array of HTML strings representing the diffs, or null if no previous version exists
120
+ */
121
+ export async function getDiffsForCurrentAndPreviousVersion(
122
+ currentVersion: string,
123
+ previousVersion: string,
124
+ collectionId: string,
125
+ allCollectionItems: CollectionEntry<CollectionTypes>[]
126
+ ): Promise<string[] | null> {
127
+ const diffs = await getFilesDiffsBetweenVersions(collectionId, allCollectionItems, currentVersion, previousVersion);
128
+
129
+ if (diffs.length === 0) return [];
130
+
131
+ return diffs.map((diff) => html(parse(diff), { drawFileList: false, matching: 'lines', outputFormat: 'side-by-side' }));
132
+ }
@@ -1,6 +1,11 @@
1
1
  import type { CollectionTypes } from '@types';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import { coerce, satisfies as satisfiesRange, validRange } from 'semver';
3
+ import { coerce, satisfies as satisfiesRange } from 'semver';
4
+
5
+ export const getPreviousVersion = (version: string, versions: string[]) => {
6
+ const index = versions.indexOf(version);
7
+ return index === -1 ? null : versions[index + 1];
8
+ };
4
9
 
5
10
  export const getVersions = (data: CollectionEntry<CollectionTypes>[]) => {
6
11
  const allVersions = data.map((item) => item.data.version).sort();