Skip to content

oj v0.2.46

Choose a tag to compare

@github-actions github-actions released this 25 Oct 09:17
· 35 commits to main since this release
e4b367d

Added

  • StyleX, compiled natively. An app keeps @stylexjs/unplugin's stylex.vite(options) in its Vite config, and oj runs it on its own Rust StyleX compiler (targeting @stylexjs/babel-plugin 0.19.0) in dev, oj build and TanStack Start. In a Start build the rules join the app's first stylesheet, as the plugin's generateBundle does, instead of a separate linked sheet. oj stylex <command> and the standalone fru binary expose the compiler's CLI.
  • OpenTelemetry export, a first version and off by default. OJ_OTEL=1 exports over OTLP, configured by the standard OTEL_EXPORTER_OTLP_*, OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES and OTEL_TRACES_SAMPLER* variables: every oj diagnostic as a log record, and for each dev-server request a server span (a child of an incoming traceparent) and an http.server.request.duration sample.

Changed

  • Cold boots are faster. V8 code-cache entries are held in memory, shared by every engine in the process, and written in batches 10 seconds apart, like Node's compile cache under Vite. Before, every module wrote its entry during the import. Shutdown and restarts write what is still held. One-shot children the server waits on (config extraction, start scripts) read the cache but no longer create entries. package.json files are parsed once per process and read again only when the file changes. On a TanStack Start fixture the cold first response drops from 2.40s to 1.99s, and a cold import of Vite plus the Start plugin in an engine job from 461ms to 233ms.
  • oj build writes pages as Vite does. The entry <script>, its modulepreload links and its stylesheets go into <head>, serialized the way Vite writes them, and the module scripts leave the body. A page's local <link rel="stylesheet"> is now imported by its entry and lands in the entry's stylesheet in document order, instead of becoming a hashed file of its own. A media or disabled one stays a plain asset. With cssCodeSplit: false the single sheet ends in Vite's hash marker, as Vite's does.
  • oj build builds JS as Vite 8.3.5 does, on the same rolldown 1.2.15 and oxc 0.155. Dynamic imports go through a port of rolldown's build import analysis, with Vite's own preload helper and modulepreload polyfill bundled and minified like any module instead of prepended after the build; JSON imports get Vite's named exports and default object; asset URLs in JS follow vite:asset ("/assets/x.png" under an absolute base). On Vite's parity apps (a lazy route with JSON, an image and CSS, base: "./", modulePreload: false, CSS-only pages, a React app) every chunk, stylesheet and asset matches vite build byte for byte, names included. As in Vite, a lazy chunk's stylesheet loads through its importer's preload list instead of a <link> the chunk injects, and a minified build keeps no legal comments, output.banner included.
  • Assets oj names itself (stylesheets, CSS url() assets, Start's emitted files) are hashed as rolldown names them for Vite 8, in URL-safe base64, and [hash:N] works in file name patterns. A stylesheet's name hashes the sheet with Vite's /*$vite$:1*/ marker, so names match Vite's.
  • Pages are the build's inputs, as in Vite. A page's entry chunk and stylesheet take the page's name (index-*.js and index-*.css, or the key of an object input) instead of its script's. A page whose scripts only import CSS or shared chunks gets no JS entry of its own, and the modulepreload polyfill is a module, shared as modulepreload-polyfill-*.js between pages.
  • ?worker, ?worker&url and new Worker(new URL(...)) build each worker as its own bundle, as Vite's bundleWorkerEntry does, so a worker never carries the page's preload helper.
  • TanStack Start worker bundles run in containers of the app's plugin host, as Vite builds a worker with worker.plugins in the same process, instead of booting a new engine per worker.
  • TanStack Start bundle hosts skip plugin hooks whose filters reject a module, as Vite's plugin container and rolldown do. Every module used to cross into the JS engine for load and transform.
  • A TanStack Start build evaluates the Vite config once less: the route tree is generated under the build's own mode and env, as TanStack's router plugin does. oj build --mode staging used to generate it under .env.development, and a config that throws only under serve no longer fails the build.

Removed

  • Unused and debug-only environment knobs: OJ_V8_COMPILE_CACHE and NODE_COMPILE_CACHE (nothing read them anymore), OJ_HMR_DEBOUNCE_MS (the watcher debounce stays 10ms), OJ_CACHE_VERIFY, OJ_PLUGIN_SYNTH_CONFIG, OJ_NO_DEPS_PRESEED and OJ_OBSERVED_READS_MAX. OJ_ENABLE_CACHE and OJ_NO_CACHE are gone too; use --enable-cache and --no-cache.

Fixed

  • With a non-root base, the dev server strips the base before routing, as Vite's base middleware does. oj's client, its HMR socket and the React refresh preamble answer under the base (Fast Refresh threw before the page rendered), / and /index.html redirect to the base, and a server.proxy bypass that returns a path serves that path.
  • oj build resolves JS imports that start with /: a file under the root (/src/x) first, then a public-dir file, whose module exports its URL (create-vite's import viteLogo from '/vite.svg'). Both failed with UNRESOLVED_IMPORT.
  • A TanStack Start oj build empties its outDir by Vite's emptyOutDir rule: an outDir outside the root is left alone with Vite's warning unless --emptyOutDir or build.emptyOutDir asks for it, .git is kept, and build.emptyOutDir: false is honored. It used to delete any outDir, .git included.
  • TanStack Start server code sees every var of the mode's .env files in process.env, in dev and in the build's prerender, as Start's load-env plugin provides them. envPrefix still gates import.meta.env.
  • Under TanStack Start, enforce: "pre" load hooks see image imports with a query (vite-imagetools' x.webp?w=768) before oj's asset handling, in dev SSR and in the bundles. Such routes returned 500.
  • import.meta.glob patterns that use an alias (@/assets/*.png) resolve through resolve.alias in dev, oj build and Start. They expanded to {}.
  • The dev server restarts only for the config file it loaded, that file's dependencies and the mode's env files in envDir, as Vite does. A .env* or *.config.* file anywhere under a watched directory, such as a nested folder or a monorepo sibling, used to restart it.
  • Dev asset preloads name an imported image by the ?url form its import uses, and responses whose body depends on Sec-Fetch-Dest send Vary: sec-fetch-dest. Overwriting an imported image in place hot-updates its importers.
  • Plugins wrapped in a promise (Promise.resolve([tanstackStart()])) are flattened as Vite's asyncFlatten does, so TanStack's own plugins stay out of the Start bundle host. Plugins returned from applyToEnvironment go through the same filter.
  • this.getModuleInfo and this.getModuleIds know every module in the bundle, in oj build and Start bundles, as rolldown's do. A module whose transform a hook filter skipped returned null.
  • chunk.modules in renderChunk, generateBundle and writeBundle carries each module's renderedLength.
  • Pre-bundled dependencies declare their top-level bindings with var, as Vite's do. Vite's optimizer sets optimizeDeps.rolldownOptions.output.topLevelVar unless the app turns it off, keeping the hoisting esbuild always gave; oj reported the option as unsupported and kept every let, const and class, so a dep that reads a binding before its declaration threw a TDZ ReferenceError under oj only. topLevelVar: false still opts out. Existing pre-bundles are rebuilt once.
  • oj build runs plugin transforms on stylesheets in Vite's order: enforce: "pre" plugins on the source, normal ones on the compiled CSS (a CSS module's classes already scoped), enforce: "post" ones on the JS module that stands in for the sheet, each once. oj ran every plugin on the source and again on that module, so @tailwindcss/vite, which returns a stylesheet of its own, turned the module into CSS and the build failed with a PARSE_ERROR. Chunks a stylesheet's transform emits are bundled, a plugin that throws on a stylesheet fails the build instead of being skipped, and a worker's stylesheets no longer go through the app's plugins (Vite bundles a worker with worker.plugins).
  • oj dev runs plugin transforms on stylesheets in the same slots, as Vite's dev server does: enforce: "pre" plugins on the source, normal ones on the compiled CSS, enforce: "post" ones on what is served, the CSS for a <link> (?direct) and the JS module for an import or ?inline. A sheet's ?direct and ?inline requests compile on their own, with their query in the id plugins see. A CSS module's class names under a PostCSS config hash the sheet before PostCSS, as Vite's do.
  • TanStack Start runs the app's plugin transforms on stylesheets, in the same slots and in each environment, in dev and in the build: the client bundle, its <link>ed sheets and SSR each through their own environment's plugins. Start compiled its stylesheets without them, and its client bundle ran every plugin over the module standing in for a sheet. A plugin load that serves a sheet in place of the file is used too.
  • Under TanStack Start, a stylesheet a plugin serves with no file behind it (a virtual sheet, UnoCSS's virtual:uno.css) is compiled and shipped like a file, and the dev page links it. Start replaced it with an empty module, so its CSS never reached the page. CSS a plugin hands vite:css-post for such a module (UnoCSS's generated utilities) takes the place of its sheet in the client build, byte-identical to Vite's.
  • In Start dev, a sheet's <link> serves what its client import already ran through the plugins, until the next bundle or edit, so each plugin sees a sheet once per environment, as under Vite. It ran them a second time for the link.
  • CSS modules scope only what postcss-modules does: classes, ids, @keyframes and animation references. Grid line names, view-transition-name, @counter-style and container names stay global, and the class map no longer lists them; a grid line name in a CSS module used to fail the compile. A .module.sss sheet's names hash its SugarSS text, as PostCSS parses it, and a module outside the root gets the same pattern names in dev as in the build.
  • In dev, a stylesheet's "post" plugins run once per change rather than on every request. Editing a sheet imported only as ?inline now reaches its importer (a reload when nothing accepts it), as in Vite; the ?inline text used to stay stale.
  • A ?url stylesheet in a TanStack Start build is compiled and emitted like Vite's (u-<hash>.css, same name and bytes), through the plugins as ?transform-only, and the page does not link it. Start emitted a Sass or Less ?url sheet as its raw source. In Start dev, SSR runs the app's plugins over ?url, ?raw and ?inline asset modules, as Vite does.
  • raw anywhere in an import's query (a.css?v=1&raw) is the file's text in oj build, as Vite's rawRE reads it; the build compiled such a stylesheet. In dev, raw is read before url, as in Vite.
  • A css.modules.generateScopedName pattern's hash covers the file's path from the directory oj was started in, as postcss-modules uses process.cwd(). It used the app root, so an app started from another directory got other names than under Vite.
  • A ?url stylesheet compiles as ?transform-only, as in Vite, so Tailwind processes it too, and it is finished and named like a linked stylesheet: the same sheet imported both ways is one file.
  • .pcss, .postcss and .sss are stylesheets in dev, oj build and Start, as in Vite. A .sss sheet is parsed with the app's sugarss, and so are the .sss files it @imports, each imported url() resolved against its own file.
  • CSS module class names are Vite's, in dev, oj build and Start. Without css.modules.generateScopedName they are postcss-modules' default _[local]_[hash]_[line], the hash over the sheet as Vite hands it to PostCSS (a Sass module's over the Sass output). A string generateScopedName is applied as postcss-modules does through generic-names, hashes included, and css.modules.hashPrefix is honored. oj named them [name]_[local]_[hash] before, so the class names in a built app change.
  • A stylesheet a plugin's load serves (a virtual .css or .scss id) is compiled and shipped like a file, through every transform slot. oj replaced it with an empty module and dropped its CSS. CSS a plugin hands to vite:css-post for a module (UnoCSS's layers) takes that module's place in the stylesheet.
  • oj build runs the order: "pre" transformIndexHtml hooks on each page's source after buildStart and before the page's scripts are read, as Vite's build-html does, and the other hooks once the bundle is written. oj ran every hook after bundling, so UnoCSS never saw the classes in index.html, and a pre hook's injected scripts were not bundled.