Files
compiler-explorer/static/widgets/runtime-tools.ts
Matt Godbolt 03d20c5fde Move assert.ts and rison.ts to shared/ directory (#8246)
## 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>
2025-11-04 10:58:11 -06:00

411 lines
14 KiB
TypeScript

// Copyright (c) 2023, 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 $ from 'jquery';
import {assert} from '../../shared/assert.js';
import {CompilerInfo} from '../../types/compiler.interfaces.js';
import {
ConfiguredRuntimeTool,
ConfiguredRuntimeTools,
PossibleRuntimeTools,
RuntimeToolOption,
RuntimeToolOptions,
RuntimeToolType,
} from '../../types/execution/execution.interfaces.js';
import * as BootstrapUtils from '../bootstrap-utils.js';
import {localStorage} from '../local.js';
import {options} from '../options.js';
const FAV_RUNTIMETOOLS_STORE_KEY = 'favruntimetools';
export type RuntimeToolsChangeCallback = () => void;
type FavRuntimeTool = {
name: RuntimeToolType;
options: string;
meta: string;
};
type FavRuntimeTools = FavRuntimeTool[];
export class RuntimeToolsWidget {
private popupDomRoot: JQuery<HTMLElement>;
private envVarsInput: JQuery<HTMLElement>;
private dropdownButton: JQuery;
private onChangeCallback: RuntimeToolsChangeCallback;
private configured: ConfiguredRuntimeTools = [];
private compiler: CompilerInfo | undefined;
private possibleTools: PossibleRuntimeTools;
constructor(dropdownButton: JQuery, onChangeCallback: RuntimeToolsChangeCallback) {
this.popupDomRoot = $('#runtimetools-selection');
this.dropdownButton = dropdownButton;
this.envVarsInput = this.popupDomRoot.find('.envvars');
this.onChangeCallback = onChangeCallback;
this.possibleTools = [];
}
private loadStateFromUI(): ConfiguredRuntimeTools {
const tools: ConfiguredRuntimeTools = [];
const envOverrides = this.getEnvOverrides();
if (envOverrides.length > 0) {
tools.push({
name: RuntimeToolType.env,
options: envOverrides,
});
}
const selects = this.popupDomRoot.find('select');
for (const select of selects) {
const jqSelect = $(select);
const rawName = jqSelect.data('tool-name');
const optionName = jqSelect.data('tool-option');
const val = jqSelect.val();
if (val) {
const name = rawName as RuntimeToolType;
assert(name !== RuntimeToolType.env);
let tool = tools.find(tool => tool.name === name);
if (!tool) {
tool = {
name: name,
options: [],
};
tools.push(tool);
}
const option: RuntimeToolOption = {
name: optionName,
value: (val || '') as string,
};
tool.options.push(option);
}
}
return tools;
}
private optionsToString(options: RuntimeToolOptions): string {
return options.map(env => `${env.name}=${env.value}`).join('\n');
}
private stringToOptions(options: string): RuntimeToolOptions {
return options
.split('\n')
.map(env => {
const firstEqPos = env.indexOf('=');
if (firstEqPos !== -1) {
return {
name: env.substring(0, firstEqPos),
value: env.substring(firstEqPos + 1),
};
}
return false;
})
.filter(Boolean) as RuntimeToolOptions;
}
private getEnvOverrides(): RuntimeToolOptions {
return this.stringToOptions(this.envVarsInput.val() as string);
}
private selectOverrideFromFave(event) {
const elem = $(event.target).parent();
const name = elem.data('ov-name');
const optionsStr = elem.data('ov-options');
const options = this.stringToOptions(optionsStr);
const tool = this.possibleTools.find(ov => ov.name === name);
if (tool) {
const configuredTools = this.loadStateFromUI();
let configuredTool = configuredTools.find(t => t.name === name);
if (!configuredTool) {
configuredTool = {
name: name,
options: [],
};
configuredTools.push(configuredTool);
}
configuredTool.options = options;
this.loadStateIntoUI(configuredTools);
}
}
private newFavoriteOverrideDiv(fave: FavRuntimeTool) {
const div = $('#overrides-favorite-tpl').children().clone();
const prefix = fave.name + ': ';
div.find('.overrides-name').html(prefix + fave.options.replace(/\n/g, ', '));
div.data('ov-name', fave.name);
div.data('ov-options', fave.options);
div.on('click', this.selectOverrideFromFave.bind(this));
return div;
}
private loadFavoritesIntoUI() {
const favoritesDiv = this.popupDomRoot.find('.runtimetools-favorites');
favoritesDiv.html('');
const faves = this.getFavorites();
for (const fave of faves) {
const div: any = this.newFavoriteOverrideDiv(fave);
favoritesDiv.append(div);
}
}
private addToFavorites(override: ConfiguredRuntimeTool) {
if (override.name === RuntimeToolType.env) return;
const faves = this.getFavorites();
const fave: FavRuntimeTool = {
name: override.name,
options: this.optionsToString(override.options),
meta: this.compiler?.baseName || this.compiler?.groupName || this.compiler?.name || this.compiler?.id || '',
};
faves.push(fave);
this.setFavorites(faves);
}
private removeFromFavorites(override: ConfiguredRuntimeTool) {
if (override.name === RuntimeToolType.env) return;
const overrideOptions = this.optionsToString(override.options);
const faves = this.getFavorites();
const faveIdx = faves.findIndex(f => f.name === override.name && f.options === overrideOptions);
if (faveIdx !== -1) {
faves.splice(faveIdx, 1);
this.setFavorites(faves);
}
}
private isAFavorite(override: ConfiguredRuntimeTool) {
if (override.name === RuntimeToolType.env) return false;
const overrideOptions = this.optionsToString(override.options);
const faves = this.getFavorites();
const fave = faves.find(f => f.name === override.name && f.options === overrideOptions);
return !!fave;
}
private cap(text: string) {
if (text.length > 0) {
return text[0].toUpperCase() + text.substring(1);
}
return '';
}
private loadStateIntoUI(configured: ConfiguredRuntimeTools) {
this.envVarsInput.val('');
for (const config of configured) {
if (config.name === RuntimeToolType.env) {
this.envVarsInput.val(this.optionsToString(config.options));
}
}
const container = this.popupDomRoot.find('.possible-runtimetools');
container.html('');
this.possibleTools = this.compiler?.possibleRuntimeTools || [];
for (const possibleTool of this.possibleTools) {
const card = $('#possible-runtime-tool').children().clone();
card.find('.tool-name').html(this.cap(possibleTool.name));
card.find('.tool-description').html(possibleTool.description);
const toolOptionsDiv = card.find('.tool-options');
const faveButton = card.find('.tool-fav-button');
faveButton.hide();
const faveStar = faveButton.find('.tool-fav-btn-icon');
const config = configured.find(c => c.name === possibleTool.name);
for (const toolOption of possibleTool.possibleOptions) {
const optionDiv = $('#possible-runtime-tool-option').children().clone();
optionDiv.attr('name', toolOption.name);
const display_text = this.cap(toolOption.name);
optionDiv.find('.tool-option-name').html(display_text);
const select = optionDiv.find('select');
select.data('tool-name', possibleTool.name);
select.data('tool-option', toolOption.name);
const option = $('<option />');
option.html('');
option.val('');
select.append(option);
for (const toolOptionValue of toolOption.possibleValues) {
const option = $('<option />');
option.html(toolOptionValue);
option.val(toolOptionValue);
if (config) {
const found = config.options.find(
configuredOption =>
configuredOption.name === toolOption.name && configuredOption.value === toolOptionValue,
);
if (found) option.attr('selected', 'selected');
}
select.append(option);
}
select.off('change').on('change', () => {
const name = possibleTool.name;
assert(name !== RuntimeToolType.env);
const configured = this.loadStateFromUI();
const configuredTool = configured.find(tool => tool.name === name);
if (configuredTool) {
if (this.isAFavorite(configuredTool)) {
faveStar.removeClass('far').addClass('fas');
} else {
faveStar.removeClass('fas').addClass('far');
}
if (configuredTool.options.length !== 0) {
faveButton.show();
} else {
faveButton.hide();
}
} else {
faveStar.removeClass('fas').addClass('far');
}
});
toolOptionsDiv.append(optionDiv);
}
if (config && this.isAFavorite(config)) {
faveStar.removeClass('far').addClass('fas');
}
faveButton.show();
faveButton.on('click', () => {
const name = possibleTool.name;
assert(name !== RuntimeToolType.env);
const configured = this.loadStateFromUI();
const configuredTool = configured.find(tool => tool.name === name);
if (configuredTool) {
if (this.isAFavorite(configuredTool)) {
this.removeFromFavorites(configuredTool);
faveStar.removeClass('fas').addClass('far');
} else {
this.addToFavorites(configuredTool);
faveStar.removeClass('far').addClass('fas');
}
}
this.loadFavoritesIntoUI();
});
container.append(card);
}
this.loadFavoritesIntoUI();
}
set(configured: ConfiguredRuntimeTools) {
this.configured = configured;
this.updateButton();
}
setDefaults() {
this.configured = [];
this.updateButton();
}
setCompiler(compilerId: string, languageId?: string) {
this.compiler = options.compilers.find(c => c.id === compilerId);
}
get(): ConfiguredRuntimeTools | undefined {
if (this.compiler) {
return this.configured;
}
return undefined;
}
private getFavorites(): FavRuntimeTools {
return JSON.parse(localStorage.get(FAV_RUNTIMETOOLS_STORE_KEY, '[]'));
}
private setFavorites(faves: FavRuntimeTools) {
localStorage.set(FAV_RUNTIMETOOLS_STORE_KEY, JSON.stringify(faves));
}
private updateButton() {
const selected = this.get();
if (selected && selected.length > 0) {
this.dropdownButton
.addClass('btn-success')
.removeClass('btn-light')
.prop(
'title',
'Current tools:\n' +
selected
.map(ov => {
return '- ' + ov.name;
})
.join('\n'),
);
} else {
this.dropdownButton.removeClass('btn-success').addClass('btn-light').prop('title', 'Overrides');
}
}
show() {
this.loadStateIntoUI(this.configured);
const lastOverrides = JSON.stringify(this.configured);
// popup is shared, so clear the events first
BootstrapUtils.setElementEventHandler(this.popupDomRoot, 'hidden.bs.modal', () => {
this.configured = this.loadStateFromUI();
const newOverrides = JSON.stringify(this.configured);
if (lastOverrides !== newOverrides) {
this.updateButton();
this.onChangeCallback();
}
});
BootstrapUtils.showModal(this.popupDomRoot);
}
}