Skip to content

refactor: combining shared code markdown renderer readme & changelog #2717

Open
WilcoSp wants to merge 236 commits into
npmx-dev:mainfrom
WilcoSp:feat/markdown-kit
Open

refactor: combining shared code markdown renderer readme & changelog #2717
WilcoSp wants to merge 236 commits into
npmx-dev:mainfrom
WilcoSp:feat/markdown-kit

Conversation

@WilcoSp
Copy link
Copy Markdown
Contributor

@WilcoSp WilcoSp commented May 11, 2026

🔗 Linked issue

This pr is part of #501

🧭 Context

Currently changelog uses a seperate marked markdown renderer from readme, this pr takes the shared code and makes it into a "markdown kit" so that code that can be shared will be shared

📚 Description

Moves shared code for both changelog & readme to a shared markdown kit while still allowing for each to have their unique needs.

additional changes

  • Heading have been changed to be more consistent and easier to use by users to the following:
    ## heading -> <hx><a href="#...>heading</a></hx>
    ## [Example](https://example.com) -> <hx><a href="https://example.com">Example</a><a href="#...></a></hx>
  • Tests have been added for the changelog markdown renderer for what not shared or changed for changelog.
  • Thanks to the tests also some bugs have been fixed.
  • Also for changelog.md the jumping to the requested version has been fixed, now if "3.5.1" is requested then "3.5.11" won't be selected.
  • 502 will now only be returned when ungh.cc is exhausted, others have been changed to 500
  • at package.takumi.vue & externallinks.vue the provider icons are now given via the useProviderIcon composable
  • fixed a bug to where when switching between package tabs that the message "package doesn't have changelogs" is being shown while earlier it did have it.
  • changed how the skeleton loaders is shown in package changelog, now pending is being used from useLazyFetch in the ChangelogReleases & ChangelogMarkdown components instead of using <suspense> at the package-changelog page, this should remove/reduce the blank screen between showing the skeleton laoders & showing data.
  • some further smaller refactors

AI usage

For app/utils/header-version-matcher.ts I've used Gemini to get the regex for matching more exact the requested version in a toc item text

notes

The branch of this pr was based on #1233 initially, that's why there are so many commits, file changes does show correctly what has changed

WilcoSp and others added 30 commits February 4, 2026 21:17
excludes changelog releases due to needing api calls
…gelog info endpoint will return ChangelogMarkdownInfo instead of ChangelogReleaseInfo
WilcoSp added 2 commits May 12, 2026 15:13
provider icons are now suplied by the useProviderIcon composable
…properly anymore, this caused "package doesnt have changelog" to appear even if it does have it
removed <suspense> & now using `pending` to show the skeletons in the releases & markdown components.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/pages/package-changelog/[[org]]/[name].vue (1)

132-173: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make loading/content/error branches mutually exclusive.

At Line 132, the skeleton uses v-if="!changelog && !changelogError", while Line 139 starts a separate v-if chain. This allows mixed states (e.g. skeleton plus fallback text) instead of a single deterministic UI state.

Suggested fix
-const { data: changelog, error: changelogError } = usePackageChangelog(packageName, version)
+const { data: changelog, error: changelogError, pending: changelogPending } = usePackageChangelog(
+  packageName,
+  version,
+)
-      <section v-if="!changelog && !changelogError" class="flex flex-col gap-2 py-3">
+      <section v-if="changelogPending" class="flex flex-col gap-2 py-3">
         <ChangelogSkeleton />
       </section>
@@
-      <LazyChangelogReleases
-        v-if="changelog?.type === 'release'"
+      <LazyChangelogReleases
+        v-else-if="changelog?.type === 'release'"
@@
-      <LazyChangelogMarkdown
-        v-else-if="changelog?.type === 'md'"
+      <LazyChangelogMarkdown
+        v-else-if="changelog?.type === 'md'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/package-changelog/`[[org]]/[name].vue around lines 132 - 173, The
UI branches (ChangelogSkeleton, LazyChangelogReleases, LazyChangelogMarkdown and
error) are not mutually exclusive because ChangelogSkeleton uses its own v-if
while the lazy components start a separate v-if chain; change to a single
conditional chain using the same root conditions (e.g., if changelogError ->
show error, else-if changelog?.type==='release' -> render LazyChangelogReleases,
else-if changelog?.type==='md' -> render LazyChangelogMarkdown, else -> render
ChangelogSkeleton) and remove the standalone v-if on ChangelogSkeleton so only
one branch (using the shared symbols changelog, changelogError,
LazyChangelogReleases, LazyChangelogMarkdown, ChangelogSkeleton,
resolvingPending) can render at a time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/composables/usePackageChangelog.ts`:
- Around line 27-38: The state is being initialized with the ComputedRef itself
instead of its boolean value; change the initializer passed to useState in
usePackageHasChangelogFromState (the call useState(key, () => hasChangelog)) to
return hasChangelog.value so the state stores a boolean, i.e. useState(key, ()
=> hasChangelog.value), ensuring key, hasChangelog and the returned state remain
consistent with consumers expecting a boolean.

In `@app/utils/header-version-matcher.ts`:
- Line 9: The versionRegex builds a RegExp from raw version text and only
escapes dots, allowing other regex metacharacters to break/weaponize the
pattern; fix by escaping all regex special characters in the incoming version
string before constructing versionRegex (add a small helper like escapeRegex
that scans the version value and prefixes every regex metacharacter with a
backslash), then use the escaped result when creating the RegExp assigned to
versionRegex (keep the existing surrounding anchors like (?<!\\d) and \\b
intact).

In `@server/api/changelog/md/`[provider]/[owner]/[repo]/[...path].get.ts:
- Line 34: getGithubMarkDown currently lets ungh.cc exhaustion responses
(FetchError with status 403 or 429) pass through unchanged via handleApiError,
so the endpoint returns raw statuses; update getGithubMarkDown to detect a
FetchError with status 403 or 429 (same logic as detectChangelog.ts lines
~100-105) and convert it to a 502 with the ERROR_UNGH_API_KEY_EXHAUSTED code
before returning, ensuring the endpoint normalises exhaustion errors to 502
rather than returning the upstream 403/429; keep handleApiError usage for other
errors but short-circuit exhaustion handling inside getGithubMarkDown.

In `@server/utils/changelog/detectChangelog.ts`:
- Line 90: The directory boundary check in detectChangelog.ts uses
path.startsWith(directory) which can match sibling prefixes; update the
condition to require a real path boundary by checking either path === directory
or that path.startsWith(directory + "/") (or use path.sep for cross-platform
safety), keeping the existing ROOT_ONLY_REGEX test; change the if that
references directory and path so only true subpaths of directory (or the
directory itself) pass.

In `@server/utils/changelog/markdown.ts`:
- Around line 28-32: The call to marked.use() is inside changelogRenderer(),
causing markedHeadingExtension to be registered repeatedly; move the
marked.use({ tokenizer: { heading: markedHeadingExtension } }) call to module
scope (outside changelogRenderer) so the extension is registered once when the
module loads, and remove the marked.use invocation from inside changelogRenderer
to prevent accumulation/recursion issues.

In `@server/utils/mdKit.ts`:
- Around line 256-262: The fallback empty permalink created when
htmlAnchorRe.test(displayHtml) is true produces an unnamed, focusable link;
update the rendering in the mdKit heading logic (the branch that returns
`<h${semanticLevel} id="${id}" ...>${displayHtml}<a
href="#${id}"></a></h${semanticLevel}>`) to include an accessible name (e.g.
aria-label or visually-hidden text) on that inner anchor using the existing
id/displayHtml to generate a descriptive label, and then update the sanitisation
allowlist to permit that aria attribute so it isn't stripped; apply the same
change to the other heading fallback branch referenced around lines 351-353 so
both cases have labeled permalinks (refer to htmlAnchorRe, displayHtml, id,
semanticLevel, preservedAttrs to locate the code and sanitiser).
- Line 203: The regex htmlAnchorRe is non-global so replace only rewrites the
first anchor; make it global (e.g. add the g flag) and ensure the replace call
uses a replacement function (callback) so every match is rewritten and
playground labels/URL collection logic runs per-anchor; apply the same change to
the other non-global anchor regex used later in this file (the second
anchor-processing block that handles playground collection) so all raw HTML
anchors are processed, not just the first.

In `@test/nuxt/app/utils/header-version-matcher.spec.ts`:
- Line 5: Update the test description string in the spec case for
header-version-matcher (the it(...) call in header-version-matcher.spec.ts) to
fix the typo: change "hould only match version 3.5.1" to "should only match
version 3.5.1" so the test description reads correctly; ensure you edit the
string passed to the it function (the test title) and save the spec.

---

Outside diff comments:
In `@app/pages/package-changelog/`[[org]]/[name].vue:
- Around line 132-173: The UI branches (ChangelogSkeleton,
LazyChangelogReleases, LazyChangelogMarkdown and error) are not mutually
exclusive because ChangelogSkeleton uses its own v-if while the lazy components
start a separate v-if chain; change to a single conditional chain using the same
root conditions (e.g., if changelogError -> show error, else-if
changelog?.type==='release' -> render LazyChangelogReleases, else-if
changelog?.type==='md' -> render LazyChangelogMarkdown, else -> render
ChangelogSkeleton) and remove the standalone v-if on ChangelogSkeleton so only
one branch (using the shared symbols changelog, changelogError,
LazyChangelogReleases, LazyChangelogMarkdown, ChangelogSkeleton,
resolvingPending) can render at a time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69ba898c-81f3-4bfd-895e-e5659a10d3b7

📥 Commits

Reviewing files that changed from the base of the PR and between 01e3d83 and 11acac8.

📒 Files selected for processing (24)
  • app/components/Changelog/Markdown.vue
  • app/components/Changelog/Releases.vue
  • app/components/Changelog/Skeleton.vue
  • app/components/OgImage/Package.takumi.vue
  • app/components/Package/ExternalLinks.vue
  • app/components/Package/Header.vue
  • app/composables/useCommandPalettePackageCommands.ts
  • app/composables/usePackageChangelog.ts
  • app/composables/useProviderIcon.ts
  • app/pages/package-changelog/[[org]]/[name].vue
  • app/utils/header-version-matcher.ts
  • app/utils/router.ts
  • server/api/changelog/info/[name]/v/[version].get.ts
  • server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts
  • server/api/changelog/releases/[provider]/[owner]/[repo].get.ts
  • server/utils/changelog/detectChangelog.ts
  • server/utils/changelog/markdown.ts
  • server/utils/mdKit.ts
  • server/utils/readme.ts
  • test/nuxt/a11y.spec.ts
  • test/nuxt/app/utils/header-version-matcher.spec.ts
  • test/unit/a11y-component-coverage.spec.ts
  • test/unit/server/utils/changelog/markdown.spec.ts
  • test/unit/server/utils/readme.spec.ts

Comment thread app/composables/usePackageChangelog.ts
Comment thread app/utils/header-version-matcher.ts Outdated
Comment thread server/api/changelog/md/[provider]/[owner]/[repo]/[...path].get.ts
Comment thread server/utils/changelog/detectChangelog.ts Outdated
Comment thread server/utils/changelog/markdown.ts
Comment thread server/utils/mdKit.ts Outdated
Comment thread server/utils/mdKit.ts
Comment thread test/nuxt/app/utils/header-version-matcher.spec.ts Outdated
@gameroman gameroman added the needs review This PR is waiting for a review from a maintainer label May 15, 2026
@WilcoSp
Copy link
Copy Markdown
Contributor Author

WilcoSp commented May 15, 2026

for #2717 (review)
this is on purpose because it prevents flashing the skeleton loader when switching versions by using the previous result from useLazyFetch while checking for new info.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/components/Changelog/Card.vue (1)

24-27: ⚡ Quick win

Drop the non-null assertion on inject and supply a safe default.

inject<{...}>('changelog-provider-linkattr')! only affects TypeScript; if any future caller (tests, Storybook, another consumer) renders Card.vue without the parent providing 'changelog-provider-linkattr', the destructuring will throw at runtime. Either provide a default object or guard the destructuring so the card degrades gracefully.

♻️ Suggested refactor
-const { providerIcon, viewOnProvider } = inject<{
-  providerIcon: ComputedRef<IconClass>
-  viewOnProvider: ComputedRef<string>
-}>('changelog-provider-linkattr')!
+const injected = inject<{
+  providerIcon: ComputedRef<IconClass>
+  viewOnProvider: ComputedRef<string>
+} | null>('changelog-provider-linkattr', null)
+const providerIcon = computed(() => injected?.providerIcon.value)
+const viewOnProvider = computed(() => injected?.viewOnProvider.value)

You can then v-if="providerIcon" around the LinkBase to skip rendering when the context is absent.

Also consider using a typed InjectionKey<...> exported from a shared module instead of a raw string — it removes the need to repeat the generic at every call site and prevents key drift between provider and consumer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/Changelog/Card.vue` around lines 24 - 27, The inject call in
Card.vue uses a non-null assertion (inject<...>('changelog-provider-linkattr')!)
which can throw at runtime if the provider is missing; change it to supply a
safe default or guard the result before destructuring: call
inject('changelog-provider-linkattr') without the bang, check for null/undefined
and fall back to a default object providing safe ComputedRefs for providerIcon
and viewOnProvider, then only render the LinkBase when providerIcon is present
(e.g., v-if="providerIcon"); optionally replace the raw string key with an
exported typed InjectionKey to keep provider/consumer keys consistent
(referencing providerIcon, viewOnProvider, inject, and
'changelog-provider-linkattr').
app/pages/package-changelog/[[org]]/[name].vue (2)

141-185: ⚡ Quick win

Remove the commented-out <Suspense> block instead of leaving it inline.

Per the PR objectives the Suspense-based loading was intentionally replaced with useLazyFetch's pending inside ChangelogReleases/ChangelogMarkdown. Leaving ~45 lines of commented template (lines 141–185) creates noise, hides the actual control flow (the v-if/v-else-if chain reads awkwardly with comments interleaved), and will rot quickly. Git history is sufficient for recovery.

♻️ Suggested cleanup
-      <!-- <Suspense v-else-if="changelog"> 
-        <template `#default`>-->
-      <LazyChangelogReleases
+      <LazyChangelogReleases
         v-if="changelog?.type === 'release'"
         :info="changelog"
@@
         />
       </LazyChangelogMarkdown>
-      <!-- </template>
-        <template `#fallback`>
-          <section class="flex flex-col gap-2 py-3">
-            <SkeletonBlock class="h-8 w-40 rounded" />
-            <ul class="ms-3 list-disc my-[1rem] ps-[1.5rem] marker:color-border-hover">
-              <li class="mb-1" v-for="_n in 5">
-                <SkeletonBlock class="h-7 w-full max-w-2xl rounded" />
-              </li>
-            </ul>
-
-            <SkeletonBlock class="h-5 w-5/6 max-w-2xl rounded" />
-            <SkeletonBlock class="h-5 w-3/4 max-w-2xl rounded" />
-          </section>
-        </template>
-      </Suspense> -->
       <!-- error handling -->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/package-changelog/`[[org]]/[name].vue around lines 141 - 185,
Remove the large commented-out Suspense block and its inner fallback template so
the template contains only the active v-if/v-else-if chain; delete the commented
"<Suspense> ... </Suspense>" section wrapping (and any commented fallback
markup) that surrounds the LazyChangelogReleases and LazyChangelogMarkdown
usage, leaving the LazyChangelogReleases, LazyChangelogMarkdown and their
LazyChangelogErrorMsg children intact; ensure no other logic relies on that
Suspense wrapper (the components ChangelogReleases/ChangelogMarkdown now handle
loading via useLazyFetch pending) and run a quick lint/format pass to confirm
the template remains valid.

151-170: 💤 Low value

Rename viewOnGit prop to viewOnProvider in ChangelogErrorMsg component for consistency.

The variable was renamed to generic provider terminology in line 51 (viewOnProvider), but the component's prop on lines 154 and 168 still uses :viewOnGit="viewOnProvider". Update the prop name in app/components/Changelog/ErrorMsg.vue to maintain uniform abstraction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/package-changelog/`[[org]]/[name].vue around lines 151 - 170, The
Changelog error component still defines/accepts the old prop name viewOnGit
while callers (LazyChangelogErrorMsg) pass viewOnProvider; open
app/components/Changelog/ErrorMsg.vue and rename the prop from viewOnGit to
viewOnProvider (keep the same type/default/behavior), update any internal
references in the component (props destructuring, template bindings, methods) to
use viewOnProvider, and ensure the component export/props declaration matches
the new name so the usages at lines where LazyChangelogErrorMsg is rendered work
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/components/Changelog/Card.vue`:
- Around line 24-27: The inject call in Card.vue uses a non-null assertion
(inject<...>('changelog-provider-linkattr')!) which can throw at runtime if the
provider is missing; change it to supply a safe default or guard the result
before destructuring: call inject('changelog-provider-linkattr') without the
bang, check for null/undefined and fall back to a default object providing safe
ComputedRefs for providerIcon and viewOnProvider, then only render the LinkBase
when providerIcon is present (e.g., v-if="providerIcon"); optionally replace the
raw string key with an exported typed InjectionKey to keep provider/consumer
keys consistent (referencing providerIcon, viewOnProvider, inject, and
'changelog-provider-linkattr').

In `@app/pages/package-changelog/`[[org]]/[name].vue:
- Around line 141-185: Remove the large commented-out Suspense block and its
inner fallback template so the template contains only the active v-if/v-else-if
chain; delete the commented "<Suspense> ... </Suspense>" section wrapping (and
any commented fallback markup) that surrounds the LazyChangelogReleases and
LazyChangelogMarkdown usage, leaving the LazyChangelogReleases,
LazyChangelogMarkdown and their LazyChangelogErrorMsg children intact; ensure no
other logic relies on that Suspense wrapper (the components
ChangelogReleases/ChangelogMarkdown now handle loading via useLazyFetch pending)
and run a quick lint/format pass to confirm the template remains valid.
- Around line 151-170: The Changelog error component still defines/accepts the
old prop name viewOnGit while callers (LazyChangelogErrorMsg) pass
viewOnProvider; open app/components/Changelog/ErrorMsg.vue and rename the prop
from viewOnGit to viewOnProvider (keep the same type/default/behavior), update
any internal references in the component (props destructuring, template
bindings, methods) to use viewOnProvider, and ensure the component export/props
declaration matches the new name so the usages at lines where
LazyChangelogErrorMsg is rendered work consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d2996937-03cf-43bc-ac75-a25809c790e4

📥 Commits

Reviewing files that changed from the base of the PR and between c335353 and 568acc2.

📒 Files selected for processing (4)
  • app/components/Changelog/Card.vue
  • app/pages/package-changelog/[[org]]/[name].vue
  • server/api/changelog/releases/[provider]/[owner]/[repo].get.ts
  • shared/types/changelog.ts
✅ Files skipped from review due to trivial changes (1)
  • server/api/changelog/releases/[provider]/[owner]/[repo].get.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs review This PR is waiting for a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants