static-tree
TypeScript icon, indicating that this package has built-in type declarations

1.3.1 • Public • Published

static-tree

bundlephobia.badge npm.static-tree.badge

This package is part of the static-tree monorepo


Table of Contents

Installation

npm install static-tree
yarn add static-tree
pnpm add static-tree

Quick Start

  1. Use the idiomatic tBuild to create a static tree. See tBuild for examples and options.

    import { tBuild } from 'static-tree';
    
    const { node: api } = tBuild('api', { // you can rename node here to whatever
      pathResolver: () => 'https://api.domain.example',
      build: (builder) =>
        builder
          .addChild('auth', {
            build: (builder) =>
              builder
                .addChild('logout')
                .addChild('oauth', {
                  build: (builder) => builder.addChild('google').addChild('discord'),
                  //...
                })
          })
          .addChild('camelCaseKey', {
            pathResolver: () => 'camel-case-key',
          })
          .addChild(':organization', { // notice some dynamic path here
            pathResolver: (_node, arg) => arg,
            build: (builder) =>
              builder.addChild(':user', {
                pathResolver: (_node, arg) => arg,
              }),
          }),
    });

    The declaration above will produce this tree structure:

    api (resolve statically to 'https://api.domain.example' at runtime)
      |
      |-- auth
      .     |-- logout
      .     |-- oauth
      .          |
      .          |-- google
      .          |-- discord
      |-- camelCaseKey (resolved statically to 'camel-case-key' at runtime)
      |-- :organization (resolved dynamically to the given argument at runtime)
            |
            |-- :user (resolved dynamically to the given argument at runtime)
    
  2. Access type-safe nested children

    root.auth.oauth.google.$.path();
    // -> 'https://api.domain.example/auth/oauth/google'
    
    // note that ":" here has no special effect
    // just for easier recognition as dynamic (think backend router system)
    root[':organization'][':user'].$.path({
      args: {
        ':organization': 'test-org',
        ':user': 'test-user',
      }
    });
    // -> 'https://api.domain.example/test-org/test-user'
    
    root.auth.logout.$.depth(); // -> 3
    root.auth.oauth.discord.$.root() // -> point back to root node

    the $ getter returns the TNodePublicApi collection of methods.

  3. Access type-safe data

    root.$.data(); // -> { some: 'data' }
    root.nestedChild.$.data().nestedChildData; // -> 101

Documentation & Terminologies

This repo includes a full api extracted documentation generated by @microsoft/api-extractor & @microsoft/api-documenter. Please refer to said docs for examples and details.

Terminology Description
static tree a tree whose nodes are declared at build time and not likely to change at runtime
TNode a node of the static tree with optional inner TNodeData, optional parent, and zero or more children
ExtendedTNode a TNode with children inline as properties for better DX
ExtendedTNodeBuilder type-safe builder for ExtendedTNode
tBuild functional wrapper for ExtendedTNodeBuilder

Original Use Case

This package was derived from the solution to a specific problem I encountered frequently. Consider having this "config" object:

const AppConfig = {
  urls: {
    web: 'https://domain.example',
    api: {
      index: 'https://api.domain.example',
      auth: {
        index: '/auth',
        logout: '/logout',
        oauth: {
          index: '/oauth',
          google: '/google',
          // ...
        },
      },
    },
  },
};

To get a full api url of google oauth, we have to do quite a lot:

const { api: { auth, index } } = AppConfig.urls;
const path = index + auth.index + auth.index.oauth.index + auth.oauth.google;

Already there are some problems:

  • Verbosity: lots of reference to get to something, more typing equals more typos equals less productive time.

  • The inconsistency of the config structure: some path will require an object index, some path is just a string. We could refactor to something more predictable, although i think we can agree that this would quickly get out of hand and is very disorienting to look at:

    const AppConfig = {
      urls: {
        web: {
          base: 'https://domain.example',
          paths: {},
        },
        api: {
          base: 'https://api.domain.example',
          paths: {
            auth: {
              base: '/auth',
              paths: {
                logout: {
                  base: '/logout',
                },
                // ...
              },
            },
          },
        },
      },
    };

Introducing static-tree, arguably a better alternative to the above.

import { tBuild } from 'static-tree';

const { node: api } = tBuild('api', {
  pathResolver: () => 'https://api.domain.example',
  build: (builder) => builder
    .addChild('auth', {
      build: (builder) => builder
        .addChild('logout')
        .addChild('oauth', {
          build: (builder) => builder
            .addChild('google')
            .addChild('discord'),
            //...
        }),
    }),
});

You might say, why the ugly builder pattern? Because I have not figured out any other pattern that allows the same level of type-safety. It seems like a lot for what would be an object declaration, but consider what we can do now:

api.auth.oauth.google.$.path(); // -> 'https://api.domain.example/auth/oauth/google'

Even more cool things (and perhaps more in the future if we need to extend the api):

api.auth.oauth.google.$.path({ depth: 2 }); // -> 'oauth/google'
api.auth.oauth.google.$.path({ depth: -2 }); // -> 'https://api.domain.example/auth'

Package Sidebar

Install

npm i static-tree

Weekly Downloads

1

Version

1.3.1

License

MIT

Unpacked Size

135 kB

Total Files

22

Last publish

Collaborators

  • vnphanquang