fix(template): build apps with relative asset URLs - #18
neuromaxer wants to merge 1 commit into
Conversation
Vite defaults to root-absolute asset paths, so a built app emitted `<script src="/assets/index-abc.js">` and its chunks referenced each other the same way. That only works when the app is served from the root of its own origin. A control plane that previews a running app by proxying it under a sub-path (`/apps/<id>/preview/`) gets the HTML back fine, and then the browser asks for `/assets/index-abc.js` at the PROXY's root — 404, blank frame, no obvious cause. Rewriting the HTML in the proxy does not fix it: the emitted JS chunks resolve sibling assets absolutely too, and a code-split app computes some of those URLs at runtime, so no text substitution catches them all. `base: "./"` makes every emitted reference relative, so one build works at its own origin, on a subdomain, or under an arbitrary prefix, and the proxy stays a plain byte pipe with no rewriting in it. Verified with a real build of the template: dist/index.html emits ./assets/index-…js, and serving dist/ from /api/apps/<id>/preview/ resolves that asset with a 200. Existing projects pick this up on their next DEV rebuild, which the deploy-app skill performs on every refinement.
|
Closing — the consumer need behind this went away, and testing it further turned up a reason Why it was opened: openorange wanted to preview a running app in an iframe by proxying it under a dashboard path ( Why that need is gone: a path-based preview only ever works for frameworks whose assets resolve relative to the document. Next.js, SvelteKit and Nuxt fix their asset prefix at build time ( Why this change would be a regression, not just unnecessary: The browser gets HTML where it expects a module, so a deep link or a refresh on any nested route white-screens. So the default is right for apps served at a root, which is where they are served. Anything wanting a sub-path deployment should set Leaving the template unchanged. The reproduction is recorded here in case a sub-path deployment target comes up again. |
The problem
Vite defaults to root-absolute asset paths, so a built app emits:
…and its JS chunks reference each other the same way. That works only when the app is served from the root of its own origin.
A control plane that previews a running app by proxying it under a sub-path —
/apps/<id>/preview/— gets the HTML back correctly, and then the browser asks for/assets/index-abc.jsat the proxy's root:Blank frame, no obvious cause.
Rewriting the HTML in the proxy does not fix this. The emitted JS chunks resolve sibling assets absolutely too, and a code-split app computes some of those URLs at runtime, so no text substitution catches them all. It would appear to work with the current single-chunk template and fail unpredictably as apps grow.
The fix
base: "./"makes every emitted reference relative, so one build works at its own origin, on a subdomain, or under an arbitrary prefix — and the proxy stays a plain byte pipe with no rewriting in it.+ base: "./", server: { host: "0.0.0.0" },Verified with a real build
Existing projects pick this up on their next DEV rebuild, which the deploy-app skill performs on every refinement.
Consumer context, and the ideal future state
openorange wants a live preview pane next to its builder chat. Since apps bind to host loopback and there is no public app hosting yet, the browser cannot reach them directly, so the preview goes through an authorized proxy under a dashboard path — which is exactly the case above.
Worth being explicit that a path-based preview is the interim design, not the target. appx solves this with per-app subdomains (
<name>-dev.<domain>), which is better on two counts:sandboxwithoutallow-same-origin, and that one attribute is the whole boundary. It works, but it is a single point of failure that someone could plausibly "fix" while debugging an app.https://app-dev.example.com/,/assets/…resolves to the app's own root. The problem this PR fixes is created by path-based serving.So the end state is subdomains (wildcard DNS + TLS + ingress routing), and this change is not wasted work when that lands:
base: "./"is strictly more portable than absolute paths and keeps working at a subdomain root. It removes a coupling between how an app is built and where it happens to be served.One caveat worth recording for whoever implements subdomains: appx scopes its session cookie to
.<baseDomain>, which means app subdomains receive the dashboard session cookie. It isHttpOnlyand the proxy strips it before forwarding upstream, but the browser still attaches it. That part should not be copied — app subdomains want their own scoped token.