From 43128bf526026410e1b9b8eef667e16dc12901ab Mon Sep 17 00:00:00 2001 From: Ivan Borshchov Date: Mon, 21 Sep 2026 14:11:29 +0000 Subject: [PATCH] fix: launch native pnpm 12 binary directly AdminForth/1956 --- adminforth/commands/createApp/utils.js | 12 ++++++++---- adminforth/commands/utils.js | 23 +++++++++++++++++++++++ adminforth/modules/codeInjector.ts | 10 +++++++--- adminforth/modules/utils.ts | 24 +++++++++++++++++++++++- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/adminforth/commands/createApp/utils.js b/adminforth/commands/createApp/utils.js index 38c83f6aa..92d6deae8 100644 --- a/adminforth/commands/createApp/utils.js +++ b/adminforth/commands/createApp/utils.js @@ -15,6 +15,7 @@ import Handlebars from 'handlebars'; import { promisify } from 'util'; import { resolveAdminforthVersionRange } from '../cli.js'; import { checkNodeVersion } from '../nodeVersion.js'; +import { isNodeScriptBin } from '../utils.js'; import { URL } from 'url' import net from 'net' @@ -684,9 +685,11 @@ async function installDependenciesPnpm(ctx, cwd) { execAsync(`pnpm install`, { cwd: customDir, env: { PATH: process.env.PATH } }), ]); } else { + // pnpm 12 bin is a native executable, only older JS entrypoints can be passed to node + const pnpmCommand = isNodeScriptBin(pnpmPath) ? `${nodeBinary} ${pnpmPath}` : pnpmPath; const res = await Promise.all([ - execAsync(`${nodeBinary} ${pnpmPath} install`, { cwd, env: { PATH: process.env.PATH } }), - execAsync(`${nodeBinary} ${pnpmPath} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), + execAsync(`${pnpmCommand} install`, { cwd, env: { PATH: process.env.PATH } }), + execAsync(`${pnpmCommand} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), ]); } } @@ -703,9 +706,10 @@ async function installDependenciesNpm(ctx, cwd) { execAsync(`npm install`, { cwd: customDir, env: { PATH: process.env.PATH } }), ]); } else { + const npmCommand = isNodeScriptBin(npmPath) ? `${nodeBinary} ${npmPath}` : npmPath; const res = await Promise.all([ - execAsync(`${nodeBinary} ${npmPath} install`, { cwd, env: { PATH: process.env.PATH } }), - execAsync(`${nodeBinary} ${npmPath} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), + execAsync(`${npmCommand} install`, { cwd, env: { PATH: process.env.PATH } }), + execAsync(`${npmCommand} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), ]); } } diff --git a/adminforth/commands/utils.js b/adminforth/commands/utils.js index 2f5492d8e..89a965e47 100644 --- a/adminforth/commands/utils.js +++ b/adminforth/commands/utils.js @@ -162,3 +162,26 @@ export async function getInstance(file, currentDirectory) { } return null; } + +/** + * Tells whether a package manager binary is a JS script which has to be launched with node, + * or a self-contained native executable which has to be launched directly. + * pnpm up to 11 shipped a JS entrypoint (bin/pnpm.mjs), pnpm 12 ships a native binary instead, + * and passing a native binary to node fails with "SyntaxError: Invalid or unexpected token". + */ +export function isNodeScriptBin(binPath) { + try { + const fd = fs.openSync(binPath, "r"); + try { + const header = Buffer.alloc(2); + // JS entrypoints of npm/pnpm always start with a "#!/usr/bin/env node" shebang, + // native executables start with a binary magic number (0x7f "ELF" on Linux) + return fs.readSync(fd, header, 0, 2, 0) === 2 && header.toString("latin1") === "#!"; + } finally { + fs.closeSync(fd); + } + } catch (e) { + // unreadable bin: keep launching it with node like we always did, so the error stays the same + return true; + } +} diff --git a/adminforth/modules/codeInjector.ts b/adminforth/modules/codeInjector.ts index baa2ce63c..e4924320f 100644 --- a/adminforth/modules/codeInjector.ts +++ b/adminforth/modules/codeInjector.ts @@ -7,7 +7,7 @@ import pLimit from 'p-limit'; import { promisify } from 'util'; import yaml from 'yaml'; import AdminForth, { AdminForthConfigMenuItem } from '../index.js'; -import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge, md5hash, slugifyString } from './utils.js'; +import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, deepMerge, md5hash, slugifyString, isNodeScriptBin } from './utils.js'; import { ICodeInjector } from '../types/Back.js'; import { StylesGenerator } from './styleGenerator.js'; import { afLogger } from '../modules/logger.js'; @@ -393,7 +393,9 @@ class CodeInjector implements ICodeInjector { // Quote paths that contain spaces (for Unix systems) const quotedNodeBinary = nodeBinary.includes(' ') ? `"${nodeBinary}"` : nodeBinary; const quotedPackagePath = packagePath.includes(' ') ? `"${packagePath}"` : packagePath; - execCommand = `${quotedNodeBinary} ${quotedPackagePath} ${command}`; + execCommand = isNodeScriptBin(packagePath) ? + `${quotedNodeBinary} ${quotedPackagePath} ${command}` : + `${quotedPackagePath} ${command}`; } const execOptions: any = { @@ -1464,7 +1466,9 @@ class CodeInjector implements ICodeInjector { if (process.platform === 'win32') { devServer = spawn(usersPackageManager, command.split(' '), { cwd, env, shell: true }); } else { - devServer = spawn(`${nodeBinary}`, [`${packageManagerPath}`, ...command.split(' ')], { cwd, env }); + devServer = isNodeScriptBin(packageManagerPath) ? + spawn(`${nodeBinary}`, [`${packageManagerPath}`, ...command.split(' ')], { cwd, env }) : + spawn(`${packageManagerPath}`, command.split(' '), { cwd, env }); } devServer.stdout.on('data', (data) => { if (data.includes('➜')) { diff --git a/adminforth/modules/utils.ts b/adminforth/modules/utils.ts index 6ce7b8d5a..bafc9ae4d 100644 --- a/adminforth/modules/utils.ts +++ b/adminforth/modules/utils.ts @@ -671,4 +671,26 @@ export function checkIfLinkInAllowedHosts(url: string, allowedHosts: string[]) { if (!allowed) { throw new Error(`Attachment host "${hostname}" is not in attachImagesAllowedHosts`); } -} \ No newline at end of file +} +/** + * Tells whether a package manager binary is a JS script which has to be launched with node, + * or a self-contained native executable which has to be launched directly. + * pnpm up to 11 shipped a JS entrypoint (bin/pnpm.mjs), pnpm 12 ships a native binary instead, + * and passing a native binary to node fails with "SyntaxError: Invalid or unexpected token". + */ +export function isNodeScriptBin(binPath: string): boolean { + try { + const fd = fs.openSync(binPath, 'r'); + try { + const header = Buffer.alloc(2); + // JS entrypoints of npm/pnpm always start with a "#!/usr/bin/env node" shebang, + // native executables start with a binary magic number (0x7f "ELF" on Linux) + return fs.readSync(fd, header, 0, 2, 0) === 2 && header.toString('latin1') === '#!'; + } finally { + fs.closeSync(fd); + } + } catch (e) { + // unreadable bin: keep launching it with node like we always did, so the error stays the same + return true; + } +}