1.3.1 • Published 1 year ago

static-tree v1.3.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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.

TerminologyDescription
static treea tree whose nodes are declared at build time and not likely to change at runtime
TNodea node of the static tree with optional inner TNodeData, optional parent, and zero or more children
ExtendedTNodea TNode with children inline as properties for better DX
ExtendedTNodeBuildertype-safe builder for ExtendedTNode
tBuildfunctional 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'
1.3.1

1 year ago

1.3.0

1 year ago

1.2.0

2 years ago

1.1.1

2 years ago

1.0.1

2 years ago

1.1.0

2 years ago

1.0.0

2 years ago

0.1.0

2 years ago