Getting Started
Welcome to Commoners! This guide will help you get started with building your first cross-platform application.
Since Commoners is built on top of Vite, you can use the create-vite
package to scaffold a new project. To do this, run the following command in your terminal:
npm create vite@latest my-commoners-app
Follow the prompts to select your favorite framework and features.
Installing commoners
After running npm install
, add Commoners as a dependency:
npm install commoners
Configuring the package.json
File
Then, modify scripts
in your package.json
to include the following:
{
"scripts": {
"start": "commoners --target desktop",
"dev": "commoners",
"build": "commoners build",
"build:desktop": "commoners build --target desktop"
}
}
Now you can run npm start
to start your application as an Electron desktop application, npm run dev
to start your application as a web application, and the npm run build
commands to create different distributions of your application.
Getting Started
commoners
allows you to customize your application by adding a commoners.config.js
file to the root of your project. This file can be used to configure your application's services, plugins, and more.
For example, you can add the following to your commoners.config.js
file to customize your application's name and icon:
export default {
name: 'My App',
icon: './assets/vite.png', // Manually converted from public/vite.svg
}
All of the available configuration options are documented in the Configuration documentation.
In your built application, you can access Commoners configuration values using the commoners
object:
console.log(commoners) // { NAME: 'My App', VERSION: '0.0.0', ICON: '<path>', DESKTOP: true, READY: Promise, SERVICES: { ... } }
Try replacing the default h1
and img
tags with your custom NAME
and ICON
using the commoners
global variable!
Adding Services
To add services to your Commoners application in development, simply add fill out the services
property of the configuration file.
export default {
// ...
services: {
node: './src/services/node.js',
tsNode: './src/services/node.ts',
python: './src/services/python.py',
}
}
We currently support JavaScript, TypeScript, and Python services.
- JavaScript / TypeScript: Reference a
.js
or.ts
file to run using Node.js. - Python: Reference a
.py
file to run using Python.- Ensure that you're running a virtual environment with the necessary dependencies. We recommend using miniconda and distributing an
environment.yml
file with your project.
- Ensure that you're running a virtual environment with the necessary dependencies. We recommend using miniconda and distributing an
For all services, you'll want to ensure that CORS is enabled to support communication between the service and the application.
Example Services
JavaScript / TypeScript
Here is an example HTTP server written in TypeScript.
import http from 'node:http';
const host = process.env.HOST
const port = process.env.PORT
const server = http.createServer((
req: http.IncomingMessage,
res: http.ServerResponse
) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Echo Request
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => body += chunk.toString());
req.on('end', () => {
res.writeHead(200, { 'Content-Type': req.headers['content-type'] });
res.end(body);
});
return;
}
// Default Response
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
return;
});
server.listen(port,
() => console.log(`Server running at http://${host}:${port}/`)
);
Python
Here is an example HTTP server written in Python.
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def _set_default_headers(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')
self.send_header('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
# Hello World
def do_GET(self):
self._set_default_headers()
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'Hello World')
# Echo
def do_POST(self):
self._set_default_headers()
self.send_header('Content-type', self.headers['Content-Type'])
self.end_headers()
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
self.wfile.write(post_data)
PORT = int(os.getenv('PORT', 8000))
HOST = os.getenv('HOST', '')
server_address = (HOST, PORT)
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
httpd.serve_forever()
Using Services
To use services in your application, you can import them using the commoners
global variable:
const { SERVICES } = commoners
const nodeServiceUrl = SERVICES.node.url
const tsNodeServiceUrl = SERVICES.tsNode.url
const pythonServiceUrl = SERVICES.python.url
Use the following code snippet and update the button to fetch data from your services!
const responses = await Promise.allSettled(Object.values(SERVICES).map(({ url }) => fetch(url).then(response => response.text())))
Building for Production
Application
To build your application for production, run the following command:
commoners build --target [platform]
Services
Service builds are generally deferred to the user.
commoners build --service node --service tsNode # Specified services are built
In the case of desktop builds, all relevant services are bundled by default.
commoners build --target desktop
Configuration
JavaScript / TypeScript
JavaScript and TypeScript services are auto-bundled using esbuild and pkg.
To package these for distribution and/or indicate they'll be hosted on a remote server, you'll add the following configuration:
export default {
// ...
services: {
// Simple configuration
node: './src/services/node.js', // Only included with Desktop builds
// Advanced configuration
tsNode: {
src: './src/services/node.ts',
// url: undefined, // Removed on remote builds (web, mobile)
// url: 'https://ts-node.example.com', // Remote for all builds (web, mobile, desktop)
// url: { remote: 'https://ts-node.example.com' }, // Remote for remote builds (web, mobile). Local for local builds (desktop)
url: { local: 'https://ts-node.example.com' }, // Remote for local builds (desktop). Removed on remote builds (web, mobile)
}
}
}
Python
For Python services, you'll to specify a terminal command that will bundle the service into a standalone executable file. We recommend using pyinstaller
for this purpose.
export default {
// ...
services: {
python: {
src: './src/services/python.py',
url: { remote: 'https://python.example.com' },
// The build command
build: 'python -m PyInstaller --name python-service --onedir --clean ./src/services/python.py --distpath ./build/python',
// What to include from the build
publish: {
base: './build/python/python-service', // The base directory to copy
src: 'python-service', // The relative source location in the base directory
}
}
}
}
Testing
Using the @commoners/testing
package, you can easily write end-to-end tests for your application.
npm install @commoners/testing
Then, add the following to your package.json
:
{
"scripts": {
"test": "commoners test"
}
}
We use vitest
to run tests—but you can use any testing framework you like.
Here's an example test for a Web + Desktop application:
import { expect, test, describe, beforeAll, afterAll } from 'vitest'
import { open, build } from '../../testing/index'
const ROOT = '../my/app'
const OUTDIR = 'dist'
const registerTests = (prod = false) => {
const OUTPUTS = {}
const opts = { build: { outDir: OUTDIR } }
beforeAll(async () => {
if (prod) OUTPUTS.build = await build(ROOT, opts)
OUTPUTS.app = await open(ROOT, opts, prod)
})
afterAll(async () => Object.values(OUTPUTS).forEach(o => o.cleanup()))
test('should load the app', async () => {
expect(await OUTPUTS.app.page.title()).toBe('Test App')
})
test('should have global variable', async () => {
expect(await OUTPUTS.app.page.evaluate(() => commoners.NAME)).toBe('Test App')
})
}
describe('App runs in development mode', () => registerTests(false))
describe('App runs in production mode', () => registerTests(true))
Release Management
Using GitHub Actions, you can automate the release of your application to GitHub Pages, GitHub Releases, and mobile app stores.
Demo coming soon...
Monorepo Development
Commoners supports monorepo development using PNPM. To get started, run the following commands:
corepack enable pnpm && pnpm install
This will install all dependencies for the monorepo.
Each application can be managed with the following command syntax:
commoners ./apps/my-app # Run the application in the specified directory
This allows you to manage multiple applications in a single repository, such as separate desktop and mobile applications that share a common core (e.g. services, plugins, frontend components, etc.).