Files
electron/lib/browser/api/menu-utils.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

187 lines
5.1 KiB
TypeScript

function splitArray<T>(arr: T[], predicate: (x: T) => boolean) {
const result = arr.reduce(
(multi, item) => {
const current = multi[multi.length - 1];
if (predicate(item)) {
if (current.length > 0) multi.push([]);
} else {
current.push(item);
}
return multi;
},
[[]] as T[][]
);
if (result[result.length - 1].length === 0) {
return result.slice(0, result.length - 1);
}
return result;
}
function joinArrays(arrays: any[][], joinIDs: any[]) {
return arrays.reduce((joined, arr, i) => {
if (i > 0 && arr.length) {
if (joinIDs.length > 0) {
joined.push(joinIDs[0]);
joinIDs.splice(0, 1);
} else {
joined.push({ type: 'separator' });
}
}
return joined.concat(arr);
}, []);
}
function pushOntoMultiMap<K, V>(map: Map<K, V[]>, key: K, value: V) {
if (!map.has(key)) {
map.set(key, []);
}
map.get(key)!.push(value);
}
function indexOfGroupContainingID<T>(groups: { id?: T }[][], id: T, ignoreGroup: { id?: T }[]) {
return groups.findIndex(
(candidateGroup) =>
candidateGroup !== ignoreGroup && candidateGroup.some((candidateItem) => candidateItem.id === id)
);
}
// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically<T>(originalOrder: T[], edgesById: Map<T, T[]>) {
const sorted = [] as T[];
const marked = new Set<T>();
const visit = (mark: T) => {
if (marked.has(mark)) return;
marked.add(mark);
const edges = edgesById.get(mark);
if (edges != null) {
for (const edge of edges) {
visit(edge);
}
}
sorted.push(mark);
};
for (const edge of originalOrder) {
visit(edge);
}
return sorted;
}
function attemptToMergeAGroup<T>(groups: { before?: T[]; after?: T[]; id?: T }[][]) {
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
for (const item of group) {
const toIDs = [...(item.before || []), ...(item.after || [])];
for (const id of toIDs) {
const index = indexOfGroupContainingID(groups, id, group);
if (index === -1) continue;
const mergeTarget = groups[index];
mergeTarget.push(...group);
groups.splice(i, 1);
return true;
}
}
}
return false;
}
function mergeGroups<T>(groups: { before?: T[]; after?: T[]; id?: T }[][]) {
let merged = true;
while (merged) {
merged = attemptToMergeAGroup(groups);
}
return groups;
}
function sortItemsInGroup<T>(group: { before?: T[]; after?: T[]; id?: T }[]) {
const originalOrder = group.map((node, i) => i);
const edges = new Map();
const idToIndex = new Map(group.map((item, i) => [item.id, i]));
for (const [i, item] of group.entries()) {
if (item.before) {
for (const toID of item.before) {
const to = idToIndex.get(toID);
if (to != null) {
pushOntoMultiMap(edges, to, i);
}
}
}
if (item.after) {
for (const toID of item.after) {
const to = idToIndex.get(toID);
if (to != null) {
pushOntoMultiMap(edges, i, to);
}
}
}
}
const sortedNodes = sortTopologically(originalOrder, edges);
return sortedNodes.map((i) => group[i]);
}
function findEdgesInGroup<T>(
groups: { beforeGroupContaining?: T[]; afterGroupContaining?: T[]; id?: T }[][],
i: number,
edges: Map<any, any>
) {
const group = groups[i];
for (const item of group) {
if (item.beforeGroupContaining) {
for (const id of item.beforeGroupContaining) {
const to = indexOfGroupContainingID(groups, id, group);
if (to !== -1) {
pushOntoMultiMap(edges, to, i);
return;
}
}
}
if (item.afterGroupContaining) {
for (const id of item.afterGroupContaining) {
const to = indexOfGroupContainingID(groups, id, group);
if (to !== -1) {
pushOntoMultiMap(edges, i, to);
return;
}
}
}
}
}
function sortGroups<T>(groups: { id?: T }[][]) {
const originalOrder = groups.map((item, i) => i);
const edges = new Map();
for (let i = 0; i < groups.length; i++) {
findEdgesInGroup(groups, i, edges);
}
const sortedGroupIndexes = sortTopologically(originalOrder, edges);
return sortedGroupIndexes.map((i) => groups[i]);
}
export function sortMenuItems(menuItems: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[]) {
const isSeparator = (i: Electron.MenuItemConstructorOptions | Electron.MenuItem) => {
const opts = i as Electron.MenuItemConstructorOptions;
return (
i.type === 'separator' && !opts.before && !opts.after && !opts.beforeGroupContaining && !opts.afterGroupContaining
);
};
const separators = menuItems.filter(isSeparator);
// Split the items into their implicit groups based upon separators.
const groups = splitArray(menuItems, isSeparator);
const mergedGroups = mergeGroups(groups);
const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup);
const sortedGroups = sortGroups(mergedGroupsWithSortedItems);
const joined = joinArrays(sortedGroups, separators);
return joined;
}