You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义npm本地包API:npm i包名安装失败问题求助

问题原因分析

当执行npm i my-package时,npm不会直接请求tarball文件,而是先向registry地址发送GET请求获取包的元数据(JSON格式)——元数据需要包含包的版本、tarball下载地址等核心信息。你的当前接口不管请求路径是什么,直接返回了二进制的tgz文件,npm尝试把二进制数据当作JSON解析,自然会抛出"Unexpected token in JSON"错误。

而npm i http://localhost:3000/tarballs/my-package能成功,是因为你直接指定了tarball的URL,npm会跳过元数据请求环节,直接下载二进制文件完成安装。

解决方案:修改Express接口适配npm规范

需要调整接口逻辑,区分两种请求场景:

  1. 请求包元数据(路径为/tarballs/:packageName,不带.tgz后缀):返回符合npm规范的JSON元数据
  2. 请求tarball文件(路径为/tarballs/:packageName.tgz):返回二进制包文件

修改后的代码示例:

const express = require('express');
const app = express();
const port = 3000;
const fs = require('fs');
const path = require('path');

// 处理包元数据请求(不带.tgz后缀)
app.get('/tarballs/:packageName', (req, res) => {
    const { packageName } = req.params;
    // 可根据实际包版本信息调整,这里以固定1.0.0版本为例
    const packageMetadata = {
        "name": packageName,
        "versions": {
            "1.0.0": {
                "name": packageName,
                "version": "1.0.0",
                "dist": {
                    "tarball": `http://localhost:3000/tarballs/${packageName}.tgz`
                }
            }
        },
        "dist-tags": {
            "latest": "1.0.0"
        }
    };
    res.json(packageMetadata);
});

// 处理tarball文件请求(带.tgz后缀)
app.get('/tarballs/:packageName.tgz', (req, res) => {
    const { packageName } = req.params;
    const tarballPath = path.join(__dirname, 'packages', `${packageName}.tgz`);
    
    // 先检查文件是否存在,避免无效请求
    if (!fs.existsSync(tarballPath)) {
        return res.status(404).json({ error: 'Package not found' });
    }

    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('Content-Disposition', `attachment; filename=${packageName}.tgz`);
    
    const readStream = fs.createReadStream(tarballPath);
    readStream.pipe(res);
});

app.listen(port, () => {
    console.log(`API running on port ${port}`);
});

关键说明

  • 元数据结构必须符合npm registry基本规范,至少包含versions(每个版本的详细信息,其中dist.tarball指向实际tarball的URL)和dist-tags(指定默认安装的版本)
  • 如果有多个版本,可以维护版本列表,或者读取每个包目录下的package.json动态生成元数据
  • 增加文件存在性检查,避免文件不存在时返回无效的二进制流

测试步骤

  1. 重启Express服务
  2. 执行npm i my-package,此时npm会先请求元数据,再根据元数据中的tarball地址下载安装
不依赖Verdaccio的自制包服务器简化方案

如果上面的方案觉得复杂,可以采用更轻量的实现方式:用express.static托管packages文件夹,同时额外添加接口处理元数据请求,既满足npm i my-package的需求,也不影响npm i my-package.tgz的使用。

简化版代码示例:

const express = require('express');
const app = express();
const port = 3000;
const fs = require('fs');
const path = require('path');

// 直接托管packages文件夹,访问/tarballs/xxx.tgz即可下载对应包
app.use('/tarballs', express.static(path.join(__dirname, 'packages')));

// 处理包元数据请求
app.get('/tarballs/:packageName', (req, res) => {
    const { packageName } = req.params;
    // 可选:将元数据存在packages目录下的xxx.json文件中,动态读取
    const metaPath = path.join(__dirname, 'packages', `${packageName}.json`);
    if (fs.existsSync(metaPath)) {
        const meta = fs.readFileSync(metaPath, 'utf8');
        res.json(JSON.parse(meta));
    } else {
        // 无自定义元数据时返回默认模板
        res.json({
            "name": packageName,
            "versions": {
                "1.0.0": {
                    "name": packageName,
                    "version": "1.0.0",
                    "dist": {
                        "tarball": `http://localhost:3000/tarballs/${packageName}.tgz`
                    }
                }
            },
            "dist-tags": {
                "latest": "1.0.0"
            }
        });
    }
});

app.listen(port, () => {
    console.log(`Registry server running on port ${port}`);
});

内容的提问来源于stack exchange,提问作者Swerk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 18:52:44