feat(adapter-wp): 包骨架与限额契约

Plan 2 Task 1:@sf/adapter-wp 包骨架(照 contracts 先例:build extends +
rootDir "." + exclude test)+ WxrLimits 十字段限额契约与 DEFAULT_WXR_LIMITS
值域冻结(设计基线 §7.2)+ 跨包接线。

跨包接线(一份 paths 三方消费):
- 根 tsconfig compilerOptions.paths 把 @sf/contracts、@sf/adapter-wp 映射到
  各自 src/index.ts;adapter tsconfig extends 根 tsconfig 使包级 tsc 读同一
  份 paths(extends 相对路径按声明处解析,映射始终落在仓库根)
- 根 vitest.config.ts resolve.alias 指向同一映射(vitest 不读 tsconfig paths)
- adapter 加 @sf/contracts workspace:* 依赖;三方均直连 src,不经 dist
  (已实测:移走 contracts dist 后 vitest 与包级 tsc 仍全绿)
- 根 devDependencies 加 @types/node@^22(对齐 apps/api 钉 22):alias 的
  fileURLToPath 辅助需 node 类型,此前根程序靠 apps/api 传递 @types 才有
  node 全局,属偶合

限额类型锁(照 contracts 单源派生先例):DEFAULT_WXR_LIMITS as const
satisfies WxrLimits + _LIMIT_KEYS 键集与 keyof WxrLimits 双向咬合断言;
timeBudgetMs 以可注入时钟(构造函数 now())测试于 Task 3、实测覆盖于 Task 8。

验证:pnpm test 78 绿(75+3);pnpm --filter @sf/adapter-wp exec tsc
-p tsconfig.json --noEmit exit 0(--listFiles 证实 contracts 经 src 解析);
根 typecheck 链与 pnpm lint 绿;build 产物形状核验后清理。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 08:05:08 +08:00
parent 2d33dd17dd
commit dc4557cccf
10 changed files with 180 additions and 27 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@sf/adapter-wp",
"version": "0.1.0",
"description": "SiteForge WordPress WXR 适配器:流式解析 + UniversalRow 映射(纯函数包)",
"type": "module",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@sf/contracts": "workspace:*"
}
}
+1
View File
@@ -0,0 +1 @@
export * from './limits.js'
+62
View File
@@ -0,0 +1,62 @@
// WXR 解析器资源限额(设计基线 §7.2 流式解析、ADR-005):
// 全部为防御恶意/畸形 WXR 的上限,超限须抛明确的限额错误,而非崩溃或静默截断。
export interface WxrLimits {
/** 单个 WXR 文件最大字节数(64MB) */
maxWxrBytes: number
/** 最大 item 数 */
maxItems: number
/** XML 最大嵌套深度 */
maxDepth: number
/** 单个 CDATA 块最大长度(1MB) */
maxCdataLength: number
/** 单字段最大长度(1MB) */
maxFieldLength: number
/** term 总数上限 */
maxTermCount: number
/** 单 item 的 postmeta 数上限 */
maxMetaPerItem: number
/** 总处理时限(毫秒);将以可注入时钟(解析器构造函数接受 now())测试于 Task 3、实测覆盖于 Task 8 */
timeBudgetMs: number
/** PHP Serialized 数据最大嵌套深度 */
phpMaxDepth: number
/** PHP Serialized 数据最大元素数 */
phpMaxElements: number
}
export const DEFAULT_WXR_LIMITS = {
maxWxrBytes: 64 * 1024 * 1024,
maxItems: 200_000,
maxDepth: 64,
maxCdataLength: 1024 * 1024,
maxFieldLength: 1024 * 1024,
maxTermCount: 50_000,
maxMetaPerItem: 200,
timeBudgetMs: 120_000,
phpMaxDepth: 8,
phpMaxElements: 1000,
} as const satisfies WxrLimits
// 单源派生冻结锁(照 contracts 的 KINDS/REQUIRED 先例):接口字段集与常量键集互锁,
// 任一侧增删字段而另一侧漏改 → 编译报错,防止两份清单静默漂移。
// _ 前缀:仅作编译期锁、无运行时消费者(同 contracts 的 _requiredKeysComplete 先例)。
const _LIMIT_KEYS = [
'maxWxrBytes',
'maxItems',
'maxDepth',
'maxCdataLength',
'maxFieldLength',
'maxTermCount',
'maxMetaPerItem',
'timeBudgetMs',
'phpMaxDepth',
'phpMaxElements',
] as const
type WxrLimitKeys = keyof WxrLimits
type _KeysMatch = [WxrLimitKeys] extends [typeof _LIMIT_KEYS[number]]
? [typeof _LIMIT_KEYS[number]] extends [WxrLimitKeys]
? true
: never
: never
const _keysMatch: _KeysMatch = true
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { UNIVERSAL_ROW_VERSION } from '@sf/contracts'
import { DEFAULT_WXR_LIMITS, type WxrLimits } from '../src/limits.js'
describe('DEFAULT_WXR_LIMITS 值域冻结(设计基线 §7.2 流式解析)', () => {
it('十字段默认值与冻结值域一致', () => {
expect(DEFAULT_WXR_LIMITS).toStrictEqual({
maxWxrBytes: 64 * 1024 * 1024,
maxItems: 200_000,
maxDepth: 64,
maxCdataLength: 1024 * 1024,
maxFieldLength: 1024 * 1024,
maxTermCount: 50_000,
maxMetaPerItem: 200,
timeBudgetMs: 120_000,
phpMaxDepth: 8,
phpMaxElements: 1000,
})
})
it('WxrLimits 类型字段齐全:显式构造完整对象通过类型(编译期保证)', () => {
const limits: WxrLimits = {
maxWxrBytes: 1,
maxItems: 1,
maxDepth: 1,
maxCdataLength: 1,
maxFieldLength: 1,
maxTermCount: 1,
maxMetaPerItem: 1,
timeBudgetMs: 1,
phpMaxDepth: 1,
phpMaxElements: 1,
}
expect(Object.keys(limits).sort()).toStrictEqual(Object.keys(DEFAULT_WXR_LIMITS).sort())
})
it('跨包接线:@sf/contracts 经根 paths/alias 源码直连可用', () => {
expect(UNIVERSAL_ROW_VERSION).toBe(1)
})
})
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"noEmit": false
},
"exclude": ["test"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "test"]
}