Releases: C9up/photon
Release list
v0.1.20
Build against the published ream, require 0.2.26 at run time
The devDependency pinned ^0.2.26, which is not on npm, so pnpm install
failed before a single test ran and CI could not say whether the code was
good. Nothing here imports a symbol that 0.2.26 added: the only reference
is the @c9up/ream/types module augmentation, which 0.2.25 carries.
The peer stays ^0.2.26 — that is where configure()'s makeUsingStub
requirement belongs — and the caret picks 0.2.26 up on its own once it is
published.
Generate the config from a stub
The file this package writes lived as a template literal inside its own
TypeScript — every backtick and every ${ escaped, and no way for an
application to change it without forking the package.
It is a stub now, read through codemods.makeUsingStub, the same route
ream-cli has always used for the make: generators. An application that
publishes stubs/<path> gets its copy instead, and the generated file is
byte-identical to what the literal produced.
The ream peer moves to ^0.2.26: that is the release the codemod appears in.
Changes since v0.1.19.
v0.1.19
release: photon 0.1.19
Fixes the router's fallback tests, which CI failed and every local run passed.
The tests stub location.href assignment with a Proxy over
window.location. jsdom 30.1.1 tightened the WebIDL brand check on Location,
so reading origin through that proxy now throws
'get origin' called on an object that is not a valid instance of Location —
the router threw while resolving the URL and never reached the fallback the
tests assert on. A get trap forwarding reads to the real Location, with
target as the receiver and methods bound to it, keeps the brand intact.
It only ever failed on CI because CI installs with --no-frozen-lockfile and
resolved 30.1.1 under the ^30.0.1 range while the lockfile here held 30.0.1.
The range now starts at 30.1.1, so a local run reproduces what CI runs.
The three assertions also waited a fixed two turns of the event loop for an
async fallback to land. waitFor waits for the condition instead: how many
microtask hops a fetch, a response read and a rejected parse take is an
implementation detail, and counting them wins on a fast machine and loses
elsewhere.
Require Node 24, and build the crates for production
Node 24, not because it is the current LTS — that is AdonisJS v7's own
stated reason and it is not one for us, since 22 still receives security
fixes until 2027, npm 11 is irrelevant under pnpm, and node:sqlite is
not what atlas uses. The reason is measurable and it is the framework's:
AsyncLocalStorage is on the request hot path, and Node 24 backs it with
AsyncContextFrame by default — 0.61 us per request instead of 1.55 us on
that exact pattern. Before 24 the same mechanism sat behind an
experimental flag, and a framework cannot base its performance on a flag
the application has to remember to pass. The reason travels with the
constraint, in a "//engines" key beside it.
Where there are crates: the default release profile leaves lto = false
and codegen-units = 16, so nothing inlines across crate boundaries —
and here the hot loop and the N-API binding that calls it are always two
different crates. Measured on atom, a scalar call through the binding
went from 18.85 ms to 13.59 ms for 50 000 operations. No panic = "abort": napi-rs catches panics and turns them into JavaScript
exceptions.
CI moves to Node 24 with them, since that is what the packages now ask
for.
Changes since v0.1.18.
v0.1.18
Type the context a pages predicate is handed
It was unknown, so a migrator could not write ctx.request.header(...)
without an assertion — the capability was there and unusable. The decoupling
argument for that did not hold: photon already needs ream's types to compile,
for the ctx.photon augmentation.
It is NOT ream's HttpContext either, though. The middleware works on a
structural shape and passes that; naming the framework class would promise
members it never hands over — the compiler said so the moment it was tried.
SsrConfig takes the shape the middleware actually serves, and stays generic
for a host that drives the renderer with a context of its own.
The concurrency test was sequential. It runs both requests through Promise.all
now, and says plainly that no mutation can fail it: with a per-call argument the
property is structural, and the test exists to catch a rewrite that moved the
context onto the renderer.
Hand the ssr.pages predicate the request it is deciding about
Upstream passes the HTTP context to that callback; photon passed the component
name alone and pointed callers at the middleware instead. That is not the same
capability: middleware owns the whole response, where this owns only whether
THIS component is server-rendered — "SSR for crawlers, hydrate for everyone
else" needs the second.
The stated reason for the deviation does not hold up. The renderer being a
shared singleton says nothing about what a per-call argument may carry, and the
context photon builds is already per request. Nothing is stored on the
renderer: the context is threaded through the call and two concurrent requests
cannot see each other's, which is what one of the tests checks.
Four mutations plus a fifth that fell nothing at first — the middleware
wiring was the uncovered caller, because every test built the context by hand.
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.17.
v0.1.17
Server-render in development, through Vite
Development emitted an empty <div id="app"> shell and let the client hydrate
it: boot() returned early because there was no built SSR bundle to load, and
loading the source through Node would have skipped every transform the
framework plugins apply — JSX, .vue, .svelte. So the one thing this package
exists for could not be exercised in the environment developers work in, and a
server-rendering bug only showed up in production.
Vite's own SSR loader applies those transforms. Photon now starts one in
middleware mode — it serves nothing, it only compiles — and fetches the entry
on EVERY render rather than caching it, so an edit to a page component shows
without restarting the process.
Vite is an optional peer. Three things keep this from becoming a cost anyone
pays involuntarily: without vite installed, dev falls back to the shell exactly
as before; the compiler starts only when entryServer actually exists, so a
client-only project never pays for a module graph and a watcher; and a
vite.config that throws degrades to the shell with a warning rather than taking
the dev server down.
close() releases the compiler — it holds a watcher, and leaving it running
keeps the process alive past a shutdown signal.
Tested both ways: a fake compiler for the wiring, and a real Vite instance for
the parts only Vite can answer — that createServer accepts these options,
that ssrLoadModule wants a root-relative URL, and that an edit to a
transitive import is actually picked up. Five mutations, each felling its test.
fix(renderer): resolve the build against the application root, not the cwd
path.resolve(process.cwd()) is only right when the process happens to have
started in the application's own directory. Under systemd, from the root of a
monorepo, or from anywhere a deployment finds convenient, photon looked for the
manifest and the SSR entry somewhere else -- and then served an unhydrated shell
rather than saying it had found nothing, which reads as a hydration bug for as
long as it takes to discover there was never a manifest.
The provider passes app.makePath(), the way bay already does for job
discovery, and an explicit appRoot in the config still wins: a deployment that
lays the build out elsewhere said so on purpose.
makePath stays optional on the context. photon is agnostic, so a host with no
notion of an application root keeps the cwd-relative lookup it had.
Name the CI workflow after the package
Every workflow already declared name: <pkg>-ci inside — twenty-eight of
twenty-nine — while the file was ci.yml almost everywhere and
<pkg>-napi-ci.yml in three places, where the -napi said nothing: half the
packages with a Rust engine did not carry it.
The file now matches the name it has always had, so one rule covers every
repository and the publish command no longer depends on remembering which
three were spelled differently.
GitHub keys run history by file path, so the runs recorded under the old name
stay reachable under it and this workflow starts a fresh history.
Drop a doc example that used an API atlas no longer has
Changes since v0.1.16.
v0.1.16
Take back the version number nothing was published under
Two releases were cut on top of each other without either reaching the
registry: the first tag was never published, and the second incremented
past it instead of folding the work in and moving the tag. That burns a
version number and leaves a tag pointing at something nobody can install.
The registry is at the version below this one, so 0.1.16 is the next release —
one tag, one number, for everything since.
Lint this package the way its own repository will
biome's configuration lived only at the workspace root. This package is
built from its own repository, where that file does not exist and biome
falls back to its defaults — so lint in CI has been checking a different
set of rules from lint here, and the bans this project actually cares
about were never enforced where it counts.
The config is now the package's own, and says the same thing the root one
did.
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.
Lint the tests too
Widening lint to tests/ surfaced literal-index access biome rewrites
to dot notation, and one useHookAtTopLevel on r.useSsrModule(...) —
a renderer method whose name happens to start with use, suppressed with
that reason rather than renamed.
Plus the rest of the gates the package already declared: CI runs
pnpm typecheck and pnpm test:coverage.
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.
Release 0.1.17
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.
Release 0.1.16
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.
Move jsdom to 30
Hydration, the router and the three framework adapters all test against
it; nothing in the 30 line touched what they use.
Turn on noUnusedLocals/noUnusedParameters
Changes since v0.1.15.
v0.1.15
Release 0.1.15
Keep the E_ prefix to error codes only
The rename swept up three kinds of name that are not codes.
PIPELINE_STAGES is the list of pipeline stage names, exported and imported by
that name — E_PIPELINE_STAGES broke a public import for nothing.
ATLAS_STRICT, ATLAS_TEST_PG_URL and ATLAS_TEST_MYSQL_URL are environment
variables. Renamed, the integration suites read a variable nothing sets, so
they would have skipped silently forever instead of running against a real
Postgres or MySQL — coverage lost with no failing test to show for it. Three
test-local variables in ream went the same way and are restored too.
A photon comment named two codes in their pre-namespace spelling.
Changes since v0.1.14.
v0.1.14
Release 0.1.14
Namespace every error code as E__
Photon's codes became E_PHOTON_*, and two places still spoke the old spelling.
The cross-realm check tested code.startsWith("PHOTON_"), so a PhotonError
arriving from a duplicated SSR bundle, a worker or a VM context stopped being
recognised as one and was rewrapped as generic — losing its code, context and
cause. Both spellings are accepted now.
The documentation anchor is built from the code, so E_PHOTON_HYDRATION_NO_DATA
pointed at #e-photon-hydration-no-data while the catalog heading is
#photon-hydration-no-data. The prefix belongs to the code, not to the heading,
and is stripped before the slug — in the browser copy of the helper as well as
the server one.
Changes since v0.1.13.
v0.1.13
Use real private fields, not the TypeScript keyword
private is erased at compile time: at runtime the field is public,
enumerable, and shows up in Object.keys and JSON.stringify. # is enforced by
the engine. The difference is not cosmetic, and one test proved it — photon's
renderer test reached into a private ssrModule through an
as unknown as { ssrModule } cast, which the keyword never prevented. It now
goes through useSsrModule(), a real seam, and the cast is gone.
Constructor parameter properties are expanded into a field plus an assignment,
since # cannot be declared in a parameter list.
Three private CONSTRUCTORS stay as they are, annotated: that form has no native
equivalent, and it is the one place the keyword expresses something # cannot.
Create the GitHub release from the publish workflow
A published version arrived with no notes: npm showed a number, GitHub showed
nothing, and the only way to learn what changed was to read a diff. The commit
messages already carry the reasoning, so the release is built from the commits
the tag contains rather than written twice.
Skips a pure version bump, leaves an existing release alone, and does nothing
when the run was not built from a tag. The job takes contents:write for this;
the workflow default stays read.
Changes since v0.1.12.
photon v0.1.12
photon 0.1.12
Version bump only — no behavioural change.
Changes since v0.1.11.