dahlia/logtape

Error in Next.js

Closed this issue · 12 comments

Summary

  • Error occured in Next.js 14.x
  • Possibly related node:fs and hydration
  • Version: @logtape/logtape@0.5.0

Log

// hydration-error-info.js:63 

node:fs
Module build failed: UnhandledSchemeError: Reading from "node:fs" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.

Screenshot

image

I assume that it 's not working as expected.

// @ts-ignore: a trick to avoid module resolution error on non-Node.js environ
const fs = fsMod as (typeof fsType | null);

Which version of LogTape are you using?

@dahlia I installed v0.5.0 using yarn.

info All dependencies
└─ @logtape/logtape@0.5.0

It's not reproduced for me… Could you show me your next.config.mjs?

@dahlia This doesn't seem to be a problem. I'll reproduce with new project.

I initialized next project with typescript, tailwind, and shadcn/ui.

/** @type {import('next').NextConfig} */

const nextConfig = {
  output: 'export',
  images: {
    unoptimized: true,
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'dummyimage.com',
        port: '',
        pathname: '/**',
      },
    ],
  },
  assetPrefix: null,
  experimental: {},
};

export default nextConfig;

This is my installed packages(before installing logtape):

{
  "dependencies": {
    "@dotenvx/dotenvx": "^1.11.1",
    "@heroicons/react": "^2.1.5",
    "@next/third-parties": "^14.2.5",
    "@radix-ui/react-checkbox": "^1.1.1",
    "@radix-ui/react-dialog": "^1.1.1",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-progress": "^1.1.0",
    "@radix-ui/react-radio-group": "^1.2.0",
    "@radix-ui/react-separator": "^1.1.0",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.1",
    "@tanstack/react-table": "^8.20.1",
    "chalk": "^5.3.0",
    "class-variance-authority": "^0.7.0",
    "classnames": "^2.5.1",
    "clipboard": "^2.0.11",
    "clsx": "^2.1.1",
    "dayjs": "^1.11.12",
    "eslint-plugin-prettier": "^5.2.1",
    "lucide-react": "^0.427.0",
    "next": "^14.2.8",
    "next-themes": "^0.3.0",
    "prettier": "^3.3.3",
    "react": "^18.3.1",
    "react-countup": "^6.5.3",
    "react-d3-speedometer": "^2.2.1",
    "react-dom": "^18.3.1",
    "react-hot-toast": "^2.4.1",
    "recharts": "^2.12.7",
    "tailwind-merge": "^2.4.0",
    "tailwindcss-animate": "^1.0.7",
    "use-debounce": "^10.0.2",
    "vaul": "^0.9.1"
  },
  "devDependencies": {
    "@trivago/prettier-plugin-sort-imports": "^4.3.0",
    "@types/node": "22.4.0",
    "@types/react": "18.3.3",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "prettier-plugin-tailwindcss": "^0.6.5",
    "tailwindcss": "^3.4.1",
    "typescript": "5.5.4"
  }
}

Also facing this issue after adding @logtape/logtape@0.5.1 into a Next.js project (Pages router / next@14.2.11). I created a new <LogtapeSetup /> component and added it to my _app.tsx. Here is the implementation:

export const LogtapeSetup: React.FunctionComponent = () => {
  const loggerEnabled = useFeatureFlag('loggerEnabled');

  // Using ref instead of state to avoid re-renders
  const previouslyEnabledRef = React.useRef(false);

  React.useLayoutEffect(() => {
    if (loggerEnabled) {
      previouslyEnabledRef.current = true;
      void configure({
        loggers: [
          { category: ['logtape', 'meta'], level: 'warning', sinks: ['console'] },
          // TODO (Alexander Kachkaev) [2024-09-20]: Make feature flag more granular (allow to log only specific categories)
          { category: [], level: 'debug', sinks: ['console'] },
        ],
        reset: true,
        sinks: { console: getConsoleSink() },
      });
    } else if (previouslyEnabledRef.current) {
      void configure({
        loggers: [],
        reset: true,
        sinks: {},
      });
    }
  }, [loggerEnabled]);

  return <></>;
};

Here is the output from next dev (no --turbo, so using webpack):

 ⨯ node:fs
Module build failed: UnhandledSchemeError: Reading from "node:fs" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
Import trace for requested module:
node:fs
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/fs.js
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/filesink.node.js
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/mod.js
./src/components/logtape-setup.tsx

Here is the output from next build:

  ▲ Next.js 14.2.11
  - Environments: .env.local
  - Experiments (use with caution):
    · instrumentationHook

   Skipping validation of types
   Skipping linting
   Creating an optimized production build ...
Failed to compile.

node:fs
Module build failed: UnhandledSchemeError: Reading from "node:fs" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
    at /path/to/project/node_modules/.pnpm/next@14.2.11_@babel+core@7.25.2_@opentelemetry+api@1.9.0_babel-plugin-macros@3.1.0_react-dom@_dthae7t7pguei4cxnw3vufputm/node_modules/next/dist/compiled/webpack/bundle5.js:28:401757
    at Hook.eval [as callAsync] (eval at create (/path/to/project/node_modules/.pnpm/next@14.2.11_@babel+core@7.25.2_@opentelemetry+api@1.9.0_babel-plugin-macros@3.1.0_react-dom@_dthae7t7pguei4cxnw3vufputm/node_modules/next/dist/compiled/webpack/bundle5.js:13:28858), <anonymous>:6:1)
    at Object.processResource (/path/to/project/node_modules/.pnpm/next@14.2.11_@babel+core@7.25.2_@opentelemetry+api@1.9.0_babel-plugin-macros@3.1.0_react-dom@_dthae7t7pguei4cxnw3vufputm/node_modules/next/dist/compiled/webpack/bundle5.js:28:401682)
    ...

Import trace for requested module:
node:fs
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/fs.js
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/filesink.node.js
./node_modules/.pnpm/@logtape+logtape@0.5.1/node_modules/@logtape/logtape/esm/mod.js
./src/pages/_app.page/logtape-setup.tsx

node:util
Module build failed: UnhandledSchemeError: Reading from "node:util" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.
...

Has anyone found a workaround?

I think I was able to pnpm patch it:

patches/@logtape__logtape.patch (for v0.5.1)

diff --git a/esm/formatter.js b/esm/formatter.js
index 1d735ed52a2b30eeda9dcab298acc83637f4a33d..94ddcda15b295f9d90285f3e7ff448f9eedd3aed 100644
--- a/esm/formatter.js
+++ b/esm/formatter.js
@@ -1,4 +1,3 @@
-import util from "node:util";
 /**
  * The severity level abbreviations.
  */
@@ -31,10 +30,13 @@ const inspect =
     ? globalThis.Deno.inspect.bind(globalThis.Deno)
     // @ts-ignore: Node.js global
     // dnt-shim-ignore
-    : "inspect" in util && typeof util.inspect === "function"
+    // https://github.com/dahlia/logtape/issues/11
+    : "require" in globalThis
+        && "inspect" in globalThis.require("util")
+        && typeof globalThis.require("util").inspect === "function"
         // @ts-ignore: Node.js global
         // dnt-shim-ignore
-        ? util.inspect.bind(util)
+        ? globalThis.require("util").inspect.bind(globalThis.require("util"))
         : (v) => JSON.stringify(v);
 /**
  * The default text formatter.  This formatter formats log records as follows:
diff --git a/esm/fs.js b/esm/fs.js
index 3e9d1edfc286044d35eca3430be81150efc66ac5..d28b75a367273ed3c8653b2ebfbda14a590ded89 100644
--- a/esm/fs.js
+++ b/esm/fs.js
@@ -7,7 +7,8 @@ if (
   "Bun" in globalThis
 ) {
   try {
-    fs = await import("node" + ":fs");
+    // https://github.com/dahlia/logtape/issues/11
+    fs = globalThis.require(["node", "fs"].join(":"));
   } catch (e) {
     if (e instanceof TypeError) {
       fs = null;
diff --git a/script/formatter.js b/script/formatter.js
index e137bdb7486289ad436b15d7d07c65d333ed6986..1082e1f0d3636465202b6da0613114e11c37cbf3 100644
--- a/script/formatter.js
+++ b/script/formatter.js
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
 exports.ansiColorFormatter = void 0;
 exports.defaultTextFormatter = defaultTextFormatter;
 exports.defaultConsoleFormatter = defaultConsoleFormatter;
-const node_util_1 = __importDefault(require("node:util"));
+const node_util_1 = __importDefault(require(["node", "util"].join(":")));
 /**
  * The severity level abbreviations.
  */
diff --git a/script/fs.js b/script/fs.js
index bcb0170234994e2e0d6947738d5dffd9375526e7..938d78910905965428f8f781b2366510ebb00675 100644
--- a/script/fs.js
+++ b/script/fs.js
@@ -7,7 +7,8 @@ if (
   "Bun" in globalThis
 ) {
   try {
-    fs = require("node" + ":fs");
+    // https://github.com/dahlia/logtape/issues/11
+    fs = require(["node", "fs"].join(":"));
   } catch (_) {
     fs = null;
   }

Key points:

  • Fool webpack on node:fs presence

    fs = require("node" + ":fs");

    fs = await import("node" + ":fs");

    Webpack is smart enough to concatenate "node:" + "fs", statically analyze import path and generate a build error. However, ["node", "fs"].join(":") is too much for it, which is what we need.

  • Avoid static import from node:util

    import util from "node:util";

    See diff for details.

I guess that it’s also possible to achieve something similar via custom webpack config inside next.config.js. Webpack supports mocking imports etc. However, that would require effort from all Next.js users who install logtape and would also require a different instruction for next dev --turbo. My patch works with --turbo out of the box.

What are the downsides of injecting something similar into logtape, so that the patch is no longer needed? Next.js is quite popular, so fixing this issue would be great for logtape’s adoption.

Could you try LogTape v0.6.1? It fixed several bugs on Vite build, and these fixes might work for Next.js too.

Thanks for the fix @dahlia! I tried 0.6.1 just now. The app compiles, but I get this warning in both next dev and next build:

 ⚠ ./node_modules/.pnpm/@logtape+logtape@0.6.1/node_modules/@logtape/logtape/esm/nodeUtil.cjs
Critical dependency: the request of a dependency is an expression

Import trace for requested module:
./node_modules/.pnpm/@logtape+logtape@0.6.1/node_modules/@logtape/logtape/esm/nodeUtil.cjs
./node_modules/.pnpm/@logtape+logtape@0.6.1/node_modules/@logtape/logtape/esm/nodeUtil.js
./node_modules/.pnpm/@logtape+logtape@0.6.1/node_modules/@logtape/logtape/esm/formatter.js
./node_modules/.pnpm/@logtape+logtape@0.6.1/node_modules/@logtape/logtape/esm/mod.js

This warning on Stack Overflow: https://stackoverflow.com/q/42908116/1818285

It did not show up in 0.5.1 with my patch.

Running next dev --turbo does not produce any Logtape-related warnings in my project for v0.6.1. But I can't switch from Webpack to Turbopack yet because of getsentry/sentry-javascript#8105.

This local patch helped with mitigating a new Webpack warning:

patches/@logtape__logtape@0.6.1.patch

diff --git a/esm/nodeUtil.cjs b/esm/nodeUtil.cjs
index 4bf836e692bf4bf2c9f7706f56a959f30e088fbb..8cd834fd267a5489e05bf1d970535a52477104a5 100644
--- a/esm/nodeUtil.cjs
+++ b/esm/nodeUtil.cjs
@@ -7,7 +7,7 @@ if (
   "Bun" in globalThis
 ) {
   try {
-    util = require(["node", "util"].join(":"));
+    util = require(`${["node", "util"].join(":")}`);
   } catch (_) {
     util = null;
   }

Source: https://stackoverflow.com/a/73359606/1818285. Interestingly,  `${...}` was not required for node:fs, only for node:util.

I applied @kachkaev's above patch, and released it as v0.6.2. Give it a try!

0.6.2 works like a charm with no warnings in Next.js! I guess we can close this issue 🎉