4558 字
15 min

把 Shirone 从源码仓库装进 npm: 一次双模式 Astro Integration 改造

今天是中秋节, 祝大家中Cia快llo~(∠・ω< )⌒★

Shirone 早期是一个只能通过 Git Clone 使用的 Astro 主题. 用户想使用它, 需要把整个仓库拉下来, 再自行安装依赖, 修改配置和维护内容.

这种模式适合主题作者本人, 却很难直接交给普通用户. 每建一个博客, 都要 fork 完整仓库. 每次升级主题, 都要面对自己的修改与上游更新之间的冲突. 对的催了这么久的更新终于来了

截至 2026 年 9 月 25 日, Shirone 的 main 分支已有 989 个提交和 897 个受 Git 跟踪的文件. 它同时包含页面, 组件, 样式, 内容示例, 构建脚本和测试. 继续把所有能力都塞在整仓复制的交付方式里, 维护成本只会继续上升.

这次改造的目标很明确: 保留源码模式的定制能力, 同时增加一个可以正常安装, 升级和初始化的 npm 包模式.

改造目标#

改造后的 Shirone 计划并行支持两种模式:

模式获取方式适用场景
源码模板Git Clone 主题仓库主题开发, 深度定制, 直接修改内部实现
npm 包执行 npx shirones init快速建站, 常规升级, 通过覆盖层扩展主题

两种模式共享同一份主题源码, 但需要明确解决几个问题:

  • 包内页面如何进入用户项目的 Astro 路由系统.
  • 用户配置, 组件和布局如何覆盖主题内部实现.
  • 主题文件与用户文件分别从哪里读取.
  • pnpm 严格依赖布局下, 哪些依赖必须暴露给用户项目.
  • 源码仓与 npm 包如何共用同一套配置入口.
  • npm tarball 如何构建和验证.

核心思路是将主题自身封装成 Astro Integration. 用户项目只需调用 Integration, 内容和配置则放在约定目录中.

历史起点: 它确实只能跑源码#

旧版 Shirone 已经使用了 Swup, Svelte, MDX, Sitemap 等第三方 integration. 真正缺失的是 Shirone 自己那层 src/integration/.

在 2026 年 8 月 28 日以前, 主题仓的运行方式仍然完全依赖源码布局:

  • 根目录的 astro.config.mjs 负责注册所有 integration.
  • tsconfig.json 中的别名全部指向仓库自身的 src.
  • 内容集合直接读取 src/content.
  • 页面由 Astro 扫描仓库中的 src/pages.
  • 字体, Markdown processor 和 Vite 插件都由根配置手工组织.

这时虽然已经有 package.json, 但它主要用于执行 dev, build 和 preview, 尚无 files, exports, bin, peerDependencies 等发布契约.

源码模式本身没有稳定性问题. 问题出现在这些文件被移动到 node_modules 之后.

架构概览#

改造后的职责被拆成两个仓库.

那么至于为什么要拆成两个呢?一开始是打算用单独的密钥去发包, 但有希并没有给我LyraVoid的权限, 所以我用的自建仓库发包, 虽然后面摆烂直接用oicd了, 另一个原因就是shirones起初还很完善, 我协作习惯pr更改而不是直接提交, Shirone因为策略, 所以每次shirones的改动提了pr后都要找黎明点个review, 其实就是嫌麻烦

主题仓: Shirone#

主题仓继续维护所有主题实现:

  • src/pages: 主题路由.
  • src/components, src/layouts: 组件和布局.
  • src/config, src/data: 默认配置与数据.
  • src/content: 示例内容与集合定义.
  • src/integration: 源码模式和 npm 模式共用的运行入口.
  • docs/npm-package-mode.md, docs/packaging-contract.md: 双模式约束.
  • tests: 主题站点, 配置所有权和集合声明测试.

包仓: shirones#

包仓不保存主题源码, 只负责转换和发布:

  • 从指定 ref 获取 Shirone.
  • 生成用户初始化模板.
  • 打包 Astro Integration 与主题源码.
  • 生成发布包清单, manifest 和构建信息.
  • 使用真实 tarball 安装并验证.
  • 验证配置, 数据, 组件和布局覆盖.
  • 通过 GitHub Actions 手动发布 npm 包.

两个仓库拥有完全独立的 Git 历史, 没有共同可达对象. 包仓也没有做持久镜像, 每次构建都从明确的上游 ref 重新获取源码.

这种分工让主题仓继续作为唯一实现来源, 同时允许发布流程独立演进.

关键技术改造#

1. 用 Integration 统一运行入口#

第一版主题 Integration 在 2026 年 8 月 28 日加入主题仓. 它一次增加了 9 个文件, 负责以下工作:

  • 在 astro:config:setup 阶段注册路由, 字体, Markdown 和 Vite 配置.
  • 扫描主题页面, 并在包模式下调用 injectRoute.
  • 建立配置, 数据, 组件和布局的覆盖映射.
  • 在 Node 构建阶段加载用户配置.
  • 管理字体声明, 子集化与缓存.
  • 在构建结束后生成 Pagefind 索引.
  • 提供 init, info 和升级相关 CLI 能力.

这一步让包模式拥有了自己的生命周期, 目录解析和用户扩展规则.

2. 明确主题文件与用户文件的归属#

包模式中, 主题代码位于 node_modules/shirones, 用户项目根目录则是另一个文件系统边界.

Integration 因此对配置, 数据, 组件和布局建立了固定映射:

主题内部位置用户覆盖位置
src/configshirones/config
src/datashirones/config/data
src/components用户项目的 src/components
src/layouts用户项目的 src/layouts

用户只需镜像包内目录结构, 即可覆盖单个文件, 无需 fork 整个主题.

Vite overlay 只处理主题别名, 以及 importer 确实位于主题包内部的相对导入. 这个范围经过一次重要修正.

早期 resolver 会拦截几乎所有 specifier, 导致 shirones/collections 也被错误接管, 内容集合类型生成失败. 后来将处理范围收窄后, 路由和公开导出才恢复正常.

3. Node 阶段也需要加载用户配置#

部分配置会在 Vite 启动前被 Astro 读取, 例如站点 URL, 字体声明和 Expressive Code 主题.

这些配置属于 TypeScript, 又需要根据当前运行模式决定从哪里加载. 因此 Integration 在 Node 阶段使用 esbuild 对用户配置进行单独打包, 并复用与 Vite overlay 相同的覆盖规则.

这一层解决了一个现实问题: 同一个配置文件既可能被页面直接导入, 也可能在 Integration 初始化时被 Node 读取. 两边必须得到一致结果.

4. 路由注入与 Astro starter 冲突#

主题页面在源码模式下可以直接被 Astro 扫描, 进入 npm 包后则需要 Integration 注入.

用户项目又可能来自 create-astro, 其中已经存在首页, Layout, 组件和配置文件. Astro 对本地文件路由的优先级高于注入路由, 结果可能是 Integration 已经注册主题页面, 用户仍然看到 starter 首页.

CLI 因此增加了 starter 清理和备份流程:

  • 普通 init 以报告漂移为主.
  • init --update 只补回缺失文件.
  • init --force 先将旧内容移动到 .shirones-backup/, 再替换模板.
  • 用户本地路由继续拥有最高优先级.

5. pnpm 严格依赖改变了解析规则#

源码项目中, 依赖都位于项目根, Vite 和 Node 通常可以直接解析.

发布包使用 pnpm 严格布局后, 主题内部可见的依赖不一定能从用户项目根访问. 这造成了几类问题:

  • 包内 bare import 无法从用户根解析.
  • sharp 的动态导入可能触发版本错配.
  • Iconify 数据集通过 require.resolve 查找, 必须真实存在于用户项目.
  • Svelte 子路径需要由用户根可见的 peer 依赖提供.

最终处理方式包括:

  • 将关键运行时依赖提升为 peer dependency.
  • 从上游依赖范围派生 peer 版本.
  • 对失败的包内导入使用 fallback resolver.
  • 将 sharp 及其原生包保持 external, 避免错误复用.
  • 在 init 时安装这些 peer 依赖.

6. CommonJS 在构建期仍需要 Node 全局#

Shirone 最终产物是静态 HTML, 但 Astro 会在构建阶段通过 Node 执行页面和组件.

Stylus 等 CommonJS 依赖被 Vite 打包进 SSR chunk 后, 会失去原本的 __dirname, __filename 和 require. Integration 后续增加了 SSR Node shim, 让这些模块在两种模式下都能找到真实文件位置.

这也是源码模式很难提前暴露的问题: 主题仓直接运行源码时, 模块通常保持原始形态. 发布包才会经历完整的 bundling 和 SSR 转换.

7. 模板转换成为独立流水线#

shirones没有保存主题源码, 从第一天开始就负责把上游源码转换成可安装的包.

构建分为几个阶段:

  1. 浅克隆指定上游 ref.
  2. 复制到临时 workspace.
  3. 生成 init 使用的 dist/template.
  4. 打包 Integration, 主题源码, 类型和 manifest.
  5. 执行 npm pack.
  6. 在临时项目中安装真实 tarball.
  7. 运行 init, Astro build, dev smoke 和覆盖测试.
  8. 生成构建信息并发布.

由于源码和用户目录布局不同, 流水线还需要重写配置, 数据与示例文章中的相对路径.

src/user 曾经因为目录白名单遗漏而没有进入包内. 后来流水线改成排除列表, 新顶层 src 目录可以自动进入发布包.

8. 两套配置最终合并为一个入口#

包化早期, 主题仓根配置和 Integration 各维护一套完整配置. Sitemap, Swup, Iconify 等选项都曾只修改其中一边.

包仓先加入配置 parity 检查, 后来将规则升级为 config ownership:

  • 源码仓的 astro.config.mjs 只负责调用 shirones().
  • 选项值由 src/config/integrationsConfig.ts 提供.
  • Vite, 路由, 字体, Markdown 和 integration wiring 全部归 Integration 管理.

2026 年 9 月 15 日, 主题仓根配置正式缩成一次 Integration 调用. 源码模式和包模式从此共用同一个入口.

pnpm link 和 workspace symlink 会将模块解析到真实路径. 旧实现使用多组布尔条件判断模式, 真实路径可能同时不满足插件模式和源码模式.

结果包括:

  • 使用包模式目录默认值.
  • 不注入主题路由.
  • 不给出 init 提示.
  • 页面静默变成空站点.

后来将判断收敛为单一的 isThemeRepo 状态, 模式边界才变得稳定.

10. 发布版本与主题版本解耦#

早期流水线直接继承主题仓的 package.json 版本. 主题版本描述源码树, npm 版本描述一次发布, 两者继续绑定时, 忘记 bump 就会在发布最后一步失败.

版本选择后来移到包仓:

  • 工作流显式输入优先.
  • 未输入时基于 npm 最新稳定版本做 patch bump.
  • 已存在版本直接终止发布.
  • prerelease 不参与稳定版本基线选择.

发布产物还会记录上游 SHA, 流水线 commit, Node 和 pnpm 版本, 便于发布后追溯.

踩坑#

问题表面现象根因处理方式
process.cwd() 指向用户项目主题文件 ENOENT把用户目录当成主题目录按文件所有权拆分路径解析
pnpm 严格依赖失败包内 import, MissingSharp用户根无法访问包内依赖peer dependency 与 fallback resolver
Overlay 拦截公开导出shirones/collections 失效resolver 接管范围过宽仅处理主题别名和包内相对导入
Starter 遮蔽首页主题已安装, 仍显示欢迎页本地文件路由优先清理并备份 starter 文件
SSR 缺少 Node 全局Stylus 找不到资源目录CommonJS 被内联进 ESM chunk增加 SSR Node shim
两套配置漂移sitemap, Swup 等行为不同source/package wiring 重复config ownership 检查
pnpm 构建脚本被忽略esbuild, sharp 安装失败pnpm 10/11 配置位置不同同时维护两代 build approval
link 模式空站点无路由, 无报错symlink realpath 改变判断收敛为 isThemeRepo
collection base 指向 src构建成功但集合为空模板仍使用源码目录从 manifest 生成用户路径

最终#

改造后的主题仓根配置只保留 Integration 调用, Integration 根据运行模式解析目录, 路由, 配置和覆盖关系.

包仓则负责把指定上游 ref 转换成 npm tarball, 并在临时项目中验证真实安装生命周期.

用户可以获得两种工作流:

需求推荐模式
修改主题内部实现Git Clone 源码模式
快速创建个人博客npm 包模式
覆盖单个组件或配置npm 包模式
跟随主题版本升级npm 包模式
参与主题开发和测试源码模式

两种模式共享同一份主题源码, 同一套路由发现和配置 ownership. 目录布局可以不同, 运行契约保持一致.

虽然没人给我发中秋快乐, 还是祝大家中秋过的开心, 我们修复了月亮不圆的Bug, 仅限今晚哦

把 Shirone 从源码仓库装进 npm: 一次双模式 Astro Integration 改造
https://fuwari.oh1.top/posts/Essay/shirones-npm/
作者
yCENzh
发布于
2026-09-25