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

@rokucommunity/sgrouter

v0.1.2

Published

A router for the Roku platform

Readme

sgRouter – Modern View Management for Roku Applications


🚀 Features

  • URL-style navigation for Roku apps
  • Dynamic routing with parameter support
  • Named routes — navigate by intent, not by hardcoded path strings
  • Route guards (canActivate) for protected screens
  • View lifecycle hooks for fine-grained control
  • Stack management (navigation, suspension, resume)
  • Observable router state for debugging or analytics

🧩 Installation

Requires Roku Promises

Install via ropm:

npx ropm install promises@npm:@rokucommunity/promises
npx ropm install sgRouter@npm:@rokucommunity/sgrouter

🧠 Core Concepts

Route Configuration

A route defines how your Roku app transitions between views. Routes are typically registered in your main scene.

Each route object can include:

| Property | Type | Required | Default | Description | |-----------|-------|-----------|---------|-------------| | pattern | string | ✅ | — | URL-like path pattern ("/details/movies/:id") | | component | string | ✅ | "" | View component to render (must extend sgRouter_View) | | name | string | ❌ | — | Stable identifier for named navigation (e.g. "movieDetail") | | allowReuse | boolean | ❌ | false | When true, navigating to the same route calls onRouteUpdate instead of creating a new view | | clearStackOnResolve | boolean | ❌ | false | Destroys all previous views in the stack when this route activates | | keepAlive | object | ❌ | { enabled: false } | When enabled: true, the view is suspended (not destroyed) when navigated away from | | canActivate | array | ❌ | [] | Guards that must allow navigation before the view is shown (see Route Guards) |

View Lifecycle Methods

Views extending sgRouter_View can define:

  • beforeViewOpen → Called before the view loads (e.g. async setup, API calls)
  • onViewOpen → Called after previous view is closed/suspended
  • beforeViewClose → Invoked before a view is destroyed
  • onViewSuspend / onViewResume → Handle stack suspensions/resumptions
  • onRouteUpdate → Fired when navigating to the same route with updated params/hash
  • handleFocus → Defines focus handling when the view becomes active

🧱 Example: Main Scene Setup

MainScene.xml

<component name="MainScene" extends="Scene">
    <script type="text/brightscript" uri="pkg:/source/roku_modules/sgrouter/router.brs" />
    <script type="text/brightscript" uri="MainScene.bs" />
    <children>
        <sgRouter_Outlet id="myOutlet" />
    </children>
</component>

MainScene.bs

sub init()
    ' Initialize the router at your main outlet
    sgRouter.initialize({ outlet: m.top.findNode("myOutlet") })

    sgRouter.addRoutes([
        { pattern: "/", component: "WelcomeScreen" },
        { pattern: "/shows", component: "CatalogScreen", clearStackOnResolve: true },
        { pattern: "/movies", component: "CatalogScreen", clearStackOnResolve: true },
        { pattern: "/details/series/:id", component: "DetailsScreen" },
        { pattern: "/details/series/:id/cast", component: "CastDetailsScreen" },
        { pattern: "/details/movies/:id", component: "DetailsScreen" },
        { pattern: "/details/movies/:id/cast", component: "CastDetailsScreen" },
        { pattern: "/:screenName", component: "DefaultScreen" }
    ])

    sgRouter.navigateTo("/") ' Go to the welcome view

    ' set the focus to the router
    sgRouter.setFocus({ focus: true })
end sub

👋 Example: Welcome View

WelcomeScreen.xml

<component name="WelcomeScreen" extends="sgRouter_View">
    <script type="text/brightscript" uri="pkg:/source/roku_modules/promises/promises.brs" />
    <script type="text/brightscript" uri="WelcomeScreen.bs" />
    <children>
        <Label id="label" />
    </children>
</component>

WelcomeScreen.bs

sub init()
    m.label = m.top.findNode("label")
end sub

' Called before the view is shown
function beforeViewOpen(params as dynamic) as dynamic
    m.label.text = "Hello!"
    return promises.resolve(invalid)
end function

🧭 Observing Router State

You can observe routerState for debugging or analytics:

sub init()
    sgRouter.getRouter().observeField("routerState", "onRouterStateChanged")
end sub

sub onRouterStateChanged(event as Object)
    data = event.getData()
    print `Router state changed: ${data.id} ${data.type} ${data.state}`
end sub

Router State Structure:

{
  "id": "",
  "type": "NavigationStart | RoutesRecognized | GuardsCheckStart | GuardsCheckEnd | ActivationStart | ActivationEnd | ResolveStart | ResolveEnd | NavigationEnd | NavigationCancel | NavigationError",
  "url": "",        // present on most events
  "state": {        // present on NavigationEnd and related events
    "routeConfig": {},
    "queryParams": {},
    "routeParams": {},
    "hash": ""
  },
  "error": {}       // only present on NavigationError
}

🔒 Route Guards

Route guards let you allow/deny navigation based on custom logic (e.g., authentication, feature flags). A guard is any node that exposes a canActivate function. The canActivate route config field takes an array of guards — all must pass before the view is shown.

1) Create a Guard (Auth example)

components/Managers/Auth/AuthManager.xml

<?xml version="1.0" encoding="utf-8"?>
<component name="AuthManager" extends="Node">
    <interface>
        <field id="isLoggedIn" type="boolean" value="false" />
        <function name="canActivate" />
    </interface>
</component>

components/Managers/Auth/AuthManager.bs

import "pkg:/source/router.bs"

' Decide whether navigation should proceed.
' Return true to allow, false or a RedirectCommand to block/redirect.
function canActivate(currentRequest = {} as Object) as Dynamic
    if m.top.isLoggedIn then
        return true
    end if

    dialog = createObject("roSGNode", "Dialog")
    dialog.title = "You must be logged in"
    dialog.optionsDialog = true
    dialog.message = "Press * To Dismiss"
    m.top.getScene().dialog = dialog

    ' Redirect unauthenticated users (e.g., to home or login)
    return sgRouter.createRedirectCommand("/login")
end function

2) Register the Guard

Create an instance and expose it globally (so routes can reference it):

components/Scene/MainScene/MainScene.bs (snippet)

' Create AuthManager and attach to globals
m.global.addFields({
    "AuthManager": createObject("roSGNode", "AuthManager")
})

' (Optional) observe auth changes
m.global.AuthManager.observeField("isLoggedIn", "onAuthManagerIsLoggedInChanged")

3) Protect Routes with canActivate

Attach one or more guards to any route using the canActivate array:

sgRouter.addRoutes([
    { pattern: "/", component: "WelcomeScreen", clearStackOnResolve: true },
    { pattern: "/login", component: "LoginScreen" },

    ' Protected content – requires AuthManager.canActivate to allow
    { pattern: "/shows", component: "CatalogScreen", clearStackOnResolve: true, canActivate: [ m.global.AuthManager ] },
    { pattern: "/movies", component: "CatalogScreen", clearStackOnResolve: true, canActivate: [ m.global.AuthManager ] },
    { pattern: "/details/:type/:id", component: "DetailsScreen", canActivate: [ m.global.AuthManager ] },
    { pattern: "/details/:type/:id/cast", component: "CastDetailsScreen", canActivate: [ m.global.AuthManager ] }
])

4) What canActivate should return

  • true → allow navigation
  • false → block navigation (stay on current view)
  • RedirectCommand → redirect elsewhere without showing the target route
    • Create via sgRouter.createRedirectCommand("/somewhere")

5) Accessing the Current Request (optional)

Your guard receives currentRequest with the full navigation context, useful for deep-links or conditional flows:

function canActivate(currentRequest as Object) as Dynamic
    ' currentRequest.route.routeConfig.pattern, currentRequest.route.routeParams, currentRequest.route.queryParams, currentRequest.route.hash, etc.
    if currentRequest?.queryParams?.requiresPro = true and not m.top.isProUser then
        return sgRouter.createRedirectCommand("/upgrade")
    end if
    return true
end function

6) Example: Feature Flag Guard

You can implement a reusable feature flag guard for gradual rollouts:

function canActivate(currentRequest as Object) as Dynamic
    feature = currentRequest?.routeParams?.feature ' e.g. "/feature/:feature"
    if m.global?.features[feature] = true then
        return true
    end if
    return sgRouter.createRedirectCommand("/")
end function

7) Testing Guards Locally

  • Toggle login in development: m.global.AuthManager.isLoggedIn = true
  • Verify redirects by attempting to navigate to a protected route while logged out:
    sgRouter.navigateTo("/shows")
  • Listen to router state changes to confirm block/redirect behavior:
    sgRouter.getRouter().observeField("routerState", "onRouterStateChanged")

The included test project already wires up an AuthManager and protects /shows, /movies, and /details/* routes using canActivate.


🏷️ Named Routes

Named routes let you navigate by a stable identifier instead of a hardcoded path string. If a path pattern ever changes, only the route config needs updating — every navigateTo call site remains valid.

1) Add a name to your routes

sgRouter.addRoutes([
    { pattern: "/",                  component: "WelcomeScreen",  name: "home",        clearStackOnResolve: true },
    { pattern: "/movies/:id",        component: "DetailsScreen",  name: "movieDetail"  },
    { pattern: "/settings",          component: "SettingsView",   name: "settings"     },
])

name is optional — routes without one continue to work exactly as before.

2) Navigate by name

Pass an associative array with a name key instead of a path string:

' Static route — no params needed
sgRouter.navigateTo({ name: "home" })

' Dynamic route — params are substituted into :segment placeholders
sgRouter.navigateTo({ name: "movieDetail", params: { id: 42 } })
' Resolves to: /movies/42

' Extra params beyond what the pattern requires become query parameters
sgRouter.navigateTo({ name: "movieDetail", params: { id: 42, autoplay: true } })
' Resolves to: /movies/42?autoplay=true

String arguments are unchanged — literal path logic runs with zero overhead:

sgRouter.navigateTo("/movies/42")   ' still works exactly as before

3) Backend-driven navigation

Named routes remove the need for client code to reconstruct URL strings from backend responses:

' Backend response: { screen: "movieDetail", id: 42 }
response = m.global.ApiManager.getDeepLink()
sgRouter.navigateTo({ name: response.screen, params: { id: response.id } })

4) Error handling

If the name is not found or a required param is missing, a warning is printed and navigation is cancelled. The history stack is unchanged and no lifecycle hooks are triggered.

sgRouter.navigateTo({ name: "doesNotExist" })
' [WARN] sgRouter: no route found with name "doesNotExist"

sgRouter.navigateTo({ name: "movieDetail" })
' [WARN] sgRouter: missing required param "id" for route "movieDetail" (/movies/:id)

Extra params beyond what the pattern requires are silently appended as query parameters — no warning is logged.

Duplicate names at registration time log a warning and the first registration wins:

' [WARN] sgRouter: duplicate route name "home" — first registration wins (existing: /, ignored: /home)

🧭 Route Snapshot in lifecycle hooks

Every view lifecycle receives a route snapshot so your screen logic can react to the URL that triggered navigation.

What you get in params

beforeViewOpen, onViewOpen, beforeViewClose, onViewSuspend, and onViewResume all receive a params object constructed by the router just before the lifecycle is called, which includes:

params.route.routeConfig          ' the matched route definition
params.route.routeParams          ' extracted from pattern placeholders (e.g. :id, :type)
params.route.queryParams          ' parsed from ?key=value pairs
params.route.hash                 ' parsed from #hash
params.route.navigationState      ' how this navigation was triggered:
  .fromPushState                  '   true on normal forward navigation
  .fromPopState                   '   true when arriving via goBack()
  .fromKeepAlive                  '   true when a keepAlive view is resumed
  .fromRedirect                   '   true when arrived via a canActivate guard redirect

The snapshot is sourced from the URL you navigated to (e.g. "/details/movies/42?page=2&sort=trending#grid=poster"). The router builds this object and passes it into beforeViewOpen(params), onViewOpen(params), beforeViewClose(params), onViewSuspend(params), and onViewResume(params).

onRouteUpdate is different — it receives an object with both the old and new route (params.oldRoute and params.newRoute), so you can diff the two and respond to exactly what changed.

Example: Using it in a Catalog view

' CatalogScreen.bs (excerpt)
function beforeViewOpen(params as object) as dynamic
    ' Read route params (e.g., /:type and /:id)
    contentType = params.route.routeParams?.type    ' "shows" or "movies"
    itemId      = params.route.routeParams?.id      ' e.g., "42"

    ' Read query params (?page=2&sort=trending)
    pageIndex = val(params.route.queryParams?.page)    ' 2
    sortKey   = params.route.queryParams?.sort         ' "trending"

    ' Optional: hash fragment (#grid=poster)
    gridMode = params.route.hash

    ' Kick off data loading based on URL snapshot
    ' ... start tasks or fetches here ...

    ' Return a promise to delay opening until ready,
    ' or return true to open immediately and manage loading UI yourself.
    return promises.resolve(invalid)
end function

' If you navigate to the **same route pattern** with different params or hash,
' `onRouteUpdate(params)` will fire (when `allowReuse` is enabled),
' allowing you to update the view without rebuilding it.
' CatalogScreen.bs (excerpt)
function onRouteUpdate(params as object) as dynamic
    oldRoute = params.oldRoute
    newRoute = params.newRoute

    return promises.resolve(invalid)
end function

Where the snapshot comes from

The route snapshot is assembled by the router by parsing:

  • the pattern match result → routeParams
  • the query stringqueryParams
  • the hashhash

That structured object is then provided to the view lifecycles mentioned above. This keeps your screens URL-driven and easy to test (you can navigate with different URLs and assert behavior based on params).


💬 Community & Support


📄 License

Licensed under the MIT License.