fix(theme): payload 契约上提 contracts;空路由明错;ui.routeHrefFor;eslint astro ignore
审查修复(Plan 4 Task 3 review): - 载荷类型单一事实来源:ContentEntry/TaxonomyEntry/TaxonomyTerm/RouteEntry/ AssetEntry/RenderManifestMeta/SiteConfigPayload/BrandConfigPayload 上提 @sf/contracts/render-snapshot(纯新增、不动既有导出——冻结面加法无需升版); 物化端(apps/api build/render-snapshot.ts)与主题 load-render-data 同源引用, 双份手写镜像漂移即编译期破裂;主题侧 re-export 保持既有 import 面 - loader 空路由表前置明错(正常发布站点至少含首页路由——静默渲染会产零页站点) - ui.routeHrefFor:targetContentId → 纯路径 html 落点(redirect/查询行不参与, 无落点 undefined 不虚构——Task 4 分派器入口)+ 测试 - 具夹 assets.json copyPath 去机器相关绝对路径(改仓库相对) - eslint ignores 增 **/.astro/**(astro build 再生目录) 354 根测试 + 18 主题测试全绿;五段 typecheck;lint(.astro 存在态验证后清理)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
// 从 published 修订 + route_registry + media ready 集合物化冻结六文件
|
||||
// (RENDER_SNAPSHOT_FILES 契约清单),**db 只读、零写入**(磁盘仅做副本存在性探测)。
|
||||
// 构建编排(job 状态机/落盘/快照行)在 build.service.ts——本文件只负责"读出什么"。
|
||||
// 载荷类型(ContentEntry/RouteEntry/AssetEntry/TaxonomyEntry/RenderManifestMeta)与
|
||||
// 主题消费端同源于 @sf/contracts/render-snapshot(Plan 4 Task 3 审查上提——双份
|
||||
// 手写镜像漂移即编译期破裂)。
|
||||
//
|
||||
// 发布语义(ADR-010):只读 entry_locales.published_revision_id 指向的修订——current
|
||||
// 分叉(发布后再导入产新修订)不影响本快照;发布后的人工/导入变化须重新 publish +
|
||||
@@ -38,7 +41,15 @@ import {
|
||||
PARSER_VERSION,
|
||||
SANITIZER_VERSION,
|
||||
} from '@sf/adapter-wp'
|
||||
import { RENDER_SNAPSHOT_VERSION, type RenderSnapshotFile } from '@sf/contracts'
|
||||
import {
|
||||
RENDER_SNAPSHOT_VERSION,
|
||||
type AssetEntry,
|
||||
type ContentEntry,
|
||||
type RenderManifestMeta,
|
||||
type RenderSnapshotFile,
|
||||
type RouteEntry,
|
||||
type TaxonomyEntry,
|
||||
} from '@sf/contracts'
|
||||
import { canonicalJson } from '../../common/canonical.js'
|
||||
import {
|
||||
contentEntries,
|
||||
@@ -325,7 +336,7 @@ export async function materializeRenderSnapshot(
|
||||
}
|
||||
}
|
||||
|
||||
const contentPayload = published.map((row) => {
|
||||
const contentPayload: ContentEntry[] = published.map((row) => {
|
||||
const data = (row.data ?? {}) as Record<string, unknown>
|
||||
let entryData = data
|
||||
if (typeof data.bodyHtml === 'string') {
|
||||
@@ -337,14 +348,14 @@ export async function materializeRenderSnapshot(
|
||||
// slug 以修订 data.slug(发布时点冻结值)为准,缺席回退 entry_locales.slug(current 跟随面)
|
||||
const slug = typeof data.slug === 'string' && data.slug !== '' ? data.slug : row.localeSlug
|
||||
const media = usageMap.get(row.revisionId!)
|
||||
const entry: Record<string, unknown> = {
|
||||
const entry: ContentEntry = {
|
||||
contentId: row.contentId,
|
||||
typeKey: row.typeKey,
|
||||
locale: row.locale,
|
||||
slug,
|
||||
data: entryData,
|
||||
revisionId: row.revisionId,
|
||||
checksum: row.checksum,
|
||||
revisionId: row.revisionId!,
|
||||
checksum: row.checksum!,
|
||||
}
|
||||
if (media !== undefined && media.size > 0) {
|
||||
entry.media = Object.fromEntries([...media.entries()].sort(([a], [b]) => cmp(a, b)))
|
||||
@@ -352,10 +363,10 @@ export async function materializeRenderSnapshot(
|
||||
return entry
|
||||
})
|
||||
contentPayload.sort((a, b) =>
|
||||
cmp(a.typeKey as string, b.typeKey as string)
|
||||
|| cmp(a.slug as string, b.slug as string)
|
||||
|| cmp(a.locale as string, b.locale as string)
|
||||
|| cmp(a.contentId as string, b.contentId as string),
|
||||
cmp(a.typeKey, b.typeKey)
|
||||
|| cmp(a.slug, b.slug)
|
||||
|| cmp(a.locale, b.locale)
|
||||
|| cmp(a.contentId, b.contentId),
|
||||
)
|
||||
|
||||
// taxonomies.json:taxonomies × terms 全量 + content_terms 关联(auto: 自建行 source_id 原样)
|
||||
@@ -396,7 +407,7 @@ export async function materializeRenderSnapshot(
|
||||
list.push(t)
|
||||
termsByTaxonomy.set(t.taxonomyId, list)
|
||||
}
|
||||
const taxonomiesPayload = taxonomyRows.map((tax) => {
|
||||
const taxonomiesPayload: TaxonomyEntry[] = taxonomyRows.map((tax) => {
|
||||
const group = (termsByTaxonomy.get(tax.id) ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
sourceId: t.sourceId,
|
||||
@@ -437,7 +448,9 @@ export async function materializeRenderSnapshot(
|
||||
`路由表含 ${unservicable.length} 条不可服务形态(dynamic/unresolved)——不进 routes.json(处置是 SP3+ 查询路由域)`,
|
||||
)
|
||||
}
|
||||
const routesPayload = routeRows.filter((r) => SERVICABLE.has(r.routeKind))
|
||||
// 契约载荷类型同源(Task 3 审查上提):filter 收窄 routeKind 枚举面(html/asset/redirect/gone)
|
||||
// TS 不可静态感知——as 是 Set 过滤的运行时保证,非断言放宽
|
||||
const routesPayload = routeRows.filter((r) => SERVICABLE.has(r.routeKind)) as RouteEntry[]
|
||||
routesPayload.sort(
|
||||
(a, b) => cmp(a.routePath, b.routePath) || cmpNull(a.queryKey, b.queryKey) || cmp(a.host, b.host),
|
||||
)
|
||||
@@ -452,7 +465,7 @@ export async function materializeRenderSnapshot(
|
||||
const bundleRootRaw = bundleRows[0]?.metadata as { storageRoot?: unknown } | undefined
|
||||
const bundleRoot = typeof bundleRootRaw?.storageRoot === 'string' ? bundleRootRaw.storageRoot : null
|
||||
|
||||
const assetsPayload = assetRows.map((a) => {
|
||||
const assetsPayload: AssetEntry[] = assetRows.map((a) => {
|
||||
const candidates: string[] = []
|
||||
if (bundleRoot !== null && bundleRoot !== '') candidates.push(join(bundleRoot, a.storagePath))
|
||||
const repoCopy = join(REPO_STORAGE, a.storagePath)
|
||||
@@ -496,7 +509,7 @@ export async function materializeRenderSnapshot(
|
||||
routes: routesPayload.length,
|
||||
assets: assetsPayload.length,
|
||||
}
|
||||
const manifestPayload = {
|
||||
const manifestPayload: RenderManifestMeta = {
|
||||
renderSnapshotVersion: RENDER_SNAPSHOT_VERSION,
|
||||
versions: {
|
||||
adapter: ADAPTER_VERSION,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
// 依赖与构建产物不参与 lint
|
||||
{ ignores: ['**/node_modules/**', '**/dist/**', 'coverage/**', '.artifacts/**'] },
|
||||
{ ignores: ['**/node_modules/**', '**/dist/**', 'coverage/**', '.artifacts/**', '**/.astro/**'] },
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
|
||||
@@ -15,3 +15,93 @@ type _RenderFilesMatchSpec = [RenderSnapshotFile] extends [FrozenRenderSnapshotF
|
||||
? ([FrozenRenderSnapshotFiles] extends [RenderSnapshotFile] ? true : never)
|
||||
: never
|
||||
const _renderFilesComplete: _RenderFilesMatchSpec = true
|
||||
|
||||
// ---- 六文件载荷类型(Plan 4 Task 3 审查上提:单一事实来源) ----
|
||||
// 物化端(apps/api build/render-snapshot.ts)与消费端(themes/*/load-render-data)
|
||||
// 的载荷形状此前双份手写——此处上提后两端同源引用,杜绝镜像漂移。
|
||||
// 契约冻结注记:本节是**纯新增**(既有导出一个未动、零运行时值),对冻结面是加法,
|
||||
// 无需升版;已上线消费方(物化器/主题)不受影响。
|
||||
|
||||
/** site.json 载荷:config 修订 data 的物化面(publish 首版 v1 = {name,origin,locale,brand:{}})。name 可为空串/占位、brand 可为 {}——消费端须缺省容忍,不得假设非空 */
|
||||
export interface SiteConfigPayload {
|
||||
name: string
|
||||
origin: string
|
||||
locale: string
|
||||
brand: BrandConfigPayload
|
||||
}
|
||||
|
||||
/** 品牌面(V1 唯一键 logoText;恒可能为 {} ——全部键可选) */
|
||||
export interface BrandConfigPayload {
|
||||
logoText?: string
|
||||
}
|
||||
|
||||
/** content.json 条目(published 修订冻结值)。slug 可能含 %xx 百分号编码序列——全链路原样贯穿,禁止 percent-decode */
|
||||
export interface ContentEntry {
|
||||
contentId: string
|
||||
typeKey: string
|
||||
locale: string
|
||||
slug: string
|
||||
data: Record<string, unknown>
|
||||
revisionId: string
|
||||
checksum: string
|
||||
/** fieldPath → public_path(仅 ready 资产引用);无引用时键缺席 */
|
||||
media?: Record<string, string>
|
||||
}
|
||||
|
||||
/** taxonomies.json 条目 */
|
||||
export interface TaxonomyEntry {
|
||||
key: string
|
||||
objectType: string
|
||||
publicBasePath: string
|
||||
terms: TaxonomyTerm[]
|
||||
}
|
||||
|
||||
export interface TaxonomyTerm {
|
||||
id: string
|
||||
sourceId: string
|
||||
slug: string
|
||||
name: string
|
||||
parentTermId: string | null
|
||||
data: Record<string, unknown>
|
||||
contentEntryIds: string[]
|
||||
}
|
||||
|
||||
/** routes.json 条目(物化端已过滤:只剩可服务形态 html/asset/redirect/gone,dynamic/unresolved 不进产物) */
|
||||
export interface RouteEntry {
|
||||
host: string
|
||||
routePath: string
|
||||
routeKind: 'html' | 'asset' | 'redirect' | 'gone'
|
||||
queryPolicy: 'ignore' | 'exact' | 'preserve' | 'dynamic'
|
||||
queryKey: string | null
|
||||
targetStatus: string
|
||||
targetContentId: string | null
|
||||
targetFile: string | null
|
||||
redirectStatus: number | null
|
||||
redirectLocation: string | null
|
||||
indexable: boolean
|
||||
priority: number
|
||||
verificationStatus: string
|
||||
}
|
||||
|
||||
/** assets.json 条目(ready 资产 + 实体副本路径解析结果) */
|
||||
export interface AssetEntry {
|
||||
publicPath: string
|
||||
storagePath: string
|
||||
copyPath: string
|
||||
sha256: string
|
||||
detectedMimeType: string
|
||||
byteSize: number
|
||||
width: number | null
|
||||
height: number | null
|
||||
altText: string | null
|
||||
}
|
||||
|
||||
/** render-manifest.json 载荷(版本块 + 计数 + 三哈希 + URL 重写统计) */
|
||||
export interface RenderManifestMeta {
|
||||
renderSnapshotVersion: number
|
||||
versions: { adapter: string; mapping: string; sanitizer: string; parser: string }
|
||||
counts: { contents: number; taxonomies: number; routes: number; assets: number }
|
||||
sourceHashes: { content: string; route: string; asset: string }
|
||||
urlRewrite: { rewritten: number; unresolved: number }
|
||||
generatedAt: string
|
||||
}
|
||||
|
||||
@@ -22,93 +22,43 @@ import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { RENDER_SNAPSHOT_FILES } from '@sf/contracts'
|
||||
|
||||
// ---- 类型定义(从 Task 2 物化面反推,与 render-snapshot.ts 落盘形状逐字段对齐) ----
|
||||
// ---- 类型定义(契约同源:Plan 4 Task 3 审查上提——载荷形状单一事实来源在
|
||||
// @sf/contracts/render-snapshot,与物化端 apps/api build/render-snapshot.ts 同源引用,
|
||||
// 镜像漂移即编译期破裂。此处 re-export 保持组件/测试的既有 import 面) ----
|
||||
|
||||
/** site.json:config 修订 data(publish 首版 v1 = {name, origin, locale, brand:{}}) */
|
||||
// 六文件载荷类型:直接转发契约(字段语义见契约处 JSDoc)
|
||||
export type {
|
||||
ContentEntry,
|
||||
TaxonomyEntry,
|
||||
TaxonomyTerm,
|
||||
RouteEntry,
|
||||
AssetEntry,
|
||||
RenderManifestMeta,
|
||||
} from '@sf/contracts'
|
||||
export type { BrandConfigPayload as BrandConfig } from '@sf/contracts'
|
||||
|
||||
import type {
|
||||
AssetEntry,
|
||||
BrandConfigPayload,
|
||||
ContentEntry,
|
||||
RenderManifestMeta,
|
||||
RouteEntry,
|
||||
TaxonomyEntry,
|
||||
} from '@sf/contracts'
|
||||
|
||||
/**
|
||||
* 归一后的 site 面(本主题的消费视图,与载荷面 SiteConfigPayload 的差异):
|
||||
* - name 恒非空(载荷允许空串/占位——空值在 loadRenderData 归一为 'Untitled Site')
|
||||
* - brand 恒为对象(载荷端 publish v1 恒写 {},此处对缺席/非对象再兜底)
|
||||
* 消费方禁止直接读 brand.xxx 落空——一律经 ui.brandText 等纯函数。
|
||||
*/
|
||||
export interface SiteConfig {
|
||||
/** 归一后恒非空(空串/缺席 → 'Untitled Site') */
|
||||
name: string
|
||||
origin: string
|
||||
locale: string
|
||||
/** 恒为对象(缺席 → {});键内容按缺省容忍读取 */
|
||||
brand: BrandConfig
|
||||
}
|
||||
|
||||
/** 品牌面(V1 唯一消费键:logoText——缺省回退 site.name) */
|
||||
export interface BrandConfig {
|
||||
logoText?: string
|
||||
}
|
||||
|
||||
/** content.json 条目(published 修订冻结值) */
|
||||
export interface ContentEntry {
|
||||
contentId: string
|
||||
typeKey: string
|
||||
locale: string
|
||||
/** 可能含 %xx 百分号编码序列——原样贯穿,禁止 decode */
|
||||
slug: string
|
||||
data: Record<string, unknown>
|
||||
revisionId: string
|
||||
checksum: string
|
||||
/** fieldPath → public_path(仅 ready 资产引用;Task 2 first-wins 定序冻结) */
|
||||
media?: Record<string, string>
|
||||
}
|
||||
|
||||
/** taxonomies.json 条目 */
|
||||
export interface TaxonomyEntry {
|
||||
key: string
|
||||
objectType: string
|
||||
publicBasePath: string
|
||||
terms: TaxonomyTerm[]
|
||||
}
|
||||
|
||||
export interface TaxonomyTerm {
|
||||
id: string
|
||||
sourceId: string
|
||||
slug: string
|
||||
name: string
|
||||
parentTermId: string | null
|
||||
data: Record<string, unknown>
|
||||
contentEntryIds: string[]
|
||||
}
|
||||
|
||||
/** routes.json 条目(物化端已过滤:只剩可服务形态,dynamic/unresolved 不进产物) */
|
||||
export interface RouteEntry {
|
||||
host: string
|
||||
routePath: string
|
||||
routeKind: 'html' | 'asset' | 'redirect' | 'gone'
|
||||
queryPolicy: 'ignore' | 'exact' | 'preserve' | 'dynamic'
|
||||
queryKey: string | null
|
||||
targetStatus: string
|
||||
targetContentId: string | null
|
||||
targetFile: string | null
|
||||
redirectStatus: number | null
|
||||
redirectLocation: string | null
|
||||
indexable: boolean
|
||||
priority: number
|
||||
verificationStatus: string
|
||||
}
|
||||
|
||||
/** assets.json 条目(ready 资产) */
|
||||
export interface AssetEntry {
|
||||
publicPath: string
|
||||
storagePath: string
|
||||
copyPath: string
|
||||
sha256: string
|
||||
detectedMimeType: string
|
||||
byteSize: number
|
||||
width: number | null
|
||||
height: number | null
|
||||
altText: string | null
|
||||
}
|
||||
|
||||
/** render-manifest.json(版本块 + 计数 + 三哈希 + 重写统计) */
|
||||
export interface RenderManifestMeta {
|
||||
renderSnapshotVersion: number
|
||||
versions: { adapter: string; mapping: string; sanitizer: string; parser: string }
|
||||
counts: { contents: number; taxonomies: number; routes: number; assets: number }
|
||||
sourceHashes: { content: string; route: string; asset: string }
|
||||
urlRewrite: { rewritten: number; unresolved: number }
|
||||
generatedAt: string
|
||||
brand: BrandConfigPayload
|
||||
}
|
||||
|
||||
/** loadRenderData 的返回形状(assets 已建 public_path 索引) */
|
||||
@@ -178,7 +128,7 @@ function normalizeSite(raw: unknown): SiteConfig {
|
||||
brand?: unknown
|
||||
}
|
||||
const name = typeof o.name === 'string' && o.name.trim() !== '' ? o.name : 'Untitled Site'
|
||||
const brand = (typeof o.brand === 'object' && o.brand !== null ? o.brand : {}) as BrandConfig
|
||||
const brand = (typeof o.brand === 'object' && o.brand !== null ? o.brand : {}) as BrandConfigPayload
|
||||
return {
|
||||
name,
|
||||
origin: typeof o.origin === 'string' ? o.origin : '',
|
||||
@@ -215,6 +165,13 @@ export function loadRenderData(inputDir?: string): RenderData {
|
||||
const contents = asArray(parsed.get('content.json'), 'content.json') as ContentEntry[]
|
||||
const taxonomies = asArray(parsed.get('taxonomies.json'), 'taxonomies.json') as TaxonomyEntry[]
|
||||
const routes = asArray(parsed.get('routes.json'), 'routes.json') as RouteEntry[]
|
||||
// 空路由表拒绝:正常发布站点至少含首页路由——空表面意味着上游物化/发布破裂;
|
||||
// 静默渲染会产零页站点,此处前置明错(§7.6 禁静默)
|
||||
if (routes.length === 0) {
|
||||
throw new Error(
|
||||
`routes.json 路由表为空(SF_BUILD_INPUT=${dir})——正常发布站点至少含首页路由,上游构建可能破裂;拒绝静默空载`,
|
||||
)
|
||||
}
|
||||
const assets = buildAssetIndex(asArray(parsed.get('assets.json'), 'assets.json') as AssetEntry[])
|
||||
const renderManifest = parsed.get('render-manifest.json') as RenderManifestMeta
|
||||
|
||||
|
||||
@@ -54,3 +54,19 @@ export function navItems(routes: RouteEntry[]): NavItem[] {
|
||||
{ label: 'News', href: '/news/', visible: hasHtmlRoute(routes, '/news/') },
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容 → 落点路由(Task 4 分派器入口):按 targetContentId 查首条纯路径 html 路由,
|
||||
* 返回其 routePath 作为 href。无落点 → undefined(调用方决定跳过/兜底——不得虚构路径,
|
||||
* Task 6 门禁①按真实路由查死链)。多落点时取 routes.json 顺序首位(物化端按
|
||||
* routePath 字典序冻结,确定性有保证);redirect/asset 行与带 query_key 的行不参与
|
||||
* (内容页的公开落点恒为纯路径 html)。
|
||||
*/
|
||||
export function routeHrefFor(routes: RouteEntry[], contentId: string): string | undefined {
|
||||
for (const r of routes) {
|
||||
if (r.routeKind === 'html' && r.queryKey === null && r.targetContentId === contentId) {
|
||||
return r.routePath
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"altText":null,"byteSize":1234,"copyPath":"D:/project/cms-one/storage/snapshots/media_bundle/2026/10/fixture-a.png","detectedMimeType":"image/png","height":80,"publicPath":"/wp-content/uploads/2026/10/fixture-a.png","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","storagePath":"snapshots/media_bundle/2026/10/fixture-a.png","width":100}]
|
||||
[{"altText":null,"byteSize":1234,"copyPath":"storage/snapshots/media_bundle/2026/10/fixture-a.png","detectedMimeType":"image/png","height":80,"publicPath":"/wp-content/uploads/2026/10/fixture-a.png","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","storagePath":"snapshots/media_bundle/2026/10/fixture-a.png","width":100}]
|
||||
|
||||
@@ -6,6 +6,7 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { loadRenderData, resetRenderDataCache } from '../src/lib/load-render-data.js'
|
||||
import { routeHrefFor } from '../src/lib/ui.js'
|
||||
|
||||
const FIXTURE = fileURLToPath(new URL('./fixtures/render-input', import.meta.url))
|
||||
const LOADER_SRC = fileURLToPath(new URL('../src/lib/load-render-data.ts', import.meta.url))
|
||||
@@ -134,4 +135,33 @@ describe('load-render-data(渲染数据唯一入口)', () => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('routes.json 空表 → 明确错误(正常发布站点至少含首页路由)', () => {
|
||||
const dir = tempCopy('noroutes')
|
||||
try {
|
||||
writeFileSync(join(dir, 'routes.json'), JSON.stringify([]))
|
||||
expect(() => loadRenderData(dir)).toThrow(/路由表为空/)
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('routeHrefFor:targetContentId → 落点路由;无落点 undefined;redirect/查询行不参与', () => {
|
||||
const data = loadRenderData(FIXTURE)
|
||||
expect(routeHrefFor(data.routes, '33333333-3333-4333-8333-333333333333')).toBe('/news/fixture-news-a.html')
|
||||
expect(routeHrefFor(data.routes, '44444444-4444-4444-8444-444444444444')).toBe('/product/fixture-product/')
|
||||
// 无落点(含 redirect 行即便带 targetContentId 也不算内容页公开落点)
|
||||
expect(routeHrefFor(data.routes, '00000000-0000-4000-8000-000000000000')).toBeUndefined()
|
||||
const redirectWithTarget: typeof data.routes[number] = {
|
||||
...data.routes.find((r) => r.routeKind === 'redirect')!,
|
||||
targetContentId: '33333333-3333-4333-8333-333333333333',
|
||||
}
|
||||
expect(routeHrefFor([redirectWithTarget], '33333333-3333-4333-8333-333333333333')).toBeUndefined()
|
||||
// 带 query_key 的 html 行不是内容页纯路径落点
|
||||
const queryRoute: typeof data.routes[number] = {
|
||||
...data.routes.find((r) => r.routePath === '/news/fixture-news-a.html')!,
|
||||
queryKey: 'attachment_id',
|
||||
}
|
||||
expect(routeHrefFor([queryRoute], '33333333-3333-4333-8333-333333333333')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user