Files
electron/lib/browser/parse-features-string.ts
Samuel Attard 2c94aac330 build: add oxfmt for JS/TS formatting and import sorting (#50692)
* build: add oxfmt for code formatting and import sorting

Adds oxfmt as a devDependency alongside oxlint and wires it into the
lint pipeline. The .oxfmtrc.json config matches Electron's current JS
style (single quotes, semicolons, 2-space indent, trailing commas off,
printWidth 100) and configures sortImports with custom groups that
mirror the import/order pathGroups previously enforced by ESLint:
@electron/internal, @electron/*, and {electron,electron/**} each get
their own ordered group ahead of external modules.

- `yarn lint:fmt` runs `oxfmt --check` over JS/TS sources and is
  chained into `yarn lint` so CI enforces it automatically.
- `yarn format` runs `oxfmt --write` for local fix-up.
- lint-staged invokes `oxfmt --write` on staged .js/.ts/.mjs/.cjs
  files before oxlint, so formatting is applied at commit time.

The next commit applies the formatter to the existing codebase so the
check actually passes.

* chore: apply oxfmt formatting to JS and TS sources

Runs `yarn format` across lib/, spec/, script/, build/, default_app/,
and npm/ to bring the codebase in line with the .oxfmtrc.json settings
added in the previous commit. This is a pure formatting pass: import
statements are sorted into the groups defined by the config, method
chains longer than printWidth are broken, single-quoted strings
containing apostrophes are switched to double quotes, and a handful of
single-statement `if` bodies are re-wrapped and get braces added by
`oxlint --fix` to satisfy the `curly: multi-line` rule.

No behavior changes.
2026-04-12 02:03:04 -07:00

184 lines
5.0 KiB
TypeScript

/**
* Utilities to parse comma-separated key value pairs used in browser APIs.
* For example: "x=100,y=200,width=500,height=500"
*/
import { BrowserWindowConstructorOptions } from 'electron/main';
type RequiredBrowserWindowConstructorOptions = Required<BrowserWindowConstructorOptions>;
type IntegerBrowserWindowOptionKeys = {
[K in keyof RequiredBrowserWindowConstructorOptions]: RequiredBrowserWindowConstructorOptions[K] extends number
? K
: never;
}[keyof RequiredBrowserWindowConstructorOptions];
// This could be an array of keys, but an object allows us to add a compile-time
// check validating that we haven't added an integer property to
// BrowserWindowConstructorOptions that this module doesn't know about.
const keysOfTypeNumberCompileTimeCheck: { [K in IntegerBrowserWindowOptionKeys]: true } = {
x: true,
y: true,
width: true,
height: true,
minWidth: true,
maxWidth: true,
minHeight: true,
maxHeight: true,
opacity: true
};
// Note `top` / `left` are special cases from the browser which we later convert
// to y / x.
// NOTE(@mlaurencin) `innerWidth` / `innerHeight` are also special cases. The spec
// states that `width` and `height` represent the window content size and are equivalent
// to `innerWidth` / `innerHeight`. However, our implementation currently incorrectly maps
// `width` and `height` to `outerWidth` and `outerHeight`, or the size of the window
// with all border and related window chrome.
const keysOfTypeNumber = new Set([
'top',
'left',
'innerWidth',
'innerHeight',
...Object.keys(keysOfTypeNumberCompileTimeCheck)
]);
/**
* Note that we only allow "0" and "1" boolean conversion when the type is known
* not to be an integer.
*
* The coercion of yes/no/1/0 represents best effort accordance with the spec:
* https://html.spec.whatwg.org/multipage/window-object.html#concept-window-open-features-parse-boolean
*/
type CoercedValue = string | number | boolean;
function coerce(key: string, value: string): CoercedValue {
if (keysOfTypeNumber.has(key)) {
return parseInt(value, 10);
}
switch (value) {
case 'true':
case '1':
case 'yes':
case undefined:
return true;
case 'false':
case '0':
case 'no':
return false;
default:
return value;
}
}
export function parseCommaSeparatedKeyValue(source: string) {
const parsed = {} as { [key: string]: any };
for (const keyValuePair of source.split(',')) {
const [key, value] = keyValuePair.split('=').map((str) => str.trim());
if (key) {
parsed[key] = coerce(key, value);
}
}
return parsed;
}
export function parseWebViewWebPreferences(preferences: string) {
return parseCommaSeparatedKeyValue(preferences);
}
const allowedWebPreferences = [
'zoomFactor',
'nodeIntegration',
'javascript',
'contextIsolation',
'webviewTag'
] as const;
type AllowedWebPreference = (typeof allowedWebPreferences)[number];
// Top-level BrowserWindow options that may be set via the window.open()
// features string. Options not listed here are silently dropped; apps that
// need to pass other options should use setWindowOpenHandler in the main
// process.
const allowedWindowOptions = new Set<string>([
// standard window.open() position/size features
'top',
'left',
'innerWidth',
'innerHeight',
// numeric
'x',
'y',
'width',
'height',
'minWidth',
'minHeight',
'maxWidth',
'maxHeight',
'opacity',
// presentational booleans
'show',
'center',
'useContentSize',
'frame',
'transparent',
'hasShadow',
'movable',
'closable',
'focusable',
'minimizable',
'maximizable',
'fullscreenable',
'alwaysOnTop',
'skipTaskbar',
'modal',
'acceptFirstMouse',
'autoHideMenuBar',
'enableLargerThanScreen',
'paintWhenInitiallyHidden',
'roundedCorners',
'thickFrame',
'disableAutoHideCursor',
'hiddenInMissionControl',
// presentational strings (no filesystem/network side effects)
'title',
'backgroundColor',
'tabbingIdentifier',
'titleBarStyle',
'vibrancy',
'visualEffectState',
'backgroundMaterial'
]);
/**
* Parses a feature string that has the format used in window.open().
*/
export function parseFeatures(features: string) {
const parsed = parseCommaSeparatedKeyValue(features);
const webPreferences: { [K in AllowedWebPreference]?: any } = {};
for (const key of allowedWebPreferences) {
if (parsed[key] === undefined) continue;
webPreferences[key] = parsed[key];
delete parsed[key];
}
// Per spec - https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-open-dev
// windows are always resizable.
if (parsed.resizable !== undefined) {
delete parsed.resizable;
}
if (parsed.left !== undefined) parsed.x = parsed.left;
if (parsed.top !== undefined) parsed.y = parsed.top;
const options: { [key: string]: CoercedValue } = {};
for (const key of Object.keys(parsed)) {
if (allowedWindowOptions.has(key)) {
options[key] = parsed[key];
}
}
return {
options: options as Omit<BrowserWindowConstructorOptions, 'webPreferences'>,
webPreferences
};
}