pnpmで@adminjs/expressを使おうとして苦労した
pnpmってもしかして大変?
環境
adminjsのexpressプラグインのサンプルコードを以下のようにシンプルな構成で試したかった。
実行にはtsxを使用。
{
"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"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noEmit": true
}
}
WSLで動かすため0.0.0.0にしただけだが一応コードを載せておく。
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では起こらないっぽいし。)
とにかくワークアラウンドとしてはバージョンを上書きしてしまうことだ。
"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により修正はできる。
"pnpm": {
"overrides": {
"@tiptap/core": "^3.0.9",
"@tiptap/pm": "^3.0.9"
},
"patchedDependencies": {
"@adminjs/express": "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