Ala Eddine Menai
Ala Eddine Menai

Reputation: 2880

How to read static file with Server Action in nextjs 15?

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:

enter image description here


"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:

enter image description here

However when I deployed the project, the fetch fails:

enter image description here

The full code is available on GitHub.

Upvotes: 0

Views: 33

Answers (1)

Gooddevil
Gooddevil

Reputation: 1

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.

Upvotes: 0

Related Questions