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

使用esbuild打包Express API时public目录未被打包的问题排查

问题:esbuild打包Express API时public目录未被正确打包

我用esbuild打包Express API,目前其他部分正常,但存放swagger UI的public目录没跟着打包。静态路由配置如下:

app.use(express.static(`${root}/public`));

试过三种方法都没解决:手动复制public到build目录、使用esbuild-plugin-public-directory插件、把public/index.html加入entryPoints配置。附上当前esbuild配置代码:

const publicDir = require("esbuild-plugin-public-directory");

const OUTDIR = "build";

const envPlugin = {
    name: "env",
    setup(build) {
        build.onResolve({ filter: /^env$/ }, (args) => ({
            path: args.path,
            namespace: "env-ns",
        }));

        build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
            contents: JSON.stringify(process.env),
            loader: "json",
        }));
    },
};

require("esbuild")
    .build({
        entryPoints: ["server/start.ts", "public/index.html"],
        platform: "node",
        bundle: true,
        minify: false,
        platform: "node",
        logLevel: "info",
        sourcemap: false,
        target: "node12",
        loader: {
            '.html': 'text',
        },
        outdir: "build",
        plugins: [envPlugin, publicDir()],
    })
    .then(() => {
        fs.copyFileSync("server/common/api.yml", `${OUTDIR}/api.yml`);
        console.log(`Successfully built, output directed to the ${OUTDIR} directory`);
    })
    .catch(() => process.exit(1));

问题排查

  1. 冗余配置冲突:配置里重复写了两次platform: "node",虽不致命但易引发混淆。
  2. 插件与entryPoints冲突:把public/index.html加入entryPoints并指定text loader,会把HTML内容打包进JS bundle,而非作为静态文件输出;同时esbuild-plugin-public-directory的复制逻辑可能被覆盖。
  3. 手动复制逻辑不全:仅复制了单个文件,未递归复制整个public目录。
  4. 静态路由路径错误:打包后root变量若指向原项目根目录,会导致静态资源路径指向未打包的原public目录,而非build目录下的副本。

解决方案

方案1:手动递归复制public目录(最可靠)

移除冲突配置,在build成功回调里添加递归复制逻辑:

const fs = require('fs');
const path = require('path');

const OUTDIR = "build";

const envPlugin = {
    name: "env",
    setup(build) {
        build.onResolve({ filter: /^env$/ }, (args) => ({
            path: args.path,
            namespace: "env-ns",
        }));

        build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
            contents: JSON.stringify(process.env),
            loader: "json",
        }));
    },
};

require("esbuild")
    .build({
        entryPoints: ["server/start.ts"], // 移除public/index.html,避免被打包进JS
        platform: "node",
        bundle: true,
        minify: false,
        logLevel: "info",
        sourcemap: false,
        target: "node12",
        outdir: OUTDIR,
        plugins: [envPlugin], // 移除publicDir插件,改用手动复制
    })
    .then(() => {
        // 复制api.yml
        fs.copyFileSync("server/common/api.yml", `${OUTDIR}/api.yml`);
        // 递归复制public目录到build下
        const publicSource = path.resolve(__dirname, 'public');
        const publicDest = path.resolve(__dirname, OUTDIR, 'public');
        // 清空目标目录(可选)
        if (fs.existsSync(publicDest)) {
            fs.rmSync(publicDest, { recursive: true });
        }
        fs.cpSync(publicSource, publicDest, { recursive: true });
        console.log(`Successfully built, output directed to the ${OUTDIR} directory`);
    })
    .catch(() => process.exit(1));

同时修正静态路由路径,确保指向build目录下的public:

// 若start.ts在build目录下,__dirname即为build目录
const root = path.resolve(__dirname);
app.use(express.static(path.join(root, 'public')));

方案2:正确使用esbuild-plugin-public-directory插件

确保插件已安装(npm install esbuild-plugin-public-directory --save-dev),并调整配置避免冲突:

const publicDir = require("esbuild-plugin-public-directory");

const OUTDIR = "build";

const envPlugin = {
    name: "env",
    setup(build) {
        build.onResolve({ filter: /^env$/ }, (args) => ({
            path: args.path,
            namespace: "env-ns",
        }));

        build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
            contents: JSON.stringify(process.env),
            loader: "json",
        }));
    },
};

require("esbuild")
    .build({
        entryPoints: ["server/start.ts"], // 移除public/index.html
        platform: "node",
        bundle: true,
        minify: false,
        logLevel: "info",
        sourcemap: false,
        target: "node12",
        outdir: OUTDIR,
        plugins: [envPlugin, publicDir({ dir: './public' })], // 显式指定源目录
    })
    .then(() => {
        fs.copyFileSync("server/common/api.yml", `${OUTDIR}/api.yml`);
        console.log(`Successfully built, output directed to the ${OUTDIR} directory`);
    })
    .catch(() => process.exit(1));

同样需确保静态路由的root指向build目录。


关键注意事项

  • 若Node.js版本低于16,fs.cpSync不支持,可改用fs-extra包的copySync方法实现递归复制。
  • 避免同时使用插件和手动复制逻辑,防止资源重复或冲突。
  • 始终验证打包后静态资源的实际路径是否与路由配置匹配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 23:30:55