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

lok-onion

v1.0.3

Published

A simple webframework for onion sites.

Readme

Løk

A simple webframework for onion sites.

Installation

npm install lok-onion

Example

/* @jsxImportSource lokjs */
// 
// This line sets the jsx import source so the typescript compiler
// knows to use the custom jsx functions from lokjs instead of React.
// 
// You can also set the "jsxImportSource" field in your
// tsconfig.json to "lokjs" instead.

import { Lok, Button, UserSessionBase, redirect} from "lokjs";

// Stored for every user on the client as a cookie.
// Note that users can choose to reset their state whenever they want.
// The user session interface should extend UserSessionBase, and should be
// JSON serializable.
interface UserSession extends UserSessionBase {
    count: number;
}

const lok = Lok.initialize<UserSession>(
    // A secret used for signing data.
    "my-secret-key",
    // A function that creates a new session, will be called for new sessions
    // or when a user resets their session.
    () => ({ count: 0 }),
);

// An SSF (Server-Side Function) is used to run javascript on the server
// whenever a user interacts with the page. It can be used to modify the
// user's session, or run code that you don't want to expose to the client.
//
// Since løk is designed for onion sites, no javascript is ran on the client
// by default. Meaning you have to run session modification on the server.
//
// An SSF has access to:
// - The user's session, through `userSession`, of type `UserSession` in this example
// - Any field values passed by the client, through `userArguments`.
//     This is an object that can contain any fields and values, so make sure to
//     validate and sanitize it before using it.
// - The express response object, through `response`, which can be used to set
//     cookies or send responses directly. You won't need to use this in most cases.
// - Any server arguments passed when calling the SSF, through `serverArguments`.
//     This object can contain any JSON serializable value, clients can see the value
//     but can't modify it. You can trust this value.
const addToCountSsf = lok.createSsf<number>(async ({ userSession, serverArguments }) => {
    userSession.count += serverArguments;

    // After modifying the session, we redirect the user back to the index page.
    return redirect(
        // The url to redirect to.
        "/",
        // True implies the modifications we made to the userSession will be saved.
        // If you error halfway through modifying the session, you can pass false here
        // instead to revert the session to its previous state.
        true,
        // An optional message that will be stored in the session and can be displayed to the user.
        // This will always be stored, even if you pass false as the second argument.
        { type: "info", text: `Added ${serverArguments} to count` }
    );
});

const setCountSsf = lok.createSsf<{count: number, message: string}, undefined>(async ({ userSession, serverArguments }) => {
  userSession.count = serverArguments.count;
  return redirect("/", true, { type: "info", text: serverArguments.message });
});

// This SSF contains a bit more logic, to validate the user input.
const userSetCountSsf = lok.createSsf(async ({ userSession, userArguments }) => {
  if (userArguments.value === undefined) {
    return redirect("/", false, { type: "error", text: "Value is required" });
  }
  const value = parseInt(userArguments.value, 10);
  if (isNaN(value)) {
    return redirect("/", false, { type: "error", text: "Value must be a number" });
  }
  userSession.count = value;
  return redirect("/", true, { type: "info", text: `Count set to ${value}` });
});

// This is the component that will be rendered for the "/" route.
// Each page receives the user's session as a property.
const HomePage = ({ userSession }: { userSession: UserSession }) => {
    return (<>
        <style>
            {`
            html {
                font-family: Arial, sans-serif;
            }

            .popup {
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                padding: 10px 20px;
                border-radius: 5px;
                font-weight: bold;
                z-index: 1000;
            }

            .fadeout {
                animation: fadeout 3s forwards;
            }

            @keyframes fadeout {
                0% { opacity: 1; }
                80% { opacity: 1; }
                100% { opacity: 0; }
            }

            .info {
                background-color: lightblue;
                color: white;
            }

            .error {
                background-color: lightcoral;
                color: white;
            }
            `}
        </style>

        {/* Show a popup if there's a message in the user session */}
        {userSession.message && (
            <p class={`popup fadeout ${userSession.message.type}`}>{userSession.message.type.toUpperCase()}: {userSession.message.text}</p>
        )}

        <h1>Welcome to løk!</h1>
        <p>Count: {userSession.count}</p>

        {/* onClickSsf specifies which SSF to call when the button is clicked */}
        {/* The ssfArguments property will be passed to the SSF as the
            serverArguments parameter, use undefined if you don't want to pass anything. */}
        <Button onClickSsf={addToCountSsf} ssfArguments={1}>+</Button>
        <Button onClickSsf={addToCountSsf} ssfArguments={-1}>-</Button>
        &nbsp;
        <input type="text" name="value" placeholder="Set count" />
        <Button onClickSsf={userSetCountSsf} ssfArguments={undefined}>Set</Button>
        &nbsp;
        <Button onClickSsf={setCountSsf} ssfArguments={{ message: "Count was reset", count: 0 }} disabled={userSession.count === 0}>Reset</Button>
        <Button onClickSsf={setCountSsf} ssfArguments={{ message: "Doubled!", count: userSession.count * 2 }} disabled={userSession.count === 0}>Double</Button>
    </>);
};

// Using the `route` method, we can specify which component should be rendered for which route.
// Note that components won't generate at the top level. Each component is wrapped in a form.
lok.route("/", HomePage);

// Using the `listen` method, we can start the server and listen for incoming requests.
const PORT = 3000;
lok.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
});