mui / pigment-css

Pigment CSS is a zero-runtime CSS-in-JS library that extracts the colocated styles to their own CSS files at build time.
MIT License
803 stars 39 forks source link

Unable to import Node builtins #239

Open joshwcomeau opened 1 month ago

joshwcomeau commented 1 month ago

Steps to reproduce

Repro URL: https://github.com/joshwcomeau/pigmentcss-fs-issue

  1. Run npm run dev
  2. Note the issue in the terminal:

EvalError: Unable to import "fs/promises". Importing Node builtins is not supported in the sandbox.

Context

In my project, I'm using the fs/promises module to load MDX content. This obviously wouldn't work in-browser, but I'm doing this specifically inside a Server Component, within the next.js App Router. So none of this code is included in the client-side bundles.

It seems as though Pigment is unable to load any Node built-ins, but I don't think it has to; I don't think any of this stuff affects the generated CSS.

I also realize that this is likely an issue with @wyw-in-js, rather than Pigment CSS itself, but I wanted to highlight it here since that repo doesn't seem active.

Your environment

npx @mui/envinfo ``` System: OS: macOS 14.5 Binaries: Node: 20.16.0 - ~/.nvm/versions/node/v20.16.0/bin/node npm: 10.8.1 - ~/.nvm/versions/node/v20.16.0/bin/npm pnpm: 9.1.4 - ~/Library/pnpm/pnpm Browsers: Chrome: 128.0.6613.138 Edge: Not Found Safari: 17.5 ```

Search keywords: import, Node, fs, sandbox

brijeshb42 commented 1 month ago

Thanks for the report. I'll check this out. Ideally, if something is imported and not used in the css directly, wyw-in-js should tree-shake it before evalutating the code for css.

brijeshb42 commented 1 month ago

@joshwcomeau Could you share a relevant code snippet that shows how you are using the fs or any other node built-ins ? I think we might have to stub it but depends in the use-case.

mathieuhasum commented 1 week ago

Hey, Just ran into this issue as well while refactoring a project using RSC and PigmentCSS.

One package (@jitl/notion-api) would always cause the dev server to hang forever (stuck at "Compiling..." without trace, which might or might not be related to this issue). Therefore I started to extract the few functionalities I needed from the library and recreate the helpers locally. One function (ensureImageDownloaded) relied on fs, path and other built-in modules.

Recreating a simpler version of it would cause the same error reported

unhandledRejection: Error: Unable to import "node:fs/promises". Importing Node builtins is not supported in the sandbox.

So to answer the question, an example of real-life use case here is using RSC to download remote images.

export async function ensureImageDownloaded(args: {
  url: string;
  filenamePrefix: string;
  directory: string;
}): Promise<string> {
  const { url, filenamePrefix, directory } = args; 

  // Check if file already exists with prefix
  const files = await readdir(directory);
  const existingFile = files.find((name) => name.startsWith(filenamePrefix));
  if (existingFile) {
    return existingFile;
  }

  // Download image
  const response = await fetch(url);
  const contentType = response.headers.get("content-type") || "image/png";
  const ext = contentType.split("/")[1];
  const filename = `${filenamePrefix}.${ext}`;
  const destPath = path.join(directory, filename);

  // Convert response to buffer and save
  const arrayBuffer = await response.arrayBuffer();
  const buffer = Buffer.from(arrayBuffer);
  await writeFile(destPath, buffer);

  return filename;
}

The repo linked by @joshwcomeau (https://github.com/joshwcomeau/pigmentcss-fs-issue) already provides a great minimal reproducible example as a starting point. You can swap content.helper.ts with any more complex use case like the one above.

brijeshb42 commented 1 week ago

Not sure if you have tried this with the latest versions of Next.js and Pigment CSS. I tried with @joshwcomeau's repo and I was getting a different error (after updating Nextjs to 15.0.2 and Pigment to 0.0.25) -

Screenshot 2024-11-04 at 7 28 33 PM

which meant that this one was not about sandbox. It wasn't able to find the file relative to itself. So as a workaround, I passed the root directory with src through an env var in next.config.mjs.

// next.config.js
import * as path from "path";
import { withPigment } from "@pigment-css/nextjs-plugin";

const nextConfig = {
  env: {
    DATA_DIR: path.join(process.cwd(), "src"),
  },
};

export default withPigment(nextConfig);

and in the file where you want to read it's contents, I did -

import * as path from "path";
import fs from "fs/promises";

export async function loadContent() {
  const data = await fs.readFile(
    path.join(process.env.DATA_DIR as string, "data.txt"),
    "utf-8",
  );
  return data;
}

This worked and the app built successfully. I am stating this as a solution as I am also doing the same thing for our Pigment CSS docs where we'll be authoring content in MDX and reading the contents through the fs module. Here's the code.

One thing to note here is that as long as your function where you are using built-in modules are not being called at the file scope, rather somewhere inside one of the next.js primitives, it should be fine. Here's an example -

function loadContent() {
}
const content = await loadContent();

export default function PageComponent() {
  return <div>{content}</div>;
}

This will throw an error because loadContent() is being called at the file scope.


function loadContent() {
}

export default function PageComponent() {
  const content = await loadContent();
  return <div>{content}</div>;
}

This should work as the actual call is inside another function.

@mathieuhasum Can you follow the above and see if it works for you ?

mathieuhasum commented 1 week ago

Thanks for the insights. I'll try again with this information and keep you updated.

If I can't identify the root cause, I'll create a reproducible example by stripping down the project I have. 🤔 Maybe there is a problem of loading from the wrong scope indeed.

For your information, as I was trying to debug yesterday I quickly swapped the package from PigmentCSS to Linaria. (From what I understand, they both leverage @wyw-in-js). And it did make the issue disappear. Will try to figure it out tonight or next weekend.

brijeshb42 commented 1 week ago

I think I spoke too soon. The issue is still there during dev. What I stated above is valid for build command. Let me look more into it. It hasn't been an issue for the Pigment docs yet.

Edit:

Moving the import to be dynamic and inside the function call worked. But this is a workaround and not a proper solution -

import * as path from "path";

export async function loadContent() {
  const fs = await import("fs/promises");
  const data = await fs.readFile(
    path.join(process.env.DATA_DIR as string, "data.txt"),
    "utf-8",
  );
  return data;
}
mathieuhasum commented 1 week ago

Surely not a perfect solution, but I confirm your workaround works :+1: Even for loading the functions from packages that were causing the dev server to get stuck.

export default async function PageComponent() {
  const blocks = await ...
  for (const block of blocks) {
    if (block.type === "image" ) {
      const { ensureImageDownloaded } = await import("@jitl/notion-api");
      await ensureImageDownloaded({
        url: block.image.file.url,
        directory: "public/images",
      ...
      }),
    }
  }
  return <>...</>