Files
ERP/ofbiz-framework/plugins/modern-ui/app/scripts/verify-modern-navigation.mjs
QiufengandClaude Opus 4.8 5e51dc3f56 SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。

== 此快照内容 ==
- 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091)
- 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static)
- 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB)
- 交接文档 go.md + go-code-reference/endpoints/entities/database.md
- 多代理建设脚本 .claude/wf-*.js

== 状态 ==
- 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板)
- 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用
- W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成)
- 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456

== 排除(gitignore, 可再生) ==
node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui)
完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules)

时间戳: 20260615-191517

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:19:15 +08:00

212 lines
6.5 KiB
JavaScript

#!/usr/bin/env node
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptPath = fileURLToPath(import.meta.url)
const appRoot = path.resolve(path.dirname(scriptPath), '..')
async function source(relativePath) {
return readFile(path.join(appRoot, relativePath), 'utf8')
}
const [
navigationSource,
moduleCatalogSource,
inventorySource,
pageRendererSource,
adapterBlockSource,
searchFormSource,
entityFormSource
] = await Promise.all([
source('src/utils/modernNavigation.ts'),
source('src/data/moduleCatalog.ts'),
source('public/generated/ui-inventory.json'),
source('src/components/erp/ErpPageRenderer.vue'),
source('src/components/erp/ErpAdapterBlock.vue'),
source('src/components/erp/ErpSearchForm.vue'),
source('src/components/erp/ErpEntityForm.vue')
])
const inventory = JSON.parse(inventorySource)
function loadNavigationExports(sourceCode) {
const executableSource = sourceCode
.replace(/export const legacyControlPagePrefixes:\s*Record<string,\s*string>\s*=/, 'const legacyControlPagePrefixes =')
.replace(/export function /g, 'function ')
.replace(/([,(]\s*)([A-Za-z_$][\w$]*):\s*(?:string|unknown)/g, '$1$2')
return Function(`${executableSource}
return {
legacyControlPagePrefixes,
isInternalLegacyControlTarget,
pageHashForTarget,
navigateTarget,
normalizeModernNavigationTarget
}
`)()
}
function moduleBlocks(sourceCode) {
return [...sourceCode.matchAll(/\{\n\s+id: '([^']+)'[\s\S]*?\n\s+workflows:/g)]
.map((match) => {
const block = match[0]
return {
id: match[1],
prefixes: stringArrayValue(block, 'prefixes'),
legacyIncludes: stringArrayValue(block, 'legacyIncludes'),
quickPages: [...block.matchAll(/pageId: '([^']+)'/g)].map((item) => item[1])
}
})
}
function stringArrayValue(sourceCode, propertyName) {
const match = sourceCode.match(new RegExp(`${propertyName}: \\[([\\s\\S]*?)\\]`))
return match?.[1].match(/'([^']+)'/g)?.map((item) => item.slice(1, -1)) || []
}
function moduleOwnsRoute(module, route) {
const haystack = [
route.pageId,
route.title,
route.component,
route.domain,
route.legacyPath,
route.modernPath,
...(route.permissions || [])
].filter(Boolean).join(' ').toLowerCase()
return module.quickPages.includes(route.pageId)
|| module.prefixes.some((prefix) => String(route.pageId || '').startsWith(prefix) || haystack.includes(prefix.toLowerCase()))
|| module.legacyIncludes.some((include) => haystack.includes(include.toLowerCase()))
}
const navigationExports = loadNavigationExports(navigationSource)
const routeManifest = inventory.routeManifest || []
const routePageIds = new Set(routeManifest.map((route) => route.pageId))
const modules = moduleBlocks(moduleCatalogSource)
for (const exportName of [
'legacyControlPagePrefixes',
'isInternalLegacyControlTarget',
'pageHashForTarget',
'navigateTarget',
'normalizeModernNavigationTarget'
]) {
assert.match(
navigationSource,
new RegExp(`export function ${exportName}|export const ${exportName}`),
`modern navigation utility must export ${exportName}`
)
}
for (const [legacyContext, pagePrefix] of [
['ordermgr', 'order'],
['partymgr', 'party'],
['sfa', 'SalesForceAutomation'],
['projectmgr', 'projectmgr'],
['webtools', 'webtools']
]) {
assert.match(
navigationSource,
new RegExp(`${legacyContext}: '${pagePrefix}'`),
`/${legacyContext}/control routes must map to ${pagePrefix} PageDefinition ids`
)
}
const generatedContexts = new Set()
for (const route of routeManifest) {
const context = String(route.legacyPath || '').match(/^\/([^/]+)\/control\//)?.[1]
if (context) generatedContexts.add(context)
}
const missingContextPrefixes = [...generatedContexts]
.filter((context) => !navigationExports.legacyControlPagePrefixes[context])
.sort()
assert.deepEqual(
missingContextPrefixes,
[],
'every generated legacy /control context must map to a PageDefinition prefix'
)
const mismatchedLegacyTargets = routeManifest
.map((route) => ({
legacyPath: route.legacyPath,
pageId: route.pageId,
actual: navigationExports.pageHashForTarget(route.legacyPath, 'ofbiz'),
expected: `#/pages/${route.pageId}`
}))
.filter((item) => item.actual !== item.expected)
assert.deepEqual(
mismatchedLegacyTargets,
[],
'legacy /control targets must normalize to existing #/pages PageDefinition ids'
)
const missingQuickPages = modules.flatMap((module) =>
module.quickPages
.filter((pageId) => !routePageIds.has(pageId))
.map((pageId) => ({ moduleId: module.id, pageId }))
)
assert.deepEqual(
missingQuickPages,
[],
'moduleCatalog quickPages must point at generated PageDefinition ids'
)
const unownedRoutes = routeManifest
.filter((route) => !modules.some((module) => moduleOwnsRoute(module, route)))
.map((route) => ({
pageId: route.pageId,
legacyPath: route.legacyPath,
component: route.component
}))
assert.deepEqual(
unownedRoutes,
[],
'every generated route must be owned by a Modern UI ERP module'
)
assert.doesNotMatch(
navigationSource,
/startsWith\('http'\)[\s\S]*return value/,
'absolute external URLs must not be treated as internal SPA page routes'
)
assert.match(
pageRendererSource,
/import \{[^}]*navigateTarget[^}]*normalizeModernNavigationTarget[^}]*\} from '\.\.\/\.\.\/utils\/modernNavigation'/,
'page renderer must use the shared modern navigation utility'
)
assert.match(
pageRendererSource,
/isSharedInternalLegacyControlTarget|sharedLegacyControlPagePrefixes|pageHashForTarget/,
'page renderer compatibility shims must delegate to the shared modern navigation utility'
)
assert.match(
pageRendererSource,
/normalizeModernNavigationTarget\(target, props\.page\.pageId\.split\('__'\)\[0\]/,
'action navigation targets must be normalized before changing location'
)
for (const [label, componentSource] of [
['adapter block', adapterBlockSource],
['search form', searchFormSource],
['entity form', entityFormSource]
]) {
assert.match(
componentSource,
/import \{[^}]*navigateTarget[^}]*\} from '\.\.\/\.\.\/utils\/modernNavigation'/,
`${label} must use the shared modern navigation utility`
)
assert.match(
componentSource,
/navigateTarget\([^)]*field\.target|navigateTarget\(item\.target/,
`${label} links must normalize generated internal targets`
)
}
console.log('Modern navigation source policy passed.')