Skip to content

Releases: lovablelabs/oj

oj v0.2.36

Choose a tag to compare

@github-actions github-actions released this 16 Oct 15:11
431d667

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 (or Bundling CSS is no longer supported for a .css file). oj's asset plugin tagged asset imports while resolving them and passed private markers through rolldown's this.resolve custom option, 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.png resolving next to a ./pov.css import 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/?inline imports are now classified by their resolved id when they load, as Vite's asset and css plugins do, .svg?react loads by its id too, and oj's Start bundle plugins pass nothing through custom. 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 logs Internal server error. It only reached the browser overlay and, under OJ_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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 19:50
68fbe28

Fixed

  • A dependency in a node_modules above 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 for react-dom's scheduler and rendered nothing. Every node_modules on 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 freeExternalMemory once 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's Failed to resolve import again in apps with no plugin resolveId. 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 a resolveId hook 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 .env edit 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 .env or 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 .env edits 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.js for a page with no file) whose imports resolve like any module's, so a bare import React from "react" in a page reaches the optimized dep instead of the browser. The same chain backs server.transformIndexHtml, which ran only the plugins' hooks, ignored its url and injected no dev client, so an HTML page a configureServer middleware 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/b points ./src/main.tsx at /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, @import inlining and aliased or relative url()s. A style attribute holding a url() or image-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 @apply or importing a package rendered unstyled.
  • Plugin transforms see a dev page's inline <style> and style attributes 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 as index.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 as url("..."), 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 buildStart runs once and its state survives rebundles. Each rebundle used to load the app's plugins fresh, so a plugin that writes a generated module from buildStart whenever 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; plugin buildEnd runs 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.js for a page with no file) whose imports resolve like any module's, so a bare import React from "react" in a page reaches the optimized dep instead of the browser. The same chain backs server.transformIndexHtml, which ran only the plugins' hooks, ignored its url and injected no dev client, so an HTML page a configureServer middleware 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/b points ./src/main.tsx at /src/main.tsx), and a /@id/ url keeps its query as part of the id. Inline <style> and style attribute 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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 11:21
c6019c9

Fixed

  • TanStack Start's generated route tree names the start entry like the framework's. For an app with src/start.*, oj wrote the import type { createStart } footer while the framework's generator (which oj's plugin host also runs) writes import type { startInstance } and a config: line, so the two rewrote each other and a committed routeTree.gen.ts kept going dirty under oj 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-entry resolves src/start.* with the framework's own lookup (.ts, .js, .mts, .mjs, .tsx, .jsx, no src/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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 09:42
3a426cb

Fixed

  • A dev-server restart (a plugin's server.restart(), a config or .env edit) boots like a fresh launch again. The Start SSR runner primed process.env (NODE_ENV, the .env values, the TSS_* 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 leaked NODE_ENV/VITE_*/TSS_SERVER_FN_BASE re-keyed the Start client bundle cache, so every restart rebuilt the client bundle from scratch (minutes on a large app), and a leaked .env value posed as shell env, so an edited .env value lost to the old one. The runner now keeps a private process.env copy 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 (find strings 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

Choose a tag to compare

@github-actions github-actions released this 14 Oct 11:52
6266a85

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 browser main field. Vite's environments never inherit the top-level resolve.mainFields (its config drops mainFields and conditions from 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 (browser first). A package like react-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 take environments.ssr.resolve.mainFields (or ssr.resolve.mainFields) when set and the server defaults otherwise; client resolution keeps the browser field, as Vite's client environment does.

Added

  • OJ_BOOT_PHASES also 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

Choose a tag to compare

@github-actions github-actions released this 14 Oct 08:05
e1d8eb0

Fixed

  • Start dev picks up a src/start.ts created or deleted while the server runs. The start entry is resolved once, in the framework plugin's config hook 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 global createStart middleware 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 existing src/start.ts stay ordinary rebundles. The client bundle cache also keys on which src/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 with SyntaxError at 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 ignored ssr.noExternal and 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/.cjs may stay external. A bare import "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 .css import as an empty module and linked the stylesheet as written, so a .module.css class map was {} in the browser and the sheet's selectors were unscoped, while dev SSR rendered scoped names: nothing was styled, hydration saw different classNames, 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 honor css.modules (generateScopedName, localsConvention, ...) now, matching the client pipeline.

oj v0.2.29

Choose a tag to compare

@github-actions github-actions released this 13 Oct 17:11
ea773df

Fixed

  • Start client bundles (dev and the prod client build) set preserveEntrySignatures: false by default, as Vite's client app build does before the app's own rolldownOptions spread over it; an app's own value (client environment over top-level) still wins. Left unset, rolldown warned INVALID_OPTION for every codeSplitting group with includeDependenciesRecursively: false and 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

Choose a tag to compare

@github-actions github-actions released this 13 Oct 15:18
ea4c5c2

Fixed

  • A codeSplitting group whose function (name or test) 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 by apply: "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 same resolveRolldownOptions path and crashes identically, so the dev-side tolerance is deliberate where the build-side error is real.

oj v0.2.27

Choose a tag to compare

@github-actions github-actions released this 13 Oct 10:58
4730a86

Added

  • Dev servers honor server.https, serving rustls with ALPN-negotiated HTTP/2 plus an http/1.1 fallback, mirroring Vite's node:http2.createSecureServer with allowHTTP1. Key and cert cross the config bridge as PEM strings, Buffers or arrays (so both readFileSync configs and certificate-injecting plugins like @vitejs/plugin-basic-ssl turn TLS on); pfx and bare true carry no usable PEM and warn. Dev, Start dev, SSR dev and preview all share the path, with preview resolving preview.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 the wss HMR 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 when server.proxy is 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 earlier advancedChunks spelling) and experimental.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.glob scan 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

Choose a tag to compare

@github-actions github-actions released this 12 Oct 18:13
dff329a

Fixed

  • TanStack Start apps on @tanstack/react-start 1.167 and earlier render and hydrate again. The shape of the tanstack-start-manifest:v virtual changed across start-server-core versions (under Vite the plugin generates it, so it always matches): 1.167 reads a top-level clientEntry and per-route assets, 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.ts is honored: #tanstack-start-entry now resolves to the app's src/start.* when one exists (after the app's package.json imports, 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's createStart options: 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.