From 7a9569dec09c6c9cfa1148427d8fca55ca6090d9 Mon Sep 17 00:00:00 2001
From: wayyoungboy <1017761807@qq.com>
Date: Sun, 7 Jun 2026 06:25:11 +0800
Subject: [PATCH] Update docs links in version script
---
build/change-version.mjs | 110 +++++++++++++++++++++++++++++-----
build/change-version.test.mjs | 32 ++++++++++
2 files changed, 128 insertions(+), 14 deletions(-)
create mode 100644 build/change-version.test.mjs
diff --git a/build/change-version.mjs b/build/change-version.mjs
index c1433d5780e6..52868a4b95a7 100644
--- a/build/change-version.mjs
+++ b/build/change-version.mjs
@@ -8,7 +8,9 @@
import { execFile } from 'node:child_process'
import fs from 'node:fs/promises'
+import path from 'node:path'
import process from 'node:process'
+import { pathToFileURL } from 'node:url'
const VERBOSE = process.argv.includes('--verbose')
const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run')
@@ -23,6 +25,21 @@ const FILES = [
'site/data/docs-versions.yml'
]
+const DOCS_LINK_FILES = [
+ 'README.md'
+]
+
+const DOCS_LINK_DIRECTORIES = [
+ 'site/src'
+]
+
+const DOCS_LINK_EXTENSIONS = new Set([
+ '.astro',
+ '.html',
+ '.md',
+ '.mdx'
+])
+
// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37
function regExpQuote(string) {
return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&')
@@ -32,19 +49,27 @@ function regExpQuoteReplacement(string) {
return string.replace(/\$/g, '$$')
}
-async function replaceRecursively(file, oldVersion, newVersion) {
+export function shortVersion(version) {
+ return version.replace(/^v/, '').split('.').slice(0, 2).join('.')
+}
+
+export function replaceDocsShortVersionLinks(string, oldVersion, newVersion) {
+ const oldShortVersion = shortVersion(oldVersion)
+ const newShortVersion = shortVersion(newVersion)
+
+ if (oldShortVersion === newShortVersion) {
+ return string
+ }
+
+ return string.replace(
+ new RegExp(`/docs/${regExpQuote(oldShortVersion)}(?=/)`, 'g'),
+ `/docs/${regExpQuoteReplacement(newShortVersion)}`
+ )
+}
+
+async function replaceInFile(file, transform) {
const originalString = await fs.readFile(file, 'utf8')
- const newString = originalString
- .replace(
- new RegExp(regExpQuote(oldVersion), 'g'),
- regExpQuoteReplacement(newVersion)
- )
- // Also replace the version used by the rubygem,
- // which is using periods (`.`) instead of hyphens (`-`)
- .replace(
- new RegExp(regExpQuote(oldVersion.replace(/-/g, '.')), 'g'),
- regExpQuoteReplacement(newVersion.replace(/-/g, '.'))
- )
+ const newString = transform(originalString)
// No need to move any further if the strings are identical
if (originalString === newString) {
@@ -52,7 +77,7 @@ async function replaceRecursively(file, oldVersion, newVersion) {
}
if (VERBOSE) {
- console.log(`Found ${oldVersion} in ${file}`)
+ console.log(`Updating ${file}`)
}
if (DRY_RUN) {
@@ -62,6 +87,60 @@ async function replaceRecursively(file, oldVersion, newVersion) {
await fs.writeFile(file, newString, 'utf8')
}
+async function replaceRecursively(file, oldVersion, newVersion) {
+ await replaceInFile(file, originalString => {
+ return originalString
+ .replace(
+ new RegExp(regExpQuote(oldVersion), 'g'),
+ regExpQuoteReplacement(newVersion)
+ )
+ // Also replace the version used by the rubygem,
+ // which is using periods (`.`) instead of hyphens (`-`)
+ .replace(
+ new RegExp(regExpQuote(oldVersion.replace(/-/g, '.')), 'g'),
+ regExpQuoteReplacement(newVersion.replace(/-/g, '.'))
+ )
+ })
+}
+
+async function findDocsLinkFiles(directory) {
+ const entries = await fs.readdir(directory, { withFileTypes: true })
+ const files = []
+
+ for (const entry of entries) {
+ const entryPath = path.join(directory, entry.name)
+
+ if (entry.isDirectory()) {
+ const nestedFiles = await findDocsLinkFiles(entryPath)
+ files.push(...nestedFiles)
+ continue
+ }
+
+ if (entry.isFile() && DOCS_LINK_EXTENSIONS.has(path.extname(entry.name))) {
+ files.push(entryPath)
+ }
+ }
+
+ return files
+}
+
+async function replaceDocsLinks(oldVersion, newVersion) {
+ const files = [
+ ...DOCS_LINK_FILES,
+ ...(
+ await Promise.all(
+ DOCS_LINK_DIRECTORIES.map(directory => findDocsLinkFiles(directory))
+ )
+ ).flat()
+ ]
+
+ await Promise.all(
+ files.map(file => replaceInFile(file, originalString => {
+ return replaceDocsShortVersionLinks(originalString, oldVersion, newVersion)
+ }))
+ )
+}
+
function bumpNpmVersion(newVersion) {
if (DRY_RUN) {
return
@@ -104,10 +183,13 @@ async function main(args) {
await Promise.all(
FILES.map(file => replaceRecursively(file, oldVersion, newVersion))
)
+ await replaceDocsLinks(oldVersion, newVersion)
} catch (error) {
console.error(error)
process.exit(1)
}
}
-main(process.argv.slice(2))
+if (import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main(process.argv.slice(2))
+}
diff --git a/build/change-version.test.mjs b/build/change-version.test.mjs
new file mode 100644
index 000000000000..72c6427773ee
--- /dev/null
+++ b/build/change-version.test.mjs
@@ -0,0 +1,32 @@
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+
+import { replaceDocsShortVersionLinks, shortVersion } from './change-version.mjs'
+
+test('shortVersion returns the major and minor parts of a semver version', () => {
+ assert.equal(shortVersion('5.3.8'), '5.3')
+ assert.equal(shortVersion('v5.4.0'), '5.4')
+})
+
+test('replaceDocsShortVersionLinks updates current docs links only', () => {
+ const input = `---
+aliases:
+ - /docs/5.3/components/navs/
+ - /docs/4.6/components/navs/
+---
+
+See the docs.
+`
+
+ assert.equal(
+ replaceDocsShortVersionLinks(input, '5.3.8', '5.4.0'),
+ `---
+aliases:
+ - /docs/5.4/components/navs/
+ - /docs/4.6/components/navs/
+---
+
+See the docs.
+`
+ )
+})