mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-02 23:20:26 -04:00
Compare commits
11 Commits
chore/docs
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b4e8c540 | ||
|
|
47c6d01617 | ||
|
|
653be9a604 | ||
|
|
2d8b74282a | ||
|
|
48752bcd06 | ||
|
|
a46620d236 | ||
|
|
3bde6df958 | ||
|
|
e1ddc06eff | ||
|
|
262b531add | ||
|
|
364af97060 | ||
|
|
7b0d1fde64 |
@@ -44,6 +44,7 @@
|
||||
8000, // used by mkdocs
|
||||
9000,
|
||||
9091, // used by docker production
|
||||
51204, // used for test coverage report
|
||||
24678 // used by nuxt when hot-reloading using polling
|
||||
],
|
||||
// Use 'onCreateCommand' to run commands at the end of container creation.
|
||||
|
||||
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -20,6 +20,10 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Install from the committed lockfile; never re-resolve (see pyproject
|
||||
# [tool.uv] exclude-newer cooling window).
|
||||
UV_FROZEN: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
4
.github/workflows/locale-sync.yml
vendored
4
.github/workflows/locale-sync.yml
vendored
@@ -14,6 +14,10 @@ permissions:
|
||||
jobs:
|
||||
sync-locales:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Install from the committed lockfile; never re-resolve (see pyproject
|
||||
# [tool.uv] exclude-newer cooling window).
|
||||
UV_FROZEN: "1"
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
|
||||
49
.github/workflows/pull-request-lint.yml
vendored
49
.github/workflows/pull-request-lint.yml
vendored
@@ -3,7 +3,7 @@ name: Pull Request Linter
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
types: [edited] # This captures the PR title changing
|
||||
types: [edited, reopened] # This captures the PR title/body changing
|
||||
branches:
|
||||
- mealie-next
|
||||
|
||||
@@ -41,3 +41,50 @@ jobs:
|
||||
ignoreLabels: |
|
||||
bot
|
||||
ignore-semantic-pull-request
|
||||
|
||||
validate-template:
|
||||
name: Validate PR template
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check required PR template sections
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
if (pr.user.type === "Bot") {
|
||||
console.log("Skipping template check for bot");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: ".github/pull_request_template.md",
|
||||
});
|
||||
|
||||
const template = Buffer.from(response.data.content, "base64").toString("utf8");
|
||||
const lines = template.split("\n");
|
||||
|
||||
const requiredHeadings = [];
|
||||
let lastHeading = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
lastHeading = line.trim();
|
||||
} else if (line.trim() === "_(REQUIRED)_" && lastHeading) {
|
||||
requiredHeadings.push(lastHeading);
|
||||
lastHeading = null;
|
||||
}
|
||||
}
|
||||
|
||||
const body = pr.body || "";
|
||||
const missing = requiredHeadings.filter(h => !body.includes(h));
|
||||
|
||||
if (missing.length > 0) {
|
||||
core.setFailed(`Missing headings:\n${missing.join("\n")}`);
|
||||
} else {
|
||||
console.log("All required headings present");
|
||||
}
|
||||
|
||||
2
.github/workflows/pull-requests.yml
vendored
2
.github/workflows/pull-requests.yml
vendored
@@ -18,6 +18,8 @@ jobs:
|
||||
name: "Lint PR"
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/workflows/pull-request-lint.yml
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
backend-tests:
|
||||
name: "Backend Server Tests"
|
||||
|
||||
4
.github/workflows/test-backend.yml
vendored
4
.github/workflows/test-backend.yml
vendored
@@ -13,6 +13,10 @@ jobs:
|
||||
|
||||
env:
|
||||
PRODUCTION: false
|
||||
# Install from the committed lockfile; never re-resolve. The rolling
|
||||
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
|
||||
# every uv command re-resolve and fail on in-window pins.
|
||||
UV_FROZEN: "1"
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
30
.vscode/test-block.code-snippets
vendored
Normal file
30
.vscode/test-block.code-snippets
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Test Block": {
|
||||
"prefix": "mtest",
|
||||
"body": [
|
||||
"import { mount } from \"@vue/test-utils\";",
|
||||
"import { describe, expect, test, vi } from \"vitest\";",
|
||||
"import { makeWrapper } from \"~/tests/utils\";",
|
||||
"",
|
||||
"const wrapper = () => makeWrapper(() => {",
|
||||
" return ${1:composable}();",
|
||||
"});",
|
||||
"",
|
||||
"describe(\"${TM_FILENAME_BASE/(.*)\\..+$/$1/}\", () => {",
|
||||
" describe(\"${2:method}\", () => {",
|
||||
" test(\"It does the thing\", () => {",
|
||||
" const { ${2:method} } = wrapper();",
|
||||
" const result = ${2:method}();",
|
||||
" expect(result).toBe(EXPECTED);",
|
||||
" });",
|
||||
" });",
|
||||
"});",
|
||||
"",
|
||||
],
|
||||
"description": "Insert a test block",
|
||||
"scope": "typescript",
|
||||
"include": [
|
||||
"**/*.test.{ts,tsx,vue}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ env:
|
||||
DEFAULT_GROUP: Home
|
||||
DEFAULT_HOUSEHOLD: Family
|
||||
PRODUCTION: false
|
||||
# Install from the committed lockfile; never re-resolve. Required because the
|
||||
# rolling `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise
|
||||
# make every `uv run`/`uv sync` re-resolve and fail on in-window pins.
|
||||
UV_FROZEN: "1"
|
||||
API_PORT: 9000
|
||||
API_DOCS: True
|
||||
TOKEN_TIME: 256 # hours
|
||||
|
||||
@@ -52,6 +52,11 @@ RUN apt-get update \
|
||||
|
||||
RUN pip install uv
|
||||
|
||||
# Install from the committed lockfile; never re-resolve. The rolling
|
||||
# `exclude-newer` cooling window (pyproject [tool.uv]) would otherwise make
|
||||
# `uv export` below re-resolve and fail on in-window pins.
|
||||
ENV UV_FROZEN=1
|
||||
|
||||
WORKDIR /mealie
|
||||
|
||||
# copy project files here to ensure they will be cached.
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
| --------------------------- | :-----: | ----------------------------------------------------------------------------------- |
|
||||
| SECURITY_MAX_LOGIN_ATTEMPTS | 5 | Maximum times a user can provide an invalid password before their account is locked |
|
||||
| SECURITY_USER_LOCKOUT_TIME | 24 | Time in hours for how long a users account is locked |
|
||||
| ALLOWED_IFRAME_HOSTS | `""` | Comma-separated extra hostnames allowed as `<iframe>` sources in recipe content. Extends the built-in list of trusted video providers (YouTube, Vimeo). Subdomains are included automatically. Only `https` sources are permitted. Adding hosts here opts into rendering embeds from those origins to all viewers, including the public, so add only origins you trust. |
|
||||
|
||||
### Database
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
To harden Mealie against malicious content, <code><iframe></code> embeds in recipe
|
||||
instructions, notes, and descriptions are now restricted to a trusted set of hosts.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
By default, embeds are allowed only from well-known video providers:
|
||||
<ul class="ml-6">
|
||||
<li>YouTube</li>
|
||||
<li>Vimeo</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
Existing recipes that embed content from <strong>other</strong> hosts will no longer render
|
||||
those embeds. The rest of the recipe is unaffected.
|
||||
</p>
|
||||
<div v-if="user?.admin">
|
||||
<hr class="mt-2 mb-4">
|
||||
<p>
|
||||
As an admin, you can allow additional hosts with the <code>ALLOWED_IFRAME_HOSTS</code>
|
||||
environment variable (comma-separated). It extends the built-in defaults, and only
|
||||
<code>https</code> sources are permitted. See the configuration docs for details:
|
||||
<br>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
color="primary"
|
||||
href="https://docs.mealie.io/documentation/getting-started/installation/backend-config/"
|
||||
target="_blank"
|
||||
>
|
||||
Backend Configuration
|
||||
</v-btn>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AnnouncementMeta } from "~/composables/use-announcements";
|
||||
|
||||
const { user } = useMealieAuth();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const meta: AnnouncementMeta = {
|
||||
title: "Recipe embeds restricted to trusted hosts",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
p {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -59,9 +59,10 @@
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
<p v-if="deleteTarget" class="mt-4 ml-4">
|
||||
<p v-if="deleteTarget" class="mt-4 mb-0 font-weight-bold">
|
||||
{{ deleteTarget.name || deleteTarget.title || deleteTarget.id }}
|
||||
</p>
|
||||
<slot name="delete-dialog-bottom" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -88,6 +89,7 @@
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-card>
|
||||
<slot name="delete-dialog-bottom" />
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -151,7 +153,7 @@ const createDialog = defineModel("createDialog", { type: Boolean, default: false
|
||||
const editForm = defineModel<{ items: AutoFormItems; data: Record<string, any> }>("editForm", { required: true });
|
||||
const editDialog = defineModel("editDialog", { type: Boolean, default: false });
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -185,6 +187,10 @@ defineProps({
|
||||
type: String,
|
||||
default: "name",
|
||||
},
|
||||
onDeleteDialogOpen: {
|
||||
type: Function as PropType<(items: any[]) => Promise<void>>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
@@ -212,8 +218,11 @@ const editEventHandler = (item: any) => {
|
||||
const deleteTarget = ref<any>(null);
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
function deleteEventHandler(item: any) {
|
||||
async function deleteEventHandler(item: any) {
|
||||
deleteTarget.value = item;
|
||||
if (props.onDeleteDialogOpen) {
|
||||
await props.onDeleteDialogOpen([item]);
|
||||
}
|
||||
deleteDialog.value = true;
|
||||
}
|
||||
|
||||
@@ -222,8 +231,11 @@ function deleteEventHandler(item: any) {
|
||||
const bulkDeleteTarget = ref<Array<any>>([]);
|
||||
const bulkDeleteDialog = ref(false);
|
||||
|
||||
function bulkDeleteEventHandler(items: Array<any>) {
|
||||
async function bulkDeleteEventHandler(items: Array<any>) {
|
||||
bulkDeleteTarget.value = items;
|
||||
if (props.onDeleteDialogOpen) {
|
||||
await props.onDeleteDialogOpen(items);
|
||||
}
|
||||
bulkDeleteDialog.value = true;
|
||||
console.log("Bulk Delete Event Handler", items);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
import { truncateText as truncatePlainText } from "~/lib/sanitize/text";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
|
||||
@@ -50,10 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
defineEmits(["item-selected"]);
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
const content = node.textContent || "";
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
return truncatePlainText(text, length, clamp);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
enum DOMPurifyHook {
|
||||
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||
}
|
||||
import { sanitizeMarkdownHtml } from "~/lib/sanitize/markdown";
|
||||
|
||||
const props = defineProps({
|
||||
source: {
|
||||
@@ -18,48 +14,11 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const ALLOWED_STYLE_TAGS = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
|
||||
function sanitizeMarkdown(rawHtml: string | null | undefined): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_TAGS.includes(property.trim().toLowerCase());
|
||||
});
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
});
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: [
|
||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote", "iframe",
|
||||
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
"href", "src", "alt", "height", "width", "class", "allow", "title", "allowfullscreen", "frameborder",
|
||||
"scrolling", "cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
],
|
||||
});
|
||||
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
const { $appInfo } = useNuxtApp();
|
||||
|
||||
const value = computed(() => {
|
||||
const rawHtml = marked.parse(props.source || "", { async: false, breaks: true });
|
||||
return sanitizeMarkdown(rawHtml);
|
||||
return sanitizeMarkdownHtml(rawHtml, $appInfo?.allowedIframeHosts ?? []);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { IngredientFood, RecipeSummary, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
|
||||
|
||||
export const MOCK_ITEM: ShoppingListItemOut = {
|
||||
shoppingListId: "",
|
||||
id: "",
|
||||
groupId: "",
|
||||
householdId: "",
|
||||
display: "MOCK_ITEM",
|
||||
updatedAt: "100",
|
||||
position: 1,
|
||||
checked: false,
|
||||
createdAt: "100",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE: RecipeSummary = {
|
||||
id: "recipe-id",
|
||||
name: "Recipe!",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE2: RecipeSummary = {
|
||||
...MOCK_RECIPE,
|
||||
id: undefined,
|
||||
name: "Recipe 2!",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD: IngredientFood = {
|
||||
id: "1",
|
||||
name: "food 1",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD2: IngredientFood = {
|
||||
id: "2",
|
||||
name: "food 2",
|
||||
};
|
||||
|
||||
export const MOCK_LABEL: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_LABEL2: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL2",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_SHOPPING_LIST: ShoppingListOut = {
|
||||
groupId: "",
|
||||
userId: "",
|
||||
id: "",
|
||||
householdId: "",
|
||||
labelSettings: [
|
||||
MOCK_LABEL,
|
||||
MOCK_LABEL2,
|
||||
],
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
recipeReferences: [{
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE,
|
||||
}, {
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE2,
|
||||
}],
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as vueusecore from "@vueuse/core";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListCopy } from "../use-shopping-list-copy";
|
||||
import { MOCK_ITEM } from "./mocks";
|
||||
|
||||
vi.mock("@vueuse/core", { spy: true });
|
||||
|
||||
const mockCopy = vi.fn().mockImplementation(args => new Promise(resolve => resolve(args)));
|
||||
|
||||
vi.mocked(vueusecore.useClipboard).mockImplementation(() => {
|
||||
return {
|
||||
isSupported: computed(() => true),
|
||||
copied: computed(() => true),
|
||||
text: computed(() => ""),
|
||||
copy: mockCopy,
|
||||
};
|
||||
});
|
||||
const wrapper = () => makeWrapper(useShoppingListCopy);
|
||||
|
||||
const TEST_HEADER = "SPECIAL HEADER!";
|
||||
|
||||
const MOCK_LIST: { [key: string]: ShoppingListItemOut[] } = {
|
||||
[TEST_HEADER]: [MOCK_ITEM],
|
||||
[TEST_HEADER + "2"]: [MOCK_ITEM],
|
||||
};
|
||||
|
||||
describe("Shopping list copy composable", () => {
|
||||
describe("copyListItems", () => {
|
||||
test("copies markdown lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "markdown");
|
||||
const expected = [
|
||||
"# SPECIAL HEADER!",
|
||||
"- [ ] MOCK_ITEM",
|
||||
"",
|
||||
"# SPECIAL HEADER!2",
|
||||
"- [ ] MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
test("copies plain text lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "plain");
|
||||
const expected = [
|
||||
"[SPECIAL HEADER!]",
|
||||
"MOCK_ITEM",
|
||||
"",
|
||||
"[SPECIAL HEADER!2]",
|
||||
"MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedLabelHeading", () => {
|
||||
test("copies markdown headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("markdown", TEST_HEADER);
|
||||
expect(header).toEqual(`# ${TEST_HEADER}`);
|
||||
});
|
||||
test("copies plain text headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("plain", TEST_HEADER);
|
||||
expect(header).toEqual(`[${TEST_HEADER}]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedListItem", () => {
|
||||
test("copies markdown items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("markdown", MOCK_ITEM);
|
||||
expect(header).toEqual(`- [ ] ${MOCK_ITEM.display}`);
|
||||
});
|
||||
test("copies plain text items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", MOCK_ITEM);
|
||||
expect(header).toEqual(MOCK_ITEM.display);
|
||||
});
|
||||
test("copies items without a display as empty", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", { ...MOCK_ITEM, display: undefined });
|
||||
expect(header).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListSorting } from "../use-shopping-list-sorting";
|
||||
import { MOCK_FOOD, MOCK_FOOD2, MOCK_ITEM, MOCK_LABEL, MOCK_LABEL2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = () => makeWrapper(() => {
|
||||
const { t } = useI18n();
|
||||
return {
|
||||
t,
|
||||
...useShoppingListSorting(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-sorting", () => {
|
||||
describe("sortItems", () => {
|
||||
const { sortItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: 0 }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts by createdAt next", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: "0" });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: "0" }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts similar items into the same spot", () => {
|
||||
const result = sortItems(MOCK_ITEM, MOCK_ITEM);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
});
|
||||
describe("sortListItems", () => {
|
||||
const { sortListItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: [MOCK_ITEM, { ...MOCK_ITEM, position: 0 }, { ...MOCK_ITEM, createdAt: "0" }] };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, position: 0 },
|
||||
{ ...MOCK_ITEM, createdAt: "0" },
|
||||
MOCK_ITEM,
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe("updateItemsByLabel", () => {
|
||||
const { updateItemsByLabel, t } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("ignores checked items", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("returns unordered labels if no ordering is specified", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST,
|
||||
labelSettings: undefined,
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("groupAndSortListItemsByFood", () => {
|
||||
const { groupAndSortListItemsByFood } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(MOCK_SHOPPING_LIST.listItems);
|
||||
});
|
||||
test("groups checked items together", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2 },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("populates position and created at if not present", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList: ShoppingListOut = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListState } from "../use-shopping-list-state";
|
||||
import { MOCK_ITEM, MOCK_RECIPE, MOCK_RECIPE2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = (list: ShoppingListOut = MOCK_SHOPPING_LIST) => makeWrapper(() => {
|
||||
const { shoppingList, ...state } = useShoppingListState();
|
||||
shoppingList.value = list;
|
||||
return {
|
||||
shoppingList,
|
||||
...state,
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-state", () => {
|
||||
describe("checked items are sorted", () => {
|
||||
const { sortCheckedItems } = wrapper();
|
||||
|
||||
test("by timestamp", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "200" });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "0" });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
});
|
||||
test("by position if timestamps match", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 2 });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const sorted3 = sortCheckedItems({ ...MOCK_ITEM, position: undefined }, { ...MOCK_ITEM, position: undefined });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
expect(sorted3).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recipeMap", () => {
|
||||
test("Updates to match shopping list recipe references", () => {
|
||||
const { recipeMap } = wrapper();
|
||||
expect(recipeMap).toEqual(new Map([
|
||||
[MOCK_RECIPE.id, MOCK_RECIPE],
|
||||
["", MOCK_RECIPE2],
|
||||
]));
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const { recipeMap } = wrapper({ ...MOCK_SHOPPING_LIST, recipeReferences: undefined });
|
||||
expect(recipeMap).toEqual(new Map([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("checked and unchecked items", () => {
|
||||
test("update appropriately", () => {
|
||||
const mockCheckedItem = { ...MOCK_ITEM, checked: true };
|
||||
const { listItems: { checked, unchecked } } = wrapper({
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
mockCheckedItem,
|
||||
],
|
||||
});
|
||||
expect(unchecked[0]).toEqual(MOCK_ITEM);
|
||||
expect(checked[0]).toEqual(mockCheckedItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -462,7 +462,7 @@
|
||||
"mealie-text": "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
|
||||
"plantoeat": {
|
||||
"title": "Plan to Eat",
|
||||
"description-long": "Mealie can import recipies from Plan to Eat."
|
||||
"description-long": "Mealie can import recipes from Plan to Eat. Upload a ZIP archive, CSV, or TXT file exported from Plan to Eat."
|
||||
},
|
||||
"myrecipebox": {
|
||||
"title": "My Recipe Box",
|
||||
@@ -1144,6 +1144,8 @@
|
||||
},
|
||||
"data-pages": {
|
||||
"foods": {
|
||||
"delete-affects-recipes": "Warning: this food is used in {count} recipe(s). Deleting it will leave an empty ingredient in the recipe(s).",
|
||||
"delete-affects-recipes-more": "View all {count} recipes",
|
||||
"merge-dialog-text": "Combining the selected foods will merge the source food and target food into a single food. The source food will be deleted and all of the references to the source food will be updated to point to the target food.",
|
||||
"merge-food-example": "Merging {food1} into {food2}",
|
||||
"seed-dialog-text": "Seed the database with foods based on your local language. This will create ~2700 common foods that can be used to organize your database. Foods are translated via a community effort.",
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface AppInfo {
|
||||
oidcRedirect: boolean;
|
||||
oidcProviderName: string;
|
||||
tokenTime: number;
|
||||
allowedIframeHosts?: string[];
|
||||
}
|
||||
export interface AppStartupInfo {
|
||||
isFirstLogin: boolean;
|
||||
|
||||
74
frontend/app/lib/sanitize/markdown.test.ts
Normal file
74
frontend/app/lib/sanitize/markdown.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { sanitizeMarkdownHtml } from "./markdown";
|
||||
|
||||
describe("sanitizeMarkdownHtml", () => {
|
||||
test("returns empty string for nullish input", () => {
|
||||
expect(sanitizeMarkdownHtml(null)).toEqual("");
|
||||
expect(sanitizeMarkdownHtml(undefined)).toEqual("");
|
||||
expect(sanitizeMarkdownHtml("")).toEqual("");
|
||||
});
|
||||
|
||||
test("keeps allowed formatting tags", () => {
|
||||
const html = sanitizeMarkdownHtml("<p>Mix <strong>flour</strong> and <em>water</em></p>");
|
||||
expect(html).toContain("<strong>flour</strong>");
|
||||
expect(html).toContain("<em>water</em>");
|
||||
});
|
||||
|
||||
test("strips script tags and event handlers", () => {
|
||||
const html = sanitizeMarkdownHtml("<p onclick=\"alert(1)\">hi</p><script>alert(1)</script>");
|
||||
expect(html).not.toContain("script");
|
||||
expect(html).not.toContain("onclick");
|
||||
expect(html).not.toContain("alert");
|
||||
});
|
||||
|
||||
test("strips img onerror payloads", () => {
|
||||
const html = sanitizeMarkdownHtml("<img src=x onerror=alert(1)>");
|
||||
expect(html).not.toContain("onerror");
|
||||
});
|
||||
|
||||
// Form controls must never render in user content.
|
||||
test("strips form, input, and button elements", () => {
|
||||
const html = sanitizeMarkdownHtml("<form action=/x><input name=p><button>go</button></form>");
|
||||
expect(html).not.toContain("<form");
|
||||
expect(html).not.toContain("<input");
|
||||
expect(html).not.toContain("<button");
|
||||
});
|
||||
|
||||
test("strips iframes when no allowed hosts are configured", () => {
|
||||
const html = sanitizeMarkdownHtml("<iframe src=\"https://evil.example/x\"></iframe>", []);
|
||||
expect(html).not.toContain("<iframe");
|
||||
});
|
||||
|
||||
test("strips iframes whose src host is not allowlisted", () => {
|
||||
const html = sanitizeMarkdownHtml(
|
||||
"<iframe src=\"https://evil.example/x\"></iframe>",
|
||||
["youtube.com"],
|
||||
);
|
||||
expect(html).not.toContain("<iframe");
|
||||
});
|
||||
|
||||
test("strips non-https iframes even for an allowlisted host", () => {
|
||||
const html = sanitizeMarkdownHtml(
|
||||
"<iframe src=\"http://www.youtube.com/embed/abc\"></iframe>",
|
||||
["youtube.com"],
|
||||
);
|
||||
expect(html).not.toContain("<iframe");
|
||||
});
|
||||
|
||||
test("keeps iframes from an allowlisted host (incl. subdomains)", () => {
|
||||
const html = sanitizeMarkdownHtml(
|
||||
"<iframe src=\"https://www.youtube.com/embed/abc\"></iframe>",
|
||||
["youtube.com"],
|
||||
);
|
||||
expect(html).toContain("<iframe");
|
||||
expect(html).toContain("https://www.youtube.com/embed/abc");
|
||||
});
|
||||
|
||||
test("does not allow a lookalike host to pass the suffix check", () => {
|
||||
const html = sanitizeMarkdownHtml(
|
||||
"<iframe src=\"https://notyoutube.com/embed/abc\"></iframe>",
|
||||
["youtube.com"],
|
||||
);
|
||||
expect(html).not.toContain("<iframe");
|
||||
});
|
||||
});
|
||||
98
frontend/app/lib/sanitize/markdown.ts
Normal file
98
frontend/app/lib/sanitize/markdown.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
enum DOMPurifyHook {
|
||||
UponSanitizeAttribute = "uponSanitizeAttribute",
|
||||
AfterSanitizeAttributes = "afterSanitizeAttributes",
|
||||
}
|
||||
|
||||
const ALLOWED_STYLE_PROPERTIES = [
|
||||
"background-color", "color", "font-style", "font-weight", "text-decoration", "text-align",
|
||||
];
|
||||
|
||||
const BASE_ALLOWED_TAGS = [
|
||||
"strong", "em", "b", "i", "u", "p", "code", "pre", "samp", "kbd", "var", "sub", "sup", "dfn", "cite",
|
||||
"small", "address", "hr", "br", "id", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"ul", "ol", "li", "dl", "dt", "dd", "abbr", "a", "img", "blockquote",
|
||||
"del", "ins", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "colgroup",
|
||||
];
|
||||
|
||||
const BASE_ALLOWED_ATTR = [
|
||||
"href", "src", "alt", "height", "width", "class", "title",
|
||||
"cite", "datetime", "name", "abbr", "target", "border", "start", "style",
|
||||
];
|
||||
|
||||
// Attributes only meaningful on an <iframe>; added to the allowlist solely when iframe embeds
|
||||
// are enabled via a configured host allowlist.
|
||||
const IFRAME_ALLOWED_ATTR = ["allow", "allowfullscreen", "frameborder", "scrolling"];
|
||||
|
||||
/**
|
||||
* Returns true if an iframe `src` points at one of the allowed hosts. Only https URLs are
|
||||
* accepted, and a configured host matches the URL's hostname exactly or as a parent domain
|
||||
* (e.g. "youtube.com" matches "www.youtube.com").
|
||||
*/
|
||||
function isAllowedIframeSrc(src: string, allowedHosts: string[]): boolean {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(src);
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
return allowedHosts.some((host) => {
|
||||
const allowed = host.toLowerCase();
|
||||
return hostname === allowed || hostname.endsWith(`.${allowed}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes pre-rendered HTML (from markdown) for display in user content such as recipe
|
||||
* instructions, notes, and descriptions.
|
||||
*
|
||||
* Only the tags in `BASE_ALLOWED_TAGS` and attributes in `BASE_ALLOWED_ATTR` survive; everything
|
||||
* else (scripts, event handlers, form controls, ...) is dropped. `style` attributes are filtered
|
||||
* down to the properties in `ALLOWED_STYLE_PROPERTIES`. `<iframe>` is only kept when
|
||||
* `allowedIframeHosts` is non-empty, and even then any iframe whose `src` is not an https URL on
|
||||
* the host allowlist is removed.
|
||||
*/
|
||||
export function sanitizeMarkdownHtml(rawHtml: string | null | undefined, allowedIframeHosts: string[] = []): string {
|
||||
if (!rawHtml) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const allowIframe = allowedIframeHosts.length > 0;
|
||||
|
||||
DOMPurify.addHook(DOMPurifyHook.UponSanitizeAttribute, (_node, data) => {
|
||||
if (data.attrName === "style") {
|
||||
const styles = data.attrValue.split(";").filter((style) => {
|
||||
const [property] = style.split(":");
|
||||
return ALLOWED_STYLE_PROPERTIES.includes(property.trim().toLowerCase());
|
||||
});
|
||||
data.attrValue = styles.join(";");
|
||||
}
|
||||
});
|
||||
|
||||
if (allowIframe) {
|
||||
DOMPurify.addHook(DOMPurifyHook.AfterSanitizeAttributes, (node) => {
|
||||
if (node.nodeName === "IFRAME" && !isAllowedIframeSrc(node.getAttribute("src") || "", allowedIframeHosts)) {
|
||||
node.parentNode?.removeChild(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sanitized = DOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: allowIframe ? [...BASE_ALLOWED_TAGS, "iframe"] : BASE_ALLOWED_TAGS,
|
||||
ALLOWED_ATTR: allowIframe ? [...BASE_ALLOWED_ATTR, ...IFRAME_ALLOWED_ATTR] : BASE_ALLOWED_ATTR,
|
||||
});
|
||||
|
||||
Object.values(DOMPurifyHook).forEach((hook) => {
|
||||
DOMPurify.removeHook(hook);
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
35
frontend/app/lib/sanitize/text.test.ts
Normal file
35
frontend/app/lib/sanitize/text.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { truncateText } from "./text";
|
||||
|
||||
describe("truncateText", () => {
|
||||
test("returns short text unchanged", () => {
|
||||
expect(truncateText("Dinner")).toEqual("Dinner");
|
||||
});
|
||||
|
||||
test("truncates long text with clamp", () => {
|
||||
expect(truncateText("a".repeat(25))).toEqual(`${"a".repeat(20)}...`);
|
||||
});
|
||||
|
||||
test("respects custom length and clamp", () => {
|
||||
expect(truncateText("abcdef", 3, "~")).toEqual("abc~");
|
||||
});
|
||||
|
||||
test("does not clamp text exactly at the length boundary", () => {
|
||||
expect(truncateText("abcde", 5)).toEqual("abcde");
|
||||
expect(truncateText("abcdef", 5)).toEqual("abcde...");
|
||||
});
|
||||
|
||||
// Markup in the input must be treated as plain text and never parsed into the live document.
|
||||
test("does not parse or execute HTML payloads", () => {
|
||||
const createElement = vi.spyOn(document, "createElement");
|
||||
const payload = "<img src=x onerror=alert(1)>";
|
||||
|
||||
const result = truncateText(payload);
|
||||
|
||||
// The payload is returned verbatim (truncated only by length), proving it is treated as text.
|
||||
expect(result).toEqual(`${payload.slice(0, 20)}...`);
|
||||
// No DOM element is constructed, so no <img> can fire its onerror handler.
|
||||
expect(createElement).not.toHaveBeenCalled();
|
||||
createElement.mockRestore();
|
||||
});
|
||||
});
|
||||
9
frontend/app/lib/sanitize/text.ts
Normal file
9
frontend/app/lib/sanitize/text.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Truncates plain text to `length` characters, appending `clamp` when truncated.
|
||||
*
|
||||
* The input is treated strictly as text and is never parsed as HTML, so markup in the input is
|
||||
* returned verbatim rather than interpreted.
|
||||
*/
|
||||
export function truncateText(text: string, length = 20, clamp = "..."): string {
|
||||
return text.length > length ? text.slice(0, length) + clamp : text;
|
||||
}
|
||||
@@ -69,11 +69,7 @@
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
|
||||
<v-alert
|
||||
v-if="foods && foods.length > 0"
|
||||
type="error"
|
||||
class="mb-0 text-body-2"
|
||||
>
|
||||
<v-alert v-if="foods && foods.length > 0" type="error" class="mb-0 text-body-2">
|
||||
{{ $t("data-pages.foods.seed-dialog-warning") }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
@@ -112,11 +108,7 @@
|
||||
:label="$t('data-pages.foods.food-label')"
|
||||
/>
|
||||
<v-card variant="outlined">
|
||||
<v-virtual-scroll
|
||||
height="400"
|
||||
item-height="25"
|
||||
:items="bulkAssignTarget"
|
||||
>
|
||||
<v-virtual-scroll height="400" item-height="25" :items="bulkAssignTarget">
|
||||
<template #default="{ item }">
|
||||
<v-list-item class="pb-2">
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
@@ -141,6 +133,7 @@
|
||||
]"
|
||||
:create-form="createForm"
|
||||
:edit-form="editForm"
|
||||
:on-delete-dialog-open="onDeleteDialogOpen"
|
||||
@create-one="handleCreate"
|
||||
@edit-one="handleEdit"
|
||||
@delete-one="foodStore.actions.deleteOne"
|
||||
@@ -151,15 +144,12 @@
|
||||
<template #icon>
|
||||
{{ $globals.icons.externalLink }}
|
||||
</template>
|
||||
{{ $t('data-pages.combine') }}
|
||||
{{ $t("data-pages.combine") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<template #[`item.label`]="{ item }">
|
||||
<MultiPurposeLabel
|
||||
v-if="item.label"
|
||||
:label="item.label"
|
||||
>
|
||||
<MultiPurposeLabel v-if="item.label" :label="item.label">
|
||||
{{ item.label.name }}
|
||||
</MultiPurposeLabel>
|
||||
</template>
|
||||
@@ -171,7 +161,7 @@
|
||||
</template>
|
||||
|
||||
<template #[`item.createdAt`]="{ item }">
|
||||
{{ item.createdAt ? $d(new Date(item.createdAt)) : '' }}
|
||||
{{ item.createdAt ? $d(new Date(item.createdAt)) : "" }}
|
||||
</template>
|
||||
|
||||
<template #table-button-bottom>
|
||||
@@ -179,18 +169,33 @@
|
||||
<template #icon>
|
||||
{{ $globals.icons.database }}
|
||||
</template>
|
||||
{{ $t('data-pages.seed') }}
|
||||
{{ $t("data-pages.seed") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<template #edit-dialog-custom-action>
|
||||
<BaseButton
|
||||
edit
|
||||
@click="aliasManagerDialog = true"
|
||||
>
|
||||
{{ $t('data-pages.manage-aliases') }}
|
||||
<BaseButton edit @click="aliasManagerDialog = true">
|
||||
{{ $t("data-pages.manage-aliases") }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<template #delete-dialog-bottom>
|
||||
<v-alert v-if="affectedRecipes.length > 0" type="warning" density="compact" class="mt-4 mb-0">
|
||||
{{ $t("data-pages.foods.delete-affects-recipes", { count: affectedRecipesTotal }) }}
|
||||
<ul class="mt-1 pl-5 mb-0">
|
||||
<li v-for="recipe in affectedRecipes.slice(0, 5)" :key="recipe.slug">
|
||||
<NuxtLink :to="recipe.url" class="text-white">{{ recipe.name }}</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<NuxtLink
|
||||
v-if="affectedRecipesTotal > 5"
|
||||
:to="affectedRecipesMoreLink"
|
||||
class="text-white d-inline-block mt-1"
|
||||
>
|
||||
{{ $t("data-pages.foods.delete-affects-recipes-more", { count: affectedRecipesTotal }) }}
|
||||
</NuxtLink>
|
||||
</v-alert>
|
||||
</template>
|
||||
</GroupDataPage>
|
||||
</div>
|
||||
</template>
|
||||
@@ -218,6 +223,7 @@ interface CreateIngredientFoodWithOnHand extends CreateIngredientFood {
|
||||
interface IngredientFoodWithOnHand extends IngredientFood {
|
||||
onHand: boolean;
|
||||
}
|
||||
|
||||
const userApi = useUserApi();
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
@@ -274,11 +280,14 @@ const tableHeaders: TableHeaders[] = [
|
||||
];
|
||||
|
||||
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
|
||||
const userGroup = computed(() => auth.user.value?.groupSlug || "");
|
||||
const foodStore = useFoodStore();
|
||||
const foods = computed(() => foodStore.store.value.map((food) => {
|
||||
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
|
||||
return { ...food, onHand } as IngredientFoodWithOnHand;
|
||||
}));
|
||||
const foods = computed(() =>
|
||||
foodStore.store.value.map((food) => {
|
||||
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
|
||||
return { ...food, onHand } as IngredientFoodWithOnHand;
|
||||
}),
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// Labels
|
||||
@@ -383,6 +392,9 @@ async function handleBulkAction(event: string, items: IngredientFoodWithOnHand[]
|
||||
if (event === "delete-selected") {
|
||||
const ids = items.map(item => item.id);
|
||||
await foodStore.actions.deleteMany(ids);
|
||||
affectedRecipes.value = [];
|
||||
affectedRecipesTotal.value = 0;
|
||||
affectedRecipesMoreLink.value = "";
|
||||
}
|
||||
else if (event === "assign-selected") {
|
||||
bulkAssignEventHandler(items);
|
||||
@@ -401,6 +413,26 @@ function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
|
||||
aliasManagerDialog.value = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Delete Foods
|
||||
|
||||
// fetch affected recipes before confirming deletion
|
||||
const affectedRecipes = ref<{ name: string; slug: string; url: string }[]>([]);
|
||||
const affectedRecipesTotal = ref(0);
|
||||
const affectedRecipesMoreLink = ref("");
|
||||
|
||||
async function onDeleteDialogOpen(items: IngredientFoodWithOnHand[]) {
|
||||
const ids = items.map(item => item.id);
|
||||
const { data } = await userApi.recipes.search({ foods: ids, perPage: 5 });
|
||||
affectedRecipes.value = (data?.items ?? []).map(r => ({
|
||||
name: r.name ?? "",
|
||||
slug: r.slug ?? "",
|
||||
url: `/g/${userGroup.value}/r/${r.slug}`,
|
||||
}));
|
||||
affectedRecipesTotal.value = data?.total ?? 0;
|
||||
affectedRecipesMoreLink.value = `/g/${userGroup.value}?${ids.map(id => `foods=${id}`).join("&")}`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Merge Foods
|
||||
|
||||
|
||||
@@ -337,16 +337,8 @@ const _content: Record<string, MigrationContent> = {
|
||||
},
|
||||
[MIGRATIONS.plantoeat]: {
|
||||
text: i18n.t("migration.plantoeat.description-long"),
|
||||
acceptedFileType: ".zip",
|
||||
tree: [
|
||||
{
|
||||
icon: $globals.icons.zip,
|
||||
title: "plantoeat-recipes-508318_10-13-2023.zip",
|
||||
children: [
|
||||
{ title: "plantoeat-recipes-508318_10-13-2023.csv", icon: $globals.icons.codeJson },
|
||||
],
|
||||
},
|
||||
],
|
||||
acceptedFileType: ".zip,.csv,.txt",
|
||||
tree: false,
|
||||
},
|
||||
[MIGRATIONS.recipekeeper]: {
|
||||
text: i18n.t("migration.recipekeeper.description-long"),
|
||||
|
||||
18
frontend/app/tests/setup.ts
Normal file
18
frontend/app/tests/setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { config } from "@vue/test-utils";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
function loadEnLocales() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../lang/messages/en-US.json") as Record<string, string>;
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: "en-US",
|
||||
messages: {
|
||||
"en-US": loadEnLocales(),
|
||||
},
|
||||
});
|
||||
|
||||
config.global.plugins = [...(config.global.plugins ?? []), i18n];
|
||||
|
||||
export { i18n };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
function loadEnLocales() {
|
||||
@@ -14,3 +15,12 @@ export function stubI18n() {
|
||||
});
|
||||
return i18n.global;
|
||||
}
|
||||
|
||||
export const makeWrapper = <T>(setup: () => T) => {
|
||||
const Wrapper = {
|
||||
template: "<div />",
|
||||
setup,
|
||||
};
|
||||
const { vm } = mount(Wrapper);
|
||||
return vm as unknown as ReturnType<typeof Wrapper.setup>;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"lint:log": "yarn lint:js --debug",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest --watch=false",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:ui": "vitest --ui",
|
||||
"cleanup": "nuxt cleanup"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -48,6 +50,9 @@
|
||||
"@types/node": "^25.5.2",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-plugin-format": "^1.0.1",
|
||||
@@ -57,8 +62,9 @@
|
||||
"prettier": "^3.5.2",
|
||||
"sass-embedded": "^1.85.1",
|
||||
"typescript": "^5.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"vite-plugin-commonjs": "^0.10.4",
|
||||
"vitest": "^3.0.7"
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"resolutions": {
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import path from "path";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
|
||||
export default {
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ["vue", "@vueuse/core", "vue-i18n"],
|
||||
dts: false,
|
||||
}),
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./app/tests/setup.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
include: ["app/{lib,components,composables,layouts,pages}/**/*.{ts,tsx,vue}"],
|
||||
exclude: ["**/*.test.*", "node_modules/**", "dist/**", "coverage/**", "**/__tests__/**"],
|
||||
reporter: ["html", "text-summary"],
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@ampproject/remapping@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
|
||||
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@antfu/install-pkg@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26"
|
||||
@@ -281,7 +289,7 @@
|
||||
"@babel/template" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
|
||||
"@babel/parser@^7.24.6", "@babel/parser@^7.25.3", "@babel/parser@^7.27.0", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5", "@babel/parser@^7.29.3", "@babel/parser@^7.29.7":
|
||||
"@babel/parser@^7.24.6", "@babel/parser@^7.25.3", "@babel/parser@^7.25.4", "@babel/parser@^7.27.0", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5", "@babel/parser@^7.29.3", "@babel/parser@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334"
|
||||
integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==
|
||||
@@ -904,7 +912,7 @@
|
||||
"@babel/types" "^7.29.7"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.26.8", "@babel/types@^7.28.4", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4":
|
||||
"@babel/types@^7.25.4", "@babel/types@^7.26.8", "@babel/types@^7.28.4", "@babel/types@^7.29.0", "@babel/types@^7.29.7", "@babel/types@^7.4.4":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
|
||||
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
|
||||
@@ -920,6 +928,11 @@
|
||||
"@babel/helper-string-parser" "^8.0.0-rc.6"
|
||||
"@babel/helper-validator-identifier" "^8.0.0-rc.6"
|
||||
|
||||
"@bcoe/v8-coverage@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz#bbe12dca5b4ef983a0d0af4b07b9bc90ea0ababa"
|
||||
integrity sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==
|
||||
|
||||
"@bomb.sh/tab@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@bomb.sh/tab/-/tab-0.0.15.tgz#678fd24b3b3ab7e9e426cf541b3ee2cf8fb661ec"
|
||||
@@ -1852,6 +1865,11 @@
|
||||
dependencies:
|
||||
minipass "^7.0.4"
|
||||
|
||||
"@istanbuljs/schema@^0.1.2":
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.6.tgz#8dc9afa2ac1506cb1a58f89940f1c124446c8df3"
|
||||
integrity sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
|
||||
version "0.3.13"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
|
||||
@@ -1886,7 +1904,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31":
|
||||
"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.31":
|
||||
version "0.3.31"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
|
||||
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
|
||||
@@ -2377,6 +2395,11 @@
|
||||
vue-i18n "^10.0.7"
|
||||
vue-router "^4.5.1"
|
||||
|
||||
"@one-ini/wasm@0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@one-ini/wasm/-/wasm-0.1.1.tgz#6013659736c9dbfccc96e8a9c2b3de317df39323"
|
||||
integrity sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==
|
||||
|
||||
"@oxc-minify/binding-android-arm-eabi@0.131.0":
|
||||
version "0.131.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.131.0.tgz#031cab3588c2b31d124699d9de3a71d4022e9ee1"
|
||||
@@ -2664,6 +2687,11 @@
|
||||
dependencies:
|
||||
"@oxc-project/types" "^0.60.0"
|
||||
|
||||
"@oxc-project/types@=0.133.0":
|
||||
version "0.133.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8"
|
||||
integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==
|
||||
|
||||
"@oxc-project/types@^0.131.0":
|
||||
version "0.131.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.131.0.tgz#be912efd221a600fdc0f7e127ef1248b9b7beb0a"
|
||||
@@ -3022,7 +3050,86 @@
|
||||
resolved "https://registry.yarnpkg.com/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz#1cfe5c557c45dd7f2988cbee278f17607010591b"
|
||||
integrity sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==
|
||||
|
||||
"@rolldown/pluginutils@^1.0.0-rc.2", "@rolldown/pluginutils@^1.0.1":
|
||||
"@rolldown/binding-android-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592"
|
||||
integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==
|
||||
|
||||
"@rolldown/binding-darwin-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc"
|
||||
integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==
|
||||
|
||||
"@rolldown/binding-darwin-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad"
|
||||
integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==
|
||||
|
||||
"@rolldown/binding-freebsd-x64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1"
|
||||
integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f"
|
||||
integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1"
|
||||
integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069"
|
||||
integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8"
|
||||
integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763"
|
||||
integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7"
|
||||
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
|
||||
|
||||
"@rolldown/binding-linux-x64-musl@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3"
|
||||
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
|
||||
|
||||
"@rolldown/binding-openharmony-arm64@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309"
|
||||
integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==
|
||||
|
||||
"@rolldown/binding-wasm32-wasi@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b"
|
||||
integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==
|
||||
dependencies:
|
||||
"@emnapi/core" "1.10.0"
|
||||
"@emnapi/runtime" "1.10.0"
|
||||
"@napi-rs/wasm-runtime" "^1.1.4"
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad"
|
||||
integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb"
|
||||
integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==
|
||||
|
||||
"@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.0-rc.2", "@rolldown/pluginutils@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
|
||||
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
|
||||
@@ -3277,6 +3384,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz#03ecc29279e3c0c832f6185a5bfa3497858ac8ca"
|
||||
integrity sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==
|
||||
|
||||
"@standard-schema/spec@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8"
|
||||
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
|
||||
|
||||
"@stylistic/eslint-plugin@^5.4.0", "@stylistic/eslint-plugin@^5.9.0":
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz#471bbd9f7a27ceaac4a217e7f5b3890855e5640c"
|
||||
@@ -3647,57 +3759,95 @@
|
||||
dependencies:
|
||||
"@rolldown/pluginutils" "^1.0.1"
|
||||
|
||||
"@vitest/expect@3.2.4":
|
||||
"@vitest/coverage-v8@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.2.4.tgz#8362124cd811a5ee11c5768207b9df53d34f2433"
|
||||
integrity sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==
|
||||
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz#a2d8d040288c1956a1c7d0a0e2cdcfc7a3319f13"
|
||||
integrity sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==
|
||||
dependencies:
|
||||
"@types/chai" "^5.2.2"
|
||||
"@vitest/spy" "3.2.4"
|
||||
"@vitest/utils" "3.2.4"
|
||||
chai "^5.2.0"
|
||||
"@ampproject/remapping" "^2.3.0"
|
||||
"@bcoe/v8-coverage" "^1.0.2"
|
||||
ast-v8-to-istanbul "^0.3.3"
|
||||
debug "^4.4.1"
|
||||
istanbul-lib-coverage "^3.2.2"
|
||||
istanbul-lib-report "^3.0.1"
|
||||
istanbul-lib-source-maps "^5.0.6"
|
||||
istanbul-reports "^3.1.7"
|
||||
magic-string "^0.30.17"
|
||||
magicast "^0.3.5"
|
||||
std-env "^3.9.0"
|
||||
test-exclude "^7.0.1"
|
||||
tinyrainbow "^2.0.0"
|
||||
|
||||
"@vitest/mocker@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.2.4.tgz#4471c4efbd62db0d4fa203e65cc6b058a85cabd3"
|
||||
integrity sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==
|
||||
"@vitest/expect@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.8.tgz#45154f1f8559f55c5281eb0dcb1ac37b581a87d8"
|
||||
integrity sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==
|
||||
dependencies:
|
||||
"@vitest/spy" "3.2.4"
|
||||
estree-walker "^3.0.3"
|
||||
magic-string "^0.30.17"
|
||||
"@standard-schema/spec" "^1.1.0"
|
||||
"@types/chai" "^5.2.2"
|
||||
"@vitest/spy" "4.1.8"
|
||||
"@vitest/utils" "4.1.8"
|
||||
chai "^6.2.2"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vitest/pretty-format@3.2.4", "@vitest/pretty-format@^3.2.4":
|
||||
"@vitest/mocker@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.8.tgz#d006bfc5894a1af51e74deddef2535d6bd436b16"
|
||||
integrity sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==
|
||||
dependencies:
|
||||
"@vitest/spy" "4.1.8"
|
||||
estree-walker "^3.0.3"
|
||||
magic-string "^0.30.21"
|
||||
|
||||
"@vitest/pretty-format@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz#3c102f79e82b204a26c7a5921bf47d534919d3b4"
|
||||
integrity sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==
|
||||
dependencies:
|
||||
tinyrainbow "^2.0.0"
|
||||
|
||||
"@vitest/runner@3.2.4":
|
||||
"@vitest/pretty-format@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.8.tgz#d9d2e248b900d7ad9556c4374fcdf1871c615193"
|
||||
integrity sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==
|
||||
dependencies:
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vitest/runner@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.8.tgz#4631808f3996359b74ccc3ca262990e14c295d50"
|
||||
integrity sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==
|
||||
dependencies:
|
||||
"@vitest/utils" "4.1.8"
|
||||
pathe "^2.0.3"
|
||||
|
||||
"@vitest/snapshot@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.8.tgz#37470135d64ea11bb2a839b1c6b7f5de7018f6ee"
|
||||
integrity sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "4.1.8"
|
||||
"@vitest/utils" "4.1.8"
|
||||
magic-string "^0.30.21"
|
||||
pathe "^2.0.3"
|
||||
|
||||
"@vitest/spy@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.8.tgz#3abfe9301d25c39f808dcaa9f10fec0dd370e564"
|
||||
integrity sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==
|
||||
|
||||
"@vitest/ui@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.2.4.tgz#5ce0274f24a971f6500f6fc166d53d8382430766"
|
||||
integrity sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==
|
||||
resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-3.2.4.tgz#df8080537c1dcfeae353b2d3cb3301d9acafe04a"
|
||||
integrity sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==
|
||||
dependencies:
|
||||
"@vitest/utils" "3.2.4"
|
||||
fflate "^0.8.2"
|
||||
flatted "^3.3.3"
|
||||
pathe "^2.0.3"
|
||||
strip-literal "^3.0.0"
|
||||
|
||||
"@vitest/snapshot@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.2.4.tgz#40a8bc0346ac0aee923c0eefc2dc005d90bc987c"
|
||||
integrity sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "3.2.4"
|
||||
magic-string "^0.30.17"
|
||||
pathe "^2.0.3"
|
||||
|
||||
"@vitest/spy@3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.2.4.tgz#cc18f26f40f3f028da6620046881f4e4518c2599"
|
||||
integrity sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==
|
||||
dependencies:
|
||||
tinyspy "^4.0.3"
|
||||
sirv "^3.0.1"
|
||||
tinyglobby "^0.2.14"
|
||||
tinyrainbow "^2.0.0"
|
||||
|
||||
"@vitest/utils@3.2.4":
|
||||
version "3.2.4"
|
||||
@@ -3708,6 +3858,15 @@
|
||||
loupe "^3.1.4"
|
||||
tinyrainbow "^2.0.0"
|
||||
|
||||
"@vitest/utils@4.1.8":
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.8.tgz#099ea5255cec08735410cf707edaba2c158c5ad9"
|
||||
integrity sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "4.1.8"
|
||||
convert-source-map "^2.0.0"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
"@vue-macros/common@^1.16.1":
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue-macros/common/-/common-1.16.1.tgz#dac7ebc57ded4d6fb19d7f9a83d2973971d9fa65"
|
||||
@@ -3877,6 +4036,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.35.tgz#192eb3d720c40715db79313454c4937432a4e86d"
|
||||
integrity sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==
|
||||
|
||||
"@vue/test-utils@^2.4.6":
|
||||
version "2.4.10"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.4.10.tgz#f3b006e03918e66b5df1f2a6f7f5200663b525d3"
|
||||
integrity sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==
|
||||
dependencies:
|
||||
js-beautify "^1.14.9"
|
||||
vue-component-type-helpers "^3.0.0"
|
||||
|
||||
"@vuetify/loader-shared@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@vuetify/loader-shared/-/loader-shared-2.1.2.tgz#faf27cb8c40ba5a45b930b9a2785e35525e9c96f"
|
||||
@@ -3911,6 +4078,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.3.0.tgz#a3e7e6391f9ed7f363cbb28c32c4a278efaacbd0"
|
||||
integrity sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==
|
||||
|
||||
abbrev@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf"
|
||||
integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==
|
||||
|
||||
abbrev@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.1.tgz#8ac8b3b5024d31464fe2a5feeea9f4536bf44025"
|
||||
@@ -4095,6 +4267,15 @@ ast-kit@^2.1.2, ast-kit@^2.1.3:
|
||||
"@babel/parser" "^7.28.5"
|
||||
pathe "^2.0.3"
|
||||
|
||||
ast-v8-to-istanbul@^0.3.3:
|
||||
version "0.3.12"
|
||||
resolved "https://registry.yarnpkg.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz#8eb1b7c86ef8499859be761b17ffd91406c0c36f"
|
||||
integrity sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "^0.3.31"
|
||||
estree-walker "^3.0.3"
|
||||
js-tokens "^10.0.0"
|
||||
|
||||
ast-walker-scope@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz#b827e8949c129802f76fe0f142e95fd7efda57dc"
|
||||
@@ -4453,16 +4634,10 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001782, caniuse-lite@^1.0.30001787:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz#238887ddf5fcfc8c36d872394d0a78a517312a72"
|
||||
integrity sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==
|
||||
|
||||
chai@^5.2.0:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-5.3.3.tgz#dd3da955e270916a4bd3f625f4b919996ada7e06"
|
||||
integrity sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==
|
||||
dependencies:
|
||||
assertion-error "^2.0.1"
|
||||
check-error "^2.1.1"
|
||||
deep-eql "^5.0.1"
|
||||
loupe "^3.1.0"
|
||||
pathval "^2.0.0"
|
||||
chai@^6.2.2:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
|
||||
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
|
||||
|
||||
chalk@^4.0.0:
|
||||
version "4.1.2"
|
||||
@@ -4482,11 +4657,6 @@ change-case@^5.4.4:
|
||||
resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02"
|
||||
integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==
|
||||
|
||||
check-error@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.3.tgz#2427361117b70cca8dc89680ead32b157019caf5"
|
||||
integrity sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==
|
||||
|
||||
chokidar@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||
@@ -4608,6 +4778,11 @@ combined-stream@^1.0.8:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^10.0.0:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
|
||||
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
|
||||
|
||||
commander@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
|
||||
@@ -4674,6 +4849,14 @@ confbox@^0.2.4:
|
||||
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.4.tgz#592e7be71f882a4a874e3c88f0ac1ef6f7da1ce5"
|
||||
integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==
|
||||
|
||||
config-chain@^1.1.13:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
|
||||
integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==
|
||||
dependencies:
|
||||
ini "^1.3.4"
|
||||
proto-list "~1.2.1"
|
||||
|
||||
consola@^3.2.3, consola@^3.4.2:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7"
|
||||
@@ -4926,11 +5109,6 @@ decimal.js@^10.6.0:
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
|
||||
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
|
||||
|
||||
deep-eql@^5.0.1:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341"
|
||||
integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
@@ -5100,6 +5278,16 @@ easy-bem@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/easy-bem/-/easy-bem-1.1.1.tgz#1bfcc10425498090bcfddc0f9c000aba91399e03"
|
||||
integrity sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==
|
||||
|
||||
editorconfig@^1.0.4:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-1.0.7.tgz#8d6e178aeb507c206d65e1804c1d7510d110d434"
|
||||
integrity sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==
|
||||
dependencies:
|
||||
"@one-ini/wasm" "0.1.1"
|
||||
commander "^10.0.0"
|
||||
minimatch "^9.0.1"
|
||||
semver "^7.5.3"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
@@ -5237,7 +5425,7 @@ es-errors@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-module-lexer@^1.5.4, es-module-lexer@^1.7.0:
|
||||
es-module-lexer@^1.5.4:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
|
||||
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
|
||||
@@ -5796,7 +5984,7 @@ execa@^8.0.1:
|
||||
signal-exit "^4.1.0"
|
||||
strip-final-newline "^3.0.0"
|
||||
|
||||
expect-type@^1.2.1:
|
||||
expect-type@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.3.0.tgz#0d58ed361877a31bbc4dd6cf71bbfef7faf6bd68"
|
||||
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
|
||||
@@ -5883,6 +6071,11 @@ fdir@^6.2.0, fdir@^6.5.0:
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
|
||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||
|
||||
fflate@^0.8.2:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc"
|
||||
integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==
|
||||
|
||||
file-entry-cache@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
|
||||
@@ -5938,7 +6131,7 @@ flat-cache@^4.0.0:
|
||||
flatted "^3.2.9"
|
||||
keyv "^4.5.4"
|
||||
|
||||
flatted@^3.2.9:
|
||||
flatted@^3.2.9, flatted@^3.3.3:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
|
||||
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
|
||||
@@ -6154,7 +6347,7 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@^10.0.0:
|
||||
glob@^10.0.0, glob@^10.4.1, glob@^10.4.2:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
|
||||
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
|
||||
@@ -6333,6 +6526,11 @@ html-entities@^2.6.0:
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.6.0.tgz#7c64f1ea3b36818ccae3d3fb48b6974208e984f8"
|
||||
integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==
|
||||
|
||||
html-escaper@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
||||
|
||||
http-errors@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||
@@ -6461,6 +6659,11 @@ ini@4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1"
|
||||
integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==
|
||||
|
||||
ini@^1.3.4:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
internal-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
||||
@@ -6804,6 +7007,37 @@ isomorphic-dompurify@^3.4.0:
|
||||
dompurify "^3.4.5"
|
||||
jsdom "^29.1.1"
|
||||
|
||||
istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756"
|
||||
integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==
|
||||
|
||||
istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d"
|
||||
integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==
|
||||
dependencies:
|
||||
istanbul-lib-coverage "^3.0.0"
|
||||
make-dir "^4.0.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
istanbul-lib-source-maps@^5.0.6:
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441"
|
||||
integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "^0.3.23"
|
||||
debug "^4.1.1"
|
||||
istanbul-lib-coverage "^3.0.0"
|
||||
|
||||
istanbul-reports@^3.1.7:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93"
|
||||
integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==
|
||||
dependencies:
|
||||
html-escaper "^2.0.0"
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
jackspeak@^3.1.2:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
||||
@@ -6839,6 +7073,27 @@ jmespath@^0.16.0:
|
||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
|
||||
integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
|
||||
|
||||
js-beautify@^1.14.9:
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.15.4.tgz#f579f977ed4c930cef73af8f98f3f0a608acd51e"
|
||||
integrity sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==
|
||||
dependencies:
|
||||
config-chain "^1.1.13"
|
||||
editorconfig "^1.0.4"
|
||||
glob "^10.4.2"
|
||||
js-cookie "^3.0.5"
|
||||
nopt "^7.2.1"
|
||||
|
||||
js-cookie@^3.0.5:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.8.tgz#444e6f4b27a5d844594fef61c9d6bca5f0787688"
|
||||
integrity sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==
|
||||
|
||||
js-tokens@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-10.0.0.tgz#dffe7599b4a8bb7fe30aff8d0235234dffb79831"
|
||||
integrity sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@@ -7032,6 +7287,80 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
|
||||
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
|
||||
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
|
||||
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
|
||||
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
|
||||
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
|
||||
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
|
||||
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
|
||||
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
|
||||
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
|
||||
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
|
||||
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
|
||||
|
||||
lightningcss@^1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
|
||||
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
|
||||
dependencies:
|
||||
detect-libc "^2.0.3"
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64 "1.32.0"
|
||||
lightningcss-darwin-arm64 "1.32.0"
|
||||
lightningcss-darwin-x64 "1.32.0"
|
||||
lightningcss-freebsd-x64 "1.32.0"
|
||||
lightningcss-linux-arm-gnueabihf "1.32.0"
|
||||
lightningcss-linux-arm64-gnu "1.32.0"
|
||||
lightningcss-linux-arm64-musl "1.32.0"
|
||||
lightningcss-linux-x64-gnu "1.32.0"
|
||||
lightningcss-linux-x64-musl "1.32.0"
|
||||
lightningcss-win32-arm64-msvc "1.32.0"
|
||||
lightningcss-win32-x64-msvc "1.32.0"
|
||||
|
||||
lilconfig@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
|
||||
@@ -7168,7 +7497,7 @@ log-update@^6.1.0:
|
||||
strip-ansi "^7.1.0"
|
||||
wrap-ansi "^9.0.0"
|
||||
|
||||
loupe@^3.1.0, loupe@^3.1.4:
|
||||
loupe@^3.1.4:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76"
|
||||
integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==
|
||||
@@ -7234,6 +7563,15 @@ magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.17, magic-strin
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.5"
|
||||
|
||||
magicast@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739"
|
||||
integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.25.4"
|
||||
"@babel/types" "^7.25.4"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
magicast@^0.5.2:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.5.3.tgz#1800f6e76dd8b0dbe7257438a2c336aefabbd905"
|
||||
@@ -7243,6 +7581,13 @@ magicast@^0.5.2:
|
||||
"@babel/types" "^7.29.0"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
make-dir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
|
||||
integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==
|
||||
dependencies:
|
||||
semver "^7.5.3"
|
||||
|
||||
marked@^15.0.12:
|
||||
version "15.0.12"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-15.0.12.tgz#30722c7346e12d0a2d0207ab9b0c4f0102d86c4e"
|
||||
@@ -7346,7 +7691,7 @@ minimatch@^5.0.1, minimatch@^5.1.0:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
minimatch@^9.0.1, minimatch@^9.0.4:
|
||||
version "9.0.9"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
|
||||
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
|
||||
@@ -7538,6 +7883,13 @@ node-releases@^2.0.36:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.46.tgz#d188a129a83f5e03a101aacb58f260f2ee8faaa1"
|
||||
integrity sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==
|
||||
|
||||
nopt@^7.2.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.1.tgz#1cac0eab9b8e97c9093338446eddd40b2c8ca1e7"
|
||||
integrity sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==
|
||||
dependencies:
|
||||
abbrev "^2.0.0"
|
||||
|
||||
nopt@^8.0.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-8.1.0.tgz#b11d38caf0f8643ce885818518064127f602eae3"
|
||||
@@ -8018,11 +8370,6 @@ pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716"
|
||||
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
|
||||
|
||||
pathval@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d"
|
||||
integrity sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==
|
||||
|
||||
perfect-debounce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||
@@ -8368,6 +8715,11 @@ proper-lockfile@^4.1.2:
|
||||
retry "^0.12.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
proto-list@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==
|
||||
|
||||
proxy-from-env@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba"
|
||||
@@ -8604,6 +8956,30 @@ rfdc@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
|
||||
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
|
||||
|
||||
rolldown@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
|
||||
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
|
||||
dependencies:
|
||||
"@oxc-project/types" "=0.133.0"
|
||||
"@rolldown/pluginutils" "^1.0.0"
|
||||
optionalDependencies:
|
||||
"@rolldown/binding-android-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-arm64" "1.0.3"
|
||||
"@rolldown/binding-darwin-x64" "1.0.3"
|
||||
"@rolldown/binding-freebsd-x64" "1.0.3"
|
||||
"@rolldown/binding-linux-arm-gnueabihf" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-arm64-musl" "1.0.3"
|
||||
"@rolldown/binding-linux-ppc64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-s390x-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-gnu" "1.0.3"
|
||||
"@rolldown/binding-linux-x64-musl" "1.0.3"
|
||||
"@rolldown/binding-openharmony-arm64" "1.0.3"
|
||||
"@rolldown/binding-wasm32-wasi" "1.0.3"
|
||||
"@rolldown/binding-win32-arm64-msvc" "1.0.3"
|
||||
"@rolldown/binding-win32-x64-msvc" "1.0.3"
|
||||
|
||||
rollup-plugin-visualizer@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz#291c10ff4a956d9b2483f8b4147b2bf0aacd3a6e"
|
||||
@@ -9086,7 +9462,7 @@ smob@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/smob/-/smob-1.6.2.tgz#190b94c25530c631a7ccc63de0d4c0087222d21d"
|
||||
integrity sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1:
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
@@ -9169,7 +9545,7 @@ std-env@^3.9.0:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b"
|
||||
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
|
||||
|
||||
std-env@^4.0.0, std-env@^4.1.0:
|
||||
std-env@^4.0.0, std-env@^4.0.0-rc.1, std-env@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-4.1.0.tgz#45899abc590d86d682e87f0acd1033a75084cd3f"
|
||||
integrity sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==
|
||||
@@ -9347,7 +9723,7 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strip-literal@^3.0.0, strip-literal@^3.1.0:
|
||||
strip-literal@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-3.1.0.tgz#222b243dd2d49c0bcd0de8906adbd84177196032"
|
||||
integrity sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==
|
||||
@@ -9513,6 +9889,15 @@ terser@^5.17.4:
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
test-exclude@^7.0.1:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.2.tgz#482392077630bc57d5630c13abe908bb910dfc65"
|
||||
integrity sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==
|
||||
dependencies:
|
||||
"@istanbuljs/schema" "^0.1.2"
|
||||
glob "^10.4.1"
|
||||
minimatch "^10.2.2"
|
||||
|
||||
text-decoder@^1.1.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.7.tgz#5d073a9a74b9c0a9d28dfadcab96b604af57d8ba"
|
||||
@@ -9540,16 +9925,16 @@ tinyclip@^0.1.12:
|
||||
resolved "https://registry.yarnpkg.com/tinyclip/-/tinyclip-0.1.13.tgz#aafa818c5378f65fd375b9d3981eadd6c784b919"
|
||||
integrity sha512-8OqlXQ35euK9+e7L68u8UwcODxkHoIkjbGsgXuARKNyQ5G6xt8nw1YPeMbxMLgCPFkToU+UEK5j05t2t8edKpQ==
|
||||
|
||||
tinyexec@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
|
||||
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
|
||||
|
||||
tinyexec@^1.0.1, tinyexec@^1.1.1, tinyexec@^1.1.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.2.tgz#b66edf362dcad61174bc9ce4f3ee279e2448a721"
|
||||
integrity sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==
|
||||
|
||||
tinyexec@^1.0.2:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.2.4.tgz#ae45bb2edebda94c70f4ea897e0f1243e470db71"
|
||||
integrity sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==
|
||||
|
||||
tinyglobby@^0.2.10, tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15, tinyglobby@^0.2.16:
|
||||
version "0.2.16"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
|
||||
@@ -9558,25 +9943,28 @@ tinyglobby@^0.2.10, tinyglobby@^0.2.13, tinyglobby@^0.2.14, tinyglobby@^0.2.15,
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.4"
|
||||
|
||||
tinyglobby@^0.2.17:
|
||||
version "0.2.17"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
|
||||
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.4"
|
||||
|
||||
tinypool@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-2.1.0.tgz#303a671d6ef68d03c9512cdc9a47c86b8a85f20c"
|
||||
integrity sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==
|
||||
|
||||
tinypool@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591"
|
||||
integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==
|
||||
|
||||
tinyrainbow@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294"
|
||||
integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==
|
||||
|
||||
tinyspy@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78"
|
||||
integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==
|
||||
tinyrainbow@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421"
|
||||
integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==
|
||||
|
||||
tldts-core@^7.4.0:
|
||||
version "7.4.0"
|
||||
@@ -9921,6 +10309,18 @@ universalify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
||||
unplugin-auto-import@^21.0.0:
|
||||
version "21.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unplugin-auto-import/-/unplugin-auto-import-21.0.0.tgz#d1ac3bf95f80fb4182ec8f3d65d3e3aad38a63ac"
|
||||
integrity sha512-vWuC8SwqJmxZFYwPojhOhOXDb5xFhNNcEVb9K/RFkyk/3VnfaOjzitWN7v+8DEKpMjSsY2AEGXNgt6I0yQrhRQ==
|
||||
dependencies:
|
||||
local-pkg "^1.1.2"
|
||||
magic-string "^0.30.21"
|
||||
picomatch "^4.0.3"
|
||||
unimport "^5.6.0"
|
||||
unplugin "^2.3.11"
|
||||
unplugin-utils "^0.3.1"
|
||||
|
||||
unplugin-utils@^0.2.3:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz#d2fe44566ffffd7f216579bbb01184f6702e379b"
|
||||
@@ -10161,17 +10561,6 @@ vite-hot-client@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz#284fa1afa63cc8781d079515884eb49d2890ac2f"
|
||||
integrity sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==
|
||||
|
||||
vite-node@3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"
|
||||
integrity sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==
|
||||
dependencies:
|
||||
cac "^6.7.14"
|
||||
debug "^4.4.1"
|
||||
es-module-lexer "^1.7.0"
|
||||
pathe "^2.0.3"
|
||||
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
|
||||
vite-node@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-5.3.0.tgz#fb9f73c2d5bd1f95b3beaad6455e04ad899c6336"
|
||||
@@ -10263,7 +10652,20 @@ vite-plugin-vuetify@^2.1.3:
|
||||
debug "^4.3.3"
|
||||
upath "^2.0.1"
|
||||
|
||||
"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.3.1, vite@^7.3.3:
|
||||
"vite@^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.0.16"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
|
||||
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
|
||||
dependencies:
|
||||
lightningcss "^1.32.0"
|
||||
picomatch "^4.0.4"
|
||||
postcss "^8.5.15"
|
||||
rolldown "1.0.3"
|
||||
tinyglobby "^0.2.17"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vite@^7.3.1, vite@^7.3.3:
|
||||
version "7.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.3.tgz#d7e07a52b5873fb86f902a3f4b3d17410337450f"
|
||||
integrity sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==
|
||||
@@ -10277,33 +10679,30 @@ vite-plugin-vuetify@^2.1.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitest@^3.0.7:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.2.4.tgz#0637b903ad79d1539a25bc34c0ed54b5c67702ea"
|
||||
integrity sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==
|
||||
vitest@^4.0.0:
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.8.tgz#9fed17277bf7350497e54338898a7afd46dfd509"
|
||||
integrity sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==
|
||||
dependencies:
|
||||
"@types/chai" "^5.2.2"
|
||||
"@vitest/expect" "3.2.4"
|
||||
"@vitest/mocker" "3.2.4"
|
||||
"@vitest/pretty-format" "^3.2.4"
|
||||
"@vitest/runner" "3.2.4"
|
||||
"@vitest/snapshot" "3.2.4"
|
||||
"@vitest/spy" "3.2.4"
|
||||
"@vitest/utils" "3.2.4"
|
||||
chai "^5.2.0"
|
||||
debug "^4.4.1"
|
||||
expect-type "^1.2.1"
|
||||
magic-string "^0.30.17"
|
||||
"@vitest/expect" "4.1.8"
|
||||
"@vitest/mocker" "4.1.8"
|
||||
"@vitest/pretty-format" "4.1.8"
|
||||
"@vitest/runner" "4.1.8"
|
||||
"@vitest/snapshot" "4.1.8"
|
||||
"@vitest/spy" "4.1.8"
|
||||
"@vitest/utils" "4.1.8"
|
||||
es-module-lexer "^2.0.0"
|
||||
expect-type "^1.3.0"
|
||||
magic-string "^0.30.21"
|
||||
obug "^2.1.1"
|
||||
pathe "^2.0.3"
|
||||
picomatch "^4.0.2"
|
||||
std-env "^3.9.0"
|
||||
picomatch "^4.0.3"
|
||||
std-env "^4.0.0-rc.1"
|
||||
tinybench "^2.9.0"
|
||||
tinyexec "^0.3.2"
|
||||
tinyglobby "^0.2.14"
|
||||
tinypool "^1.1.1"
|
||||
tinyrainbow "^2.0.0"
|
||||
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
vite-node "3.2.4"
|
||||
tinyexec "^1.0.2"
|
||||
tinyglobby "^0.2.15"
|
||||
tinyrainbow "^3.1.0"
|
||||
vite "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
why-is-node-running "^2.3.0"
|
||||
|
||||
vscode-uri@^3.1.0:
|
||||
@@ -10327,6 +10726,11 @@ vue-bundle-renderer@^2.2.0:
|
||||
dependencies:
|
||||
ufo "^1.6.1"
|
||||
|
||||
vue-component-type-helpers@^3.0.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.3.tgz#9f31a7610348de6fa151c0166600f7ed5808beb3"
|
||||
integrity sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==
|
||||
|
||||
vue-demi@^0.14.10:
|
||||
version "0.14.10"
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
|
||||
|
||||
@@ -179,7 +179,7 @@ def validate_file_token(token: str | None = None) -> Path:
|
||||
@contextmanager
|
||||
def get_temporary_zip_path(auto_unlink=True) -> Generator[Path, None, None]:
|
||||
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
|
||||
temp_path = app_dirs.TEMP_DIR.joinpath("my_zip_archive.zip")
|
||||
temp_path = app_dirs.TEMP_DIR / f"{uuid4().hex}.zip"
|
||||
try:
|
||||
yield temp_path
|
||||
finally:
|
||||
|
||||
@@ -33,6 +33,16 @@ class FeatureDetails(NamedTuple):
|
||||
return s
|
||||
|
||||
|
||||
DEFAULT_ALLOWED_IFRAME_HOSTS = [
|
||||
"youtube.com",
|
||||
"youtube-nocookie.com",
|
||||
"vimeo.com",
|
||||
"player.vimeo.com",
|
||||
]
|
||||
"""Secure-by-default hostnames permitted as `<iframe>` sources in user content. Limited to
|
||||
well-known video providers. Subdomains of these hosts are also allowed (e.g. `www.youtube.com`)."""
|
||||
|
||||
|
||||
MaskedNoneString = Annotated[
|
||||
str | None,
|
||||
PlainSerializer(lambda x: None if x is None else "*****", return_type=str | None),
|
||||
@@ -150,6 +160,19 @@ class AppSettings(AppLoggingSettings):
|
||||
ALLOW_SIGNUP: bool = False
|
||||
ALLOW_PASSWORD_LOGIN: bool = True
|
||||
|
||||
ALLOWED_IFRAME_HOSTS: str = ""
|
||||
"""Comma-separated list of additional hostnames allowed as `<iframe>` sources in user content
|
||||
(recipe instructions, notes, descriptions). Extends `DEFAULT_ALLOWED_IFRAME_HOSTS`. Subdomains of
|
||||
a listed host are also allowed. Adding hosts is opt-in to riskier behavior; the defaults are
|
||||
limited to well-known video providers."""
|
||||
|
||||
@property
|
||||
def allowed_iframe_hosts(self) -> list[str]:
|
||||
"""The full set of hostnames permitted as `<iframe>` sources, secure defaults plus any
|
||||
admin-configured additions via `ALLOWED_IFRAME_HOSTS`."""
|
||||
extra = [host.strip().lower() for host in self.ALLOWED_IFRAME_HOSTS.split(",") if host.strip()]
|
||||
return list(dict.fromkeys(DEFAULT_ALLOWED_IFRAME_HOSTS + extra))
|
||||
|
||||
DAILY_SCHEDULE_TIME: str = "23:45"
|
||||
"""Local server time, in HH:MM format. See `DAILY_SCHEDULE_TIME_UTC` for the parsed UTC equivalent"""
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ def get_app_info(session: Session = Depends(generate_session)):
|
||||
oidc_provider_name=settings.OIDC_PROVIDER_NAME,
|
||||
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
|
||||
token_time=settings.TOKEN_TIME,
|
||||
allowed_iframe_hosts=settings.allowed_iframe_hosts,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ async def get_recipe_asset(recipe_id: UUID4, file_name: str):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if file.exists():
|
||||
return FileResponse(file, filename=file.name, content_disposition_type="attachment")
|
||||
# Force download and disable MIME sniffing so uploaded assets cannot be
|
||||
# served as active content in Mealie's origin.
|
||||
return FileResponse(
|
||||
file,
|
||||
filename=file.name,
|
||||
content_disposition_type="attachment",
|
||||
headers={"X-Content-Type-Options": "nosniff"},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -22,6 +22,7 @@ class AppInfo(MealieModel):
|
||||
oidc_redirect: bool
|
||||
oidc_provider_name: str
|
||||
token_time: int
|
||||
allowed_iframe_hosts: list[str] = []
|
||||
|
||||
|
||||
class AppTheme(MealieModel):
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.settings.static import APP_VERSION
|
||||
from mealie.services._base_service import BaseService
|
||||
from mealie.services.backups_v2.alchemy_exporter import AlchemyExporter
|
||||
from mealie.services.backups_v2.backup_file import BackupFile
|
||||
@@ -43,8 +44,15 @@ class BackupV2(BaseService):
|
||||
def backup(self) -> Path:
|
||||
# sourcery skip: merge-nested-ifs, reintroduce-else, remove-redundant-continue
|
||||
timestamp = datetime.datetime.now(datetime.UTC).strftime("%Y.%m.%d.%H.%M.%S")
|
||||
short_hash = self.settings.GIT_COMMIT_HASH[:7]
|
||||
|
||||
if APP_VERSION == "develop":
|
||||
backup_name = f"mealie_dev-{short_hash}_{timestamp}.zip"
|
||||
elif APP_VERSION == "nightly":
|
||||
backup_name = f"mealie_nightly-{short_hash}_{timestamp}.zip"
|
||||
else:
|
||||
backup_name = f"mealie_{APP_VERSION}_{timestamp}.zip"
|
||||
|
||||
backup_name = f"mealie_{timestamp}.zip"
|
||||
backup_file = self.directories.BACKUP_DIR / backup_name
|
||||
|
||||
database_json = self.db_exporter.dump()
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from slugify import slugify
|
||||
|
||||
from mealie.pkgs.cache import cache_key
|
||||
from mealie.schema.reports.reports import ReportEntryCreate
|
||||
from mealie.services.scraper import cleaner
|
||||
|
||||
from ._migration_base import BaseMigrator
|
||||
@@ -15,15 +16,23 @@ from .utils.migration_helpers import scrape_image, split_by_comma
|
||||
|
||||
|
||||
def plantoeat_recipes(file: Path):
|
||||
"""Yields all recipes inside the export file as dict"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(file) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
"""Yields all recipes inside the export file as dict.
|
||||
|
||||
for name in Path(tmpdir).glob("**/[!.]*.csv"):
|
||||
with open(name, newline="") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
Accepts a ZIP archive containing a CSV, or a raw CSV/TXT file.
|
||||
"""
|
||||
if zipfile.is_zipfile(file):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with zipfile.ZipFile(file) as zip_file:
|
||||
zip_file.extractall(tmpdir)
|
||||
|
||||
for name in Path(tmpdir).glob("**/[!.]*.csv"):
|
||||
with open(name, newline="") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
else:
|
||||
with open(file, newline="", encoding="utf-8", errors="ignore") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
yield from reader
|
||||
|
||||
|
||||
def get_value_as_string_or_none(dictionary: dict, key: str):
|
||||
@@ -112,7 +121,32 @@ class PlanToEatMigrator(BaseMigrator):
|
||||
|
||||
return recipe_dict
|
||||
|
||||
def _validate_archive(self) -> bool:
|
||||
"""Returns False and appends a failure report entry if the file is not a ZIP, CSV, or TXT."""
|
||||
if zipfile.is_zipfile(self.archive):
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(self.archive, encoding="utf-8", errors="strict") as f:
|
||||
f.read(512)
|
||||
return True
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
self.report_entries.append(
|
||||
ReportEntryCreate(
|
||||
report_id=self.report_id,
|
||||
success=False,
|
||||
message="Unsupported file format. Please upload a ZIP archive, CSV file, or TXT file.",
|
||||
exception="",
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
def _migrate(self) -> None:
|
||||
if not self._validate_archive():
|
||||
return
|
||||
|
||||
recipe_image_urls = {}
|
||||
|
||||
recipes = []
|
||||
|
||||
@@ -12,8 +12,6 @@ dependencies = [
|
||||
"SQLAlchemy==2.0.50",
|
||||
"aiofiles==25.1.0",
|
||||
"alembic==1.18.4",
|
||||
"aniso8601==10.0.1",
|
||||
"appdirs==1.4.4",
|
||||
"apprise==1.10.0",
|
||||
"bcrypt==5.0.0",
|
||||
"extruct==0.18.0",
|
||||
@@ -71,9 +69,9 @@ dev = [
|
||||
"pre-commit==4.6.0",
|
||||
"pylint==4.0.5",
|
||||
"pytest==9.0.3",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"pytest-asyncio==1.4.0",
|
||||
"rich==15.0.0",
|
||||
"ruff==0.15.14",
|
||||
"ruff==0.15.15",
|
||||
"types-PyYAML==6.0.12.20260518",
|
||||
"types-python-dateutil==2.9.0.20260518",
|
||||
"types-python-slugify==8.0.2.20240310",
|
||||
@@ -178,3 +176,7 @@ max-complexity = 24 # Default is 10.
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "exact"
|
||||
# Cooling period: ignore package releases newer than 5 days to mitigate
|
||||
# supply-chain attacks (compromised releases are usually caught and yanked
|
||||
# within days). Evaluated at resolve time as a rolling window.
|
||||
exclude-newer = "5 days"
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"minimumReleaseAge": "5 days",
|
||||
"internalChecksFilter": "strict",
|
||||
"addLabels": [
|
||||
"dependencies"
|
||||
],
|
||||
@@ -51,8 +53,7 @@
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"automergeStrategy": "squash",
|
||||
"minimumReleaseAge": "5 days"
|
||||
"automergeStrategy": "squash"
|
||||
},
|
||||
{
|
||||
"description": "Auto-merge Docker digest and patch updates",
|
||||
@@ -66,8 +67,7 @@
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeType": "pr",
|
||||
"automergeStrategy": "squash",
|
||||
"minimumReleaseAge": "5 days"
|
||||
"automergeStrategy": "squash"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ migrations_tandoor = CWD / "migrations/tandoor.zip"
|
||||
|
||||
migrations_plantoeat = CWD / "migrations/plantoeat.zip"
|
||||
|
||||
migrations_plantoeat_csv = CWD / "migrations/plantoeat.csv"
|
||||
|
||||
migrations_myrecipebox = CWD / "migrations/myrecipebox.csv"
|
||||
|
||||
migrations_recipekeeper = CWD / "migrations/recipekeeper.zip"
|
||||
|
||||
13
tests/data/migrations/plantoeat.csv
Normal file
13
tests/data/migrations/plantoeat.csv
Normal file
@@ -0,0 +1,13 @@
|
||||
Title,Course,Cuisine,Main Ingredient,Description,Source,Url,Url Host,Prep Time,Cook Time,Total Time,Servings,Yield,Ingredients,Directions,Tags,Rating,Public Url,Photo Url,Private,Nutritional Score (generic),Calories,Fat,Saturated Fat,Cholesterol,Sodium,Sugar,Carbohydrate,Fiber,Protein,Cost,Created At,Updated At
|
||||
Test Recipe,Main Course,American,Beans,"This is a description.
|
||||
Here is new line.",Manually entered source,https://eatwithclarity.com/sushi-bowl-with-sesame-tofu/,,75,75,150,7,1 loaf,", Heading
|
||||
2 itm Test, note
|
||||
, Heading2
|
||||
3 pkg Two, note2
|
||||
|
||||
","Directions.
|
||||
Will go here.","Allergen-Friendly, Cheap, Test",3,https://app.plantoeat.com/recipes/38843883,https://plantoeat.s3.amazonaws.com/recipes/29516709/470292506c8d9b71582487a7879ab7b197d06490-large.jpg?1628205591,yes,,13,16,17,18,19,22,20,21,23,,2023-10-13 20:29:29,2023-10-13 20:32:48
|
||||
Test Recipe2,,,,,,,,,,,,,"2 itm Test, note
|
||||
3 pkg Two, note2
|
||||
","Directions.
|
||||
Will go here.",,,,,,,,,,,,,,,,,2023-10-13 20:29:29,2023-10-13 20:32:48
|
||||
|
@@ -94,6 +94,15 @@ test_cases = [
|
||||
"transFatContent",
|
||||
},
|
||||
),
|
||||
MigrationTestData(
|
||||
typ=SupportedMigrations.plantoeat,
|
||||
archive=test_data.migrations_plantoeat_csv,
|
||||
search_slug="test-recipe",
|
||||
nutrition_filter={
|
||||
"unsaturatedFatContent",
|
||||
"transFatContent",
|
||||
},
|
||||
),
|
||||
MigrationTestData(
|
||||
typ=SupportedMigrations.myrecipebox,
|
||||
archive=test_data.migrations_myrecipebox,
|
||||
@@ -124,6 +133,7 @@ test_ids = [
|
||||
"mealie_alpha_archive",
|
||||
"tandoor_archive",
|
||||
"plantoeat_archive",
|
||||
"plantoeat_csv",
|
||||
"myrecipebox_csv",
|
||||
"recipekeeper_archive",
|
||||
"cookn_archive",
|
||||
@@ -190,6 +200,30 @@ def test_recipe_migration(api_client: TestClient, unique_user_fn_scoped: TestUse
|
||||
# TODO: validate other types of content
|
||||
|
||||
|
||||
def test_plantoeat_rejects_invalid_file_type(api_client: TestClient, unique_user: TestUser) -> None:
|
||||
# Simulate uploading a binary file (e.g. PDF) that is neither ZIP nor CSV/TXT
|
||||
binary_content = bytes(range(256)) * 4 # arbitrary binary data that is not valid UTF-8
|
||||
payload = {"migration_type": SupportedMigrations.plantoeat.value}
|
||||
file_payload = {"archive": binary_content}
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.groups_migrations,
|
||||
data=payload,
|
||||
files=file_payload,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
report_id = response.json()["id"]
|
||||
|
||||
response = api_client.get(api_routes.groups_reports_item_id(report_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
report = response.json()
|
||||
assert report["entries"]
|
||||
assert not report["entries"][0]["success"]
|
||||
assert "ZIP" in report["entries"][0]["message"] or "CSV" in report["entries"][0]["message"]
|
||||
|
||||
|
||||
def test_bad_mealie_alpha_data_is_ignored(api_client: TestClient, unique_user: TestUser):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
with ZipFile(test_data.migrations_mealie) as zf:
|
||||
|
||||
@@ -104,6 +104,33 @@ def test_recipe_asset_dangerous_extension_blocked(
|
||||
assert response.status_code == 400, f"expected 400 for extension={ext}, got {response.status_code}"
|
||||
|
||||
|
||||
def test_recipe_asset_served_as_attachment(
|
||||
api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe
|
||||
):
|
||||
"""Assets must be served as downloads with MIME sniffing disabled so uploaded files cannot
|
||||
execute as active content in Mealie's origin."""
|
||||
recipe = recipe_ingredient_only
|
||||
payload = {"name": random_string(10), "icon": "mdi-file", "extension": "txt"}
|
||||
file_payload = {"file": b"<script>alert(1)</script>"}
|
||||
|
||||
response = api_client.post(
|
||||
f"/api/recipes/{recipe.slug}/assets",
|
||||
data=payload,
|
||||
files=file_payload,
|
||||
headers=unique_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
recipe_response = api_client.get(f"/api/recipes/{recipe.slug}", headers=unique_user.token).json()
|
||||
recipe_id = recipe_response["id"]
|
||||
file_name = recipe_response["assets"][0]["fileName"]
|
||||
|
||||
media_response = api_client.get(f"/api/media/recipes/{recipe_id}/assets/{file_name}")
|
||||
assert media_response.status_code == 200
|
||||
assert "attachment" in media_response.headers["content-disposition"].lower()
|
||||
assert media_response.headers["x-content-type-options"] == "nosniff"
|
||||
|
||||
|
||||
def test_recipe_image_upload(api_client: TestClient, unique_user: TestUser, recipe_ingredient_only: Recipe):
|
||||
data_payload = {"extension": "jpg"}
|
||||
file_payload = {"image": data.images_test_image_1.read_bytes()}
|
||||
|
||||
@@ -27,6 +27,31 @@ def test_non_default_settings(monkeypatch):
|
||||
assert app_settings.DOCS_URL is None
|
||||
|
||||
|
||||
def test_allowed_iframe_hosts_defaults(monkeypatch):
|
||||
monkeypatch.delenv("ALLOWED_IFRAME_HOSTS", raising=False)
|
||||
get_app_settings.cache_clear()
|
||||
app_settings = get_app_settings()
|
||||
|
||||
# Secure defaults are always present and never empty (empty would disable iframe embeds).
|
||||
assert "youtube.com" in app_settings.allowed_iframe_hosts
|
||||
assert "vimeo.com" in app_settings.allowed_iframe_hosts
|
||||
|
||||
|
||||
def test_allowed_iframe_hosts_extends_defaults(monkeypatch):
|
||||
monkeypatch.setenv("ALLOWED_IFRAME_HOSTS", " Example.com , trusted.tld ,, ")
|
||||
get_app_settings.cache_clear()
|
||||
app_settings = get_app_settings()
|
||||
|
||||
hosts = app_settings.allowed_iframe_hosts
|
||||
# Configured hosts are normalized, blanks dropped, and defaults retained.
|
||||
assert "example.com" in hosts
|
||||
assert "trusted.tld" in hosts
|
||||
assert "youtube.com" in hosts
|
||||
assert "" not in hosts
|
||||
# No duplicates.
|
||||
assert len(hosts) == len(set(hosts))
|
||||
|
||||
|
||||
def test_default_connection_args(monkeypatch):
|
||||
monkeypatch.setenv("DB_ENGINE", "sqlite")
|
||||
get_app_settings.cache_clear()
|
||||
|
||||
74
uv.lock
generated
74
uv.lock
generated
@@ -2,6 +2,10 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = "==3.12.*"
|
||||
|
||||
[options]
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "25.1.0"
|
||||
@@ -25,15 +29,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aniso8601"
|
||||
version = "10.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/52179c4e3f1978d3d9a285f98c706642522750ef343e9738286130423730/aniso8601-10.0.1.tar.gz", hash = "sha256:25488f8663dd1528ae1f54f94ac1ea51ae25b4d531539b8bc707fed184d16845", size = 47190, upload-time = "2025-04-18T17:29:42.995Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/75/e0e10dc7ed1408c28e03a6cb2d7a407f99320eb953f229d008a7a6d05546/aniso8601-10.0.1-py2.py3-none-any.whl", hash = "sha256:eb19717fd4e0db6de1aab06f12450ab92144246b257423fe020af5748c0cb89e", size = 52848, upload-time = "2025-04-18T17:29:41.492Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
@@ -65,15 +60,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "appdirs"
|
||||
version = "1.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apprise"
|
||||
version = "1.10.0"
|
||||
@@ -902,8 +888,6 @@ source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "alembic" },
|
||||
{ name = "aniso8601" },
|
||||
{ name = "appdirs" },
|
||||
{ name = "apprise" },
|
||||
{ name = "authlib" },
|
||||
{ name = "bcrypt" },
|
||||
@@ -978,8 +962,6 @@ docs = [
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = "==25.1.0" },
|
||||
{ name = "alembic", specifier = "==1.18.4" },
|
||||
{ name = "aniso8601", specifier = "==10.0.1" },
|
||||
{ name = "appdirs", specifier = "==1.4.4" },
|
||||
{ name = "apprise", specifier = "==1.10.0" },
|
||||
{ name = "authlib", specifier = "==1.7.2" },
|
||||
{ name = "bcrypt", specifier = "==5.0.0" },
|
||||
@@ -1034,9 +1016,9 @@ dev = [
|
||||
{ name = "pydantic-to-typescript2", specifier = "==1.0.6" },
|
||||
{ name = "pylint", specifier = "==4.0.5" },
|
||||
{ name = "pytest", specifier = "==9.0.3" },
|
||||
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
||||
{ name = "pytest-asyncio", specifier = "==1.4.0" },
|
||||
{ name = "rich", specifier = "==15.0.0" },
|
||||
{ name = "ruff", specifier = "==0.15.14" },
|
||||
{ name = "ruff", specifier = "==0.15.15" },
|
||||
{ name = "types-python-dateutil", specifier = "==2.9.0.20260518" },
|
||||
{ name = "types-python-slugify", specifier = "==8.0.2.20240310" },
|
||||
{ name = "types-pyyaml", specifier = "==6.0.12.20260518" },
|
||||
@@ -1601,15 +1583,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1819,27 +1801,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.14"
|
||||
version = "0.15.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user