npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

funhub-material-starter

v0.0.5

Published

A starter for creating a funhub material package.

Readme

funhub-material-starter

一个用于开发、预览和发布 Funhub 物料包的 SDK 模板。

当前模板已按最新物料文档调整,核心链路是:

  1. defineComponentPropsSchema((t) => ({ ... })) 定义可编辑字段
  2. getSchemaDefaultProps(...) 取 schema 顶层默认值
  3. defineMaterial(...) 绑定 serverComponentclientComponentpropsSchema

快速开始

环境要求

  • Node.js >= 22.22.0
  • 推荐使用 nvm 管理 Node 版本,项目内已提供 .nvmrc.node-version

Node 安装与版本管理

方案一:使用 nvm(推荐)

nvm 官方 GitHub:https://github.com/nvm-sh/nvm

安装完成后,在项目目录执行:

nvm install
nvm use
node -v

如果你想手动指定版本,也可以执行:

nvm install 22.22.0
nvm use 22.22.0

方案二:直接安装 Node.js

Node.js 官方 GitHub:https://github.com/nodejs/node

如果不使用版本管理器,请直接安装 22.22.0 或更高版本,并确认:

node -v
pnpm install
pnpm run dev

常用命令:

pnpm run dev
pnpm run build
pnpm run typecheck
pnpm run dev:preview
pnpm run build:preview
pnpm run start:preview
pnpm run publish:npm
pnpm run create:material
npx <packageName> setup app
npx <packageName> setup admin
  • pnpm run dev:监听构建 SDK 产物
  • pnpm run build:构建 SDK 到 dist/
  • pnpm run typecheck:校验类型
  • pnpm run dev:preview:启动内置 playground,默认 http://localhost:3031
  • pnpm run create:material:交互式创建一个新的 material 脚手架并自动导出
  • npx <packageName> setup app:在 APP 项目里自动接入 SDK 物料和 i18n Provider
  • npx <packageName> setup admin:在 ADMIN 项目里自动注册 SDK 物料,并尝试接入 Provider / i18n

项目结构

sdk/
├─ src/
│  ├─ index.ts
│  ├─ preview.ts
│  ├─ global.css
│  ├─ components/sdk-funhub-provider.tsx
│  ├─ i18n/
│  └─ sdk/materials/
│     ├─ example/
│     │  ├─ material.ts
│     │  ├─ schema.ts
│     │  ├─ server.tsx
│     │  └─ client.tsx
│     └─ input/
│        ├─ material.ts
│        ├─ schema.ts
│        ├─ server.tsx
│        └─ client.tsx
├─ app/
├─ scripts/
├─ tsdown.config.mts
└─ package.json

最小物料写法

schema.ts

import type z from 'zod';
import { defineComponentPropsSchema, getSchemaDefaultProps } from '@funhub/platform/utils';

export const bannerPropsSchema = defineComponentPropsSchema((t) => ({
  title: t.string('默认标题'),
  variant: t.enum(['blue', 'red'] as const, 'blue'),
  autoplay: t.boolean(true),
}));

export const bannerDefaultProps = getSchemaDefaultProps(bannerPropsSchema);
export type BannerProps = z.infer<typeof bannerPropsSchema>;

material.ts

import { defineMaterial } from '@funhub/platform/utils';
import { BannerClient } from './client';
import { BannerServer } from './server';
import { bannerPropsSchema } from './schema';

export const bannerMaterial = defineMaterial({
  type: 'basic-banner',
  name: '基础轮播图',
  icon: '/icon/banner.png',
  category: '行为组件',
  serverComponent: BannerServer,
  clientComponent: BannerClient,
  propsSchema: bannerPropsSchema,
});

server.tsx

import type { BannerProps } from './schema';
import { HydrationBoundary } from '@funhub/platform/hooks';
import { BannerClient } from './client';

async function getBannerDetail() {
  return Promise.resolve({
    description: '这是通过 HydrationBoundary 预取的文案',
  });
}

export async function BannerServer(props: BannerProps) {
  const prefetch: Parameters<typeof HydrationBoundary>[0]['prefetch'] = [
    {
      queryKey: ['banner-detail'],
      queryFn: getBannerDetail,
    },
  ];

  return (
    <HydrationBoundary prefetch={prefetch}>
      <BannerClient {...props} />
    </HydrationBoundary>
  );
}

client.tsx

'use client';

import type { BannerProps } from './schema';
import { useRequest } from '@funhub/platform/hooks';

export function BannerClient(props: BannerProps) {
  const { title, variant, autoplay, mode = 'renderer' } = props;
  const { data } = useRequest(['banner-detail'], () => {
    return Promise.resolve({
      description: '这是通过 HydrationBoundary 预取的文案',
    });
  }, {
    enabled: mode === 'renderer',
  });

  return (
    <section data-variant={variant}>
      <h2>{title}</h2>
      <p>{data?.description}</p>
      <button type="button">
        {autoplay ? '暂停轮播' : '开始轮播'}
      </button>
    </section>
  );
}

三个核心概念

propsSchema

propsSchema 只描述 inspector 可编辑字段,不等于组件完整 props。

  • schema 里有的字段,组件必须支持
  • schema 类型要和组件 props 类型兼容
  • 组件可以有更多 props,但不一定都开放给编辑器

defineComponentPropsSchema 还会自动补上:

mode?: 'renderer' | 'editor'

所以组件 props 通常直接写成:

export type BannerProps = z.infer<typeof bannerPropsSchema>;

serverComponent

常见职责:

  • 做 prefetch
  • 做服务端数据收口
  • 返回 clientComponent

在 C 端应用渲染时,通常优先走 material.serverComponent

从 React 19 / Next.js App Router 的语义看,Server Component 默认运行在服务端环境,适合:

  • 靠近数据源取数,比如数据库、内部 API、BFF
  • 使用 token、密钥、服务端环境变量等不能暴露到浏览器的信息
  • 减少发送到浏览器的 JavaScript 体积
  • 配合流式渲染更快输出首屏内容

在物料里,serverComponent 通常负责“取数和收口”,然后把可序列化的 props 传给 clientComponent

注意点:

  • Server Component 不是拿来做浏览器交互的,不适合放 onClickuseStateuseEffect
  • 传给 Client Component 的 props 需要是 React 可序列化数据
  • 如果某个模块依赖服务端密钥或服务端专属逻辑,应放在 server 侧而不是 client 侧

clientComponent

常见职责:

  • 管理端编辑器内预览
  • 交互渲染
  • 消费 HydrationBoundary 预取结果

在编辑器场景里,通常走 material.clientComponent

从 React 19 / Next.js 的语义看,Client Component 通过文件顶部的 'use client' 声明为客户端组件,适合:

  • 处理交互事件,比如 onClickonChange
  • 使用 useStateuseEffect 等客户端 hooks
  • 访问浏览器 API,比如 windowlocalStoragenavigator
  • 承担编辑器预览、动画、表单输入、弹层切换等交互逻辑

在物料里,clientComponent 更像“真正负责交互展示的那层 UI”。

注意点:

  • 一旦文件声明了 'use client',它的 imports 和子组件都会进入 client bundle,所以边界要尽量收小
  • 不要在 Client Component 中直接依赖服务端密钥、私有 token 或 server-only 逻辑
  • 编辑器态通常建议关闭真实请求、副作用和真实跳转,只保留稳定可预览行为

为什么物料要拆成 server/client 两层

这是因为 Funhub 的物料运行场景天然同时覆盖服务端渲染和客户端交互:

  • C 端应用更适合优先走 serverComponent,先完成取数、首屏输出和预取
  • 管理端编辑器更适合直接走 clientComponent,保证可编辑、可交互、可即时预览

推荐心智模型:

  • serverComponent = 负责数据与服务端边界
  • clientComponent = 负责交互与编辑器预览

一个常见模式就是当前模板里的 example

  • server.tsx 中用 HydrationBoundary prefetch
  • client.tsx 中用 useRequest(...) 消费结果
  • mode === 'editor' 时关闭真实请求或副作用

这样做的好处是:

  • 运行时首屏更稳
  • 编辑器预览更可控
  • server/client 职责更清晰

使用 server/client component 时的几个注意点

  • 最小化 client 边界:不是整个物料都必须是 client component,只有需要交互的部分才加 'use client'
  • props 必须可序列化:server 传给 client 的数据不要放函数、类实例、不可序列化对象
  • 避免环境串用:不要在 client component 里导入依赖服务端环境变量或私密逻辑的模块
  • context 只能在 client 中创建:如果某些能力依赖 React context provider,应由 client component 承担 provider 边界
  • 第三方库要注意运行环境:依赖浏览器能力的库,应包在 client component 里使用

builder helper 怎么用

标量字段

title: t.string('默认标题', { label: '标题' })
count: t.number(3, { label: '数量' })
visible: t.boolean(true, { label: '是否展示', fieldType: 'switch' })
period: t.timeRange({ label: '展示周期' })

枚举字段

variant: t.enum(['blue', 'red'] as const, 'blue', {
  label: '样式',
})

列表和对象

items: t.array(
  t.object({
    title: t.string({ label: '标题' }),
    image: t.string({ label: '图片' }),
  }),
  {
    label: '列表项',
    defaultValue: [{ title: '第一项', image: '/a.png' }],
  },
)

click: t.object({
  enabled: t.boolean(true, {
    label: '支持点击',
    fieldType: 'switch',
  }),
  link: t.string({ label: '跳转链接' }),
}, {
  label: '点击配置',
  defaultValue: {
    enabled: true,
    link: '',
  },
})

默认值规则

defaultValue 不是 zod 的 .default()

  • safeParse({}) 不会自动补默认值
  • 要通过 getSchemaDefaultProps(propsSchema) 显式取默认值
  • getSchemaDefaultProps(...) 当前只提取顶层字段默认值

推荐运行时写法:

const defaultProps = getSchemaDefaultProps(material.propsSchema);
const mergedProps = {
  ...defaultProps,
  ...remoteProps,
};

const parsed = material.propsSchema.safeParse(mergedProps);

新增物料流程

  1. src/sdk/materials/<your-material>/ 新建目录
  2. schema.ts 里用 defineComponentPropsSchema((t) => ({ ... })) 定义 schema
  3. server.tsx / client.tsx 里实现渲染逻辑
  4. material.ts 里调用 defineMaterial(...)
  5. src/sdk/materials/index.ts 里补充导出
  6. 执行 pnpm run dev:preview 检查效果

如果想直接生成这套脚手架,可以执行:

pnpm run create:material

CLI 会依次询问:

  • 物料文件名:用于目录名 src/sdk/materials/<fileName>
  • 组件 type:APP / ADMIN 运行时识别这个物料的唯一标识
  • 组件 name:后台和预览界面展示的物料名称
  • 组件 category:物料分组名称

执行完成后会自动创建 material.tsschema.tsserver.tsxclient.tsx,并把导出追加到 src/sdk/materials/index.ts

Playground 怎么工作

内置 playground 会:

  • 自动扫描 src/sdk/**/material.ts
  • getSchemaDefaultProps(...) 生成默认 props
  • 用当前物料的 serverComponent 进行预览渲染
  • 支持 ?material=<type>&locale=<locale>&mode=<renderer|editor> 切换

因此只要物料目录和导出规范正确,就会自动出现在预览列表里。

对外导出

当前模板默认使用子路径导出:

import { SDKFunhubProvider } from '<packageName>/components';
import { getSDKMessages, mergeSDKMessages } from '<packageName>/i18n';
import { exampleButtonMaterial, exampleInputMaterial } from '<packageName>/materials';

新增物料后,记得同步补到 src/sdk/materials/index.ts

i18n

模板内置 next-intl,推荐业务侧直接复用 SDKFunhubProvider

import { SDKFunhubProvider } from '<packageName>/components';

export function AppProviders({ children }: { children: React.ReactNode }) {
  return <SDKFunhubProvider locale="zh-CN">{children}</SDKFunhubProvider>;
}

自动接入脚本

发布后的 SDK 包默认带有 CLI,可在业务项目根目录直接执行:

npx <packageName> setup app
npx <packageName> setup admin

也支持额外参数:

npx <packageName> setup app --dry-run
npx <packageName> setup admin --cwd ../your-admin-project

当前脚本默认会做这些事情:

  • setup app
    • '<packageName>/materials' 合并进 src/components/index.tsmaterialMap
    • SDKFunhubProvidermergeSDKMessages 接入到 src/app/**/layout.tsx
  • setup admin
    • '<packageName>/materials' 合并进 src/materials/index.ts
    • 尝试把 SDKFunhubProvidermergeSDKMessages 接入到 src/app/**/layout.tsx

注意:

  • admin 侧的 editor 映射和 materialMapByCategory 往往和具体后台项目结构强相关,当前脚本不会强行生成这部分 UI。
  • 如果目标项目文件结构和模板约定差异较大,脚本会跳过无法识别的文件,并在终端输出说明。

构建与发布

本地构建:

pnpm run build

发布到 npm:

pnpm run publish:npm

参考文档

  • 文档总览:https://docs-dev.guadd.fun/docs
  • Guide 索引:https://docs-dev.guadd.fun/docs/guide
  • 物料指南:https://docs-dev.guadd.fun/docs/guide/material
  • Server and Client Components 章节:https://nextjs.org/docs/app/getting-started/server-and-client-components
  • use client 指令:https://nextjs.org/docs/app/api-reference/directives/use-client
  • SDK 初始化:https://docs-dev.guadd.fun/docs/guide/sdk-init
  • 应用自定义 Material 接入:https://docs-dev.guadd.fun/docs/guide/custom-material-integration
  • 管理端自定义 Material 接入:https://docs-dev.guadd.fun/docs/guide/admin-custom-material-integration
  • 项目配置:https://docs-dev.guadd.fun/docs/guide/next-config
  • 项目初始化:https://docs-dev.guadd.fun/docs/guide/project-init
  • next-intl:https://next-intl.dev/docs/getting-started/app-router