TT Lab
开始
学习 学习路径 课程

CBA — Backstage 认证助理

改了源码,本地后端的响应却没变

在 TT Lab 中继续学习

目标

亲手搭建一个与 Backstage 应用仓库结构相同的 Yarn 4 工作区,并完整走一遍从依赖安装、锁文件、TypeScript 编译、构建产物到本地运行的开发流程。评分器不只看文件,而是根据重新运行 yarn、tsc、node 的结果来判定。

为什么重要

“在我的电脑上能跑,CI 里安装却失败”“明明有类型错误,dist 为什么还是生成了”“改了源码,响应却没变”——开发 Backstage 时最常碰到的,往往不是插件代码,而是这套流程。只有清楚锁文件锁定了什么、tsc 能抓到什么又会漏掉什么、后端实际运行的是哪个文件,才能快速找到原因。

VM 首次启动大约需要 4 分钟,第 2 步的安装需要从互联网下载,要几十秒。实际项目中的 backstage-cli(yarn start、yarn build)和前端应用(packages/app)安装体积大、构建耗时长,所以不使用,而是用 yarn、tsc、node 手动完成同样的工作。这台 VM 上没有 docker,因此不构建镜像。

步骤

  1. 用根目录、backend、插件三个 package.json 和 .yarnrc.yml 搭建工作区。
  2. 通过 yarn install 生成锁文件和工作区链接。
  3. 记录与锁文件不一致的 package.json 在 --immutable 下被拦截,然后恢复原状。
  4. 记录 tsc 能抓到错误的策略值,但仍然会生成 JS。
  5. 开启 noEmitOnError,修正后再构建。
  6. 在本地启动接入了插件的后端。
  7. 确认只修改源码并重启不会改变结果,必须构建才会改变。
  8. 用当前仓库中测得的值编写开发流程检查清单。

参考

搭建与应用仓库结构相同的工作区

在 /usr/local/cba-dev 中创建 Yarn 工作区。根目录 package.json 设置 private: true、packageManager: "yarn@4.9.2"、workspaces: ["packages/*", "plugins/*"]。根目录 .yarnrc.yml 设置 nodeLinker: node-modules、enableTelemetry: false、enableGlobalCache: false、enableMirror: false、globalFolder: /usr/local/cba-dev/.yarn-global。packages/backend/package.json(名称为 backend)的 dependencies 为 @backstage/backend-defaults 0.17.8、better-sqlite3 12.4.1、@internal/plugin-hello-backend workspace:^。plugins/hello-backend/package.json(名称为 @internal/plugin-hello-backend,main 为 dist/index.js,types 为 dist/index.d.ts,scripts 为 build: tsc -p tsconfig.json)的 dependencies 为 @backstage/backend-plugin-api 1.10.0 和 express 4.22.3,devDependencies 为 typescript 5.9.3 和 @types/express 4.17.25。所有外部版本都不带范围符号。corepack yarn workspaces list 应列出三个工作区。

Backstage 应用是由 packages/app(前端)、packages/backend 和 plugins/* 组成的 Yarn 工作区。这台 VM 不构建应用包,所以只放 backend 和后端插件。corepack 会根据 packageManager 字段下载并使用该版本的 yarn。根磁盘很小,请先在 shell 中执行 export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COREPACK_HOME=/usr/local/cba-dev/.corepack TMPDIR=/usr/local/cba-dev/.tmp(以及 mkdir -p /usr/local/cba-dev/.tmp)。如果不关闭全局缓存和镜像,复制到主目录时会因空间不足而失败。

一次安装就生成锁文件和工作区链接

在 /usr/local/cba-dev 中执行 corepack yarn install。根目录应生成 yarn.lock,node_modules/@internal/plugin-hello-backend 应是指向 plugins/hello-backend 的链接,node_modules/@backstage/backend-defaults 应为 0.17.8。随后 corepack yarn install --immutable 也必须成功。

Yarn 工作区会把依赖集中到根目录的 node_modules 中,workspace: 协议的依赖不是复制而是链接。请用 ls -l node_modules/@internal 和 grep -n 'backend-defaults@npm' yarn.lock 确认。peer 依赖警告(YN0086)不是安装失败。

与锁文件不一致的 package.json 会在 CI 中被拦截

只把 plugins/hello-backend/package.json 中 express 的版本改为 4.21.2,并且保持 yarn.lock 不变,执行 corepack yarn install --immutable,把完整输出保存到 /root/cba-dev/immutable.txt(同时留意退出码)。然后把 express 恢复为 4.22.3,让 --immutable 重新成功。yarn.lock 自始至终都不应改变。

如果安装结果需要修改锁文件,--immutable 就不会执行而是失败。CI 中使用这个选项,是为了防止只在开发者电脑上解析出的依赖版本悄悄混入部署。恢复之后,不要不带该选项执行 install——否则不一致的版本会被写进锁文件。失败码以 YN 开头的编号形式给出。

tsc 能抓到错误的策略值,但仍然会生成 JS

创建 plugins/hello-backend/tsconfig.json(target 为 ES2022,module 为 commonjs,moduleResolution 为 node,以及 strict、esModuleInterop、skipLibCheck、declaration,rootDir 为 src,outDir 为 dist,include 为 ["src"],不含 noEmitOnError)和 src/index.ts。index.ts 用 createBackendPlugin 创建 pluginId 为 hello 的插件,让 GET /ping 返回 {"version": 1},并故意写成 http.addAuthPolicy({ path: '/ping', allow: 'public' })。把 helloPlugin 既作为具名导出,也作为 default 导出。删除 dist 后,在插件目录中执行 corepack yarn tsc -p tsconfig.json,把输出保存到 /root/cba-dev/tsc-error.txt,并在末尾追加一行 emitted_despite_error=<dist/index.js 가 생겼으면 yes, 아니면 no>(占位符为:若生成了 dist/index.js 则写 yes,否则写 no)。

addAuthPolicy 的 allow 不是任意字符串,而是固定的字面量联合类型。如果是 JavaScript,这种错误要到启动之后才会发现;在编译阶段就抓住它,正是使用 TypeScript 的原因。不过 tsc 的默认行为是即使有类型错误也会写出输出——请同时查看退出码和 dist。

构建时只要有类型错误就什么都不输出

在 tsconfig 中加入 "noEmitOnError": true,把 index.ts 中的策略值改为正确的 'unauthenticated',然后用 corepack yarn workspace @internal/plugin-hello-backend build 构建。dist/index.js 和 dist/index.d.ts 必须由当前的 src 生成,并且在插件目录中执行 corepack yarn tsc -p tsconfig.json --noEmit 应无错误结束。

构建产物(dist)必须比源码新。yarn workspace <이름> <스크립트>(占位符依次为工作区名称和脚本名称)会在根目录运行指定软件包的脚本。开启 noEmitOnError 后故意写错,还能确认 dist 不会被更新。

在本地启动接入了工作区插件的后端

在 packages/backend/index.js 中,对 createBackend() 执行 add require('@internal/plugin-hello-backend') 后 start。app-config.yaml(backend.baseUrl 为 http://localhost:7007,listen.port 为 7007,数据库为 better-sqlite3、':memory:')放在工作区根目录 /usr/local/cba-dev/app-config.yaml 中。在 packages/backend 中用 node index.js 启动,输出追加写入 packages/backend/backend.log,不带认证时 GET http://127.0.0.1:7007/api/hello/ping 返回 200 {"version":1} 即可。

如果不给出 --config,后端查找的不是运行目录,而是仓库根目录的 app-config.yaml——如果把配置放在 packages/backend 中会出什么错,请在日志第一行查看。require 会沿着 node_modules 中的链接,走到插件 package.json 的 main。请用 node -p "require('fs').realpathSync(require.resolve('@internal/plugin-hello-backend'))" 确认实际文件。

修改源码、构建并重新启动,响应才会改变

把 index.ts 中 /ping 的响应改为 {"version": 2}。先不构建,只重启后端,看到响应仍是 1;然后构建插件,再重启后端,让它变成 2。在 /root/cba-dev/dev-loop.txt 中留下 without_build=<빌드 없이 재시작했을 때 version> 和 after_build=<빌드 뒤 재시작했을 때 version> 两行(占位符依次为不构建就重启时的 version 和构建后重启时的 version)。

后端运行的不是 src 中的 TypeScript,而是 main 所指向的 dist 中的 JavaScript。在真实的 Backstage 仓库中,yarn start(backstage-cli)会代为完成这种转换和重启,但本实验不使用这个工具,而是手动走完这些步骤。

用当前仓库填写开发流程检查清单

在 /root/cba-dev/report.md 的前五行写入当前测得的值——yarn_version=(在根目录执行 corepack yarn --version)、workspace_count=(workspaces list 的行数)、immutable_install=(此刻 --immutable 成功则为 pass,否则为 fail)、plugin_entry=(在 packages/backend 中对插件执行 require.resolve 得到的实际路径)、ping_version=(当前 /api/hello/ping 的 version)。在其下写明锁文件、类型检查和构建产物在开发流程中各自防止了什么,以及在这台 VM 上没有构建 Docker 镜像的原因。

所有值都要通过现在执行命令来获得。实际路径是沿着符号链接得到的 realpath。用 command -v docker 确认是否有该工具。