最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

reactjs - How to read static file with Server Action in nextjs 15? - Stack Overflow

programmeradmin0浏览0评论

Reproduction steps

  1. Visit vibrant-ui
  2. Click on : Components > Month Slider > Code.

Expected

The component should display the code.

Current

Failed to read file

Explanation

I have a server action that reads a static file ( component file ) that's exists in components/vibrant/component-name.tsx:


"use server"

import { promises as fs } from "fs"
import path from "path"
import { z } from "zod"

// Define the response type
type CodeResponse = {
  content?: string
  error?: string
  details?: string
}

// Validation schema
const fileSchema = z.object({
  fileName: z.string().min(1),
})

export async function getFileContent(fileName: string): Promise<CodeResponse> {
  // Validate input
  try {
    fileSchema.parse({ fileName })
  } catch {
    return {
      error: "File parameter is required",
    }
  }

  try {
    // Use path.join for safe path concatenation
    const filePath = path.join(process.cwd(), "components", "vibrant", fileName)

    const content = await fs.readFile(filePath, "utf8")

    return { content }
  } catch (error) {
    console.error("Error reading file:", error)
    const errorMessage =
      error instanceof Error ? error.message : "Unknown error"

    return {
      error: "Failed to read file",
      details: errorMessage,
    }
  }
}

I call this function from a client component:


"use client"

import { getFileContent } from "@/app/actions/file"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { Check, Copy } from "lucide-react"
import { useEffect, useState } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"

type Props = {
  source: string
  language?: string
}

export const CodeBlock = ({ source, language = "typescript" }: Props) => {
  const [code, setCode] = useState("")
  const [error, setError] = useState("")
  const [copied, setCopied] = useState(false)
  const [isExpanded, setIsExpanded] = useState(false)

  useEffect(() => {
    const fetchCode = async () => {
      const result = await getFileContent(source)

      if (result.error) {
        setError(result.error)
        setCode("")
        return
      }

      if (result.content) {
        setCode(result.content)
        setError("")
      }
    }

    fetchCode()
  }, [source])

  const copyToClipboard = async () => {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error("Failed to copy text: ", err)
    }
  }

  if (error) {
    return <div className="p-4 bg-red-50 text-red-600 rounded-lg">{error}</div>
  }

  return (
    <div className="relative w-full">
      <Button
        size="icon"
        onClick={copyToClipboard}
        className={cn(
          "absolute top-4 right-6 p-2 bg-white hover:bg-gray-100 transition-colors rounded-full",
          isExpanded && "right-2"
        )}
        aria-label="Copy code"
      >
        {copied ? (
          <Check className="w-4 h-4 text-green-500" />
        ) : (
          <Copy className="w-4 h-4 text-black" />
        )}
      </Button>

      <SyntaxHighlighter
        language={language}
        style={oneDark}
        className={cn("w-full", isExpanded ? "h-full" : "h-[480px]")}
      >
        {code}
      </SyntaxHighlighter>

      <div className="absolute bottom-0 left-0 right-4 flex justify-center pb-2">
        <div
          className={cn(
            "backdrop-blur-sm bg-transparent p-1 w-full h-16 flex items-center justify-center",
            isExpanded && "backdrop-blur-none"
          )}
        >
          <Button
            onClick={() => setIsExpanded(!isExpanded)}
            className="bg-white hover:bg-gray-100 text-black rounded-full"
            size="sm"
          >
            {isExpanded ? "Show Less" : "Show All"}
          </Button>
        </div>
      </div>
    </div>
  )
}

In the local environment, it works with dev and prod commands:

However when I deployed the project, the fetch fails:

The full code is available on GitHub.

Reproduction steps

  1. Visit vibrant-ui
  2. Click on : Components > Month Slider > Code.

Expected

The component should display the code.

Current

Failed to read file

Explanation

I have a server action that reads a static file ( component file ) that's exists in components/vibrant/component-name.tsx:


"use server"

import { promises as fs } from "fs"
import path from "path"
import { z } from "zod"

// Define the response type
type CodeResponse = {
  content?: string
  error?: string
  details?: string
}

// Validation schema
const fileSchema = z.object({
  fileName: z.string().min(1),
})

export async function getFileContent(fileName: string): Promise<CodeResponse> {
  // Validate input
  try {
    fileSchema.parse({ fileName })
  } catch {
    return {
      error: "File parameter is required",
    }
  }

  try {
    // Use path.join for safe path concatenation
    const filePath = path.join(process.cwd(), "components", "vibrant", fileName)

    const content = await fs.readFile(filePath, "utf8")

    return { content }
  } catch (error) {
    console.error("Error reading file:", error)
    const errorMessage =
      error instanceof Error ? error.message : "Unknown error"

    return {
      error: "Failed to read file",
      details: errorMessage,
    }
  }
}

I call this function from a client component:


"use client"

import { getFileContent } from "@/app/actions/file"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { Check, Copy } from "lucide-react"
import { useEffect, useState } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"

type Props = {
  source: string
  language?: string
}

export const CodeBlock = ({ source, language = "typescript" }: Props) => {
  const [code, setCode] = useState("")
  const [error, setError] = useState("")
  const [copied, setCopied] = useState(false)
  const [isExpanded, setIsExpanded] = useState(false)

  useEffect(() => {
    const fetchCode = async () => {
      const result = await getFileContent(source)

      if (result.error) {
        setError(result.error)
        setCode("")
        return
      }

      if (result.content) {
        setCode(result.content)
        setError("")
      }
    }

    fetchCode()
  }, [source])

  const copyToClipboard = async () => {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error("Failed to copy text: ", err)
    }
  }

  if (error) {
    return <div className="p-4 bg-red-50 text-red-600 rounded-lg">{error}</div>
  }

  return (
    <div className="relative w-full">
      <Button
        size="icon"
        onClick={copyToClipboard}
        className={cn(
          "absolute top-4 right-6 p-2 bg-white hover:bg-gray-100 transition-colors rounded-full",
          isExpanded && "right-2"
        )}
        aria-label="Copy code"
      >
        {copied ? (
          <Check className="w-4 h-4 text-green-500" />
        ) : (
          <Copy className="w-4 h-4 text-black" />
        )}
      </Button>

      <SyntaxHighlighter
        language={language}
        style={oneDark}
        className={cn("w-full", isExpanded ? "h-full" : "h-[480px]")}
      >
        {code}
      </SyntaxHighlighter>

      <div className="absolute bottom-0 left-0 right-4 flex justify-center pb-2">
        <div
          className={cn(
            "backdrop-blur-sm bg-transparent p-1 w-full h-16 flex items-center justify-center",
            isExpanded && "backdrop-blur-none"
          )}
        >
          <Button
            onClick={() => setIsExpanded(!isExpanded)}
            className="bg-white hover:bg-gray-100 text-black rounded-full"
            size="sm"
          >
            {isExpanded ? "Show Less" : "Show All"}
          </Button>
        </div>
      </div>
    </div>
  )
}

In the local environment, it works with dev and prod commands:

However when I deployed the project, the fetch fails:

The full code is available on GitHub.

Share Improve this question asked Feb 3 at 20:32 Ala Eddine MenaiAla Eddine Menai 2,8807 gold badges30 silver badges56 bronze badges 0
Add a comment  | 

1 Answer 1

Reset to default 0

I think this behavior is due to how Next.js handles static assets. Next.js removes static files from other directories and serves them through the /public folder during the build process. So, when you're reading a file like ./assets/example.tsx using fs, Next.js essentially moves it into the /public folder at build time.

To ensure it works, you should place these static files in the public folder before the build process. This will allow Next.js to handle them correctly in both development and production.

发布评论

评论列表(0)

  1. 暂无评论