@data-vegle/serial
v1.4.1
Published
Serialize roblox-ts types into buffers.
Maintainers
Readme
@rbxts/serial
Serialize roblox-ts types into buffers.
Status: scaffolding only. The toolchain is set up and verified; the serializer itself is not written yet.
Toolchain
| Piece | Why it's here | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | roblox-ts 3.0 | TypeScript → Luau compiler | | Flamework 1.3 | its transformer exposes compile-time type metadata (user macros) | | Rojo 7 | syncs the compiled output into Studio | | Prettier | formatting, enforced in CI |
Flamework is the reason this project isn't a plain roblox-ts package. Its transformer can hand you a type's identity, a printed form of the type, and a generated runtime guard — all derived from a TypeScript type at build time. That is what the serializer templates will be generated from, instead of asking callers to hand-write a schema next to every interface.
Setup
npm install
npm run buildStudio-side tooling is pinned in rokit.toml:
rokit install # installs RojoScripts
| Script | What it does |
| --------------------- | ----------------------------------------------------------------------- |
| npm run build | compile src/ → out/ as a package |
| npm run watch | same, in watch mode |
| npm run build:place | compile as a game instead, emitting the include/ runtime |
| npm run place | build the place and pack it into serial-test.rbxl for Studio |
| npm run clean | remove out/ and include/ |
| npm run format | run Prettier over src/ |
build and build:place write to the same out/ directory, so build:place
cleans first. Run npm run build again before publishing.
Layout
src/ library source, compiled to out/
default.project.json Rojo project for the package itself
bench/ benchmark: its own roblox-ts project (src/ → out/) plus the Lune and Studio runners
bench/test.project.json Rojo place used to run the benchmark (and try the library) in Studio
flamework.build Flamework's identifier table — commit it, don't edit itflamework.build holds the stable ids the transformer hands out. It is tracked
in git and shipped in the published package on purpose; deleting it makes
previously generated ids drift.
Writing a type macro
Flamework user macros need two things: the plugins entry in tsconfig.json
(already there), and a @metadata macro JSDoc tag on the function. Without the
tag the transformer silently skips the call and the parameter arrives as
undefined.
src/index.ts has the minimal working example:
/** @metadata macro */
export function typeIdOf<T>(id?: Modding.Generic<T, "id">): string {
assert(id !== undefined, "typeIdOf<T>() was called without the Flamework transformer");
return id;
}Callers just write typeIdOf<PlayerState>() and the transformer rewrites it to
typeIdOf("@rbxts/serial:src/index@PlayerState"). Swapping "id" for "text"
or "guard" in Modding.Generic gives the printed type or a runtime type guard
instead — those are the hooks the buffer serializer will be built from.
Benchmark
bench/src/bench.ts serializes and deserializes a set of representative types
against the compiled library in out/ and prints bytes per payload, microseconds
per call and a round-trip check. It is a roblox-ts project of its own
(bench/tsconfig.json, compiled to bench/out/): every schema is generated by
the Flamework transformer from a real TypeScript type, and the library is imported
through the declarations in out/, so the benchmark exercises the package exactly
the way a consumer does. The compiled module is then required from plain Luau:
| Command | Where it runs |
| ---------------------- | ------------------------------------------------------------------------------ |
| npm run build:bench | compiles bench/src/ → bench/out/ (needs out/ from npm run build) |
| npm run bench | builds both, then Lune runs bench/lune.luau |
| npm run bench:lune | same, without rebuilding the library first |
| npm run bench:studio | builds serial-test.rbxl and runs bench/studio.server.luau through run-in-roblox |
| npm run test | builds both, then Lune runs only the test suite (bench/src/test.ts) |
| npm run test:lune | same, without rebuilding the library first |
Both runners execute the test suite before the benchmark and stop when a case
fails. The suite round-trips deliberately awkward values (NaN, negative zero,
NUL bytes, varuint boundaries, empty and table-keyed collections, blobs,
serializers and serialized payloads used as data) and asserts the error text of
invalid inputs; npm run test:lune -- <text> keeps only matching cases.
Both runners need rokit install (Lune and run-in-roblox are pinned in
rokit.toml). Pass a filter to keep only matching cases:
npm run bench:lune -- list. Pass native to run with Luau native code
generation (npm run bench:lune -- native); the results then go to
<version>-lune-native.md and must not be compared with interpreted files.
bench/test.project.json describes the Studio place and doubles as the Rojo
project roblox-ts uses to resolve the ../../out import: serial (out/) and
bench (bench/out/) sit side by side under ReplicatedStorage, so the compiled
bench reaches the library through script.Parent.Parent.serial. The Lune runner
mirrors that tree with fake instances. The compiled bench is loaded with a
stand-in for the roblox-ts runtime that only implements TS.import, so
bench.ts must avoid anything that needs RuntimeLib (try/catch, async,
generators, instanceof, spread).
A full Lune run writes its table to bench/bench_results/<version>-lune.md, keyed
on the version in package.json and stamped with the commit hash, so the numbers
for each release stay in the repository. The Studio run prints the same Markdown to
the Output window for pasting into bench/bench_results/<version>-studio.md.
Lune numbers are good for comparing one commit against another. Use the Studio
run for absolute numbers: Studio has a native vector type and Lune does not,
so Vector3/CFrame heavy cases are slower under Lune than they will be in game.
License
MIT
