Releases: C9up/inker
Release list
v0.1.21
Re-ship what 0.1.x lost to a stale tag
The published artifact was built from a commit older than the fix it
was meant to carry, because the release tag had not been moved onto it.
The code here is unchanged; only the version needed to advance so the
registry gets it.
Declare the Node floor this package already has
24 of the cohort's packages declared engines.node >=22.0.0 and 8 did
not. Seven of the eight depend on @c9up/ream, which declares it, so
their real floor was already 22 -- just invisible to npm. helix is
standalone but its CI builds and tests on Node 22 only.
Undeclared, a Node 20 install succeeds without a word and the failure
arrives later as a runtime error far from its cause.
Changes since v0.1.20.
v0.1.20
Publish the plugin registrar, so a package can reach the engine at any time
import inker from '@c9up/inker' now hands back something with use(), the
way the upstream engine's default export does. It queues; InkerProvider
drains the queue into the engine it builds, where the plugins keep the
deferral they would have had upstream — they run just before the first
render.
The singleton is the registrar and not the engine, because a Templates
takes its root at construction and the root is a containment boundary: an
engine created at import time would have nothing to be contained by. The
contract a caller sees is the same either way.
InkerProvider's constructor also sets app.usingInker, mirroring the
upstream engine provider's flag. Without it a contributing package has no
way to ask whether templates exist except through the container, and that
question has a lifecycle.
Release 0.1.20.
Read the artifact where cargo actually wrote it
The NAPI copy script looked under <package>/target unconditionally. Cargo
writes elsewhere whenever CARGO_TARGET_DIR is set — a shared cache, a CI mount,
a read-only external directory — so the build produced the library and then
failed to find it, or silently copied a stale one from a previous run.
CARGO_TARGET_DIR is honoured now, resolved against the package root when it
is relative, as cargo resolves it. All fourteen scripts had the same line; an
audit reported it in ream-mcp alone.
Verified end to end, not by reading: a real cargo build redirected to a
temporary directory, the artifact copied out of it, and the package suite green
on that binary.
Keep the dev-dependency alignment, drop the workspace: protocol
The internal ranges had been rewritten to workspace:^. That resolves inside
this monorepo and nowhere else: every package CI checks out its own repository
alone and runs pnpm install, where the protocol has no workspace to point at
and fails with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND before a single test runs. The
concrete ranges are back; the dev-dependency bumps that came with the same edit
are kept, and now match what the lockfile already resolved.
Changes since v0.1.19.
v0.1.19
fix(provider): release the renderer on shutdown, and let a restart rebuild
shutdown() was a deliberate no-op, so a late-arriving handler would not meet a
torn-down proxy mid-render. That reasoning does not survive the cases it
creates: the renderer left behind holds the OLD router, the old Rosetta, the old
templates and the old ALS context, and #started then refused to build a new
one. A hot reload or a second application in the same process kept rendering
through a dead graph with nothing to say so.
The window it protected is not real in a graceful shutdown either -- in-flight
requests drain before providers are torn down. What IS real is two applications
sharing a process, which is why this clears only the instance it bound: the
survivor's renderer has to stay.
Templates still owns its own cache and AsyncLocalStorage has no destroy
contract, so neither is ours to tear down.
Changes since v0.1.18.
v0.1.18
Keep the vendored copies out of this package's coverage floor
src/vendor/** is generated and identical in every package that carries it, so
measuring it here counts the same lines N times and holds this package to a
floor for code it cannot change — which is what pushed several suites under
their thresholds the moment the copies landed.
The behaviour is not left unmeasured: it is pinned where it broke, in bay's
quasar-bridge suite, which now covers both manager shapes the loader has to
accept.
Run cargo with --locked in CI
Without it, cargo rewrites Cargo.lock in place when it has drifted from the
manifests — so CI resolves dependencies fresh and tests a graph nobody
committed, then the release is built from it. The workspace lock had drifted
by 382 lines before the same flag caught it locally.
Every package here commits a Cargo.lock, so --locked is meaningful: it fails
loudly instead of silently updating. Verified against the current lock before
the flag went in.
The Node side is deliberately left alone: these repositories ship no
pnpm-lock.yaml, so --frozen-lockfile has nothing to freeze against, and
resolving from the registry is what a consumer gets anyway.
Take the NAPI platform table from the vendored copy
The map from Node's platform/arch to napi-rs's suffix was repeated here as
well. Adding a target means adding it everywhere, and a package that misses
the edit fails only on that platform.
The table, the path and the require come from scripts/vendor/nativeBinary.ts.
The policy does not: what this package does when the binary is absent, and the
error it raises, stay here — they carry this package's code and its build
instruction, which a shared helper has no business inventing.
Take nodeEnv from the vendored copy
The file was identical to the other packages' copies, line for line, and they
had drifted apart only in their comments — each explaining its own
consequence, which is what made three identical files impossible to diff.
It is generated from scripts/vendor/nodeEnv.ts now and never edited here; the
reason this package cares moved to the decision it explains. It cannot be a
dependency: this package is published and built from its own repository, where
nothing of the workspace exists.
Give the packing hook a timeout of its own
The beforeAll runs package: @c9up/inker@0.1.18
Tarball Contents
dist/augmentations.d.ts
dist/augmentations.d.ts.map
dist/augmentations.js
dist/augmentations.js.map
dist/globals.d.ts
dist/globals.d.ts.map
dist/globals.js
dist/globals.js.map
dist/helpers.d.ts
dist/helpers.d.ts.map
dist/helpers.js
dist/helpers.js.map
dist/identifierGuards.d.ts
dist/identifierGuards.d.ts.map
dist/identifierGuards.js
dist/identifierGuards.js.map
dist/index.d.ts
dist/index.d.ts.map
dist/index.js
dist/index.js.map
dist/InkerProvider.d.ts
dist/InkerProvider.d.ts.map
dist/InkerProvider.js
dist/InkerProvider.js.map
dist/InkerRenderer.d.ts
dist/InkerRenderer.d.ts.map
dist/InkerRenderer.js
dist/InkerRenderer.js.map
dist/InkerRenderError.d.ts
dist/InkerRenderError.d.ts.map
dist/InkerRenderError.js
dist/InkerRenderError.js.map
dist/loadNapi.d.ts
dist/loadNapi.d.ts.map
dist/loadNapi.js
dist/loadNapi.js.map
dist/native/generated.d.ts
dist/native/generated.d.ts.map
dist/native/generated.js
dist/native/generated.js.map
dist/nodeEnv.d.ts
dist/nodeEnv.d.ts.map
dist/nodeEnv.js
dist/nodeEnv.js.map
dist/renderNode.d.ts
dist/renderNode.d.ts.map
dist/renderNode.js
dist/renderNode.js.map
dist/SafeString.d.ts
dist/SafeString.d.ts.map
dist/SafeString.js
dist/SafeString.js.map
dist/services/main.d.ts
dist/services/main.d.ts.map
dist/services/main.js
dist/services/main.js.map
dist/stacks.d.ts
dist/stacks.d.ts.map
dist/stacks.js
dist/stacks.js.map
dist/Templates.d.ts
dist/Templates.d.ts.map
dist/Templates.js
dist/Templates.js.map
dist/testing/index.d.ts
dist/testing/index.d.ts.map
dist/testing/index.js
dist/testing/index.js.map
index.linux-x64-gnu.node
LICENSE
package.json
README.md
Tarball Details
c9up-inker-0.1.18.tgz, which builds and archives the package — a
minute's work on a loaded machine. It inherited the suite's per-test timeout,
sized for tests rather than for a build, so the hook failed under load and
passed when run alone.
Run clippy as a gate, not a note
cargo fmt --check was gating the formatting while nothing gated the
lints that catch a real defect — the one place the compiler stays silent
and clippy does not. Nine of the ten crates in this cohort had the same
hole; only one ran it.
No version change: this gates what is already there, and every crate
passes it today.
Changes since v0.1.17.
v0.1.17
Release 0.1.17
Declare the ctx.view the provider installs
InkerProvider.start() installs it as a context getter, seeded with the
request, so a controller renders without threading anything. The property
existed at run time and not for the compiler: every ctx.view.render(...)
was a type error in an application that had done nothing wrong.
ContainerBindings was already augmented, which is what made this easy to
miss — resolving container.make('inker') had a type, and the surface a
controller actually writes did not.
Not optional, unlike a middleware-attached property: the getter goes on
the context class itself, so an application that registered inker has it
on every request.
A type-only failure is invisible to a runtime suite, so it is asserted by
compiling a snippet the way an application would — including one that must
still be REFUSED, so the augmentation adds what the provider installs
rather than opening the context to anything.
Lint this package the way its own repository will
biome's configuration lived only at the workspace root, so lint in CI —
which runs from this package's own repository, with no such file — fell
back to biome's defaults and checked different rules from lint here.
What its own config surfaced: the Macroable stand-in is static-only
because the host's is, which is exactly what the provider duck-types.
Declare what CI has to install
Each package is its own repository: pnpm install there sees only this
file, so a dependency the workspace happened to hoist locally is simply
absent in CI. --coverage needs @vitest/coverage-v8 named here, and an
optional peer a test imports has to be a devDependency as well — optional
is exactly what keeps it from being installed.
Run the gates the package already declared
Three guard-rails were configured and never reached CI, so each one was a
gate nothing ran:
tsconfig.jsonincludestests, but CI typechecked only
tsconfig.build.json— every type a test relied on went unchecked.vitest.config.tsdeclares coverage thresholds, but CI ran plain
vitest run, which does not read them.lintpointed atsrc/alone, so no test file was ever linted.
CI now runs pnpm typecheck, pnpm test:coverage and a lint that covers
tests/ as well.
Changes since v0.1.16.
v0.1.16
Reformat what the strictness pass reflowed
Two files-worth of blank lines and one long call the formatter wraps
differently now that a helper sits above them. CI resolves biome from a
caret range and installs a newer one than the lockfile pins.
Turn on noUncheckedIndexedAccess
It was not missing here — it was explicitly false, in sixteen of the
seventeen tsconfigs. eon alone had it on, which is why nobody had seen
what it finds.
It stays a named deviation from upstream: @adonisjs/tsconfig sets
strictNullChecks and noImplicitAny but not this one. We keep it because
turning it on is what caught an as asserting a possibly-absent regex
group was a known value — the exact shape the flag exists to find. Doing
better than upstream is kept and written down, not reverted to parity.
Every site is restated rather than silenced: no !, no cast, no ?? 0
standing in for a branch that cannot happen. A reversed copy read by
value where an index walked a callback list backwards, the winner of a
scan kept as the value it found rather than its position, destructuring
where a length check was doing the proving, and an explicit break where a
loop condition already bounds the read.
Say what container.make() returns for the tokens this package binds
ream declares ContainerBindings open on purpose: it registers its own
entries and expects each package to contribute the ones it owns — its
comment on the interface names auth (warden), logger (spectrum) and db
(atlas) as exactly this. None of them did, and every other package that
binds a string token was in the same state, so container.make('cache'),
make('mail'), make('hash') and the rest all answered unknown and
every call site had to assert a type it could not prove.
Loaded from the barrel AND from the provider, the second of which is where
AdonisJS puts its own (providers/redis_provider.ts carries the
declare module for redis, database_provider.ts for lucid.db).
Verified live rather than assumed: a declare module naming a specifier
that does not resolve is silently inert, so renaming the member has to
break the compile. It does.
Release 0.1.16
Stop nl2br and classNames from emitting unescaped user input
Both marked their result as a SafeString, which means "already escaped, print
verbatim". Neither escaped anything, so {{ }} — the form that reads as safe —
emitted whatever was passed in as live markup.
{{ nl2br(comment) }}
comment = "hi\n<img src=x onerror=alert(1)>"
→ hi<br><img src=x onerror=alert(1)>
<i class="{{ html.classNames(c) }}">
c = 'a" onmouseover="alert(1)'
→ <i class="a" onmouseover="alert(1)">
nl2br is the worse of the two: it exists to print the multi-line text a user
typed, so its input is hostile by definition, and the output is a stored XSS on
any view that renders a comment, a description or an address.
Edge returns a plain string from both. There, {{ }} escapes it, and a caller
who wants markup types {{{ }}} — the unescaping is a choice the developer
writes out. Returning a SafeString reached that same output with no such choice
made anywhere.
classNames now matches Edge exactly: a plain string. Escaping changes nothing
about a legitimate class name, which contains no character that escapes.
nl2br keeps its SafeString but escapes the text before inserting the breaks —
a NAMED DEVIATION, and a hardening one. Edge's plain string would be escaped by
{{ }} and the <br> would never render, making the helper useless outside
{{{ }}}. Escaping first keeps the breaks working and takes the trap out.
html.safe() still passes through untouched, so the opt-in remains.
Move thiserror to 2
Move the NAPI bindings to napi 3
The Rust needed no change; the toolchain did. napi-derive 3 writes one type-def
file per crate into NAPI_TYPE_DEF_TMP_FOLDER and panics outright when it sees
the old single-file TYPE_DEF_TMP_PATH — that variable is how it detects an
out-of-date toolchain, so the failure reads as "upgrade @napi-rs/cli" even
though the generator here is our own.
It also emits a function as a bare function name(...) where 2 emitted the
signature alone, so concatenating the name onto it produced
function xfunction x(...). The generator handles all three shapes now.
napi-build stays at 2 — there is no 3 on crates.io.
Verified by what the migration could break rather than by it compiling: the
generated src/native/generated.ts comes out byte-identical to the napi 2 one,
and the native binary is rebuilt and exercised by the JS suite.
Changes since v0.1.15.
v0.1.15
Turn on noUnusedLocals/noUnusedParameters
It surfaced one thing here: InkerProvider kept the AsyncLocalStorage on a
private field that nothing read. The same instance goes to InkerRenderer, which
is the only thing that enters a context into it, so the field was a second
reference no caller could reach — the wiring is unchanged.
Release 0.1.15
Fold the unreachable then branch, keeping what it explained
The check above it already answers undefined for then, so the second one
never ran. Its comment was the part worth having — why then matters, not
just that it does — and it moves up to the branch that does the work.
Changes since v0.1.14.
v0.1.14
Format the Rust crates, and gate it so they stay formatted
Twelve of the thirteen crates had drifted — 949 differences in all, atlas
alone 345, and build.rs files that had never been through the formatter.
None of their workflows checked, so nothing ever said so; the drift only
surfaced when it took a publish job down.
cargo fmt applied throughout, and a cargo fmt --check step added to each
workflow so this cannot happen again. Formatting only: the Rust tests pass
unchanged in every crate.
One spot in atlas needed a real edit rather than the formatter: cargo fmt
rewrote a return Err(format!(…)) arm back to the inline form on every run
while --check kept asking for the block form, so the file could never
converge. The message is bound to a name, which fits the line budget and
settles it.
Release 0.1.14
Namespace every error code as E__
The convention was announced and not kept: 115 framework codes across ten
packages carried no E_ prefix, so an application filtering on the documented
rule handled some failures and missed others.
Blanket-prefixing them was the wrong fix, and trying it proved why — it made
E_FORBIDDEN mean three different things across ream, relay and warden, which
is worse than the inconsistency it replaced. The namespace after E_ is what
tells them apart.
Where a package already prefixed inside its error constructor — atlas, rune,
warden, and ream's module classes — the constructor now emits E__ and the
call sites stay bare, so the rule lives in one place per package instead of at
every throw. Each of those constructors passes through a code that already
starts with E_, which is how the upstream identifiers keep their exact
spelling: E_UNAUTHORIZED_ACCESS, E_INVALID_CREDENTIALS and E_VALIDATION_ERROR
are the ones a consumer branches on, and two packages naming the same upstream
failure legitimately share one.
Ignore a relocated cargo target
target/ matches a directory only. When the build output is moved elsewhere
and a symlink named target is left in its place, that pattern does not catch
it — it shows up untracked, and a stray git add -A commits a path that only
resolves on one machine.
Changes since v0.1.13.
v0.1.13
Read the NODE_ENV aliases when choosing the cache mode
auto means never re-read a template in production and watch mtimes
everywhere else. With NODE_ENV=prod read verbatim it answered 'not
production': every render stats the file, and a template edited in place on
a live box is picked up — the thing never exists to prevent.
Say plainly that a template is code, not input
Expressions are evaluated as JavaScript by design, at the same trust level
as the application's own source. Rendering a template whose TEXT came from a
user is remote code execution; user-supplied DATA is a different matter and
stays escaped. Stated at both eval sites and in the README.
Changes since v0.1.12.
v0.1.12
Release the work that landed after the last published version
The registry now carries the version this package.json was still on, so
everything committed since ships under the next one rather than
retroactively changing what a published version means.
Build the cross-platform matrix only when the version moves
The five-runner matrix exists to produce the prebuilt binaries a release
ships. It ran on every push to main, rebuilding artefacts nobody
downloads — five runners, every time, for a comment fix.
A version-gate job now compares the package version at HEAD^ with
the one at HEAD and the matrix runs only when they differ. Anything
that is not a push passes the gate unconditionally, so workflow_dispatch
— how a release is actually cut — is unaffected, and so is publish,
which still waits on the full matrix. A missing HEAD^ reads as a bump:
erring towards building is the safe direction.
The test signal deliberately does NOT move with it. quality and the
cargo/integration jobs were already independent of the matrix, but
vitest ran INSIDE it, so gating the matrix alone would have quietly
taken the TypeScript suite off every ordinary push. A ts-tests job now
runs it on ubuntu, building its own napi binary rather than waiting on a
gated artefact. On a push without a bump that leaves typecheck, lint,
cargo and vitest — one runner instead of five.
Drop the render types left behind by the Node-renderer pivot
NapiInvocation, NapiHelperResult and NapiRenderContext described
the helper-tape and render-context surface of the embedded QuickJS VM.
That surface went away when rendering moved to Node (62-2); the Rust no
longer exposes any of it, nothing in the package references them, and
the barrel does not re-export them. They only described something that
is not there.
Changes since v0.1.11.