mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 07:04:04 -05:00
## Summary Moves `static/assert.ts` and `static/rison.ts` to `shared/` directory to make them available to both frontend and backend code without browser dependencies. Updates all import paths across the codebase (~47 files). ## Motivation This refactoring eliminates browser dependencies in these utilities, allowing them to be imported by Node.js contexts (like Cypress test files) without causing module load failures. This is a prerequisite for upcoming Cypress test improvements. ## Changes - Move `static/assert.ts` → `shared/assert.ts` - Move `static/rison.ts` → `shared/rison.ts` - Update `biome.json` to allow `hasOwnProperty` in `shared/` directory - Update all imports across `static/`, `lib/`, and `test/` directories (47 files changed) ## Benefits - No functional changes, purely a code reorganization - Makes these utilities accessible to both frontend and backend without circular dependencies - Enables future Cypress improvements that require these utilities in Node.js context - All tests pass ✓ (699 tests) ## Test Plan - [x] TypeScript compilation passes - [x] Linting passes - [x] All unit tests pass (699 tests) - [x] Pre-commit hooks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
344 lines
14 KiB
TypeScript
344 lines
14 KiB
TypeScript
// Copyright (c) 2025, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import {Buffer} from 'buffer';
|
|
import $ from 'jquery';
|
|
import {assert, unwrap} from '../shared/assert.js';
|
|
import {CompilationResult} from '../types/compilation/compilation.interfaces.js';
|
|
import {Artifact, ArtifactType} from '../types/tool.interfaces.js';
|
|
import * as BootstrapUtils from './bootstrap-utils.js';
|
|
import {Alert} from './widgets/alert.js';
|
|
|
|
export class ArtifactHandler {
|
|
private alertSystem: Alert;
|
|
|
|
constructor(alertSystem: Alert) {
|
|
this.alertSystem = alertSystem;
|
|
}
|
|
|
|
handle(result: CompilationResult): void {
|
|
if (result.artifacts) {
|
|
for (const artifact of result.artifacts) {
|
|
this.handleArtifact(artifact);
|
|
}
|
|
} else if (result.execResult) {
|
|
// Handle executor results recursively
|
|
this.handle(result.execResult);
|
|
}
|
|
}
|
|
|
|
private handleArtifact(artifact: Artifact): void {
|
|
switch (artifact.type) {
|
|
case ArtifactType.nesrom:
|
|
this.emulateNESROM(artifact);
|
|
break;
|
|
case ArtifactType.bbcdiskimage:
|
|
this.emulateBbcDisk(artifact);
|
|
break;
|
|
case ArtifactType.zxtape:
|
|
this.emulateSpeccyTape(artifact);
|
|
break;
|
|
case ArtifactType.smsrom:
|
|
this.emulateMiracleSMS(artifact);
|
|
break;
|
|
case ArtifactType.timetrace:
|
|
this.offerViewInSpeedscope(artifact);
|
|
break;
|
|
case ArtifactType.c64prg:
|
|
this.emulateC64Prg(artifact);
|
|
break;
|
|
case ArtifactType.heaptracktxt:
|
|
this.offerViewInSpeedscope(artifact);
|
|
break;
|
|
case ArtifactType.gbrom:
|
|
this.emulateGameBoyROM(artifact);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private offerViewInSpeedscope(artifact: Artifact): void {
|
|
this.alertSystem.notify(
|
|
'Click ' +
|
|
'<a class="link-primary" target="_blank" id="download_link" style="cursor:pointer;">here</a>' +
|
|
' to view ' +
|
|
artifact.title +
|
|
' in Speedscope',
|
|
{
|
|
group: artifact.type,
|
|
collapseSimilar: false,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#download_link').on('click', () => {
|
|
const tmstr = Date.now();
|
|
const live_url = 'https://static.ce-cdn.net/speedscope/index.html';
|
|
const speedscope_url =
|
|
live_url +
|
|
'?' +
|
|
tmstr +
|
|
'#customFilename=' +
|
|
encodeURIComponent(artifact.name) +
|
|
'&b64data=' +
|
|
encodeURIComponent(artifact.content);
|
|
window.open(speedscope_url);
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
offerViewInPerfetto(artifact: Artifact): void {
|
|
this.alertSystem.notify(
|
|
'Click ' +
|
|
'<a class="link-primary" target="_blank" id="download_link" style="cursor:pointer;">here</a>' +
|
|
' to view ' +
|
|
artifact.title +
|
|
' in Perfetto',
|
|
{
|
|
group: artifact.type,
|
|
collapseSimilar: false,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#download_link').on('click', () => {
|
|
const perfetto_url = 'https://ui.perfetto.dev';
|
|
const win = window.open(perfetto_url);
|
|
if (win) {
|
|
const timer = setInterval(() => win.postMessage('PING', perfetto_url), 50);
|
|
|
|
const onMessageHandler = (evt: MessageEvent) => {
|
|
if (evt.data !== 'PONG') return;
|
|
clearInterval(timer);
|
|
|
|
const data = {
|
|
perfetto: {
|
|
buffer: Buffer.from(artifact.content, 'base64'),
|
|
title: artifact.name,
|
|
filename: artifact.name,
|
|
},
|
|
};
|
|
win.postMessage(data, perfetto_url);
|
|
};
|
|
window.addEventListener('message', onMessageHandler);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateMiracleSMS(artifact: Artifact): void {
|
|
const dialog = $('#miracleemu');
|
|
|
|
this.alertSystem.notify(
|
|
'Click ' +
|
|
'<a target="_blank" id="miracle_emulink" style="cursor:pointer;" class="link-primary">here</a>' +
|
|
' to emulate',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#miracle_emulink').on('click', () => {
|
|
BootstrapUtils.showModal(dialog);
|
|
|
|
const miracleMenuFrame = dialog.find('#miracleemuframe')[0];
|
|
assert(miracleMenuFrame instanceof HTMLIFrameElement);
|
|
if ('contentWindow' in miracleMenuFrame) {
|
|
const emuwindow = unwrap(miracleMenuFrame.contentWindow);
|
|
const tmstr = Date.now();
|
|
emuwindow.location =
|
|
'https://miracle.xania.org/?' +
|
|
tmstr +
|
|
'#b64sms=' +
|
|
encodeURIComponent(artifact.content);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateSpeccyTape(artifact: Artifact): void {
|
|
const dialog = $('#jsspeccyemu');
|
|
|
|
this.alertSystem.notify(
|
|
'Click ' +
|
|
'<a target="_blank" id="jsspeccy_emulink" style="cursor:pointer;" class="link-primary">here</a>' +
|
|
' to emulate',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#jsspeccy_emulink').on('click', () => {
|
|
BootstrapUtils.showModal(dialog);
|
|
|
|
const speccyemuframe = dialog.find('#speccyemuframe')[0];
|
|
assert(speccyemuframe instanceof HTMLIFrameElement);
|
|
if ('contentWindow' in speccyemuframe) {
|
|
const emuwindow = unwrap(speccyemuframe.contentWindow);
|
|
const tmstr = Date.now();
|
|
emuwindow.location =
|
|
'https://static.ce-cdn.net/jsspeccy/index.html?' +
|
|
tmstr +
|
|
'#b64tape=' +
|
|
encodeURIComponent(artifact.content);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateBbcDisk(artifact: Artifact): void {
|
|
const dialog = $('#jsbeebemu');
|
|
|
|
this.alertSystem.notify(
|
|
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#emulink').on('click', () => {
|
|
BootstrapUtils.showModal(dialog);
|
|
|
|
const jsbeebemuframe = dialog.find('#jsbeebemuframe')[0];
|
|
assert(jsbeebemuframe instanceof HTMLIFrameElement);
|
|
if ('contentWindow' in jsbeebemuframe) {
|
|
const emuwindow = unwrap(jsbeebemuframe.contentWindow);
|
|
const tmstr = Date.now();
|
|
emuwindow.location =
|
|
'https://bbc.godbolt.org/?' +
|
|
tmstr +
|
|
'#embed&autoboot&disc1=b64data:' +
|
|
encodeURIComponent(artifact.content);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateNESROM(artifact: Artifact): void {
|
|
const dialog = $('#jsnesemu');
|
|
|
|
this.alertSystem.notify(
|
|
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#emulink').on('click', () => {
|
|
BootstrapUtils.showModal(dialog);
|
|
|
|
const jsnesemuframe = dialog.find('#jsnesemuframe')[0];
|
|
assert(jsnesemuframe instanceof HTMLIFrameElement);
|
|
if ('contentWindow' in jsnesemuframe) {
|
|
const emuwindow = unwrap(jsnesemuframe.contentWindow);
|
|
const tmstr = Date.now();
|
|
emuwindow.location =
|
|
'https://static.ce-cdn.net/jsnes-ceweb/index.html?' +
|
|
tmstr +
|
|
'#b64nes=' +
|
|
encodeURIComponent(artifact.content);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateC64Prg(prg: Artifact): void {
|
|
this.alertSystem.notify(
|
|
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#emulink').on('click', () => {
|
|
const tmstr = Date.now();
|
|
const url =
|
|
'https://static.ce-cdn.net/viciious/viciious.html?' +
|
|
tmstr +
|
|
'#filename=' +
|
|
encodeURIComponent(prg.title) +
|
|
'&b64c64=' +
|
|
encodeURIComponent(prg.content);
|
|
|
|
window.open(url, '_blank');
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
private emulateGameBoyROM(prg: Artifact): void {
|
|
const dialog = $('#gbemu');
|
|
|
|
this.alertSystem.notify(
|
|
'Click <a target="_blank" id="emulink" style="cursor:pointer;" class="link-primary">here</a> to emulate with a debugger, ' +
|
|
'or <a target="_blank" id="emulink-play" style="cursor:pointer;" class="link-primary">here</a> to emulate just to play.',
|
|
{
|
|
group: 'emulation',
|
|
collapseSimilar: true,
|
|
dismissTime: 10000,
|
|
onBeforeShow: elem => {
|
|
elem.find('#emulink').on('click', () => {
|
|
const tmstr = Date.now();
|
|
const url =
|
|
'https://static.ce-cdn.net/wasmboy/index.html?' +
|
|
tmstr +
|
|
'#rom-name=' +
|
|
encodeURIComponent(prg.title) +
|
|
'&rom-data=' +
|
|
encodeURIComponent(prg.content);
|
|
window.open(url, '_blank');
|
|
});
|
|
|
|
elem.find('#emulink-play').on('click', () => {
|
|
BootstrapUtils.showModal(dialog);
|
|
|
|
const gbemuframe = dialog.find('#gbemuframe')[0];
|
|
assert(gbemuframe instanceof HTMLIFrameElement);
|
|
if ('contentWindow' in gbemuframe) {
|
|
const emuwindow = unwrap(gbemuframe.contentWindow);
|
|
const tmstr = Date.now();
|
|
const url =
|
|
'https://static.ce-cdn.net/wasmboy/iframe/index.html?' +
|
|
tmstr +
|
|
'#rom-name=' +
|
|
encodeURIComponent(prg.title) +
|
|
'&rom-data=' +
|
|
encodeURIComponent(prg.content);
|
|
emuwindow.location = url;
|
|
}
|
|
});
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|