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

@alwatr/embedded-data

v9.38.2

Published

Framework-agnostic utility to safely extract, parse, and validate embedded JSON data from <script type="application/json"> DOM nodes. Designed for SSR-friendly state hydration in SPAs and MPAs.

Readme

@alwatr/embedded-data

Framework-agnostic utility to safely extract, parse, and validate embedded JSON data from <script type="application/json"> DOM nodes.

Designed for SSR-friendly state hydration in SPAs and MPAs: the server renders initial state into a script tag, and the client reads it on boot without an extra HTTP round-trip.


Why This Package Exists

Modern web applications often need to pass server-side data to the client without making an extra API call. The standard pattern is to embed JSON in the HTML:

<script
  type="application/json"
  data-user-profile
>
  {"userId": 42, "name": "Ali"}
</script>

But extracting this data safely requires:

  1. SSR compatibilitydocument might not exist in Node.js/Bun server contexts.
  2. Error handling — the script tag might be missing, empty, or contain invalid JSON.
  3. Type safety — the parsed JSON might not match the expected TypeScript type.
  4. Memory management — the (potentially large) JSON string should be freed after extraction.

@alwatr/embedded-data encapsulates this entire pattern in a single, reusable class that follows the Single Responsibility Principle: it only handles DOM extraction and JSON parsing. Validation logic is externalized via an optional validator parameter (Open/Closed Principle).


Architecture

┌───────────────────────────────────────────────────────────┐
│              EmbeddedDataCollector<T>                     │
│                                                           │
│  constructor(attributeName, validator?)                   │
│    └─ stores attributeName_ and optional validator_       │
│                                                           │
│  collect(): T | null                                      │
│    ├─ getElement_() → querySelector script tag            │
│    ├─ extractRawData_() → read textContent, clear DOM     │
│    ├─ JSON.parse(rawData)                                 │
│    ├─ validator_?.(parsedData) → optional type-guard      │
│    └─ return validated data or null                       │
└───────────────────────────────────────────────────────────┘

Flow:

[HTML with <script data-foo>]
│
▼
getElement_() ──→ querySelector('[data-foo]')
│
▼
extractRawData_() ──→ element.textContent.trim()
│ element.textContent = '' ← GC hint
▼
JSON.parse()
│
▼
validator_?.(data) ──→ optional type-guard / Zod schema
│
▼
return T | null

Installation

bun add @alwatr/embedded-data
# or
npm install @alwatr/embedded-data

Usage

Basic Usage (No Validation)

import {EmbeddedDataCollector} from '@alwatr/embedded-data';

// HTML: <script type="application/json" data-config>{"apiUrl": "https://api.example.com"}</script>

const collector = new EmbeddedDataCollector<{apiUrl: string}>('data-config');
const config = collector.collect();

if (config) {
  console.log('API URL:', config.apiUrl);
}

With Type-Guard Validation

import {EmbeddedDataCollector} from '@alwatr/embedded-data';

interface UserProfile {
  userId: number;
  name: string;
}

function isUserProfile(data: unknown): data is UserProfile {
  return (
    typeof data === 'object'
    && data !== null
    && 'userId' in data
    && typeof (data as any).userId === 'number'
    && 'name' in data
    && typeof (data as any).name === 'string'
  );
}

// HTML: <script type="application/json" data-user>{"userId": 42, "name": "Ali"}</script>

const collector = new EmbeddedDataCollector<UserProfile>('data-user', isUserProfile);
const user = collector.collect();

if (user) {
  console.log(`User ${user.name} (ID: ${user.userId})`);
} else {
  console.error('Invalid user data');
}

With Zod Schema Validation

import {EmbeddedDataCollector} from '@alwatr/embedded-data';
import {z} from 'zod';

const userSchema = z.object({
  userId: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof userSchema>;

const collector = new EmbeddedDataCollector<User>('data-user', (data) => {
  const result = userSchema.safeParse(data);
  return result.success;
});

const user = collector.collect();

Lazy Initialization with @alwatr/lazy

Combine with @alwatr/lazy to defer extraction until the data is actually needed:

import {lazy} from '@alwatr/lazy';
import {EmbeddedDataCollector} from '@alwatr/embedded-data';

interface AppConfig {
  apiUrl: string;
  features: string[];
}

function isAppConfig(data: unknown): data is AppConfig {
  return typeof data === 'object' && data !== null && 'apiUrl' in data;
}

export const appConfig = lazy(() => {
  const collector = new EmbeddedDataCollector<AppConfig>('data-app-config', isAppConfig);
  return collector.collect();
});

// Later, when config is needed:
const config = appConfig.instance;
if (config) {
  console.log('API URL:', config.apiUrl);
}

Centralized Data Registry Pattern

For larger applications, create a centralized registry of all embedded data sources:

// src/data/embedded-data.ts
import {lazy} from '@alwatr/lazy';
import {EmbeddedDataCollector} from '@alwatr/embedded-data';
import type {Shop, Product, User} from '../types.js';

function isShopCollection(data: unknown): data is {items: Shop[]} {
  return typeof data === 'object' && data !== null && Array.isArray((data as any).items);
}

function isProductCollection(data: unknown): data is {items: Product[]} {
  return typeof data === 'object' && data !== null && Array.isArray((data as any).items);
}

function isUser(data: unknown): data is User {
  return typeof data === 'object' && data !== null && 'userId' in data;
}

export const embeddedData = {
  shops: lazy(() => {
    const collector = new EmbeddedDataCollector<{items: Shop[]}>('data-shops', isShopCollection);
    return collector.collect();
  }),

  products: lazy(() => {
    const collector = new EmbeddedDataCollector<{items: Product[]}>('data-products', isProductCollection);
    return collector.collect();
  }),

  currentUser: lazy(() => {
    const collector = new EmbeddedDataCollector<User>('data-user', isUser);
    return collector.collect();
  }),
} as const;
// Usage in components
import {embeddedData} from './data/embedded-data.js';

const shops = embeddedData.shops.instance;
if (shops) {
  console.log('Loaded', shops.items.length, 'shops');
}

API Reference

class EmbeddedDataCollector<T>

constructor(attributeName: string, validator?: (data: unknown) => data is T)

Creates a new collector instance.

  • attributeName: The HTML attribute used to query the script tag (e.g., 'data-config').
  • validator (optional): A type-guard function or validation function (e.g., Zod schema parser) to ensure runtime type safety. Must be synchronous.

collect(): T | null

Synchronously extracts, parses, and validates the embedded JSON payload.

Returns the validated data or null on any failure (missing element, parse error, validation failure).


Design Decisions

| Decision | Rationale | | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Attribute-based selector | More flexible than hardcoded IDs. Allows multiple script tags with different attributes on the same page. | | element.textContent = '' after extraction | Proactive memory management — the (potentially large) JSON string can be GC'd immediately after parsing. | | Externalized validation | Follows the Open/Closed Principle. The class is closed for modification but open for extension via the validator parameter. | | Sync-only validator | Keeps the API simple and synchronous. Async validation belongs in the business layer, not in the data extraction layer. | | SSR guard (typeof document === 'undefined') | Prevents crashes in Node.js/Bun server contexts where document is not available. | | Logger with optional chaining | Debug methods (logMethod, logMethodArgs) are stripped in production builds when ALWATR_DEBUG=0. |


Relation to Other Packages

| Package | When to use instead | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | | @alwatr/lazy | Defer extraction until the data is actually needed (combine with EmbeddedDataCollector). | | @alwatr/signal | Reactive state management — use EmbeddedDataCollector to hydrate the initial state, then manage updates via signals. | | @alwatr/fetch | Fetching data from HTTP APIs (not embedded in the DOM). | | @alwatr/local-storage | Persisting state across sessions in the browser (not server-rendered). |


HTML Setup

On the server side (SSR), render the JSON data into a script tag:

<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <!-- Embedded state for client-side hydration -->
    <script
      type="application/json"
      data-app-config
    >
      {
        "apiUrl": "https://api.example.com",
        "features": ["dark-mode", "analytics"]
      }
    </script>

    <script
      type="application/json"
      data-user-profile
    >
      {
        "userId": 42,
        "name": "Ali Mihandoost",
        "email": "[email protected]"
      }
    </script>

    <!-- Your app bundle -->
    <script
      type="module"
      src="/app.js"
    ></script>
  </body>
</html>

On the client side, use EmbeddedDataCollector to extract and validate the data:

import {EmbeddedDataCollector} from '@alwatr/embedded-data';

const configCollector = new EmbeddedDataCollector('data-app-config');
const config = configCollector.collect();

const userCollector = new EmbeddedDataCollector('data-user-profile');
const user = userCollector.collect();

License

MPL-2.0 © Alwatr