@marimo-team/islands 0.19.8-dev8 → 0.19.8-dev9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +12 -9
- package/package.json +1 -1
- package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
- package/src/core/MarimoApp.tsx +12 -8
- package/src/plugins/core/registerReactComponent.tsx +9 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
- package/src/utils/__tests__/mime-types.test.ts +8 -10
- package/src/utils/mime-types.ts +5 -5
package/dist/main.js
CHANGED
|
@@ -52274,7 +52274,9 @@ Database schema: ${c}`), (_a3 = r2.aiFix) == null ? void 0 : _a3.setAiCompletion
|
|
|
52274
52274
|
let { visible: c, hidden: d } = applyHidingRules(new Set(e.map(([e2]) => e2)), r.hidingRules);
|
|
52275
52275
|
return {
|
|
52276
52276
|
entries: sortByPrecedence(e.filter(([e2]) => c.has(e2)), r.precedence),
|
|
52277
|
-
hidden:
|
|
52277
|
+
hidden: [
|
|
52278
|
+
...d
|
|
52279
|
+
]
|
|
52278
52280
|
};
|
|
52279
52281
|
}
|
|
52280
52282
|
const LazyVegaEmbed = import_react.lazy(() => import("./react-vega-3WcLHYC7.js").then((e) => ({
|
|
@@ -63542,10 +63544,10 @@ ${O}`,
|
|
|
63542
63544
|
}
|
|
63543
63545
|
var import_client$1 = __toESM(require_client(), 1);
|
|
63544
63546
|
function PluginSlotInternal({ hostElement: e, plugin: r, children: c, getInitialValue: d }, f) {
|
|
63545
|
-
let [_, v] = (0, import_react.useState)(c), [y, S] = (0, import_react.useState)(d()), { theme: w } = useTheme(), [E, O] = (0, import_react.useState)(() => r.validator.safeParse(parseDataset(e)));
|
|
63547
|
+
let [_, v] = (0, import_react.useState)(c), [y, S] = (0, import_react.useState)(d()), { theme: w } = useTheme(), [E, O] = (0, import_react.useState)(() => r.validator.safeParse(parseDataset(e))), [M, I] = (0, import_react.useState)(0);
|
|
63546
63548
|
(0, import_react.useImperativeHandle)(f, () => ({
|
|
63547
63549
|
reset: () => {
|
|
63548
|
-
S(d()), O(r.validator.safeParse(parseDataset(e)));
|
|
63550
|
+
S(d()), O(r.validator.safeParse(parseDataset(e))), I((e2) => e2 + 1);
|
|
63549
63551
|
},
|
|
63550
63552
|
setChildren: (e2) => {
|
|
63551
63553
|
v(e2);
|
|
@@ -63568,12 +63570,12 @@ ${O}`,
|
|
|
63568
63570
|
e,
|
|
63569
63571
|
r.validator
|
|
63570
63572
|
]);
|
|
63571
|
-
let
|
|
63573
|
+
let z = useEvent_default((r2) => {
|
|
63572
63574
|
S((c2) => {
|
|
63573
63575
|
let d2 = Functions.asUpdater(r2)(c2);
|
|
63574
63576
|
return shallowCompare(d2, c2) || e.dispatchEvent(createInputEvent(d2, e)), d2;
|
|
63575
63577
|
});
|
|
63576
|
-
}),
|
|
63578
|
+
}), G = (0, import_react.useMemo)(() => {
|
|
63577
63579
|
if (!r.functions) return {};
|
|
63578
63580
|
let c2 = {};
|
|
63579
63581
|
for (let [d2, f2] of Objects.entries(r.functions)) {
|
|
@@ -63606,7 +63608,8 @@ ${O}`,
|
|
|
63606
63608
|
return c2;
|
|
63607
63609
|
}, [
|
|
63608
63610
|
r.functions,
|
|
63609
|
-
e
|
|
63611
|
+
e,
|
|
63612
|
+
M
|
|
63610
63613
|
]);
|
|
63611
63614
|
return E.success ? (0, import_jsx_runtime.jsx)(StyleNamespace, {
|
|
63612
63615
|
children: (0, import_jsx_runtime.jsx)("div", {
|
|
@@ -63614,12 +63617,12 @@ ${O}`,
|
|
|
63614
63617
|
children: (0, import_jsx_runtime.jsx)(import_react.Suspense, {
|
|
63615
63618
|
fallback: (0, import_jsx_runtime.jsx)("div", {}),
|
|
63616
63619
|
children: r.render({
|
|
63617
|
-
setValue:
|
|
63620
|
+
setValue: z,
|
|
63618
63621
|
value: y,
|
|
63619
63622
|
data: E.data,
|
|
63620
63623
|
children: _,
|
|
63621
63624
|
host: e,
|
|
63622
|
-
functions:
|
|
63625
|
+
functions: G
|
|
63623
63626
|
})
|
|
63624
63627
|
})
|
|
63625
63628
|
})
|
|
@@ -73175,7 +73178,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
|
|
|
73175
73178
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
73176
73179
|
}
|
|
73177
73180
|
}
|
|
73178
|
-
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.8-
|
|
73181
|
+
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.8-dev9"), showCodeInRunModeAtom = atom(true);
|
|
73179
73182
|
atom(null);
|
|
73180
73183
|
var import_compiler_runtime$88 = require_compiler_runtime();
|
|
73181
73184
|
function useKeydownOnElement(e, r) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
ColumnDef,
|
|
4
|
+
PaginationState,
|
|
5
|
+
RowSelectionState,
|
|
6
|
+
SortingState,
|
|
7
|
+
} from "@tanstack/react-table";
|
|
8
|
+
import { render, screen, within } from "@testing-library/react";
|
|
4
9
|
import { describe, expect, it, vi } from "vitest";
|
|
5
10
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
11
|
import { DataTable } from "../data-table";
|
|
@@ -95,4 +100,91 @@ describe("DataTable", () => {
|
|
|
95
100
|
expect(rows[1]).toHaveAttribute("title", "Michael Scott");
|
|
96
101
|
expect(rows[2]).toHaveAttribute("title", "Jim Halpert");
|
|
97
102
|
});
|
|
103
|
+
|
|
104
|
+
it("should display updated data after rerender with manual sorting and pagination", () => {
|
|
105
|
+
// Simulates the bug from issue #8023:
|
|
106
|
+
// When a user sorts a table, rows that moved from page 2 to page 1
|
|
107
|
+
// don't visually refresh after the underlying data is updated.
|
|
108
|
+
|
|
109
|
+
interface RowData {
|
|
110
|
+
id: number;
|
|
111
|
+
status: string;
|
|
112
|
+
value: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Initial data: 4 rows, page_size=3
|
|
116
|
+
const initialData: RowData[] = [
|
|
117
|
+
{ id: 4, status: "pending", value: 40 },
|
|
118
|
+
{ id: 3, status: "pending", value: 30 },
|
|
119
|
+
{ id: 2, status: "pending", value: 20 },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const columns: ColumnDef<RowData>[] = [
|
|
123
|
+
{ id: "id", accessorFn: (row) => row.id, header: "id" },
|
|
124
|
+
{ id: "status", accessorFn: (row) => row.status, header: "status" },
|
|
125
|
+
{ id: "value", accessorFn: (row) => row.value, header: "value" },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Simulate sorted state (value descending) - manual sorting means
|
|
129
|
+
// data comes pre-sorted from backend
|
|
130
|
+
const sorting: SortingState = [{ id: "value", desc: true }];
|
|
131
|
+
const setSorting = vi.fn();
|
|
132
|
+
|
|
133
|
+
const paginationState: PaginationState = { pageIndex: 0, pageSize: 3 };
|
|
134
|
+
const setPaginationState = vi.fn();
|
|
135
|
+
|
|
136
|
+
const commonProps = {
|
|
137
|
+
columns,
|
|
138
|
+
selection: null as "single" | "multi" | null,
|
|
139
|
+
totalRows: 4,
|
|
140
|
+
totalColumns: 3,
|
|
141
|
+
pagination: true,
|
|
142
|
+
manualPagination: true,
|
|
143
|
+
paginationState,
|
|
144
|
+
setPaginationState,
|
|
145
|
+
manualSorting: true,
|
|
146
|
+
sorting,
|
|
147
|
+
setSorting,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const { rerender } = render(
|
|
151
|
+
<TooltipProvider>
|
|
152
|
+
<DataTable {...commonProps} data={initialData} />
|
|
153
|
+
</TooltipProvider>,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Verify initial data is displayed - look for "pending" in cells
|
|
157
|
+
const rows = screen.getAllByRole("row");
|
|
158
|
+
// Row 0 is header, rows 1-3 are data rows
|
|
159
|
+
expect(rows).toHaveLength(4); // 1 header + 3 data rows
|
|
160
|
+
// All rows should show "pending"
|
|
161
|
+
expect(within(rows[1]).getByText("pending")).toBeTruthy();
|
|
162
|
+
expect(within(rows[2]).getByText("pending")).toBeTruthy();
|
|
163
|
+
expect(within(rows[3]).getByText("pending")).toBeTruthy();
|
|
164
|
+
|
|
165
|
+
// Now simulate data update: row with id=4 is now "approved"
|
|
166
|
+
// Backend returns sorted data with the update applied
|
|
167
|
+
const updatedData: RowData[] = [
|
|
168
|
+
{ id: 4, status: "approved", value: 40 },
|
|
169
|
+
{ id: 3, status: "pending", value: 30 },
|
|
170
|
+
{ id: 2, status: "pending", value: 20 },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Rerender with updated data (same sorting, same pagination)
|
|
174
|
+
rerender(
|
|
175
|
+
<TooltipProvider>
|
|
176
|
+
<DataTable {...commonProps} data={updatedData} />
|
|
177
|
+
</TooltipProvider>,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// BUG: The row should show "approved" but might show stale "pending"
|
|
181
|
+
const updatedRows = screen.getAllByRole("row");
|
|
182
|
+
expect(updatedRows).toHaveLength(4);
|
|
183
|
+
|
|
184
|
+
// The first data row (id=4) should now show "approved"
|
|
185
|
+
expect(within(updatedRows[1]).getByText("approved")).toBeTruthy();
|
|
186
|
+
// Other rows should still show "pending"
|
|
187
|
+
expect(within(updatedRows[2]).getByText("pending")).toBeTruthy();
|
|
188
|
+
expect(within(updatedRows[3]).getByText("pending")).toBeTruthy();
|
|
189
|
+
});
|
|
98
190
|
});
|
package/src/core/MarimoApp.tsx
CHANGED
|
@@ -39,14 +39,18 @@ const LazyGalleryPage = reactLazyWithPreload(
|
|
|
39
39
|
);
|
|
40
40
|
|
|
41
41
|
export function preloadPage(mode: string) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
switch (mode) {
|
|
43
|
+
case "home":
|
|
44
|
+
LazyHomePage.preload();
|
|
45
|
+
break;
|
|
46
|
+
case "gallery":
|
|
47
|
+
LazyGalleryPage.preload();
|
|
48
|
+
break;
|
|
49
|
+
case "read":
|
|
50
|
+
LazyRunPage.preload();
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
LazyEditPage.preload();
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -101,10 +101,17 @@ function PluginSlotInternal<T>(
|
|
|
101
101
|
return plugin.validator.safeParse(parseDataset(hostElement));
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// Incremented on each reset to invalidate memoized function references.
|
|
105
|
+
// This ensures that plugin functions (e.g., search) are re-created when
|
|
106
|
+
// the underlying UI element instance changes (new object-id), even if
|
|
107
|
+
// the element's data attributes haven't changed.
|
|
108
|
+
const [resetNonce, setResetNonce] = useState(0);
|
|
109
|
+
|
|
104
110
|
useImperativeHandle(ref, () => ({
|
|
105
111
|
reset: () => {
|
|
106
112
|
setValue(getInitialValue());
|
|
107
113
|
setParsedResult(plugin.validator.safeParse(parseDataset(hostElement)));
|
|
114
|
+
setResetNonce((n) => n + 1);
|
|
108
115
|
},
|
|
109
116
|
setChildren: (children) => {
|
|
110
117
|
setChildNodes(children);
|
|
@@ -224,7 +231,8 @@ function PluginSlotInternal<T>(
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
return methods;
|
|
227
|
-
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
}, [plugin.functions, hostElement, resetNonce]);
|
|
228
236
|
|
|
229
237
|
// If we failed to parse the initial value, render an error
|
|
230
238
|
if (!parsedResult.success) {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
|
4
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import { Provider } from "jotai";
|
|
6
|
+
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { DownloadAsArgs } from "@/components/data-table/schemas";
|
|
8
|
+
import type { FieldTypesWithExternalType } from "@/components/data-table/types";
|
|
9
|
+
import { store } from "@/core/state/jotai";
|
|
10
|
+
import {
|
|
11
|
+
type GetDataUrl,
|
|
12
|
+
type GetRowIds,
|
|
13
|
+
LoadingDataTableComponent,
|
|
14
|
+
} from "../DataTablePlugin";
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
global.ResizeObserver = class ResizeObserver {
|
|
18
|
+
observe() {
|
|
19
|
+
// do nothing
|
|
20
|
+
}
|
|
21
|
+
unobserve() {
|
|
22
|
+
// do nothing
|
|
23
|
+
}
|
|
24
|
+
disconnect() {
|
|
25
|
+
// do nothing
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("LoadingDataTableComponent", () => {
|
|
31
|
+
/**
|
|
32
|
+
* Regression test for https://github.com/marimo-team/marimo/issues/8023
|
|
33
|
+
*
|
|
34
|
+
* When a table is replaced via mo.output.replace() with updated data,
|
|
35
|
+
* but the initial page data (unsorted first page) hasn't changed,
|
|
36
|
+
* the useAsyncData hook's deps may all remain the same.
|
|
37
|
+
* Previously, the `search` function reference was memoized on
|
|
38
|
+
* [plugin.functions, hostElement] and wouldn't change on reset(),
|
|
39
|
+
* so the useAsyncData effect wouldn't re-fire.
|
|
40
|
+
*
|
|
41
|
+
* The fix adds a resetNonce to the functionMethods memo deps,
|
|
42
|
+
* so when the plugin is reset (table instance changes), the search
|
|
43
|
+
* function reference changes, triggering useAsyncData to re-fetch.
|
|
44
|
+
*
|
|
45
|
+
* This test verifies that when the search function reference changes
|
|
46
|
+
* (simulating reset()), the component re-fetches data even if
|
|
47
|
+
* props.data hasn't changed.
|
|
48
|
+
*/
|
|
49
|
+
it("should refetch data when search function reference changes", async () => {
|
|
50
|
+
const host = document.createElement("div");
|
|
51
|
+
const setValue = vi.fn();
|
|
52
|
+
|
|
53
|
+
// The initial page data string - identical for both renders.
|
|
54
|
+
// This simulates the case where only a row on page 2 changed,
|
|
55
|
+
// so the first page data is the same.
|
|
56
|
+
const initialPageData = JSON.stringify([
|
|
57
|
+
{ id: 1, status: "pending", value: 10 },
|
|
58
|
+
{ id: 2, status: "pending", value: 20 },
|
|
59
|
+
{ id: 3, status: "pending", value: 30 },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const searchResult = {
|
|
63
|
+
data: [
|
|
64
|
+
{ id: 1, status: "pending", value: 10 },
|
|
65
|
+
{ id: 2, status: "pending", value: 20 },
|
|
66
|
+
{ id: 3, status: "pending", value: 30 },
|
|
67
|
+
],
|
|
68
|
+
total_rows: 4,
|
|
69
|
+
cell_styles: null,
|
|
70
|
+
cell_hover_texts: null,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const searchFn1 = vi.fn().mockResolvedValue(searchResult);
|
|
74
|
+
const searchFn2 = vi.fn().mockResolvedValue(searchResult);
|
|
75
|
+
|
|
76
|
+
const fieldTypes: FieldTypesWithExternalType = [
|
|
77
|
+
["id", ["integer", "integer"]],
|
|
78
|
+
["status", ["string", "string"]],
|
|
79
|
+
["value", ["integer", "integer"]],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const commonProps = {
|
|
83
|
+
label: null,
|
|
84
|
+
totalRows: 4,
|
|
85
|
+
pagination: true,
|
|
86
|
+
pageSize: 3,
|
|
87
|
+
selection: "single" as const,
|
|
88
|
+
showDownload: false,
|
|
89
|
+
showFilters: false,
|
|
90
|
+
showColumnSummaries: false as const,
|
|
91
|
+
showDataTypes: false,
|
|
92
|
+
showPageSizeSelector: false,
|
|
93
|
+
showColumnExplorer: false,
|
|
94
|
+
showRowExplorer: false,
|
|
95
|
+
showChartBuilder: false,
|
|
96
|
+
rowHeaders: [] as FieldTypesWithExternalType,
|
|
97
|
+
fieldTypes,
|
|
98
|
+
totalColumns: 3,
|
|
99
|
+
maxColumns: "all" as const,
|
|
100
|
+
hasStableRowId: false,
|
|
101
|
+
lazy: false,
|
|
102
|
+
host,
|
|
103
|
+
enableSearch: true,
|
|
104
|
+
value: [] as (number | string | { rowId: string; columnName?: string })[],
|
|
105
|
+
setValue,
|
|
106
|
+
download_as: vi.fn() as DownloadAsArgs,
|
|
107
|
+
get_column_summaries: vi.fn().mockResolvedValue({
|
|
108
|
+
data: null,
|
|
109
|
+
stats: {},
|
|
110
|
+
bin_values: {},
|
|
111
|
+
value_counts: {},
|
|
112
|
+
show_charts: false,
|
|
113
|
+
}),
|
|
114
|
+
get_data_url: vi.fn() as GetDataUrl,
|
|
115
|
+
get_row_ids: vi.fn() as GetRowIds,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
119
|
+
<Provider store={store}>
|
|
120
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
121
|
+
</Provider>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Render with first search function
|
|
125
|
+
const { rerender } = render(
|
|
126
|
+
<Wrapper>
|
|
127
|
+
<LoadingDataTableComponent
|
|
128
|
+
{...commonProps}
|
|
129
|
+
data={initialPageData}
|
|
130
|
+
search={searchFn1}
|
|
131
|
+
/>
|
|
132
|
+
</Wrapper>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Wait for the table to render with data
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(screen.getAllByRole("row").length).toBeGreaterThan(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Search was called on initial load (fire-and-forget for canShowInitialPage)
|
|
141
|
+
expect(searchFn1).toHaveBeenCalled();
|
|
142
|
+
|
|
143
|
+
// Now rerender with the same data but a NEW search function reference.
|
|
144
|
+
// This simulates what happens after reset() when resetNonce increments
|
|
145
|
+
// and functionMethods is recreated.
|
|
146
|
+
await act(async () => {
|
|
147
|
+
rerender(
|
|
148
|
+
<Wrapper>
|
|
149
|
+
<LoadingDataTableComponent
|
|
150
|
+
{...commonProps}
|
|
151
|
+
data={initialPageData}
|
|
152
|
+
search={searchFn2}
|
|
153
|
+
/>
|
|
154
|
+
</Wrapper>,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// The new search function should be called because the search
|
|
159
|
+
// dependency changed in useAsyncData.
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(searchFn2).toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -132,7 +132,7 @@ describe("mime-types", () => {
|
|
|
132
132
|
|
|
133
133
|
describe("sortByPrecedence", () => {
|
|
134
134
|
it("should sort entries by precedence order", () => {
|
|
135
|
-
const entries:
|
|
135
|
+
const entries: [MimeType, string][] = [
|
|
136
136
|
["text/plain", "plain"],
|
|
137
137
|
["text/html", "html"],
|
|
138
138
|
["image/png", "png"],
|
|
@@ -151,7 +151,7 @@ describe("mime-types", () => {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
it("should place unknown mime types at the end", () => {
|
|
154
|
-
const entries:
|
|
154
|
+
const entries: [MimeType, string][] = [
|
|
155
155
|
["text/plain", "plain"],
|
|
156
156
|
["text/html", "html"],
|
|
157
157
|
["application/json", "json"],
|
|
@@ -173,7 +173,7 @@ describe("mime-types", () => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
it("should handle empty precedence", () => {
|
|
176
|
-
const entries:
|
|
176
|
+
const entries: [MimeType, string][] = [
|
|
177
177
|
["text/plain", "plain"],
|
|
178
178
|
["text/html", "html"],
|
|
179
179
|
];
|
|
@@ -184,7 +184,7 @@ describe("mime-types", () => {
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
it("should not mutate original array", () => {
|
|
187
|
-
const entries:
|
|
187
|
+
const entries: [MimeType, string][] = [
|
|
188
188
|
["text/plain", "plain"],
|
|
189
189
|
["text/html", "html"],
|
|
190
190
|
];
|
|
@@ -198,7 +198,7 @@ describe("mime-types", () => {
|
|
|
198
198
|
|
|
199
199
|
describe("processMimeBundle", () => {
|
|
200
200
|
it("should filter and sort mime entries", () => {
|
|
201
|
-
const entries:
|
|
201
|
+
const entries: [MimeType, string][] = [
|
|
202
202
|
["text/plain", "plain"],
|
|
203
203
|
["text/html", "html"],
|
|
204
204
|
["image/png", "png"],
|
|
@@ -226,7 +226,7 @@ describe("mime-types", () => {
|
|
|
226
226
|
});
|
|
227
227
|
|
|
228
228
|
it("should use default config when not provided", () => {
|
|
229
|
-
const entries:
|
|
229
|
+
const entries: [MimeType, string][] = [
|
|
230
230
|
["text/html", "html"],
|
|
231
231
|
["image/png", "png"],
|
|
232
232
|
["text/markdown", "md"],
|
|
@@ -240,9 +240,7 @@ describe("mime-types", () => {
|
|
|
240
240
|
|
|
241
241
|
it("should preserve data associated with mime types", () => {
|
|
242
242
|
const htmlData = { content: "<h1>Hello</h1>" };
|
|
243
|
-
const entries:
|
|
244
|
-
["text/html", htmlData],
|
|
245
|
-
];
|
|
243
|
+
const entries: [MimeType, typeof htmlData][] = [["text/html", htmlData]];
|
|
246
244
|
|
|
247
245
|
const result = processMimeBundle(entries);
|
|
248
246
|
|
|
@@ -250,7 +248,7 @@ describe("mime-types", () => {
|
|
|
250
248
|
});
|
|
251
249
|
|
|
252
250
|
it("should sort by precedence after filtering", () => {
|
|
253
|
-
const entries:
|
|
251
|
+
const entries: [MimeType, string][] = [
|
|
254
252
|
["text/plain", "plain"],
|
|
255
253
|
["text/markdown", "md"],
|
|
256
254
|
["text/html", "html"],
|
package/src/utils/mime-types.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface MimeTypeConfig {
|
|
|
26
26
|
*/
|
|
27
27
|
export interface ProcessedMimeTypes<T> {
|
|
28
28
|
/** The filtered and sorted mime entries */
|
|
29
|
-
entries:
|
|
29
|
+
entries: [MimeType, T][];
|
|
30
30
|
/** Mime types that were hidden by rules */
|
|
31
31
|
hidden: MimeType[];
|
|
32
32
|
}
|
|
@@ -146,9 +146,9 @@ export function applyHidingRules(
|
|
|
146
146
|
* Mime types not in the map are placed at the end, preserving their original order.
|
|
147
147
|
*/
|
|
148
148
|
export function sortByPrecedence<T>(
|
|
149
|
-
entries:
|
|
149
|
+
entries: [MimeType, T][],
|
|
150
150
|
precedence: ReadonlyMap<MimeType, number>,
|
|
151
|
-
):
|
|
151
|
+
): [MimeType, T][] {
|
|
152
152
|
const unknownPrecedence = precedence.size;
|
|
153
153
|
|
|
154
154
|
return [...entries].sort((a, b) => {
|
|
@@ -162,7 +162,7 @@ export function sortByPrecedence<T>(
|
|
|
162
162
|
* Main entry point: processes mime entries by applying hiding rules and sorting.
|
|
163
163
|
*/
|
|
164
164
|
export function processMimeBundle<T>(
|
|
165
|
-
entries:
|
|
165
|
+
entries: [MimeType, T][],
|
|
166
166
|
config: MimeTypeConfig = getDefaultMimeConfig(),
|
|
167
167
|
): ProcessedMimeTypes<T> {
|
|
168
168
|
if (entries.length === 0) {
|
|
@@ -176,6 +176,6 @@ export function processMimeBundle<T>(
|
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
178
|
entries: sortedEntries,
|
|
179
|
-
hidden:
|
|
179
|
+
hidden: [...hidden],
|
|
180
180
|
};
|
|
181
181
|
}
|