Skip to content

Commit 783e651

Browse files
Merge branch 'main' into remove-framer-motion
2 parents d274c4c + d748fa5 commit 783e651

15 files changed

Lines changed: 153 additions & 78 deletions

File tree

package-lock.json

Lines changed: 2 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
"@testing-library/jest-dom": "^5.14.1",
2929
"@testing-library/react": "^14.0.0",
3030
"@testing-library/user-event": "^14.4.3",
31-
"@types/lodash.debounce": "^4.0.6",
32-
"@types/lodash.sortby": "^4.7.6",
3331
"@types/lunr": "^2.3.4",
3432
"@types/marked": "^4.0.1",
3533
"@types/node": "^24.12.0",
@@ -39,8 +37,6 @@
3937
"crelt": "^1.0.5",
4038
"dompurify": "^3.2.5",
4139
"file-saver": "^2.0.5",
42-
"lodash.debounce": "^4.0.8",
43-
"lodash.sortby": "^4.7.0",
4440
"lunr": "^2.3.9",
4541
"lunr-languages": "^1.14.0",
4642
"lzma": "^2.3.2",
@@ -56,7 +52,6 @@
5652
"vite": "^7.3.1",
5753
"vscode-jsonrpc": "^9.0.0",
5854
"vscode-languageserver-protocol": "^3.16.0",
59-
"web-vitals": "^1.1.1",
6055
"xterm": "4.19.0",
6156
"xterm-addon-fit": "^0.5.0"
6257
},

src/common/debounce-util.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* (c) 2026, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
import { vi } from "vitest";
7+
import { debounce } from "./debounce-util";
8+
9+
describe("debounce", () => {
10+
beforeEach(() => {
11+
vi.useFakeTimers();
12+
});
13+
afterEach(() => {
14+
vi.useRealTimers();
15+
});
16+
17+
it("calls the function once with the latest arguments after the wait", () => {
18+
const fn = vi.fn();
19+
const debounced = debounce(fn, 100);
20+
debounced("first");
21+
debounced("second");
22+
vi.advanceTimersByTime(99);
23+
expect(fn).not.toHaveBeenCalled();
24+
vi.advanceTimersByTime(1);
25+
expect(fn).toHaveBeenCalledTimes(1);
26+
expect(fn).toHaveBeenCalledWith("second");
27+
});
28+
29+
it("restarts the wait on each call", () => {
30+
const fn = vi.fn();
31+
const debounced = debounce(fn, 100);
32+
debounced();
33+
vi.advanceTimersByTime(60);
34+
debounced();
35+
vi.advanceTimersByTime(60);
36+
expect(fn).not.toHaveBeenCalled();
37+
vi.advanceTimersByTime(40);
38+
expect(fn).toHaveBeenCalledTimes(1);
39+
});
40+
41+
it("fires again for calls after a completed wait", () => {
42+
const fn = vi.fn();
43+
const debounced = debounce(fn, 100);
44+
debounced();
45+
vi.advanceTimersByTime(100);
46+
debounced();
47+
vi.advanceTimersByTime(100);
48+
expect(fn).toHaveBeenCalledTimes(2);
49+
});
50+
});

src/common/debounce-util.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* (c) 2026, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
/**
8+
* Returns a function that delays calling fn until waitMs have elapsed
9+
* since the last call, invoking it with the most recent arguments.
10+
*/
11+
export const debounce = <A extends unknown[]>(
12+
fn: (...args: A) => void,
13+
waitMs: number
14+
): ((...args: A) => void) => {
15+
let timeout: ReturnType<typeof setTimeout> | undefined;
16+
return (...args: A) => {
17+
clearTimeout(timeout);
18+
timeout = setTimeout(() => fn(...args), waitMs);
19+
};
20+
};

src/common/sort-util.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* (c) 2026, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
import { sortBy } from "./sort-util";
7+
8+
describe("sortBy", () => {
9+
it("sorts ascending by a single iteratee", () => {
10+
expect(sortBy(["banana", "apple", "cherry"], (s) => s)).toEqual([
11+
"apple",
12+
"banana",
13+
"cherry",
14+
]);
15+
});
16+
17+
it("uses later iteratees as tie-breakers", () => {
18+
const files = [
19+
{ name: "b.py", main: false },
20+
{ name: "main.py", main: true },
21+
{ name: "a.py", main: false },
22+
];
23+
expect(
24+
sortBy(
25+
files,
26+
(f) => !f.main,
27+
(f) => f.name
28+
).map((f) => f.name)
29+
).toEqual(["main.py", "a.py", "b.py"]);
30+
});
31+
32+
it("is stable and does not mutate its input", () => {
33+
const input = [
34+
{ key: 1, id: "first" },
35+
{ key: 0, id: "a" },
36+
{ key: 1, id: "second" },
37+
];
38+
const result = sortBy(input, (x) => x.key);
39+
expect(result.map((x) => x.id)).toEqual(["a", "first", "second"]);
40+
expect(input.map((x) => x.id)).toEqual(["first", "a", "second"]);
41+
});
42+
});

src/common/sort-util.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* (c) 2026, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
type Iteratee<T> = (item: T) => string | number | boolean;
8+
9+
/**
10+
* Returns a copy of the array sorted ascending by the iteratees,
11+
* comparing by the first iteratee and using the others as tie-breakers.
12+
* The sort is stable.
13+
*/
14+
export const sortBy = <T>(
15+
items: readonly T[],
16+
...iteratees: Iteratee<T>[]
17+
): T[] =>
18+
[...items].sort((a, b) => {
19+
for (const iteratee of iteratees) {
20+
const left = iteratee(a);
21+
const right = iteratee(b);
22+
if (left < right) {
23+
return -1;
24+
}
25+
if (left > right) {
26+
return 1;
27+
}
28+
}
29+
return 0;
30+
});

src/documentation/api/ApiDocumentation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66
import { Divider, Link, List, ListItem } from "@microbit/ui";
7-
import sortBy from "lodash.sortby";
7+
import { sortBy } from "../../common/sort-util";
88
import { ReactNode, useCallback } from "react";
99
import { FormattedMessage, useIntl } from "react-intl";
1010
import { SystemStyleObject } from "styled-system/types";

src/documentation/search/extracts.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
*
44
* SPDX-License-Identifier: MIT
55
*/
6+
import { sortBy } from "../../common/sort-util";
67
import { Extract } from "./common";
78

89
export type Position = [number, number];
910

10-
// Avoid lodash in the worker
11-
export const sortByStart = (positions: Position[]): Position[] => {
12-
const copy = [...positions];
13-
copy.sort((a, b) => (a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0));
14-
return copy;
15-
};
11+
export const sortByStart = (positions: Position[]): Position[] =>
12+
sortBy(positions, (p) => p[0]);
1613

1714
/**
1815
* Return text or matches covering the string from start to end.

src/documentation/search/search-hooks.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* SPDX-License-Identifier: MIT
55
*/
6-
import debounce from "lodash.debounce";
6+
import { debounce } from "../../common/debounce-util";
77
import {
88
createContext,
99
ReactNode,

src/editor/codemirror/language-server/autocompletion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
insertBracket,
1212
} from "@codemirror/autocomplete";
1313
import { TransactionSpec } from "@codemirror/state";
14-
import sortBy from "lodash.sortby";
14+
import { sortBy } from "../../../common/sort-util";
1515
import { IntlShape } from "react-intl";
1616
import * as LSP from "vscode-languageserver-protocol";
1717
import {

0 commit comments

Comments
 (0)