@ayco/astro-resume

中文文档
1.1.1发布于 2026/08/13MITES Module零依赖构建来源已验证近 30 天下载 1895

Type-safe server-to-client data in Astro

原始使用文档
文档目录14 节
  1. Install via npm
  2. Usage
  3. Type Safety
  4. Passing all Astro.props to client
  5. Serialize server data once, access everywhere
  6. Using a custom serializer and parser
  7. Escaping & XSS
  8. Errors & Warning in deserialize()
  9. About
  10. Trade-Off
  11. Development
  12. Upgrading to v1
  13. Road Map
  14. Reporting Issues

Astro Resume

Package information: NPM version Package information: NPM license Package information: NPM downloads

CI OpenSSF Scorecard Runtime dependencies

[!Warning] On v0? Upgrade. Every version before 1.0.0 writes serialized data to the page without escaping <, so a string containing </script> in your data closes the element early and the rest runs as HTML. If any of that data comes from user input, treat it as exploitable. Fixed in 1.0.0, whose only breaking change is a TypeScript one — see Escaping & XSS and Upgrading to v1.

[!Note] New in v1 — deserialize<T> now defaults to unknown instead of any, so a call without a type argument no longer type-checks when you read properties off the result. See Upgrading to v1.

Utilities for serializing data from the server for use in the client.

  1. Serialize - Astro component that takes id and data
  2. deserialize() - a function for use in the client that takes an id string and returns the data object

Install via npm

On your Astro project:

npm i @ayco/astro-resume

Requires Astro 4 or newer.

Usage

Serializing and deserializing basic primitive data

---
import Serialize from "@ayco/astro-resume";

const data = {
    hello: 'world',
}
---

<Serialize id="my-data" data={data} />

<script>
import {deserialize} from '@ayco/astro-resume';
const data = deserialize('my-data');
console.log(data) // {hello: 'world'}
</script>

Without a type argument data is unknown — enough to log it or pass it along, but not to read properties off it. Name the type to do that.

Type Safety

deserialize<T>() returns T, which defaults to unknown. You can define a type for the data and use it in the client script.

---
import Serialize from "@ayco/astro-resume";

const data = {
    hello: 'world',
    isOkay: true
}
// define the type of data to be serialized
export type Data = typeof data;
---

<Serialize id="my-data" data={data} />

<script>
import {deserialize} from '@ayco/astro-resume';

/**
* reuse the type in the client
* assuming this component's name is `ThisComponent.astro`
*/
import type {Data} from './ThisComponent.astro';

const data = deserialize<Data>('my-data');

console.log(data) // {hello: 'world', isOkay: true}
</script>

A custom parser is typed (serialized: string) => T, the same T the call returns. So deserialize<Data>('my-data', parse) checks that your parser produces Data, and deserialize('my-data', myParser) infers T from myParser instead of falling back to unknown.

Passing all Astro.props to client

If you need to make all the component props to the client script:

---
import Serialize from "@ayco/astro-resume";
export interface Props {
    hello: string;
    isOkay: boolean;
}
---

<Serialize id="preferences" data={{...Astro.props}} />

<script>
import {deserialize} from '@ayco/astro-resume';
import type {Props} from './ThisComponent.astro';
const {hello, isOkay} = deserialize<Props>('preferences');
console.log(hello, isOkay);
</script>

Serialize server data once, access everywhere

If you have shared data that needs to be initialized from the server and accessed in several places on the client-side, you can use Serialize once and deserialize in any number of Astro components as long as they are in the same page.

In this example, an appConfig object is built and serialized in index.astro and accessed in child Astro components.

In index.astro:

import Serialize from "@ayco/astro-resume";

const appConfig = {
    someClientSideKey: '1234hello',
}
export type AppConfig = typeof appConfig;
---

<Serialize id="app-config" data={appConfig} />
<Child />

In Child.astro:

<h1>I'm a child. I have access to the appConfig in index!</h1>
<GrandChild />
<script>
import {deserialize} from '@ayco/astro-resume';
import type {AppConfig} from '..pages/index.astro';
const data = deserialize<AppConfig>('app-config');

// ... do something with the app config
</script>

In GrandChild.astro:

<h1>I'm a grand child. I also have access to the appConfig in index!</h1>
<script>
import {deserialize} from '@ayco/astro-resume';
import type {AppConfig} from '..pages/index.astro';
const data = deserialize<AppConfig>('app-config');

// ... do something with the app config
</script>

Using a custom serializer and parser

You can bring your own custom serializer/parser if you want to opt for more complex data handling.

Here's an example of serializing data that JSON.stringify cannot (e.g., Date or BigInt) using Rich Harris' devalue:

---
import {stringify} from 'devalue';
import Serialize from "@ayco/astro-resume";
const data = {
    now: new Date(),
    age: BigInt('3218378192378')
}
export type Data = typeof data;
---

<Serialize data={data} id="my-data" use={stringify} />

<script>
import {parse} from 'devalue';
import {deserialize} from '@ayco/astro-resume';
import type {Data} from './index.astro';

const {age, now} = deserialize<Data>('my-data', parse);
console.log(typeof age); // 'bigint'
console.log(now instanceof Date); // true
</script>

Note that a custom serializer's output is written to the page as-is, so it has to do its own escaping — see below. devalue does. If it doesn't, Serialize refuses to render rather than shipping a breakout.

Escaping & XSS

Serialized data is written inside a <script> element, so a string like </script><img src=x onerror=alert(1)> in your data would otherwise close that element early and let the rest of it run as HTML.

Serialize prevents this by escaping every < in its output as \u003c. That is a valid JSON string escape, so nothing is lost — deserialize() gives you the original string back — but </script can never appear in the serialized payload.

<Serialize id="my-data" data={{evil: '</script><img src=x onerror=alert(1)>'}} />

<!-- renders as: -->
<script type="application/json" id="my-data">{"evil":"\u003c/script>\u003cimg src=x onerror=alert(1)>"}</script>

Two limits worth knowing:

  1. Custom serializers are not escaped for you. When you pass use, its return value is written to the page unchanged, so that a serializer emitting its own format is not corrupted. Use one that escapes < itself — devalue does.

    As a backstop, Serialize throws rather than rendering if the serialized output still contains </script, so an unescaping serializer fails loudly at render time instead of shipping an XSS. That check is a targeted one, not a full model of HTML script parsing — a serializer that escapes < is still the thing to rely on.

  2. This protects the payload, not what you do with it. Data you take out of deserialize() and put back into the DOM still needs the usual care — prefer textContent over innerHTML.

Errors & Warning in deserialize()

The deserialize() function may give you the following:

  1. ERR: No match found - there are no JSON scripts with the given ID
  2. ERR: Failed to parse data for <id> - a script was found, but JSON.parse (or your custom parser) threw on its contents
  3. WARNING: Multiple matches for <id> - there were multiple JSON scripts found with the same ID

Thrown errors keep the underlying failure as their cause, so the real problem is still there to inspect:

try {
  const data = deserialize<Data>('my-data');
} catch (err) {
  console.error(err.message); // astro-resume ERR: Failed to parse data for "my-data".
  console.error(err.cause);   // SyntaxError: Unexpected token ...
}

Serialize does the same on the server: if your data cannot be serialized it throws "Data unserializable" with the original TypeError — a circular structure, or a BigInt with no custom serializer — as the cause.

About

This is a quick and easy pattern to embed serialized information into your HTML and make it available in the client-side script. This doesn't require client-side JS and directly embeds the data on your HTML.

The Serialize component will write the data as JSON wrapped in a <script type="application/json"> element to hold the string.

The deserialize() function can then parse the value string for use in your client script.

There is also a pattern given in the Astro docs to use a Custom Element that takes a data- prop which properly protects the scope of your component. It requires client-side JS.

Trade-Off

Some other frameworks themselves will manage serialized information and the IDs for you, but we don't have access to this in Astro as we are not really shipping a framework to the browser.

That's nice and ideal (in my opinion), as we are aware of how the HTML is formed and what we are shipping to our users. The trade off is that we do have to manage things ourselves.

You have to manage the IDs (i.e., make sure they are unique) and understand that the deserialize() function will crawl the whole document incurring a minimal performance cost depending on how big your HTML is.

The IDs themselves can be any string. Ones that are not valid CSS identifiers — my.data, ns:data[0], 1data — are escaped with CSS.escape before the lookup, so they resolve rather than silently failing to match.

Development

This repository is a pnpm workspace: package/ is the published package and demo/ is an Astro site that consumes it.

pnpm install
pnpm dev      # run the demo site
pnpm lint     # eslint
pnpm test     # vitest (happy-dom for deserialize, Astro Container API for Serialize)
pnpm check    # astro check on the demo
pnpm release  # bumpp: bump package/package.json, commit, tag, push

pnpm lint, pnpm test and pnpm check are the three commands CI runs.

Upgrading to v1

The breaking change: deserialize<T> defaults to unknown instead of any.

// v0 — `data` was `any`, so this compiled and checked nothing
const data = deserialize('my-data');
data.hello.toUpperCase();

// v1 — name the type
const data = deserialize<Data>('my-data');
data.hello.toUpperCase();

Calls that only move the value around — logging it, handing it to something else — keep working untouched. Calls that reach into the result need a type argument or a narrowing check. A custom parser is now typed (serialized: string) => T rather than returning any, so passing one also gives T something to infer from.

Everything else in v1 is backwards compatible:

  • Security fix — the reason to upgrade even if you have no type errors. < in the serialized output is escaped as \u003c, so data can no longer break out of the <script> element. v0 did not escape it at all, so data containing </script> could run as HTML. This changes the bytes on the page, not the value you get back. See Escaping & XSS.
  • IDs that are not valid CSS identifiers are escaped with CSS.escape, so they now match instead of throwing "No match found".
  • A failed parse throws its own error naming the ID, with the parser's SyntaxError as cause, instead of the bare parser error.
  • Errors from Serialize carry the original error as cause. Previously it was passed as a second argument to Error() and silently dropped.
  • The astro peer dependency widened from ^6 to >=4, so the package installs on Astro 4, 5 and 6.

Road Map

See the TODO tracker for planned work items.

Reporting Issues

To report issues or request features, open a ticket on GitHub, or send a plain text email to ~ayoayco/[email protected]

版本与发布记录

@ayco/astro-resume 共发布 40 个版本,首次发布于 2023/07/16;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.1.1latest2026/08/132 个月前
1.1.02026/08/122 个月前
1.0.02026/08/083 个月前
0.4.62026/04/047 个月前
0.4.52026/04/047 个月前
0.4.42025/03/061.7 年前

关于 @ayco/astro-resume 的常见问题

@ayco/astro-resume 是什么?

Type-safe server-to-client data in Astro

如何安装 @ayco/astro-resume?

在项目目录运行 npm install @ayco/astro-resume,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:astro(>=4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ayco/astro-resume 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/ayco__astro-resume 是否存在。

@ayco/astro-resume 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ayco/astro-resume 可以商用吗?

@ayco/astro-resume 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ayco/astro-resume 的下载量有多少?

根据 npm 官方下载统计,@ayco/astro-resume 在 2026/10/08 – 2026/11/06 这 30 天内被下载 1,895 次,日均约 63 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ayco/astro-resume 还在维护吗?

最新版本 1.1.1 发布于 2026/08/13(2 个月前);至今共发布 40 个版本,其中过去 12 个月发布了 5 个;首次发布于 2023/07/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。