https://divriots.com/blog/vite-in-the-browser
RIOTS logo WHAT WE DOWHO WE AREWORK WITH USBLOGOPEN SOURCE MENU
WHAT WE DOWHO WE AREWORK WITH USBLOGOPEN SOURCE
browser-vite logo and Vite in the browser text Wed, 05 Jan 2022
Vite in the browser
* [francois]
Name
Francois
Twitter
@Gluckies
---------------------------------------------------------------------
We made browser-vite - a patched version of Vite running in the
browser with Workers.
How it works - in a nutshell
* A Service Worker: replaces Vite's HTTP server. Capturing the HTTP
calls of an embedded iframe from example.
* A Web Worker: Run browser-vite to process off the main thread.
* Calls to the file system are replaced by an in-memory file
system.
* Import of files with special extensions (.ts, .tsx, .scss...) are
transformed.
The challenges
No real File System
Vite does a lot with files. The files of the project but also config
files, watchers, and globs. These are difficult to implement in the
browser with a shimmed in-memory FS. We removed watchers, globs, and
config file calls to limit the complexity and the surface API.
The project files stay in the in-memory FS that browser-vite and vite
plugins can access normally.
No "node_modules"
Vite relies on the presence of node_modules to resolve dependencies.
And it bundles them in a Dependencing Pre-Bundling optimization at
startup.
We didn't want to run a node_modules folder in the browser's memory
because we think it's just too much data to download and store into
the browser's memory. So we carefully stripped out node resolvers and
Dependencing Pre-Bundling from Vite.
Users of browser-vite have to create a Vite plugin to resolve bare
module imports.
Our products: Backlight.dev, Components.studio and WebComponents.dev,
are running a server-side bundler optimizer for the past 2 years now.
We created a Vite plugin for browser-vite to resolve node
dependencies automatically. As of the date of this post, this
server-side bundler is not open-sourced.
Regex "lookbehind"
Some regexs in Vite are using lookbehind. This works great locally
when executed by Node.js, but it's not supported in Safari.
So we rewrote the regexs for more browser compatibility.
Hot Module Reload (HMR)
Vite uses WebSockets to communicate code changes from the server
(node) to the client (browser).
In browser-vite, the server is the ServiceWorker + Vite worker and
the client is the iframe. So we changed the communication from
WebSockets to a post message to the iframe.
For this, the client side code of Vite in iframe has been replaced by
a special browser version handling messages outside of WebSockets.
How to use it
As of the time of this writing, it's not a plug and play process.
There is a lot to figure out by reading Vite's internal processing in
order to use browser-vite.
Note: This post may become obsolete over time, so make sure you check
browser-vite's README for always up to date information on
browser-vite's usage.
Installation
Install the browser-vite npm package.
$ npm install --save browser-vite
or
$ npm install --save vite@npm:browser-vite
To channel "vite" imports to "browser-vite".
iframe - window to browser-vite
You need an iframe that will show the pages served internally by
browser-vite.
Service Worker - the in-browser web server
The Service Worker will capture certain URLs requests coming from the
iframe.
Here is an example using workbox.
workbox.routing.registerRoute(
/^https?:\/\/HOST/BASE_URL\/(\/.*)$/,
async ({
request,
params,
url,
}: import('workbox-routing/types/RouteHandler').RouteHandlerCallbackContext): Promise => {
const req = request?.url || url.toString();
const [pathname] = params as string[];
// send the request to vite worker
const response = await postToViteWorker(pathname)
return response;
}
);
Mostly posting a message to the "Vite Worker" using postMessage or
broadcast-channel.
Vite Worker - processing request
The Vite Worker is a Web Worker that will process requests captured
by the Service Worker.
Example of creating a Vite Server:
import {
transformWithEsbuild,
ModuleGraph,
transformRequest,
createPluginContainer,
createDevHtmlTransformFn,
resolveConfig,
generateCodeFrame,
ssrTransform,
ssrLoadModule,
ViteDevServer,
PluginOption
} from 'browser-vite';
export async function createServer(
const config = await resolveConfig(
{
plugins: [
// virtual plugin to provide vite client/env special entries (see below)
viteClientPlugin,
// virtual plugin to resolve NPM dependencies, e.g. using unpkg, skypack or another provider (browser-vite only handles project files)
nodeResolvePlugin,
// add vite plugins you need here (e.g. vue, react, astro ...)
]
base: BASE_URL, // as hooked in service worker
// not really used, but needs to be defined to enable dep optimizations
cacheDir: 'browser',
root: VFS_ROOT,
// any other configuration (e.g. resolve alias)
},
'serve'
);
const plugins = config.plugins;
const pluginContainer = await createPluginContainer(config);
const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url));
const watcher: any = {
on(what: string, cb: any) {
return watcher;
},
add() {},
};
const server: ViteDevServer = {
config,
pluginContainer,
moduleGraph,
transformWithEsbuild,
transformRequest(url, options) {
return transformRequest(url, server, options);
},
ssrTransform,
printUrls() {},
_globImporters: {},
ws: {
send(data) {
// send HMR data to vite client in iframe however you want (post/broadcast-channel ...)
},
async close() {},
on() {},
off() {},
},
watcher,
async ssrLoadModule(url) {
return ssrLoadModule(url, server, loadModule);
},
ssrFixStacktrace() {},
async close() {},
async restart() {},
_optimizeDepsMetadata: null,
_isRunningOptimizer: false,
_ssrExternals: [],
_restartPromise: null,
_forceOptimizeOnRestart: false,
_pendingRequests: new Map(),
};
server.transformIndexHtml = createDevHtmlTransformFn(server);
// apply server configuration hooks from plugins
const postHooks: ((() => void) | void)[] = [];
for (const plugin of plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server));
}
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
postHooks.forEach((fn) => fn && fn());
await pluginContainer.buildStart({});
await runOptimize(server);
return server;
}
Pseudo code to process requests via browser-vite
import {
transformRequest,
isCSSRequest,
isDirectCSSRequest,
injectQuery,
removeImportQuery,
unwrapId,
handleFileAddUnlink,
handleHMRUpdate,
} from 'vite/dist/browser';
...
async (req) => {
let { url, accept } = req
const html = accept?.includes('text/html');
// strip ?import
url = removeImportQuery(url);
// Strip valid id prefix. This is prepended to resolved Ids that are
// not valid browser import specifiers by the importAnalysis plugin.
url = unwrapId(url);
// for CSS, we need to differentiate between normal CSS requests and
// imports
if (isCSSRequest(url) && accept?.includes('text/css')) {
url = injectQuery(url, 'direct');
}
let path: string | undefined = url;
try {
let code;
path = url.slice(1);
if (html) {
code = await server.transformIndexHtml(`/${path}`, fs.readFileSync(path,'utf8'));
} else {
const ret = await transformRequest(url, server, { html });
code = ret?.code;
}
// Return code reponse
} catch (err: any) {
// Return error response
}
}
Check Vite's internal middlewares for more details.
How does it compare to Stackblitz WebContainers
"WebContainers: Run Node.js natively in your browser"
Stackblitz's WebContainers can also run Vite in the browser. You can
elegantly go to vite.new to have a working environment.
We are not experts in WebContainers but, in a nutshell, where
browser-vite shims the FS and the HTTPS server at the Vite level,
WebContainers shims the FS and a lot of other things at the Node.js
level, and Vite runs on it with a few additional changes.
It goes as far as storing a node_modules in the WebContainer, in the
browser. But it doesn't run npm or yarn directly because it would
take too much space (I guess). They aliased these commands to Turbo -
their package manager.
WebContainers can run other frameworks too, like Remix, SvelteKit or
Astro.
It's magical It's mind-blowing We have massive respect for
what the Stackblitz team has built here.
One downside of WebContainers is that it can only run on Chrome today
but will probably run on Firefox soon. browser-vite works on Chrome,
Firefox and Safari today.
In a nutshell, WebContainers operates at a lower level of abstraction
to run Vite in the browser. browser-vite operates at a higher level
of abstraction, very close to Vite itself.
Metaphorically, for the retro-gamers out there, browser-vite is a
little bit like UltraHLE [?]
(*) gametechwiki.com: High/Low level emulation
What's next?
browser-vite is at the heart of our solutions. We are progressively
rolling it out to all our products:
* Backlight.dev
* Components.studio
* WebComponents.dev
* Replic.dev (New app coming up very soon!)
Going forward, we will continue to invest in browser-vite and report
back upstream. Last month, we also announced that we sponsored Vite
via Evan You and Patak to support this wonderful project.
Want to know more?
* GitHub Repository: browser-vite
* Join our Discord server, we have a #browser-vite channel going on
Share
* # Tweet
* # Linkedin
Subscribe to monthly newsletter
[ ]
[Subscribe]
[divriots-l]
WHAT WE DO Backlight.devWebComponents.devComponents.studio
LINKS Blog Careers Press
CONNECT
discord twitter linkedin github rss
CONTACT hello@divriots.com
SHOUTOUTS
Design by [cosavostra]
Built with [astro]