Skip to content

Commit c168e08

Browse files
authored
refactor(examples): streamline HTTPS dev cert workflow (#8759)
* feat(examples): add HTTPS dev cert workflow * fix(certificates): rename devHttpsConfig to httpsConfig for consistency
1 parent 6189ec2 commit c168e08

6 files changed

Lines changed: 111 additions & 131 deletions

File tree

examples/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,20 @@ Or directly from the source:
3838
ENGINE_PATH=../src/index.js npm run dev
3939
```
4040

41+
The dev server binds to `0.0.0.0:5555` by default. Use `EXAMPLES_HOST` or
42+
`EXAMPLES_PORT` to override those values.
43+
4144
## HTTPS dev for mobile / XR device testing
4245

43-
`npm run develop` serves the examples browser over plain HTTP on `localhost`,
46+
`npm run dev` serves the examples browser over plain HTTP on `localhost`,
4447
which is enough for everyday work — browsers treat `localhost` as a secure
4548
context, so WebGPU and WebXR features that require one still work.
4649

4750
For testing on a phone, tablet, Quest, or Apple Vision Pro you need to reach
4851
the dev server over the LAN, and the device will require HTTPS for WebXR (and
49-
exposes WebGPU only in a secure context). This is what `npm run develop:https`
50-
is for. Certs are generated locally with [mkcert](https://github.com/FiloSottile/mkcert);
52+
exposes WebGPU only in a secure context). This is what `npm run dev:https` is
53+
for. Use `npm run develop:https` when automatic reloads should be disabled.
54+
Certs are generated locally with [mkcert](https://github.com/FiloSottile/mkcert);
5155
none are committed to the repo.
5256

5357
### One-time setup
@@ -68,13 +72,13 @@ none are committed to the repo.
6872
```
6973
npm run cert
7074
```
71-
Output: `examples/.cert/dev-cert.pem` and `dev-key.pem`. The script prints
75+
Output: `examples/.cert/cert.pem` and `key.pem`. The script prints
7276
the LAN URL to open.
7377

7478
### Day-to-day
7579

7680
```
77-
npm run develop:https
81+
npm run dev:https
7882
```
7983

8084
Opens on:
@@ -133,9 +137,6 @@ These are added on top of the defaults (`localhost`, `127.0.0.1`, `::1`,
133137
`<hostname>.local`). Already-trusted devices stay trusted — the leaf cert is
134138
re-signed by the same root CA, no re-install needed.
135139

136-
`EXAMPLES_CERT_HOSTS=<comma-separated>` is also accepted as a fallback if
137-
you'd rather set an env var.
138-
139140
### What's expensive vs cheap to lose
140141

141142
- **Root CA** (in `$(mkcert -CAROOT)`) — expensive. Re-creating it means

examples/build.mjs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,31 @@ import fs from 'node:fs';
22
import { parseArgs } from 'node:util';
33

44
import { buildProd } from './utils/build-prod.mjs';
5+
import { generateCertificates } from './utils/certificates.mjs';
56
import { buildMetadata } from './utils/metadata.mjs';
67
import { buildThumbnails } from './utils/thumbnails.mjs';
78

89
const USAGE = `Usage: node build.mjs [options]
910
1011
Options:
12+
--cert Generate local HTTPS dev certificates
1113
--metadata Generate cache/metadata.mjs
1214
--thumbnails Generate thumbnails
1315
--clean Remove dist and cache
1416
--debug Enable thumbnail debug logging
1517
--help, -h Show help`;
1618

17-
const { values } = parseArgs({
19+
const { values, positionals } = parseArgs({
1820
args: process.argv.slice(2),
1921
options: {
22+
cert: { type: 'boolean' },
2023
metadata: { type: 'boolean' },
2124
thumbnails: { type: 'boolean' },
2225
clean: { type: 'boolean' },
2326
debug: { type: 'boolean' },
2427
help: { type: 'boolean', short: 'h' }
2528
},
26-
allowPositionals: false
29+
allowPositionals: true
2730
});
2831

2932
/**
@@ -45,6 +48,17 @@ const main = async () => {
4548
return;
4649
}
4750

51+
if (values.cert) {
52+
process.exitCode = generateCertificates(positionals) ? 0 : 1;
53+
return;
54+
}
55+
56+
if (positionals.length) {
57+
console.error(USAGE);
58+
process.exitCode = 1;
59+
return;
60+
}
61+
4862
if (values.metadata) {
4963
await buildMetadata();
5064
return;

examples/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
"build": "cross-env NODE_ENV=production node build.mjs",
99
"build:metadata": "node build.mjs --metadata",
1010
"build:thumbnails": "node build.mjs --thumbnails",
11-
"cert": "node utils/generate-certs.mjs",
11+
"cert": "node build.mjs --cert",
1212
"clean": "node build.mjs --clean",
1313
"dev": "npm run -s build:metadata && cross-env NODE_ENV=development vite",
14+
"dev:https": "npm run -s build:metadata && cross-env NODE_ENV=development EXAMPLES_HTTPS=1 vite",
1415
"develop": "npm run -s build:metadata && cross-env NODE_ENV=development EXAMPLES_AUTO_RELOAD=false vite",
1516
"develop:https": "npm run -s build:metadata && cross-env NODE_ENV=development EXAMPLES_AUTO_RELOAD=false EXAMPLES_HTTPS=1 vite",
1617
"lint": "eslint .",

examples/utils/certificates.mjs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { spawnSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
6+
const CERT_DIR = path.resolve('.cert');
7+
const CERT_FILE = path.join(CERT_DIR, 'cert.pem');
8+
const KEY_FILE = path.join(CERT_DIR, 'key.pem');
9+
const MKCERT_HELP = `mkcert failed.
10+
11+
Install mkcert, run:
12+
mkcert -install
13+
14+
Then re-run:
15+
npm run cert`;
16+
17+
export const ALLOWED_HOSTS = ['localhost', '.local'];
18+
19+
/**
20+
* @returns {{ cert: Buffer, key: Buffer } | undefined} vite https config.
21+
*/
22+
export const httpsConfig = () => {
23+
if (process.env.EXAMPLES_HTTPS !== '1') {
24+
return undefined;
25+
}
26+
27+
if (!fs.existsSync(CERT_FILE) || !fs.existsSync(KEY_FILE)) {
28+
throw new Error(
29+
`HTTPS dev requested but certs not found in ${CERT_DIR}. ` +
30+
'Run "npm run cert" to generate them.'
31+
);
32+
}
33+
34+
return {
35+
cert: fs.readFileSync(CERT_FILE),
36+
key: fs.readFileSync(KEY_FILE)
37+
};
38+
};
39+
40+
/**
41+
* @param {string[]} hosts - extra host names or lan ips.
42+
* @returns {boolean} true if certs were generated.
43+
*/
44+
export const generateCertificates = (hosts) => {
45+
fs.mkdirSync(CERT_DIR, { recursive: true });
46+
47+
const local = process.platform === 'darwin' ?
48+
spawnSync('scutil', ['--get', 'LocalHostName'], { encoding: 'utf8' }) : null;
49+
const host = local?.status === 0 && local.stdout.trim() ?
50+
local.stdout.trim() : os.hostname().replace(/\.local$/, '');
51+
const sans = [...new Set([
52+
'localhost',
53+
'127.0.0.1',
54+
'::1',
55+
`${host}.local`,
56+
...hosts.map(value => value.trim()).filter(Boolean)
57+
])];
58+
59+
console.log(`Generating dev cert for: ${sans.join(', ')}`);
60+
61+
const result = spawnSync('mkcert', ['-cert-file', CERT_FILE, '-key-file', KEY_FILE, ...sans], {
62+
stdio: 'inherit'
63+
});
64+
if (result.status !== 0) {
65+
console.error(MKCERT_HELP);
66+
return false;
67+
}
68+
69+
console.log(`Wrote ${path.relative(process.cwd(), CERT_FILE)}
70+
Wrote ${path.relative(process.cwd(), KEY_FILE)}
71+
72+
Start the HTTPS dev server with:
73+
npm run dev:https
74+
75+
Or without automatic reloads:
76+
npm run develop:https
77+
78+
Then open on this machine:
79+
https://${host}.local:${process.env.EXAMPLES_PORT ?? 5555}
80+
https://localhost:${process.env.EXAMPLES_PORT ?? 5555}`);
81+
82+
return true;
83+
};

examples/utils/generate-certs.mjs

Lines changed: 0 additions & 99 deletions
This file was deleted.

examples/vite.config.mjs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,15 @@
1-
import fs from 'node:fs';
21
import path from 'node:path';
32

43
import { defineConfig } from 'vite';
54

65
import { revision, version } from '../utils/rollup-version-revision.mjs';
6+
import { ALLOWED_HOSTS, httpsConfig } from './utils/certificates.mjs';
77
import { examplesDevServer } from './utils/vite-dev-server.mjs';
88

99
const HOST = process.env.EXAMPLES_HOST ?? '0.0.0.0';
1010
const PORT = Number(process.env.EXAMPLES_PORT ?? 5555);
1111
const AUTO_RELOAD = process.env.EXAMPLES_AUTO_RELOAD !== 'false';
1212

13-
const CERT_DIR = path.resolve('.cert');
14-
const CERT_FILE = path.join(CERT_DIR, 'dev-cert.pem');
15-
const KEY_FILE = path.join(CERT_DIR, 'dev-key.pem');
16-
17-
const httpsConfig = () => {
18-
if (process.env.EXAMPLES_HTTPS !== '1') return undefined;
19-
if (!fs.existsSync(CERT_FILE) || !fs.existsSync(KEY_FILE)) {
20-
throw new Error(
21-
`HTTPS dev requested but certs not found in ${CERT_DIR}. ` +
22-
'Run "npm run cert" to generate them.'
23-
);
24-
}
25-
return {
26-
cert: fs.readFileSync(CERT_FILE),
27-
key: fs.readFileSync(KEY_FILE)
28-
};
29-
};
30-
31-
const ALLOWED_HOSTS = ['localhost', '.local'];
32-
3313
const examplesPreviewServer = () => ({
3414
name: 'playcanvas-examples-preview-server',
3515

0 commit comments

Comments
 (0)