mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-05-16 05:32:52 -04:00
Enable Biome's `organizeImports` with groups matching the original ESLint `import/order` configuration: 1. **Node builtins** (`node:fs`, `path`, etc.) 2. *(blank line)* 3. **Third-party packages** (`express`, `@sentry/node`, etc.) 4. *(blank line)* 5. **Local/relative imports** (`../foo.js`, `./bar.js`, aliases) This resolves the inconsistency where Biome wasn't enforcing import grouping, meaning new files would lose the blank-line separation that the old ESLint config enforced. ### Impact - **354 files** updated out of 738 checked (~48%) - **+188 / -240 lines** (net -52) — almost entirely single blank line additions/removals between import groups - No import reordering; purely group separator consistency Fixes #7373 🤖 Generated by LLM (Claude, via OpenClaw)
281 lines
9.0 KiB
TypeScript
281 lines
9.0 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 lzstring from 'lz-string';
|
|
|
|
import * as rison from './rison.js';
|
|
|
|
/**
|
|
* ConfigMinifier - Reimplementation of GoldenLayout's ConfigMinifier
|
|
*
|
|
* Minifies and unminifies configs by replacing frequent keys and values
|
|
* with one letter substitutes using base36 encoding.
|
|
*
|
|
* This is a Node-compatible reimplementation that doesn't depend on
|
|
* the browser-only GoldenLayout library.
|
|
*/
|
|
class ConfigMinifier {
|
|
private readonly _keys: string[];
|
|
private readonly _values: (string | boolean)[];
|
|
|
|
constructor() {
|
|
// Array position matters - these map to base36 characters (0-9, a-z)
|
|
this._keys = [
|
|
'settings',
|
|
'hasHeaders',
|
|
'constrainDragToContainer',
|
|
'selectionEnabled',
|
|
'dimensions',
|
|
'borderWidth',
|
|
'minItemHeight',
|
|
'minItemWidth',
|
|
'headerHeight',
|
|
'dragProxyWidth',
|
|
'dragProxyHeight',
|
|
'labels',
|
|
'close',
|
|
'maximise',
|
|
'minimise',
|
|
'popout',
|
|
'content',
|
|
'componentName',
|
|
'componentState',
|
|
'id',
|
|
'width',
|
|
'type',
|
|
'height',
|
|
'isClosable',
|
|
'title',
|
|
'popoutWholeStack',
|
|
'openPopouts',
|
|
'parentId',
|
|
'activeItemIndex',
|
|
'reorderEnabled',
|
|
'borderGrabWidth',
|
|
];
|
|
|
|
this._values = [
|
|
true,
|
|
false,
|
|
'row',
|
|
'column',
|
|
'stack',
|
|
'component',
|
|
'close',
|
|
'maximise',
|
|
'minimise',
|
|
'open in new window',
|
|
];
|
|
|
|
if (this._keys.length > 36) {
|
|
throw new Error('Too many keys in config minifier map');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes a GoldenLayout configuration object and replaces its keys
|
|
* and values recursively with one letter counterparts
|
|
*/
|
|
minifyConfig(config: any): any {
|
|
const min: any = {};
|
|
this._nextLevel(config, min, '_min');
|
|
return min;
|
|
}
|
|
|
|
/**
|
|
* Takes a configuration Object that was previously minified
|
|
* using minifyConfig and returns its original version
|
|
*/
|
|
unminifyConfig(minifiedConfig: any): any {
|
|
const orig: any = {};
|
|
this._nextLevel(minifiedConfig, orig, '_max');
|
|
return orig;
|
|
}
|
|
|
|
/**
|
|
* Recursive function, called for every level of the config structure
|
|
*/
|
|
private _nextLevel(from: any, to: any, translationFn: '_min' | '_max'): void {
|
|
for (const key in from) {
|
|
// Skip prototype properties
|
|
if (!from.hasOwnProperty(key)) continue;
|
|
|
|
// For arrays, cast keys to numbers (not strings!)
|
|
// This is important because the single-char check in _min/_max
|
|
// should not trigger for numeric indices like "0", "1", etc.
|
|
let processedKey: string | number = key;
|
|
if (Array.isArray(from)) {
|
|
processedKey = parseInt(key, 10);
|
|
}
|
|
|
|
// Translate the key to a one letter substitute
|
|
const minKey = this[translationFn](processedKey, this._keys);
|
|
|
|
// For Arrays and Objects, create a new Array/Object and recurse
|
|
if (typeof from[key] === 'object' && from[key] !== null) {
|
|
to[minKey] = Array.isArray(from[key]) ? [] : {};
|
|
this._nextLevel(from[key], to[minKey], translationFn);
|
|
} else {
|
|
// For primitive values, minify the value
|
|
to[minKey] = this[translationFn](from[key], this._values);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Minifies value based on a dictionary
|
|
*/
|
|
private _min(value: any, dictionary: readonly (string | boolean)[]): any {
|
|
// If a value actually is a single character, prefix it with ___
|
|
// to avoid mistaking it for a minification code
|
|
if (typeof value === 'string' && value.length === 1) {
|
|
return '___' + value;
|
|
}
|
|
|
|
const index = dictionary.indexOf(value);
|
|
|
|
// Value not found in the dictionary, return it unmodified
|
|
if (index === -1) {
|
|
return value;
|
|
}
|
|
|
|
// Value found in dictionary, return its base36 counterpart
|
|
return index.toString(36);
|
|
}
|
|
|
|
/**
|
|
* Unminifies value based on a dictionary
|
|
*/
|
|
private _max(value: any, dictionary: readonly (string | boolean)[]): any {
|
|
// Value is a single character - assume it's a translation
|
|
if (typeof value === 'string' && value.length === 1) {
|
|
return dictionary[parseInt(value, 36)];
|
|
}
|
|
|
|
// Value originally was a single character and was prefixed with ___
|
|
if (typeof value === 'string' && value.substr(0, 3) === '___') {
|
|
return value[3];
|
|
}
|
|
|
|
// Value was not minified
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// Create a singleton instance
|
|
const configMinifier = new ConfigMinifier();
|
|
|
|
/**
|
|
* Minify a GoldenLayout config object
|
|
*/
|
|
export function minifyConfig(config: any): any {
|
|
return configMinifier.minifyConfig(config);
|
|
}
|
|
|
|
/**
|
|
* Unminify a GoldenLayout config object
|
|
*/
|
|
export function unminifyConfig(config: any): any {
|
|
return configMinifier.unminifyConfig(config);
|
|
}
|
|
|
|
/**
|
|
* Convert object to rison-encoded string
|
|
*/
|
|
export function risonify(obj: rison.JSONValue): string {
|
|
return rison.quote(rison.encode_object(obj));
|
|
}
|
|
|
|
/**
|
|
* Convert rison-encoded string to object
|
|
*/
|
|
export function unrisonify(text: string): any {
|
|
return rison.decode_object(decodeURIComponent(text.replace(/\+/g, '%20')));
|
|
}
|
|
|
|
/**
|
|
* Serialise state object to URL hash string
|
|
*
|
|
* Process:
|
|
* 1. Minify the config (replace common keys/values with single chars)
|
|
* 2. Rison encode the minified config
|
|
* 3. If compression saves >20%, compress with lzstring and wrap in {z: ...}
|
|
* 4. Return the final rison-encoded string
|
|
*/
|
|
export function serialiseState(stateText: any): string {
|
|
const ctx = minifyConfig({content: stateText.content});
|
|
ctx.version = 4;
|
|
const uncompressed = risonify(ctx);
|
|
const compressed = risonify({z: lzstring.compressToBase64(uncompressed)});
|
|
const MinimalSavings = 0.2;
|
|
if (compressed.length < uncompressed.length * (1.0 - MinimalSavings)) {
|
|
return compressed;
|
|
}
|
|
return uncompressed;
|
|
}
|
|
|
|
/**
|
|
* Deserialise URL hash string to state object
|
|
*
|
|
* Process:
|
|
* 1. Rison decode the hash
|
|
* 2. If it contains {z: ...}, decompress the lzstring data
|
|
* 3. Rison decode again if decompressed
|
|
* 4. Unminify the config (expand single chars back to full keys/values)
|
|
* 5. Handle version migrations for old state formats
|
|
*/
|
|
export function deserialiseState(stateText: string): any {
|
|
let state;
|
|
try {
|
|
state = unrisonify(stateText);
|
|
if (state?.z) {
|
|
const data = lzstring.decompressFromBase64(state.z);
|
|
// lzstring returns empty string on failure rather than throwing
|
|
if (data === '') {
|
|
throw new Error('lzstring decompress error, url is corrupted');
|
|
}
|
|
state = unrisonify(data);
|
|
}
|
|
} catch (ex) {
|
|
// If we can't parse it, return false so caller can handle
|
|
console.warn('Failed to deserialise state:', ex);
|
|
return false;
|
|
}
|
|
|
|
// Handle version migrations
|
|
if (!state || state.version === undefined) return false;
|
|
|
|
switch (state.version) {
|
|
case 4:
|
|
state = unminifyConfig(state);
|
|
break;
|
|
default:
|
|
// Versions 1-3 require GoldenLayout and Components, which are browser-only
|
|
// These should be handled by the frontend-specific code in static/url.ts
|
|
return state;
|
|
}
|
|
|
|
return state;
|
|
}
|