feat(updater): sanitize release notes and standardize semver

This commit is contained in:
Tunglies
2026-08-29 11:52:08 +08:00
parent b6de05ffe4
commit 598b5e690c
4 changed files with 281 additions and 64 deletions
+47 -4
View File
@@ -58,16 +58,50 @@ const GITHUB_ALERT_PATTERN =
/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][\t ]*\n?/i
const GITHUB_ALERT_CLASS_PATTERN =
/markdown-alert-(note|tip|important|warning|caution)/
const UPDATE_MARKDOWN_ID_PREFIX = 'update-markdown-'
const shouldShowReleaseNotes = (language: string) => language === 'zh'
const LazyReactMarkdown = lazy(async () => {
const [{ default: ReactMarkdown }, { default: rehypeRaw }] =
await Promise.all([import('react-markdown'), import('rehype-raw')])
const [
{ default: ReactMarkdown },
{ default: rehypeRaw },
{ default: rehypeSanitize, defaultSchema },
{ default: remarkGfm },
] = await Promise.all([
import('react-markdown'),
import('rehype-raw'),
import('rehype-sanitize'),
import('remark-gfm'),
])
const sanitizeSchema = {
...defaultSchema,
clobberPrefix: UPDATE_MARKDOWN_ID_PREFIX,
attributes: {
...defaultSchema.attributes,
blockquote: [
...(defaultSchema.attributes?.blockquote ?? []),
[
'className',
'markdown-alert',
...Object.keys(GITHUB_ALERTS).map((type) => `markdown-alert-${type}`),
],
],
p: [
...(defaultSchema.attributes?.p ?? []),
['className', 'markdown-alert-title'],
],
},
}
return {
default: (props: ReactMarkdownOptions) => (
<ReactMarkdown {...props} rehypePlugins={[rehypeRaw]} />
<ReactMarkdown
{...props}
remarkPlugins={[remarkGfm, ...(props.remarkPlugins ?? [])]}
rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeSchema]]}
/>
),
}
})
@@ -425,18 +459,27 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
remarkPlugins={[remarkGitHubAlertsPlugin]}
components={{
a: ({ href, children, ...props }) => {
const isFragment = href?.startsWith('#') ?? false
const renderedHref = isFragment
? `#${UPDATE_MARKDOWN_ID_PREFIX}${href?.slice(1)}`
: href
? '#'
: undefined
return (
<a
{...props}
href={href ? '#' : undefined}
href={renderedHref}
target={undefined}
rel={undefined}
onClick={(event) => {
if (isFragment) return
event.preventDefault()
if (!href) return
openUrlWithNotice(href)
}}
onAuxClick={(event) => {
if (isFragment) return
event.preventDefault()
if (event.button === 1 && href) {
openUrlWithNotice(href)
+7 -60
View File
@@ -3,14 +3,10 @@ import {
type CheckOptions,
type Update,
} from '@tauri-apps/plugin-updater'
import { compareVersions as compareSemver } from 'compare-versions'
import { version as appVersion } from '@root/package.json'
type VersionParts = {
main: number[]
pre: (number | string)[]
}
const SEMVER_FULL_REGEX =
/^\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
const SEMVER_SEARCH_REGEX =
@@ -36,62 +32,13 @@ const extractSemver = (input: string | null | undefined): string | null => {
return normalizeVersion(match[0])
}
const splitVersion = (version: string | null): VersionParts | null => {
if (!version) return null
const [mainPart, preRelease] = version.split('-')
const main = mainPart
.split('.')
.map((part) => Number.parseInt(part, 10))
.map((num) => (Number.isNaN(num) ? 0 : num))
const pre =
preRelease?.split('.').map((token) => {
const numeric = Number.parseInt(token, 10)
return Number.isNaN(numeric) ? token : numeric
}) ?? []
return { main, pre }
}
const compareVersionParts = (a: VersionParts, b: VersionParts): number => {
const length = Math.max(a.main.length, b.main.length)
for (let i = 0; i < length; i += 1) {
const diff = (a.main[i] ?? 0) - (b.main[i] ?? 0)
if (diff !== 0) return diff > 0 ? 1 : -1
}
if (a.pre.length === 0 && b.pre.length === 0) return 0
if (a.pre.length === 0) return 1
if (b.pre.length === 0) return -1
const preLen = Math.max(a.pre.length, b.pre.length)
for (let i = 0; i < preLen; i += 1) {
const aToken = a.pre[i]
const bToken = b.pre[i]
if (aToken === undefined) return -1
if (bToken === undefined) return 1
if (typeof aToken === 'number' && typeof bToken === 'number') {
if (aToken > bToken) return 1
if (aToken < bToken) return -1
continue
}
if (typeof aToken === 'number') return -1
if (typeof bToken === 'number') return 1
if (aToken > bToken) return 1
if (aToken < bToken) return -1
}
return 0
}
const compareVersions = (a: string | null, b: string | null): number | null => {
const partsA = splitVersion(a)
const partsB = splitVersion(b)
if (!partsA || !partsB) return null
return compareVersionParts(partsA, partsB)
if (!a || !b) return null
try {
return compareSemver(a, b)
} catch {
return null
}
}
const resolveRemoteVersion = (update: Update): string | null => {