Creating a standalone web application
Jul 19, 2023
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
, changehref
to./
<!-- index.html -->
<base href="./" />
- Run
npm run build
(orng build
, if you have Angular CLI installed) - Open up
dist/index.html
and 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.