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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 21:38:38 +08:00
parent a487b17165
commit 698a20a1d4
7 changed files with 81 additions and 61 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Sparkles } from "lucide-react"; import { Sparkles } from "lucide-react";
import { guaNumFromMark } from "@/lib/content/zhouyi"; import { guaNumFromMark } from "@/lib/content/gua-utils";
export default function GuaFooter({ export default function GuaFooter({
guaMark, guaMark,
+1 -1
View File
@@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import type { LearnVariant } from "@/lib/content/zhouyi"; import type { LearnVariant } from "@/lib/content/gua-utils";
function resolveLearnHref( function resolveLearnHref(
href: string | undefined, href: string | undefined,
+1 -1
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import { guaNumFromMark } from "@/lib/content/zhouyi"; import { guaNumFromMark } from "@/lib/content/gua-utils";
export interface ResultObj { export interface ResultObj {
guaTitle: string; guaTitle: string;
+58
View File
@@ -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;
}
+12 -58
View File
@@ -1,11 +1,21 @@
import "server-only";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; 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 DOCS_ROOT = path.join(process.cwd(), "content", "zhouyi", "docs");
const OTHER_ROOT = path.join(DOCS_ROOT, "other"); const OTHER_ROOT = path.join(DOCS_ROOT, "other");
export type LearnVariant = "traditional" | "simplified";
function getVariantRoot(variant: LearnVariant): string { function getVariantRoot(variant: LearnVariant): string {
return variant === "traditional" ? DOCS_ROOT : OTHER_ROOT; return variant === "traditional" ? DOCS_ROOT : OTHER_ROOT;
} }
@@ -21,11 +31,6 @@ export async function listGuaMarks(
.sort(); .sort();
} }
/** 序号 slug,如 "35" */
export function guaNumFromMark(guaMark: string): string {
return guaMark.split(".")[0];
}
/** 由序号解析目录名,如 "35" → "35.火地晋" */ /** 由序号解析目录名,如 "35" → "35.火地晋" */
export async function markFromNum( export async function markFromNum(
num: string, num: string,
@@ -52,54 +57,3 @@ export async function readLearnMarkdown(
: path.join(root, guaMark, "index.md"); : path.join(root, guaMark, "index.md");
return fs.readFile(filePath, "utf-8"); 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(".");
}
+7
View File
@@ -22,6 +22,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"server-only": "^0.0.1",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
@@ -7369,6 +7370,12 @@
"node": ">=10" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+1
View File
@@ -23,6 +23,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"server-only": "^0.0.1",
"tailwind-merge": "^2.6.0" "tailwind-merge": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {