From 698a20a1d44d92d0ad5c0b19c193f0298bac9472 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 10 Jun 2026 21:38:38 +0800 Subject: [PATCH] Fix Docker build by splitting client-safe gua utils from fs-based content loader. Client components were importing zhouyi.ts which pulled in fs/promises; move pure helpers to gua-utils.ts and mark zhouyi.ts server-only. Co-authored-by: Cursor --- components/learn/gua-footer.tsx | 2 +- components/learn/markdown-content.tsx | 2 +- components/result.tsx | 2 +- lib/content/gua-utils.ts | 58 ++++++++++++++++++++++ lib/content/zhouyi.ts | 70 +++++---------------------- package-lock.json | 7 +++ package.json | 1 + 7 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 lib/content/gua-utils.ts diff --git a/components/learn/gua-footer.tsx b/components/learn/gua-footer.tsx index 6d79f33..b59da80 100644 --- a/components/learn/gua-footer.tsx +++ b/components/learn/gua-footer.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Sparkles } from "lucide-react"; -import { guaNumFromMark } from "@/lib/content/zhouyi"; +import { guaNumFromMark } from "@/lib/content/gua-utils"; export default function GuaFooter({ guaMark, diff --git a/components/learn/markdown-content.tsx b/components/learn/markdown-content.tsx index 653425f..a42f8c1 100644 --- a/components/learn/markdown-content.tsx +++ b/components/learn/markdown-content.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import Markdown from "react-markdown"; -import type { LearnVariant } from "@/lib/content/zhouyi"; +import type { LearnVariant } from "@/lib/content/gua-utils"; function resolveLearnHref( href: string | undefined, diff --git a/components/result.tsx b/components/result.tsx index f9fe3f5..8e3119c 100644 --- a/components/result.tsx +++ b/components/result.tsx @@ -1,6 +1,6 @@ import React from "react"; import Link from "next/link"; -import { guaNumFromMark } from "@/lib/content/zhouyi"; +import { guaNumFromMark } from "@/lib/content/gua-utils"; export interface ResultObj { guaTitle: string; diff --git a/lib/content/gua-utils.ts b/lib/content/gua-utils.ts new file mode 100644 index 0000000..3f256be --- /dev/null +++ b/lib/content/gua-utils.ts @@ -0,0 +1,58 @@ +/** 客户端/服务端均可用的卦名工具(不含 fs) */ + +export type LearnVariant = "traditional" | "simplified"; + +export function guaNumFromMark(guaMark: string): string { + return guaMark.split(".")[0]; +} + +export function getGuaNumber(guaMark: string): number { + return parseInt(guaMark.split(".")[0], 10); +} + +export function getGuaName(guaMark: string): string { + return guaMark.split(".").slice(1).join("."); +} + +export function stripFrontmatter(content: string): string { + if (!content.startsWith("---")) { + return content; + } + const end = content.indexOf("---", 3); + if (end === -1) { + return content; + } + return content.slice(end + 3).trimStart(); +} + +export function extractZhangMingRen(guaDetail: string): string | undefined { + return guaDetail + .match(/(\*\*台灣張銘仁[\s\S]*?)(?=周易第\d+卦)/)?.[1] + ?.replaceAll("\n\n", "\n"); +} + +export function extractChangeDetails( + guaDetail: string, + guaChange: string, + guaTitle: string, +): string[] { + const changeList: string[] = []; + if (guaChange === "无变爻") { + return changeList; + } + + guaChange + .split(":")[1] + .trim() + .split(",") + .forEach((change) => { + const detail = guaDetail + .match(`(\\*\\*${change}變卦[\\s\\S]*?)(?=${guaTitle}|$)`)?.[1] + ?.replaceAll("\n\n", "\n"); + if (detail) { + changeList.push(detail.trim()); + } + }); + + return changeList; +} diff --git a/lib/content/zhouyi.ts b/lib/content/zhouyi.ts index 934ac0f..9b2b8d7 100644 --- a/lib/content/zhouyi.ts +++ b/lib/content/zhouyi.ts @@ -1,11 +1,21 @@ +import "server-only"; import fs from "fs/promises"; import path from "path"; +import type { LearnVariant } from "@/lib/content/gua-utils"; + +export type { LearnVariant } from "@/lib/content/gua-utils"; +export { + extractChangeDetails, + extractZhangMingRen, + getGuaName, + getGuaNumber, + guaNumFromMark, + stripFrontmatter, +} from "@/lib/content/gua-utils"; const DOCS_ROOT = path.join(process.cwd(), "content", "zhouyi", "docs"); const OTHER_ROOT = path.join(DOCS_ROOT, "other"); -export type LearnVariant = "traditional" | "simplified"; - function getVariantRoot(variant: LearnVariant): string { return variant === "traditional" ? DOCS_ROOT : OTHER_ROOT; } @@ -21,11 +31,6 @@ export async function listGuaMarks( .sort(); } -/** 序号 slug,如 "35" */ -export function guaNumFromMark(guaMark: string): string { - return guaMark.split(".")[0]; -} - /** 由序号解析目录名,如 "35" → "35.火地晋" */ export async function markFromNum( num: string, @@ -52,54 +57,3 @@ export async function readLearnMarkdown( : path.join(root, guaMark, "index.md"); return fs.readFile(filePath, "utf-8"); } - -export function stripFrontmatter(content: string): string { - if (!content.startsWith("---")) { - return content; - } - const end = content.indexOf("---", 3); - if (end === -1) { - return content; - } - return content.slice(end + 3).trimStart(); -} - -export function extractZhangMingRen(guaDetail: string): string | undefined { - return guaDetail - .match(/(\*\*台灣張銘仁[\s\S]*?)(?=周易第\d+卦)/)?.[1] - ?.replaceAll("\n\n", "\n"); -} - -export function extractChangeDetails( - guaDetail: string, - guaChange: string, - guaTitle: string, -): string[] { - const changeList: string[] = []; - if (guaChange === "无变爻") { - return changeList; - } - - guaChange - .split(":")[1] - .trim() - .split(",") - .forEach((change) => { - const detail = guaDetail - .match(`(\\*\\*${change}變卦[\\s\\S]*?)(?=${guaTitle}|$)`)?.[1] - ?.replaceAll("\n\n", "\n"); - if (detail) { - changeList.push(detail.trim()); - } - }); - - return changeList; -} - -export function getGuaNumber(guaMark: string): number { - return parseInt(guaMark.split(".")[0], 10); -} - -export function getGuaName(guaMark: string): string { - return guaMark.split(".").slice(1).join("."); -} diff --git a/package-lock.json b/package-lock.json index c4a6d75..56eafd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.3", + "server-only": "^0.0.1", "tailwind-merge": "^2.6.0" }, "devDependencies": { @@ -7369,6 +7370,12 @@ "node": ">=10" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index d21f0d3..bf758dd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.3", + "server-only": "^0.0.1", "tailwind-merge": "^2.6.0" }, "devDependencies": {