Creating a standalone web application
One of the first questions to answer when building a web application for a client is “where will it be deployed?“. While the list of hosting providers continues to grow, occasionally I have run into situations where the client needs an application that they can run on their desktop. Recently, I faced the following set of constraints:
- Needs a Single-Page Application (SPA)
- Must be able to run locally (without an internet connection)
- Cannot execute custom executables
- Cannot execute bash scripts like
.sh
That really narrowed down my options quickly… can’t deploy online, can’t create an Electron executable for them to run, and can’t even create a script to spin up a local server. I could give users step-by-step instructions on how to open the command line in a given folder and a start a simple web server, but that’s adding too many steps for them to run.
So to get around all of those constraints, I decided to just give them a folder containing the built
application as an index.html file and all of its supporting files (.js, .css, assets, etc).
Then, they could just double-click index.html and open the application in the browser. It should
function as expected via the (file:// protocol)[TODO:].
This approach looks a little different in each of the modern frameworks, so below I’ve outlined the steps to take in each of them. But first, some caveats:
Caveats
- Routing will not work, so keep your application to a single page (TODO:)
- Any custom fonts need to be included in the output directory. I recommend loading your fonts with fontsource which will do this automatically.
Frameworks
Nextjs
Shoutout to Next, who make this process easy via their export configuration.
- Create a new Nextjs application
npx create-next-app@latest - Add the following to your next config:
// next.config.js
const nextConfig = {
// ...
output: 'export',
assetPrefix: './',
// ...
}; - Make sure you don’t have any imports with
/in your code. As of writing, the default app includes imports like/vercel.svg, which will need to be changed to./vercel.svg - Run
npm run build
The out/ directory will contain a standalone application that can be run by double-clicking the index.html file.
SvelteKit
Angular
Angular is a bit trickier because the index.html file imports scripts using type=module, which
is not supported when running applications via the file:// protocol.
- Create a new Angular application
npm init @angular angular myApp - In
index.html, changehrefto./
<!-- index.html -->
<base href="./" /> - Run
npm run build(orng build, if you have Angular CLI installed) - Open up
dist/index.htmland remove all instances oftype=module
Automating with a script
It’s a little tedious to have to scrub the dist/index.html file after every build. So instead, we
can create a script to do it for us:
// scripts/build-standalone.js
const fs = require('fs');
const jsdom = require('jsdom');
const path = require('path');
const { JSDOM } = jsdom;
var argv = require('minimist')(process.argv.slice(2));
// Normalize so that this can be run on any OS
const distPath = path.normalize('./dist/standalone');
const indexPath = argv._.at(0) ?? path.join(distPath, 'index.html');
if (!fs.existsSync(indexPath)) {
console.error('Invalid file path:', indexPath);
}
const dom = await JSDOM.fromFile(indexPath);
// Remove `type="module"` from all of the scripts
dom.window.document.querySelectorAll('script').forEach((script) => {
script.removeAttribute('type');
});
// Write a new index.html file in place of the old one
fs.writeFileSync(
indexPath,
'<!DOCTYPE html>' + dom.window.document.documentElement.outerHTML,
function (error) {
if (error) throw error;
},
); To use this script, add a new command to package.json:
{
"scripts": {
"build": "ng build",
"build:standalone": "npm run build -- --output-path=dist/standalone && node ./scripts/build-standalone.js"
}
} Then you can simply run npm run build:standalone. The dist/standalone/ directory will contain a
standalone application that can be run by double-clicking the index.html file.
Edit this page