Repository navigation
FastBoot 7: render with a DOM, for classic and Vite apps - #990
Draft
NullVoxPopuli-ai-agent wants to merge 4 commits into
Draft
NullVoxPopuli-ai-agent wants to merge 4 commits into
NullVoxPopuli-ai-agent wants to merge 4 commits into
Conversation
BREAKING CHANGE: FastBoot renders the app the same way a browser runs it. - fastboot runs the app in worker threads. happy-dom makes each worker a browser window (window === globalThis), so Ember's own environment check sees a browser. FastBoot passes no isBrowser, isInteractive, or document boot options, and does not use the -document service. - One loader reads a classic dist from index.html. Another imports the SSR bundle of a Vite build (createSsrApp). The fastboot/vite plugin renders pages in the Vite dev server with ssrLoadModule. - fastboot/middleware replaces fastboot-express-middleware. fastboot-app-server is removed. - ember-cli-fastboot is a v2 addon with a v1 shim for classic builds. It keeps the fastboot service for the upgrade. boot() rehydrates a rendered page in the browser. - Removed: the vm sandbox, the FastBoot global, fastboot/ trees, the dist manifest, the fastboot location, and result.chunks(). - pnpm 12, Node 22.12 or later, CI on Node 22, 24, and 26. Tests use vitest, a classic and a Vite fixture app, and Chromium for rehydration. The @embroider/core patch fixes a crash: Embroider walks <app>/_fastboot_ for any active addon named ember-cli-fastboot, and that directory does not exist in a Vite app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `fastboot/prerender`: `prerender({ distPath, entry, routes, template, outDir })`
renders each URL and writes `<outDir>/<url>/index.html`.
It rejects with an AggregateError when a page fails or has a status of 400 or more,
so a broken page fails the build.
- `fastboot/vite`: the `prerender: { routes, template }` option. After the client
build, the plugin runs the SSR build with the same config into a temporary
directory, prerenders the routes, and removes the SSR bundle.
The temporary directory is outside node_modules: an SSR build that writes into
node_modules leaves the app modules of an Embroider app unresolved (`./app`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A route chunk that loads lazily has its own CSS, and the entry of the page
does not link it. Without the link, a prerendered page shows that route
unstyled until the JavaScript loads.
- `Result#routeNames`: the active routes after a render.
- The Vite plugin reads the manifest of the client build, maps source
modules to route names, and links the CSS of each route of the page.
The default `routeName` reads the routes that Embroider splits
(`-embroider-route-entrypoint.js:route=<name>`) and the classic
`app/templates`, `app/routes`, and `app/controllers` paths.
`prerender.routeName` sets another layout.
- `prerender()` takes `transformHtml(html, { url, result })`.
- If the plugin turned on the manifest, it removes it after the prerender.
The Vite fixture splits a new `lazy` route with its own CSS. A Chromium test
with JavaScript turned off checks that the prerendered page has its styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With `prerender`, the dev server renders only the prerender routes, and renders them into `prerender.template`. Before, it rendered every page into index.html. An app whose index.html boots without rehydration (the template does the rehydration) then showed each page twice. - `FastBoot` takes a `template` option: the HTML file to render into. - Vite calls `closeBundle` after a failed build too. The prerender now runs only after `writeBundle`, so a build error is no longer hidden behind a missing manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
It works:
|
Contributor
|
if someone could put this in draft, that'd be great. I haven't reviewed the code yet, only outputs |
NullVoxPopuli-ai-agent
marked this pull request as draft
October 29, 2026 01:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FastBoot 7 renders the app with a DOM, the same way a browser runs it, for classic (broccoli) apps and Vite apps. This is a breaking change. It is the SSR path that emberjs/rfcs#1178 asks for: FastBoot no longer needs
isBrowser,isInteractive,hasDOM, or the-documentservice.What changes for apps and addons
document,window,location, andfetchexist, and modifiers run. Apps and addons write normal browser code, with noif (fastboot)checks.fastboot/trees, noFastBootglobal, and no sandbox.fastbootservice stays, for the upgrade.shoebox.put()andfastboot.responsenow work in the browser too (they do nothing there), so code that uses them needs no condition.isFastBoot.UPGRADING.md has the full list of removed options.
How it works
fastbootruns the app in worker threads. In each worker,@happy-dom/global-registratormakeswindow === globalThis. Ember's ownhasDOMcheck then sees a browser, so FastBoot passes onlyrootElementand_renderMode: 'serialize'.distloads from itsindex.html, script by script. A Vite build loads from its SSR bundle, which exportscreateSsrApp().fastboot/viterenders pages in the Vite dev server withssrLoadModule.prerender()fromfastboot/prerender, and theprerenderoption offastboot/vite, write a static page for each route at build time. Each page links the stylesheets of its lazy routes, from the Vite manifest. Withprerender, the dev server renders only those routes, into the same template. Spike: prerender the docs with FastBoot instead of vite-ember-ssr NullVoxPopuli/limber#2298 uses it in place ofvite-ember-ssr.fastboot/middlewareis a(req, res, next)handler fornode:http, Express, Connect, and Vite.ember-cli-fastbootis a v2 addon. A small v1 shim gives classic appsember serverendering and the boot code.Decisions to check
fastboot-express-middlewareandfastboot-app-server.fastboot/middlewarereplaces the first. The app server's cluster and its deploy downloaders and notifiers have no replacement. If you want to keep the app server, I can port it to the new API.patches/@embroider__core@4.6.10.patchfixes a crash in every Vite app with this addon. Embroider walks<app>/_fastboot_for any active addon namedember-cli-fastboot, and that directory does not exist. The fix is on the branchnvp/fastboot-dir-optionalof the agent's Embroider fork, and I have not opened that PR yet. A release of this PR needs that fix released first.ember-source >= 6.4. CI tests onlyember-source7.4.Tests
pnpm testbuilds two fixture apps (a classic app, and the default Vite blueprint with WarpDrive), then runs 83 tests with Vitest:deferRendering, run loop timers,<html>and<body>attributes, errors, concurrent renders, andreload()ember serveprerenderAll tests pass locally on Node 22.22, 24.21, and 26.12.
🤖 Generated with Claude Code