feat(contracts): Route/Render/Theme Manifest 契约 v1 冻结
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1,4 @@
|
||||
export * from './universal-row.js'
|
||||
export * from './route-manifest.js'
|
||||
export * from './render-snapshot.js'
|
||||
export * from './theme-manifest.js'
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const RENDER_SNAPSHOT_VERSION = 1
|
||||
|
||||
export const RENDER_SNAPSHOT_FILES = [
|
||||
'site.json', 'content.json', 'taxonomies.json', 'routes.json', 'assets.json', 'render-manifest.json',
|
||||
] as const
|
||||
|
||||
export type RenderSnapshotFile = (typeof RENDER_SNAPSHOT_FILES)[number]
|
||||
|
||||
// 契约冻结锁(spec §9.2):清单与冻结集双向咬合——漏改/多改/改名任何一项 → TS2322
|
||||
type FrozenRenderSnapshotFiles =
|
||||
| 'site.json' | 'content.json' | 'taxonomies.json'
|
||||
| 'routes.json' | 'assets.json' | 'render-manifest.json'
|
||||
|
||||
type _RenderFilesMatchSpec = [RenderSnapshotFile] extends [FrozenRenderSnapshotFiles]
|
||||
? ([FrozenRenderSnapshotFiles] extends [RenderSnapshotFile] ? true : never)
|
||||
: never
|
||||
const _renderFilesComplete: _RenderFilesMatchSpec = true
|
||||
@@ -0,0 +1,45 @@
|
||||
export const ROUTE_MANIFEST_VERSION = 1
|
||||
|
||||
export type RouteKind = 'html' | 'asset' | 'redirect' | 'gone' | 'dynamic' | 'unresolved'
|
||||
export type QueryPolicy = 'ignore' | 'exact' | 'preserve' | 'dynamic'
|
||||
|
||||
export interface RouteManifestRoute {
|
||||
exact_key: string
|
||||
query_policy: QueryPolicy
|
||||
query_key?: string | null
|
||||
kind: RouteKind
|
||||
target_file?: string
|
||||
status: number
|
||||
location?: string
|
||||
content_type?: string
|
||||
indexable: boolean
|
||||
}
|
||||
|
||||
export interface RouteManifest {
|
||||
manifest_version: number
|
||||
site_id: string
|
||||
build_job_id: string
|
||||
origin: string
|
||||
generated_from_route_snapshot_hash: string
|
||||
routes: RouteManifestRoute[]
|
||||
}
|
||||
|
||||
const KINDS = ['html', 'asset', 'redirect', 'gone', 'dynamic', 'unresolved'] as const
|
||||
const QUERY_POLICIES = ['ignore', 'exact', 'preserve', 'dynamic'] as const
|
||||
|
||||
// 守卫查值类型(不只查键存在):版本号、顶层必填字符串、routes 元素的枚举与标量类型全部校验
|
||||
export function isRouteManifest(v: unknown): v is RouteManifest {
|
||||
if (typeof v !== 'object' || v === null) return false
|
||||
const m = v as RouteManifest
|
||||
return m.manifest_version === ROUTE_MANIFEST_VERSION
|
||||
&& typeof m.site_id === 'string'
|
||||
&& typeof m.build_job_id === 'string'
|
||||
&& typeof m.origin === 'string'
|
||||
&& typeof m.generated_from_route_snapshot_hash === 'string'
|
||||
&& Array.isArray(m.routes) && m.routes.every((r) =>
|
||||
typeof r.exact_key === 'string'
|
||||
&& (QUERY_POLICIES as readonly string[]).includes(r.query_policy)
|
||||
&& (KINDS as readonly string[]).includes(r.kind)
|
||||
&& typeof r.status === 'number'
|
||||
&& typeof r.indexable === 'boolean')
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const THEME_MANIFEST_VERSION = 1
|
||||
|
||||
export interface ThemeManifest {
|
||||
key: string
|
||||
version: string
|
||||
renderContractVersion: string
|
||||
supportedContentTypes: string[]
|
||||
requiredSiteConfig: string[]
|
||||
templates: Array<{ key: string; appliesTo: string }>
|
||||
}
|
||||
|
||||
const SEMVER = /^\d+\.\d+\.\d+$/
|
||||
|
||||
// 守卫查值类型(不只查键存在):semver 格式、字符串字段、数组元素类型全部校验
|
||||
export function isThemeManifest(v: unknown): v is ThemeManifest {
|
||||
if (typeof v !== 'object' || v === null) return false
|
||||
const m = v as ThemeManifest
|
||||
return typeof m.key === 'string'
|
||||
&& typeof m.version === 'string' && SEMVER.test(m.version)
|
||||
&& typeof m.renderContractVersion === 'string'
|
||||
&& Array.isArray(m.supportedContentTypes)
|
||||
&& m.supportedContentTypes.every((t) => typeof t === 'string')
|
||||
&& Array.isArray(m.requiredSiteConfig)
|
||||
&& m.requiredSiteConfig.every((t) => typeof t === 'string')
|
||||
&& Array.isArray(m.templates)
|
||||
&& m.templates.every((t) => typeof t === 'object' && t !== null
|
||||
&& typeof (t as { key: unknown }).key === 'string'
|
||||
&& typeof (t as { appliesTo: unknown }).appliesTo === 'string')
|
||||
}
|
||||
Reference in New Issue
Block a user