cybermax / studwright /app.js
CyberMax-tools's picture
Deploy studwright to the hub
5e00f68 verified
Raw History Blame Contribute Delete
6.6 kB
// Studwright hub page: module catalogue, test list and an in-browser Luau playground for the free modules.
// © 2026 CyberMax.
const $ = (s) => document.querySelector(s);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const toast = (msg) => { const t = $('#toast'); t.textContent = msg; t.classList.add('on'); setTimeout(() => t.classList.remove('on'), 2200); };
const EXAMPLES = {
format: `local Format = require(Studwright.Format)
print(Format.abbreviate(1530000)) -- coins on a simulator HUD
print(Format.abbreviate(999999)) -- never rounds up into the wrong suffix
print(Format.abbreviate(2.5e15))
print(Format.commas(1234567))
print(Format.clock(3725), Format.duration(3725))
print("You placed " .. Format.ordinal(22))
print("Legendary drop chance: " .. Format.percent(0.0125))`,
signal: `local Signal = require(Studwright.Signal)
local coinsChanged = Signal.new()
local hud = coinsChanged:Connect(function(new, delta)
print(("HUD: %d coins (%+d)"):format(new, delta))
end)
coinsChanged:Once(function() print("first coins ever: show a tip") end)
coinsChanged:Fire(10, 10)
coinsChanged:Fire(25, 15)
hud:Disconnect()
coinsChanged:Fire(30, 5) -- nothing listening any more
print("still connected?", hud.Connected)`,
cleanup: `local Cleanup = require(Studwright.Cleanup)
local Signal = require(Studwright.Signal)
local round = Cleanup.new()
local tick = Signal.new()
round:Connect(tick, function(n) print("round tick", n) end)
round:Add(function() print("map reset") end)
round:Add({ Destroy = function() print("boss despawned") end })
tick:Fire(1)
print(round:Count(), "things to clean")
round:Clean() -- reverse order: boss, map, then the connection
tick:Fire(2) -- prints nothing: the connection is gone`,
};
// ---- module catalogue + tests ---------------------------------------------------------------
async function loadCatalogue() {
const data = await (await fetch('modules.json')).json();
$('#mods').innerHTML = data.modules.map((m) => `
<details class="mod${m.free ? ' free' : ''}">
<summary><span class="nm">${esc(m.name)}</span><span class="side ${m.side.toLowerCase()}">${esc(m.side)}</span>${m.free ? '<span class="free-badge">FREE</span>' : ''}
<span class="sm">${esc(m.summary)}</span></summary>
<pre><code>${esc(m.usage || '')}</code></pre>
<p class="hint">${m.lines} lines${m.free ? ` · <a href="free/${esc(m.name)}.luau" target="_blank" rel="noopener">view the full source</a>` : ''}</p>
</details>`).join('');
}
async function loadTests() {
const r = await fetch('tests.json');
if (!r.ok) return;
const t = await r.json();
$('#test-count').textContent = t.pass;
const items = t.tests.map((x) => `<li class="${x.ok ? 'ok' : 'bad'}">${x.ok ? '✓' : '✗'} ${esc(x.name)}</li>`);
const list = $('#tests');
list.innerHTML = items.slice(0, 12).join('');
$('#more-tests').addEventListener('click', (e) => { list.innerHTML = items.join(''); e.target.remove(); });
}
// ---- playground --------------------------------------------------------------------------------
let LuauState = null;
const sources = {};
const PRELUDE = `
local __out = {}
local function print(...)
local n = select("#", ...)
local parts = table.create(n)
for i = 1, n do parts[i] = tostring((select(i, ...))) end
table.insert(__out, table.concat(parts, " "))
end
local function warn(...) print("warning:", ...) end
local task = {}
function task.spawn(f, ...)
local th = type(f) == "thread" and f or coroutine.create(f)
local ok, err = coroutine.resume(th, ...)
if not ok then print("error: " .. tostring(err)) end
return th
end
task.defer = task.spawn
function task.cancel(th) if coroutine.status(th) == "suspended" then coroutine.close(th) end end
function task.wait() return 0 end
local function typeof(v) return type(v) end
`;
async function loadVM() {
try {
({ LuauState } = await import('./vendor/luau-web/index.js'));
for (const n of ['Signal', 'Cleanup', 'Format']) sources[n] = await (await fetch(`free/${n}.luau`)).text();
$('#run').disabled = false;
$('#vm').textContent = 'Luau VM ready.';
} catch (e) {
$('#vm').textContent = 'Could not load the Luau VM in this browser. You can still download the free modules below.';
}
}
function bundle(user) {
const defs = Object.entries(sources).map(([n, src]) => `__defs[${JSON.stringify(n)}] = function(script)\n${src}\nend`).join('\n');
return `${PRELUDE}
local __defs, __cache = {}, {}
local __folder = setmetatable({}, { __index = function(_, k) return { __module = k } end })
local function require(ref)
local name = ref.__module
assert(__defs[name], "only the free modules (Signal, Cleanup, Format) run here; the kit has all 16")
if __cache[name] == nil then __cache[name] = __defs[name]({ Parent = __folder, Name = name }) end
return __cache[name]
end
${defs}
local Studwright = __folder
local __ok, __err = pcall(function()
${user}
end)
if not __ok then print("error: " .. tostring(__err)) end
return table.concat(__out, "\\n")`;
}
async function run() {
const btn = $('#run');
btn.disabled = true;
const out = $('#out');
try {
const st = await LuauState.createAsync({});
const fn = st.loadstring(bundle($('#code').value), 'playground', false);
if (typeof fn !== 'function') {
out.textContent = 'Syntax error: ' + String(fn).replace(/\[string "playground"\]:\d+: /, '');
} else {
const res = await fn();
out.textContent = (Array.isArray(res) ? res[0] : res) || '(no output)';
}
} catch (e) {
out.textContent = 'error: ' + (e && e.message ? e.message : e);
}
btn.disabled = false;
}
function pick(name) {
document.querySelectorAll('#examples .chip').forEach((c) => c.classList.toggle('on', c.dataset.ex === name));
$('#code').value = EXAMPLES[name];
$('#out').textContent = 'Press Run.';
}
$('#examples').addEventListener('click', (e) => { const b = e.target.closest('[data-ex]'); if (b) pick(b.dataset.ex); });
$('#run').addEventListener('click', run);
$('#code').addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!$('#run').disabled) run(); }
if (e.key === 'Tab') { e.preventDefault(); const t = e.target; const s = t.selectionStart; t.setRangeText(' ', s, t.selectionEnd, 'end'); }
});
$('#btn-free').addEventListener('click', () => toast('Free modules downloading: drag Studwright.rbxmx into Studio.'));
pick('format');
loadCatalogue();
loadTests();
loadVM();