Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
env:
VITE_BASE: ${{ env.VITE_BASE || format('{0}/', steps.pages.outputs.base_path) }}

- name: Verify links and asset paths
run: npm test
env:
VITE_BASE: ${{ env.VITE_BASE || format('{0}/', steps.pages.outputs.base_path) }}

- name: Collect PRs to deploy
uses: actions/github-script@v9
with:
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
- name: Determine Base URL Prefix
id: base_url
run: |
if [ "${{ github.event.repository.name }}" = "${{ github.repository_owner }}.github.io" ]; then
REPO_NAME=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')
OWNER_NAME=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
if [ "$REPO_NAME" = "${OWNER_NAME}.github.io" ]; then
echo "prefix=/" >> $GITHUB_OUTPUT
else
echo "prefix=/${{ github.event.repository.name }}/" >> $GITHUB_OUTPUT
Expand All @@ -44,6 +46,11 @@ jobs:
env:
VITE_BASE: ${{ steps.base_url.outputs.prefix }}pr-${{ github.event.number }}/

- name: Verify links and asset paths
run: npm test
env:
VITE_BASE: ${{ steps.base_url.outputs.prefix }}pr-${{ github.event.number }}/

- name: Package build
run: |
cd dist
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ARG VITE_BASE=/
ENV VITE_BASE=${VITE_BASE}

RUN npm run build
RUN npm test

FROM nginx:alpine

Expand Down
2 changes: 1 addition & 1 deletion docs/news/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description: Latest developments, changes, war updates, and traffic plots for Mu
<dt><a href="https://mume.org/news/game/" target="_self" rel="external">Game News</a></dt>
<dd>Find out about the latest news from the game!</dd>

<dt><a href="/news/web">Web News</a></dt>
<dt><a href="./web">Web News</a></dt>
<dd>The news from this Web-page. Note these do not really get used, usually everything important gets put up as Game News.</dd>

<dt><a href="https://mume.org/plots/" target="_self" rel="external">Frequentation Plots</a></dt>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"preview": "vitepress preview docs",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"test": "node scripts/test-link-and-asset-paths.js"
},
"dependencies": {
"vue": "^3.5.42"
Expand Down
6 changes: 4 additions & 2 deletions scripts/ci-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ else
fi
echo "VITE_HOSTNAME=$VITE_HOSTNAME" >> $GITHUB_ENV

# Explicitly set VITE_BASE=/ for the official production environment
if [ "$OWNER" = "mume" ] && [ "$REPO" = "mume.github.io" ]; then
# Explicitly set VITE_BASE=/ for the official production environment if not already set
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
REPO_LOWER=$(echo "$REPO" | tr '[:upper:]' '[:lower:]')
if [ -z "$VITE_BASE" ] && [ "$OWNER_LOWER" = "mume" ] && [ "$REPO_LOWER" = "mume.github.io" ]; then
export VITE_BASE="/"
echo "VITE_BASE=$VITE_BASE" >> $GITHUB_ENV
fi
163 changes: 163 additions & 0 deletions scripts/test-link-and-asset-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const distDir = path.resolve(__dirname, '../dist')

if (!fs.existsSync(distDir)) {
console.error(`[TEST ERROR] dist directory does not exist at ${distDir}. Run 'npm run build' first.`)
process.exit(1)
}

function getAllHtmlFiles(dir, fileList = []) {
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
if (fs.statSync(filePath).isDirectory()) {
getAllHtmlFiles(filePath, fileList)
} else if (file.endsWith('.html')) {
fileList.push(filePath)
}
}
return fileList
}

const htmlFiles = getAllHtmlFiles(distDir)
if (htmlFiles.length === 0) {
console.error('[TEST ERROR] No HTML files found in dist directory.')
process.exit(1)
}

let errors = []
let totalLinksChecked = 0
let totalAssetsChecked = 0

let baseEnv = process.env.VITE_BASE || '/'
if (!baseEnv.startsWith('/')) baseEnv = '/' + baseEnv
if (!baseEnv.endsWith('/')) baseEnv = baseEnv + '/'
const expectedBase = baseEnv

console.log(`[TEST] Verifying ${htmlFiles.length} HTML files in dist/ against expected base: "${expectedBase}"...`)

for (const htmlFile of htmlFiles) {
const content = fs.readFileSync(htmlFile, 'utf8')
const relPath = path.relative(distDir, htmlFile)

// 1. Check for invalid prefix '/mume.github.io/' if expectedBase does not contain it
if (!expectedBase.includes('/mume.github.io/') && content.includes('/mume.github.io/')) {
errors.push(`${relPath}: Contains invalid hardcoded path prefix '/mume.github.io/'`)
}

// 2. Extract and check src/href attributes (<img src="...">, <script src="...">, <link href="...">)
const matches = [...content.matchAll(/(?:src|href|srcset)=["']([^"']+)["']/g)]
for (const match of matches) {
const rawUrl = match[1]

// Skip external links, data URIs, anchor hashes, mailto, etc.
if (
rawUrl.startsWith('http://') ||
rawUrl.startsWith('https://') ||
rawUrl.startsWith('//') ||
rawUrl.startsWith('data:') ||
rawUrl.startsWith('#') ||
rawUrl.startsWith('mailto:')
) {
continue
}

// Process srcset entries if space-separated
const urlParts = rawUrl.split(',').map(s => s.trim().split(/\s+/)[0])
for (const url of urlParts) {
if (!url) continue

if (!expectedBase.includes('/mume.github.io/') && url.includes('/mume.github.io/')) {
errors.push(`${relPath}: URL "${url}" contains '/mume.github.io/'`)
}

// Is asset (js, css, images, fonts, ico)?
if (/\.(js|css|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)$/i.test(url)) {
totalAssetsChecked++
let distRelAssetPath = ''

if (url.startsWith('/')) {
if (expectedBase !== '/' && url.startsWith(expectedBase)) {
distRelAssetPath = url.slice(expectedBase.length - 1)
} else {
distRelAssetPath = url
}
const resolvedAssetPath = path.join(distDir, distRelAssetPath)
if (!fs.existsSync(resolvedAssetPath)) {
errors.push(`${relPath}: Asset referenced by "${url}" not found at "${resolvedAssetPath}"`)
}
} else {
const resolvedAssetPath = path.resolve(path.dirname(htmlFile), url)
if (!fs.existsSync(resolvedAssetPath)) {
errors.push(`${relPath}: Relative asset referenced by "${url}" not found at "${resolvedAssetPath}"`)
}
}
} else if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
// Internal page route link check
totalLinksChecked++
let targetRoute = url.split('?')[0].split('#')[0]

if (expectedBase !== '/' && targetRoute.startsWith(expectedBase)) {
targetRoute = '/' + targetRoute.slice(expectedBase.length)
}

let possibleDistPaths = []
if (targetRoute.startsWith('/')) {
const cleanRoute = targetRoute.replace(/^\//, '')
if (!cleanRoute) {
possibleDistPaths.push(path.join(distDir, 'index.html'))
} else {
possibleDistPaths.push(
path.join(distDir, `${cleanRoute}.html`),
path.join(distDir, cleanRoute, 'index.html'),
path.join(distDir, cleanRoute)
)
}
} else {
const absoluteUrl = path.resolve(path.dirname(htmlFile), targetRoute)
possibleDistPaths.push(
`${absoluteUrl}.html`,
path.join(absoluteUrl, 'index.html'),
absoluteUrl
)
}

const exists = possibleDistPaths.some(p => fs.existsSync(p))
if (!exists) {
errors.push(`${relPath}: Internal route "${url}" could not be resolved in dist (checked: ${possibleDistPaths.join(', ')})`)
}
}
}
}

// 3. Verify __VP_SITE_DATA__ base if present
const siteDataMatch = content.match(/window\.__VP_SITE_DATA__=JSON\.parse\((".*?")\)/)
if (siteDataMatch) {
try {
const siteData = JSON.parse(JSON.parse(siteDataMatch[1]))
if (siteData.base !== expectedBase) {
errors.push(`${relPath}: __VP_SITE_DATA__.base is "${siteData.base}", expected "${expectedBase}"`)
}
} catch (e) {
// Ignore parse errors
}
}
}

console.log(`[TEST SUMMARY] Checked ${htmlFiles.length} HTML files, ${totalLinksChecked} internal links, and ${totalAssetsChecked} asset references.`)

if (errors.length > 0) {
console.error(`\n[TEST FAILURES] Found ${errors.length} link/asset path issues:\n`)
for (const err of errors) {
console.error(` - ${err}`)
}
process.exit(1)
} else {
console.log('[TEST SUCCESS] All link and asset path checks passed successfully!')
process.exit(0)
}
Loading