Summary
The assetBaseUrlTransform plugin (dist/server/asset-base-url-transform-plugin.js) runs a source-level regex over every module and rewrites any quoted asset path into (window.skybridge?.serverUrl ?? "") + "/path". Because Vite serves .css imports as JS modules with the CSS embedded as string literals, the regex also matches inside those strings — and the rewritten output is invalid CSS once it's injected into a <style> tag.
Net effect: every @font-face / background-image / mask-image / any url("/foo.<woff2|woff|svg|png|jpg|jpeg|gif|webp|mp3|mp4|ttf|eot>") silently fails to load in dev, with the asset URL itself being perfectly reachable.
Repro
- Scaffold a fresh Skybridge project.
- Drop any matching file into
web/public/fonts/Brand.woff2.
- Add
web/src/fonts.css:
@font-face {
font-family: "Brand";
src: url("/fonts/Brand.woff2") format("woff2");
}
import "./fonts.css" from the widget entry.
pnpm dev, open the widget, inspect the font.
Expected: "Brand" loads.
Actual: font is missing. Fetching the CSS module that Vite serves shows the src: line transformed to:
src: url((window.skybridge?.serverUrl ?? "") + "/assets/fonts/Brand.woff2") format("woff2");
…which is not valid CSS. curl http://localhost:3000/assets/fonts/Brand.woff2 returns 200 OK — the asset is reachable, only the reference is broken.
Root cause
dist/server/asset-base-url-transform-plugin.js:
const assetStringPattern = /(?<!https?:\/\/)(["'`])(\/[^"'`]+\.(svg|png|jpeg|jpg|gif|webp|mp3|mp4|woff|woff2|ttf|eot))\1/g;
code = code.replace(assetStringPattern, (_match, _quote, assetPath) => {
return `(window.skybridge?.serverUrl ?? "") + "${assetPath}"`;
});
The transform hook runs over every module's source without checking the module type. Vite wraps CSS imports as JS modules where the stylesheet is passed as a string to __vite__updateStyle(...). The regex matches asset paths inside those string literals and rewrites them into JS concatenation expressions, which are syntactically invalid inside a CSS url(...).
Impact
- Silent: the browser drops the unresolvable reference and falls back to defaults — no hard error in the console beyond occasional
NetworkError.
- Surface: any CSS file authored in
web/src/** that references assets by absolute path.
- Workaround requires moving both the CSS file AND its assets into
public/ and injecting via <link rel="stylesheet">, which bypasses the Vite asset pipeline entirely (no hashing, no pruning).
Suggested fix
The same plugin file already has the correct mechanism via experimental.renderBuiltUrl in web/plugin/plugin.js — that hook is context-aware and works for both JS and CSS referents. The regex-based source transform looks redundant for most cases and wrong for CSS. Options in order of preference:
- Remove the source-level transform, rely solely on
renderBuiltUrl. Needs verification that dev-mode runtime URLs still work.
- Skip CSS in the
transform hook: in transform(code, id), bail when id indicates a CSS-ish module (.css, .module.css, ?direct, ?inline, ?raw, framework-specific variants).
- Constrain the regex to not match inside
url(...). Fragile; (1) or (2) is cleaner.
Happy to open a PR if a direction is picked.
Environment
Summary
The
assetBaseUrlTransformplugin (dist/server/asset-base-url-transform-plugin.js) runs a source-level regex over every module and rewrites any quoted asset path into(window.skybridge?.serverUrl ?? "") + "/path". Because Vite serves.cssimports as JS modules with the CSS embedded as string literals, the regex also matches inside those strings — and the rewritten output is invalid CSS once it's injected into a<style>tag.Net effect: every
@font-face/background-image/mask-image/ anyurl("/foo.<woff2|woff|svg|png|jpg|jpeg|gif|webp|mp3|mp4|ttf|eot>")silently fails to load in dev, with the asset URL itself being perfectly reachable.Repro
web/public/fonts/Brand.woff2.web/src/fonts.css:import "./fonts.css"from the widget entry.pnpm dev, open the widget, inspect the font.Expected: "Brand" loads.
Actual: font is missing. Fetching the CSS module that Vite serves shows the
src:line transformed to:…which is not valid CSS.
curl http://localhost:3000/assets/fonts/Brand.woff2returns200 OK— the asset is reachable, only the reference is broken.Root cause
dist/server/asset-base-url-transform-plugin.js:The
transformhook runs over every module's source without checking the module type. Vite wraps CSS imports as JS modules where the stylesheet is passed as a string to__vite__updateStyle(...). The regex matches asset paths inside those string literals and rewrites them into JS concatenation expressions, which are syntactically invalid inside a CSSurl(...).Impact
NetworkError.web/src/**that references assets by absolute path.public/and injecting via<link rel="stylesheet">, which bypasses the Vite asset pipeline entirely (no hashing, no pruning).Suggested fix
The same plugin file already has the correct mechanism via
experimental.renderBuiltUrlinweb/plugin/plugin.js— that hook is context-aware and works for both JS and CSS referents. The regex-based source transform looks redundant for most cases and wrong for CSS. Options in order of preference:renderBuiltUrl. Needs verification that dev-mode runtime URLs still work.transformhook: intransform(code, id), bail whenidindicates a CSS-ish module (.css,.module.css,?direct,?inline,?raw, framework-specific variants).url(...). Fragile; (1) or (2) is cleaner.Happy to open a PR if a direction is picked.
Environment
[email protected]vite@^8.0.3, React 19skybridge dev)