|
| 1 | +# ADR-0043: SSG Phase 3 依赖策略 — external + noExternal 两层模型 |
| 2 | + |
| 3 | +> **Status**: PROPOSED |
| 4 | +> **Date**: 2026-05-25 |
| 5 | +> **Applies to**: v0.21.x → v0.22.0 |
| 6 | +> **Extends**: ADR-0042 (Import Map Universal Resolution) |
| 7 | +> **Supersedes**: ADR-0008 Phase C 中的 `noExternal: ALL` 策略 |
| 8 | +
|
| 9 | +## Context |
| 10 | + |
| 11 | +当前 SSG Phase 3 的 `viteBuild({ssr:true})` 配置使用统一的 `noExternal` 策略: |
| 12 | + |
| 13 | +```ts |
| 14 | +const defaultNoExternal = [ |
| 15 | + /^lit/, /^@lit/, /^@lessjs\/ui/, /^@lessjs\/adapter-lit/, |
| 16 | + 'parse5', 'entities', 'node-fetch', 'fetch-blob', |
| 17 | + 'data-uri-to-buffer', 'formdata-polyfill', 'domexception', 'node-domexception', |
| 18 | +]; |
| 19 | +``` |
| 20 | + |
| 21 | +这个策略将所有依赖内联到单一 SSR bundle 中。动机是: |
| 22 | +1. 生成自包含 bundle,方便 Deno Deploy 部署 |
| 23 | +2. 确保模块级变量(Phase B)在整个图中共享 |
| 24 | + |
| 25 | +问题: |
| 26 | +1. **Rolldown 子路径解析失败**:`parse5` 内部引用 `entities/lib/escape.js`,Rolldown 无法正确解析 |
| 27 | +2. **bundle 膨胀**:node-fetch、fetch-blob 等 Deno 已有原生实现的包被冗余打包 |
| 28 | +3. **维护成本**:每个新增 SSR 依赖都需要手动添加到 `noExternal` 列表 |
| 29 | +4. **诊断困难**:Rolldown 的 npm 解析错误信息不友好,难以定位根因 |
| 30 | + |
| 31 | +## Decision |
| 32 | + |
| 33 | +**SSG Phase 3 采用两层依赖策略:`external`(SSR 传递依赖)+ `noExternal`(LessJS/Lit 业务代码)。** |
| 34 | + |
| 35 | +### 第一层:noExternal — LessJS 业务代码 + Lit 生态 |
| 36 | + |
| 37 | +这些包由 Rolldown 打包进 SSR bundle,因为它们需要: |
| 38 | +- TypeScript 编译(Lit decorators) |
| 39 | +- 模块级变量共享(Phase B 单例) |
| 40 | +- Tree-shaking 和 dead code elimination |
| 41 | + |
| 42 | +```ts |
| 43 | +const ssrNoExternal = [ |
| 44 | + /^@lessjs\//, // 所有 LessJS 框架包 |
| 45 | + /^lit/, // lit, lit-html, lit-element |
| 46 | + /^@lit/, // @lit/reactive-element, @lit-labs/ssr-dom-shim |
| 47 | + /^@lit-labs\//, // @lit-labs/* (ssr-dom-shim 等) |
| 48 | +]; |
| 49 | +``` |
| 50 | + |
| 51 | +### 第二层:external — SSR 传递依赖 |
| 52 | + |
| 53 | +这些包由 Deno ESM 运行时在 `import()` 阶段通过 import map 解析: |
| 54 | + |
| 55 | +```ts |
| 56 | +const ssrExternal = [ |
| 57 | + 'parse5', // HTML parser — 有子路径导出 |
| 58 | + 'entities', // HTML entity codec — 有子路径导出 |
| 59 | + 'hono', // HTTP framework |
| 60 | + 'hono/*', // Hono subpath exports |
| 61 | + 'node-fetch', // Deno 有原生 fetch |
| 62 | + 'fetch-blob', // Deno 有原生 Blob |
| 63 | + 'data-uri-to-buffer', |
| 64 | + 'formdata-polyfill', |
| 65 | + 'domexception', |
| 66 | + 'node-domexception', |
| 67 | +]; |
| 68 | +``` |
| 69 | + |
| 70 | +### 决策规则 |
| 71 | + |
| 72 | +判断一个依赖属于哪一层的规则: |
| 73 | + |
| 74 | +| 条件 | 层级 | 理由 | |
| 75 | +|------|------|------| |
| 76 | +| `@lessjs/*` 包 | `noExternal` | 需要 TypeScript 编译 + Phase B 单例共享 | |
| 77 | +| `lit` / `@lit/*` 生态 | `noExternal` | 需要 decorator 编译 + Lit SSR 内部状态 | |
| 78 | +| npm 包有子路径导出且被传递依赖引用 | `external` | Rolldown 无法解析 → 交还 Deno | |
| 79 | +| npm 包在 Deno 有原生替代 | `external` | 避免冗余打包 | |
| 80 | +| npm 纯 JS 包无子路径依赖 | `external`(默认) | 减少 Rolldown 负担 | |
| 81 | + |
| 82 | +## Architecture |
| 83 | + |
| 84 | +``` |
| 85 | +viteBuild({ ssr: true }) |
| 86 | +│ |
| 87 | +├── ssr.noExternal ┌─────────────────────────┐ |
| 88 | +│ ├── /^@lessjs\// ──────────────▶ │ Rolldown 打包 │ |
| 89 | +│ ├── /^lit/ │ - TypeScript 编译 │ |
| 90 | +│ └── /^@lit/ │ - Decorator 转换 │ |
| 91 | +│ │ - Tree-shaking │ |
| 92 | +│ │ - 模块级变量共享 │ |
| 93 | +│ └──────────┬──────────────┘ |
| 94 | +│ │ |
| 95 | +├── ssr.external ▼ |
| 96 | +│ ├── parse5 ┌─────────────────────────┐ |
| 97 | +│ ├── entities │ server/entry.js │ |
| 98 | +│ ├── hono │ │ |
| 99 | +│ └── ... │ import { Hono } from │ |
| 100 | +│ │ 'hono'; ← external │ |
| 101 | +│ │ import { parse } from │ |
| 102 | +│ │ 'parse5'; ← external │ |
| 103 | +│ └──────────┬──────────────┘ |
| 104 | +│ │ |
| 105 | +└── build.outDir: dist/server/ ▼ |
| 106 | + ┌─────────────────────────┐ |
| 107 | + │ Deno import() │ |
| 108 | + │ │ |
| 109 | + │ 通过 deno.json import │ |
| 110 | + │ map 解析所有 external: │ |
| 111 | + │ │ |
| 112 | + │ hono → npm:hono@^4 │ |
| 113 | + │ parse5 → npm:parse5@7 │ |
| 114 | + │ entities → npm:entities@^4│ |
| 115 | + │ │ |
| 116 | + │ ✅ 子路径正确 │ |
| 117 | + └─────────────────────────┘ |
| 118 | +``` |
| 119 | + |
| 120 | +## importmap.json Sidecar |
| 121 | + |
| 122 | +为保持与 Deno Deploy 的兼容性,SSG Phase 3 继续生成 `importmap.json` sidecar 文件。但内容从"所有依赖"简化为"external 依赖": |
| 123 | + |
| 124 | +```json |
| 125 | +{ |
| 126 | + "imports": { |
| 127 | + "hono": "npm:hono@^4.12.18", |
| 128 | + "parse5": "npm:parse5@7.0.0", |
| 129 | + "entities": "npm:entities@^4", |
| 130 | + "entities/": "npm:entities@^4/" |
| 131 | + } |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +`noExternal` 的包已在 bundle 中内联,不需要出现在 import map 中。 |
| 136 | + |
| 137 | +## Consequences |
| 138 | + |
| 139 | +### Positive |
| 140 | + |
| 141 | +- **消除子路径解析 bug**:`entities/lib/escape.js` 等所有子路径由 Deno 正确解析 |
| 142 | +- **bundle 缩小**:不再打包 parse5(~200KB)、hono(~50KB)等大包 |
| 143 | +- **构建速度提升**:Rolldown 处理的模块数量大幅减少 |
| 144 | +- **维护简化**:新增 SSR 依赖默认走 external,无需手动配置 |
| 145 | +- **诊断清晰**:external 依赖的解析错误由 Deno 报告,错误信息更友好 |
| 146 | + |
| 147 | +### Negative |
| 148 | + |
| 149 | +- **部署复杂度增加**:不再是单一 self-contained bundle |
| 150 | +- **import map 同步**:`deno.json` 和 `importmap.json` 需要保持一致 |
| 151 | +- **vendor 依赖**:Deno Deploy 环境需要 vendor 缓存或远程拉取 |
| 152 | +- **诊断分散**:问题可能出在 Rolldown(noExternal)或 Deno(external)两个层级 |
| 153 | + |
| 154 | +### Mitigation |
| 155 | + |
| 156 | +- 在 CI 中增加 `deno vendor` 步骤,预缓存所有 external 依赖 |
| 157 | +- `importmap.json` 从 `deno.json` 自动生成,避免手动同步 |
| 158 | +- 构建日志区分 "Rolldown bundled" 和 "Deno external" 两类依赖 |
| 159 | + |
| 160 | +## Related |
| 161 | + |
| 162 | +- ADR-0008: Build Pipeline Phases (Phase C: SSG via viteBuild) |
| 163 | +- ADR-0041: ESM Module Graph First for JSR Consumer Builds |
| 164 | +- ADR-0042: Import Map as Universal Resolution Layer |
0 commit comments