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

@geektech/tsone

v0.6.1

Published

TSone 是轻量级纯 TypeScript 前端框架,提供响应式系统、类组件、策略化渲染和路由功能

Readme

TSone

English | 简体中文

Documentation: https://geektech-team.github.io/tsone/

A lightweight frontend framework written entirely in TypeScript, with reactivity, class-based components, strategy-driven rendering, and routing.

Features

  • TypeScript-first implementation with typed public APIs
  • Bun-native tooling for installation, testing, builds, playgrounds, and docs
  • Documentation backed by a TypeScript typed-content registry
  • Reactive primitives: reactive, effect, and computed
  • Object-oriented components with Component<Props, State>, lifecycle hooks, events, and slots
  • Strategy-based rendering that dispatches text, elements, components, and slots by VNode type
  • Built-in routing with createRouter, RouterView, and RouterLink
  • Lightweight runtime with no external production dependencies

Repository Structure

This repository is a Bun workspace monorepo with three published packages: packages/tsone/ contains the browser framework @geektech/tsone, packages/one/ contains the component library @geektech/one built on the framework, and packages/tsone-cli/ contains the Bun-native development tooling @geektech/tsone-cli. Standalone example projects live in the root playground/ directory.

Installation

bun add @geektech/tsone @geektech/tsone-cli
pnpm add @geektech/tsone @geektech/tsone-cli

The framework and CLI require Bun >=1.3.0 for the documented workflow.

Quick Start

import {
  Component,
  VNode,
  createApp,
  computed,
  reactive,
} from '@geektech/tsone';

interface AppState {
  count: number;
  version: string;
}

class App extends Component<Record<string, never>, AppState> {
  protected initState(): AppState {
    return {
      count: 0,
      version: '0.6.1',
    };
  }

  protected initStyles(): void {}

  protected render(): VNode {
    return {
      tag: 'main',
      props: { className: 'app' },
      children: [
        { tag: 'h1', children: ['TSone'] },
        { tag: 'p', children: [`version: '{{version}}'`] },
        {
          tag: 'button',
          listeners: {
            click: () => {
              this.state.count += 1;
            },
          },
          children: [`count: {{count}}`],
        },
      ],
    };
  }
}

const state = reactive({ ready: true });
const status = computed(() => (state.ready ? 'ready' : 'pending'));

const app = createApp({ root: App, state });
app.mount();

console.log(status.value);

createApp uses #app as its default mount target, so an application can call app.mount() immediately after creation. If the target is not currently in the document, mount() safely skips that attempt. Pass rootElement only when you need to override the default target.

Development Tooling

The separate @geektech/tsone-cli package provides tsone create, tsone dev, and tsone build. tsone create scaffolds a basic project (including a homepage showing the TSone name and a GitHub link) into the current directory. The CLI's default entry is src/main.ts; that module must expose the application as export const app, and the value must provide renderHtmlDocument().

Create an optional tsone.config.ts at the project root:

The config file supports only a plain-object default export; functional or function-valued config is not supported.

import { defineConfig } from '@geektech/tsone-cli';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
  build: { outDir: 'dist' },
});

The string proxy shorthand is also supported, for example { '/backend': 'http://localhost:4000' }. Defaults are src/main.ts, 127.0.0.1, port 52211, an empty server.proxy, dist, and no additional pages.

Multi-page applications map routes to page entries in the same config file; each page entry exposes app with renderHtmlDocument() exactly like the root entry:

export default defineConfig({
  entry: 'src/main.ts',
  pages: {
    '/about': 'src/about.ts',
    '/docs/guide': 'src/guide.ts',
  },
});

pages keys must start with / and may nest; the root / is served by entry. During development each page is served at its route, and tsone build emits one HTML document per page (index.html, about.html, docs/guide.html) with page-relative asset URLs. Site JavaScript entries use content-hashed filenames such as main-<hash>.js, and each document references the emitted filename.

tsone create
tsone dev [--host <host>] [--port <port>] [--no-watch]
tsone build [--out-dir <path>]

create takes no options. dev accepts host/port overrides and --no-watch; build accepts the output-directory override; both --port 3000 and --port=3000 forms are valid. tsone dev watches the project by default and notifies browsers to reload over /__tsone/reload when a watched file changes. Watch mode keeps the latest successful bundle generation for each entry and deletes the previous generation before notifying browsers; old resource URLs return 404. Failed builds keep the last working artifacts and do not trigger a reload. --no-watch retains its on-demand generations. Config changes restart the server and remove the old instance's session artifacts after it stops and its pending requests finish. Build output must remain a safe child directory inside the project root.

Programmatic tooling is imported from @geektech/tsone-cli, not from the framework root. It exports defineConfig, resolveConfig, startDevServer, and build. The dev server is owned by the caller, which must call server.stop(); build() returns absolute root and outDir values plus assetsBuilt.

The development server serves HTTP only. Proxy targets may use HTTP or HTTPS. Rules use literal prefix matching with the longest match first, retain query/body/end-to-end headers, optionally apply changeOrigin and rewrite, and return 502 Bad Gateway when the upstream is unreachable. CLI v1 has no config plugins, WebSocket, HMR, SSR, functional config, public/ copying, or public minify/sourcemap settings.

Routing

import { Component, VNode, createApp } from '@geektech/tsone';
import { RouterLink, RouterView, createRouter } from '@geektech/tsone/router';

class Layout extends Component {
  protected initState(): object {
    return {};
  }

  protected initStyles(): void {}

  protected render(): VNode {
    return {
      tag: 'main',
      children: [
        {
          component: RouterLink,
          props: { to: '/', children: ['Home'] },
        },
        {
          component: RouterLink,
          props: { to: '/users/42', children: ['User'] },
        },
        { component: RouterView },
      ],
    };
  }
}

const router = createRouter({
  mode: 'history',
  routes: [
    { path: '/', component: HomePage },
    { path: '/old', redirect: '/new' },
    { path: '/new', component: NewPage },
    {
      path: '/users/:id',
      component: UserPage,
      meta: { title: 'User Details' },
    },
    { path: '*', component: NotFoundPage },
  ],
});

router.beforeEach((to, from) => {
  if (to.path === '/admin' && !isAuthenticated) {
    return false;
  }
  return true;
});
router.afterEach((to) => {
  document.title = to.meta?.title ?? 'TSone';
});

createApp({ root: Layout }).use(router).mount();

Guards run on programmatic navigation only. Returning false cancels the navigation, returning a string redirects to that path. redirect routes resolve to their target, and a * route matches every otherwise unmatched path with the remaining path available as params.pathMatch.

Requests

Import the optional request client from its dedicated subpath. It uses native fetch, parses JSON by default, and has no runtime dependencies:

import { createRequest } from '@geektech/tsone/request';

const api = createRequest({ baseURL: '/api' });

api.interceptors.request.use((config) => ({
  ...config,
  headers: { ...config.headers, Authorization: 'Bearer token' },
}));
api.interceptors.error.use(() => ({ offline: true }));

const user = await api.get('/users/1');
const health = await api.get('/health', { responseType: 'text' });

Request, response, and error interceptors run in registration order and may be asynchronous. A non-2xx response rejects unless an error interceptor recovers with a value. Set responseType to text, blob, arrayBuffer, or response when the default JSON result is not appropriate; response returns the raw Response instance.

Development Commands

bun install
bun test
bun run build
bun run dev
bun run docs
bun run docs:build

The root playground/ directory contains two standalone projects:

bun run dev
bun run dev:site
bun run dev:admin
  • playground/official-site: product website example
  • playground/admin-dashboard: admin dashboard example

Documentation content is maintained in the typed-content registry. Chinese content lives in packages/tsone/docs/app/content/zh/, while English content lives in packages/tsone/docs/app/content/en/. Every logical route must exist in both directories.

Chinese and English catalogs each contain exactly 17 logical routes. Add or remove a route in both catalogs in the same change.

Content links stay locale-neutral: never write /en/ manually. Chinese public routes are unprefixed, while English routes use /en/. Browser-language detection runs only at /; manual selection takes precedence and persists for later visits.

Build the static documentation site with:

bun run docs:build

The build fails strictly for missing, extra, duplicate, empty, or mixed-language pages.

To host the site under a sub-path (for example a GitHub Pages project page such as https://<owner>.github.io/tsone/), build it with a base path:

bun run docs:build -- --base=/tsone/

You can also set the DOCS_BASE_PATH environment variable. When a base path is configured, every link and asset URL is prefixed with it, and the locale bootstrap reads the base path from the rendered document, so the site works under any sub-path.

The base HTML document shell can also be generated from createApp. It emits a #app mount node by default; pass rootElement only to use a different target. When a custom body is needed, pass a component or VNode rather than an HTML string. This API renders the mount node through TSone's renderer. In a Bun or Node static-generation environment, provide a DOM-like document first:

import { createApp, type StyleSheet } from '@geektech/tsone';

const styles: StyleSheet = [
  {
    selector: '.app',
    properties: { maxWidth: '72rem' },
  },
];

const app = createApp({
  root: App,
  document: {
    lang: 'en',
    title: 'TSone App',
    styles,
  },
});

const html = app.renderHtmlDocument({
  scripts: [{ type: 'module', src: '/assets/app.js' }],
});

Public API

The main @geektech/tsone entry point exports:

  • createApp(options)

  • createApp({ root, rootProps })

  • Component<Props, State>

  • TransitionGroup / TransitionGroupProps / TransitionAnimationType

  • Transition / TransitionProps — enter/leave CSS class transitions

  • KeepAlive / KeepAliveProps — keep child components mounted across activeKey switches

  • Router guards: Router.beforeEach / Router.afterEach, plus RouteRecord.redirect and * catch-all routes

  • VNode

  • h() / createComponent() / slot() / each()

  • Tag(tag, options) for arbitrary HTML elements

  • Div() / Span() / P() / Button() / Input()

  • Section() / Main() / Header() / Footer() / Nav() / Article() / Aside()

  • H1() through H6() / Strong() / Em() / Small() / Pre() / Code() / Blockquote()

  • Ul() / Ol() / Li() / A() / Img()

  • Form() / Label() / Textarea() / Select() / Option()

  • Table() / Thead() / Tbody() / Tr() / Th() / Td()

  • Directions / ModelBinding

  • InjectionKey and component/application provide() / inject()

  • createForm() / required() / minLength() / validate()

  • createApp(options).renderHtmlDocument(options)

  • renderHtmlDocument(options)

  • StyleSheet / renderStyleSheet(styles)

  • reactive() / readonly()

  • effect() / stop()

  • computed()

  • ref() / isRef() / unref()

  • watch(source, callback, options) — reactive watching with immediate, deep, and sync options

  • nextTick() / flushSync()

  • version, currently 0.6.1

The @geektech/tsone/style entry exports StyleManager and renderManagedStyleSheet(styles: Iterable<StyleOptions>).

Site builds and development automatically emit content-hashed CSS for document.styles and fixed initStyles() / fresh StyleManager registrations. Existing literals, local or module arrays with forEach, and supported pure theme expressions need no changes. Uncertain rules stay dynamic. Components keep synchronous inline CSS until matching external CSS loads; edits restore inline styles at the original position and unmount removes only that instance's nodes. useStaticStyleSheet(href) is used by generated code; styleElement becomes null while external CSS is active. Rules remain in the JavaScript bundle for fallback, and first initialization still serializes CSS. Relative/fragment URLs, @import, ambiguous URLs and HTML base elements retain inline behavior. Dev serves CSS from its exact generation URL. Watch replaces JavaScript and CSS together after a successful rebuild; failures keep the previous artifacts. Library builds keep their existing path. Module discovery adds a build pass; this optimization does not promise shorter build times.

Rendering, Communication, and Forms

directions.if controls whether an element, component, or slot is mounted. The node is unmounted when the condition is false:

{
  component: ProfilePanel,
  directions: { if: this.state.visible },
}

each() creates stable keys for list items so the renderer can reuse nodes during reordering, insertion, and removal:

const items = each(
  this.state.users,
  (user) => ({ tag: 'li', children: [user.name] }),
  (user) => user.id
);

TransitionGroup animates the enter and exit of direct keyed children:

{
  component: TransitionGroup,
  props: { tag: 'ul', type: 'fade', duration: 300 },
  children: each(
    this.state.items,
    (item) => Li({ children: [item.label] }),
    (item) => item.id
  ),
}

The animation types are fade, slide-up, slide-down, slide-left, slide-right, and scale. The defaults are a div wrapper, fade, and 300 ms with ease easing. Every direct child must have a unique key. Initial children animate in, and removed children stay mounted until their exit ends. TSone automatically disables these animations for prefers-reduced-motion: reduce or when Web Animations is unavailable. Reordering reuses and moves existing nodes without a reorder or FLIP animation.

Transition animates a single element as show flips. It applies {name}-enter-from / {name}-enter-to and {name}-leave-from / {name}-leave-to CSS classes (each paired with a -active class) and keeps the element mounted until duration ms after the leave starts:

{
  component: Transition,
  props: { show: this.state.open, name: 'fade', duration: 300 },
  children: [Dialog({ children: ['Settings'] })],
}

KeepAlive keeps every keyed child component mounted (state and DOM are preserved) while hiding inactive ones with display: none:

{
  component: KeepAlive,
  props: { activeKey: this.state.activeTab },
  children: [
    createComponent(Editor, {}, [], 'editor'),
    createComponent(Preview, {}, [], 'preview'),
  ],
}

Component events return an unsubscribe function, while component VNodes can declare parent listeners through emitters:

const stopListening = child.on('saved', (payload) => console.log(payload));
stopListening();
// { component: Editor, emitters: { saved: (payload) => this.save(payload) } }

Dependency injection resolves values from the current component, then its parents, and finally the application instance:

const THEME: InjectionKey<{ mode: string }> = Symbol('theme');
app.provide(THEME, { mode: 'dark' });
const theme = this.inject(THEME, { mode: 'light' });

directions.model supports dot-separated paths and conversion functions. Native input, textarea, checkbox, radio, and select controls stay synchronized:

Input({
  props: { type: 'number' },
  directions: {
    model: {
      path: 'profile.age',
      parse: (value) => Number(value),
      format: (value) => String(value ?? ''),
    },
  },
});

Validation uses pure functions and does not own error UI or submission:

const form = createForm(this.state, {
  'profile.name': [required('Name is required'), minLength(2)],
  'profile.age': [
    validate((value) => Number(value) >= 18 || 'Age must be at least 18'),
  ],
});

const result = form.validate();
form.resetErrors();

The @geektech/tsone/router entry point exports:

  • createRouter({ routes, mode, base })
  • Router
  • RouterView
  • RouterLink
  • useRouter()
  • RouteRecord
  • RouteLocation

The @geektech/tsone/style entry point exports:

  • StyleManager
  • StyleSheet / renderStyleSheet(styles)

The @geektech/tsone/request entry point exports:

  • createRequest(defaults?) / default request
  • RequestClient / RequestError
  • RequestConfig / ResponseType

Pre-Publish Checklist

bun test
bunx tsc --noEmit
bun run build
bun pm pack --cwd packages/tsone --dry-run

Contributing

Issues and pull requests are welcome. Before publishing an open-source release, make sure the tests, type checks, and build all pass.

License

MIT