Repository navigation
Releases: lovablelabs/oj
Releases · lovablelabs/oj
Release list
oj v0.2.36
Fixed
- A TanStack Start client bundle no longer fails at random on an image or stylesheet import with
[UNLOADABLE_DEPENDENCY] ... stream did not contain valid UTF-8(orBundling CSS is no longer supportedfor a.cssfile). oj's asset plugin tagged asset imports while resolving them and passed private markers through rolldown'sthis.resolvecustomoption, which concurrent resolves can swap: rolldown keys each call's saved options by the size of a map, so a finished call frees an index another call still holds. An aliased@/logo.pngresolving next to a./pov.cssimport from the same file could then skip the asset plugin and hand rolldown the bare file to read as source, and the overlay stayed until the next edit. Asset, stylesheet, worker and?raw/?url/?inlineimports are now classified by their resolved id when they load, as Vite's asset and css plugins do,.svg?reactloads by its id too, and oj's Start bundle plugins pass nothing throughcustom. This applies to Start builds as well. - TanStack Start dev honors
server.hmr.overlay: false. A failed client bundle mounted the full-page error overlay on every page whatever the config said; now, as in Vite's client (__HMR_ENABLE_OVERLAY__), the error is only logged to the browser console when the overlay is off. - A served compile or transform failure prints a human line to the terminal (
oj: internal server error: <message>), as Vite logsInternal server error. It only reached the browser overlay and, underOJ_LOG_JSON=1, the NDJSON diagnostics line, so a supervisor that keeps the NDJSON record out of its stored logs lost the error entirely.
oj v0.2.35
Fixed
- A dependency in a
node_modulesabove the app root (a workspace's hoisted install) is served however its request arrives. oj allowed such a package only once a module importing it had compiled, so a page reloading into a restarted dev server, which fetches the imports it remembers before their importers, got a 403 forreact-dom'sschedulerand rendered nothing. Everynode_moduleson the root's lookup path is now allowed from boot, as Vite serves what its resolver resolves. - The TanStack Start dev bundler holds less memory between rebundles. Each build's outputs kept their code in rolldown's native memory until V8 collected their JS wrappers, and a bundler idling until the next edit allocates nothing, so V8 never did. The outputs are now freed with rolldown's
freeExternalMemoryonce they are written: on a 3,000-module app on Linux, the idle bundler drops from about 570MB to about 455MB. - A bare import nothing resolves (
import x from "not-installed-pkg") fails the importer with Vite'sFailed to resolve importagain in apps with no pluginresolveId. Since the plugin host runs for every app, oj took its mere presence to mean a plugin might serve the specifier and shipped a/@id/url instead. Now only a plugin with aresolveIdhook gets that chance, as in Vite's importAnalysis. - Paths under
/__belong to the app, as in Vite, which reserves no such prefix. TanStack Start and SSR dev handed every/__*request to the dev server's own routes, so an app route like a component preview (/__component/preview/$) never reached SSR and answered "oj: no such file" whenever no plugin middleware served it; the SPA fallback skipped/__*navigations the same way. Only oj's own endpoints (/__ws,/__hmr_flush,/__hmr_gate, and SSR dev's/__oj_fn) stay the dev server's. - The dev server (plain, SSR and TanStack Start) binds its port only once the file watcher covers the root, as Vite creates its watcher before listening. A file written the moment the server first answered could go unseen, a
.envedit right after a restart included. - A slow or hung plugin hot-update hook no longer holds every later file change back. The dev watcher ran each batch's HMR (which awaits the plugin host for its watch files and
handleHotUpdate, as long as host init or the hook takes) on its own thread, so a.envor config edit behind it never restarted the server. Each batch's HMR now runs as its own task, as Vite handles each watcher event on its own, so a hung hook parks only its own file's update: other files still hot-update and config and.envedits restart right away. The restart line names the file that changed, as Vite's does. - Start dev rebundles pick up an edited CSS module or preprocessor sheet again. The client bundler now lives for the whole session, and its engine kept each stylesheet compile cached by url, so after an edit the client bundle kept the old class names while SSR rendered the new ones. Style imports now carry an epoch that moves whenever a stylesheet, partial or PostCSS/Tailwind config changes.
- Dev pages go through Vite's whole dev HTML transform. An inline
<script type="module">is lifted into an html-proxy module (/index.html?html-proxy&index=0.js, or/@id/__x00__/<page>?html-proxy&index=0.jsfor a page with no file) whose imports resolve like any module's, so a bareimport React from "react"in a page reaches the optimized dep instead of the browser. The same chain backsserver.transformIndexHtml, which ran only the plugins' hooks, ignored its url and injected no dev client, so an HTML page aconfigureServermiddleware rendered through it shipped unresolvable imports. Hooks run in Vite's order (import map, CSP meta,pre,%ENV%, the dev transform, normal,post, nonces), tags inject after each hook the way Vite's do (<head lang>included), script and asset urls are rebased like Vite's (a SPA fallback on/a/bpoints./src/main.tsxat/src/main.tsx), and a/@id/url keeps its query as part of the id. - An inline
<style>in a dev page compiles like a stylesheet beside the page, as Vite runs it through its CSS pipeline: plugin transforms, PostCSS and Tailwind directives,@importinlining and aliased or relativeurl()s. Astyleattribute holding aurl()orimage-set()goes through the app's PostCSS plugins and then gets its urls rewritten as text, as Vite's default PostCSS transformer does, so the declarations stay as written. oj shipped both as written, so a page<style>using@applyor importing a package rendered unstyled. - Plugin transforms see a dev page's inline
<style>andstyleattributes under Vite's ids (/abs/index.html?html-proxy&direct&index=0.css,...&inline-css&style-attr&index=1.css); attributes skipped plugin transforms entirely, and a<style>reached them asindex.html.css. An app with no active plugins (or no config) now gets the dev HTML transform too, as under Vite: its plugin host stays up, idle, instead of shutting down, and pages served as written before. - An
image-set()candidate in a dev stylesheet prints asurl("..."), as Vite's url rewrite writes it, instead of a bare string. - A relative
url()in a dev stylesheet is rewritten only when it names a file, as Vite's dev url resolver does: one naming no file stays as written instead of being pointed at a server path. That includes every relative url in an inline style of a page with no file (one a middleware renders), which Vite resolves from no directory at all. - TanStack Start dev keeps one client plugin container for the whole session, as Vite keeps one per server, so a plugin's
buildStartruns once and its state survives rebundles. Each rebundle used to load the app's plugins fresh, so a plugin that writes a generated module frombuildStartwhenever its in-memory copy differs (the usual codegen shape) rewrote that file on every bundle, the watcher fed the write back as the next rebundle, and the page reloaded about once a second forever. The client bundler is now one long-lived child per dev server; pluginbuildEndruns when the session ends, as in Vite dev. - Dev pages go through Vite's whole dev HTML transform. An inline
<script type="module">is lifted into an html-proxy module (/index.html?html-proxy&index=0.js, or/@id/__x00__/<page>?html-proxy&index=0.jsfor a page with no file) whose imports resolve like any module's, so a bareimport React from "react"in a page reaches the optimized dep instead of the browser. The same chain backsserver.transformIndexHtml, which ran only the plugins' hooks, ignored its url and injected no dev client, so an HTML page aconfigureServermiddleware rendered through it shipped unresolvable imports. Hooks run in Vite's order (import map, CSP meta,pre,%ENV%, the dev transform, normal,post, nonces), tags inject after each hook the way Vite's do (<head lang>included), script and asset urls are rebased like Vite's (a SPA fallback on/a/bpoints./src/main.tsxat/src/main.tsx), and a/@id/url keeps its query as part of the id. Inline<style>andstyleattribute urls are not transformed yet. - A file no module graph knows no longer gets read when it changes. The plugin host hashed every changed file's full contents to drop oj's duplicate notification for one edit, so a database a local worker rewrites on every request (Miniflare's trace store under
.wrangler/state, which grows to gigabytes) was read whole on each write: memory spiked by the file's size and the host stalled, leaving every page request hanging. Such files are now told apart by their stat, as Vite never reads a changed file unless a plugin asks; module files still compare by content.
oj v0.2.34
Fixed
- TanStack Start's generated route tree names the start entry like the framework's. For an app with
src/start.*, oj wrote theimport type { createStart }footer while the framework's generator (which oj's plugin host also runs) writesimport type { startInstance }and aconfig:line, so the two rewrote each other and a committedrouteTree.gen.tskept going dirty underoj dev. The route-tree cache key now includes which start entry exists, so adding or removing it regenerates the tree. - TanStack Start's
#tanstack-start-entryresolvessrc/start.*with the framework's own lookup (.ts,.js,.mts,.mjs,.tsx,.jsx, nosrc/start/index.*), so the served entry, the dev restart on its creation or deletion, and the route tree's footer always agree on which file it is.
oj v0.2.33
Fixed
- A dev-server restart (a plugin's
server.restart(), a config or.envedit) boots like a fresh launch again. The Start SSR runner primedprocess.env(NODE_ENV, the.envvalues, theTSS_*defaults, oj's script settings) on the real process environment, and the restart's exec carried all of it into the next boot. There the leakedNODE_ENV/VITE_*/TSS_SERVER_FN_BASEre-keyed the Start client bundle cache, so every restart rebuilt the client bundle from scratch (minutes on a large app), and a leaked.envvalue posed as shell env, so an edited.envvalue lost to the old one. The runner now keeps a privateprocess.envcopy like the plugin host and the Start scripts, and a restart execs with the environment oj was launched with. - A call whose deadline passed while it waited on the engine no longer terminates an unrelated call. The watchdog's termination is isolate-wide, so when the deadline fired while another call ran synchronous JS, that innocent call died with "execution terminated" and every other pending call failed with it. The same happened when a call's deadline passed during its module's top-level await, and the killed call then hung forever. The watchdog now runs only while no other call is waiting; a waiting call's deadline settles on its own timer.
- TanStack Start client bundles apply the app's
resolve.alias, in dev and in production builds. Only SSR applied it, so an@/import in the client graph reached the browser as a bare specifier and the page rendered blank. The client, worker and server bundles now match aliases the way Vite does (findstrings and regexes, object or array form), and an alias whose target does not resolve keeps the rewritten id, as Vite's alias plugin does.
Changed
- Tailwind/PostCSS, Less/Stylus and Svelte compiles no longer fail on elapsed time (the 20s dev, 30s Start and 60s build budgets are gone), matching Vite's CSS pipeline, which runs with no timeout. Compiles share one engine, and a slow one under CPU contention used to fail as "compile timed out".
oj v0.2.32
Fixed
- A native addon's call into JS (a napi threadsafe function, the way rolldown's worker threads run JS plugins) no longer waits for an unrelated wake of oj's engine. deno_core's cross-thread task queue checks for work before registering its waker, so a task queued in between woke nothing. On CI that stalled about one Start dev rebundle in eight by ~30s inside Vite's config bundling, so an edit took half a minute to reach the browser. oj's engine now polls its event loop again whenever a poll comes back pending, which closes the window for every engine (the dev server's, the plugin host's, the Start scripts').
- Dev SSR and TanStack Start SSR no longer resolve dependencies through their
browsermain field. Vite's environments never inherit the top-levelresolve.mainFields(its config dropsmainFieldsandconditionsfrom what environments inherit), so its server environment resolves with the browser-less server defaults; oj's SSR resolver took the top-level list, which a resolved config always carries filled with Vite's client defaults (browserfirst). A package likereact-simple-maps@3(browser= UMD build,module= ESM,main= CJS) then served its UMD to the server runtime, whose static CJS analysis cannot see exports a factory assigns at runtime, and every route 500'd at link time with "The requested module … does not provide an export named …". The SSR resolver and the SSR production bundle now takeenvironments.ssr.resolve.mainFields(orssr.resolve.mainFields) when set and the server defaults otherwise; client resolution keeps the browser field, as Vite's client environment does.
Added
OJ_BOOT_PHASESalso traces the Start scripts: the client bundle child's rolldown import, plugin config load, build and output writes, and the child's spawn and exit, as[oj-phase]lines alongside the dev server's.
oj v0.2.30
Fixed
- Start dev picks up a
src/start.tscreated or deleted while the server runs. The start entry is resolved once, in the framework plugin'sconfighook upstream and at boot in oj, so a presence flip now restarts the dev server the way Vite restarts for its config-shaped inputs (config file, config dependencies, env files; Vite's restart branch runs for create and delete too). Previously the create matched no served graph and nothing rebundled, so globalcreateStartmiddleware silently never ran, and the next unrelated rebundle half-applied it: the client bundle adopted the entry while SSR kept the stub until restart. Edits to an existingsrc/start.tsstay ordinary rebundles. The client bundle cache also keys on whichsrc/start.*exists, so a boot after the flip bundles the real entry instead of restoring the stale stub bundle (this also fixes adding the entry between runs). - A dependency that ships raw TypeScript and imports its own stylesheets (
import "./select.css", a CSS module, a JSON file) renders under dev SSR and TanStack Start dev instead of 500ing every route withSyntaxErrorat the stylesheet's first line. oj transforms such dependency files through the pipeline, but their imports still took the native runtime's Node path, which parsed CSS as JavaScript (and ignoredssr.noExternaland plugins). Vite never externalizes a relative or absolute specifier, so a module it transformed has every import resolved through the plugins; oj now does the same for the dependency modules it claims, while node builtins still short-circuit first. - SSR externalization follows Vite's
canExternalizeFile: only a dependency file with no extension or.js/.mjs/.cjsmay stay external. A bareimport "pkg/styles.css"(or a dependency JSON file) under dev SSR is served by the pipeline instead of the runtime, and an SSR production build bundles it instead of leaving an import Node cannot load. - CSS modules work under TanStack Start, in dev and in production builds. The client bundles loaded every
.cssimport as an empty module and linked the stylesheet as written, so a.module.cssclass map was{}in the browser and the sheet's selectors were unscoped, while dev SSR rendered scoped names: nothing was styled, hydration saw differentclassNames, and a production build rendered no class at all. The Start bundles now take each CSS module from oj's CSS pipeline, the compile dev SSR renders from, so the server, the client's class map and the emitted (or dev-served) stylesheet name the same scoped class, as Vite's single postcss-modules compile does. Scoping runs before Tailwind, as postcss-modules runs before the app's postcss plugins in Vite. Dev SSR CSS modules also honorcss.modules(generateScopedName,localsConvention, ...) now, matching the client pipeline.
oj v0.2.29
Fixed
- Start client bundles (dev and the prod client build) set
preserveEntrySignatures: falseby default, as Vite's client app build does before the app's ownrolldownOptionsspread over it; an app's own value (client environment over top-level) still wins. Left unset, rolldown warnedINVALID_OPTIONfor everycodeSplittinggroup withincludeDependenciesRecursively: falseand fell back to'allow-extension'itself, so chunking was already correct; the warning is gone. oj's client entry exports nothing, so the default drops nothing.
oj v0.2.28
Fixed
- A
codeSplittinggroup whose function (nameortest) throws during the dev client bundle no longer takes the dev server down at boot: the group steps aside with one warning naming it, every other group still applies, and the production build stays loud. Groups computed byapply: "build"plugins guard on data those plugins produce, and under dev (serve-mode plugin resolution, as in Vite) those plugins never ran; Vite's own bundled dev passes the user's output options through the sameresolveRolldownOptionspath and crashes identically, so the dev-side tolerance is deliberate where the build-side error is real.
oj v0.2.27
Added
- Dev servers honor
server.https, serving rustls with ALPN-negotiated HTTP/2 plus an http/1.1 fallback, mirroring Vite'snode:http2.createSecureServerwithallowHTTP1. Key and cert cross the config bridge as PEM strings, Buffers or arrays (so bothreadFileSyncconfigs and certificate-injecting plugins like@vitejs/plugin-basic-sslturn TLS on);pfxand baretruecarry no usable PEM and warn. Dev, Start dev, SSR dev and preview all share the path, with preview resolvingpreview.https ?? server.https. WebSockets ride http/1.1 by design (the h2 SETTINGS never advertise extended CONNECT, like node's http2 server under Vite), so thewssHMR connection takes the ordinary Upgrade path. Remote dev pays one round trip per request over at most six HTTP/1.1 connections, so a many-chunk page over real latency gets order-of-magnitude faster with h2. Unlike Vite, h2 stays on whenserver.proxyis set (Vite's downgrade is a node proxy-library limitation the axum proxy does not share). - Start client bundles honor the app's chunk policy from
build.rolldownOptions:output.codeSplitting(also accepted under its earlieradvancedChunksspelling) andexperimental.chunkOptimization, with the client environment's options winning over top-level, in the dev client bundle and the prod client build. Vite passes the app's rolldown options through in build and bundled dev alike, so chunk grouping is the app's to declare; oj ignored it and a large app with many dynamic imports exploded into thousands of micro-chunks. The config is live in-process, so function-valued group names work as under Vite. The dev bundle cache re-keys on a chunk-policy edit for free (the key digests the closure, which includes the config file).
Fixed
- Start dev no longer rebundles for every created file. A create triggers a client rebundle only under a recorded
import.meta.globscan base (the bundler now writes the scan bases beside the closure), matching what a create can actually change under Vite, where an added file only re-expands matching glob importers and heals modules whose imports previously failed to resolve (oj's equivalent: a failed bundle rebundles on every batch). The old any-create escape hatch let continuously written state (a local store's sqlite journal files under the project root) drive a rebundle-restart loop with multi-gigabyte memory spikes on large apps. The "rebuilding for" log now prints only when a rebundle actually runs.
oj v0.2.26
Fixed
- TanStack Start apps on
@tanstack/react-start1.167 and earlier render and hydrate again. The shape of thetanstack-start-manifest:vvirtual changed across start-server-core versions (under Vite the plugin generates it, so it always matches): 1.167 reads a top-levelclientEntryand per-routeassets, and oj's shim provided neither, so the served HTML carried<script type="module">import(undefined)</script>and the page never left the SSR placeholder. The shim now serves the union of both shapes; each version copies only the fields it knows. src/start.tsis honored:#tanstack-start-entrynow resolves to the app'ssrc/start.*when one exists (after the app's package.jsonimports, before the bundled default), matching upstream's entry resolution, in the dev server, the dev client bundle and builds. oj's stub previously always answered, silently dropping the app'screateStartoptions: request middleware never ran server-side and function middleware never wrapped server-fn calls, so an app attaching auth that way got Unauthorized from every authed server fn. The stub still stands in when the app has no start entry, exactly like upstream's default (export const startInstance = undefined).- The client and build bundlers resolve
tanstack-start-injected-head-scripts:v(start-server-core imports it dynamically in dev), fixing[UNRESOLVED_IMPORT]warnings. - The preseed self-heal fails the seed when it cannot rewrite the include snapshot, instead of retrying to success over a file that still replays the toxic dep ids: a warm stamp over an unhealed snapshot would skip the child (and the heal) on later boots while the plugin host keeps folding the poison.