这篇文章上次修改于 235 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

又是一个万恶的周一,回到公司发现上周五改完需求提交的代码在 CI 的时候发生了故障(还好只是测试环境)。简单概括,就是不知道为什么 ua-parser-js 这个依赖的 TypeScript 类型读取不到,需要安装一个叫 @types/ua-parser-js 的库。

Type error: Could not find a declaration file for module 'ua-parser-js'. '/app/node_modules/.pnpm/[email protected]/node_modules/ua-parser-js/src/ua-parser.js' implicitly has an 'any' type.

Try `npm i --save-dev @types/ua-parser-js` if it exists or add a new declaration (.d.ts) file containing `declare module 'ua-parser-js';`

  1 | import axios from "axios";
> 2 | import uap from "ua-parser-js";
    |                 ^

而实际上这个库我是已经安装了的,只是它写在了 package.json 文件的 devDependencies 里面。这个项目的 package.json 有个很诡异的点,就是几乎所有的包都写在了 dependencies 里面,包括其他 @types/xxxx 的包,我就觉得很奇怪,为什么读取不到 devDependencies 里面的包呢?

我从 CI 的流程一步一步看,最终定位到了 DockerFile 文件,光看代码的执行流程并没有发现什么奇怪的问题,总体来说就是安装依赖,启动进程,然后导出 3000 端口映射。我尝试用自己本地的 Docker 环境跑了一遍。

FROM node:18.15.0 AS runner

WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 941 nodejs
RUN adduser --system --uid 941 nextjs

COPY . ./

WORKDIR ./

RUN chmod 777 .
RUN npx --yes pnpm install
RUN npx pnpm build

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["npm", "start"]
docker build -t usercenter .

结果和前文一样,也是一模一样的报错。我尝试排查问题,并把这个配置文件发到了群里,@提莫 认为可能是环境变量 ENV NODE_ENV production 这行设置导致的。这小小的变量设置影响会有这么大么,我去掉它重新打包,发现的确正常了。

devDeps 是干什么的

先不说环境变量的事,为什么要有 devDependencies 而不是直接 dependencies 一把梭呢,我认为这个包如果要提供给其他包使用,总会有一个打包好的版本可以直接使用(不需要在父项目里面再执行构建一遍代码),那么负责构建过程的包是不是就没有必要被再次安装了?

如果你要构建这个项目本身,则还是必须安装 devDependencies 下的依赖才行,可以看看用 Vite 一类的脚手架,他们的 package.json 是不是就是这么写的?

为什么没有安装 devDeps

既然 devDependencies 的使用方式是正确的,那么错就错在其他地方了。仔细看过程,环境变量的设置放在了最前面,后面才安装依赖文件,这就可能导致没能安装上 devDependencies 下的所有依赖。

这个猜想是正确的,我查阅了 PNPM 的文档,的确是这样子。因此这个 DockerFile 的过程是存在问题的,不应该在安装依赖之前强制设置成 production 模式。

pnpm will not install any package listed in devDependencies and will remove those insofar they were already installed, if the NODE_ENV environment variable is set to production. Use this flag to instruct pnpm to ignore NODE_ENV and take its production status from this flag instead.

如果 NODE_ENV 环境变量被设置为 production, pnpm 将不会安装 devDependencies 中列出的任何包,并且将删除那些已经安装的包。使用这个标志可以指示 pnpm 忽略 NODE_ENV,并从这个标志中获取它的生产状态。

同时感谢群友 @咲奈 的回答:

  1. 好像确实是这样,如果你在 pnpm i 安装的时候就给了 NODE_ENV 为生产,就不装 devDeps
  2. Docker 文件里不需要声明 NODE_ENV,因为你也没用到这个变量,还影响了 pnpm i 安装的依赖
  3. Scripts 里不需要使用 cross-env 设定 NODE_ENV,在常规情况下,它的值一般只有 development / test / production,并且由构建工具自动帮你设定,你不需要设定。
  4. 如果在特殊情况下,需要在过程中 productionNODE_ENV 下执行 pnpm i,workaround 时先把 NODE_ENV 改成 development 装完依赖再改回来。
  5. 不应该把 @types/* 下面的依赖放到 deps 里,虽然大部分情况下,依赖在哪都是无所谓的,这只是个通俗约定,但你问的 GPT 骗了你