Production-grade ExtendScript infrastructure for Illustrator-era JavaScript engines.
|
ESON ESB64 ESARR ESSTR ESCHARS ESHTTP ESTIMER |
ESPACK ESMIN ESOBF coming soon |
Also from the same team: ArcFit.dev, deterministic arc warp for Illustrator.
- Why ESPACK?
- Features
- Get the Release
- Installation
- Quick Start
- API
- Validation
- Performance
- Security Model
- Compatibility
- Engine quirks that shaped the design
- Development
- Repository layout
- Credits
- License
ExternalObject requires a file path — there is no in-memory DLL load. Every library that wants a native lane (esb64, eson, eschars) therefore has to ship a DLL next to the script, or materialize it at runtime. ESPACK is the materializer: a build-time packer + ES3 self-extracting loader so ExternalObject DLLs ship inside a single .jsx and materialize on disk at runtime, in the "1 + n" model:
- 1 shared esb64 accelerator DLL (vendored at
vendor/ESB64Native.dll, inlined as base64 into every bundle by default). It is unpacked once per system (%LOCALAPPDATA%\espack\ESB64Native_v<v>.dll) using the bundle's inlined ES3 base64 lane, then loaded viaExternalObjectand reused by every espack bundle on that machine. - n payload DLLs (
--embed, repeatable; eachname_v<version>.dllin the per-bundle cache dir%LOCALAPPDATA%\<bundle-name>\). Every payload is decoded by the accelerator (b64decodeToFile: native decode written straight to disk — NUL-safe, no string channel), so payload extraction is microseconds instead of ~140 ms of JSX-lane decoding.
Runtime order: start in ES3 mode → the inlined lane unpacks the accelerator (only if it is not already on the system) → the accelerator natively unpacks every payload → all DLLs load via ExternalObject. If the accelerator is unavailable (read-only cache, locked host), payloads fall back to the JSX lane transparently with the reason surfaced on ESPAK.lastError().
Accel-less bundles still get the native lane when the shared accelerator is already on the system. A bundle built with --no-accel discovers the canonical shared accelerator at %LOCALAPPDATA%\espack\ESB64Native_v1.dll (the exact path every accel-carrying bundle materializes) and reuses it for native payload decode — no composition change needed. If the file is absent, cannot be loaded, or is not the ESB64Native accelerator (no b64decodeToFile), payloads fall back to the JSX lane with the reason surfaced. The same fallback applies when an embedded accelerator cannot be extracted (e.g. read-only cache) but the shared one is already on disk. This is what makes merged/accel-less bundles fast on machines that have run any accel-carrying bundle.
- Single-file distribution: the packer inlines the shared accelerator and every payload DLL as base64 into one self-contained
.jsx— no$.evalFile, no#include, no references to DLL or vendor paths. Runtime always materializes the DLLs on disk (ExternalObjectrequires a file path; there is no in-memory load). - A complete esb64 ships inside every bundle, by construction. The JSX extractor IS the vendored esb64 runtime lane (
vendor/esb64-runtime.js— WHATWG-exactatob/btoa, the same code esb64 ships) — the bundle cannot exist without it, because it decodes its own payloads. Once the shared accelerator is materialized and loaded, the same codec is also available natively (b64encode/b64decode/b64decodeToFile). A script that evals an espack bundle already has base64 — use it (see API) before vendoring a second copy. - "1 + n" model: one shared esb64 accelerator unpacked once per system (reused by every espack bundle on the machine); every payload decoded natively by
b64decodeToFile— 3-10 µs instead of ~140 ms of JSX-lane decoding (measured, live). - Byte-exact BINARY I/O: file
"BINARY"reads/writes round-trip all 256 byte values including NULs (the surrogate window is a JS↔native string-channel problem, not a BINARY-file problem — verified). - Versioned, GC'd cache: extracted files are
<name>_v<version>.dll, extracted once, never deleted while loaded (locked until host exit); older versions are GC'd best-effort after a successful extraction, scoped per DLL name. - Arbitrary-file payloads (
kind=file):--embedaccepts any file — EXEs, PNGs, fonts, JSON, anything. The original extension is preserved in the versioned cache name (Tool_v1.exe), extraction is byte-exact through the same native/JSX lanes, and GC works identically. Non-DLL payloads are materialized viaESPAK.extract(i)/ESPAK.payloadPath(i);ESPAK.load(i)rejects them with a clearkind=fileerror instead of ever feeding a non-DLL toExternalObject. DLL payloads are unchanged and byte-compatible with v0.3.0. - Deterministic output: the emitted bundle contains no timestamps; identical input produces byte-identical output.
- The
attachcapability switch: the consumer pattern esb64 and eson use — the library's ES3 impls stay in place until the native lib is loaded and the native impls are built; any failure keeps the bundle in ES3 mode with the reason surfaced, never a thrown consumer-facing error. - Zero dependencies: the packer is Node ESM with no runtime deps; the emitted loader is ES3-clean.
esb64's own bundle is the accelerator-only form: --accel ESB64Native.dll
with no payloads. Its ES3 runtime is its own extractor (self-referential),
and once the shared accelerator is on disk/loaded, btoa/atob switch to
the native lane via ESPAK.attach. Parity policy (user-approved):
differential-corpus parity — verified live: the same 66-vector WHATWG
battery passes byte-identically in ES3 mode and in native mode
(esb64: npm run live-verify). Measured acceleration: btoa 16 K:
18.6 ms → 317 µs (58.5×); atob 48 K: 66.8 ms → 957 µs (69.8×).
Design constraint honored from the skill's measured channel rules: the
kTypeString return channel truncates at the first NUL, so a native atob
whose decoded output contains NULs cannot round-trip — the native lane
returns a kTypeUndefined sentinel for NUL-containing outputs and the facade
falls back to the ES3 lane transparently (e.g. atob("AA==") still yields
"\u0000" exactly). Payload extraction avoids the channel entirely via
b64decodeToFile.
Build the accelerated bundle with: cd esb64 && npm run native-build && npm run build:accel → esb64/dist/ESB64.accel.jsx (self-extracting single
file; accelerator in the shared %LOCALAPPDATA%\espack\ dir).
eson ships as dist/ESON.accel.jsx: ESONJson.dll is the espack payload
(+ the shared esb64 accelerator), the bundle auto-enables ESON's native
parse gate with the espack-provided lib (ESON.useEspack(), outcome on
ESON.espack). Live-verified on Illustrator 30.6.0 (cd eson && npm run build:accel && npm run accel-live): gate enabled + certified (71 cases),
gate-ON vs gate-OFF verdict parity on 28 valid/invalid cases, byte-exact
extraction. Measured: gate-ON parse ≈ 191 µs vs gate-OFF ≈ 179 µs at
49 KB — parity-speed (ESON's current pre-scan is ~3.7 µs/KB; the historical
14-17× native win was against the old pre-scan) — the gate's value is the
certified RFC-exact native verdict + single-file delivery.
The integration also fixed an ESON core bug the live runs exposed: the ES3
engine truncates property names at U+0000, so ESON's parse memo collided on
raw-NUL texts (the invalid case {"a":1}\u0000,1 poisoned parses of the
valid {"a":1}). Fixed in eson/src/parse.ts (NUL texts never memoized).
All production bundles ship as GitHub release assets — this repo holds the packer. Grab the runnable demo bundle from the Releases page.
How it works, in three steps:
- Open the Releases page.
- Pick the latest stable tag (top of the list — today that is
v0.4.0). - Download the asset that matches your use case:
| You are... | Take this release | And this asset |
|---|---|---|
| Trying the 1+n model end-to-end | Latest stable | ESPAK-demo.accel.jsx — runnable demo bundle with the shared accelerator |
| Building your own bundle | Latest stable | Build from source: node espack-build.mjs (this repo holds the packer, not release bundles) |
The packer is a build-time tool, used from Node:
git clone ... # or copy the espack/ folder from the family checkout
cd espack
npm install # zero runtime deps; devDeps only for tests
npm run vendor-sync # refresh vendor/ from the sibling esb64 build (optional)node espack-build.mjs --embed path/to/MyDll.dll --embed OtherDll.dll=2 \
--embed path/to/Tool.exe --embed path/to/logo.png \
--out dist/MyBundle.jsx --name mybundleEmits dist/MyBundle.jsx — a single self-contained file. Drop it into any
Illustrator/ExtendScript host (File > Scripts, $.evalFile, or
COM/automation eval --file); on first eval it materializes the shared
accelerator (once per system) and the payload DLLs (once per bundle), then
loads them via ExternalObject.
ESPAK composes several bundles into ONE at build time — the model the family uses to ship eson + esarr + arcfit as a single composed file with one loader, one shared accelerator, and N payloads.
espack-build.mjs --manifest-out <path> emits a deterministic sidecar
manifest (fixed key order, no machine paths):
{
"format": "espack-manifest",
"version": 1,
"bundleName": "eson",
"cacheDir": "",
"chunkSize": 24576,
"accel": { "name": "ESB64Native", "version": "1", "len": 9728, "b64": "...", "fileName": "ESB64Native_v1.dll" },
"payloads": [ { "name": "ESONJson", "version": "1", "len": 103424, "b64": "...", "fileName": "ESONJson_v1.dll" } ]
}accel is null for --no-accel bundles. The manifest is the merge input;
it is never loaded at runtime.
espack-merge.mjs --merge <m1.json> <m2.json> ... --out <bundle.jsx> reads
the manifests and re-renders ONE loader with ONE shared accelerator and N
payloads. Merge happens pre-minify/pre-obfuscate — the merged bundle is
the normal deterministic output, so minified/obfuscated variants are produced
by the same downstream pipeline as any other bundle. Collision policy (hard
errors, not warnings):
- same payload name+version+same b64 → dedupe (keep one)
- same payload name+version+different b64 → HARD ERROR
- same payload name, different version → keep the higher integer version (non-integer versions → HARD ERROR)
- accel: all manifests' accel must be identical (name+version+len+b64) or
HARD ERROR;
nullaccel is allowed (merged bundle accel-less → warn — the loader still discovers the shared accelerator on the system) - merged bundle name defaults to the FIRST manifest's bundleName; its cache dir is reused, so payloads already extracted there are skipped
Four layers:
- Runtime — one loader per composed file (the emitted
ESPAKfacade). - Tooling —
espack-build --manifest-out+espack-merge. - Consumers — manifest + loader-free facade artifacts; adapters load by
payload NAME (
ESONJson/ESARRArray/ArcFit_IPC), neverload(0). - Composer — the build script (e.g. arcfit
build.mjs) runsespack-merge, then appends the consumer facades.
Facade ordering: $.global.ESPAK is last-wins — the merged bundle must be
evaluated LAST in the injection order so its facade (all payloads) is the
active one. Cache migration: payloads move from %LOCALAPPDATA%\<old-bundle>
to the merged dir; old extracted files become stale but harmless (GC is
scoped per DLL name inside the merged cache dir only, so the merged bundle
never deletes the old bundles' files).
ESPAK has two API surfaces: the build-time packer CLI and the runtime
ESPAK facade the emitted bundle defines.
node espack-build.mjs --embed <dll>[=<ver>] [--embed <dll2>[=<ver>] ...] \
--out <bundle.jsx> \
[--name <bundle-name>] # cache-dir segment; default = out stem
[--dll-version <v>] # default version for --embed payloads
[--cache-dir <abs>] # override for the per-bundle payload dir
[--accel <dll> | --no-accel] # default: vendor/ESB64Native.dll
[--accel-version <v>] # shared accelerator version (v1, v2, ...)
[--accel-dir <abs>] # override for the shared accel dir
[--standalone] # prepend `#target illustrator`
[--quiet]
Environment: ESB64_RUNTIME_PATH overrides the inlined atob lane source;
ESB64_ACCEL_PATH overrides the accelerator. Output is deterministic (no
timestamps). The emitted bundle is self-contained: no $.evalFile, no
#include, no references to the DLL or vendor paths.
The bundle defines the global ESPAK (installed on $.global so COM-eval'd
bundles persist across DoJavaScript calls):
ESPAK.config; // { bundleName, cacheDir, chunkSize,
// accel: { name, version, fileName, len, dir } | null,
// payloads: [{ name, version, fileName, len }] }
ESPAK.load(i); // load payload i (index or name; default 0); extract-if-
// needed -> new ExternalObject; { ok, mode, lib, path }.
// Rejects kind=file payloads (not loadable DLLs).
ESPAK.extract(i); // { ok, lane: "native"|"jsx"|"skip", path } - materialize any
// payload (DLL or arbitrary file) to the cache dir
ESPAK.isExtracted(i); // file exists && size matches (size + versioned name)
ESPAK.mode(); // "es3" | "native"
ESPAK.lastError();
ESPAK.accelReady(); // accelerator loaded (only needed for extraction)
ESPAK.accelExtractMs(); // JSX-lane accelerator extraction µs (-1 = skipped/absent)
ESPAK.nativeExtractMs(); // native payload extraction µs (-1 = JSX lane used)
ESPAK.extractMs(); // last payload extraction µs (either lane)
ESPAK.loadMs();Cache directories: payloads in %LOCALAPPDATA%\<bundle-name>\, the shared
accelerator in %LOCALAPPDATA%\espack\ (per-user, writable — verified from
JSX; per-segment directory creation is used). Extracted files are
<name>_v<version>.dll, extracted once, never deleted while loaded
(locked until host exit). After a successful extraction of a newer
version, older <name>_v*.dll files are GC'd best-effort (locked files fail
silently and survive until the host exits — verified live). GC masks are
scoped per DLL name, so different DLLs in the same directory never touch
each other. Loaded libraries are cached on $.global.__ESPAK_LIBS__ keyed
by path.
--embed accepts any file, not just DLLs. Non-DLL payloads are tagged
kind=file: the original extension is preserved in the versioned cache name
(Tool_v1.exe), extraction is byte-exact via the same native (b64decodeToFile)
or JSX lanes, and GC works identically. They are never handed to
ExternalObject — use extract() + payloadPath() instead:
var x = ESPAK.extract('Tool'); // { ok: true, lane: "native", path }
if (x.ok) {
var exe = new File(x.path); // Tool_v1.exe in the cache dir
exe.execute(); // spawn it (Windows)
}
var r = ESPAK.load('Tool'); // { ok: false, error: "...kind=file..." }
// load() rejects kind=file payloads with a clear error - no ExternalObject.Typical use: ship a freestanding helper EXE (or a logo PNG, a font, a JSON
asset) inside the same self-extracting bundle as your DLLs — the Scripts
folder stays .jsx-only and everything materializes on first use. DLL
payloads are unchanged and byte-compatible with v0.3.0 (no kind is emitted
for them in bundles or manifests).
Every bundle carries esb64 (see Features): the WHATWG-exact ES3 lane inlined as the extractor, and — once the shared accelerator is on disk and loaded — the same codec natively. Two ways to reach it:
// 1. direct native calls, once the lib is loaded:
var r = ESPAK.load(0);
if (r.mode === "native") {
var b64 = r.lib.b64encode("Hello World"); // "SGVsbG8gV29ybGQ="
var raw = r.lib.b64decode(b64); // "Hello World"
}
// r.lib.b64decodeToFile(b64, path) writes bytes straight to disk (NUL-safe)// 2. the capability switch — full atob/btoa contract, ES3-first:
ESPAK.attach({
es3: { atob: myEs3Atob, btoa: myEs3Btoa },
buildNative: function (lib) {
return { atob: function (s) { return lib.b64decode(s); },
btoa: function (s) { return lib.b64encode(s); } };
}
});
// mode "es3" keeps your impls; mode "native" swaps in the lib-backed onesIf you need a ready-made full facade, esb64's own accelerated bundle
(ESB64.accel.jsx, shipped in esb64 v1.1.0) is the reference
implementation of this exact pattern.
The core pattern for consumer libraries (esb64 first — see above):
var result = ESPAK.attach({
es3: { atob: es3Atob, btoa: es3Btoa }, // the library's fallback impls
buildNative: function (lib) { // returns native impls
return { atob: function (s) { return lib.b64decode(s); },
btoa: function (s) { return lib.b64encode(s); } };
},
onMode: function (mode, lib, impl) { ... } // swap point
}, 0); // payload index (default 0)
// result = { ok, mode, lib, impl, path, error } — impl is the active setOrder: start in ES3 → load() → on success build the native impls → swap;
on any failure stay in ES3 with a surfaced lastError. attach never
throws to the consumer.
| Check | Command | Result |
|---|---|---|
Packer units + loader logic in a vm sandbox (stubbed File/Folder/ExternalObject/$) — 1+n sharing, shared-accel discovery, chunk-boundary mirror, failure paths |
npm test |
33/33 + vendor-sync drift guard |
| Live end-to-end on Illustrator via the COM tool — 1+n extraction, sharing, skip-extract, version bumps, GC across sessions, failure paths, multi-payload load-by-name, merged-bundle accel dedupe, cache migration, facade ordering | npm run e2e |
60+ live checks (requires COM tool + an automation instance) |
| Vendored esb64 artifacts match upstream (runtime lane + accelerator) | npm test (vendor-sync guard) |
drift-guarded |
The e2e suite runs: fresh system → v1 (accel JSX extraction + native payload extraction + smoke + byte-exact) → skip-extract re-run → v2/v3 bumps (accel never re-extracted; locked versions survive GC) → failure path (blocker cache dir → clean es3 fallback) → fresh instance (GC removes all stale versions) → 1+n cross-bundle sharing (lib1 unpacks the shared accel, lib2 reuses it untouched, both payloads byte-exact).
All measurements live on Illustrator 30.6.0 / ExtendScript 4.5.6 via COM (2026-08-07). Medians of 3 fresh inputs per size.
| Input | 16,384 | 24,576 | 32,768 | 49,152 | 65,536 |
|---|---|---|---|---|---|
| decode µs | ~16,000 | ~25,000 | ~32,000 | ~49,000 | ~65,000 |
Linear, no engine wedge through 64 K (the encode-path wedge threshold is 128 K — decode is a different implementation and must not be assumed to transfer; measured). Chunk = 24,576 chars (≈25 ms/pass).
| Step | Lane | Measured |
|---|---|---|
| Accelerator extraction (first ever on a system) | JSX (chunked atob) | ~151-168 ms |
| Payload extraction (any bundle, any version) | native b64decodeToFile |
3-10 µs |
| Second bundle on the same system | accel skipped (exists + size match) | 0 µs |
| Load (skip-extract path) | — | ~1.2-1.9 ms |
Full DLL payload decode check (99,328 bytes → 132,440 base64 chars): chunked
JSX ~132 ms across 6 chunks of 24,576; window-sums matched Node exactly;
extracted files verified byte-exact vs the source DLL (Buffer.equals).
- 512-unit round trip of bytes 0x00–0xFF ×2: byte-exact,
firstMismatch = -1. - 98,304-unit round trip: file length 98,304, window sums + samples matched Node, byte-exact. NULs survive; the surrogate-window caveat does NOT apply to BINARY file I/O (it is a UTF-8 string-channel issue).
Folder.create() on nested missing parents succeeded live (contradicts the
documentation claim that parents are not created — the loader uses
per-segment creation anyway for cross-version robustness). Writes and file
lengths verified.
| Question | Decision | Evidence |
|---|---|---|
| Chunk size | 24,576 | linear decode; ~25 ms/pass; ≪ wedge threshold |
| Write strategy | native decode-to-file; JSX per-chunk writes as fallback | byte-exact (both) |
| Integrity | size + versioned name | cheap; partial writes caught (verified: wrong-size file triggers re-extraction) |
| Cleanup | versioned names, GC best-effort, never delete loaded | locked files survive until host exit (verified live) |
| Cache dirs | payloads %LOCALAPPDATA%\<bundle>\, accel %LOCALAPPDATA%\espack\ |
writable, per-user |
ESPAK extracts and loads native DLLs at runtime, so the trust surface is concrete and bounded:
- No remote fetch, ever. Everything that materializes on disk comes from
the bundle's own embedded base64 (build-time inlined from local files).
The extractor is either the bundle's inlined ES3 base64 lane or the
embedded accelerator's
b64decodeToFile— no other code runs during extraction. - Per-user, writable cache only. DLLs land in
%LOCALAPPDATA%\<bundle-name>\and%LOCALAPPDATA%\espack\— no elevation, no writes outside the user profile (verified from JSX; per-segment directory creation is used). - Extraction is verified before use. Integrity is checked by file size + versioned name; a wrong-size file (partial/corrupt write) triggers re-extraction and is never loaded.
- Versioned + GC'd. Old versions are removed only after a successful extraction of the newer one, best-effort, scoped per DLL name; a loaded DLL is never deleted (the host locks it until exit).
- Fail-open posture. If the accelerator is unavailable (read-only cache,
locked host, missing
ExternalObject), payloads fall back to the JSX lane transparently with the reason surfaced onESPAK.lastError()— the bundle degrades to pure ES3, it never executes a half-extracted DLL. - No code execution beyond the embedded payloads. The DLLs that load are
exactly the ones the packer embedded at build time;
attachswaps in native impls only afterload()succeeds.
| Target | Status |
|---|---|
| Windows x64 (PE64 DLLs) | Required — ExternalObject loads local files only; no macOS scope |
| Adobe Illustrator 30.6.0 | Verified live (extraction, load, attach, GC, failure paths) |
| ExtendScript ES3 (the emitted loader template is ES3-clean) | Bundled |
| Node.js (ESM, no deps) | Build-time packer + test harnesses |
Measured on Illustrator 30.6.0 (ExtendScript 4.5.6) — see Performance:
- Pure-JSX per-unit string loops wedge the engine at ≥128 K input; a 90 KB DLL is ~132 K base64 chars — the runtime decode must be chunked.
- The esb64
atoblane decodes linearly (~1 µs/base64 char, no wedge through 64 K passes) — chunking to 24,576 chars per pass is conservative. - File
"BINARY"I/O round-trips all 256 byte values byte-exact (incl. NULs; the surrogate window is a JS↔native string-channel problem, not a BINARY-file problem). - The JSX lane is a one-time system cost: the accelerator (~155 ms) is decoded once per machine; every payload afterwards decodes natively in ~5-10 µs (measured end-to-end inside Illustrator).
- A loaded DLL stays locked until the host exits (LNK1104) — hence versioned filenames + best-effort GC of older versions.
npm install # devDeps only (esbuild + typescript for tests/build)
npm run build # bundles src/loader.jsx -> dist bundles via espack-build.mjs
npm test # 33 Node tests (packer + vm loader logic incl. 1+n sharing +
# shared-accel discovery + chunk mirror) + vendor-sync drift guard
npm run e2e # 50+ live checks on Illustrator (requires COM tool + an
# automation instance; kills stale automation instances first)
npm run vendor-sync # refresh vendor/ from the sibling esb64 build
vendor/ carries the two esb64 artifacts every bundle needs, so espack
builds standalone (no sibling-repo dependency):
| File | Source (upstream esb64 build) | Purpose |
|---|---|---|
vendor/esb64-runtime.js |
esb64/dist/vendor-esb64-runtime.js |
The inlined atob/btoa lane (the JSX extractor) |
vendor/ESB64Native.dll |
esb64/native/bin/ESB64Native.dll |
The shared WHATWG-exact accelerator (b64encode/b64decode/b64decodeToFile) |
Refresh: npm run vendor-sync (copies from ../esb64, or ESB64_RUNTIME_SRC
/ ESB64_NATIVE_SRC). npm test includes the drift guard
(tests/vendor-sync-test.mjs).
| Path | Purpose |
|---|---|
espack-build.mjs |
The packer (Node ESM, no deps): embed(s) + accel → single-file JSX. build() is exported for tests |
src/loader.jsx |
The ES3 loader template (__TOKENS__ replaced at build time) |
vendor/ |
Vendored esb64 artifacts: esb64-runtime.js (the inlined atob lane) + ESB64Native.dll (the shared accelerator) — refreshed via npm run vendor-sync, drift-guarded by tests/vendor-sync-test.mjs |
tests/espack-test.mjs |
Node-side suite: packer units + loader logic in a vm sandbox (stubbed File/Folder/ExternalObject/$) + chunk-boundary mirror |
tests/espack-e2e.mjs |
Live end-to-end on Illustrator via the COM tool (1+n extraction, sharing, skip-extract, version bumps, GC across sessions, failure paths) |
probes/espack-measure-prep.mjs |
The measurement generator that produced the evidence below |
dist/ |
Built bundles (gitignored) |
ESPAK stands on the shoulders of the ExtendScript community:
- docsforadobe and the docsforadobe.dev community: maintainers of the de-facto reference documentation for the ExtendScript runtime and the
ExternalObjectinterface this library is built on. - esb64: the vendored runtime lane and the shared
ESB64Native.dllaccelerator that make extraction fast — the first consumer of this library in its own right. - eson and eschars: the peer libraries the 1+n model was proven against live (the second consumer, and the native-DLL channel-rules research).
- The ArcFit family: the freestanding-native-build and measured-channel-rules culture this library's design inherits.
GPL-3.0-or-later (family convention; identical text to eson/esb64). See LICENSE.
ESPACK: ExtendScript Packer. Built for the engine, measured on the engine, single-file by construction.