Skip to content

oj v0.2.50

Choose a tag to compare

@github-actions github-actions released this 28 Oct 22:02
· 5 commits to main since this release
7658aaa

Fixed

  • A server-side render leaves to Node only the dependency files Vite's canExternalizeFile does: .js, .mjs, .cjs or no extension, with the query read as part of the extension. import url from "pkg/styles.css?url" was handed to Node, which loaded the stylesheet as JavaScript; a raw .ts dependency file was handed over too. Such a file now goes through the ssr pipeline, which answers ?url, ?raw and ?inline on a file as vite:asset does (the same URL the browser imports, a public file's under the public dir), after any plugin that loads the id itself.
  • A dev server-side render honors ssr.noExternal and ssr.external as Vite's createIsConfiguredAsExternal does. A noExternal package, and every import it makes, goes through the ssr pipeline instead of Node. An exact ssr.external id is always Node's, and a package ssr.external names is Node's even when linked outside node_modules. A relative or absolute specifier is never handed to Node. As in Vite's createIsExternal, a Node builtin (events, node:fs, fs/promises) stays Node's even under ssr.noExternal, so an installed polyfill of the same name no longer runs in its place. An aliased specifier is never Node's: resolve.alias: { react: "preact/compat" } no longer hands Node react.
  • Dev asset URLs follow Vite's fileToDevUrl on the page and in a server-side render alike. They start with server.origin and the base, so an app under base: "/app/" gets /app/src/logo.png. An svg under build.assetsInlineLimit (4 KiB by default) is inlined as a data URL, unless ?no-inline or a fragment says otherwise. That applies to a plain import, ?url or new URL(..., import.meta.url). An svg's ?inline is encoded as Vite's svgToDataURL does (URL-encoded markup) instead of base64. The same rules name a stylesheet's url(), a ?worker script and a ?init wasm. ?url&inline inlines any file, while ?inline on a file that is not an asset (data.json?inline) is its plain module. A ?url keeps every query parameter but url (?no-inline included), its path is encoded as Vite's encodeURIPath does, and the base is decoded first. ../public/logo.png?url stays /public/logo.png; only /logo.png names the public file. In the module runner an asset module is transformed like any other.
  • Dev workers follow Vite's vite:worker and workerImportMetaUrl plugins.
    • URLs. A worker's URL is its file's dev URL with ?worker_file&type=<type>.
    • ?worker. It exports a WorkerWrapper that passes options.name.
    • ?worker&url. It exports the worker's URL.
    • new Worker(new URL(...), options). The worker type comes from the options, as Vite's getWorkerType reads them: classic without options or a static type, ignore under /* @vite-ignore */.
    • The served module. A worker_file module starts with Vite's env prelude: import "/@vite/env" for module, importScripts under the base for classic, the env module inlined for ignore.
    • /@vite/env. It is served and assigns the client's user define values (import.meta.env.* aside) onto the global object.
  • A function-valued build.assetsInlineLimit decides in dev, as Vite's shouldInline calls it. It is called with the file and its content: true inlines, false keeps the URL, and undefined falls back to the 4096 byte default. The plugin host keeps the function and stays up for it even with no plugin to run. A build still uses the 4096 byte default. Each environment's own limit decides there, a number or a function (environments.ssr.build.assetsInlineLimit: 0 under a top-level function, or a function under a top-level number), and a function that throws fails the request, as in Vite. A slow function is waited for, not cut off.
  • A server-side render reads no file outside the public dir or the allowed roots through an asset query. A root-relative id that climbed out of the public dir (/../../.ssh/id_rsa?raw) was served, as was any queried id outside server.fs.allow. A plain import of a public asset (import logo from "/logo.png") is its URL module, as Vite's asset load names it. A path with a node_modules component outside the project and the allowed roots is no longer readable either. server.fs.deny holds there too, as Vite's isFileLoadingAllowed checks it first: /@ssr-module?id=<root>/.env?raw returned the file. server.fs.strict: false allows every load (the deny list still holds, as in oj's browser gate), and the file an import of a served module resolved to loads wherever it is linked (Vite's safe module paths). Only an import the module's own code makes counts, never what a /@ssr-resolve request names. A ?inline=1 on a module that is not an asset no longer recurses until the server aborts.
  • An aliased builtin name (resolve.alias: { events: "events-polyfill" }) goes through the ssr pipeline, as Vite checks matchAlias before builtins. The ssr environment's own resolve.builtins decide what is a builtin there, with each RegExp's i, m, s and y flags; a pattern oj cannot run is skipped with a warning. A runner-backed ssr environment (Cloudflare's workerd) keeps Node's builtins, as it keeps Node's conditions. An alias Node loads hands Node its target: { "my-fs": "node:fs" } imports node:fs. An exact ssr.external id that is not bare (#db) is Node's, as in Vite's createIsExternal.
  • A stylesheet's url()s are rewritten inside the sheet's compile, where Vite's url replacer runs: after its pre plugins and preprocessors, before its normal plugins. An inlined svg follows the svg after an edit or a restart, and a hot-updated asset gets its t= stamp. Each asset is a graph import of the sheet, so an asset edit sends a <link>-loaded sheet a css-update, and a sheet that inlined a file (a small svg, or any file under ?inline, JSON included) is not kept in the persistent cache. A url resolves in the css resolver's order: the public file, resolve.alias (a root-relative target included), the sheet's directory (a page's <style> resolves beside the page), then a package's file as the resolver finds it, its exports honored (url(pkg/img.png)). A url inside a comment stays as written, while a /* inside a string opens none. A malformed escape (%E0%A4%A) fails the sheet, as decodeURI throws, and an unclosed quoted image-set() candidate no longer panics. Start dev's sheets take the same inline rules on the url as written (#fragment, ?no-inline and ?inline count) under each environment's own limit, and an escaped paren (url(./a\(1\).png)) belongs to the url. Quotes are kept as written and added only when the new url needs them, so a url inside a string no longer breaks it. my-url(...) and @import stay as written, image-set() candidates are rewritten too, a url is decoded before it is resolved, and a public file's URL joins the base as written.
  • ?inline counts in a parameter as Vite's [?&]inline\b reads it (?inline-x).
  • A server-side render loads a plain asset import (import logo from "./logo.svg") as its URL module and a stylesheet's ?inline as the text the client's ?inline compiles (urls rewritten, PostCSS and Tailwind applied), as Vite's asset and css-post plugins do in every environment. Both failed before. An asset module takes the ssr environment's transforms, and a plain svg a transform plugin componentizes is that component there too, as on the client.
  • A worker's type is read from TypeScript-annotated options ({ type: "module" as const }, satisfies) and from the last top-level type key, never from a nested object, a string or a comment; a comment around the colon is whitespace, and a comma inside a comment before the options is not theirs. /* @vite-ignore */ anywhere after the URL's comma makes it ignore. A user's type= on a ?worker import no longer changes its type.
  • Start dev names workers as Vite's dev does, in the server render and the client bundle alike: the worker file's dev URL with ?worker_file&type=<type> (module, or the type new Worker(new URL(...), options) gave). The served worker starts with the env prelude.
  • A plugin's this.resolve and this.load (its importedIds, an oj field: Vite's dev ModuleInfo has only id and meta) resolve with the calling environment's own options, as Vite builds its resolve plugin per environment: a package's browser field no longer wins in a server-side render. The transforming module's own imports, any other specifier, and the importedIds of a module this.load reads from disk all resolved the client way before, and the last two ignored the app's resolve options (aliases, conditions, main fields) in every environment. Each environment takes its own resolve.conditions and resolve.mainFields from Vite's resolved config: a custom server environment no longer borrows the ssr environment's, a runner-backed ssr environment's plugins see its own conditions, and ssr.target: "webworker" keeps the client defaults.
  • An environment's this.resolve forgets a cached miss once the file is created, as Vite's resolve plugin clears its cache on a created or deleted file. A plugin that resolved ./later.js before it existed kept getting null. It also resolves with that environment's own resolve.dedupe and resolve.extensions (each extending the top-level list, as Vite's mergeConfig does) and with the top-level resolve.alias, which Vite gives every environment. development|production maps to production under NODE_ENV=production. The ssr transform's pre-resolved imports use the ssr environment's own consumer.
  • Plugins resolve imports in Vite's order: the pre plugins' resolveId (enforce: "pre" or order: "pre"), then Vite's resolver, then every other plugin. A pre plugin can now answer a bare import that a package also provides, in the browser and in a server-side render, so a plugin that points a workspace package at its sources works under oj. A normal plugin still only sees what the resolver could not resolve. A plugin's this.resolve takes the same order instead of running every plugin before the resolver, and in the browser it answers a pre-bundled dependency with its bundle, as Vite's tryOptimizedResolve does. In a server-side render an installed dependency Node loads is still externalized before any plugin sees it, as in Vite. A plugin's external: true answer stays external in the browser only for a remote URL, as in Vite's import analysis. A this.resolve that runs while the dependency scan is still going waits for it, as Vite's tryOptimizedResolve awaits scanProcessing, so the first transform on a cold start gets the bundle too. resolve.alias comes between them as in Vite's chain: order: "pre" hooks see an import as written, an aliased import is resolved again under its target, and enforce: "pre" plugins see only the target, in the browser, in a server-side render, in this.resolve and in the dependency scan. A pre answer is used as Vite's import analysis uses it: a remote URL stays as is whatever its external flag, the specifier itself is the plugin's own module, a root-relative id (/src/x.js) is that file, and in a server-side render a plugin's file is a module even under node_modules, as is a queried or non-URL external answer. A pre-bundle path this.resolve hands a plugin is served at the bundle's URL, so emitting it keeps one module instance. A this.resolve of a dependency the scan missed registers it and answers with its future bundle; one that skips optimization is versioned with ?v=. Only the client environment takes the client's pre-bundles. A this.resolve from a virtual module resolves from the root.
  • A TanStack Start dev server whose cache dir is reached through a symlink restores its client bundle on the next boot. The bundler reports files by realpath, so manifest.ts, a file oj writes itself, slipped past the cache-dir check into the bundle's key, and a boot could miss the bundle it had persisted.
  • In TanStack Start dev, a server-side render resolves an import once, as written, as Vite's import analysis does, so a plugin's resolveId sees its query and can answer pkg/styles.css?url with a module of its own. oj stripped ?url, ?raw, ?inline and ?worker first, and the render failed when only the plugin knew the import. The intent is read off the answer's own query, so a plugin that answers with a file (/src/x.css?url) gets that file, even over an installed package, and a plugin that claims only the bare id no longer takes the queried import. A relative or /-prefixed import with a query reaches the pre hooks too, ahead of an existing file and of the root and publicDir probe. A remote URL a plugin answers stays as is, and a resolveId hook that throws fails the import instead of being skipped.