admin管理员组

文章数量:1305315

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.

本文标签: reactjsHow to read static file with Server Action in nextjs 15Stack Overflow