refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+53
View File
@@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest"
import { smartTruncate, buildBreadcrumbList, BreadcrumbSegment } from "@/lib/utils/seo"
describe("smartTruncate", () => {
it("returns full text when shorter than maxLen", () => {
expect(smartTruncate("Short text", 100)).toBe("Short text")
})
it("truncates at word boundary and appends ellipsis", () => {
const result = smartTruncate("This is a longer sentence that should be truncated properly.", 30)
expect(result).toBe("This is a longer sentence...")
expect(result.length).toBeLessThanOrEqual(30 + 3) // + "..."
})
it("handles text with no spaces gracefully", () => {
const result = smartTruncate("SuperLongWordThatHasNoSpaces", 10)
expect(result).toBe("SuperLongW...")
})
it("does not append ellipsis when text fits exactly", () => {
expect(smartTruncate("abc", 3)).toBe("abc")
})
it("handles empty string", () => {
expect(smartTruncate("", 10)).toBe("")
})
})
describe("buildBreadcrumbList", () => {
it("builds a valid BreadcrumbList from segments", () => {
const segments = [
{ name: "Home", url: "https://deckyvault.xyz" },
{ name: "Games", url: "https://deckyvault.xyz/games" },
{ name: "Elden Ring", url: "https://deckyvault.xyz/game/123" },
]
const result = buildBreadcrumbList(segments)
expect(result["@context"]).toBe("https://schema.org")
expect(result["@type"]).toBe("BreadcrumbList")
expect(result.itemListElement).toHaveLength(3)
expect(result.itemListElement[0]).toEqual({
"@type": "ListItem",
position: 1,
name: "Home",
item: "https://deckyvault.xyz",
})
})
it("handles a single segment", () => {
const segments = [{ name: "Home", url: "https://deckyvault.xyz" }]
const result = buildBreadcrumbList(segments)
expect(result.itemListElement).toHaveLength(1)
})
})
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect } from "vitest"
import { generateSlug } from "@/lib/utils/slug"
describe("generateSlug", () => {
it("converts a simple title to lowercase hyphenated slug", () => {
expect(generateSlug("The Witcher 3")).toBe("the-witcher-3")
})
it("replaces special characters with hyphens", () => {
expect(generateSlug("Hades II: The Sequel")).toBe("hades-ii-the-sequel")
})
it("collapses multiple consecutive hyphens", () => {
expect(generateSlug("Game!!! -- Test")).toBe("game-test")
})
it("trims leading and trailing hyphens", () => {
expect(generateSlug(" -- My Game -- ")).toBe("my-game")
})
it("truncates to 80 characters", () => {
const longTitle = "A".repeat(100)
expect(generateSlug(longTitle).length).toBeLessThanOrEqual(80)
})
it("does not leave trailing hyphen after truncation", () => {
const title = "A".repeat(79) + " -"
expect(generateSlug(title)).not.toMatch(/-$/)
})
it("handles empty string", () => {
expect(generateSlug("")).toBe("")
})
})