From faa970f2b051bc0945b20b82502fe1bd0d2fcec5 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 22 Dec 2024 00:24:17 -0800 Subject: [PATCH] Refactor FrameButton component to update close button styling and add support for displaying full changelog --- packages/studiocms_core/package.json | 10 +- .../src/routes/full-changelog.json.ts | 182 +++++++++++++++++- .../src/components/FrameButton.astro | 1 + pnpm-lock.yaml | 18 ++ 4 files changed, 208 insertions(+), 3 deletions(-) diff --git a/packages/studiocms_core/package.json b/packages/studiocms_core/package.json index c176d7b0a5..51ea2844f4 100644 --- a/packages/studiocms_core/package.json +++ b/packages/studiocms_core/package.json @@ -66,7 +66,12 @@ "@matthiesenxyz/astrodtsbuilder": "catalog:studiocms-shared", "@matthiesenxyz/integration-utils": "catalog:studiocms-shared", "drizzle-orm": "catalog:studiocms-core", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "mdast": "3.0.0", + "mdast-util-from-markdown": "2.0.2", + "mdast-util-to-markdown": "2.1.2", + "mdast-util-to-string": "4.0.0", + "unist-util-visit": "5.0.0" }, "peerDependencies": { "@astrojs/db": "catalog:min", @@ -75,6 +80,7 @@ }, "devDependencies": { "typescript": "catalog:", - "@types/lodash": "^4.17.13" + "@types/lodash": "^4.17.13", + "@types/mdast": "4.0.4" } } diff --git a/packages/studiocms_core/src/routes/full-changelog.json.ts b/packages/studiocms_core/src/routes/full-changelog.json.ts index 5f7db914dc..648bec39a1 100644 --- a/packages/studiocms_core/src/routes/full-changelog.json.ts +++ b/packages/studiocms_core/src/routes/full-changelog.json.ts @@ -1,6 +1,32 @@ import contentRenderer from 'studiocms:renderer/current'; import type { APIRoute } from 'astro'; import { HTMLString } from 'astro/runtime/server/index.js'; +import type { List, Root } from 'mdast'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { toMarkdown } from 'mdast-util-to-markdown'; +import { toString as ToString } from 'mdast-util-to-string'; +import { visit } from 'unist-util-visit'; + +export type Changelog = { + packageName: string; + versions: Version[]; +}; + +export type Version = { + version: string; + changes: { [key in SemverCategory]: List }; + includes: Set; +}; + +export const semverCategories = ['major', 'minor', 'patch'] as const; +export type SemverCategory = (typeof semverCategories)[number]; + +function parsePackageReference(str: string) { + const matches = str.match(/^([@/a-z0-9-]+)@([0-9.]+)$/); + if (!matches) return; + const [, packageName, version] = matches; + return { packageName, version }; +} export const GET: APIRoute = async () => { const Changelog = await fetch( @@ -10,7 +36,161 @@ export const GET: APIRoute = async () => { const ChangelogText = await Changelog.text(); if (ChangelogText) { - const renderedContent = await contentRenderer(ChangelogText); + let markdown = ChangelogText; + + // Convert GitHub usernames in "Thanks ..." sentences to links + markdown = markdown.replace( + /(?<=Thank[^.!]*? )@([a-z0-9-]+)(?=[\s,.!])/gi, + '[@$1](https://github.com/$1)' + ); + + const astStart = fromMarkdown(markdown); + + const changelog: Changelog = { + packageName: '', + versions: [], + }; + + type ParserState = 'packageName' | 'version' | 'semverCategory' | 'changes'; + let state: ParserState = 'packageName'; + let version: Version | undefined; + let semverCategory: SemverCategory | undefined; + + function handleNode(node: ReturnType['children'][number]) { + if (node.type === 'heading') { + if (node.depth === 1) { + if (state !== 'packageName') throw new Error('Unexpected h1'); + changelog.packageName = ToString(node); + state = 'version'; + return; + } + if (node.depth === 2) { + if (state === 'packageName') throw new Error('Unexpected h2'); + version = { + version: ToString(node), + changes: { + major: { type: 'list', children: [] }, + minor: { type: 'list', children: [] }, + patch: { type: 'list', children: [] }, + }, + includes: new Set(), + }; + changelog.versions.push(version); + state = 'semverCategory'; + return; + } + if (node.depth === 3) { + if (state === 'packageName' || state === 'version') throw new Error('Unexpected h3'); + semverCategory = (ToString(node).split(' ')[0] || '').toLowerCase() as SemverCategory; + if (!semverCategories.includes(semverCategory)) + throw new Error(`Unexpected semver category: ${semverCategory}`); + state = 'changes'; + return; + } + } + if (node.type === 'list') { + if (state !== 'changes' || !version || !semverCategory) throw new Error('Unexpected list'); + // Go through list items + for (let listItemIdx = 0; listItemIdx < node.children.length; listItemIdx++) { + const listItem = node.children[listItemIdx]; + if (!listItem) continue; + + // Check if the current list item ends with a nested sublist that consists + // of items matching the pattern `@` + const lastChild = listItem.children[listItem.children.length - 1]; + if (lastChild?.type === 'list') { + const packageRefs: string[] = []; + // biome-ignore lint/complexity/noForEach: + lastChild.children.forEach((subListItem) => { + const text = ToString(subListItem); + if (parsePackageReference(text)) packageRefs.push(text); + }); + if (packageRefs.length === lastChild.children.length) { + // If so, add the packages to `includes` + for (const packageRef of packageRefs) { + version.includes.add(packageRef); + } + // Remove the sub-list from the list item + listItem.children.pop(); + } + } + + const firstPara = + listItem.children[0]?.type === 'paragraph' ? listItem.children[0] : undefined; + if (firstPara) { + // Remove IDs like `bfed62a: ...` or `... [85dbab8]` from the first paragraph + visit(firstPara, 'text', (textNode) => { + textNode.value = textNode.value.replace(/(^[0-9a-f]{7,}: | \[[0-9a-f]{7,}\]$)/, ''); + }); + // Skip list items that only contain the text `Updated dependencies` + const firstParaText = ToString(firstPara); + if (firstParaText === 'Updated dependencies') continue; + // If the list item is a package reference, add it to `includes` instead + const packageRef = parsePackageReference(firstParaText); + if (packageRef) { + version.includes.add(firstParaText); + continue; + } + // Add the list item to the changes + version.changes[semverCategory].children.push(listItem); + } + } + return; + } + throw new Error(`Unexpected node: ${JSON.stringify(node)}`); + } + + // biome-ignore lint/complexity/noForEach: + astStart.children.forEach((node) => { + handleNode(node); + }); + + const ToProcess = changelog; + + const output: string[] = []; + + const astEnd: Root = { + type: 'root', + children: [], + }; + + for (const version of ToProcess.versions) { + const versionChanges: List = { type: 'list', children: [] }; + + for (const semverCategory of semverCategories) { + for (const listItem of version.changes[semverCategory].children) { + versionChanges.children.push(listItem); + } + } + + if (version.includes.size) { + versionChanges.children.push({ + type: 'listItem', + children: [ + { + type: 'paragraph', + children: [{ type: 'text', value: `Includes: ${[...version.includes].join(', ')} ` }], + }, + ], + }); + } + + if (!versionChanges.children.length) continue; + + astEnd.children.push({ + type: 'heading', + depth: 2, + children: [{ type: 'text', value: version.version }], + }); + + astEnd.children.push(versionChanges); + } + + output.push(toMarkdown(astEnd, { bullet: '-' })); + + const markdownString = output.join('\n'); + + const renderedContent = await contentRenderer(markdownString); return new Response( JSON.stringify({ success: true, changelog: new HTMLString(renderedContent) }), diff --git a/packages/studiocms_dashboard/src/components/FrameButton.astro b/packages/studiocms_dashboard/src/components/FrameButton.astro index 0ba2c5c7d4..8707b5976b 100644 --- a/packages/studiocms_dashboard/src/components/FrameButton.astro +++ b/packages/studiocms_dashboard/src/components/FrameButton.astro @@ -86,6 +86,7 @@ const { link } = Astro.props; const jsonRes = await linkResponse.json(); frameContent.innerHTML = ` +

Full Changelog

Read more on GitHub ${jsonRes.changelog} `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 191ba90731..690a0271ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -643,9 +643,21 @@ importers: lodash: specifier: ^4.17.21 version: 4.17.21 + mdast: + specifier: 3.0.0 + version: 3.0.0 + mdast-util-from-markdown: + specifier: 2.0.2 + version: 2.0.2 mdast-util-to-hast: specifier: catalog:studiocms-core version: 13.2.0 + mdast-util-to-markdown: + specifier: 2.1.2 + version: 2.1.2 + mdast-util-to-string: + specifier: 4.0.0 + version: 4.0.0 mrmime: specifier: catalog:studiocms-core version: 2.0.0 @@ -658,6 +670,9 @@ importers: unified: specifier: catalog:studiocms-core version: 11.0.5 + unist-util-visit: + specifier: 5.0.0 + version: 5.0.0 vite: specifier: catalog:min version: 6.0.3(@types/node@22.10.2)(jiti@2.4.1)(yaml@2.6.1) @@ -665,6 +680,9 @@ importers: '@types/lodash': specifier: ^4.17.13 version: 4.17.13 + '@types/mdast': + specifier: 4.0.4 + version: 4.0.4 typescript: specifier: 'catalog:' version: 5.7.2