Skip to content

Commit a8173f5

Browse files
author
LessJS CI
committed
feat(v0.21.13): Clean Architecture — Import Map universal resolution (ADR-0042~0046)
Architecture: - Import Map (deno.json) as single source of truth for all 3 phases - Phase 1: @deno/vite-plugin + virtual-passthrough plugin - Phase 2: build-client.ts import map resolution + resolve.extensions - Phase 3: external (Deno native) + noExternal (Rolldown) two-tier strategy Bug fixes (7 total): - entities/lib/escape.js subpath resolution failure - customElements is not defined in SSR - TS type annotations leaking into polyfill banner - @lit-labs/ssr-dom-shim import unresolvable in SSG - Phase 2 @lessjs/ui/less-card bare specifier resolution - Phase 1 virtual:less-hono-entry unsupported scheme - ssg-package-resolver.ts deno fmt New files: - packages/adapter-vite/src/ssr-polyfills.ts - docs/adr/ADR-0042~0046 - docs/sop/v0.21.x/SOP-014, SOP-015 - CHANGELOG.md Bump: all 16 packages 0.21.12 → 0.21.13
1 parent 7236cc3 commit a8173f5

33 files changed

Lines changed: 1630 additions & 106 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ deliverables/
7171
# Hub submission template (local tool output, timestamp drifts on validate)
7272
hub-submission.json
7373
lighthouse-result*.json
74+
.deno-cache/

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## 0.21.13 (2026-05-25)
2+
3+
### Architecture: Clean Architecture — Import Map Universal Resolution (ADR-0042~0045)
4+
5+
- **SSG Phase 3 refactor**: Split SSR dependency strategy into external + noExternal two tiers.
6+
- `noExternal`: @lessjs/* + Lit ecosystem (bundled by Rolldown)
7+
- `external`: parse5, entities, hono, node-fetch, etc. (resolved by Deno ESM Runtime via import map)
8+
- **SSR polyfill unification**: New `ssr-polyfills.ts` module — CSSStyleSheet → HTMLElement → customElements
9+
- **Import map hardening**: Added `entities/` subpath mapping for Deno native npm resolution
10+
- **Consumer template**: genenated deno.json now includes SSR transitive deps (hono, parse5, entities)
11+
- **ADR**: 0042 (Import Map Universal Resolution), 0043 (SSG Phase3 dependency strategy), 0044 (SSR polyfill), 0045 (Native API first-class)
12+
13+
- **Phase 2 import map resolution**: Client island build now uses deno.json import map
14+
for module resolution, unified with Phase 1 and Phase 3. (ADR-0046)
15+
- **Phase 1 virtual module fix**: Consumer template now includes a `virtual-passthrough`
16+
resolve plugin (`enforce: 'pre'`) to intercept `virtual:*` module IDs before
17+
`@deno/vite-plugin`, avoiding unsupported scheme errors. (SOP-015)
18+
19+
### Fixed
20+
21+
- `entities/lib/escape.js` subpath resolution failure in Rolldown SSR bundle
22+
- `customElements is not defined` ReferenceError in SSR environment
23+
- `deno fmt` check failure in ssg-package-resolver.ts
24+
25+
### Changed
26+
27+
- `defaultNoExternal` in build-ssg.ts now only covers @lessjs/* + Lit ecosystem
28+
- SSG entry code uses shared polyfill module instead of inline + output.banner
29+
- importmap.json sidecar now only records external dependencies

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"@types/node": "npm:@types/node@^22.0.0",
7373
"parse5": "npm:parse5@7.0.0",
7474
"entities": "npm:entities@^4",
75+
"entities/": "npm:entities@^4/",
7576
"flexsearch": "npm:flexsearch@0.8.212",
7677
"@playwright/test": "npm:@playwright/test@1.59.1",
7778
"ws": "npm:ws@^8.20.1",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# ADR-0042: Import Map 作为 Universal Resolution Layer
2+
3+
> **Status**: PROPOSED
4+
> **Date**: 2026-05-25
5+
> **Applies to**: v0.21.x → v0.22.0
6+
> **Extends**: ADR-0041 (ESM Module Graph First)
7+
> **Supersedes**: 部分替代 ADR-0028 中 SSR bundle 的 noExternal 策略
8+
9+
## Context
10+
11+
LessJS SSG 构建管线 Phase 3 当前使用 `viteBuild({ssr:true, noExternal: [...]})` 将所有依赖打包进单一日包含 bundle。`noExternal` 列表包含 `parse5``entities``node-fetch` 等 npm 包。
12+
13+
问题:Rolldown(Vite 底层打包器)在做 ESM 解析时,无法正确处理 npm 包的子路径导出(subpath exports)。具体表现为 `parse5` 内部引用 `entities/lib/escape.js` 时,Rolldown 无法通过 npm package.json 的 `exports` 字段正确解析该子路径,导致构建失败。
14+
15+
根因分析:
16+
- **ESM 子路径解析是运行时(Deno/Browser)的职责**,不是打包工具的职责
17+
- Rolldown 的 npm 包解析基于 node_modules 文件系统布局,而非 package.json `exports` 语义
18+
- `noExternal` 策略强行将运行时解析责任转移给了打包工具
19+
- Deno 原生 import map 完整支持 npm 子路径映射,包括 `"entities/"` 尾部斜杠语法
20+
21+
## Decision
22+
23+
**LessJS 将 Import Map(`deno.json` `"imports"` 字段)作为所有依赖的唯一分辨率来源。**
24+
25+
具体决策:
26+
27+
1. **`deno.json` import map 覆盖所有依赖**:包括 LessJS 自身包(`@lessjs/*`)、Lit 生态(`lit``@lit/reactive-element` 等)、SSR 传递依赖(`parse5``entities``hono` 等)。
28+
29+
2. **SSR 传递依赖使用 subpath mapping**:对于有子路径导出的包(如 `entities`),使用 Deno import map 的尾部斜杠语法:
30+
```json
31+
{
32+
"imports": {
33+
"entities": "npm:entities@^4",
34+
"entities/": "npm:entities@^4/"
35+
}
36+
}
37+
```
38+
39+
3. **Import map 是单一事实来源**:无论是 Deno workspace 开发、JSR consumer 构建、还是 SSG SSR bundle,所有模块解析都通过同一个 import map。
40+
41+
4. **Rolldown 仅负责业务代码打包**:LessJS 框架代码 + Lit 生态代码由 Rolldown 打包;npm 传递依赖标记为 `external`,由 Deno ESM 运行时在 `import()` 阶段解析。
42+
43+
## Architecture
44+
45+
```
46+
┌──────────────────────────────────────────────────────────────┐
47+
│ deno.json (Import Map) │
48+
│ │
49+
│ "imports": { │
50+
│ // LessJS own packages │
51+
│ "@lessjs/core": "jsr:@lessjs/core@^0.21", │
52+
│ "@lessjs/ui/": "jsr:@lessjs/ui@^0.21/", │
53+
│ │
54+
│ // Lit ecosystem │
55+
│ "lit": "npm:lit@^3.2.0", │
56+
│ "@lit/reactive-element": "npm:@lit/reactive-element@^2", │
57+
│ │
58+
│ // SSR transitive deps (with subpath mappings) │
59+
│ "parse5": "npm:parse5@7.0.0", │
60+
│ "entities": "npm:entities@^4", │
61+
│ "entities/": "npm:entities@^4/", ← subpath mapping │
62+
│ "hono": "npm:hono@^4.12.18" │
63+
│ }, │
64+
│ "vendor": true │
65+
│ │
66+
└──────────────────────────────────────────────────────────────┘
67+
│ │
68+
│ workspace dev │ consumer build
69+
▼ ▼
70+
┌──────────────────┐ ┌──────────────────────┐
71+
│ Deno LSP + │ │ deno.json import map │
72+
│ deno check │ │ → Vite @deno/plugin │
73+
│ (native resolve)│ │ → Rolldown alias │
74+
└──────────────────┘ └──────────────────────┘
75+
76+
┌───────────────┴───────────────┐
77+
▼ ▼
78+
┌──────────────────┐ ┌──────────────────────┐
79+
│ Phase 3: │ │ Phase 3: │
80+
│ viteBuild(ssr) │ │ Deno import() │
81+
│ │ │ │
82+
│ noExternal: │ │ Resolves external: │
83+
│ /^@lessjs\// │ │ parse5, entities, │
84+
│ /^lit/ │ │ hono via import map │
85+
│ /^@lit/ │ │ │
86+
│ │ │ ✅ Subpath ok │
87+
│ external: │ │ ✅ exports ok │
88+
│ parse5 │ │ │
89+
│ entities │ │ │
90+
│ hono │ │ │
91+
└──────────────────┘ └──────────────────────┘
92+
```
93+
94+
## Resolution Responsibility Boundary
95+
96+
| 组件 | 旧职责 | 新职责 |
97+
|------|--------|--------|
98+
| `deno.json` import map | 仅 LessJS workspace 开发 | **所有环境**的模块分辨率来源 |
99+
| Rolldown/Vite | 解析所有依赖(含 npm 子路径) | 仅打包 LessJS 业务代码 + Lit 生态 |
100+
| Deno ESM Runtime | 不使用(self-contained bundle) | 解析所有 external 依赖(含子路径) |
101+
| `@deno/vite-plugin` | 开发服务器 bare specifier 解析 | 消费者构建时的 Deno import map bridge |
102+
103+
## Non-Goals
104+
105+
- 不引入新的包管理器或 registry
106+
- 不替换 Vite/Rolldown
107+
- 不改变 npm 包的分发方式
108+
- 不在消费者项目中要求 vendor 目录(vendor 对 SSG 阶段是可选优化)
109+
110+
## Consequences
111+
112+
### Positive
113+
114+
- **消除子路径解析 bug**`parse5 → entities/lib/escape.js` 等所有子路径由 Deno 正确解析
115+
- **减少 Rolldown 复杂度**:Rolldown 只需处理 LessJS/Lit 代码,不用理解 npm `exports` 语义
116+
- **单一分辨率来源**:import map 是唯一的 resolution 配置,消除 deno.json 与 bundle 配置之间的不一致
117+
- **与 ADR-0041 对齐**:ESM module graph 是主合同,Deno/JSR 原生解析是唯一路径
118+
- **消费者友好**:消费者只需一个 `deno.json` import map,无需理解 bundler 内部
119+
120+
### Negative
121+
122+
- **Bundle 结构变化**:从单一 self-contained bundle 变为 bundle + external imports
123+
- **部署模型调整**:Deno Deploy 需要 `deno vendor` 或上传 vendor 目录
124+
- **Import map 维护成本**:新增 SSR 依赖时需要同时在 import map 中声明
125+
- **向后兼容**:现有消费者项目的 `noExternal` 配置可能需要调整
126+
127+
## Related
128+
129+
- ADR-0041: ESM Module Graph First for JSR Consumer Builds
130+
- ADR-0043: SSG Phase 3 Dependency Strategy (external + noExternal)
131+
- ADR-0045: Native API First-Class Citizen Strategy
132+
- `deno.json` import map configuration
133+
- Deno import maps spec: https://deno.com/manual@v2.3.5/basics/import_maps
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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

Comments
 (0)