Jinjiang
Posted on March 7, 2024
Today, I'd like to introduce Vlite, a lite demo server inspired by Vite.
Usage
With Vlite, you can show your ideas in an instant way.
To install Vlite:
npm install -g @jinjiang/vlite
# or
yarn global add @jinjiang/vlite
# or
pnpm add -g @jinjiang/vlite
Now you can prepare an index.html
and further write your Vue, React, or vanilla code besides it. e.g.:
Then just run vlite
on this directory, you will preview your app on your browser. That's it. No npm installation, no build steps, just works like a static local server.
The idea
It came out when I built some quick frontend demos. Usually there are 3 possible ways to do that:
- use static server like
http-serve
orserve
, with which you can write HTML, CSS, and JavaScript that browsers natively support. - use a local dev env like Vite, more or less you have to set up your local project and install dependencies via a package manager like npm.
- use an online edit-n-preview platform like CodeSandbox or StackBlitz.
For me, none of them was straightforward enough. So I was wondering whether we could have a more instant way to achieve that. It was better to be local first, but not too many steps to get started.
At the same time, I'd like to put some extra considerations and trade-offs in:
Target 1: ESM-friendly
Nowadays, all the browsers natively support ES Modules very well. That means you don't have to bundle your large project to run. We can just treat every file as an ESM to response.
For the possible upstream and downstream workflow, I think sticking on ESM is always a good idea. For example, I've noticed a new proposal from tc39 called "JavaScript Module Declarations" to inline your modules. Maybe one day we can further bundle your project in a simple action with this. That's why I'd like to make it as ESM-friendly as possible.
Target 2: Simplifying the compilations
For local demos, I can see a lot of potentials to simplify the compilations.
- Polyfills and syntax sugar compilations can be skipped for local demos.
- I feel the frontend syntax choices are slightly converging. And their compilers are more and more mature. I decided to start from React and Vue projects, which include TS/JSX, CSS, and Vue SFC.
- You can find faster and simpler options every day. They make the compilation for each file on the fly possible.
- Removing all the optional processes like HMR, SSR, source maps, or any other DX enhancements.
It's just for local demos, right? So taking all above is not a big deal. On the other hand, you might be surprised how simple it could be. 😉
How it works?
Basically, the whole tool consists of 3 parts:
- a http server to accept requests
- a plugin system to manage the compilation process
- a set of plugins to handle all kinds of files into ESMs
A simple plugin system
The plugin system is heavily inspired by Vite/Rollup. I have to say their plugin system is SO good.
In Vlite, the plugin system only accepts 2 hooks (in pseudo code):
-
load(filepath): string | Buffer | undefined
: First of all, the default loader is always the plain file reader. And every plugin has a chance to overwrite the result by returning a string or a buffer -
transform(filepath, queryParams, content): string | Buffer | undefined
: Every plugin has a chance to handle it. You can detect whether to handle or skip it by checking the file extension name or query params.
Comparing to Vite/Rollup:
- I removed the
resolveId()
hook since technically every request should correspond to a local file. - I provided a default
load()
behavior to read content from file system. In all the cases so far it's enough. Theload()
hook was only designed for future usage like virtual modules. - I didn't introduce source maps neither. So the input and output could be simple and straightforward.
Compiler choices
For TS and JSX files, I chose sucrase, the same choice as Vue Playground made. It's super fast and esay to config.
For CSS files, I converted each of them into ESM code with document.createElement('style')
and document.head.appendChild()
.
e.g. imaging a CSS file:
body { margin: 0 }
it will be processed into:
const style = document.createElement('style')
style.textContent = 'body { margin: 0 }'
document.head.appendChild(style)
- You might ask how to deal with CSS Modules and their generated class names. I used postcss-modules to convert it and export those class names as a JSON object.
e.g. a CSS Module file:
.foo { color: red }
will be processed into:
const style = document.createElement('style')
style.textContent = '.foo_xxxxxx { color: red }'
document.head.appendChild(style)
export default { foo: 'foo_xxxxxx' }
For Vue SFCs, I reused another package that I created: vue-simple-compiler. It can compile a Vue SFC into plain JavaScript and CSS imports.
For assets like images, it's a little bit tricky. Since there are 2 ways to consume them:
- request the file in browsers,
- import the file path in JavaScript.
So I introduced a special query parameter ?url
. If the request has this query parameter, the server will respond with the file path as the default export in ESM.
e.g. for an image logo.png
:
- If you request it directly from the browser, you will get the image file in binary data.
- If you write
import logo from './logo.png'
in JS/TS, the import statement will be compiled intoimport logo from './logo.png?url'
. Then the request/logo.png?url
would returnexport default '/logo.png'
. So when you further write code like<img src={logo} />
it will work as well.
For npm packages, I used a ES imports parser parse-imports to find them, and replaced each of them with a esm.sh URL. It's a fast CDN providing npm packages in ESM on the fly.
e.g. the import import { createApp } from 'vue'
will be compiled into import { createApp } from 'https://esm.sh/vue'
.
Conclusions and future plans
That's all above Vlite. It's designed for quick demos with simplicity and straightforwardness. No extra stuff like HMR, SSR, source maps, etc.
To be honest, I still have some other ideas to put in, like:
- Pick more performant parsers and compilers.
- Support a few more frameworks, probably.
- SPA mode to serve HTML entry file in any case the request path doesn't match a file.
- Offline mode by caching certain packages from esm.sh
- Build mode to generate all the files into ESM for further static servers usage.
- A new "bundle" mode to bundle generated file into one piece by JavaScript Module Declarations
Anyway, just all for quick demos. And you are very welcome to give a try.
Cheers.
Posted on March 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.