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

tempo-sdk

v0.0.14

Published

Tempo SDK — Vite, Next.js, and Expo plugins for JSX annotation and shared page/storyboard types

Readme

tempo-sdk

Tempo's JSX-annotation plugins for Vite and Next.js. At build time, every JSX element in your codebase gets data-tempo-* attributes that map the rendered DOM back to the exact source-file location that produced it. Tempo's design surface uses these attributes to navigate from a clicked element to its source.

This package ships two plugins from one workspace:

  • tempo-sdk — Vite plugin (tempoVitePlugin)
  • tempo-sdk/nextjs — Next.js plugin (tempoNextjsPlugin)
  • tempo-sdk/nextjs/plugin — default-exported Next.js plugin for ESM config files
  • tempo-sdk/expo/plugin — Expo Babel plugin (tempoExpoBabelPlugin)

Both produce byte-identical data-tempo-* output, so downstream consumers (source-editor.ts, frame-preload.ts) work the same regardless of which framework the user's project is on.


Next.js usage

1. Install

In a workspace package:

{
  "dependencies": { "tempo-sdk": "workspace:*" }
}

In an external project: pnpm add tempo-sdk (or your package manager's equivalent).

2. Wrap next.config

CommonJS (next.config.js):

const tempoNextjsPlugin = require("tempo-sdk/nextjs");

module.exports = tempoNextjsPlugin()({
  reactStrictMode: true,
  // ...rest of your existing config
});

ESM (next.config.mjs / next.config.ts):

import tempoNextjsPlugin from "tempo-sdk/nextjs/plugin";

export default tempoNextjsPlugin()({
  reactStrictMode: true,
});

You can also use the named export from tempo-sdk/nextjs:

import { tempoNextjsPlugin } from "tempo-sdk/nextjs";

3. Set the TEMPO env var when launching dev

TEMPO=true next dev            # Turbopack (Next.js 16+ default)
TEMPO=true next dev --webpack  # Webpack mode

Without TEMPO set, the plugin is a complete no-op. You can leave tempoNextjsPlugin() in next.config.js permanently — there is zero overhead in normal dev. Tempo's start script sets TEMPO=true automatically when launching the project under Tempo.

4. App Router — that's it

The loader auto-injects <meta name="tempo-project-root"> into the first JSX <head> it sees (typically app/layout.tsx). Every JSX element in your project gets data-tempo-filepath, data-tempo-position, data-tempo-modifiedts, and data-tempo-supports-* attributes baked into the SWC-compiled output.

Example rendered HTML:

<button
  data-tempo-filepath="components/Button.tsx"
  data-tempo-position="142"
  data-tempo-modifiedts="1708900000000"
  data-tempo-supports-children="true"
>
  Click me
</button>

<head>
  <meta name="tempo-project-root" content="/abs/path/to/project" />
</head>

5. Pages Router — one extra line in _document.tsx

<Head> from next/document is a custom React component (capital H), not intrinsic JSX <head> (lowercase), so auto-inject doesn't fire there. Add the project-root meta manually:

// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";
import { tempoProjectRoot } from "tempo-sdk/nextjs";

export default function Document() {
  return (
    <Html>
      <Head>
        <meta name="tempo-project-root" content={tempoProjectRoot()} />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

tempoProjectRoot() returns the same canonical root the loader uses for data-tempo-filepath, so paths reconstruct correctly.

Options

All optional:

tempoNextjsPlugin({
  autoInjectRootMeta: false,                       // disable head meta auto-inject
  productionBuild: true,                           // run in `next build` too (rare)
  computeAnnotatedPropsModule: "./tempo-strategy.js", // custom prop-computation
})(nextConfig);

computeAnnotatedPropsModule is a path (not a function) because Turbopack requires loader options to be JSON-serializable. The module's default export must be a function (info, filepath, modifiedtsMs) => Record<string, PropValue>.

Composing with other wrappers (@next/mdx, etc.)

tempoNextjsPlugin follows the @next/mdx higher-order-config pattern, so it composes naturally:

const tempoNextjsPlugin = require("tempo-sdk/nextjs");
const withMDX = require("@next/mdx")();

module.exports = tempoNextjsPlugin()(
  withMDX({ reactStrictMode: true })
);

tempoNextjsPlugin should be the outermost wrapper so its webpack/turbopack chaining sees the inner wrappers' contributions.

Supported Next.js versions

| Next.js | Webpack | Turbopack | Notes | |---|---|---|---| | 16.x | --webpack flag | default | Validated end-to-end | | 15.3 – 15.x | default | --turbo flag | Wrapper writes to turbopack.* | | 13.0 – 15.2 | default | --turbo flag | Wrapper writes to experimental.turbo.* | | 12.x | default | n/a | Webpack only; turbopack field omitted | | < 12 | n/a | n/a | Out of scope |

The wrapper detects the version via next/package.json and writes to the correct field automatically.


Vite usage

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { tempoVitePlugin } from "tempo-sdk";

export default defineConfig({
  plugins: [tempoVitePlugin(), react()],
});

Same TEMPO env-var gating, same data-tempo-* output as the Next.js plugin. The Vite plugin additionally exposes an /__tempo/errors SSE endpoint for streaming compile errors to the design surface — see SPEC.md for the full design.


Expo usage

Expo support is for Tempo's web canvas runtime. It annotates Expo web output for selection and editing; it does not make native-device canvases editable.

Configure the Tempo-owned Expo sidecar app's Babel config:

// tempo/babel.config.js
const path = require("node:path");
const { tempoExpoBabelPlugin } = require("tempo-sdk/expo/plugin");

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      [
        tempoExpoBabelPlugin,
        {
          root: path.resolve(__dirname, ".."),
        },
      ],
    ],
  };
};

If the Expo app uses NativeWind-style className, pass reactNativeClassName: true in the plugin options.

The sidecar Metro config must watch the parent project so tempo/ can import real app components:

// tempo/metro.config.js
const path = require("node:path");
const { getDefaultConfig } = require("expo/metro-config");

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, "..");
const sidecarNodeModules = path.join(projectRoot, "node_modules");

const config = getDefaultConfig(projectRoot);
const sidecarModule = (moduleName) =>
  path.join(sidecarNodeModules, ...moduleName.split("/"));
const singletonModules = {
  react: sidecarModule("react"),
  "react-dom": sidecarModule("react-dom"),
  "react-native": sidecarModule("react-native"),
  "react-native-web": sidecarModule("react-native-web"),
  "react/jsx-runtime": sidecarModule("react/jsx-runtime.js"),
  "react/jsx-dev-runtime": sidecarModule("react/jsx-dev-runtime.js"),
};

config.watchFolders = Array.from(
  new Set([...(config.watchFolders ?? []), workspaceRoot]),
);
config.resolver.nodeModulesPaths = Array.from(
  new Set([
    sidecarNodeModules,
    path.join(workspaceRoot, "node_modules"),
    ...(config.resolver.nodeModulesPaths ?? []),
  ]),
);
config.resolver.disableHierarchicalLookup = true;
config.resolver.extraNodeModules = {
  ...(config.resolver.extraNodeModules ?? {}),
  ...singletonModules,
};
config.resolver.alias = {
  ...(config.resolver.alias ?? {}),
  ...singletonModules,
};

module.exports = config;

Those singleton aliases are important for sidecar Expo projects. Without them, Metro can resolve React Native files imported from the parent app against the parent app's node_modules while the sidecar resolves its own runtime against tempo/node_modules, which can produce duplicate React instances and invalid hook-call failures.

Launch Expo web with TEMPO=true:

TEMPO=true npx expo start --web

Tempo discovers the printed local URL and probes /tempo-host; no fixed port is required.


How it works

The Next.js plugin is a webpack-compatible loader (nextjs-loader.cjs) plus a higher-order config wrapper (tempoNextjsPlugin) that registers the loader for both Webpack and Turbopack from a single user-facing entrypoint.

The flow per source file:

  1. Resource gating — .tsx/.jsx only, exclude node_modules, require process.env.TEMPO.
  2. Read mtime via fs.statSync.
  3. Compute project-relative filepath via path.relative(rootContext, …).
  4. Call annotateFile from @modules/annotation with a callback that returns the data-tempo-* props for each JSX element. The walker visits every JSXElement, fragments are skipped, and props are inserted as JSX attribute strings at the end of each opening tag.
  5. If the file contains an intrinsic JSX <head>, splice <meta name="tempo-project-root" content="…" /> into the post-annotation source. The injected meta does not itself carry data-tempo-* attrs (it has no source location), which keeps every other element's data-tempo-position pointing at the byte offset of < in the on-disk source file.
  6. Return the annotated source. Errors at any step cause the loader to return the original source unchanged — correct or untouched, always.

The tempoNextjsPlugin wrapper:

  • Adds an enforce: "pre" rule under config.module.rules for Webpack (/.(tsx|jsx)$/).
  • Adds a *.{tsx,jsx} rule under nextConfig.turbopack.rules for Turbopack with condition: { all: [{ not: "foreign" }, "development"] } (excludes node_modules / Next internals AND production builds).
  • Chains an existing user-supplied webpack(config, options) function and merges with any existing turbopack.rules so other plugins are preserved.
  • No-ops entirely when process.env.TEMPO is not set.

See SPEC_NEXTJS.md for the complete design specification.


Demo

pnpm --filter @tempo-modules/tempo-sdk-nextjs-demo dev          # Turbopack on :3477
pnpm --filter @tempo-modules/tempo-sdk-nextjs-demo dev:webpack  # Webpack on :3478

Then:

curl -s localhost:3477/ | grep -oE 'data-tempo-filepath="[^"]+"' | sort -u

Should print one line per source file rendered on the home page.


Tests

pnpm test                       # all 308 tests across both plugins
pnpm test:nextjs                # 175 Next.js tests (~24s)
pnpm test:nextjs:unit           # fast unit tests, no servers
pnpm test:nextjs:integration    # live `next dev` against demo (Turbopack + Webpack)
pnpm test:nextjs:production     # `next build` correctness (no annotations in prod)

Test layers:

| File | Tests | Coverage | |---|---|---| | nextjs-loader.test.ts | 41 | Loader function — file filtering, default strategy, auto-inject, error handling | | nextjs-wrapper.test.ts | 26 | tempoNextjsPlugin — webpack chaining, turbopack merging, version detection, options | | nextjs-fixtures.test.ts | 40 | Project shapes — App/Pages Router, server/client components, all routing types | | nextjs-published.test.ts | 14 | Built dist/*.cjs and dist/*.js — CJS callable, ESM imports, parity | | nextjs-production.test.ts | 6 | next build produces zero data-tempo-* attributes | | nextjs-e2e-correctness.test.ts | — | Live next dev against demo (both bundlers); every recorded data-tempo-position actually points at < in the on-disk source file |


Related docs

  • SPEC.md — Vite plugin specification
  • SPEC_NEXTJS.md — Next.js plugin specification
  • SPEC_TSC.md — TypeScript type-analyzer (used by both plugins for data-tempo-supports-*)