diff --git a/package.json b/package.json index 1be9c01..6c0995a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/adapter-wordpress/package.json b/packages/adapter-wordpress/package.json new file mode 100644 index 0000000..7ae9121 --- /dev/null +++ b/packages/adapter-wordpress/package.json @@ -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:*" + } +} diff --git a/packages/adapter-wordpress/src/index.ts b/packages/adapter-wordpress/src/index.ts new file mode 100644 index 0000000..82bd09c --- /dev/null +++ b/packages/adapter-wordpress/src/index.ts @@ -0,0 +1 @@ +export * from './limits.js' diff --git a/packages/adapter-wordpress/src/limits.ts b/packages/adapter-wordpress/src/limits.ts new file mode 100644 index 0000000..6753ea3 --- /dev/null +++ b/packages/adapter-wordpress/src/limits.ts @@ -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 diff --git a/packages/adapter-wordpress/test/limits.test.ts b/packages/adapter-wordpress/test/limits.test.ts new file mode 100644 index 0000000..e4fc988 --- /dev/null +++ b/packages/adapter-wordpress/test/limits.test.ts @@ -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) + }) +}) diff --git a/packages/adapter-wordpress/tsconfig.build.json b/packages/adapter-wordpress/tsconfig.build.json new file mode 100644 index 0000000..48c77c2 --- /dev/null +++ b/packages/adapter-wordpress/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": false + }, + "exclude": ["test"] +} diff --git a/packages/adapter-wordpress/tsconfig.json b/packages/adapter-wordpress/tsconfig.json new file mode 100644 index 0000000..1bcc876 --- /dev/null +++ b/packages/adapter-wordpress/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "test"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aed8456..ac70d3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/tsconfig.json b/tsconfig.json index a49212b..b79d775 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] } diff --git a/vitest.config.ts b/vitest.config.ts index b5fd956..2fb447e 100644 --- a/vitest.config.ts +++ b/vitest.config.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_ 临时库并全量迁移,