anvilsign in

collin/mahjong

1#!/usr/bin/env node
2// Deploy the built game to antics.gg as a multi-file bundle.
3//
4// npm run build && node scripts/antics-deploy.mjs # keyless: a fresh link each time
5// node scripts/antics-deploy.mjs --project <proj_id> # under a project: stable /p/ share link
6//
7// A keyless deploy needs no account at all. Deploying under a project needs a
8// token from `npx antics-cli login`, read from ANTICS_TOKEN or ~/.antics/token.
9
10import { readdir, readFile } from 'node:fs/promises';
11import { homedir } from 'node:os';
12import { join, relative, extname } from 'node:path';
13
14const HOST = process.env.ANTICS_HOST || 'https://antics.gg';
15const DIST = new URL('../dist', import.meta.url).pathname;
16
17// Anything not in this list rides along as a data: URI.
18const TEXT = new Set(['.html', '.js', '.css', '.svg', '.json', '.webmanifest', '.txt', '.map']);
19const MIME = {
20 '.png': 'image/png',
21 '.jpg': 'image/jpeg',
22 '.webp': 'image/webp',
23 '.mp3': 'audio/mpeg',
24 '.woff2': 'font/woff2',
25 '.ico': 'image/x-icon',
26};
27
28async function* walk(dir) {
29 for (const e of await readdir(dir, { withFileTypes: true })) {
30 const p = join(dir, e.name);
31 if (e.isDirectory()) yield* walk(p);
32 else yield p;
33 }
34}
35
36async function collect() {
37 const files = {};
38 const voice = {};
39 let bytes = 0;
40 for await (const p of walk(DIST)) {
41 const rel = relative(DIST, p);
42 const ext = extname(rel);
43 const buf = await readFile(p);
44 bytes += buf.length;
45 // A bundle may hold at most 50 files and the voice pack alone is 57, so
46 // the clips ship as one JSON of base64 — sound.ts prefers it when present.
47 if (rel.startsWith('voice/') && ext === '.mp3') {
48 voice[rel.slice('voice/'.length, -'.mp3'.length)] = buf.toString('base64');
49 continue;
50 }
51 if (rel === 'voice/manifest.json') continue;
52 files[rel] = TEXT.has(ext)
53 ? buf.toString('utf8')
54 : `data:${MIME[ext] ?? 'application/octet-stream'};base64,${buf.toString('base64')}`;
55 }
56 if (Object.keys(voice).length) files['voice/pack.json'] = JSON.stringify(voice);
57 return { files, bytes };
58}
59
60async function token() {
61 if (process.env.ANTICS_TOKEN) return process.env.ANTICS_TOKEN;
62 try {
63 return (await readFile(join(homedir(), '.antics', 'token'), 'utf8')).trim();
64 } catch {
65 return null;
66 }
67}
68
69const args = process.argv.slice(2);
70const projectAt = args.indexOf('--project');
71const project = projectAt >= 0 ? args[projectAt + 1] : null;
72
73const { files, bytes } = await collect();
74console.log(`deploying ${Object.keys(files).length} files, ${(bytes / 1024 / 1024).toFixed(1)} MB from dist/`);
75
76const body = { files };
77const headers = { 'content-type': 'application/json' };
78if (project) {
79 body.project = project;
80 const t = await token();
81 if (!t) {
82 console.error('a --project deploy needs a token: run `npx antics-cli login`, or set ANTICS_TOKEN');
83 process.exit(1);
84 }
85 headers.authorization = `Bearer ${t}`;
86}
87
88const res = await fetch(`${HOST}/api/deploy`, {
89 method: 'POST',
90 headers,
91 body: JSON.stringify(body),
92});
93const out = await res.json().catch(() => ({}));
94if (!res.ok) {
95 console.error(`deploy failed: ${res.status}`, out);
96 process.exit(1);
97}
98console.log('deployed.');
99if (out.playUrl) console.log(` play (this build): ${out.playUrl}`);
100if (out.shareUrl) console.log(` share (follows updates): ${out.shareUrl}`);
101if (out.hash) console.log(` hash: ${out.hash}`);