🪨

pnpmで@adminjs/expressを使おうとして苦労した

に公開

pnpmってもしかして大変?

環境

adminjsのexpressプラグインのサンプルコードを以下のようにシンプルな構成で試したかった。
実行にはtsxを使用。

package.json
{
  "name": "adminjsexpressexperiment",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.14.0",
  "dependencies": {
    "@adminjs/express": "^6.1.0",
    "adminjs": "^7.8.16",
    "express": "^5.1.0",
    "tsx": "^4.20.3"
  },
  "devDependencies": {
    "@types/express": "^5.0.3",
    "@types/node": "^24.0.15"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noEmit": true
  }
}

WSLで動かすため0.0.0.0にしただけだが一応コードを載せておく。

index.ts
import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'

const PORT = 3000

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const adminRouter = AdminJSExpress.buildRouter(admin)
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, '0.0.0.0', () => {
    console.log(`AdminJS started on http://0.0.0.0:${PORT}${admin.options.rootPath}`)
  })
}

start()

@adminjs/design-systemのtiptapバージョン固定問題

まず以下のように起動できなくて困る。

$ pnpm tsx index.ts
file:///home/foo/src/adminjsexp/node_modules/.pnpm/@tiptap+extension-horizontal-rule@2.26.1_@tiptap+core@2.1.13_@tiptap+pm@2.1.13__@tiptap+pm@2.1.13/node_modules/@tiptap/extension-horizontal-rule/dist/index.js:1
import { Node, mergeAttributes, canInsertNode, isNodeSelection, nodeInputRule } from '@tiptap/core';
                                ^^^^^^^^^^^^^
SyntaxError: The requested module '@tiptap/core' does not provide an export named 'canInsertNode'
    at #_instantiate (node:internal/modules/esm/module_job:248:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:350:5)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:99:5)

Node.js v24.2.0

tiptapのissueにもあるように、これは@tiptap/core 2.22.3で追加された。しかし実際にインストールされているのは2.1.13で、何故こんな古いバージョンなのか。

原因は@adminjs/design-systemが何故かバージョン固定しているからだ。

    "@tiptap/core": "2.1.13",

類似のissueがあったが、この時は別のモジュールとの競合? でそちらを修正して閉じてしまったようだ。しかしtiptapのバージョンを固定したのもその競合が原因だったらしい? ならもう固定しなくていいのではないか? ちゃんと検証してはいないが…。(npmでは起こらないっぽいし。)

とにかくワークアラウンドとしてはバージョンを上書きしてしまうことだ。

package.json (一部)
  "pnpm": {
    "overrides": {
      "@tiptap/core": "^3.0.9",
      "@tiptap/pm": "^3.0.9"
    }
  }

res.sendFileのsymlink非対応問題

これで起動するようになった。ただ実際にブラウザで開くとエラーが発生する。

$ pnpm tsx index.ts
AdminJS started on http://0.0.0.0:3000/admin
NotFoundError: Not Found
    at createHttpError (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:861:12)
    at SendStream.error (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:168:31)
    at SendStream.pipe (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:468:14)
    at sendfile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:1000:8)
    at ServerResponse.sendFile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:409:3)
    at file:///home/foo/src/adminjsexp/node_modules/.pnpm/@adminjs+express@6.1.1_adminjs@7.8.17_@types+react@18.3.23__express-formidable@1.2.0_ex_34fb4839757f3087902e3b15ff2f4d4d/node_modules/@adminjs/express/lib/buildRouter.js:61:17
    at Layer.handleRequest (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/layer.js:152:17)
    at next (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:117:3)
    at handle (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/index.js:435:11)
NotFoundError: Not Found
    at createHttpError (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:861:12)
    at SendStream.error (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:168:31)
    at SendStream.pipe (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:468:14)
    at sendfile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:1000:8)
    at ServerResponse.sendFile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:409:3)
    at file:///home/foo/src/adminjsexp/node_modules/.pnpm/@adminjs+express@6.1.1_adminjs@7.8.17_@types+react@18.3.23__express-formidable@1.2.0_ex_34fb4839757f3087902e3b15ff2f4d4d/node_modules/@adminjs/express/lib/buildRouter.js:61:17
    at Layer.handleRequest (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/layer.js:152:17)
    at next (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:117:3)
    at handle (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/index.js:435:11)
NotFoundError: Not Found
    at createHttpError (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:861:12)
    at SendStream.error (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:168:31)
    at SendStream.pipe (/home/foo/src/adminjsexp/node_modules/.pnpm/send@1.2.0/node_modules/send/index.js:468:14)
    at sendfile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:1000:8)
    at ServerResponse.sendFile (/home/foo/src/adminjsexp/node_modules/.pnpm/express@5.1.0/node_modules/express/lib/response.js:409:3)
    at file:///home/foo/src/adminjsexp/node_modules/.pnpm/@adminjs+express@6.1.1_adminjs@7.8.17_@types+react@18.3.23__express-formidable@1.2.0_ex_34fb4839757f3087902e3b15ff2f4d4d/node_modules/@adminjs/express/lib/buildRouter.js:61:17
    at Layer.handleRequest (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/layer.js:152:17)
    at next (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:157:13)
    at Route.dispatch (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/lib/route.js:117:3)
    at handle (/home/foo/src/adminjsexp/node_modules/.pnpm/router@2.2.0/node_modules/router/index.js:435:11)

これの原因はexpressのres.sendFileがシンボリックリンクに対応できていないことが原因だった。(Claude Code調べ)

@adminjs/expressは自分のモジュール内部の静的ファイルを配信するわけだが、pnpmの場合はnode_module下にシンボリックリンクが張ってあり、res.sendFileがそれを解析できずエラーということになる。

これは元を辿ればsendモジュールがシンボリックリンクに対応していないという話ではあるが、普通はシンボリックリンク渡さないでしょという気もする。どのレイヤーで対応して貰えばいいのか?

差し当たってpnpm patchにより修正はできる。

package.json (一部改)
  "pnpm": {
    "overrides": {
      "@tiptap/core": "^3.0.9",
      "@tiptap/pm": "^3.0.9"
    },
    "patchedDependencies": {
      "@adminjs/express": "patches/@adminjs__express.patch"
    }
  }
patches/@adminjs__express.patch
diff --git a/lib/buildRouter.js b/lib/buildRouter.js
index 3bfdd1560867ecb935a63b830b101d2568157265..e7c0e6b2f5650ee3836cd391700681e9c9dfd995 100644
--- a/lib/buildRouter.js
+++ b/lib/buildRouter.js
@@ -2,6 +2,9 @@ import { Router as AdminRouter } from "adminjs";
 import { Router } from "express";
 import formidableMiddleware from "express-formidable";
 import path from "path";
+import { createRequire } from "node:module";
+import fs from "fs";
+import mime from "mime-types";
 import { WrongArgumentError } from "./errors.js";
 import { log } from "./logger.js";
 import { convertToExpressRoute } from "./convertRoutes.js";
@@ -56,9 +59,19 @@ export const buildAssets = ({ admin, assets, routes, router, }) => {
     if (componentBundlerRoute) {
         buildRoute({ route: componentBundlerRoute, router, admin });
     }
+    const require = createRequire(import.meta.url);
     assets.forEach((asset) => {
         router.get(asset.path, async (_req, res) => {
-            res.sendFile(path.resolve(asset.src));
+            try {
+                const resolvedPath = require.resolve(asset.src);
+                const content = fs.readFileSync(resolvedPath);
+                const contentType = mime.lookup(resolvedPath) || 'application/octet-stream';
+                res.set('Content-Type', contentType);
+                res.send(content);
+            } catch (err) {
+                // Fallback to original behavior
+                res.sendFile(path.resolve(asset.src));
+            }
         });
     });
 };

Discussion