核心概念

模板注册表

.ktr/ 三个文件的作用、生成流程和加载策略

ktr 通过自动生成的注册表管理模板、mock 数据和类型增强。本页深入讲解 .ktr/ 目录下三个文件的作用和运行机制。

.ktr/ 目录概览

.ktr/                           ← 框架自动生成,类似 Next.js 的 .next/
├── template-registry.ts        ← 路由 → 组件的映射表
├── mock-registry.ts            ← mock 数据统一导出
└── registry-types.d.ts         ← TypeScript 模块增强
这三个文件由框架自动管理,不要手动编辑、不要提交到 Git、源码不要 import 它们。

.ktr/ 加入 .gitignore

.gitignore
.ktr/

template-registry.ts

作用

存储路由到组件的映射,供运行时加载使用。

生成内容

// .ktr/template-registry.ts(框架自动生成)
import type {  } from '@karinjs/template-react'

// 导入所有模板组件(静态 import,路径不含扩展名)
import  from '../ktr/template/hello/card/index'
import  from '../ktr/template/user/profile/index'
import  from '../ktr/template/stats/dashboard/index'

// 透传模板文件的具名导出
export * from '../ktr/template/hello/card/index'
export * from '../ktr/template/user/profile/index'
export * from '../ktr/template/stats/dashboard/index'

// 导出映射表(宽松标注,逐路由精确类型由 registry-types.d.ts 模块增强提供)
export const : <string, <any>> = {
  'hello/card': ,
  'user/profile': ,
  'stats/dashboard': 
}

// 辅助类型(用于运行时推导)
export type  = typeof 

路由命名规则

  • Key:路由字符串,格式 <板块>/<模板>
  • Value:组件的 default export
  • 变量名template_ + 路由路径(/ 替换为 _
// 示例:路由 user/profile/card
import  from '../ktr/template/user/profile/card/index'

export const  = {
  'user/profile/card': 
  //      ↑ 路由 key              ↑ 变量名
}

加载方式

开发态和生产态使用不同的加载策略:

export async function (: { : string; ?: string }) {
  const { ,  } = 

  // 1. 开发态:.ktr 源文件优先(tsx 即时转译);
  //    渲染器自身跑在下游 bundle 里时跳过这一步,直接用产物(见下方加载策略表)
  const  = .(, '.ktr', 'template-registry.ts')
  if (.()) {
    const  = await import()
    return .templates
  }

  // 2. 生产态:回退到打包产物
  const  = (, 'template-registry.js', )
  if () {
    const  = await import()
    return .templates
  }

  throw new ('未找到注册表:开发态请先运行 ktr sync,生产态请确保已打包')
}

加载策略:

环境加载路径说明
开发态.ktr/template-registry.tstsx 动态导入,支持 HMR
生产态打包产物中的 template-registry.jsbundledDir → package.json main/exports 入口目录 → 根目录扫描发现;渲染器自身在 bundle 里时直接使用,不看 .ktr

mock-registry.ts

作用

统一导出所有 mock 数据(TS mock 和 JSON mock 清单)。

生成内容

// .ktr/mock-registry.ts(框架自动生成)

// ===== Part 1: TS mock 具名导出 =====
export * from '../ktr/template/hello/card/mock'
export * from '../ktr/template/user/profile/mock'

// 如果模板没有 mock.ts,跳过该行

// ===== Part 2: JSON mock 文件清单 =====
export const  = {
  'hello/card': ['captured.json', 'default.json', 'variant.json'],
  'user/profile': ['basic.json']
} as 

// 面板按「路由 + 文件名」定位 data/ 下的 JSON 文件

TS mock vs JSON mock

特性TS mockJSON mock
位置mock.ts(与 index.tsx 同级)data/*.json 子目录
导出方式具名导出(export const basic = {...}文件名字符串清单
类型安全✅ 编译期校验(satisfies)❌ 运行时解析
面板可编辑❌ 只读✅ 可新建/编辑/删除
适合场景固定示例,插件代码可复用快速调试,多组对照数据

面板如何加载数据

开发面板通过 Mock API 读取数据:

export function () {
  return async (: any, : any) => {
    const  = new (.url, 'http://localhost')

    // GET /data?path=hello/card&name=basic
    if (. === '/data') {
      const  = ..('path')!
      const  = ..('name')!

      // 1. 优先读取 JSON 文件(面板可编辑)
      const  = .(., , 'data', `${}.json`)
      if (.()) {
        .json(.(.(, 'utf-8')))
        return
      }

      // 2. 回退到 TS mock(面板只读)
      const  = await ()
      if ([]) {
        .json([])
        return
      }

      .status(404).json({ : 'Data entry not found' })
    }
  }
}

数据源优先级:JSON mock > TS mock

registry-types.d.ts

作用

通过 TypeScript 模块增强(Module Augmentation)注入类型,让 renderImage 获得:

  1. 路由补全:编辑器自动提示可用路由
  2. Data 类型推导:根据模板的 Props 类型推导 data 参数

生成内容

// .ktr/registry-types.d.ts(框架自动生成)

declare module '@karinjs/template-react/registry-types' {
  interface ProjectRegistry {
    'hello/card': typeof import('../ktr/template/hello/card/index').default
    'user/profile': typeof import('../ktr/template/user/profile/index').default
    'stats/dashboard': typeof import('../ktr/template/stats/dashboard/index').default
  }
}

export {}

这是一个环境声明文件(Ambient Declaration),TypeScript 编译器自动加载。

类型增强原理

框架的核心类型定义分两处:

// packages/core/src/registry-types.ts —— 只有增强位本身

// 用户项目通过模块增强填充这个接口
export interface ProjectRegistry {}

// packages/core/src/types/index.ts —— 其余类型定义在这里

// 从模板定义中提取 data 类型
export type <> =  extends <infer > ?  : never

// 宽松降级类型(向后兼容)
export type  = <string, <any>>

// 注册表的实际类型(带降级)
export type  = keyof ProjectRegistry extends never
  ?  // 未增强时降级为宽松类型
  : ProjectRegistry // 增强后为精确类型

类型推导效果

有了类型增强,renderImage 自动获得智能提示:

// @filename: virtual.d.ts
import type {  } from '@karinjs/template-react'

declare module '@karinjs/template-react/registry-types' {
  interface ProjectRegistry {
    'hello/card': <{
      : string
      : <{ : string; : string }>
    }>
  }
}
// @filename: utils/render.ts
import type { ,  } from '@karinjs/template-react'
import type { ImageElement } from 'node-karin'

// 插件胶水层封装的渲染函数(快速上手一节有完整实现)
export declare const : < extends keyof  & string>(
  : ,
  : <[]>
) => <ImageElement[]>
// @filename: index.ts
import {  } from './utils/render'

// ✅ 路由补全:输入 'hello/ 自动提示 'hello/card'
// ✅ data 类型推导:根据模板的 Props 自动推导
await ('hello/card', {
  : 'Karin',
  : [{ : '版本', : '2.0.0' }]
})

// ❌ 类型错误:路由不存在
await ('typo/card', {})
Argument of type '"typo/card"' is not assignable to parameter of type '"hello/card"'.
// ❌ 类型错误:data 类型不匹配 await ('hello/card', { title: 123, // 应为 string
Type 'number' is not assignable to type 'string'.
items: 'wrong' // 应为 array
Type 'string' is not assignable to type '{ label: string; value: string; }[]'.
})

类型安全的最佳实践

在模板中显式定义 Data 接口:

// @filename: ./index.tsx
// ktr/template/hello/card/index.tsx
import {  } from '@karinjs/template-react'

// 显式定义并导出 Data 接口
export interface HelloCardData {
  : string
  ?: string
  : <{
    : string
    : string | number
  }>
}

// 泛型参数传入类型
export default <HelloCardData>({
  : 'Hello 卡片',
  : '简单的信息卡片',
  : ({ ,  }) => {
    // data 有完整类型推导
    return (
      < ="p-6 bg-surface rounded-lg">
        < ="text-xl font-bold">{.}</>
        {. && (
          < ="text-muted">{.}</>
        )}
        < ="mt-4 space-y-2">
          {..( => (
            < ={.} ="flex justify-between">
              < ="text-muted">{.}</>
              < ="font-medium">{.}</>
            </>
          ))}
        </>
      </>
    )
  }
})

mock 文件引用该接口:

// ktr/template/hello/card/mock.ts
import type {  } from './index'

export const  = {
  : 'Karin Template React',
  : [{ : '渲染方式', : 'SSR HTML' }]
} satisfies 

注册表生成流程

完整流程由 ensureConventions() 函数协调:

// conventions/registry.ts

import {  } from 'fast-glob'
import * as  from 'node:fs'
import * as  from 'node:path'

export async function (: any) {
  const { ,  } = 

  // 1. 扫描模板路由
  const  = await ()
  // → ['hello/card', 'user/profile', 'stats/dashboard']

  // 2. 生成 template-registry.ts
  await (, , )

  // 3. 生成 mock-registry.ts
  await (, , )

  // 4. 生成 registry-types.d.ts
  await (, , )
}

async function (: string): <string[]> {
  const  = await ('**/index.tsx', {
    : ,
    : ['**/components/**', '**/_*/**']
  })

  return .(() => .(/\/index\.tsx$/, '')).()
}

async function (: string[], : string, : string) {
  const : string[] = []
  const : string[] = []

  for (const  of ) {
    const  = `template_${.(/\//g, '_')}`
    // 生成路径不含扩展名
    const  = .(, , 'index')

    .(`import ${} from '../${}'`)
    .(`  '${}': ${}`)
  }

  const  = `import type { TemplateDef } from '@karinjs/template-react'
${.('\n')}

export const templates: Record<string, TemplateDef<any>> = {
${.(',\n')}
}

export type TemplateRegistry = typeof templates
`

  .(.(, 'template-registry.ts'), )
}

async function (: string[], : string, : string) {
  const : string[] = []
  const : <string, string[]> = {}

  for (const  of ) {
    const  = .(, )

    // 扫描 TS mock(生成路径不含扩展名)
    const  = .(, 'mock.ts')
    if (.()) {
      .(`export * from '../${.(/\.ts$/, '')}'`)
    }

    // 扫描 JSON mock,按路由聚合文件名字符串清单
    const  = .(, 'data')
    if (.()) {
      const  = .().(() => .('.json'))
      if (. > 0) {
        [] = 
      }
    }
  }

  let  = '// TS mock exports\n'
  if (. > 0) {
     += .('\n') + '\n\n'
  }

   += '// JSON mock 文件清单\n'
   += 'export const mockDataFiles = {\n'
  for (const [, ] of .()) {
     += `  '${}': [${.(() => `'${}'`).(', ')}],\n`
  }
   += '} as const\n'

  .(.(, 'mock-registry.ts'), )
}

async function (: string[], : string, : string) {
  const : string[] = []

  for (const  of ) {
    const  = .(, , 'index')
    .(`    '${}': typeof import('../${}').default`)
  }

  const  = `declare module '@karinjs/template-react/registry-types' {
  interface ProjectRegistry {
${.('\n')}
  }
}

export {}
`

  .(.(, 'registry-types.d.ts'), )
}

开发态 vs 生产态加载

开发态加载

开发服务器和沙盒直接加载 .ktr/*.ts 源码:

export async function () {
  // 约定扫描模板路由,逐路由生成 /@fs/ 动态导入(vite 按需编译,HMR 即时生效)
  const  = await (.)
  const  = .(() => `['${.}', () => import('/@fs/${.(., .)}')]`)

  // 生成虚拟模块代码
  return `
    import { createRoot } from 'react-dom/client'
    
    const templateLoaders = [
      ${.(',\n      ')}
    ]
    // 沙盒逻辑(见开发面板架构一章)
  `
}

特点:

  • 即时更新:修改模板代码,HMR 立即生效
  • 无需打包:tsx 动态导入,开发体验流畅

生产态加载

Karin 插件从打包产物加载:

// 用户插件代码
import {  } from '@karinjs/template-react'

// 定位包根(从 import.meta.url 向上找 package.json)
const  = (import.meta.)

// 首次调用时内部加载注册表,查找顺序:
// 1. .ktr/template-registry.ts(开发态源文件,存在时永远优先)
// 2. bundledDir 显式指定的产物目录
// 3. package.json main 字段所在目录
// 4. package.json exports 主入口所在目录(纯 exports 包没有 main)
// 5. 根目录下一层目录扫描

特点:

  • 独立部署:注册表随包发布,无需源码
  • 性能优化:打包后的 JS,加载更快

注册表失效排查

问题 1:路由未注册

现象:新建模板后,renderImage 提示路由不存在

排查步骤

  1. 检查文件路径格式
✅ ktr/template/hello/card/index.tsx
❌ ktr/template/hello/card.tsx           (裸文件不注册)
❌ ktr/template/hello/card/main.tsx      (必须是 index)
❌ ktr/template/_draft/card/index.tsx    (下划线目录不扫描)
  1. 检查是否执行过 ktr sync
# 手动刷新注册表
pnpm ktr sync

# 或重启开发服务器
pnpm template
  1. 查看生成的注册表
# 检查路由是否在列表中
cat .ktr/template-registry.ts | grep 'hello/card'

问题 2:类型推导失效

现象renderImage 没有路由补全或 data 类型为 any

排查步骤

  1. 确认 registry-types.d.ts 存在
ls -la .ktr/registry-types.d.ts
  1. 检查 TypeScript 配置
tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler", // 或 "node16"
    "types": ["node"]
  },
  "include": [
    "src/**/*",
    ".ktr/**/*" // 确保包含 .ktr 目录
  ]
}
  1. 重启 TypeScript 服务器

VSCode: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"

问题 3:Mock 数据不显示

现象:开发面板数据卡片为空

排查步骤

  1. 检查 mock 文件位置
✅ ktr/template/hello/card/mock.ts          (TS mock)
✅ ktr/template/hello/card/data/default.json (JSON mock)
❌ ktr/template/hello/mock.ts                (错误位置)
  1. 检查 TS mock 导出方式
// ✅ 具名导出
export const  = { : 'Hello' }

// ❌ 默认导出(不会被注册)
export default { : 'Hello' }
  1. 查看生成的 mock 注册表
cat .ktr/mock-registry.ts

问题 4:生产环境加载失败

现象:部署后 renderImage 提示"未找到注册表"

排查步骤

  1. 确认已构建 CSS
pnpm build  # 或 vite build
ls lib/style.css  # 产物目录随你的打包器 outDir,这里以 lib/ 为例
  1. 确认注册表在产物目录
# 检查打包产物(目录名同上)
ls lib/template-registry.js
  1. 产物目录不在自动发现路径时,检查 bundledDir 配置
export const  = (import.meta., {
  : 'dist' // 打包产物目录;不传时按 main/exports 入口和根目录扫描自动发现
})

下一步

On this page