sc4
Version:
A command line utility for automating SimCity 4 modding tasks & modifying savegames
149 lines (148 loc) • 4.85 kB
JavaScript
// # menu-icon-prompt.js
import path from 'node:path';
import fs from 'node:fs';
import sea from 'node:sea';
import http from 'node:http';
import { pathToFileURL } from 'node:url';
import open from 'open';
import { createPrompt, useState, useMemo, usePrefix, makeTheme, } from '@inquirer/core';
import { concatUint8Arrays, isUint8Array, uint8ArrayToBase64 } from 'uint8array-extras';
// The prompt that we use when requesting menu icons from the user. We'll do
// this by starting a server where the browser is doing the job of compiling the
// icons for us.
export const menuIcon = createPrompt((config, done) => {
const { message } = config;
const theme = makeTheme(config.theme);
const [url, setUrl] = useState('');
const [status, setStatus] = useState('idle');
const [address, setAddress] = useState('');
const prefix = usePrefix({ status, theme });
useMemo(() => {
const { default: file, ...rest } = config;
const { server, promises } = runServer({
...file && { default: defaultToUrl(file) },
...rest,
});
promises.listening().then(url => {
setUrl(url);
setAddress(theme.style.help(`(Visit ${url} if the browser doesn't open)`));
});
promises.ready().then(buffer => {
done(buffer);
setStatus('done');
});
return server;
}, []);
return url ? `${prefix} ${message} ${status === 'done' ? theme.style.answer('<icon>') : address}` : '';
});
// # defaultToUrl(file)
function defaultToUrl(file) {
if (isUint8Array(file)) {
return `data:image/png;base64,${uint8ArrayToBase64(file)}`;
}
else {
return pathToFileURL(file);
}
}
// # runServer(config)
// Opens a server and returns a promise that returns when the server is running.
// Use `server.ready()` to get the promise for when the icon was uploaded.
function runServer(config) {
const ready = Promise.withResolvers();
const listen = Promise.withResolvers();
const closed = Promise.withResolvers();
const send = getSendHandler();
const server = http.createServer(handleError(async (req, res) => {
if (req.url === '/data') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(config));
}
else if (req.url === '/favicon.ico') {
res.statusCode = 404;
res.end();
}
else if (req.url.startsWith('/fetch?')) {
let parsed = new URL(req.url, 'http://127.0.0.1');
let url = new URL(parsed.searchParams.get('url'));
if (url.protocol === 'file:') {
fs.createReadStream(url).pipe(res);
}
else {
let buffer = await fetch(url).then(res => res.arrayBuffer());
res.end(new Uint8Array(buffer));
}
}
else if (req.url === '/upload') {
let parts = [];
for await (let chunk of req) {
parts.push(chunk);
}
res.statusCode = 202;
res.end();
ready.resolve(concatUint8Arrays(parts));
}
else {
let id = req.url === '/' ? '/index.html' : req.url;
send(res, id);
}
}));
const promises = {
ready: () => ready.promise,
listening: () => listen.promise,
closed: () => closed.promise,
};
server.listen({
host: '127.0.0.1',
port: 0,
}, () => {
const { address, port } = server.address();
const url = `http://${address}:${port}`;
listen.resolve(url);
open(url);
});
// Close the server again when ready.
ready.promise.then(() => {
server.closeAllConnections();
server.close(() => closed.resolve(void 0));
});
return { server, promises };
}
// # handleError(fn)
function handleError(fn) {
return async (req, res) => {
try {
return await fn(req, res);
}
catch (e) {
res.statusCode = 500;
res.end(e.message);
}
};
}
// # getSendHandler()
function getSendHandler() {
return sea.isSea() ? sendSea : sendNode;
}
// # sendSea(res, key)
function sendSea(res, key) {
let asset = new Uint8Array(sea.getAsset(`assets${key}`));
sendType(res, key);
res.end(asset);
}
// # sendNode();
function sendNode(res, key) {
sendType(res, key);
let filePath = import.meta.resolve(`#cli/assets${key}`);
let stream = fs.createReadStream(new URL(filePath));
stream.pipe(res);
}
// # sendType(res, file)
function sendType(res, file) {
let ext = path.extname(file);
res.setHeader('Content-Type', mimes[ext]);
}
const mimes = {
'.html': 'text/html',
'.png': 'image/png',
'.js': 'text/javascript',
};