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:
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.0",
|
||||
"@types/node": "^22",
|
||||
"eslint": "^9.39.0",
|
||||
"typescript": "^5.6.0",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './limits.js'
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
Generated
+21
-26
@@ -11,6 +11,9 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: ^9.39.0
|
||||
version: 9.39.5
|
||||
'@types/node':
|
||||
specifier: ^22
|
||||
version: 22.20.5
|
||||
eslint:
|
||||
specifier: ^9.39.0
|
||||
version: 9.39.5
|
||||
@@ -22,7 +25,7 @@ importers:
|
||||
version: 8.72.0(eslint@9.39.5)(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.9(@types/node@26.6.5)
|
||||
version: 2.1.9(@types/node@22.20.5)
|
||||
|
||||
apps/api:
|
||||
dependencies:
|
||||
@@ -58,6 +61,12 @@ importers:
|
||||
specifier: ^5.6.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/adapter-wordpress:
|
||||
dependencies:
|
||||
'@sf/contracts':
|
||||
specifier: workspace:*
|
||||
version: link:../contracts
|
||||
|
||||
packages/contracts: {}
|
||||
|
||||
packages:
|
||||
@@ -905,9 +914,6 @@ packages:
|
||||
'@types/node@22.20.5':
|
||||
resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
|
||||
|
||||
'@types/node@26.6.5':
|
||||
resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.72.0':
|
||||
resolution: {integrity: sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1622,9 +1628,6 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@8.9.0:
|
||||
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -2215,11 +2218,6 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@26.6.5':
|
||||
dependencies:
|
||||
undici-types: 8.9.0
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.72.0(@typescript-eslint/parser@8.72.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -2318,13 +2316,13 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@26.6.5))':
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.5))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@26.6.5)
|
||||
vite: 5.4.21(@types/node@22.20.5)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
dependencies:
|
||||
@@ -2938,20 +2936,17 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@8.9.0:
|
||||
optional: true
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
vite-node@2.1.9(@types/node@26.6.5):
|
||||
vite-node@2.1.9(@types/node@22.20.5):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 5.4.21(@types/node@26.6.5)
|
||||
vite: 5.4.21(@types/node@22.20.5)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -2963,19 +2958,19 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.4.21(@types/node@26.6.5):
|
||||
vite@5.4.21(@types/node@22.20.5):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.31
|
||||
rollup: 4.64.12
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 22.20.5
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@2.1.9(@types/node@26.6.5):
|
||||
vitest@2.1.9(@types/node@22.20.5):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@26.6.5))
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.5))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
@@ -2991,11 +2986,11 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.4.21(@types/node@26.6.5)
|
||||
vite-node: 2.1.9(@types/node@26.6.5)
|
||||
vite: 5.4.21(@types/node@22.20.5)
|
||||
vite-node: 2.1.9(@types/node@22.20.5)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
'@types/node': 22.20.5
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
|
||||
+6
-1
@@ -2,7 +2,12 @@
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@sf/contracts": ["packages/contracts/src/index.ts"],
|
||||
"@sf/adapter-wp": ["packages/adapter-wordpress/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["vitest.config.ts", "tests/**/*.ts"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
// 跨包源码直连(Plan 2 Task 1):与根 tsconfig compilerOptions.paths 保持同一映射——
|
||||
// vitest 不读 tsconfig paths,须在此镜像一份;三方消费(包级 tsc / vitest / tsx)
|
||||
// 均直连 src/index.ts,不经 dist、无需预构建。
|
||||
const src = (p: string) => fileURLToPath(new URL(p, import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@sf/contracts': src('packages/contracts/src/index.ts'),
|
||||
'@sf/adapter-wp': src('packages/adapter-wordpress/src/index.ts'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
include: ['tests/**/*.test.ts', 'packages/*/test/**/*.test.ts', 'apps/*/test/**/*.test.ts'],
|
||||
// 集成测试库供给(Task 13):setup 建 sf_test_<ts> 临时库并全量迁移,
|
||||
|
||||
Reference in New Issue
Block a user